Compare commits

12 Commits
Author SHA1 Message Date
MagicBotandClaude Opus 5.5 d7e518bbd3 Note in README that unpack reads only plain-named caches
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:56:06 -04:00
MagicBotandClaude Opus 5.5 4286e886fc Remove JSON_CHANGES.md (moved to MBDATA)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:47:14 -04:00
MagicBotandClaude Opus 5.5 027b6e143f Fix ARCANE_DUMP folder name in README
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:46:20 -04:00
MagicBotandClaude Opus 5.5 a29d00ce23 Document numbered pack/unpack output in README
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:46:01 -04:00
MagicBotandClaude Opus 5.5 9005de76d9 Number output files and folders instead of overwriting
Pack now writes <Name>.cache, or <Name>.cache.1, .2, ... if that file
already exists (replaces the fixed .cache.new name). Unpack writes each
target folder under ARCANE_DUMP the same way, e.g. COBJECTS.1. The
output folder is created only after the cache loads, so a missing cache
does not leave an empty numbered folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:43:40 -04:00
MagicBotandClaude Opus 5.5 7bcbed08a8 Accept unsigned values in signed fields when packing
Dumps made before the signed-read change store values like -1 as
4294967295. write_long now converts 2147483648..4294967295 to the
signed equivalent (same bytes), so existing community dumps still
pack. Verified: all 10332 files of an older dump repack identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:09:05 -04:00
MagicBotandClaude Opus 5.5 50d8d76a21 Document COBJECTS JSON format changes
List every field now read signed, the grant key hashing, the effect on
the sample cache, compatibility with older dumps, related parser fixes
and open items, for readers of the unpacked JSON.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:06:08 -04:00
MagicBotandClaude Opus 5.5 ca4d2b2565 Hash grant attr and power keys, read grant values signed
Power and skill grants read every sub-array as (u32 key, int32 value)
(sb.exe 0x6D7640). Skill keys were already hashed; attr and power keys
now are too, and the values plus attribute/skill/resource amounts and
power action arguments are read signed. Shared pair read/write and key
hashing moved into helpers; load_json no longer mutates its input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 12:55:04 -04:00
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
MagicBotandClaude Opus 5.5 6a8598f33f Fix ArcContainerObject save_binary and load_json
save_binary called stream.read_dword(value) instead of write_dword,
so packing any container raised a TypeError. load_json called
super().load_json() without data, so the item base never loaded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 12:53:31 -04:00
MagicBotandClaude Opus 5.5 2008697d6c Fail on unsupported asset template trigger objects
When the bool after USE_TRIGGER / LOOT_TRIGGER is set, the client
(sb.exe 0xC46DE0) instantiates a trigger object by name and loads
extra data from the stream. The parser silently ignored the branch,
which would desync the rest of the template. Raise instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 12:42:57 -04:00
MagicBotandClaude Opus 5.5 7da18716a8 Fix sparse MerchantData write size
The client loads MerchantData as ArcCacheID + ArcCacheID + int32
(20 bytes, sb.exe 0x4859B0). save_binary wrote the final field as a
qword, corrupting any packed object carrying merchant sparse data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 12:42:57 -04:00
20 changed files with 338 additions and 340 deletions

No files matched your search

