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 still pack. 4294967295 and -1 are the same four bytes, and
ResStream.write_long accepts either form: values from 2147483648 to
4294967295 are converted to their signed equivalent before packing. Integer
grant keys in old dumps also pack unchanged. Re-running unpack cobjects is
only needed to see the new signed values and grant key names in the JSON.
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
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.