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>
This commit is contained in:
1 parent
ca4d2b2565
commit
50d8d76a21
1 file changed
+219
@@ -0,0 +1,219 @@
|
||||
```
|
||||
• ▌ ▄ ·. ▄▄▄▄· ▄▄▄ .·▄▄▄▄ ▪ ▄▄▄▄▄ ▄▄▄ ▄▄▄·▄▄▄
|
||||
·██ ▐███▪▐█ ▀█▪ ▀▄.▀·██▪ ██ ██ •██ ▪ ▀▄ █· ▐█ ▄█▀▄ █·▪
|
||||
▐█ ▌▐▌▐█·▐█▀▀█▄ ▐▀▀▪▄▐█· ▐█▌▐█· ▐█.▪ ▄█▀▄ ▐▀▀▄ ██▀·▐▀▀▄ ▄█▀▄
|
||||
██ ██▌▐█▌██▄▪▐█ ▐█▄▄▌██. ██ ▐█▌ ▐█▌·▐█▌.▐▌▐█•█▌ ▐█▪·•▐█•█▌▐█▌.▐▌
|
||||
▀▀ █▪▀▀▀·▀▀▀▀ ▀▀▀ ▀▀▀▀▀• ▀▀▀ ▀▀▀ ▀█▄▀▪.▀ ▀ .▀ .▀ ▀ ▀█▄▀▪
|
||||
Magicbane Emulator Project © 2013 - 2022
|
||||
www.magicbane.com
|
||||
```
|
||||
|
||||
# COBJECTS JSON format changes
|
||||
|
||||
This note covers the COBJECTS JSON produced by `unpack cobjects` after the
|
||||
client parser audit on branch `mbeditor-update`. It is written for anything
|
||||
that reads or writes that JSON: the server COBJECTS loader, database imports,
|
||||
the GUI, and hand edits.
|
||||
|
||||
The binary `.cache` format did not change. Every resource in
|
||||
`cachefiles/CObjects.cache` still round-trips byte for byte.
|
||||
|
||||
## Summary
|
||||
|
||||
| Change | Effect on the JSON | Action for readers |
|
||||
|---|---|---|
|
||||
| Signed 32-bit fields | Values that were `4294967295` are now `-1`, `4294967286` is now `-10`, and so on | Accept negative integers |
|
||||
| Grant attribute and power keys | First element of each pair is now a name string when the hash is known | Accept a string or an integer; convert names with `string_to_hash` |
|
||||
|
||||
Old JSON dumps are not fully compatible. `4294967295` and `-1` are the same
|
||||
four bytes, but the packer now writes signed fields with `struct.pack('<i')`,
|
||||
which rejects values above `2147483647` with `struct.error`. The sample dump has
|
||||
such values (`rank_hirelings`, `rank_rank`, `attr_value`). Re-run
|
||||
`unpack cobjects` to regenerate old dumps, or convert those values by
|
||||
subtracting `4294967296`, before packing. Integer grant keys in old dumps still
|
||||
pack unchanged.
|
||||
|
||||
## 1. Signed 32-bit fields
|
||||
|
||||
The client reads 4-byte integers through two entry points. Values read with
|
||||
`ResStream_ReadInt32` (sb.exe `0x7E1700`) are now loaded with
|
||||
`ResStream.read_long` and written with `write_long`. Fields the client reads
|
||||
through the folded ATL wrapper (`0x54C700`, used for hash and enum fields), list
|
||||
counts, and the item flag words (`0x7E1720`, not yet classified) stay unsigned.
|
||||
|
||||
Fields marked *(enum)* are written to JSON as a name, so their JSON text does
|
||||
not change.
|
||||
|
||||
### ArcObj (all object types)
|
||||
|
||||
| JSON path | Notes |
|
||||
|---|---|
|
||||
| `obj_sound_events[].sound_type` | *(enum)* |
|
||||
| `obj_arc_hardpoint_list[][0]`, `[][1]` | the two integers before the 10 floats |
|
||||
| `obj_sparse_data.<tag>` and `rune_sparse_data.<tag>` for `VAL_LONG` tags | client type `LoadSaveBehavior<long>`; tags: `ADMIN_SNOOPLEVEL`, `GEN_TABLE_ID`, `ADMIN_REMOVEEQUIPMENTEFFECTSLEVEL`, `trainerSkillCost`, `ADMIN_TOKENREADACCESSLEVEL`, `MAX_POWER_LEVEL`, `ADMIN_ACCESSLEVEL`, `banker`, `expLost`, `ADMIN_CHANNELACCESSLEVEL`, `ADMIN_TOKENWRITEACCESSLEVEL`, `ADMIN_REMOVEPOWEREFFECTSLEVEL` |
|
||||
|
||||
### ArcCombatObj (static, item, rune, character and subclasses)
|
||||
|
||||
| JSON path | Notes |
|
||||
|---|---|
|
||||
| `combat_powers[][1]` | value; `[0]` stays unsigned |
|
||||
|
||||
### ArcStaticObject
|
||||
|
||||
| JSON path |
|
||||
|---|
|
||||
| `static_sound.sound_chance`, `static_sound.sound_reserved` |
|
||||
|
||||
### ArcItem (also rune, container, deed, key)
|
||||
|
||||
| JSON path | Notes |
|
||||
|---|---|
|
||||
| `item_type` | *(enum)* |
|
||||
| `item_eq_slots_value` | |
|
||||
| `item_eq_slots_or`, `item_eq_slots_and` | *(bitmask written as names)* |
|
||||
| `item_value`, `item_wt` | |
|
||||
| `item_parry_anim_id` | |
|
||||
| `item_defense_rating` | armor only |
|
||||
| `item_initial_charges` | |
|
||||
| `item_level_req`, `item_rank_req` | |
|
||||
| `item_sex_req` | *(enum)* |
|
||||
| `item_sheath_slot` | *(enum)* |
|
||||
| `item_bane_rank` | |
|
||||
| `item_user_power_action[].arguments[]` | `power` stays a hash |
|
||||
| `item_skill_req[].skill_level` | |
|
||||
| `item_attr_req[].attr_value` | |
|
||||
| `item_resource_costs[].resource_value` | |
|
||||
| `item_offering_info[].offering_value`, `item_offering_adjustments[].offering_value` | |
|
||||
| `item_power_action[].power_actions[].power_arguments[]` | |
|
||||
| `item_power_grant[]` | see section 2 |
|
||||
| `item_weapon.weapon_combat_idle_anim` | |
|
||||
| `item_weapon.weapon_damage[].damage_type` | *(enum)* |
|
||||
| `item_weapon.weapon_damage[].damage_min`, `damage_max` | |
|
||||
| `item_weapon.weapon_attack_anim_right[][]`, `weapon_attack_anim_left[][]` | |
|
||||
|
||||
### ArcRune
|
||||
|
||||
| JSON path | Notes |
|
||||
|---|---|
|
||||
| `rune_type` | *(enum)* |
|
||||
| `rune_sex` | *(enum)* |
|
||||
| `rune_creation_cost`, `rune_rank`, `rune_pracs_per_level` | |
|
||||
| `rune_health`, `rune_mana`, `rune_stamina` | |
|
||||
| `rune_attack`, `rune_defense`, `rune_level` | |
|
||||
| `rune_enemy_gender[]` | |
|
||||
| `rune_skill_adj[].skill_adjusts[][]` | `skill_type` stays a hash |
|
||||
| `rune_attr_adj[].attr_value`, `rune_max_attr_adj[].attr_value` | seen as `-10` and `-5` in the sample cache |
|
||||
| `rune_naturalattacks[][2]`, `[3]`, `[6]`, `[7]`, `[8]`, `[9]` | the integer slots; floats and the projectile id are unchanged |
|
||||
| `rune_skill_grant[]` | see section 2 |
|
||||
|
||||
### ArcDoorObject (also doors inside structures)
|
||||
|
||||
| JSON path | Notes |
|
||||
|---|---|
|
||||
| `door_type`, `door_movement_axis`, `door_swing_direction` | *(enum)* |
|
||||
| `door_locked` | |
|
||||
|
||||
### ArcDungeonUnitObject, ArcDungeonExitObject, ArcDungeonStairObject
|
||||
|
||||
| JSON path |
|
||||
|---|
|
||||
| `dungeon_connectable_edges[]` |
|
||||
|
||||
### ArcContainerObject
|
||||
|
||||
| JSON path |
|
||||
|---|
|
||||
| `container_open`, `container_locked`, `container_locked_rank`, `container_barred`, `container_barred_rank`, `container_trapped`, `container_trappe_rank` |
|
||||
|
||||
`container_inventory_contents[]` follows the inventory rules below.
|
||||
|
||||
### ArcDeed
|
||||
|
||||
| JSON path |
|
||||
|---|
|
||||
| `deed_type`, `deed_employment`, `deed_start_rank`, `deed_name_lookup` |
|
||||
|
||||
### ArcKey
|
||||
|
||||
| JSON path |
|
||||
|---|
|
||||
| `key_setting` |
|
||||
|
||||
### Inventory entries (rune and container contents)
|
||||
|
||||
| JSON path |
|
||||
|---|
|
||||
| `table[1]` (booty table entries) |
|
||||
|
||||
### ArcCityAssetTemplate (also `template_embed_template`)
|
||||
|
||||
| JSON path | Notes |
|
||||
|---|---|
|
||||
| `template_max_ranks`, `template_start_rank`, `template_asset_type`, `template_trade_icon`, `template_landmark` | |
|
||||
| `template_creator[]`, `template_terrain[]`, `template_valid_npc_type[]`, `template_valid_npc_cat[]` | |
|
||||
| `template_cap_info[][0]`, `[][1]` | `[2]` is an ArcCacheID |
|
||||
| `template_event_rules[][0]` | `[1]` stays unsigned |
|
||||
| `template_resource_limit[][1]`, `[][2]` | `[0]` stays unsigned |
|
||||
| `template_rank_info[].rank_hirelings`, `rank_shrines`, `rank_spires`, `rank_barracks`, `rank_rank`, `rank_health` | `rank_hirelings` and `rank_rank` are `-1` in many sample records |
|
||||
| `template_rank_info[].rank_placement_limit[][1]` | `[0]` stays unsigned |
|
||||
|
||||
## 2. Grant keys
|
||||
|
||||
Power grants (`item_power_grant[]`) and skill grants (`rune_skill_grant[]`)
|
||||
each carry three lists of `[key, value]` pairs. The client reads every pair as
|
||||
an unsigned key and a signed value (sb.exe `0x6D7640`).
|
||||
|
||||
| JSON path | Before | After |
|
||||
|---|---|---|
|
||||
| `power_granted_skills[][0]`, `skill_granted_skills[][0]` | name string or integer | unchanged |
|
||||
| `power_granted_attrs[][0]`, `skill_granted_attrs[][0]` | integer | name string when the hash is known, otherwise integer |
|
||||
| `power_granted_powers[][0]`, `skill_granted_powers[][0]` | integer | name string when the hash is known, otherwise integer (example: `428458144` becomes `"HLR-021"`) |
|
||||
| every `[][1]` in those lists | unsigned | signed |
|
||||
| `power_value`, `skill_value` | unsigned | signed |
|
||||
|
||||
`power_monster_types` and `skill_monster_types` are unchanged.
|
||||
|
||||
A reader that needs the numeric key should run it through
|
||||
`arcane.enums.hashes.string_to_hash`, which returns integers unchanged.
|
||||
|
||||
## Sample data impact
|
||||
|
||||
From `cachefiles/CObjects.cache`, comparing JSON from `master` with this branch:
|
||||
|
||||
| JSON path | Records changed | Example |
|
||||
|---|---|---|
|
||||
| `ASSET.template_rank_info[].rank_hirelings` (incl. embedded templates) | 539 | `4294967295` → `-1` |
|
||||
| `ASSET.template_rank_info[].rank_rank` | 182 | `4294967295` → `-1` |
|
||||
| `RUNE.item_power_grant[].power_granted_powers[][0]` | 100 | `428458144` → `"HLR-021"` |
|
||||
| `RUNE.rune_attr_adj[].attr_value` | 17 | `4294967286` → `-10` |
|
||||
| `RUNE.rune_max_attr_adj[].attr_value` | 12 | `4294967291` → `-5` |
|
||||
|
||||
Nothing else differs in that file.
|
||||
|
||||
## Related parser fixes on the same branch
|
||||
|
||||
These do not change the JSON layout but affect packing:
|
||||
|
||||
- Sparse `MERCHANT_DATA` now writes its last field as 4 bytes, matching the
|
||||
client (sb.exe `0x4859B0`). Packing it previously corrupted the resource.
|
||||
- `ArcContainerObject` packs again; `save_binary` used read calls and
|
||||
`load_json` skipped the item base class.
|
||||
- `ArcCityAssetTemplate` raises `NotImplementedError` when
|
||||
`template_unknown_check1` or `template_unknown_check2` is true. The client
|
||||
loads extra trigger-object data in that case, which this tool does not
|
||||
support. No record in the sample cache sets either flag.
|
||||
|
||||
## Open items
|
||||
|
||||
- The signed/unsigned split depends on which client read entry point each
|
||||
field uses. That split is marked `[MB] GUESS:` in IDA and *unverified* in
|
||||
`MBDATA/docs/client-map.md`.
|
||||
- `item_flags`, `item_use_flags`, structure floor/level/trigger numbers and
|
||||
region content behavior/state are read through `0x7E1720`, which is not yet
|
||||
classified. They stay unsigned.
|
||||
- Region byte fields (`region_stairs`, `region_unknown2`, `region_unknown3`)
|
||||
default to `0xFF` in the client and may be signed; they are still read as
|
||||
unsigned bytes.
|
||||
- `cachefiles/CObjects.cache` holds 286 resource ids twice (identical bytes).
|
||||
`unpack` keeps one copy, so a repacked cache has 10,334 entries instead of
|
||||
10,620.
|
||||
Reference in new issue
Block a user