Files
MagicBotandClaude Opus 5.5 0adbfe0d99 Read client signed int32 fields as signed
Add ResStream.read_long/write_long and use them for every CObjects
field the client loads with ResStream_ReadInt32 (sb.exe 0x7E1700)
as a value: rank info, attribute/skill/resource amounts, requirements,
door/deed/container/key fields, natural attacks, hardpoints, sound
event types and sparse VAL_LONG (LoadSaveBehavior<long>). Hash/enum
fields, list counts and flag words keep unsigned reads.

The binary is unchanged; JSON now shows e.g. -1 instead of 4294967295
(rank_hirelings, rank_rank) and -10 instead of 4294967286 (attr_adj).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 12:55:04 -04:00

518 lines
24 KiB
Python

# • ▌ ▄ ·. ▄▄▄▄· ▄▄▄ .·▄▄▄▄ ▪ ▄▄▄▄▄ ▄▄▄ ▄▄▄·▄▄▄
# ·██ ▐███▪▐█ ▀█▪ ▀▄.▀·██▪ ██ ██ •██ ▪ ▀▄ █· ▐█ ▄█▀▄ █·▪
# ▐█ ▌▐▌▐█·▐█▀▀█▄ ▐▀▀▪▄▐█· ▐█▌▐█· ▐█.▪ ▄█▀▄ ▐▀▀▄ ██▀·▐▀▀▄ ▄█▀▄
# ██ ██▌▐█▌██▄▪▐█ ▐█▄▄▌██. ██ ▐█▌ ▐█▌·▐█▌.▐▌▐█•█▌ ▐█▪·•▐█•█▌▐█▌.▐▌
# ▀▀ █▪▀▀▀·▀▀▀▀ ▀▀▀ ▀▀▀▀▀• ▀▀▀ ▀▀▀ ▀█▄▀▪.▀ ▀ .▀ .▀ ▀ ▀█▄▀▪
# Magicbane Emulator Project © 2013 - 2022
# www.magicbane.com
from collections import OrderedDict
from arcane.enums.arc_item import *
from arcane.enums.hashes import hash_to_string, string_to_hash
from arcane.util import ResStream
from .ArcCombatObj import ArcCombatObj
from .common.Arcane import AttributeValue, SkillLevel, PowerGrant, PowerAction, ResourceInfo
from .common.Class import ClassRequired
from .common.Discipline import DiscRequired
from .common.Race import RaceRequired
class Damage:
def load_binary(self, stream: ResStream):
self.damage_type = stream.read_long()
self.damage_min = stream.read_long()
self.damage_max = stream.read_long()
def save_binary(self, stream: ResStream):
stream.write_long(self.damage_type)
stream.write_long(self.damage_min)
stream.write_long(self.damage_max)
def load_json(self, data):
self.damage_type = STRING_TO_DAMAGE[data['damage_type']]
self.damage_min = data['damage_min']
self.damage_max = data['damage_max']
def save_json(self):
data = OrderedDict()
data['damage_type'] = DAMAGE_TO_STRING[self.damage_type]
data['damage_min'] = self.damage_min
data['damage_max'] = self.damage_max
return data
class Offerings:
def load_binary(self, stream: ResStream):
self.offering_type = stream.read_string()
self.offering_value = stream.read_long()
def save_binary(self, stream: ResStream):
stream.write_string(self.offering_type)
stream.write_long(self.offering_value)
def load_json(self, data):
self.offering_type = data['offering_type']
self.offering_value = data['offering_value']
def save_json(self):
data = OrderedDict()
data['offering_type'] = self.offering_type
data['offering_value'] = self.offering_value
return data
class Weapon:
def load_binary(self, stream: ResStream):
self.weapon_wepspeed = stream.read_float()
self.weapon_max_range = stream.read_float()
self.weapon_projectile_id = stream.read_qword()
self.weapon_projectile_speed = stream.read_float()
self.weapon_combat_idle_anim = stream.read_long()
num_damages = stream.read_dword()
self.weapon_damage = [Damage() for _ in range(num_damages)]
for damage in self.weapon_damage:
damage.load_binary(stream)
num_right_anims = stream.read_dword()
self.weapon_attack_anim_right = [
[
stream.read_long(),
stream.read_long(),
] for _ in range(num_right_anims)
]
num_left_anims = stream.read_dword()
self.weapon_attack_anim_left = [
[
stream.read_long(),
stream.read_long(),
] for _ in range(num_left_anims)
]
def save_binary(self, stream: ResStream):
stream.write_float(self.weapon_wepspeed)
stream.write_float(self.weapon_max_range)
stream.write_qword(self.weapon_projectile_id)
stream.write_float(self.weapon_projectile_speed)
stream.write_long(self.weapon_combat_idle_anim)
stream.write_dword(len(self.weapon_damage))
for damage in self.weapon_damage:
damage.save_binary(stream)
stream.write_dword(len(self.weapon_attack_anim_right))
for anim in self.weapon_attack_anim_right:
stream.write_long(anim[0])
stream.write_long(anim[1])
stream.write_dword(len(self.weapon_attack_anim_left))
for anim in self.weapon_attack_anim_left:
stream.write_long(anim[0])
stream.write_long(anim[1])
def load_json(self, data):
self.weapon_wepspeed = data['weapon_wepspeed']
self.weapon_max_range = data['weapon_max_range']
self.weapon_projectile_id = data['weapon_projectile_id']
self.weapon_projectile_speed = data['weapon_projectile_speed']
self.weapon_combat_idle_anim = data['weapon_combat_idle_anim']
self.weapon_damage = []
for damage_data in data['weapon_damage']:
damage = Damage()
damage.load_json(damage_data)
self.weapon_damage.append(damage)
self.weapon_attack_anim_right = data['weapon_attack_anim_right']
self.weapon_attack_anim_left = data['weapon_attack_anim_left']
def save_json(self):
data = OrderedDict()
data['weapon_wepspeed'] = self.weapon_wepspeed
data['weapon_max_range'] = self.weapon_max_range
data['weapon_projectile_id'] = self.weapon_projectile_id
data['weapon_projectile_speed'] = self.weapon_projectile_speed
data['weapon_combat_idle_anim'] = self.weapon_combat_idle_anim
data['weapon_damage'] = []
for damage in self.weapon_damage:
data['weapon_damage'].append(damage.save_json())
data['weapon_attack_anim_right'] = self.weapon_attack_anim_right
data['weapon_attack_anim_left'] = self.weapon_attack_anim_left
return data
class ArcItem(ArcCombatObj):
def load_binary(self, stream: ResStream):
super().load_binary(stream)
self.item_type = stream.read_long()
self.item_eq_slots_value = stream.read_long()
self.item_eq_slots_or = stream.read_long()
self.item_eq_slots_and = stream.read_long()
self.item_eq_slots_type = stream.read_bool()
self.item_takeable = stream.read_bool()
self.item_value = stream.read_long()
self.item_wt = stream.read_long()
self.item_passive_defense_mod = stream.read_float()
self.item_base_name = stream.read_string()
self.item_dsc = stream.read_string()
self.item_render_object_female = stream.read_qword()
self.item_health_full = stream.read_float()
self.item_skill_used = stream.read_dword()
self.item_skill_mastery_used = stream.read_dword()
self.item_parry_anim_id = stream.read_long()
num_adjustments = stream.read_dword()
self.item_offering_info = [Offerings() for _ in range(num_adjustments)]
for offering in self.item_offering_info:
offering.load_binary(stream)
if self.item_type == ITEM_TYPE_WEAPON:
self.item_weapon = Weapon()
self.item_weapon.load_binary(stream)
self.item_primary_attr = stream.read_dword()
self.item_secondary_attr = stream.read_dword()
if self.item_type == ITEM_TYPE_ARMOR:
self.item_bulk_factor = stream.read_float()
self.item_defense_rating = stream.read_long()
self.item_flags = stream.read_dword()
self.item_use_flags = stream.read_dword()
self.item_post_item_id = stream.read_qword()
self.item_initial_charges = stream.read_long()
self.item_book_arcana = stream.read_dword()
num_skills = stream.read_dword()
self.item_skill_req = [SkillLevel() for _ in range(num_skills)]
for skill in self.item_skill_req:
skill.load_binary(stream)
self.item_race_req = RaceRequired()
self.item_race_req.load_binary(stream)
self.item_class_req = ClassRequired()
self.item_class_req.load_binary(stream)
self.item_disc_req = DiscRequired()
self.item_disc_req.load_binary(stream)
num_attrs = stream.read_dword()
self.item_attr_req = [AttributeValue() for _ in range(num_attrs)]
for attr in self.item_attr_req:
attr.load_binary(stream)
self.item_level_req = stream.read_long()
self.item_rank_req = stream.read_long()
self.item_sex_req = stream.read_long()
num_user_actions = stream.read_dword()
self.item_user_power_action = [
[
stream.read_dword(),
stream.read_long(),
stream.read_long(),
] for _ in range(num_user_actions)
]
num_power_grants = stream.read_dword()
self.item_power_grant = [PowerGrant() for _ in range(num_power_grants)]
for action in self.item_power_grant:
action.load_binary(stream)
num_power_actions = stream.read_dword()
self.item_power_action = [PowerAction() for _ in range(num_power_actions)]
for grant in self.item_power_action:
grant.load_binary(stream)
self.item_sheathable = stream.read_bool()
if self.item_sheathable:
self.item_sheath_slot = stream.read_long()
self.item_sheath_offset = stream.read_tuple()
self.item_sheath_rotation = stream.read_tuple()
self.item_has_stub = stream.read_bool()
if self.item_has_stub:
self.item_stub_holder = [
stream.read_bool(),
stream.read_qword(),
]
num_adjustments = stream.read_dword()
self.item_offering_adjustments = [Offerings() for _ in range(num_adjustments)]
for offering in self.item_offering_adjustments:
offering.load_binary(stream)
num_costs = stream.read_dword()
self.item_resource_costs = [ResourceInfo() for _ in range(num_costs)]
for res in self.item_resource_costs:
res.load_binary(stream)
self.item_bane_rank = stream.read_long()
self.item_ignore_saved_actions = stream.read_bool()
def save_binary(self, stream: ResStream):
super().save_binary(stream)
stream.write_long(self.item_type)
stream.write_long(self.item_eq_slots_value)
stream.write_long(self.item_eq_slots_or)
stream.write_long(self.item_eq_slots_and)
stream.write_bool(self.item_eq_slots_type)
stream.write_bool(self.item_takeable)
stream.write_long(self.item_value)
stream.write_long(self.item_wt)
stream.write_float(self.item_passive_defense_mod)
stream.write_string(self.item_base_name)
stream.write_string(self.item_dsc)
stream.write_qword(self.item_render_object_female)
stream.write_float(self.item_health_full)
stream.write_dword(self.item_skill_used)
stream.write_dword(self.item_skill_mastery_used)
stream.write_long(self.item_parry_anim_id)
stream.write_dword(len(self.item_offering_info))
for offering in self.item_offering_info:
offering.save_binary(stream)
if self.item_type == ITEM_TYPE_WEAPON:
self.item_weapon.save_binary(stream)
stream.write_dword(self.item_primary_attr)
stream.write_dword(self.item_secondary_attr)
if self.item_type == ITEM_TYPE_ARMOR:
stream.write_float(self.item_bulk_factor)
stream.write_long(self.item_defense_rating)
stream.write_dword(self.item_flags)
stream.write_dword(self.item_use_flags)
stream.write_qword(self.item_post_item_id)
stream.write_long(self.item_initial_charges)
stream.write_dword(self.item_book_arcana)
stream.write_dword(len(self.item_skill_req))
for skill in self.item_skill_req:
skill.save_binary(stream)
self.item_race_req.save_binary(stream)
self.item_class_req.save_binary(stream)
self.item_disc_req.save_binary(stream)
stream.write_dword(len(self.item_attr_req))
for attr in self.item_attr_req:
attr.save_binary(stream)
stream.write_long(self.item_level_req)
stream.write_long(self.item_rank_req)
stream.write_long(self.item_sex_req)
stream.write_dword(len(self.item_user_power_action))
for power in self.item_user_power_action:
stream.write_dword(power[0])
stream.write_long(power[1])
stream.write_long(power[2])
stream.write_dword(len(self.item_power_grant))
for action in self.item_power_grant:
action.save_binary(stream)
stream.write_dword(len(self.item_power_action))
for grant in self.item_power_action:
grant.save_binary(stream)
stream.write_bool(self.item_sheathable)
if self.item_sheathable:
stream.write_long(self.item_sheath_slot)
stream.write_tuple(self.item_sheath_offset)
stream.write_tuple(self.item_sheath_rotation)
stream.write_bool(self.item_has_stub)
if self.item_has_stub:
stream.write_bool(self.item_stub_holder[0])
stream.write_qword(self.item_stub_holder[1])
stream.write_dword(len(self.item_offering_adjustments))
for offering in self.item_offering_adjustments:
offering.save_binary(stream)
stream.write_dword(len(self.item_resource_costs))
for res in self.item_resource_costs:
res.save_binary(stream)
stream.write_long(self.item_bane_rank)
stream.write_bool(self.item_ignore_saved_actions)
def load_json(self, data):
super().load_json(data)
self.item_type = STRING_TO_ITEM_TYPE[data['item_type']]
self.item_eq_slots_value = data['item_eq_slots_value']
self.item_eq_slots_type = data['item_eq_slots_type']
value = 0
for i in range(ITEM_EQUIP_SLOT_NUM):
eq_slot = 1 << i
if ITEM_EQIP_SLOT_TO_STRING[eq_slot] in data['item_eq_slots_or']:
value |= eq_slot
self.item_eq_slots_or = value
value = 0
for i in range(ITEM_EQUIP_SLOT_NUM):
eq_slot = 1 << i
if ITEM_EQIP_SLOT_TO_STRING[eq_slot] in data['item_eq_slots_and']:
value |= eq_slot
self.item_eq_slots_and = value
self.item_takeable = data['item_takeable']
self.item_value = data['item_value']
self.item_wt = data['item_wt']
self.item_passive_defense_mod = data['item_passive_defense_mod']
self.item_base_name = data['item_base_name']
self.item_dsc = data['item_dsc']
self.item_render_object_female = data['item_render_object_female']
self.item_health_full = data['item_health_full']
self.item_skill_used = string_to_hash(data['item_skill_used'])
self.item_skill_mastery_used = string_to_hash(data['item_skill_mastery_used'])
self.item_parry_anim_id = data['item_parry_anim_id']
self.item_offering_info = []
for offering_data in data['item_offering_info']:
offering = Offerings()
offering.load_json(offering_data)
self.item_offering_info.append(offering)
if self.item_type == ITEM_TYPE_WEAPON:
self.item_weapon = Weapon()
self.item_weapon.load_json(data['item_weapon'])
self.item_primary_attr = string_to_hash(data['item_primary_attr'])
self.item_secondary_attr = string_to_hash(data['item_secondary_attr'])
if self.item_type == ITEM_TYPE_ARMOR:
self.item_bulk_factor = data['item_bulk_factor']
self.item_defense_rating = data['item_defense_rating']
value = 0
for i in range(ITEM_FLAG_NUM):
item_flag = 1 << i
if ITEM_FLAG_TO_STRING[item_flag] in data['item_flags']:
value |= item_flag
self.item_flags = value
value = 0
for i in range(ITEM_USE_FLAGS_NUM):
item_use_flag = 1 << i
if ITEM_USE_FLAGS_TO_STRING[item_use_flag] in data['item_use_flags']:
value |= item_use_flag
self.item_use_flags = value
self.item_post_item_id = data['item_post_item_id']
self.item_initial_charges = data['item_initial_charges']
self.item_book_arcana = data['item_book_arcana']
self.item_skill_req = []
for skill_data in data['item_skill_req']:
skill = SkillLevel()
skill.load_json(skill_data)
self.item_skill_req.append(skill)
self.item_race_req = RaceRequired()
self.item_race_req.load_json(data['item_race_req'])
self.item_class_req = ClassRequired()
self.item_class_req.load_json(data['item_class_req'])
self.item_disc_req = DiscRequired()
self.item_disc_req.load_json(data['item_disc_req'])
self.item_attr_req = []
for attr_data in data['item_attr_req']:
attr = AttributeValue()
attr.load_json(attr_data)
self.item_attr_req.append(attr)
self.item_level_req = data['item_level_req']
self.item_rank_req = data['item_rank_req']
self.item_sex_req = STRING_TO_ITEM_SEX_REQ[data['item_sex_req']]
self.item_user_power_action = []
for action in data['item_user_power_action']:
self.item_user_power_action.append([
string_to_hash(action['power']),
action['arguments'][0],
action['arguments'][1],
])
self.item_power_grant = []
for grant_data in data['item_power_grant']:
grant = PowerGrant()
grant.load_json(grant_data)
self.item_power_grant.append(grant)
self.item_power_action = []
for action_data in data['item_power_action']:
action = PowerAction()
action.load_json(action_data)
self.item_power_action.append(action)
self.item_sheathable = data['item_sheathable']
if self.item_sheathable:
self.item_sheath_slot = STRING_TO_ITEM_SHEATHSLOT[data['item_sheath_slot']]
self.item_sheath_offset = data['item_sheath_offset']
self.item_sheath_rotation = data['item_sheath_rotation']
self.item_has_stub = data['item_has_stub']
if self.item_has_stub:
self.item_stub_holder = data['item_has_stub']
self.item_offering_adjustments = []
for offering_data in data['item_offering_adjustments']:
offering = Offerings()
offering.load_json(offering_data)
self.item_offering_adjustments.append(offering)
self.item_resource_costs = []
for res_data in data['item_resource_costs']:
res = ResourceInfo()
res.load_json(res_data)
self.item_resource_costs.append(res)
self.item_bane_rank = data['item_bane_rank']
self.item_ignore_saved_actions = data['item_ignore_saved_actions']
def save_json(self):
data = super().save_json()
data['item_type'] = ITEM_TYPE_TO_STRING[self.item_type]
data['item_eq_slots_value'] = self.item_eq_slots_value
data['item_eq_slots_type'] = self.item_eq_slots_type
if not self.item_eq_slots_or:
data['item_eq_slots_or'] = [ITEM_EQIP_SLOT_TO_STRING[ITEM_EQUIP_SLOT_NONE]]
else:
data['item_eq_slots_or'] = []
for i in range(ITEM_EQUIP_SLOT_NUM):
eq_slot = 1 << i
if self.item_eq_slots_or & eq_slot:
data['item_eq_slots_or'].append(ITEM_EQIP_SLOT_TO_STRING[eq_slot])
if not self.item_eq_slots_and:
data['item_eq_slots_and'] = [ITEM_EQIP_SLOT_TO_STRING[ITEM_EQUIP_SLOT_NONE]]
else:
data['item_eq_slots_and'] = []
for i in range(ITEM_EQUIP_SLOT_NUM):
eq_slot = 1 << i
if self.item_eq_slots_and & eq_slot:
data['item_eq_slots_and'].append(ITEM_EQIP_SLOT_TO_STRING[eq_slot])
data['item_takeable'] = self.item_takeable
data['item_value'] = self.item_value
data['item_wt'] = self.item_wt
data['item_passive_defense_mod'] = self.item_passive_defense_mod
data['item_base_name'] = self.item_base_name
data['item_dsc'] = self.item_dsc
data['item_render_object_female'] = self.item_render_object_female
data['item_health_full'] = self.item_health_full
data['item_skill_used'] = hash_to_string(self.item_skill_used)
data['item_skill_mastery_used'] = hash_to_string(self.item_skill_mastery_used)
data['item_parry_anim_id'] = self.item_parry_anim_id
data['item_offering_info'] = []
for offering in self.item_offering_info:
data['item_offering_info'].append(offering.save_json())
if self.item_type == ITEM_TYPE_WEAPON:
data['item_weapon'] = self.item_weapon.save_json()
data['item_primary_attr'] = hash_to_string(self.item_primary_attr)
data['item_secondary_attr'] = hash_to_string(self.item_secondary_attr)
if self.item_type == ITEM_TYPE_ARMOR:
data['item_bulk_factor'] = self.item_bulk_factor
data['item_defense_rating'] = self.item_defense_rating
data['item_flags'] = []
if self.item_flags:
for i in range(ITEM_FLAG_NUM):
item_flag = 1 << i
if self.item_flags & item_flag:
data['item_flags'].append(ITEM_FLAG_TO_STRING[item_flag])
data['item_use_flags'] = []
if self.item_use_flags:
for i in range(ITEM_USE_FLAGS_NUM):
item_use_flag = 1 << i
if self.item_use_flags & item_use_flag:
data['item_use_flags'].append(ITEM_USE_FLAGS_TO_STRING[item_use_flag])
data['item_post_item_id'] = self.item_post_item_id
data['item_initial_charges'] = self.item_initial_charges
data['item_book_arcana'] = self.item_book_arcana
data['item_skill_req'] = []
for skill in self.item_skill_req:
data['item_skill_req'].append(skill.save_json())
data['item_race_req'] = self.item_race_req.save_json()
data['item_class_req'] = self.item_class_req.save_json()
data['item_disc_req'] = self.item_disc_req.save_json()
data['item_attr_req'] = []
for attr in self.item_attr_req:
data['item_attr_req'].append(attr.save_json())
data['item_level_req'] = self.item_level_req
data['item_rank_req'] = self.item_rank_req
data['item_sex_req'] = ITEM_SEX_REQ_TO_STRING[self.item_sex_req]
data['item_user_power_action'] = []
for action in self.item_user_power_action:
data['item_user_power_action'].append({
'power': hash_to_string(action[0]),
'arguments': action[1:3],
})
data['item_power_grant'] = []
for grant in self.item_power_grant:
data['item_power_grant'].append(grant.save_json())
data['item_power_action'] = []
for action in self.item_power_action:
data['item_power_action'].append(action.save_json())
data['item_sheathable'] = self.item_sheathable
if self.item_sheathable:
data['item_sheath_slot'] = ITEM_SHEATHSLOT_TO_STRING[self.item_sheath_slot]
data['item_sheath_offset'] = self.item_sheath_offset
data['item_sheath_rotation'] = self.item_sheath_rotation
data['item_has_stub'] = self.item_has_stub
if self.item_has_stub:
data['item_has_stub'] = self.item_stub_holder
data['item_offering_adjustments'] = []
for offering in self.item_offering_adjustments:
data['item_offering_adjustments'].append(offering.save_json())
data['item_resource_costs'] = []
for res in self.item_resource_costs:
data['item_resource_costs'].append(res.save_json())
data['item_bane_rank'] = self.item_bane_rank
data['item_ignore_saved_actions'] = self.item_ignore_saved_actions
return data