diff --git a/CLAUDE.md b/CLAUDE.md index 471f736..16934d3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,7 +27,7 @@ Box2D v3.1.0 (fetched by CMake) - **LCB binding** (`src/box2dxt.lcb`, `library org.openxtalk.box2dxt`): declares `foreign handler` bindings to the shared library and public `b2PascalCase` handlers callable from xTalk. This API speaks **metres and radians**; body type codes are `0=static, 1=kinematic, 2=dynamic`. -- **The Kit** (`src/box2dxt-kit.livecodescript`): a pure-xTalk convenience layer (260+ `b2k*` +- **The Kit** (`src/box2dxt-kit.livecodescript`): a pure-xTalk convenience layer (300+ `b2k*` handlers incl. the game modules: input, sprites, player controller, camera) that speaks **screen pixels and degrees**, binds bodies to LiveCode controls, and runs the animation loop. This is what the examples and most users actually call. @@ -95,7 +95,7 @@ failure. Run it after **every** `.livecodescript` edit. pass" and let the user confirm. **The self-test harness** (`examples/box2dxt-selftest.livecodescript`) is the runtime safety net: -~125 deterministic assertions (currently **v12**) driving the real Kit (paused world + +~177 deterministic assertions (currently **v18**) driving the real Kit (paused world + `b2kStepOnce` hand-stepping + `b2kInputInject` scripted keys). The workflow for every **Kit** change: (1) add/extend an assertion that captures the new behavior, (2) **bump `kStHarnessV`** (the report header prints it, so a @@ -145,9 +145,13 @@ OXT's compiler is **stricter than LiveCode's**. These are the recurring footguns OXT reports "missing end if" at the handler's end. Chains like `if c then s1` / `else s2` (statement on the else line) are fine. The static checker's dangling-else gate flags the broken pairing. -11. **Declare `local` only at the top of a handler.** A `local` nested inside - an `if`/`repeat` block has broken OXT compilation of the entire script. - Keep all declarations together at the handler's top. +11. **Prefer declaring `local` at the top of a handler** (house style, not a + hard compiler rule). Keeping all declarations together at the handler's top + is the convention and a hedge against stricter OXT builds. A `local` nested + inside an `if`/`repeat` block was once suspected of breaking OXT compilation, + but the in-use OXT build tolerates it (several ship in the examples, e.g. the + contraption builder and demo), so the static checker deliberately does NOT + flag nested `local`. Still: when you add code, declare at the top. 12. **Scrolled-group coordinates are visual.** `the loc`/`rect` of a grouped control is reported and set SCROLL-ADJUSTED on OXT, so a per-frame write of world coordinates into a scrolled group cancels the pan (objects diff --git a/docs/api-reference.md b/docs/api-reference.md index 35f7e47..b695dc7 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -236,7 +236,7 @@ rendering into an LCB widget canvas, rather than one control per body. **Extending the binding.** See [architecture.md](architecture.md#extending-the-binding) for the step-by-step recipe (add a `b2lc_*` C function, a `foreign handler`, and -a public wrapper; bump `LC_ABI_VERSION`; rebuild). As of ABI `3` the binding +a public wrapper; bump `LC_ABI_VERSION`; rebuild). The binding (ABI 4) covers the full Box2D v3.1 **live-object** surface (chains, sensors, filtering, hit & body-move events, shape casts, motor/filter joints, world tuning, mass data, …). What's intentionally **not** wrapped: pre-solve / custom-filter diff --git a/docs/architecture.md b/docs/architecture.md index de8e271..fd92fd4 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -118,7 +118,7 @@ Exposing more of Box2D is mechanical. To add a handler: Add a smoke-test assertion in `tests/smoke_test.c` for anything non-trivial so CI exercises it on every platform. -As of ABI `3` the binding already covers the full Box2D v3.1 **live-object** +The binding (currently ABI 4) covers the full Box2D v3.1 **live-object** surface. The newer additions reuse a few shared shim patterns worth knowing when you extend further: diff --git a/docs/expansion-prep.md b/docs/expansion-prep.md index 4b3a42e..17ca00d 100644 --- a/docs/expansion-prep.md +++ b/docs/expansion-prep.md @@ -7,7 +7,7 @@ that keep the expansion as reliable as the engine underneath it. | | | |---|---| -| Baseline | Kit + games user-verified; self-test harness **v10, ~113 assertions, all pass** (Wave 2 closed 2026-06-13) | +| Baseline | Kit + games user-verified; self-test harness **v10, ~113 assertions, all pass** (Wave 2 closed 2026-06-13; the harness has since grown to **v18, 177 assertions** — the figures below are historical Wave checkpoints) | | Assets | **LANDED (2026-06-11)** — Kenney's iconic platformer family, ~900 frames; Wave 0 catalogue below | | Wave 1 | **COMPLETE — user-verified 2026-06-12** (the three-level platformer; see §7) | | Wave 2 | **COMPLETE — user-verified 2026-06-13** (player actions I, harness v10; see §9) | diff --git a/docs/kit-guide.md b/docs/kit-guide.md index 0a875eb..690ab64 100644 --- a/docs/kit-guide.md +++ b/docs/kit-guide.md @@ -66,7 +66,8 @@ This guide is entirely about the **Kit** layer. ## 2. Install and your first scene **Requirements:** the `box2dxt` extension loaded. Check with `put b2Version()` -— it should return `3`. The Kit runs in OpenXTalk and LiveCode 9.6.3+. +— it should return `4` (the shim ABI version, unrelated to Box2D's own 3.1.0). +The Kit runs in OpenXTalk and LiveCode 9.6.3+. **Install:** paste the contents of `src/box2dxt-kit.livecodescript` into your card or stack script. (Or save it as a library stack and `start using` it.) @@ -812,7 +813,7 @@ b2kSetCollisionGroup tWheelB, -1 ### Just these two ```livecode -b2kNoCollide tArm, tBody) -- exempt one specific pair (a filter joint +b2kNoCollide tArm, tBody -- exempt one specific pair (a filter joint) ``` --- @@ -1214,9 +1215,13 @@ A few things that trip up LiveCode/OpenXTalk users specifically: (`type`, `name`, `layer`, `number`, `time`, `id`, `mode`…) are reserved. The Kit prefixes everything (`b2k…`, internal `s…`); prefix **your** variables too (`tBox`, `gScore`) so you never collide with a keyword. -- **`get` vs. `put` for functions that return.** `b2kSpawn…`, `b2kGrab`, and the - joint constructors *return* a value. Use `put … into tVar` to keep it, or - `get …` to discard it. Calling them as a bare statement is a syntax error. +- **`the result` for commands, `()` for functions.** Most value-returning Kit + handlers — `b2kSpawn…`, the joint constructors (`b2kHinge`, `b2kWeld`, …), + `b2kAddSensor` — are **commands**: call them as a bare statement and read + `the result` on the very next line (`b2kSpawnBox 100,80,40,40` then + `put the result into tBox`). A few are true **functions** (`b2kGrab`, + `b2kPosition`, the getters): call those with `()` — `put b2kGrab(x,y) into tVar`. + Don't mix the two forms. - **Custom properties stick to objects.** A handy pattern is to stash per-object data as `set the uColor of tBox to …` and read it back later — the Kit and the examples use `u…` custom properties throughout. @@ -1227,8 +1232,10 @@ A few things that trip up LiveCode/OpenXTalk users specifically: ## 23. Complete API index -Every public handler, grouped. `[f]` marks a **function** (returns a value — call -it with `()` / `get` / `put`); everything else is a **command** (a statement). +Every public handler, grouped. `[f]` marks a handler that **returns a value** — +either a *function* (call with `()` / `get` / `put`) or a value-returning +*command* (call as a statement, then read `the result`; see §22 and +kit-reference.md for which is which). Unmarked handlers are plain commands. Optional arguments are in `[…]`. ### World & lifecycle @@ -1316,7 +1323,7 @@ Optional arguments are in `[…]`. ### Player (the platformer controller) `b2kPlayerMake x,y,w,h [,sheet]` · `b2kPlayerAttach ctrl` · -`b2kPlayerAnims idle,run,jump [,fall] [,land] [,duck] [,climb] [,hurt] [,swim]` · +`b2kPlayerAnims idle,run,jump [,fall] [,land] [,duck] [,climb] [,hurt] [,swim] [,wall] [,dash]` · `b2kPlayerSet key,value` · `b2kPlayerGet(key)` `[f]` · `b2kPlayerOnGround()` `[f]` · `b2kPlayerState()` `[f]` · `b2kPlayerFacing()` `[f]` · `b2kPlayerJump [speed]` · `b2kPlayerControl flag` · diff --git a/src/box2d_lc.c b/src/box2d_lc.c index 71783da..f2d44f3 100644 --- a/src/box2d_lc.c +++ b/src/box2d_lc.c @@ -1058,7 +1058,7 @@ LC_API int b2lc_cast_ray_closest(int w, double x1, double y1, double x2, double s_ray_hit = s_ray_body = s_ray_shape = 0; s_ray_px = s_ray_py = s_ray_nx = s_ray_ny = s_ray_frac = 0; b2WorldId wid = worlds_get(w); - if (!b2World_IsValid(wid)) return 0; + if (!b2World_IsValid(wid) || !finite2(x1, y1) || !finite2(x2, y2)) return 0; b2RayResult r = b2World_CastRayClosest(wid, v2(x1, y1), v2(x2 - x1, y2 - y1), b2DefaultQueryFilter()); if (r.hit) { s_ray_hit = 1; @@ -1457,7 +1457,7 @@ static double s_shapeRay[5]; /* px, py, nx, ny, fraction */ LC_API int b2lc_shape_raycast(int s, double x1, double y1, double x2, double y2) { s_shapeRayHit = 0; memset(s_shapeRay, 0, sizeof s_shapeRay); b2ShapeId id = shapes_get(s); - if (!b2Shape_IsValid(id)) return 0; + if (!b2Shape_IsValid(id) || !finite2(x1, y1) || !finite2(x2, y2)) return 0; b2RayCastInput in; in.origin = v2(x1, y1); in.translation = v2(x2 - x1, y2 - y1); in.maxFraction = 1.0f; b2CastOutput out = b2Shape_RayCast(id, &in); if (out.hit) { @@ -1502,7 +1502,7 @@ static b2Vec2 s_chain[LC_MAX_CHAIN]; static int s_chainCnt = 0; static b2ShapeId *s_chainSeg = NULL; static int s_chainSegCap = 0, s_chainSegCnt = 0; LC_API void b2lc_chain_begin(void) { s_chainCnt = 0; } -LC_API void b2lc_chain_add_point(double x, double y) { if (s_chainCnt < LC_MAX_CHAIN) s_chain[s_chainCnt++] = v2(x, y); } +LC_API void b2lc_chain_add_point(double x, double y) { if (s_chainCnt < LC_MAX_CHAIN && finite2(x, y)) s_chain[s_chainCnt++] = v2(x, y); } static void retire_chain_segments(b2ChainId id) { if (!b2Chain_IsValid(id)) return; int n = b2Chain_GetSegmentCount(id); @@ -1532,8 +1532,8 @@ LC_API int b2lc_chain_create(int b, int isLoop, double friction, double restitut cd.points = s_chain; cd.count = s_chainCnt; b2SurfaceMaterial mat = b2DefaultSurfaceMaterial(); - mat.friction = (float)friction; - mat.restitution = (float)restitution; + mat.friction = (float)nonneg_or(friction, 0.0); /* reject NaN/Inf/negative — match the shape paths */ + mat.restitution = (float)nonneg_or(restitution, 0.0); cd.materials = &mat; cd.materialCount = 1; cd.isLoop = isLoop ? true : false; @@ -1559,9 +1559,9 @@ LC_API void b2lc_chain_destroy(int c) { chains_free_handle(c); } LC_API int b2lc_chain_is_valid(int c) { return b2Chain_IsValid(chains_get(c)) ? 1 : 0; } -LC_API void b2lc_chain_set_friction(int c, double f) { b2ChainId id = chains_get(c); if (b2Chain_IsValid(id)) b2Chain_SetFriction(id, (float)f); } +LC_API void b2lc_chain_set_friction(int c, double f) { b2ChainId id = chains_get(c); if (b2Chain_IsValid(id) && finite1(f) && f >= 0.0) b2Chain_SetFriction(id, (float)f); } LC_API double b2lc_chain_friction(int c) { b2ChainId id = chains_get(c); return b2Chain_IsValid(id) ? (double)b2Chain_GetFriction(id) : 0.0; } -LC_API void b2lc_chain_set_restitution(int c, double r) { b2ChainId id = chains_get(c); if (b2Chain_IsValid(id)) b2Chain_SetRestitution(id, (float)r); } +LC_API void b2lc_chain_set_restitution(int c, double r) { b2ChainId id = chains_get(c); if (b2Chain_IsValid(id) && finite1(r) && r >= 0.0) b2Chain_SetRestitution(id, (float)r); } LC_API double b2lc_chain_restitution(int c) { b2ChainId id = chains_get(c); return b2Chain_IsValid(id) ? (double)b2Chain_GetRestitution(id) : 0.0; } LC_API int b2lc_chain_segment_count(int c) { b2ChainId id = chains_get(c); @@ -1769,7 +1769,7 @@ LC_API int b2lc_query_overlap_point(int w, double x, double y) { LC_API int b2lc_query_overlap_circle(int w, double cx, double cy, double r) { q_reset(); b2WorldId wid = worlds_get(w); - if (!b2World_IsValid(wid)) return 0; + if (!b2World_IsValid(wid) || !finite3(cx, cy, r) || r < 0.0) return 0; b2Vec2 pt = v2(cx, cy); b2ShapeProxy proxy = b2MakeProxy(&pt, 1, (float)r); b2World_OverlapShape(wid, &proxy, b2DefaultQueryFilter(), overlap_cb, NULL); @@ -1778,7 +1778,7 @@ LC_API int b2lc_query_overlap_circle(int w, double cx, double cy, double r) { LC_API int b2lc_query_overlap_shape(int w, double radius) { q_reset(); b2WorldId wid = worlds_get(w); - if (!b2World_IsValid(wid) || s_polyCnt < 1) return 0; + if (!b2World_IsValid(wid) || s_polyCnt < 1 || !finite1(radius) || radius < 0.0) return 0; b2ShapeProxy proxy = b2MakeProxy(s_poly, s_polyCnt, (float)radius); b2World_OverlapShape(wid, &proxy, b2DefaultQueryFilter(), overlap_cb, NULL); return s_qCnt; @@ -1786,7 +1786,7 @@ LC_API int b2lc_query_overlap_shape(int w, double radius) { LC_API int b2lc_query_raycast_all(int w, double x1, double y1, double x2, double y2) { q_reset(); b2WorldId wid = worlds_get(w); - if (!b2World_IsValid(wid)) return 0; + if (!b2World_IsValid(wid) || !finite2(x1, y1) || !finite2(x2, y2)) return 0; b2World_CastRay(wid, v2(x1, y1), v2(x2 - x1, y2 - y1), b2DefaultQueryFilter(), cast_cb, NULL); if (s_qCnt > 1) qsort(s_q, (size_t)s_qCnt, sizeof(LcQRow), q_cmp_frac); return s_qCnt; @@ -1794,7 +1794,7 @@ LC_API int b2lc_query_raycast_all(int w, double x1, double y1, double x2, double LC_API int b2lc_query_shapecast(int w, double radius, double dx, double dy) { q_reset(); b2WorldId wid = worlds_get(w); - if (!b2World_IsValid(wid) || s_polyCnt < 1) return 0; + if (!b2World_IsValid(wid) || s_polyCnt < 1 || !finite3(radius, dx, dy) || radius < 0.0) return 0; b2ShapeProxy proxy = b2MakeProxy(s_poly, s_polyCnt, (float)radius); b2World_CastShape(wid, &proxy, v2(dx, dy), b2DefaultQueryFilter(), cast_cb, NULL); if (s_qCnt > 1) qsort(s_q, (size_t)s_qCnt, sizeof(LcQRow), q_cmp_frac); diff --git a/src/box2dxt.lcb b/src/box2dxt.lcb index 8516e83..265adee 100644 --- a/src/box2dxt.lcb +++ b/src/box2dxt.lcb @@ -130,7 +130,7 @@ private foreign handler _contact_begin_b(in pI as CInt) returns CInt binds to "c private foreign handler _contact_end_a(in pI as CInt) returns CInt binds to "c:box2dxt>b2lc_contact_end_a!cdecl" private foreign handler _contact_end_b(in pI as CInt) returns CInt binds to "c:box2dxt>b2lc_contact_end_b!cdecl" --- ===== ABI v3 additions: foreign bindings =========================== +-- ===== Extended foreign bindings (added at ABI v3; current ABI is 4) ===== -- shape-def builder (one-shot extras applied by the next shape creation) private foreign handler _shapedef_reset() returns nothing binds to "c:box2dxt>b2lc_shapedef_reset!cdecl" private foreign handler _shapedef_set_sensor(in pF as CInt) returns nothing binds to "c:box2dxt>b2lc_shapedef_set_sensor!cdecl" @@ -1165,7 +1165,7 @@ public handler b2ContactEndBodyB(in pIndex as Integer) returns Integer end handler -- ===================================================================== --- ABI v3 additions: public API +-- Extended public API (added at ABI v3; current ABI is 4) -- ===================================================================== -- ---- shape-def builder (one-shot; consumed by the next shape creation) ---- diff --git a/tools/check-livecodescript.py b/tools/check-livecodescript.py index d014884..d96ab26 100755 --- a/tools/check-livecodescript.py +++ b/tools/check-livecodescript.py @@ -14,9 +14,10 @@ ``setprop`` / ``before`` / ``after`` has a matching ``end ``. 3. Control-structure balance — every block ``if … then`` / ``repeat`` / ``switch`` / ``try`` is closed by its ``end`` inside the handler that opens - it. (Logical lines are reassembled across ``\\`` continuations and comments - are stripped first, so multi-line ``if … then`` and ``else if`` do not - false-positive.) + it, and a stray/extra ``end if`` (more closes than block-``if`` opens) is + flagged. (Logical lines are reassembled across ``\\`` continuations and + comments are stripped first, so multi-line ``if … then`` and ``else if`` do + not false-positive.) And it shells out to ``sync-embedded-kit.py --check`` so a single run also proves the embedded Kit copies have not drifted. @@ -127,7 +128,15 @@ def check_structure(text): kind = toks[1] if kind == "if": if ctrl and ctrl[-1][0] == "if": - ctrl.pop() # else: hybrid chain / stray — leniently ignore + ctrl.pop() + else: + # A hybrid if / else-if / end-if chain keeps its single pushed + # 'if' frame until the 'end if', so reaching here means more + # 'end if' closes than block-'if' opens: a stray/extra or + # misplaced 'end if'. OXT rejects an 'end if' with no open if. + errors.append( + f" L{lineno}: stray/extra 'end if' in handler '{handler[0]}' (no open block 'if')" + ) elif ctrl and ctrl[-1][0] == kind: ctrl.pop() else: