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>
390 lines
20 KiB
Python
390 lines
20 KiB
Python
# • ▌ ▄ ·. ▄▄▄▄· ▄▄▄ .·▄▄▄▄ ▪ ▄▄▄▄▄ ▄▄▄ ▄▄▄·▄▄▄
|
|
# ·██ ▐███▪▐█ ▀█▪ ▀▄.▀·██▪ ██ ██ •██ ▪ ▀▄ █· ▐█ ▄█▀▄ █·▪
|
|
# ▐█ ▌▐▌▐█·▐█▀▀█▄ ▐▀▀▪▄▐█· ▐█▌▐█· ▐█.▪ ▄█▀▄ ▐▀▀▄ ██▀·▐▀▀▄ ▄█▀▄
|
|
# ██ ██▌▐█▌██▄▪▐█ ▐█▄▄▌██. ██ ▐█▌ ▐█▌·▐█▌.▐▌▐█•█▌ ▐█▪·•▐█•█▌▐█▌.▐▌
|
|
# ▀▀ █▪▀▀▀·▀▀▀▀ ▀▀▀ ▀▀▀▀▀• ▀▀▀ ▀▀▀ ▀█▄▀▪.▀ ▀ .▀ .▀ ▀ ▀█▄▀▪
|
|
# Magicbane Emulator Project © 2013 - 2022
|
|
# www.magicbane.com
|
|
|
|
from collections import OrderedDict
|
|
|
|
from arcane.util import ResStream
|
|
|
|
|
|
class RankInfo:
|
|
def load_binary(self, stream: ResStream):
|
|
self.rank_hirelings = stream.read_long()
|
|
self.rank_shrines = stream.read_long()
|
|
self.rank_spires = stream.read_long()
|
|
self.rank_barracks = stream.read_long()
|
|
self.rank_rank = stream.read_long()
|
|
self.rank_health = stream.read_long()
|
|
self.rank_automatic = stream.read_bool()
|
|
self.rank_energy_k1 = stream.read_float()
|
|
self.rank_energy_k2 = stream.read_float()
|
|
self.rank_level_val = stream.read_float()
|
|
num_builiding_ids = stream.read_dword()
|
|
self.rank_building_id = [
|
|
[
|
|
stream.read_dword(),
|
|
stream.read_qword(),
|
|
] for _ in range(num_builiding_ids)
|
|
]
|
|
self.rank_formula = stream.read_dword()
|
|
num_placement_limits = stream.read_dword()
|
|
self.rank_placement_limit = [
|
|
[
|
|
stream.read_dword(),
|
|
stream.read_long(),
|
|
] for _ in range(num_placement_limits)
|
|
]
|
|
|
|
def save_binary(self, stream: ResStream):
|
|
stream.write_long(self.rank_hirelings)
|
|
stream.write_long(self.rank_shrines)
|
|
stream.write_long(self.rank_spires)
|
|
stream.write_long(self.rank_barracks)
|
|
stream.write_long(self.rank_rank)
|
|
stream.write_long(self.rank_health)
|
|
stream.write_bool(self.rank_automatic)
|
|
stream.write_float(self.rank_energy_k1)
|
|
stream.write_float(self.rank_energy_k2)
|
|
stream.write_float(self.rank_level_val)
|
|
stream.write_dword(len(self.rank_building_id))
|
|
for building in self.rank_building_id:
|
|
stream.write_dword(building[0])
|
|
stream.write_qword(building[1])
|
|
stream.write_dword(self.rank_formula)
|
|
stream.write_dword(len(self.rank_placement_limit))
|
|
for placement in self.rank_placement_limit:
|
|
stream.write_dword(placement[0])
|
|
stream.write_long(placement[1])
|
|
|
|
def load_json(self, data):
|
|
self.rank_hirelings = data['rank_hirelings']
|
|
self.rank_shrines = data['rank_shrines']
|
|
self.rank_spires = data['rank_spires']
|
|
self.rank_barracks = data['rank_barracks']
|
|
self.rank_rank = data['rank_rank']
|
|
self.rank_health = data['rank_health']
|
|
self.rank_automatic = data['rank_automatic']
|
|
self.rank_energy_k1 = data['rank_energy_k1']
|
|
self.rank_energy_k2 = data['rank_energy_k2']
|
|
self.rank_level_val = data['rank_level_val']
|
|
self.rank_building_id = data['rank_building_id']
|
|
self.rank_formula = data['rank_formula']
|
|
self.rank_placement_limit = data['rank_placement_limit']
|
|
|
|
def save_json(self):
|
|
data = OrderedDict()
|
|
data['rank_hirelings'] = self.rank_hirelings
|
|
data['rank_shrines'] = self.rank_shrines
|
|
data['rank_spires'] = self.rank_spires
|
|
data['rank_barracks'] = self.rank_barracks
|
|
data['rank_rank'] = self.rank_rank
|
|
data['rank_health'] = self.rank_health
|
|
data['rank_automatic'] = self.rank_automatic
|
|
data['rank_energy_k1'] = self.rank_energy_k1
|
|
data['rank_energy_k2'] = self.rank_energy_k2
|
|
data['rank_level_val'] = self.rank_level_val
|
|
data['rank_building_id'] = self.rank_building_id
|
|
data['rank_formula'] = self.rank_formula
|
|
data['rank_placement_limit'] = self.rank_placement_limit
|
|
return data
|
|
|
|
|
|
class ArcCityAssetTemplate():
|
|
def load_binary(self, stream: ResStream):
|
|
self.template_max_ranks = stream.read_long()
|
|
self.template_start_rank = stream.read_long()
|
|
self.template_asset_type = stream.read_long()
|
|
self.template_trade_icon = stream.read_long()
|
|
self.template_landmark = stream.read_long()
|
|
self.template_is_maintenance = stream.read_bool()
|
|
self.template_has_keys = stream.read_bool()
|
|
self.template_use_hardpoints = stream.read_bool()
|
|
self.template_is_fort_asset = stream.read_bool()
|
|
self.template_is_building_of_war = stream.read_bool()
|
|
self.template_bow_can_place_on_grid = stream.read_bool()
|
|
self.template_requires_nation_tree_slot = stream.read_bool()
|
|
self.template_requires_guild_tree_slot = stream.read_bool()
|
|
self.template_use_fort_grid = stream.read_bool()
|
|
self.template_is_fort_start = stream.read_bool()
|
|
self.template_is_cap_asset = stream.read_bool()
|
|
self.template_zone_no_build = [
|
|
stream.read_float(),
|
|
stream.read_float(),
|
|
]
|
|
self.template_zone_influence = [
|
|
stream.read_float(),
|
|
stream.read_float(),
|
|
]
|
|
self.template_eject_loc = stream.read_tuple()
|
|
self.template_npc_load = stream.read_tuple()
|
|
self.template_fort_grid_offset = stream.read_tuple()
|
|
self.template_power_action_id = stream.read_dword()
|
|
self.template_zone_flag = stream.read_dword()
|
|
self.template_spire_event_rule = stream.read_dword()
|
|
self.template_maintenance_set = stream.read_dword()
|
|
self.template_damage_set = stream.read_dword()
|
|
self.template_energy_set = stream.read_dword()
|
|
self.template_use_trigger = stream.read_string()
|
|
|
|
# When set, the client builds a trigger object named by the
|
|
# preceding string and loads its data from the stream. The
|
|
# trigger formats are not implemented; fail rather than desync.
|
|
self.template_unknown_check1 = stream.read_bool()
|
|
if self.template_unknown_check1:
|
|
raise NotImplementedError('use trigger object data is not supported')
|
|
self.template_loot_trigger = stream.read_string()
|
|
|
|
self.template_unknown_check2 = stream.read_bool()
|
|
if self.template_unknown_check2:
|
|
raise NotImplementedError('loot trigger object data is not supported')
|
|
|
|
self.has_embedded_template = stream.read_bool()
|
|
if self.has_embedded_template:
|
|
self.template_embed_template = ArcCityAssetTemplate()
|
|
self.template_embed_template.load_binary(stream)
|
|
num_creators = stream.read_dword()
|
|
self.template_creator = [
|
|
stream.read_long() for _ in range(num_creators)
|
|
]
|
|
num_terrains = stream.read_dword()
|
|
self.template_terrain = [
|
|
stream.read_long() for _ in range(num_terrains)
|
|
]
|
|
num_valid_npc_types = stream.read_dword()
|
|
self.template_valid_npc_type = [
|
|
stream.read_long() for _ in range(num_valid_npc_types)
|
|
]
|
|
num_valid_npc_cat = stream.read_dword()
|
|
self.template_valid_npc_cat = [
|
|
stream.read_long() for _ in range(num_valid_npc_cat)
|
|
]
|
|
num_rank_info = stream.read_dword()
|
|
self.template_rank_info = [RankInfo() for _ in range(num_rank_info)]
|
|
for rank_info in self.template_rank_info:
|
|
rank_info.load_binary(stream)
|
|
num_cap_info = stream.read_dword()
|
|
self.template_cap_info = [
|
|
[
|
|
stream.read_long(),
|
|
stream.read_long(),
|
|
stream.read_qword(),
|
|
] for _ in range(num_cap_info)
|
|
]
|
|
num_event_rules = stream.read_dword()
|
|
self.template_event_rules = [
|
|
[
|
|
stream.read_long(),
|
|
stream.read_dword(),
|
|
] for _ in range(num_event_rules)
|
|
]
|
|
num_archs = stream.read_dword()
|
|
self.template_architecture = [
|
|
stream.read_string() for _ in range(num_archs)
|
|
]
|
|
self.template_offering_type = stream.read_string()
|
|
self.template_placement_type = stream.read_dword()
|
|
num_offering_adjusts = stream.read_dword()
|
|
self.template_offering_adjustment = [
|
|
[
|
|
stream.read_dword(),
|
|
stream.read_float(),
|
|
] for _ in range(num_offering_adjusts)
|
|
]
|
|
num_resource_limit = stream.read_dword()
|
|
self.template_resource_limit = [
|
|
[
|
|
stream.read_dword(),
|
|
stream.read_long(),
|
|
stream.read_long(),
|
|
] for _ in range(num_resource_limit)
|
|
]
|
|
self.template_unknown = stream.read_dword()
|
|
|
|
def save_binary(self, stream: ResStream):
|
|
stream.write_long(self.template_max_ranks)
|
|
stream.write_long(self.template_start_rank)
|
|
stream.write_long(self.template_asset_type)
|
|
stream.write_long(self.template_trade_icon)
|
|
stream.write_long(self.template_landmark)
|
|
stream.write_bool(self.template_is_maintenance)
|
|
stream.write_bool(self.template_has_keys)
|
|
stream.write_bool(self.template_use_hardpoints)
|
|
stream.write_bool(self.template_is_fort_asset)
|
|
stream.write_bool(self.template_is_building_of_war)
|
|
stream.write_bool(self.template_bow_can_place_on_grid)
|
|
stream.write_bool(self.template_requires_nation_tree_slot)
|
|
stream.write_bool(self.template_requires_guild_tree_slot)
|
|
stream.write_bool(self.template_use_fort_grid)
|
|
stream.write_bool(self.template_is_fort_start)
|
|
stream.write_bool(self.template_is_cap_asset)
|
|
stream.write_float(self.template_zone_no_build[0])
|
|
stream.write_float(self.template_zone_no_build[1])
|
|
stream.write_float(self.template_zone_influence[0])
|
|
stream.write_float(self.template_zone_influence[1])
|
|
stream.write_tuple(self.template_eject_loc)
|
|
stream.write_tuple(self.template_npc_load)
|
|
stream.write_tuple(self.template_fort_grid_offset)
|
|
stream.write_dword(self.template_power_action_id)
|
|
stream.write_dword(self.template_zone_flag)
|
|
stream.write_dword(self.template_spire_event_rule)
|
|
stream.write_dword(self.template_maintenance_set)
|
|
stream.write_dword(self.template_damage_set)
|
|
stream.write_dword(self.template_energy_set)
|
|
stream.write_string(self.template_use_trigger)
|
|
stream.write_bool(self.template_unknown_check1)
|
|
stream.write_string(self.template_loot_trigger)
|
|
stream.write_bool(self.template_unknown_check2)
|
|
stream.write_bool(self.has_embedded_template)
|
|
if self.has_embedded_template:
|
|
self.template_embed_template.save_binary(stream)
|
|
stream.write_dword(len(self.template_creator))
|
|
for creator in self.template_creator:
|
|
stream.write_long(creator)
|
|
stream.write_dword(len(self.template_terrain))
|
|
for terrain in self.template_terrain:
|
|
stream.write_long(terrain)
|
|
stream.write_dword(len(self.template_valid_npc_type))
|
|
for npc_type in self.template_valid_npc_type:
|
|
stream.write_long(npc_type)
|
|
stream.write_dword(len(self.template_valid_npc_cat))
|
|
for npc_cat in self.template_valid_npc_cat:
|
|
stream.write_long(npc_cat)
|
|
stream.write_dword(len(self.template_rank_info))
|
|
for rank_info in self.template_rank_info:
|
|
rank_info.save_binary(stream)
|
|
stream.write_dword(len(self.template_cap_info))
|
|
for cap in self.template_cap_info:
|
|
stream.write_long(cap[0])
|
|
stream.write_long(cap[1])
|
|
stream.write_qword(cap[2])
|
|
stream.write_dword(len(self.template_event_rules))
|
|
for event in self.template_event_rules:
|
|
stream.write_long(event[0])
|
|
stream.write_dword(event[1])
|
|
stream.write_dword(len(self.template_architecture))
|
|
for arch in self.template_architecture:
|
|
stream.write_string(arch)
|
|
stream.write_string(self.template_offering_type)
|
|
stream.write_dword(self.template_placement_type)
|
|
stream.write_dword(len(self.template_offering_adjustment))
|
|
for adjust in self.template_offering_adjustment:
|
|
stream.write_dword(adjust[0])
|
|
stream.write_float(adjust[1])
|
|
stream.write_dword(len(self.template_resource_limit))
|
|
for resource in self.template_resource_limit:
|
|
stream.write_dword(resource[0])
|
|
stream.write_long(resource[1])
|
|
stream.write_long(resource[2])
|
|
stream.write_dword(self.template_unknown)
|
|
|
|
def load_json(self, data):
|
|
self.template_max_ranks = data['template_max_ranks']
|
|
self.template_start_rank = data['template_start_rank']
|
|
self.template_asset_type = data['template_asset_type']
|
|
self.template_trade_icon = data['template_trade_icon']
|
|
self.template_landmark = data['template_landmark']
|
|
self.template_is_maintenance = data['template_is_maintenance']
|
|
self.template_has_keys = data['template_has_keys']
|
|
self.template_use_hardpoints = data['template_use_hardpoints']
|
|
self.template_is_fort_asset = data['template_is_fort_asset']
|
|
self.template_is_building_of_war = data['template_is_building_of_war']
|
|
self.template_bow_can_place_on_grid = data['template_bow_can_place_on_grid']
|
|
self.template_requires_nation_tree_slot = data['template_requires_nation_tree_slot']
|
|
self.template_requires_guild_tree_slot = data['template_requires_guild_tree_slot']
|
|
self.template_use_fort_grid = data['template_use_fort_grid']
|
|
self.template_is_fort_start = data['template_is_fort_start']
|
|
self.template_is_cap_asset = data['template_is_cap_asset']
|
|
self.template_zone_no_build = data['template_zone_no_build']
|
|
self.template_zone_influence = data['template_zone_influence']
|
|
self.template_eject_loc = data['template_eject_loc']
|
|
self.template_npc_load = data['template_npc_load']
|
|
self.template_fort_grid_offset = data['template_fort_grid_offset']
|
|
self.template_power_action_id = data['template_power_action_id']
|
|
self.template_zone_flag = data['template_zone_flag']
|
|
self.template_spire_event_rule = data['template_spire_event_rule']
|
|
self.template_maintenance_set = data['template_maintenance_set']
|
|
self.template_damage_set = data['template_damage_set']
|
|
self.template_energy_set = data['template_energy_set']
|
|
self.template_use_trigger = data['template_use_trigger']
|
|
self.template_unknown_check1 = data['template_unknown_check1']
|
|
self.template_loot_trigger = data['template_loot_trigger']
|
|
self.template_unknown_check2 = data['template_unknown_check2']
|
|
self.has_embedded_template = data['has_embedded_template']
|
|
if self.has_embedded_template:
|
|
self.template_embed_template = ArcCityAssetTemplate()
|
|
self.template_embed_template.load_json(data['template_embed_template'])
|
|
self.template_creator = data['template_creator']
|
|
self.template_terrain = data['template_terrain']
|
|
self.template_valid_npc_type = data['template_valid_npc_type']
|
|
self.template_valid_npc_cat = data['template_valid_npc_cat']
|
|
self.template_rank_info = []
|
|
for rank_info_data in data['template_rank_info']:
|
|
rank_info = RankInfo()
|
|
rank_info.load_json(rank_info_data)
|
|
self.template_rank_info.append(rank_info)
|
|
self.template_cap_info = data['template_cap_info']
|
|
self.template_event_rules = data['template_event_rules']
|
|
self.template_architecture = data['template_architecture']
|
|
self.template_offering_type = data['template_offering_type']
|
|
self.template_placement_type = data['template_placement_type']
|
|
self.template_offering_adjustment = data['template_offering_adjustment']
|
|
self.template_resource_limit = data['template_resource_limit']
|
|
self.template_unknown = data['template_unknown']
|
|
|
|
def save_json(self):
|
|
data = OrderedDict()
|
|
data['template_max_ranks'] = self.template_max_ranks
|
|
data['template_start_rank'] = self.template_start_rank
|
|
data['template_asset_type'] = self.template_asset_type
|
|
data['template_trade_icon'] = self.template_trade_icon
|
|
data['template_landmark'] = self.template_landmark
|
|
data['template_is_maintenance'] = self.template_is_maintenance
|
|
data['template_has_keys'] = self.template_has_keys
|
|
data['template_use_hardpoints'] = self.template_use_hardpoints
|
|
data['template_is_fort_asset'] = self.template_is_fort_asset
|
|
data['template_is_building_of_war'] = self.template_is_building_of_war
|
|
data['template_bow_can_place_on_grid'] = self.template_bow_can_place_on_grid
|
|
data['template_requires_nation_tree_slot'] = self.template_requires_nation_tree_slot
|
|
data['template_requires_guild_tree_slot'] = self.template_requires_guild_tree_slot
|
|
data['template_use_fort_grid'] = self.template_use_fort_grid
|
|
data['template_is_fort_start'] = self.template_is_fort_start
|
|
data['template_is_cap_asset'] = self.template_is_cap_asset
|
|
data['template_zone_no_build'] = self.template_zone_no_build
|
|
data['template_zone_influence'] = self.template_zone_influence
|
|
data['template_eject_loc'] = self.template_eject_loc
|
|
data['template_npc_load'] = self.template_npc_load
|
|
data['template_fort_grid_offset'] = self.template_fort_grid_offset
|
|
data['template_power_action_id'] = self.template_power_action_id
|
|
data['template_zone_flag'] = self.template_zone_flag
|
|
data['template_spire_event_rule'] = self.template_spire_event_rule
|
|
data['template_maintenance_set'] = self.template_maintenance_set
|
|
data['template_damage_set'] = self.template_damage_set
|
|
data['template_energy_set'] = self.template_energy_set
|
|
data['template_use_trigger'] = self.template_use_trigger
|
|
data['template_unknown_check1'] = self.template_unknown_check1
|
|
data['template_loot_trigger'] = self.template_loot_trigger
|
|
data['template_unknown_check2'] = self.template_unknown_check2
|
|
data['has_embedded_template'] = self.has_embedded_template
|
|
if self.has_embedded_template:
|
|
data['template_embed_template'] = self.template_embed_template.save_json()
|
|
data['template_creator'] = self.template_creator
|
|
data['template_terrain'] = self.template_terrain
|
|
data['template_valid_npc_type'] = self.template_valid_npc_type
|
|
data['template_valid_npc_cat'] = self.template_valid_npc_cat
|
|
data['template_rank_info'] = []
|
|
for rank_info in self.template_rank_info:
|
|
data['template_rank_info'].append(rank_info.save_json())
|
|
data['template_cap_info'] = self.template_cap_info
|
|
data['template_event_rules'] = self.template_event_rules
|
|
data['template_architecture'] = self.template_architecture
|
|
data['template_offering_type'] = self.template_offering_type
|
|
data['template_placement_type'] = self.template_placement_type
|
|
data['template_offering_adjustment'] = self.template_offering_adjustment
|
|
data['template_resource_limit'] = self.template_resource_limit
|
|
data['template_unknown'] = self.template_unknown
|
|
return data
|