Compare commits

20 Commits
Author SHA1 Message Date
MagicBotandClaude Opus 5.5 c07073cc09 Merge READMEs into the root README
Bring the wpak documentation into the root README and remove the
duplicate copy under mbEditorPro2.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:05:37 -04:00
MagicBotandClaude Opus 5.5 1e4ab7ec36 Ignore wpak files, backups and ARCANE_DUMP output
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:03:00 -04:00
MagicBotandClaude Opus 5.5 44cf72b394 Add .gitignore for IDE, cache and bytecode folders
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:02:33 -04:00
MagicBotandClaude Opus 5.5 72e8b4811c Document wpak target in README
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 11:55:10 -04:00
MagicBotandClaude Opus 5.5 b2e38bdc06 Support all .wpak files with a wpak target
Replaces the config target with wpak, which handles every known pak or a
single one named on the command line. Map icon .tga entries are stored
unencrypted (sb.exe 0xAACA60).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 11:55:10 -04:00
MagicBotandClaude Opus 5.5 8a8ebc5632 Document wpak support in README
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 11:22:52 -04:00
MagicBotandClaude Opus 5.5 a312c9270a Add Config.wpak unpack, pack and test
A .wpak is a zip whose entries are each Blowfish CFB-64 encrypted with
a zero IV and the client's hardcoded key (sb.exe 0x54DEC0). Pure Python
Blowfish, no new dependencies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 11:22:52 -04:00
MagicBotandClaude Opus 5.5 a1ed572aca Fix item use flag bit order to match client
The client's use flag name table (sb.exe 0x16C9728) has None at bit 0,
so USE_SELF is bit 1 through EMPTY_NEWITEM at bit 4. Names in the JSON
were one bit low.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 09:47:38 -04:00
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
23 changed files with 622 additions and 422 deletions

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
.idea/
cachefiles/
__pycache__/
*.wpak
*.wpak.*
*.old
ARCANE_DUMP/
+19 -3
View File
@@ -18,6 +18,7 @@ Team Magicbane presents to the community technology allowing MagicBox administra
to fully modify their game using nothing more than notepad.exe. <br>
- Tool directly converts Shadowbane .cache file data to and from a high fidelity pure JSON representation.
- Shadowbane .wpak archives are decrypted to and from their plain text config files.
- Media such as textures and sound are converted in-situ.
- Enumerations are used throughout hiding from the user ugly signed ulong values.
- Written in Python and open-source allowing modification and extension with ease.
@@ -38,7 +39,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 +48,21 @@ 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>
### wpak files
```
mbEditorPro.py unpack wpak
mbEditorPro.py unpack wpak Config
mbEditorPro.py pack wpak
mbEditorPro.py pack wpak Config
```
A .wpak is a zip archive whose entries are each Blowfish encrypted (.tga icons are stored unencrypted).
Unpack decrypts each pak into **ARCANE_DUMP/<name>** (e.g. ARCANE_DUMP/Config), one file per entry.
Pack encrypts each folder back into <name>.wpak. Name a single pak to work on just that one.
The same numbering rules as the caches apply. <br>
### Limitations
@@ -63,10 +80,9 @@ The tool generating pure JSON has the following benefits:
### Future
mbEditorPro will be the cornerstone of Magicbane's data project. The Team's goals are threefold:
mbEditorPro will be the cornerstone of Magicbane's data project. The Team's goals are twofold:
- Ship MagicBox 1.5 with both Aerynth and Vorringia mapsets.
- Integrate Magicbane's wpak extractor technology into the tool.
- Use data for a detailed audit of Magicbane's database.<br>
### More Information
-75
View File
@@ -1,75 +0,0 @@
```
• ▌ ▄ ·. ▄▄▄▄· ▄▄▄ .·▄▄▄▄ ▪ ▄▄▄▄▄ ▄▄▄ ▄▄▄·▄▄▄
·██ ▐███▪▐█ ▀█▪ ▀▄.▀·██▪ ██ ██ •██ ▪ ▀▄ █· ▐█ ▄█▀▄ █·▪
▐█ ▌▐▌▐█·▐█▀▀█▄ ▐▀▀▪▄▐█· ▐█▌▐█· ▐█.▪ ▄█▀▄ ▐▀▀▄ ██▀·▐▀▀▄ ▄█▀▄
██ ██▌▐█▌██▄▪▐█ ▐█▄▄▌██. ██ ▐█▌ ▐█▌·▐█▌.▐▌▐█•█▌ ▐█▪·•▐█•█▌▐█▌.▐▌
▀▀ █▪▀▀▀·▀▀▀▀ ▀▀▀ ▀▀▀▀▀• ▀▀▀ ▀▀▀ ▀█▄▀▪.▀ ▀ .▀ .▀ ▀ ▀█▄▀▪
Magicbane Emulator Project © 2013 - 2022
www.magicbane.com
```
# mbEditorPro v 2.0
![Example JSON](https://cdn.discordapp.com/attachments/399623779531096074/1029063849640468550/unknown.png)<br>
![Example Textures](https://cdn.discordapp.com/attachments/534260038039109632/1056584030310641696/image.png)<br>
![Example Usage](https://cdn.discordapp.com/attachments/398522378545790976/1058787471544025138/image.png)<br>
Team Magicbane presents to the community technology allowing MagicBox administrators<br>
to fully modify their game using nothing more than notepad.exe. <br>
- Tool directly converts Shadowbane .cache file data to and from a high fidelity pure JSON representation.
- Media such as textures and sound are converted in-situ.
- Enumerations are used throughout hiding from the user ugly signed ulong values.
- Written in Python and open-source allowing modification and extension with ease.
- Want to raise all weapon damage by x%; Just modify item values as they are being parsed!
The tool built with Eldritch Technologies parses **all Shadowbane .cache files** giving the user complete control over the game<br>
Team Magicbane aims to deliver you the best crackhead free MMO technology around; all for free!
### Requirements:
mbEditorPro requires pillow for image manipulation and tqdm for metrics.
```$ pip install -r requirements.txt```
### Usage:
```
mbEditorPro.py unpack cobjects
mbEditorPro.py unpack all
```
JSON output will be placed into the folder **ARCANE DUMP** <br>
```
mbEditorPro.py pack textures
mbEditorPro.py pack all
```
New binary cache file(s) will be built from the JSON and output into the current directory. <br>
### Limitations
Hirelings, ZoneEvents and Mobile Inventory are not parsed or written as there were no <br>
examples in the .cache files to begin with. The game does seem to support them, however.
### JSON
The tool generating pure JSON has the following benefits:
- It's ubiquitous and the tooling is mature. The standard interop exchange format.
- Human and machine-readable.
- Ease of importing into MySQL or other databases.
- Use as an intermediate representation to build a GUI World Editor or FBX importer.
### Future
mbEditorPro will be the cornerstone of Magicbane's data project. The Team's goals are threefold:
- Ship MagicBox 1.5 with both Aerynth and Vorringia mapsets.
- Integrate Magicbane's wpak extractor technology into the tool.
- Use data for a detailed audit of Magicbane's database.<br>
### More Information
See the mbEditorPro2.0 Wiki for more detailed information.
+7 -6
View File
@@ -218,12 +218,13 @@ STRING_TO_ITEM_TYPE = {
'REALMCHARTER': ITEM_TYPE_REALMCHARTER,
}
ITEM_USE_FLAGS_NONE = 0
ITEM_USE_FLAGS_USE_SELF = 1 << 0
ITEM_USE_FLAGS_USE_TARGET = 1 << 1
ITEM_USE_FLAGS_EMPTY_DESTROY = 1 << 2
ITEM_USE_FLAGS_EMPTY_NEWITEM = 1 << 3
ITEM_USE_FLAGS_NUM = 4
# Bit order from the client's use flag name table (sb.exe 0x16C9728)
ITEM_USE_FLAGS_NONE = 1 << 0
ITEM_USE_FLAGS_USE_SELF = 1 << 1
ITEM_USE_FLAGS_USE_TARGET = 1 << 2
ITEM_USE_FLAGS_EMPTY_DESTROY = 1 << 3
ITEM_USE_FLAGS_EMPTY_NEWITEM = 1 << 4
ITEM_USE_FLAGS_NUM = 5
ITEM_USE_FLAGS_TO_STRING = {
ITEM_USE_FLAGS_NONE: 'NONE',
@@ -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)
+184
View File
@@ -0,0 +1,184 @@
# • ▌ ▄ ·. ▄▄▄▄· ▄▄▄ .·▄▄▄▄ ▪ ▄▄▄▄▄ ▄▄▄ ▄▄▄·▄▄▄
# ·██ ▐███▪▐█ ▀█▪ ▀▄.▀·██▪ ██ ██ •██ ▪ ▀▄ █· ▐█ ▄█▀▄ █·▪
# ▐█ ▌▐▌▐█·▐█▀▀█▄ ▐▀▀▪▄▐█· ▐█▌▐█· ▐█.▪ ▄█▀▄ ▐▀▀▄ ██▀·▐▀▀▄ ▄█▀▄
# ██ ██▌▐█▌██▄▪▐█ ▐█▄▄▌██. ██ ▐█▌ ▐█▌·▐█▌.▐▌▐█•█▌ ▐█▪·•▐█•█▌▐█▌.▐▌
# ▀▀ █▪▀▀▀·▀▀▀▀ ▀▀▀ ▀▀▀▀▀• ▀▀▀ ▀▀▀ ▀█▄▀▪.▀ ▀ .▀ .▀ ▀ ▀█▄▀▪
# Magicbane Emulator Project © 2013 - 2022
# www.magicbane.com
import os
import time
import zipfile
from .ArcFileCache import next_available_path
# A .wpak is a zip (client uses minizip, ArcWPakFile 0x54D3C0) whose entries are
# each Blowfish CFB-64 encrypted with a zero IV (BF_cfb64_encrypt 0xCD9000),
# except image entries.
WPAK_KEY = bytes([
0x85, 0x71, 0x40, 0x3C, 0x14, 0x50, 0x0B, 0x52,
0x73, 0x2D, 0x10, 0x08, 0x63, 0x59, 0x5B, 0xAA,
]) # hardcoded at 0x54DEC0
WPAK_COMPRESS_LEVEL = 9 # zipOpenNewFileInZip level in ArcWPakFile writer
# Map icon paks keep their .tga entries unencrypted (LoadMapZoneData 0xAACA60).
WPAK_PLAIN_EXTENSIONS = ('.tga',)
MASK32 = 0xFFFFFFFF
def pi_fraction_words(count):
# Blowfish's initial P-array and S-boxes are the hex digits of pi's fraction.
guard_bits = 64
bits = count * 32 + guard_bits
one = 1 << bits
def arctan_inverse(x):
x_squared = x * x
term = one // x
total = term
sign = -1
n = 1
while term:
term //= x_squared
total += sign * (term // (2 * n + 1))
sign = -sign
n += 1
return total
pi = 4 * (4 * arctan_inverse(5) - arctan_inverse(239))
fraction = (pi - (3 << bits)) >> guard_bits
return [(fraction >> (32 * (count - 1 - i))) & MASK32 for i in range(count)]
class ArcBlowfish:
p_array = None
s_boxes = None
def __init__(self, key):
words = pi_fraction_words(18 + 4 * 256)
self.p_array = words[:18]
self.s_boxes = [words[18 + 256 * i: 18 + 256 * (i + 1)] for i in range(4)]
self.set_key(key)
def set_key(self, key):
left = 0
right = 0
for i in range(18):
key_word = 0
for j in range(4):
key_word = (key_word << 8) | key[(i * 4 + j) % len(key)]
self.p_array[i] ^= key_word
for i in range(0, 18, 2):
left, right = self.encrypt_block(left, right)
self.p_array[i] = left
self.p_array[i + 1] = right
for s_box in self.s_boxes:
for i in range(0, 256, 2):
left, right = self.encrypt_block(left, right)
s_box[i] = left
s_box[i + 1] = right
def encrypt_block(self, left, right):
p_array = self.p_array
s0, s1, s2, s3 = self.s_boxes
for i in range(16):
left ^= p_array[i]
feistel = ((s0[left >> 24] + s1[(left >> 16) & 0xFF]) & MASK32) ^ s2[(left >> 8) & 0xFF]
right ^= (feistel + s3[left & 0xFF]) & MASK32
left, right = right, left
return right ^ p_array[17], left ^ p_array[16]
def keystream_block(self, iv):
left, right = self.encrypt_block(
int.from_bytes(iv[:4], 'big'),
int.from_bytes(iv[4:], 'big')
)
return left.to_bytes(4, 'big') + right.to_bytes(4, 'big')
def cfb64_decrypt(self, data):
output = bytearray(len(data))
iv = bytes(8)
for offset in range(0, len(data), 8):
keystream = self.keystream_block(iv)
cipher_block = data[offset:offset + 8]
for i, cipher_byte in enumerate(cipher_block):
output[offset + i] = cipher_byte ^ keystream[i]
iv = cipher_block
return bytes(output)
def cfb64_encrypt(self, data):
output = bytearray(len(data))
iv = bytes(8)
for offset in range(0, len(data), 8):
keystream = self.keystream_block(iv)
plain_block = data[offset:offset + 8]
for i, plain_byte in enumerate(plain_block):
output[offset + i] = plain_byte ^ keystream[i]
iv = bytes(output[offset:offset + 8])
return bytes(output)
WPAK_CIPHER = ArcBlowfish(WPAK_KEY)
def is_encrypted_entry(filename):
return not filename.lower().endswith(WPAK_PLAIN_EXTENSIONS)
def load_wpak_file(filename):
entries = []
with zipfile.ZipFile(filename) as wpak_file:
for info in wpak_file.infolist():
if info.is_dir():
continue
data = wpak_file.read(info)
if is_encrypted_entry(info.filename):
data = WPAK_CIPHER.cfb64_decrypt(data)
entries.append([info.filename, data])
return entries
def load_wpak_directory(directory):
entries = []
# Client finds entries by name (unzLocateFile 0xCD7920), so order only needs to be stable.
for filename in sorted(os.listdir(directory), key=str.upper):
filepath = os.path.join(directory, filename)
if not os.path.isfile(filepath):
continue
with open(filepath, 'rb') as fp:
entries.append([filename, fp.read()])
return entries
def build_wpak_bytes(entries, wpak_file):
with zipfile.ZipFile(wpak_file, 'w') as archive:
for filename, data in entries:
info = zipfile.ZipInfo(filename, date_time=time.localtime()[:6])
info.compress_type = zipfile.ZIP_DEFLATED
if is_encrypted_entry(filename):
data = WPAK_CIPHER.cfb64_encrypt(data)
archive.writestr(info, data, compresslevel=WPAK_COMPRESS_LEVEL)
def save_wpak_file(filename, entries):
filename = next_available_path(filename)
print('Packing to ' + filename)
with open(filename, 'wb') as wpak_file:
build_wpak_bytes(entries, wpak_file)
+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 = [
+2 -1
View File
@@ -6,5 +6,6 @@
# 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 .ArcWpak import load_wpak_file, load_wpak_directory, save_wpak_file, build_wpak_bytes
from .ResStream import ResStream
+116 -44
View File
@@ -85,9 +85,33 @@ EDITOR_TARGETS = [
'skeleton',
'terrain',
'render',
'wpak',
'all'
]
# Each pak unpacks to ARCANE_DUMP/<name> and packs back to <name>.wpak.
WPAK_NAMES = [
'AerynthIcons',
'Config',
'DalgothIcons',
'Environment',
'GeneralItemTables',
'ItemTables',
'ModTables',
'ModTypeTables',
'Transitions',
'VendorENGLISH',
'VorringiaIcons'
]
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:
@@ -146,9 +170,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 +184,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 +219,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 +247,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 +282,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 +305,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 +343,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 +374,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 +409,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 +441,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 +474,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 +508,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 +543,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 +566,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 +601,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 +624,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 +659,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 +682,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 +717,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 +740,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 +776,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 +799,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 +835,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():
@@ -841,6 +857,61 @@ def test_render():
print(res_id, in_stream.buffer.tell(), out_stream.buffer.tell())
def selected_wpak_names():
# An optional third argument picks a single pak, e.g. "unpack wpak Config".
return sys.argv[3:] or WPAK_NAMES
def unpack_wpak():
for wpak_name in selected_wpak_names():
wpak_filename = wpak_name + '.wpak'
if not os.path.isfile(wpak_filename):
print('***Skipping: ' + wpak_filename + ' not found.')
continue
entries = load_wpak_file(wpak_filename)
directory = create_unpack_directory(os.path.join(TARGET_DIRECTORY, wpak_name))
with tqdm(total=len(entries)) as pBar:
for filename, data in entries:
filepath = os.path.join(directory, filename)
with open(filepath, 'wb') as fp:
fp.write(data)
pBar.update(1)
def pack_wpak():
for wpak_name in selected_wpak_names():
directory = os.path.join(TARGET_DIRECTORY, wpak_name)
if not os.path.isdir(directory):
print('***Skipping: ' + directory + ' not found.')
continue
entries = load_wpak_directory(directory)
save_wpak_file(wpak_name + '.wpak', entries)
def test_wpak():
for wpak_name in selected_wpak_names():
wpak_filename = wpak_name + '.wpak'
if not os.path.isfile(wpak_filename):
print('***Skipping: ' + wpak_filename + ' not found.')
continue
print(wpak_filename)
entries = load_wpak_file(wpak_filename)
out_stream = io.BytesIO()
build_wpak_bytes(entries, out_stream)
repacked = dict(load_wpak_file(out_stream))
for filename, data in entries:
try:
assert repacked[filename] == data
except:
print(filename, len(data), len(repacked.get(filename, b'')))
def unpack_all():
for cache_type in EDITOR_TARGETS[:-1]:
method = 'unpack_' + cache_type + '()'
@@ -882,6 +953,7 @@ def usage():
print('')
print('example: unpack texture')
print('example: pack texture')
print('example: unpack wpak Config')
exit(1)