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:
MagicBotandClaude Opus 5.5 committed 2026-10-16 13:06:08 -04:00
1 parent ca4d2b2565
commit 50d8d76a21
1 file changed
+219
+219
View File
@@ -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.