Item JSON format v2: data changes (issue #47) #48
Loading…
Reference in a new issue
No description provided.
Delete branch "muse-lantern/da-game:object-grip-overhaul"
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?
Item JSON format v2: data changes (issue #47)
The data half of #47: every item file, both loaders, and the rename blast radius. The editor reads and writes the new shape (minimal adaptation — no redesign, no new features). This branch has since grown past the data build — it now also carries the grip-driver runtime, the gun_bolt rename, the mag-rail fix, grip-pose slice 5 (bolt slam + lock), and the Phase 3 UIX rebuild. The UIX rebuild still needs Zach's in-editor check of the viewport click itself. No merge until item-v2 + toolkit-uix-v2 + grip-pose-v2 all land (Zach, 2026-11-15).
What changed
a_object(controller, name, model, tags, value/weight, colliders) /b_properties(per-type tables:weapon_onehandwithaccepts_mag/accepts_ammo,data,refpoints,states) /c_assembly(fire_select, bolt, trigger) /d_grips(grip_points).ItemDefinition.from_dict()andItemSpecread the four sections, resolve namespaced paths, and resolve the newcontrollerfield through thedeadair:controller/...registry (absent =generic_object). The loader resolves extension keys:weapon_onehandcounts asweaponforis_type("weapon"). The old shape loaded through a fallback compose layer while the migration landed; the fallback is now removed.grip_offset/grip_rotateinto explicitgrip_pointsprimary poses — the fold equals the old composed runtime pose).reload_timeandstackdropped.value/weightmoved intoa_object(optional, default 0). Colliders moved toa_object.colliders(box, mesh, convex, sphere, cylinder). Bolt origin z0.00004→0.0. Travel lives only inassembly— no travel in grip points.devkit:weapon/gun/generic_handgun,devkit:mag/generic_handgun,devkit:ammo.9mm).holster.hipis gone; the slot isdevkit:weapon.secondary. Controller isdeadair:controller/generic_weapon— the object controller and the grip controller are one thing.gun_slide→gun_bolt(bolt is the general term;slideis reserved for pump-action shotguns with their own handler); assembly partslide→bolt, grip pointslide→bolt, driverslide_grip_pose→bolt_grip_pose. The editor's internal slide-sim identifiers (slide_amount,set_slide_amount,play_slide_cycle,_slide_*,SLIDE_*, dock "Slide" header) are renamed to bolt as well.examples/example_module/items/example_gun.jsonwas also migrated (its accepts_mag/ammo point at the handgun mag — an inference, see below).74bb4e4,fbe68b2,2bf6e7e). (A) Dock rebuilt on Inspector patterns: dark-band bold left-aligned section headers, one slim flat toolbar (Duplicate / Delete / Open staging / Save JSON), click-to-select marker rows with a highlight band (Select button removed), expandable colliders with Vector3 pos/rot/size rows, low-key '+ Add …' rows; Grip preview kept. (B) Click-to-select in the staging scene:item_stage.selectable_for()maps markers, rails, collider gizmos, and assembly-bound model nodes; the dock mirrorsEditorSelection. (C) Secondary 'Selected' tab showing the selected thing's item-data properties (never node properties), plus the driver surface in the Grip preview: driver picker fromGripDrivers.ids()and a trigger slider that injects the pull into the stage's tree-free evaluation, applied viaHandView.set_finger_pose. One bug caught building: a bolt origin edit would have been reverted bycollect_item()writing stale bolt-rail ends back — fixed withstage.refresh_bolt_rail().25b37ca).GripDriverbase (tree-freeevaluate, default-inheritance state resolution),GripDriversregistry (static_grip_pose/gun_grip_pose/bolt_grip_pose),GunGripDriver(trigger_rest → trigger_full blend by analog pull),HandView.set_finger_pose(per-finger curl + spread + thumb opposition). Newtests/grip_driver_check.gd(81 checks, wired into CI). The editor doesn't evaluate drivers yet — the driver surface (picker, trigger slider, grip preview) folds into the UIX rebuild.13b93a7,7b3c6cd).*.modtool_meta.jsonfiles carry no game data; the editor scan and the runtime loader both skip them.tfix (245eb97, shape b). While a mag is snapped,_snap_twas computed from the hand follower's previous-frame position (one physics tick stale, ~2.5 mm at slide speed). NowHandFollower.desired_global_transform()computes the follower's desired position for the current frame, shared by the follower and_update_snap. Proving test initem_follow_check(drive the grip at 0.15 m/s, assert the mag's rail pose against the hand's current rail projection, <1 mm).f9f92fb).Firearmholdsbolt_t0..1 +bolt_locked;fire_once()drives the bolt to 1 then springs it back (default 250 ms, tunable); on the last round (chamber empty + mag empty) it stays at 1 (locked);rack()releases the lock and chambers from a fresh mag. The bolt node is posed per physics frame as origin + t·travel fromc_assembly.bolt, and the bolt grip point is recomputed from the node's live transform every frame. Bolt behaviour is tuned in a newb_properties.weapon.bolttable on the base weapon table (visible to theweapon_onehand/weapon_twohandmirrors via the deep-mergedItemSpec.table()):dry_fire_primesfalse ·snap_back_ms250.0 ·stroke_ms30.0 ·lock_on_emptytrue ·release_on_mag_insertfalse ·rack_empty_cyclestrue ·drop_mid_cycle"finish". New signalbolt_reached_rearfires once per rearward stroke — the hook point for slice 4's casing eject (eject itself not built). Newtests/bolt_check.gd(90 checks, wired into tests.yml). Two bugs found by tests and fixed in the same commit: the editor'stype_table()returned the bolt-only base table so markers/accepts_magread null (fixed — merged base+extension view viaItemSpec._merged_type_table, single source of truth, +5 regression checks); a freeze-test race (re-grab + drop one idle frame apart misses the physics frame — test-side 0.2 s settle, no gameplay impact).Commits (19, on
object-grip-overhaul, basef9919ea)d8d61f2Phase 1: nested loaders with old-shape fallback16de7d6Phase 2: migrate all item JSONs to the four-section shapeafbb45fPhase 4: rename blast radius — tests, bridge, start kit8a15d86Editor: schema/stage/dock + tests read the four-section shape21d765aPhase 6: remove the old-shape fallbackbfd0c90Phase 6: last stale mag_well_b / grip_poses wording55a3298Phase 6: a_object is optional in the v2 loaders35d0e86Phase 6: last stale doc referencesf8146c1Zach: resave object JSONs in the editor's format (15 files format-only; testgun magwell point → 5 cm segment)1fe7f44Revert test gun magwell to a point rail (rail fixtures assume it)25b37caGrip-driver runtime (amended; author merith-tk per Zach)13b93a7Editor scan fix: modtool_meta.json ignored56024cdRename gun_slide → gun_bolt7b3c6cdRuntime loader ignores *.modtool_meta.json245eb97Mag rail t uses the hand's current-frame position (shape b)f9f92fbSlice 5: bolt slams on fire and locks on the last round74bb4e4UIX Part A: Inspector-style dockfbe68b2UIX Part B: click-to-select in the staging scene2bf6e7eUIX Part C: Selected tab + driver surfaceVerification (current head
2bf6e7e)--import: 0 ERROR both passes.python3 tools/check.py: 6/6.item_editor_checkPASS, 359 ok (was 284 at the data build; +25 Part A, +13 Part B, +37 Part C).item_editor_manage_checkPASS, 111 ok.grip_driver_checkPASS, 81 ok. Newtests/bolt_check.gdPASS, 90 checks.item_follow_checkPASS.xvfb-run(not--headless): PASS, exit 0.weapon_onehandaccepts, bolt travel +Z 0.05 with origin z 0.0, primary pose[0, -0.0606891736388206, 0.0534712076187134]rot[90,0,0], bolt secondary pose, no travel in grips). No old-shape leftovers in any item JSON; nogeneric_pistolrefs left in modules code or JSON.35d0e86: battery was 36/38 PASS, 1758 ok — same two stack-fallout failures, counts unchanged. Atf9f92fb: 35/37 PASS, 1777 ok.)The two failing suites — stack fallout, not a mag regression
mag_filo_check(31 ok, was 47) andmag_interaction_check(50 ok, was 69) fail on the same mechanism, traced in code and confirmed by re-running both suites:stackis gone per #47, somax_stackis 1 andItemInstance.create(id, 60)now makes a count-1 round.Magazines.load_from_stackmovesmini(room, stack.count(), n)rounds, so a test that loads "2 regular" or "10 regular" from a count-N stack now moves 1, and every downstream check (13 rounds, FILO order, rack counts, the 60-stack eject scenario) fails with it.load_from_stackcreates one count-1 child per round moved;make_loadedfills with individual instances), and the game spawn path already handles non-stackable items by spawning N separate copies (game.gd). The mag still loads, holds, and ejects individual rounds; FILO order is preserved in the children list.Known gaps and caveats (stated plainly, not hidden)
JSON.stringify(a point'srole, object, bothcomes backboth, object, role), and"schema": 1becomes"schema": 1.0. Zach (2026-11-15): alphabetical order is fine; a byte-identical round-trip via a custom order-preserving serializer is a parked later patch.Handgun_Upper's live transform; the bolt moves on fire and locks on the last round. (Was: static point only.)25b37ca) + editor surface (Part C). trigger_rest → trigger_full blending by analog pull;HandView.set_finger_pose; driver picker + trigger slider + grip preview in the editor. The viewport click itself still needs Zach's in-editor check — headless cannot prove the click →EditorSelection→ dock glue (the mapping and dock-side selection are suite-proven).docs/modding.md,MECHANICS.md,mechanics.jsonstill referencegeneric_pistolin places.[safe, semi], index 0) — expected from modes-from-file-order.Not in this PR
The mag test redesign around loose rounds, the float cap (0.1 mm round-on-save + normalize-on-load), doc regeneration, the fire-select input-binding change, and casing eject (slice 4; hook
bolt_reached_rearwired at t=1). Issue #46 is subsumed by this format —d_gripsis its recommended shape.ItemSpec gains the v2 vocabulary: controller / grip_points / colliders fields, TYPE_EXTENSIONS (weapon_onehand/twohand resolve to weapon), namespace-aware has_tag/matches_tag, namespaced path and controller resolution, and normalize_old_* helpers that fold flat old tables into the nested {data, refpoints, states} form. primary_grip_pose resolves from grip_points first (folded pose, no model offset on top) and keeps the old grip_poses + offset branch as fallback. WorldItem builds one GrabPoint per secondary grip point (GrabSecondary_<point>_<side>); Firearm, Magazines and the mag/consumable readers go through the nested tables; the mag and bullet base bands now key off the accepted caliber local part (ammo.9mm), restoring two dead branches. Old JSONs still load: battery 35/38 green (terrain N/A); the 3 FAILs (firearm, modloader, spawn) are tests reading the flat table keys, updated in Phase 4.item_schema.gd: markers re-pointed to b_properties.<base>.refpoints / c_assembly / d_grips.grip_points (base segment resolved per item via TYPE_EXTENSIONS); grip functions now operate on grip_points with fixed key order (role, object, mirror, both/right/left; pose, driver, state inside a hand entry); resolved_grip_pose mirrors ItemSpec.resolve_grip_point exactly (right = right??both??default; left = own entry, else mirror(right) when point.mirror ?? centered, else right as-is); templates rewritten to the new shape (weapon_onehand, accepts_mag devkit:mag/generic_handgun, accepts_ammo devkit:ammo.9mm, c_assembly.fire_select + slide travel, folded d_grips poses); colliders read/write a_object.colliders; duplicate keeps the legacy top-level model_path/world_scene branch for old-shape dicts and leaves namespaced a_object.model as-is; looks_like_item_data needs a_object + name. item_stage.gd: grip/magwell/slide rails and markers use the resolved marker paths; gun_slide rail now builds from c_assembly (origin + origin+travel) with its own collect/build pair; ref_b written as a {pos} dict (the runtime only reads dict ref_b); _accepted_mag_bounds resolves accepts_mag as an item id first, else a tag scan. Tests: item_editor_check 284 ok (baseline 282: +4 slide-rail checks, -1 merged grip check, -1 merged mag-grip check), item_editor_manage_check 111 ok (baseline 110: +1 no-world_scene check), both 0 FAIL. The manage duplicate fixture stays old-shape on purpose (tests the legacy branch). Round-trip proof (generic handgun, load -> collect unmoved -> save): values stable except two float32 transform-noise spots (magwell ref_b y -3.7e-9, slide travel z +7.5e-10, both far inside the 1e-4 test tolerance); the saved file is re-sorted alphabetically and ints become floats (schema 1 -> 1.0) — both are the pre-existing save/load path (JSON.stringify sorts keys; Godot JSON parse returns floats), save_item_json/load_item_json untouched by this change. A byte-identical round-trip would need a custom order-preserving serializer (out of scope).Loaders read the four-section shape only: normalize_old_* deleted from ItemSpec; _from_dict_old deleted from ItemDefinition; _apply_dict_old deleted from GenericItem; grip_poses field, grip_pose_for/_hand and pose_from_dict deleted; primary_grip_pose resolves from grip_points with the default fallback (resolve_grip_point({})). Firearm/Magazines/WorldItem flat-table fallbacks deleted (_data/_refpoints/_mag_data/_mag_refpoints return the nested subtable only); accepts_magazine is id equality only; legacy mag_well/mag_well_b branches deleted; array-form opening branches deleted (v2 opening is a {pos, rot} dict). Tests: the remaining old-shape apply_dict/from_dict probes rewritten to the new shape (modloader g/g2/g3/g4 + replacer + coded coin + frozen register; module_check fixtures incl. the two-hand sword grip point and the pack item; mag_filo no-offset probe). The manage duplicate fixture stays old-shape on purpose (it tests the legacy duplicate branch, which is kept). Known fallout (unchanged from Phase 4, decision for Zach): stack is gone from the JSONs, so multi-round loads cannot exist — mag_filo and mag_interaction stack scenarios fail on counts, not on mechanics.The loader builds a sample element with make_element(type, {}, "") once per JSON element to read _path_fields() — the direct d["a_object"] index in _apply_dict_v2/_from_dict_v2 crashed on that empty dict (16 SCRIPT ERRORs per discovery, and the undefined_model probes aborted mid-load). Both v2 entry points now read a_object via .get() with a {} default: a dict without a_object loads as an empty basic item. undefined_model probes rewritten to the new shape (a_object.name/model).a7033dcaa725b37cab5dThe module loader enumerated the mag's .modtool_meta.json as an item and rejected its dotted id ("bad element id mag/generic_handgun.modtool_meta"). Per Zach's decision the meta files are editor-only; the game ignores them entirely, so the element scan skips them at the enumeration level.The bolt cycle is driven by firing and by the grip (rack), on the shared desired-position mechanism from Part 1. Geometry stays in c_assembly.bolt; behaviour is a new b_properties.weapon.bolt table on the base weapon table. - ItemSpec.table() deep-merges base + extension (extension wins per key), so a weapon_onehand item sees the base bolt knobs and a per-handedness table can override them knob-by-knob. With only one present the stored dict is returned as-is. - Firearm: bolt_t 0..1 + bolt_locked; fire_once() drives the bolt to 1 then springs it back, and locks it at 1 on the last round (chamber empty + mag empty); rack() releases the lock (and chambers from a fresh mag when release_on_mag_insert). The bolt node is posed per frame as origin + t*travel from c_assembly.bolt, and the bolt grip points are recomputed from the node's live transform every physics frame. bolt_reached_rear fires once per rearward stroke (slice 4's casing eject hooks here). - Bolt knobs (b_properties.weapon.bolt, handgun defaults): dry_fire_primes false (a priming pull chambers, no shot, no cycle); snap_back_ms 250 (200-300 range); stroke_ms 30; lock_on_empty true; release_on_mag_insert false; rack_empty_cycles true; drop_mid_cycle "finish" ("finish"|"reset"|"freeze"). - Editor: DAItemSchema.type_table() / dict_get_marker_path() now see the same merged base+extension view as the runtime (the base table previously shadowed the extension's refpoints, breaking the muzzle/magwell/chamber markers); marker writes route to the table that owns the segment. Naming purge: the editor's slide-sim identifiers are now bolt (bolt_amount, set_bolt_amount, play_bolt_cycle, BoltForward/Back/Travel gizmos, "Bolt" section); the gun_bolt rail visualisation, magwell rail, GLB node Handgun_Upper, and 'slide' in ASSEMBLY_KNOWN_PARTS stay. _bolt_travel_vec() no longer falls back to the gun_bolt rail. Tests: new tests/bolt_check.gd (90 checks: merge incl. a synthetic per-knob override, fire cycle, last-round lock, rack release, dry fire + priming, rack-empty on/off, drop finish/reset/freeze+resume, boltless test gun inert). item_editor_check gains 5 merge checks (289 total). Full battery: 35/37 runnable suites pass (1777 ok); mag_filo / mag_interaction stay red on the known stack fallout (unchanged counts); terrain_check is skipped by CI design (no file on this branch). Bridge e2e PASS under xvfb-run.Selection syncs viewport and dock list: - item_stage.selectable_for(node) maps a viewport-selected node to a dock selection descriptor: markers (and their triad children), rails (and their end markers and line), collider gizmos (by index), and model nodes bound to an assembly part. Preview hands, references, and the model root map to nothing. marker_node() is public for the dock. - The dock listens to the editor selection (EditorSelection via Engine.get_singleton("EditorInterface"), null-guarded headless) and mirrors the first mapped node as its selected thing; an empty or unmapped selection clears it. The sync is factored as _apply_viewport_selection() so the mapping is headless-provable. - Component rows are click-to-select too (row gui_input; labels ignore mouse input so clicks reach the row) and the selected component's part name highlights in the selection colour. The viewport click itself is editor-interactive and needs Zach's in-editor check; the suite proves the mapping (selectable_for) and the dock-side selection, not the click. item_editor_check 322 ok, item_editor_manage_check 111 ok, check.py 6/6.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.