Item JSON format v2: a_object / b_properties / c_assembly / d_grips (+ generic_pistol → generic_handgun rename) #47
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The plan
What we're doing, and why
We're rebuilding the item JSON format: every item file, both loaders, and the editor writer.
The current format has the data in the wrong places. The properties table is barely used. Weapon numbers, reference points, and states are scattered across flat top-level keys. The grip pose lives in two places at once (
model.grip_offset/grip_rotateplusgrip_poses). And file paths are fragile relative paths (../../../assets/...).The new format puts each kind of data in exactly one place, namespaced so any module can address it, and ordered so a person reading the file sees what the object is before they see the pose data.
The generic pistol becomes the generic handgun in the same change. Its id, its mag's id, its file, and its model are all being renamed anyway — doing the rename separately would mean touching every reference twice.
The new shape, in words
An item file has four sections, in this order:
a_object— what the thing is. The controller that runs it, its display name, its model, its tags, its value and weight (both optional, default 0), and its colliders.a_objectalone is a valid basic object — you can define a thing in the game with just this section.b_properties— what the thing does, per type. Keyed by type (weapon_onehand). Each table holds the numbers (data), the positional references (refpoints), the state hook (states), and what it accepts (accepts_mag,accepts_ammo).c_assembly— the moving parts. Each part is bound to a model object with an origin and a travel: the fire-select lever, the slide, the trigger. The fire-select modes and their lever poses live here.d_grips— where hands go. Grip points keyed by name. Each has a role, the model object it attaches to, and per-hand pose + finger driver + finger states. A grip point is a grab trigger — it says "this spot can be grabbed" — and the pose stays the same while the part moves. Pose data goes last in the file on purpose — you read what the object is first.Colliders live in
a_objectas acolliderstable. It replacesmodel.colliders— the editor points ata_object.collidersinstead. The table supports every collider type: box, mesh, convex, sphere, cylinder. A box entry has a size ([Length, Width, Height]), a position, and a rotation, and its origin is the center of its volume. Colliders show in the debug view when it is enabled.Handedness and slot are separate properties. A weapon can be one-handed but a primary, or two-handed but a secondary.
Namespacing follows the module that owns the thing. Objects in the devkit carry the
devkit:prefix (devkit:weapon/gun/generic_handgun,devkit:mag/generic_handgun,devkit:ammo.9mm). The controller carries thedeadair:prefix (deadair:controller/generic_weapon) because it is deadair code, not devkit code.The work, in order
Phase 0 — Settle the shape (this issue). The shape is settled. Nothing is built until the open questions are answered, because every item file and both loaders change in the same move.
Phase 1 — Loaders first. Teach both loaders to read the new shape before any item file is converted, so the game never has a half-migrated state.
ItemDefinition.from_dict()andItemSpeclearn the four sections, the namespaced paths, and the newcontrollerfield. (from_dict()does not copygrip_posestoday, so the new top-level keys need plumbing in both loaders, plusItemSpec@exports andto_item_definition().)weapon_onehandmust count asweaponforis_type("weapon"). Today that is an exact key match.controllerfield resolves through the newdeadair:controller/...registry. When the field is absent, it defaults togeneric_object.grip_offset/grip_rotate). Then the fallback is removed.Phase 2 — Migrate the items. Convert every item file (11+ items carry
model.grip_offset, 3 carrygrip_rotate). Scripted or editor-assisted — not by hand.grip_offset/grip_rotateinto explicitgrip_pointsprimary poses. The editor already computes the composed pose, so the fold is mechanical.b_propertiesunder its type key, its reference points intorefpoints, and its states intostates.reload_timeandstack(see "What changes" below for why).value/weightintoa_object(both optional, default 0).generic_handgun.jsonand the mag file to match, and rename the GLB and its textures. Ids becomedevkit:weapon/gun/generic_handgunanddevkit:mag/generic_handgun.a_object.colliders, replacingmodel.colliders. The table supports box, mesh, convex, sphere, and cylinder.0.00004→0.0).travelfrom the grip points. Travel lives only inassemblynow.Phase 3 — Editor. The editor writes and reads the new shape, and the dock is rebuilt to match the Inspector.
grip_pointsinternals (role, object, mirror, then the hand entries) instead of an alphabetical sort. Thea_/b_/c_/d_section prefixes already survive sorting.Phase 4 — Rename blast radius. Follow the renames everywhere they are referenced: the registry, tests and suites, spawn lists, and the editor. The choke points are
ItemDefinition.from_dict()and the registry rebuild — if those two are right, the references follow.Phase 5 — Verify. The format is done when all of these are true:
tests/grip_pose_check.gd(40 checks) still passes. The slice-1 behaviours survive: right hand as mirror master, 5 mm centeredness epsilon, derived poses not stored, poses in model space plus the scale rule.What changes
a_object,b_properties,c_assembly,d_grips) and lose the flat top-level tables.model.grip_offset/grip_rotatefold into the primary grip pose;properties.weaponis replaced byb_properties.weapon_onehand.devkit:for devkit objects,deadair:for the controller (deadair code).acceptssplits intoaccepts_mag(which mags fit) andaccepts_ammo(which caliber).holster.hipis gone. The slot isdevkit:weapon.secondary.reload_timeis removed. It does nothing for a VR game — a fixed reload time is an FPS-game holder, and in VR reload is as fast as the person can actually reload.stackis removed. There is no stacking items in inventory — either you physically have the object or you don't. Storage containers define what they can hold, not the objects.valueandweightmove intoa_object(optional, default 0).a_object.colliders, replacingmodel.colliders, and the table supports box, mesh, convex, sphere, and cylinder.assemblynow. Grip points no longer carrytravel— they are grab triggers, and the pose stays the same while the part moves.assembly.fire_select. The trigger gains atravel(rotation, 30°). The slide keeps its travel (0.05 m rearward).Handgun_Upperwith aslide_grip_posedriver. Physical racking replaces button-based racking, and the freed button cycles fire-select when holding a gun. That last part is an input binding change, not grip data.a_object.controllerselects the controller for the object, and that controller also interprets the grips. The per-handdrivers (gun_grip_pose,slide_grip_pose) are the per-point interpreters it dispatches to.What does not change
assembly.fire_selectas they are.object.typetaxonomy is still undecided. The slide is the case that will define it.Decisions already made
grip_controller≡driver≡a_object.controller. One controller, unified, reusable across multiple items (Zach, 2026-11-13). For the handgun it isgeneric_weapon. (generic_gunin the first draft was a typo.)generic_objectis the base and the default.generic_weaponandgeneric_utilityextend it.controlleris a new addressable kind (deadair:controller/...).weapon.onehand(dot). Properties key:weapon_onehand(underscore). The #33 type-table spellingweapon_one_handis superseded byweapon_onehand.reload_timeandstackdropped.value/weightintoa_object(optional, default 0).weapon.secondaryreplaces the holster tag.0.00004→0.0is a deliberate cleanup, not a retype. That much precision is not useful for dev data.grip_offset.y"discrepancy" is not real (verified 2026-11-13).-0.0607is Zach's own value from commit8d466cf, identical at PR base6eec0f2. The "-0.03 on main" was a misattribution — that is the bullet's and the mag's y. Search tip: the stored value is full-precision-0.0606891736388206, so searching"0.0607"misses it.schemafirst ingrip_points.Handgun_Upper's live transform. GrabPoints are static today and the slide does not move at runtime. The slide grip pose[0, 0.01, -0.05]is illustrative only, not authored or measured.assembly. Grip points are grab triggers; the pose stays the same while the part travels (Zach, 2026-11-13).a_object.collidersreplacesmodel.colliders. The editor points ata_object.collidersinstead, and the table supports box, mesh, convex, sphere, and cylinder (Zach, 2026-11-13).devkit:; the controller carriesdeadair:because it is deadair code (Zach, 2026-11-13). The handgun takesdevkit:ammo.9mm; a future 9x18 ammo would bedeadair:ammo.9x18.The full example (appendix — the Generic Handgun)
References
15dc67a, verified in-editor and in-headset 2026-11-13, awaiting merge. This build starts after #45 merges (it touches the same files; after it lands, close PR #44).d_gripsis the recommended shape from the 2026-11-13 grip-data-shape investigation.Answered questions
These were the open questions. Zach answered all six in comment 1016 (2026-11-14). They stay at the bottom as the record.
value/weightplacement. Intoa_object, for simplicity. Both optional — value defaults to 0, weight is a general weight rating.devkit:vsdeadair:. Thedeadair:in the first draft was for reference. The objects are still in the devkit, so they carry thedevkit:prefix. The controller carries thedeadair:prefix because it is deadair code, not devkit code. (The model path and tags follow the same rule in the example above —devkit:for devkit things.)9mmvs9x18. The Generic Handgun usesdevkit:ammo.9mm. Later, when new ammo types are defined, the 9x18 ammo would bedeadair:ammo.9x18.travelvsassembly.slide.travel. Remove the travel values from the grip points — they live inassemblynow. Grip points only serve as "bool" triggers of "spot is grabbed". Some interactions (slide / racking a round) allow travel, but the grip pose in those instances remains the same.a_object.collidersvsmodel.colliders. Make the editor point toa_object.collidersinstead. Effectively the same thing, moved to a dedicated table in the object instead of sitting willy-nilly.a_object.collidersstill supports the other collider definitions: box, mesh, convex, sphere, cylinder.I feel like they should go into
a_objectfor simplicity, they are optional values (default to 0 value, and a general weight rating)yes they get renamed
okay my use of
deadair:in those areas was for reference, since the objects are still in the devkit they would have thedevkit:prefix, however the controller would use thedeadairprefix as that is deadair code and not devkit codethe Generic Handgun uses
devkit:ammo.9mmammo, however later on when new ammo types are defined, the 9x18 ammo would bedeadair:ammo.9x18,this is a conundrum, and a complex situation to correctly do,
grip_points is meant for placing the hand relative to the object defined, however in this case it has diplucated values, which we can remove the travel values from the grip points, as those are now apart of the assembly, and with the grip points only serving as "bool" triggers of "spot is grabbed" and some interactions (slide/racking a round) allow travel, but the grip pose in those instances would remain the same.
make the editor point to
a_object.collidersinstead, effectively mean the same thing, we are moving the colliders to a dedicated table in the object instead of being there willy-nilly,of course the
a_object.collidersshould still support the other collider method definitions, (box, mesh, convex, sphere, cylinder) now that I think about it, makes it easier,