Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
2 changes: 1 addition & 1 deletion docs/expansion-prep.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
23 changes: 15 additions & 8 deletions docs/kit-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.)
Expand Down Expand Up @@ -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)
```

---
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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` ·
Expand Down
22 changes: 11 additions & 11 deletions src/box2d_lc.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -1778,23 +1778,23 @@ 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;
}
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;
}
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);
Expand Down
4 changes: 2 additions & 2 deletions src/box2dxt.lcb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) ----
Expand Down
17 changes: 13 additions & 4 deletions tools/check-livecodescript.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@
``setprop`` / ``before`` / ``after`` has a matching ``end <name>``.
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.
Expand Down Expand Up @@ -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:
Expand Down
Loading