+3 -1
View File
@@ -38,7 +38,8 @@ mbEditorPro requires pillow for image manipulation and tqdm for metrics.
mbEditorPro.py unpack cobjects
mbEditorPro.py unpack all
```
JSON output will be placed into the folder **ARCANE DUMP** <br>
JSON output will be placed into the folder **ARCANE_DUMP** <br>
Unpack only reads the plain-named cache files (e.g. CObjects.cache). If a target folder already exists, a numbered folder is created instead (e.g. COBJECTS.1) so earlier output is never overwritten. <br>
```
mbEditorPro.py pack textures
@@ -46,6 +47,7 @@ mbEditorPro.py pack all
```
New binary cache file(s) will be built from the JSON and output into the current directory. <br>
Pack only reads from the un-numbered folders (e.g. COBJECTS). If a cache file already exists, a numbered copy is written instead (e.g. CObjects.cache.1). <br>
### Limitations
+3 -1
View File
@@ -38,7 +38,8 @@ mbEditorPro requires pillow for image manipulation and tqdm for metrics.
mbEditorPro.py unpack cobjects
mbEditorPro.py unpack all
```
JSON output will be placed into the folder **ARCANE DUMP** <br>
JSON output will be placed into the folder **ARCANE_DUMP** <br>
Unpack only reads the plain-named cache files (e.g. CObjects.cache). If a target folder already exists, a numbered folder is created instead (e.g. COBJECTS.1) so earlier output is never overwritten. <br>
```
mbEditorPro.py pack textures
@@ -46,6 +47,7 @@ mbEditorPro.py pack all
```
New binary cache file(s) will be built from the JSON and output into the current directory. <br>
Pack only reads from the un-numbered folders (e.g. COBJECTS). If a cache file already exists, a numbered copy is written instead (e.g. CObjects.cache.1). <br>
### Limitations
@@ -13,12 +13,12 @@ from arcane.util import ResStream
class RankInfo:
def load_binary(self, stream: ResStream):
self.rank_hirelings = stream.read_dword()
self.rank_shrines = stream.read_dword()
self.rank_spires = stream.read_dword()
self.rank_barracks = stream.read_dword()
self.rank_rank = stream.read_dword()
self.rank_health = stream.read_dword()
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()
@@ -35,17 +35,17 @@ class RankInfo:
self.rank_placement_limit = [
[
stream.read_dword(),
stream.read_dword(),
stream.read_long(),
] for _ in range(num_placement_limits)
]
def save_binary(self, stream: ResStream):
stream.write_dword(self.rank_hirelings)
stream.write_dword(self.rank_shrines)
stream.write_dword(self.rank_spires)
stream.write_dword(self.rank_barracks)
stream.write_dword(self.rank_rank)
stream.write_dword(self.rank_health)
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)
@@ -58,7 +58,7 @@ class RankInfo:
stream.write_dword(len(self.rank_placement_limit))
for placement in self.rank_placement_limit:
stream.write_dword(placement[0])
stream.write_dword(placement[1])
stream.write_long(placement[1])
def load_json(self, data):
self.rank_hirelings = data['rank_hirelings']
@@ -95,11 +95,11 @@ class RankInfo:
class ArcCityAssetTemplate():
def load_binary(self, stream: ResStream):
self.template_max_ranks = stream.read_dword()
self.template_start_rank = stream.read_dword()
self.template_asset_type = stream.read_dword()
self.template_trade_icon = stream.read_dword()
self.template_landmark = stream.read_dword()
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()
@@ -130,14 +130,17 @@ class ArcCityAssetTemplate():
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:
pass
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:
pass
raise NotImplementedError('loot trigger object data is not supported')
self.has_embedded_template = stream.read_bool()
if self.has_embedded_template:
@@ -145,19 +148,19 @@ class ArcCityAssetTemplate():
self.template_embed_template.load_binary(stream)
num_creators = stream.read_dword()
self.template_creator = [
stream.read_dword() for _ in range(num_creators)
stream.read_long() for _ in range(num_creators)
]
num_terrains = stream.read_dword()
self.template_terrain = [
stream.read_dword() for _ in range(num_terrains)
stream.read_long() for _ in range(num_terrains)
]
num_valid_npc_types = stream.read_dword()
self.template_valid_npc_type = [
stream.read_dword() for _ in range(num_valid_npc_types)
stream.read_long() for _ in range(num_valid_npc_types)
]
num_valid_npc_cat = stream.read_dword()
self.template_valid_npc_cat = [
stream.read_dword() for _ in range(num_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)]
@@ -166,15 +169,15 @@ class ArcCityAssetTemplate():
num_cap_info = stream.read_dword()
self.template_cap_info = [
[
stream.read_dword(),
stream.read_dword(),
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_dword(),
stream.read_long(),
stream.read_dword(),
] for _ in range(num_event_rules)
]
@@ -195,18 +198,18 @@ class ArcCityAssetTemplate():
self.template_resource_limit = [
[
stream.read_dword(),
stream.read_dword(),
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_dword(self.template_max_ranks)
stream.write_dword(self.template_start_rank)
stream.write_dword(self.template_asset_type)
stream.write_dword(self.template_trade_icon)
stream.write_dword(self.template_landmark)
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)
@@ -240,27 +243,27 @@ class ArcCityAssetTemplate():
self.template_embed_template.save_binary(stream)
stream.write_dword(len(self.template_creator))
for creator in self.template_creator:
stream.write_dword(creator)
stream.write_long(creator)
stream.write_dword(len(self.template_terrain))
for terrain in self.template_terrain:
stream.write_dword(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_dword(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_dword(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_dword(cap[0])
stream.write_dword(cap[1])
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_dword(event[0])
stream.write_long(event[0])
stream.write_dword(event[1])
stream.write_dword(len(self.template_architecture))
for arch in self.template_architecture:
@@ -274,8 +277,8 @@ class ArcCityAssetTemplate():
stream.write_dword(len(self.template_resource_limit))
for resource in self.template_resource_limit:
stream.write_dword(resource[0])
stream.write_dword(resource[1])
stream.write_dword(resource[2])
stream.write_long(resource[1])
stream.write_long(resource[2])
stream.write_dword(self.template_unknown)
def load_json(self, data):
@@ -25,7 +25,7 @@ class ArcCombatObj(ArcObj):
self.combat_powers = [
[
stream.read_dword(),
stream.read_dword(),
stream.read_long(),
] for _ in range(num_powers)
]
@@ -38,7 +38,7 @@ class ArcCombatObj(ArcObj):
stream.write_dword(len(self.combat_powers))
for power in self.combat_powers:
stream.write_dword(power[0])
stream.write_dword(power[1])
stream.write_long(power[1])
def load_json(self, data):
super().load_json(data)
@@ -14,13 +14,13 @@ from .common.Inventory import Inventory
class ArcContainerObject(ArcItem):
def load_binary(self, stream: ResStream):
super().load_binary(stream)
self.container_open = stream.read_dword()
self.container_locked = stream.read_dword()
self.container_locked_rank = stream.read_dword()
self.container_barred = stream.read_dword()
self.container_barred_rank = stream.read_dword()
self.container_trapped = stream.read_dword()
self.container_trappe_rank = stream.read_dword()
self.container_open = stream.read_long()
self.container_locked = stream.read_long()
self.container_locked_rank = stream.read_long()
self.container_barred = stream.read_long()
self.container_barred_rank = stream.read_long()
self.container_trapped = stream.read_long()
self.container_trappe_rank = stream.read_long()
num_contents = stream.read_dword()
self.container_inventory_contents = [Inventory() for _ in range(num_contents)]
for content in self.container_inventory_contents:
@@ -28,19 +28,19 @@ class ArcContainerObject(ArcItem):
def save_binary(self, stream: ResStream):
super().save_binary(stream)
stream.read_dword(self.container_open)
stream.read_dword(self.container_locked)
stream.read_dword(self.container_locked_rank)
stream.read_dword(self.container_barred)
stream.read_dword(self.container_barred_rank)
stream.read_dword(self.container_trapped)
stream.read_dword(self.container_trappe_rank)
stream.read_dword(len(self.container_inventory_contents))
stream.write_long(self.container_open)
stream.write_long(self.container_locked)
stream.write_long(self.container_locked_rank)
stream.write_long(self.container_barred)
stream.write_long(self.container_barred_rank)
stream.write_long(self.container_trapped)
stream.write_long(self.container_trappe_rank)
stream.write_dword(len(self.container_inventory_contents))
for content in self.container_inventory_contents:
content.save_binary(stream)
def load_json(self, data):
super().load_json()
super().load_json(data)
self.container_open = data['container_open']
self.container_locked = data['container_locked']
self.container_locked_rank = data['container_locked_rank']
+8 -8
View File
@@ -13,12 +13,12 @@ from .ArcItem import ArcItem
class ArcDeed(ArcItem):
def load_binary(self, stream: ResStream):
super().load_binary(stream)
self.deed_type = stream.read_dword()
self.deed_type = stream.read_long()
self.deed_furniture_id = stream.read_qword()
self.deed_target_id = stream.read_qword()
self.deed_employment = stream.read_dword()
self.deed_start_rank = stream.read_dword()
self.deed_name_lookup = stream.read_dword()
self.deed_employment = stream.read_long()
self.deed_start_rank = stream.read_long()
self.deed_name_lookup = stream.read_long()
self.deed_indoors = stream.read_bool()
self.deed_is_fortress = stream.read_bool()
self.deed_namelookup_val = stream.read_float()
@@ -33,12 +33,12 @@ class ArcDeed(ArcItem):
def save_binary(self, stream: ResStream):
super().save_binary(stream)
stream.write_dword(self.deed_type)
stream.write_long(self.deed_type)
stream.write_qword(self.deed_furniture_id)
stream.write_qword(self.deed_target_id)
stream.write_dword(self.deed_employment)
stream.write_dword(self.deed_start_rank)
stream.write_dword(self.deed_name_lookup)
stream.write_long(self.deed_employment)
stream.write_long(self.deed_start_rank)
stream.write_long(self.deed_name_lookup)
stream.write_bool(self.deed_indoors)
stream.write_bool(self.deed_is_fortress)
stream.write_float(self.deed_namelookup_val)
@@ -15,9 +15,9 @@ from arcane.util import ResStream
class ArcDoorObject():
def load_binary(self, stream: ResStream):
self.door_id = stream.read_qword()
self.door_type = stream.read_dword()
self.door_movement_axis = stream.read_dword()
self.door_swing_direction = stream.read_dword()
self.door_type = stream.read_long()
self.door_movement_axis = stream.read_long()
self.door_swing_direction = stream.read_long()
self.door_min_time_to_stay_open = stream.read_float()
self.door_max_time_to_stay_open = stream.read_float()
self.door_swing_time = stream.read_float()
@@ -26,16 +26,16 @@ class ArcDoorObject():
self.door_slide_direction = stream.read_tuple()
self.door_slide_start = stream.read_tuple()
self.door_slide_end = stream.read_tuple()
self.door_locked = stream.read_dword()
self.door_locked = stream.read_long()
self.door_linked_to = stream.read_qword()
self.door_key = stream.read_qword()
self.door_sound_event = stream.read_qword()
def save_binary(self, stream: ResStream):
stream.write_qword(self.door_id)
stream.write_dword(self.door_type)
stream.write_dword(self.door_movement_axis)
stream.write_dword(self.door_swing_direction)
stream.write_long(self.door_type)
stream.write_long(self.door_movement_axis)
stream.write_long(self.door_swing_direction)
stream.write_float(self.door_min_time_to_stay_open)
stream.write_float(self.door_max_time_to_stay_open)
stream.write_float(self.door_swing_time)
@@ -44,7 +44,7 @@ class ArcDoorObject():
stream.write_tuple(self.door_slide_direction)
stream.write_tuple(self.door_slide_start)
stream.write_tuple(self.door_slide_end)
stream.write_dword(self.door_locked)
stream.write_long(self.door_locked)
stream.write_qword(self.door_linked_to)
stream.write_qword(self.door_key)
stream.write_qword(self.door_sound_event)
@@ -15,14 +15,14 @@ class ArcDungeonUnitObject(ArcStructureObject):
super().load_binary(stream)
num_connectables = stream.read_dword()
self.dungeon_connectable_edges = [
stream.read_dword() for _ in range(num_connectables)
stream.read_long() for _ in range(num_connectables)
]
def save_binary(self, stream: ResStream):
super().save_binary(stream)
stream.write_dword(len(self.dungeon_connectable_edges))
for edge in self.dungeon_connectable_edges:
stream.write_dword(edge)
stream.write_long(edge)
def load_json(self, data):
super().load_json(data)
+50 -50
View File
@@ -20,14 +20,14 @@ from .common.Race import RaceRequired
class Damage:
def load_binary(self, stream: ResStream):
self.damage_type = stream.read_dword()
self.damage_min = stream.read_dword()
self.damage_max = stream.read_dword()
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_dword(self.damage_type)
stream.write_dword(self.damage_min)
stream.write_dword(self.damage_max)
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']]
@@ -45,11 +45,11 @@ class Damage:
class Offerings:
def load_binary(self, stream: ResStream):
self.offering_type = stream.read_string()
self.offering_value = stream.read_dword()
self.offering_value = stream.read_long()
def save_binary(self, stream: ResStream):
stream.write_string(self.offering_type)
stream.write_dword(self.offering_value)
stream.write_long(self.offering_value)
def load_json(self, data):
self.offering_type = data['offering_type']
@@ -68,7 +68,7 @@ class Weapon:
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_dword()
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:
@@ -76,15 +76,15 @@ class Weapon:
num_right_anims = stream.read_dword()
self.weapon_attack_anim_right = [
[
stream.read_dword(),
stream.read_dword(),
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_dword(),
stream.read_dword(),
stream.read_long(),
stream.read_long(),
] for _ in range(num_left_anims)
]
@@ -93,18 +93,18 @@ class Weapon:
stream.write_float(self.weapon_max_range)
stream.write_qword(self.weapon_projectile_id)
stream.write_float(self.weapon_projectile_speed)
stream.write_dword(self.weapon_combat_idle_anim)
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_dword(anim[0])
stream.write_dword(anim[1])
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_dword(anim[0])
stream.write_dword(anim[1])
stream.write_long(anim[0])
stream.write_long(anim[1])
def load_json(self, data):
self.weapon_wepspeed = data['weapon_wepspeed']
@@ -138,14 +138,14 @@ class Weapon:
class ArcItem(ArcCombatObj):
def load_binary(self, stream: ResStream):
super().load_binary(stream)
self.item_type = stream.read_dword()
self.item_eq_slots_value = stream.read_dword()
self.item_eq_slots_or = stream.read_dword()
self.item_eq_slots_and = stream.read_dword()
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_dword()
self.item_wt = stream.read_dword()
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()
@@ -153,7 +153,7 @@ class ArcItem(ArcCombatObj):
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_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:
@@ -167,11 +167,11 @@ class ArcItem(ArcCombatObj):
if self.item_type == ITEM_TYPE_ARMOR:
self.item_bulk_factor = stream.read_float()
self.item_defense_rating = stream.read_dword()
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_dword()
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)]
@@ -187,15 +187,15 @@ class ArcItem(ArcCombatObj):
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_dword()
self.item_rank_req = stream.read_dword()
self.item_sex_req = stream.read_dword()
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_dword(),
stream.read_dword(),
stream.read_long(),
stream.read_long(),
] for _ in range(num_user_actions)
]
num_power_grants = stream.read_dword()
@@ -209,7 +209,7 @@ class ArcItem(ArcCombatObj):
self.item_sheathable = stream.read_bool()
if self.item_sheathable:
self.item_sheath_slot = stream.read_dword()
self.item_sheath_slot = stream.read_long()
self.item_sheath_offset = stream.read_tuple()
self.item_sheath_rotation = stream.read_tuple()
@@ -227,19 +227,19 @@ class ArcItem(ArcCombatObj):
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_dword()
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_dword(self.item_type)
stream.write_dword(self.item_eq_slots_value)
stream.write_dword(self.item_eq_slots_or)
stream.write_dword(self.item_eq_slots_and)
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_dword(self.item_value)
stream.write_dword(self.item_wt)
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)
@@ -247,7 +247,7 @@ class ArcItem(ArcCombatObj):
stream.write_float(self.item_health_full)
stream.write_dword(self.item_skill_used)
stream.write_dword(self.item_skill_mastery_used)
stream.write_dword(self.item_parry_anim_id)
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)
@@ -257,11 +257,11 @@ class ArcItem(ArcCombatObj):
stream.write_dword(self.item_secondary_attr)
if self.item_type == ITEM_TYPE_ARMOR:
stream.write_float(self.item_bulk_factor)
stream.write_dword(self.item_defense_rating)
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_dword(self.item_initial_charges)
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:
@@ -272,14 +272,14 @@ class ArcItem(ArcCombatObj):
stream.write_dword(len(self.item_attr_req))
for attr in self.item_attr_req:
attr.save_binary(stream)
stream.write_dword(self.item_level_req)
stream.write_dword(self.item_rank_req)
stream.write_dword(self.item_sex_req)
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_dword(power[1])
stream.write_dword(power[2])
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)
@@ -288,7 +288,7 @@ class ArcItem(ArcCombatObj):
grant.save_binary(stream)
stream.write_bool(self.item_sheathable)
if self.item_sheathable:
stream.write_dword(self.item_sheath_slot)
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)
@@ -301,7 +301,7 @@ class ArcItem(ArcCombatObj):
stream.write_dword(len(self.item_resource_costs))
for res in self.item_resource_costs:
res.save_binary(stream)
stream.write_dword(self.item_bane_rank)
stream.write_long(self.item_bane_rank)
stream.write_bool(self.item_ignore_saved_actions)
def load_json(self, data):
+2 -2
View File
@@ -16,14 +16,14 @@ class ArcKey(ArcItem):
self.key_keyval = stream.read_qword()
self.key_keyval2 = stream.read_qword()
self.key_keyval3 = stream.read_qword()
self.key_setting = stream.read_dword()
self.key_setting = stream.read_long()
def save_binary(self, stream: ResStream):
super().save_binary(stream)
stream.write_qword(self.key_keyval)
stream.write_qword(self.key_keyval2)
stream.write_qword(self.key_keyval3)
stream.write_dword(self.key_setting)
stream.write_long(self.key_setting)
def load_json(self, data):
super().load_json(data)
+6 -6
View File
@@ -16,11 +16,11 @@ from .common.SparseData import SparseData
class SoundEvent:
def load_binary(self, stream: ResStream):
self.sound_type = stream.read_dword()
self.sound_type = stream.read_long()
self.sound_event = stream.read_qword()
def save_binary(self, stream: ResStream):
stream.write_dword(self.sound_type)
stream.write_long(self.sound_type)
stream.write_qword(self.sound_event)
def load_json(self, data):
@@ -55,8 +55,8 @@ class ArcObj:
num_hard_points = stream.read_dword()
self.obj_arc_hardpoint_list = [
[
stream.read_dword(),
stream.read_dword(),
stream.read_long(),
stream.read_long(),
[stream.read_float() for _ in range(10)]
] for _ in range(num_hard_points)
]
@@ -84,8 +84,8 @@ class ArcObj:
sound_event.save_binary(stream)
stream.write_dword(len(self.obj_arc_hardpoint_list))
for n, m, points in self.obj_arc_hardpoint_list:
stream.write_dword(n)
stream.write_dword(m)
stream.write_long(n)
stream.write_long(m)
for i in range(10):
stream.write_float(points[i])
self.obj_sparse_data.save_binary(stream)
+40 -40
View File
@@ -48,8 +48,8 @@ class SkillAdjust:
num_adjusts = stream.read_dword()
self.skill_adjusts = [
[
stream.read_dword(),
stream.read_dword(),
stream.read_long(),
stream.read_long(),
] for _ in range(num_adjusts)
]
@@ -57,8 +57,8 @@ class SkillAdjust:
stream.write_dword(self.skill_type)
stream.write_dword(len(self.skill_adjusts))
for skill in self.skill_adjusts:
stream.write_dword(skill[0])
stream.write_dword(skill[1])
stream.write_long(skill[0])
stream.write_long(skill[1])
def load_json(self, data):
self.skill_type = string_to_hash(data['skill_type'])
@@ -98,23 +98,23 @@ class BodyPart:
class ArcRune(ArcItem):
def load_binary(self, stream: ResStream):
super().load_binary(stream)
self.rune_type = stream.read_dword()
self.rune_type = stream.read_long()
self.rune_sub_type = stream.read_dword()
self.rune_is_standard_character_creation = stream.read_bool()
self.rune_creation_cost = stream.read_dword()
self.rune_rank = stream.read_dword()
self.rune_pracs_per_level = stream.read_dword()
self.rune_creation_cost = stream.read_long()
self.rune_rank = stream.read_long()
self.rune_pracs_per_level = stream.read_long()
self.rune_exp_req_to_level = stream.read_float()
self.rune_sex = stream.read_dword()
self.rune_sex = stream.read_long()
self.rune_class_icon = stream.read_qword()
self.rune_health = stream.read_dword()
self.rune_mana = stream.read_dword()
self.rune_stamina = stream.read_dword()
self.rune_health = stream.read_long()
self.rune_mana = stream.read_long()
self.rune_stamina = stream.read_long()
self.rune_min_damage = stream.read_float()
self.rune_max_damage = stream.read_float()
self.rune_attack = stream.read_dword()
self.rune_defense = stream.read_dword()
self.rune_level = stream.read_dword()
self.rune_attack = stream.read_long()
self.rune_defense = stream.read_long()
self.rune_level = stream.read_long()
self.rune_speed = [
stream.read_float() for _ in range(7)
]
@@ -142,7 +142,7 @@ class ArcRune(ArcItem):
]
num_genders = stream.read_dword()
self.rune_enemy_gender = [
stream.read_dword() for _ in range(num_genders)
stream.read_long() for _ in range(num_genders)
]
num_skill_grants = stream.read_dword()
self.rune_skill_grant = [SkillGrant() for _ in range(num_skill_grants)]
@@ -165,14 +165,14 @@ class ArcRune(ArcItem):
[
stream.read_float(),
stream.read_float(),
stream.read_dword(),
stream.read_dword(),
stream.read_long(),
stream.read_long(),
stream.read_qword(),
stream.read_float(),
stream.read_dword(),
stream.read_dword(),
stream.read_dword(),
stream.read_dword(),
stream.read_long(),
stream.read_long(),
stream.read_long(),
stream.read_long(),
] for _ in range(num_natural_attacks)
]
num_enchants = stream.read_dword()
@@ -210,23 +210,23 @@ class ArcRune(ArcItem):
def save_binary(self, stream: ResStream):
super().save_binary(stream)
stream.write_dword(self.rune_type)
stream.write_long(self.rune_type)
stream.write_dword(self.rune_sub_type)
stream.write_bool(self.rune_is_standard_character_creation)
stream.write_dword(self.rune_creation_cost)
stream.write_dword(self.rune_rank)
stream.write_dword(self.rune_pracs_per_level)
stream.write_long(self.rune_creation_cost)
stream.write_long(self.rune_rank)
stream.write_long(self.rune_pracs_per_level)
stream.write_float(self.rune_exp_req_to_level)
stream.write_dword(self.rune_sex)
stream.write_long(self.rune_sex)
stream.write_qword(self.rune_class_icon)
stream.write_dword(self.rune_health)
stream.write_dword(self.rune_mana)
stream.write_dword(self.rune_stamina)
stream.write_long(self.rune_health)
stream.write_long(self.rune_mana)
stream.write_long(self.rune_stamina)
stream.write_float(self.rune_min_damage)
stream.write_float(self.rune_max_damage)
stream.write_dword(self.rune_attack)
stream.write_dword(self.rune_defense)
stream.write_dword(self.rune_level)
stream.write_long(self.rune_attack)
stream.write_long(self.rune_defense)
stream.write_long(self.rune_level)
for i in range(7):
stream.write_float(self.rune_speed[i])
self.rune_group.save_binary(stream)
@@ -248,7 +248,7 @@ class ArcRune(ArcItem):
stream.write_dword(monster)
stream.write_dword(len(self.rune_enemy_gender))
for gender in self.rune_enemy_gender:
stream.write_dword(gender)
stream.write_long(gender)
stream.write_dword(len(self.rune_skill_grant))
for grant in self.rune_skill_grant:
grant.save_binary(stream)
@@ -265,14 +265,14 @@ class ArcRune(ArcItem):
for attack in self.rune_naturalattacks:
stream.write_float(attack[0])
stream.write_float(attack[1])
stream.write_dword(attack[2])
stream.write_dword(attack[3])
stream.write_long(attack[2])
stream.write_long(attack[3])
stream.write_qword(attack[4])
stream.write_float(attack[5])
stream.write_dword(attack[6])
stream.write_dword(attack[7])
stream.write_dword(attack[8])
stream.write_dword(attack[9])
stream.write_long(attack[6])
stream.write_long(attack[7])
stream.write_long(attack[8])
stream.write_long(attack[9])
stream.write_dword(len(self.rune_enchantment_type))
for enchantment in self.rune_enchantment_type:
stream.write_dword(enchantment)
@@ -20,9 +20,9 @@ class Sound:
self.sound_max_dist = stream.read_float()
self.sound_dist = stream.read_bool()
self.sound_loop = stream.read_bool()
self.sound_chance = stream.read_dword()
self.sound_chance = stream.read_long()
self.sound_interval = stream.read_float()
self.sound_reserved = stream.read_dword()
self.sound_reserved = stream.read_long()
def save_binary(self, stream: ResStream):
stream.write_qword(self.sound_0)
@@ -31,9 +31,9 @@ class Sound:
stream.write_float(self.sound_max_dist)
stream.write_bool(self.sound_dist)
stream.write_bool(self.sound_loop)
stream.write_dword(self.sound_chance)
stream.write_long(self.sound_chance)
stream.write_float(self.sound_interval)
stream.write_dword(self.sound_reserved)
stream.write_long(self.sound_reserved)
def load_json(self, data):
self.sound_0 = data['sound_0']
+71 -106
View File
@@ -1,5 +1,5 @@
# • ▌ ▄ ·. ▄▄▄▄· ▄▄▄ .·▄▄▄▄ ▪ ▄▄▄▄▄ ▄▄▄ ▄▄▄·▄▄▄
# ·██ ▐███▪▐█ ▀█▪ ▀▄.▀·██▪ ██ ██ •██ ▪ ▀▄ █· ▐█ ▄█▀▄ █·▪
# • ▌ ▄ ·. ▄▄▄▄· ▄▄▄ .·▄▄▄▄ ▪ ▄▄▄▄▄ ▄▄▄ ▄▄▄·▄▄▄
# ·██ ▐███▪▐█ ▀█▪ ▀▄.▀·██▪ ██ ██ •██ ▪ ▀▄ █· ▐█ ▄█▀▄ █·▪
# ▐█ ▌▐▌▐█·▐█▀▀█▄ ▐▀▀▪▄▐█· ▐█▌▐█· ▐█.▪ ▄█▀▄ ▐▀▀▄ ██▀·▐▀▀▄ ▄█▀▄
# ██ ██▌▐█▌██▄▪▐█ ▐█▄▄▌██. ██ ▐█▌ ▐█▌·▐█▌.▐▌▐█•█▌ ▐█▪·•▐█•█▌▐█▌.▐▌
# ▀▀ █▪▀▀▀·▀▀▀▀ ▀▀▀ ▀▀▀▀▀• ▀▀▀ ▀▀▀ ▀█▄▀▪.▀ ▀ .▀ .▀ ▀ ▀█▄▀▪
@@ -15,11 +15,11 @@ from arcane.util import ResStream
class AttributeValue:
def load_binary(self, stream: ResStream):
self.attr_type = stream.read_dword()
self.attr_value = stream.read_dword()
self.attr_value = stream.read_long()
def save_binary(self, stream: ResStream):
stream.write_dword(self.attr_type)
stream.write_dword(self.attr_value)
stream.write_long(self.attr_value)
def load_json(self, data):
self.attr_type = string_to_hash(data['attr_type'])
@@ -35,13 +35,13 @@ class AttributeValue:
class PowerArgument:
def load_binary(self, stream: ResStream):
self.power_arguments = [
stream.read_dword(),
stream.read_dword(),
stream.read_long(),
stream.read_long(),
]
def save_binary(self, stream: ResStream):
stream.write_dword(self.power_arguments[0])
stream.write_dword(self.power_arguments[1])
stream.write_long(self.power_arguments[0])
stream.write_long(self.power_arguments[1])
def load_json(self, data):
self.power_arguments = data['power_arguments']
@@ -55,11 +55,11 @@ class PowerArgument:
class SkillLevel:
def load_binary(self, stream: ResStream):
self.skill_type = stream.read_dword()
self.skill_level = stream.read_dword()
self.skill_level = stream.read_long()
def save_binary(self, stream: ResStream):
stream.write_dword(self.skill_type)
stream.write_dword(self.skill_level)
stream.write_long(self.skill_level)
def load_json(self, data):
self.skill_type = string_to_hash(data['skill_type'])
@@ -75,11 +75,11 @@ class SkillLevel:
class ResourceInfo:
def load_binary(self, stream: ResStream):
self.resource_type = stream.read_dword()
self.resource_value = stream.read_dword()
self.resource_value = stream.read_long()
def save_binary(self, stream: ResStream):
stream.write_dword(self.resource_type)
stream.write_dword(self.resource_value)
stream.write_long(self.resource_value)
def load_json(self, data):
self.resource_type = string_to_hash(data['resource_type'])
@@ -124,31 +124,44 @@ class PowerAction:
return data
def _read_granted_pairs(stream: ResStream):
num_pairs = stream.read_dword()
return [
[
stream.read_dword(),
stream.read_long(),
] for _ in range(num_pairs)
]
def _write_granted_pairs(stream: ResStream, pairs):
stream.write_dword(len(pairs))
for pair in pairs:
stream.write_dword(pair[0])
stream.write_long(pair[1])
def _hash_pair_keys(pairs):
pairs_copy = copy.deepcopy(pairs)
for pair in pairs_copy:
pair[0] = hash_to_string(pair[0])
return pairs_copy
def _unhash_pair_keys(pairs):
pairs_copy = copy.deepcopy(pairs)
for pair in pairs_copy:
pair[0] = string_to_hash(pair[0])
return pairs_copy
class PowerGrant:
def load_binary(self, stream: ResStream):
self.power_type = stream.read_dword()
self.power_value = stream.read_dword()
num_attrs = stream.read_dword()
self.power_granted_attrs = [
[
stream.read_dword(),
stream.read_dword(),
] for _ in range(num_attrs)
]
num_skills = stream.read_dword()
self.power_granted_skills = [
[
stream.read_dword(),
stream.read_dword(),
] for _ in range(num_skills)
]
num_powers = stream.read_dword()
self.power_granted_powers = [
[
stream.read_dword(),
stream.read_dword(),
] for _ in range(num_powers)
]
self.power_value = stream.read_long()
self.power_granted_attrs = _read_granted_pairs(stream)
self.power_granted_skills = _read_granted_pairs(stream)
self.power_granted_powers = _read_granted_pairs(stream)
num_monster_types = stream.read_dword()
self.power_monster_types = [
stream.read_dword() for _ in range(num_monster_types)
@@ -156,19 +169,10 @@ class PowerGrant:
def save_binary(self, stream: ResStream):
stream.write_dword(self.power_type)
stream.write_dword(self.power_value)
stream.write_dword(len(self.power_granted_attrs))
for attr in self.power_granted_attrs:
stream.write_dword(attr[0])
stream.write_dword(attr[1])
stream.write_dword(len(self.power_granted_skills))
for skill in self.power_granted_skills:
stream.write_dword(skill[0])
stream.write_dword(skill[1])
stream.write_dword(len(self.power_granted_powers))
for power in self.power_granted_powers:
stream.write_dword(power[0])
stream.write_dword(power[1])
stream.write_long(self.power_value)
_write_granted_pairs(stream, self.power_granted_attrs)
_write_granted_pairs(stream, self.power_granted_skills)
_write_granted_pairs(stream, self.power_granted_powers)
stream.write_dword(len(self.power_monster_types))
for value in self.power_monster_types:
stream.write_dword(value)
@@ -176,24 +180,18 @@ class PowerGrant:
def load_json(self, data):
self.power_type = string_to_hash(data['power_type'])
self.power_value = data['power_value']
self.power_granted_attrs = data['power_granted_attrs']
grants_copy = data['power_granted_skills']
for subarr in grants_copy:
subarr[0] = string_to_hash(subarr[0])
self.power_granted_skills = grants_copy
self.power_granted_powers = data['power_granted_powers']
self.power_granted_attrs = _unhash_pair_keys(data['power_granted_attrs'])
self.power_granted_skills = _unhash_pair_keys(data['power_granted_skills'])
self.power_granted_powers = _unhash_pair_keys(data['power_granted_powers'])
self.power_monster_types = data['power_monster_types']
def save_json(self):
data = OrderedDict()
data['power_type'] = hash_to_string(self.power_type)
data['power_value'] = self.power_value
data['power_granted_attrs'] = self.power_granted_attrs
grants_copy = copy.deepcopy(self.power_granted_skills)
for subarr in grants_copy:
subarr[0] = hash_to_string(subarr[0])
data['power_granted_skills'] = grants_copy
data['power_granted_powers'] = self.power_granted_powers
data['power_granted_attrs'] = _hash_pair_keys(self.power_granted_attrs)
data['power_granted_skills'] = _hash_pair_keys(self.power_granted_skills)
data['power_granted_powers'] = _hash_pair_keys(self.power_granted_powers)
data['power_monster_types'] = self.power_monster_types
return data
@@ -201,28 +199,10 @@ class PowerGrant:
class SkillGrant:
def load_binary(self, stream: ResStream):
self.skill_type = stream.read_dword()
self.skill_value = stream.read_dword()
num_attrs = stream.read_dword()
self.skill_granted_attrs = [
[
stream.read_dword(),
stream.read_dword(),
] for _ in range(num_attrs)
]
num_skills = stream.read_dword()
self.skill_granted_skills = [
[
stream.read_dword(),
stream.read_dword(),
] for _ in range(num_skills)
]
num_powers = stream.read_dword()
self.skill_granted_powers = [
[
stream.read_dword(),
stream.read_dword(),
] for _ in range(num_powers)
]
self.skill_value = stream.read_long()
self.skill_granted_attrs = _read_granted_pairs(stream)
self.skill_granted_skills = _read_granted_pairs(stream)
self.skill_granted_powers = _read_granted_pairs(stream)
num_monster_types = stream.read_dword()
self.skill_monster_types = [
stream.read_dword() for _ in range(num_monster_types)
@@ -230,19 +210,10 @@ class SkillGrant:
def save_binary(self, stream: ResStream):
stream.write_dword(self.skill_type)
stream.write_dword(self.skill_value)
stream.write_dword(len(self.skill_granted_attrs))
for attr in self.skill_granted_attrs:
stream.write_dword(attr[0])
stream.write_dword(attr[1])
stream.write_dword(len(self.skill_granted_skills))
for skill in self.skill_granted_skills:
stream.write_dword(skill[0])
stream.write_dword(skill[1])
stream.write_dword(len(self.skill_granted_powers))
for power in self.skill_granted_powers:
stream.write_dword(power[0])
stream.write_dword(power[1])
stream.write_long(self.skill_value)
_write_granted_pairs(stream, self.skill_granted_attrs)
_write_granted_pairs(stream, self.skill_granted_skills)
_write_granted_pairs(stream, self.skill_granted_powers)
stream.write_dword(len(self.skill_monster_types))
for value in self.skill_monster_types:
stream.write_dword(value)
@@ -250,23 +221,17 @@ class SkillGrant:
def load_json(self, data):
self.skill_type = string_to_hash(data['skill_type'])
self.skill_value = data['skill_value']
self.skill_granted_attrs = data['skill_granted_attrs']
grants_copy = data['skill_granted_skills']
for subarr in grants_copy:
subarr[0] = string_to_hash(subarr[0])
self.skill_granted_skills = grants_copy
self.skill_granted_powers = data['skill_granted_powers']
self.skill_granted_attrs = _unhash_pair_keys(data['skill_granted_attrs'])
self.skill_granted_skills = _unhash_pair_keys(data['skill_granted_skills'])
self.skill_granted_powers = _unhash_pair_keys(data['skill_granted_powers'])
self.skill_monster_types = data['skill_monster_types']
def save_json(self):
data = OrderedDict()
data['skill_type'] = hash_to_string(self.skill_type)
data['skill_value'] = self.skill_value
data['skill_granted_attrs'] = self.skill_granted_attrs
grants_copy = copy.deepcopy(self.skill_granted_skills)
for subarr in grants_copy:
subarr[0] = hash_to_string(subarr[0])
data['skill_granted_skills'] = grants_copy
data['skill_granted_powers'] = self.skill_granted_powers
data['skill_granted_attrs'] = _hash_pair_keys(self.skill_granted_attrs)
data['skill_granted_skills'] = _hash_pair_keys(self.skill_granted_skills)
data['skill_granted_powers'] = _hash_pair_keys(self.skill_granted_powers)
data['skill_monster_types'] = self.skill_monster_types
return data
@@ -30,7 +30,7 @@ class Inventory:
elif self.inventory_type == INVENTORY_TYPE_BOOTYTABLE:
self.inventory_table = [
stream.read_float(),
stream.read_dword(),
stream.read_long(),
]
def save_binary(self, stream: ResStream):
@@ -44,7 +44,7 @@ class Inventory:
stream.write_qword(self.inventory_items[1])
elif self.inventory_type == INVENTORY_TYPE_BOOTYTABLE:
stream.write_float(self.inventory_table[0])
stream.write_dword(self.inventory_table[1])
stream.write_long(self.inventory_table[1])
def load_json(self, data):
self.inventory_type = STRING_TO_INVENTORY_TYPE[data['type']]
@@ -23,7 +23,7 @@ class SparseData:
value = None
if sparse_type == SPARSE_VAL_LONG:
value = stream.read_dword()
value = stream.read_long()
elif sparse_type == SPARSE_VAL_FLOAT:
value = stream.read_float()
@@ -65,7 +65,7 @@ class SparseData:
sparse_type = SPARSE_TAG_TO_SPARSE_TYPE[tag]
if sparse_type == SPARSE_VAL_LONG:
stream.write_dword(value)
stream.write_long(value)
elif sparse_type == SPARSE_VAL_FLOAT:
stream.write_float(value)
@@ -85,7 +85,7 @@ class SparseData:
elif sparse_type == SPARSE_REF_MERCHANT_DATA:
stream.write_qword(value[0])
stream.write_qword(value[1])
stream.write_qword(value[2])
stream.write_dword(value[2])
elif sparse_type == SPARSE_REF_ARC_CACHE_ID:
stream.write_qword(value)
@@ -6,6 +6,7 @@
# Magicbane Emulator Project © 2013 - 2022
# www.magicbane.com
import os
import struct
import zlib
@@ -81,7 +82,21 @@ def write_bytes(cache_file, value):
cache_file.write(value)
def next_available_path(path):
# Returns path if unused, otherwise the first free path.1, path.2, ...
if not os.path.exists(path):
return path
index = 1
while os.path.exists(f'{path}.{index}'):
index += 1
return f'{path}.{index}'
def save_cache_file(filename, resources, compress=True):
filename = next_available_path(filename)
print('Packing to ' + filename)
cache_file = open(filename, 'wb')
num_resources = len(resources)
+11
View File
@@ -30,6 +30,10 @@ class ResStream:
data = self.buffer.read(4)
return struct.unpack('<I', data)[0]
def read_long(self):
data = self.buffer.read(4)
return struct.unpack('<i', data)[0]
def read_qword(self):
dwords = struct.unpack('<II', self.buffer.read(8))
buffer = struct.pack('<II', dwords[1], dwords[0])
@@ -72,6 +76,13 @@ class ResStream:
data = struct.pack('<I', value)
self.buffer.write(data)
def write_long(self, value):
# Older dumps stored signed fields as unsigned; both forms are the same bytes.
if 0x7FFFFFFF < value <= 0xFFFFFFFF:
value -= 0x100000000
data = struct.pack('<i', value)
self.buffer.write(data)
def write_qword(self, value):
qword = struct.pack('<Q', value)
dwords = [
+1 -1
View File
@@ -6,5 +6,5 @@
# Magicbane Emulator Project © 2013 - 2022
# www.magicbane.com
from .ArcFileCache import load_cache_file, save_cache_file
from .ArcFileCache import load_cache_file, save_cache_file, next_available_path
from .ResStream import ResStream
+44 -44
View File
@@ -89,6 +89,14 @@ EDITOR_TARGETS = [
]
def create_unpack_directory(base_directory):
# Never unpack over an existing folder; use the next free numbered name.
directory = next_available_path(base_directory)
os.makedirs(directory)
print('Unpacking to ' + directory)
return directory
def init_cobjects():
for obj_type in COBJECTS_MAP:
directory_path = os.path.join(COBJECTS_DIRECTORY, OBJECT_TYPE_TO_STRING[obj_type])
@@ -146,9 +154,11 @@ def init_render():
def unpack_cobjects():
init_cobjects()
resources = load_cache_file('CObjects.cache')
directory = create_unpack_directory(COBJECTS_DIRECTORY)
for obj_type in COBJECTS_MAP:
os.makedirs(os.path.join(directory, OBJECT_TYPE_TO_STRING[obj_type]))
with tqdm(total=len(resources)) as pBar:
for res_id, data in resources:
@@ -158,7 +168,7 @@ def unpack_cobjects():
arc_in = COBJECTS_MAP[obj_type]()
filepath = os.path.join(
COBJECTS_DIRECTORY,
directory,
OBJECT_TYPE_TO_STRING[obj_type],
f'{res_id:d}.json'
)
@@ -193,7 +203,7 @@ def pack_cobjects():
resources.append([res_id, out_stream.get_bytes()])
save_cache_file('CObjects.cache.new', resources)
save_cache_file('CObjects.cache', resources)
def test_cobjects():
@@ -221,16 +231,15 @@ def test_cobjects():
def unpack_czones():
init_czones()
resources = load_cache_file('CZone.cache')
directory = create_unpack_directory(CZONE_DIRECTORY)
with tqdm(total=len(resources)) as pBar:
for res_id, data in resources:
arc_zone = ArcZone()
in_stream = ResStream(data)
filepath = os.path.join(
CZONE_DIRECTORY,
directory,
f'{res_id:d}.json'
)
arc_zone.load_binary(in_stream)
@@ -257,7 +266,7 @@ def pack_czones():
resources.append([res_id, out_stream.get_bytes()])
save_cache_file('CZone.cache.new', resources)
save_cache_file('CZone.cache', resources)
def test_czones():
@@ -280,16 +289,15 @@ def test_czones():
def unpack_sound():
init_sound()
resources = load_cache_file('Sound.cache')
directory = create_unpack_directory(SOUND_DIRECTORY)
with tqdm(total=len(resources)) as pBar:
for res_id, data in resources:
arc_sound = ArcSound()
in_stream = ResStream(data)
filepath = os.path.join(
SOUND_DIRECTORY,
directory,
f'{res_id:d}.wav'
)
@@ -319,7 +327,7 @@ def pack_sound():
resources.append([res_id, out_stream.get_bytes()])
save_cache_file('Sound.cache.new', resources, False)
save_cache_file('Sound.cache', resources, False)
def test_sound():
@@ -350,16 +358,15 @@ def test_sound():
def unpack_texture():
init_texture()
print('Decompressing Texture Cache...')
resources = load_cache_file('Textures.cache')
directory = create_unpack_directory(TEXTURE_DIRECTORY)
print('Writing images')
with tqdm(total=len(resources)) as pBar:
for res_id, data in resources:
filepath = os.path.join(
TEXTURE_DIRECTORY,
directory,
f'{res_id:d}.tga'
)
@@ -386,7 +393,7 @@ def pack_texture():
resources.append([res_id, out_stream.get_bytes()])
save_cache_file('Textures.cache.new', resources)
save_cache_file('Textures.cache', resources)
def test_texture():
@@ -418,14 +425,13 @@ def test_texture():
def unpack_terrain():
init_terrain()
resources = load_cache_file('TerrainAlpha.cache')
directory = create_unpack_directory(TERRAIN_DIRECTORY)
with tqdm(total=len(resources)) as pBar:
for res_id, data in resources:
filepath = os.path.join(
TERRAIN_DIRECTORY,
directory,
f'{res_id:d}.tga'
)
@@ -452,7 +458,7 @@ def pack_terrain():
resources.append([res_id, out_stream.get_bytes()])
save_cache_file('TerrainAlpha.cache.new', resources)
save_cache_file('TerrainAlpha.cache', resources)
def test_terrain():
@@ -486,16 +492,15 @@ def test_terrain():
def unpack_mesh():
init_mesh()
resources = load_cache_file('Mesh.cache')
directory = create_unpack_directory(MESH_DIRECTORY)
with tqdm(total=len(resources)) as pBar:
for res_id, data in resources:
arc_mesh = ArcMesh()
in_stream = ResStream(data)
filepath = os.path.join(
MESH_DIRECTORY,
directory,
f'{res_id:d}.json'
)
arc_mesh.load_binary(in_stream)
@@ -522,7 +527,7 @@ def pack_mesh():
resources.append([res_id, out_stream.get_bytes()])
save_cache_file('Mesh.cache.new', resources)
save_cache_file('Mesh.cache', resources)
def test_mesh():
@@ -545,16 +550,15 @@ def test_mesh():
def unpack_visual():
init_visual()
resources = load_cache_file('Visual.cache')
directory = create_unpack_directory(VISUAL_DIRECTORY)
with tqdm(total=len(resources)) as pBar:
for res_id, data in resources:
arc_visual = ArcVisual()
in_stream = ResStream(data)
filepath = os.path.join(
VISUAL_DIRECTORY,
directory,
f'{res_id:d}.json'
)
arc_visual.load_binary(in_stream)
@@ -581,7 +585,7 @@ def pack_visual():
resources.append([res_id, out_stream.get_bytes()])
save_cache_file('Visual.cache.new', resources)
save_cache_file('Visual.cache', resources)
def test_visual():
@@ -604,16 +608,15 @@ def test_visual():
def unpack_motion():
init_motion()
resources = load_cache_file('Motion.cache')
directory = create_unpack_directory(MOTION_DIRECTORY)
with tqdm(total=len(resources)) as pBar:
for res_id, data in resources:
arc_motion = ArcMotion()
in_stream = ResStream(data)
filepath = os.path.join(
MOTION_DIRECTORY,
directory,
f'{res_id:d}.json'
)
arc_motion.load_binary(in_stream)
@@ -640,7 +643,7 @@ def pack_motion():
resources.append([res_id, out_stream.get_bytes()])
save_cache_file('Motion.cache.new', resources)
save_cache_file('Motion.cache', resources)
def test_motion():
@@ -663,16 +666,15 @@ def test_motion():
def unpack_tile():
init_tile()
resources = load_cache_file('Tile.cache')
directory = create_unpack_directory(TILE_DIRECTORY)
with tqdm(total=len(resources)) as pBar:
for res_id, data in resources:
arc_tile = ArcTileManager()
in_stream = ResStream(data)
filepath = os.path.join(
TILE_DIRECTORY,
directory,
f'{res_id:d}.json'
)
arc_tile.load_binary(in_stream)
@@ -699,7 +701,7 @@ def pack_tile():
resources.append([res_id, out_stream.get_bytes()])
save_cache_file('Tile.cache.new', resources)
save_cache_file('Tile.cache', resources)
def test_tile():
@@ -722,16 +724,15 @@ def test_tile():
def unpack_skeleton():
init_skeleton()
resources = load_cache_file('Skeleton.cache')
directory = create_unpack_directory(SKELETON_DIRECTORY)
with tqdm(total=len(resources)) as pBar:
for res_id, data in resources:
arc_skeleton = ArcSkeleton()
in_stream = ResStream(data)
filepath = os.path.join(
SKELETON_DIRECTORY,
directory,
f'{res_id:d}.json'
)
arc_skeleton.load_binary(in_stream)
@@ -759,7 +760,7 @@ def pack_skeleton():
resources.append([res_id, out_stream.get_bytes()])
save_cache_file('Skeleton.cache.new', resources)
save_cache_file('Skeleton.cache', resources)
def test_skeleton():
@@ -782,16 +783,15 @@ def test_skeleton():
def unpack_render():
init_render()
resources = load_cache_file('render.cache')
directory = create_unpack_directory(RENDER_DIRECTORY)
with tqdm(total=len(resources)) as pBar:
for res_id, data in resources:
arc_render = ArcRender()
in_stream = ResStream(data)
filepath = os.path.join(
RENDER_DIRECTORY,
directory,
f'{res_id:d}.json'
)
arc_render.load_binary(in_stream)
@@ -819,7 +819,7 @@ def pack_render():
resources.append([res_id, out_stream.get_bytes()])
save_cache_file('Render.cache.new', resources)
save_cache_file('Render.cache', resources)
def test_render():