Porting catalogue - every built-in drill and game against the Lua API

Status: survey of fw 0.8.508 (2026-09-14) and the v0.1 API; every port DONE on fw 0.8.624 (2026-09-26) - the Lua twins live in foogolf-ota-cloud/extension-store/extensions/ (share codes in its README), and the runtime additions they forced are the 0.8.624 section of foodoublebassesp32/docs/lua-runtime.md. The Face angle notches drill (gameMode 32, 2026-09-23) post-dates the survey; its port follows the Face angle shape with docs/face-notch-drill.md as the spec. This is the examination the API was designed from: for each of the seven drills and two games, what it consumes, how its round works, what it scores, draws, says and persists, and which API calls a Lua port needs. Section 11 is the common functionality the survey found; section 12 lists the API decisions the survey forced. File and line references are into foodoublebassesp32/src/ at 0.8.508.

Legend for “port”: every bullet names an API call from api-reference.md. “Gap” means something the v0.1 API had to add for this port.


1. Face angle (FaceAngleReduxGame, gameMode 22)

InputfaceAngleBall only; every putt counts (no left-handed rule). Five fields cached for the widget.
RoundFixed 10 putts (not shots-per-drill). Resumable in memory (HOME keeps the picture; Play restarts).
ScoreFaceAngleReduxScore: quantise to 0.1 deg, factor exp(-sqrt(q / 2)) (1.0 at zero), score = 10 x mean over the putts so far. 10.0 all straight, 8.0 at 0.1, 4.9 at 1.0, 1.1 at 10. Trend key redux, recorded at the results page.
SettingsNone. No Settings row.
PictureFull-screen canvas, black. A circle edge whose sagitta grows with the envelope min(lo,0)..max(hi,0) (60 arc-degrees per degree), history of earlier edges as fading arcs (step 12 px, opacity 70), radial construction lines (dashed for the zero line), per-putt dots (newest orange 0xFF8C00), a blue 0x44AAFF rolling 3-putt average polyline. Labels: title 18 pt top-left; two 32 pt extreme values whose y tracks the edge ends; a third 32 pt “0” at the zero line; centre “Score” / value / “7/10”. 600 ms ease-out animation per putt at 30 ms frames. Combined widget bottom-right, rebuilt per putt.
AudioOwns audio (shot_speech = none); speaks the face angle after the redraw; no stimp in the opener; game-over tone at the end.
KeysHOME/OK exit to the menu; UP/DOWN/BACK nothing; on results any key restarts.
Results“Face angle” / “Drill over”, rows Score 7.5/10, Range L1.2 to R0.4, 10 s restart.

Port

  • foogolf.drill{ speaks_stimp = false, shot_speech = "none", trend = {label = "Face angle"} }
  • canvas.begin(0), canvas.arc, canvas.dashed_line, canvas.line, canvas.disc, canvas.invalidate
  • hud.title, hud.label x 6, hud.move (the value labels follow the edge)
  • hud.widget(shot)
  • audio.say_face_angle(shot.face_deg) from the tick after the redraw; audio.tone("game_over")
  • on_tick for the 600 ms ease (util.ease_out_cubic)
  • trends.record(score), ui.results{...}
  • Gap: hud.move (labels that track a picture), canvas.arc with sub-pixel width and opacity.
  • Pixel parity (fw 0.8.626): the history band is canvas.arc_band - the native’s own row-cut routine, not an approximation with canvas.arc.

The scoring formula itself is ten lines of Lua; the pure C++ class has a native test (test/test_face_angle_redux) whose table doubles as the script’s test vector.


2. Face rotation consistency (FaceRotationGame, gameMode 19)

InputfaceRotationRateDegPerInch (from the last shot’s saved rate). A rate above +0.20 deg/in (opening) is ignored: spoken “negative”, no dot, no count.
RoundFixed 10 counted putts. Resumable in memory.
ScoreConsistencyScore (shared with Arc strength): recomputed after every putt as 10 minus the sum of min(abs(rate - mean of ALL putts) / 0.15, 2.0), floor 0 - no free band, order-independent (fw 0.8.660, v7). Trend key frot.
SettingsNone.
PictureSame circle-edge renderer as Face angle with 120 arc-degrees per deg/in, pinned to the raw extremes (no zero line), pink 0xFF4FA0 rolling average. Widget bottom-right, rebuilt even on an ignored putt.
AudioOwns audio, no stimp; speaks the rate (“point three five”) or “negative”; game-over tone.
Keys / ResultsAs Face angle; rows Score, Range (-.08 to .35).

Port

  • Identical shape to Face angle: the same 200-line drawing module shared by both scripts (open-source scripts can share code by copy; there is no require).
  • shot.rotation; audio.say_rotation(rate); audio.say_cue("negative").
  • Gap: none beyond Face angle’s.

3. Arc strength consistency (ArcStrengthGame, gameMode 10)

InputThe same rotation rate and the same “ignore above +0.20” rule (an ignored putt does not update the widget here - a difference from Face rotation).
RoundFixed 10 (the shots-per-drill row was dropped because the drill never read it). Resumable in memory.
ScoreThe same ConsistencyScore. Trend key arc.
SettingsOne: Appearance arcLook = Classic / Stripes / Clean (default Stripes), NVS + guest cfg, live rebuild of the page.
PictureStripes/Clean: header green 0x1B4D1F, four stripe bands from the SBST / Slight / Medium boundaries (0.20 / 0.70 / 1.25 deg/in, 90 px per deg/in), a mirrored lighter negative band, band-name labels 14 pt, dashed guide lines and left-edge tangent arrows (Stripes only), the putter clubhead image at zoom 18 beside a 12 px ball with a two-disc halo, arcs through the apex (newest yellow 0xFFE400 5 px, older 0xB39F00 2 px). Classic: light blue 0x44AAFF background, black ruler with ticks, top-right text stack. No per-shot easing (accumulating arcs). Widget bottom-right in the Stripes family only.
AudioOwns audio; stimp IS spoken (inherits the default); rate or “negative”; game-over tone.
Keys / ResultsAs above; rows Score, Average +0.12.

Port

  • settings.choice{ key = "look", options = {"Classic","Stripes","Clean"}, default = 2 }, on_setting rebuilds.
  • canvas.rect bands, canvas.image("putter_clubhead", x, y, 18, 0); the arcs and the dashed guides are canvas.trace (the C++ draws them column by column with 2 px rectangles, dashed on the absolute column - the trace IS that fill; fw 0.8.626), the arrowheads canvas.polygon(..., "lvgl"), the ball and its halo canvas.rect(..., "circle") - the native’s LVGL objects.
  • hud.widget(shot) only when settings.look ~= 1.
  • Gap: canvas.image with zoom (the putter asset), canvas.polygon.

4. Swing path and lie (RailGunGame, gameMode 3)

InputleftSensorsInnerProportion, rightSensorsInnerProportion only; speed and face are shown, never judged.
RoundApp::shotsPerDrill() shots (3..20, default 10). Not resumable.
Score+1 when both arrow ends lie inside the chevron band, -1 otherwise (can go negative). Trend key path.
SettingsSkill level (sets the arrow sensitivity 2/5/8/12), Shots per drill - both device-wide rows.
PictureGrass texture full screen, eleven white chevrons on a raised band 0x2E8B12 with a shadow, an 8 px arrow (green 0x00FF00 hit / red miss) whose ends are 85 + (prop - 0.5) * 80 * sensitivity/2, speed 48 pt (mph or ft), face angle 48 pt, score 48 pt, the tick/cross row. Result held 600 ms. No widget: this drill IS the widget at full size.
AudioDevice shot speech stays ON (the only drill besides Close-in); hit / miss tones; game-over tone.
Keys / ResultsHOME/OK exit; rows Score, Difficulty (skill name).

Port

  • foogolf.drill{ shot_speech = "device" }, settings.shared("skill_level", "shots_per_drill"), device.shots_per_drill(), device.path_lie_sensitivity(), device.skill_name().
  • canvas.grass(0, 170, ...) or canvas.scenery, canvas.polygon chevrons, canvas.line arrow, hud.value, hud.indicators, hud.widget_text(shot) for the speed/face strings.
  • audio.tone("hit"|"miss").
  • Gap: canvas.grass (the Grass settings texture), hud.widget_text.

5. Lag putting (ContinuousSwingGame, gameMode 9) and Metrics only (gameMode 0)

InputSpeed + face for physics; the five widget fields.
Roundshots_per_drill stations, up to 3 putts each; a station ends on a good putt (+1) or after three. One random distance per station in [csMin, csMax] (4..120, default 4..100), re-rolled at station end unless UP/DOWN froze it (“manual”). Metrics only: no stations, no score, the target re-rolls every three putts forever.
PhysicsRollout by the shared model; resting position closed-form on the straight line; holed by wouldBallBeHoled; in real-time mode the rolling page’s own verdict wins. Good = holed or inside 3 ft.
AnimationFeedback real time: the perspective green rolls with the cup at the target. Instant: the settled view at once. Settled view = the green with a faint flag and no cup, a sight reticle over the ball (green when good), no history; a new distance zooms in over 850 ms.
ScoreStations won. Trend key lag. Metrics only records nothing.
SettingsSkill level, Min distance, Max distance, Stimp, Impact ratio. Guest cfg csMin csMax manual target.
PictureGreen 0x1A6004, title 18 pt, distance 48 pt top-right, tick/cross row, widget bottom-right.
AudioOwns audio; opener speaks the name then the distance; per putt the dominant-miss word (good / left / right / short / long) then the next distance when it changed; UP/DOWN re-speaks the distance after 900 ms quiet; game-over tone.
KeysUP/DOWN +-1 ft (manual mode); HOME/OK exit; results any key restarts.
ResultsRows Score 7/10, Putts 23.

Port

  • settings.number x 2, settings.shared("skill_level","stimp","impact_ratio"), device.shots_per_drill().
  • green.show{ flag = "faint", halo = false }, green.roll{ instant = device.instant_feedback() } (the default), green.reticle, hud.widget, hud.indicators, hud.value.
  • putt.dominant_miss(t) + audio.say_cue, audio.say_distance, timer.after(900, ...) for the settle speech, settings.set("target", ...) (not needed: manual target is session state, kept in a local).
  • A metrics_only flag is a second manifest in the same file? No: Metrics only is a separate script sharing the code by copy, trend = nil is not allowed for a drill - so Metrics only is declared as foogolf.drill{ trend = false } (gap: a drill with no trend, allowed only for a metrics view; open whether the built-in Metrics only stays C++).
  • Gap: the 850 ms distance zoom (green.show re-render animates the camera when hole_ft changes: added as green.show{ animate_ms = 850 }).

6. Close-in putting (StrokePlayGame Variant::CloseIn, gameMode 15)

InputSpeed + face; the seat’s impact ratio (the player’s own, else the device’s).
Round10 one-putt holes on the round engine, roster = current player. Tees dealt as a deck at hole 1 from [ciMin, ciMax] (2..120, default 2..15) so a guest plays the same distances; UP/DOWN retargets the current hole in place and freezes the deck. Never an online round. Resumable.
PhysicsThe rolling page’s verdict (holed, final position). Make pattern overlay computed per hole. Spoken tiers: good; short (rollout < distance - cup radius); left/right when a straight putt at this pace would drop; else left/right if abs(x) >= dy, otherwise long.
AnimationPerspective green; instant or real time per the Drill feedback setting; flash gold when holed, green when not; make pattern drawn on the idle view and during the roll; pulsing halo.
ScoreNet = good - miss (+-1 per hole). Trend key close.
SettingsSkill level, Min, Max, Stimp, Impact ratio. Cfg ciMin ciMax ciDist ciD0..2.
Picture“Putt 3/10” 18 pt, distance 48 pt top-right, tick/cross row, widget bottom-right; no seat cards.
AudioDevice shot speech stays ON, the verdict word chains behind it; opener speaks the name then the distance; the fanfare at the end (the only drill that does).
KeysUP/DOWN retarget; HOME/OK exit; results any key restarts.
Results“Game over” page: Name +3, Good: 6 of 10, 10 s restart.

Port

  • foogolf.drill{ shot_speech = "device" }; settings.number x 2 with min = 2.
  • A local deck: deck = {} of ten math.random(min, max); when the same script is played as a game in Online play the deck would go through round.new{ deck = ... } - but Close-in is a drill, so it is simply local.
  • green.show{ hole_ft = deck[h], pattern = putt.make_pattern(deck[h], device.stimp()) }, green.roll{ flash = ..., pattern = ... }, t.cue + audio.say_cue, audio.fanfare().
  • hud.title(("Putt %d/%d"):format(h, 10)), hud.value, hud.indicators, hud.widget.
  • Gap: putt.make_pattern, the pattern option, putt.cue tiers exposed as data.

7. Make distance (MakeDistanceGame, gameMode 18)

InputSpeed + face; widget fields.
RoundFixed 10 putts, no target. Not resumable.
PhysicsVerdict = maxMakeDistanceFt (0 = “Missed”); its own constant-decel roll for the animation, on a straight line; the ball sinks only when it stays inside the strip. A live odometer of feet rolled latches to “Missed” the instant the ball leaves the strip.
AnimationOwn first-person strip picture (eye height 5.79 ft, anamorphic 15x, horizon at y 85), the camera trailing the ball by 4 ft and tracking it sideways; a rolling frame is memcpy of a static backdrop (scenery above the horizon, the Grass base colour below) plus strip, ruler and ball; roll capped at 5.5 s (long rolls play faster), a 2.6 s result hold, then walk-back after a miss. Instant feedback: the ball drawn at its end, the verdict at once.
ScoreRound total feet of made putts (a miss banks 0); “Longest” on the results. Trend key make.
SettingsSkill level, Hole style (Yellow coat / Cup lines, mdHoleYel), Stimp, Impact ratio.
PictureTitle with counter, verdict 48 pt top-centre, running total 18 pt in brackets, ground-painted seven-segment ruler numbers, amber 0xFF8C00 cup rings, widget bottom-right.
AudioOwns audio; speaks the make distance or “missed”; game-over tone after the speech drains.
KeysHOME/BACK exit; OK fast-forwards; results any key restarts.
ResultsRows Total 312 ft, Longest 62 ft.

Port

  • putt.make_distance_ft(shot) is the verdict; putt.simulate(shot, {hole_ft = 1e6}) gives the straight roll and t:at(s) drives the ball (the strip has no cup interaction until the end).
  • canvas.scenery() + canvas.grass(85, 170) once, canvas.save(); per frame canvas.restore() then strip lines, ruler, cup, ball, canvas.invalidate (full width of the strip only).
  • settings.toggle{ key = "coat", on = "Yellow", off = "Cup lines" }, hud.title, hud.label for the verdict/total, hud.widget.
  • audio.say_distance(math.floor(d + 0.5)) / audio.say_cue("missed"), audio.tone("game_over").
  • Gap: canvas.scenery, canvas.grass, canvas.save/restore, canvas.text with a size list (the seven-segment ruler numerals become canvas.text(..., 8) or stay as line segments).

8. Golf course (StrokePlayGame, gameMode 11; stroke / match / skins)

Round1-4 seats from the active players (+ guests when hosting, four total); 9 holes default (1..18, mpHoles); par 2; tees from the USNDP 18-distance deck shuffled once (default) or random in [putMin, putMax]; honour dealt by OnlineRound::dealHonour (random, alternating devices), later holes fewest-strokes-first, the away ball plays next; no pick-up limit; a leave becomes the next stroke’s distance; resumable; a finished round shows the all-players scorecard for 10 s with the fanfare and restarts.
PhysicsPerspective green show(speed, face, dist, stimp, seat ratio), verdict from the page: holed = gold flash + holed tone; inside 3 ft = green flash + hit tone; else blue 0x44AAFF flash + miss tone. Games always roll.
ScoringStroke: to-par over completed holes (sat-out holes skipped). Match: pure MatchPlay_* (1 point per hole won). Skins: pure Skins_* (carry, void, leader mask). Solo plays a Par seat. Colours: leading red 0xC0392B, trailing blue 0x2C6FBB. Handicaps: PuttStats_Record per local seat’s putt - the only feed on the device.
OnlineOnlineRound::State JSON <= 1600 B; the host publishes before and after each verdict; guests overwrite their engine and replay other seats’ putts; “Putt ignored, waiting for X on Y”; ” has left”; strike-through of a non-following device’s seats; sit-outs; a spectator’s private round.
PictureGreen canvas; “Hole 3/9” (+ “This hole: 2 skins”); distance 32 pt top-right; bottom-left player card (queue above in reverse order, the current seat’s one 14 pt line: dot, white hole box, name, coloured score box, “Shot N”); widget bottom-right; classic scorecard page (holes as rows, Feet yellow, Par, SI red, A-D, birdie circles, bogey boxes, Tot/Pts/Skins, Hcp/Rnd) with up to five archived cards.
SettingsSkill level, Distances (USNDP / random), Min, Max, Number of holes, Players; Scorecard row; the Scoring row (the Games row’s Scoring cell until fw 0.8.673).
AudioOpener name + stimp, then “hole N . colour . 12 feet” (hole only when changed); tones per verdict; fanfare at the end.
KeysHOME/OK exit; any key on the finished card restarts.

Port

  • foogolf.game{ online = true, scoring = "device", players = "active" }.
  • settings.number Min/Max, settings.number holes (1..18), settings.choice Distances, settings.shared("skill_level").
  • round.new{ holes = settings.holes, par = 2, tees = settings.dist == 1 and "usndp" or "random" }.
  • green.show{ hole_ft = r:remaining_ft(r.turn) }, hud.hole_line(r), hud.value, hud.player_card(r), hud.widget.
  • green.roll{ instant = false, flash = ..., on_done = ... } then r:record{...}, audio.tone(...), r:announce_turn(), r:skins_halved() + ui.flash("Skins carry over ...").
  • ui.scorecard(r) (Scorecard menu row via settings.action), ui.results_card(r) + audio.fanfare().
  • Not portable: the handicap feed (deliberately, section 10.5 of the reference).
  • Gap: the whole round module and the four standard cards; settings.action for a Scorecard row.

9. Mini golf (MiniGolfGame, gameMode 23)

RoundSame engine shape with a course: seed (mgSeed) -> deterministic holes, par 1..3 from a Monte Carlo playability model, up to 18 holes (mgHoles default 9), pick-up at 6 strokes, honour and away-player by route distance; scoring by the Scoring row (solo plays a Par seat built from each hole’s own par); resumable.
PhysicsLaunch angle = explicit aim + face; the shared roll model and cup predicate; walls with restitution 0.65 / 0.92 tangential, 45-degree chamfers, 0.4 ft thick; 2 ms steps, 15 s cap; bounces tick the key sound.
AnimationOwn canvas: 22 x 9 cells of 2 ft (44 x 18 ft), hole-fit camera (at most 70 px/ft), no zoom during the roll (a user decision), only the ball’s patch redrawn per frame; blueprint background (yard grid labelled in feet from the cup) or green; 3-leg aim path with bounce dots; brick walls, mortar, cup, tee ring.
OnlineThe round state extended with g = 23, seed, ball bx/by, per-hole par, shot ang; any tethered device may aim (absolute angle + sequence, host stamps the sender; guests skip their own echo); OK menu (Cancel / Repeat aim / Mulligan) from any device.
PictureHUD strip top (“Hole 3/9 Par 2”, turn dot, “Name Shot 2”), score strip at y 150 (dot + score per seat, red when leading), “N ft to cup / wall” bottom-right, big notices in a translucent box (“Holed!”, “Hole in one!”, “Picked up (6)”).
SettingsNumber of holes, Regenerate course, Background (Blueprint / Green), Stimp, Players.
AudioOpener, “hole N . colour” (no distance: the player aims first), holed tone, key tick on bounces, game-over tone.
KeysBACK/RIGHT aim 1 deg, UP/DOWN 0.25 deg, OK popup (or fast-forward while rolling), HOME exit.

Port (v1.1)

  • foogolf.game{ online = true }, settings.number holes, settings.action Regenerate (course.seed() re-mint is a runtime call (gap: course.new_seed())), settings.choice Background.
  • course.generate(seed, h), course.fit_view, course.draw(hole, view, {background = ...}), course.aim_path, putt.simulate(shot, {course = hole, from = ball, aim_deg = aim}), t:at(s) + canvas.disc + canvas.invalidate for the ball patch, audio.tone("key") per bounce.
  • round.new{ holes, par = function(h) return holes[h].par end, tees = nil, pick_up = 6, deck = {seed} }, r:record{ seat, holed, x, y }, r:undo() for mulligan, hud.score_strip(r, 150), ui.popup, ui.box.
  • g.state() = { aim = ..., ball = {...} }, g.on_input("aim", t, from), net.send("aim", { deg = a, seq = n }).
  • Gap: the whole course module and course.draw; r:undo; hud.score_strip; ui.box; ui.popup; sequenced net.send with sender stamping.

Done. The port shipped in stages and was completed on 2026-09-18: course.*

  • the segment physics (fw 0.8.514), the canvas (0.8.514), round.* with the score strip, results card and r:undo (0.8.530), ui.popup / ui.box (0.8.528/0.8.530), the scorecard browser (0.8.533), the device-offered mulligan (0.8.565) and finally settings.choice for Background, cross- experience Resume and the shared online round (0.8.570: net.*, g.state() / g.apply_state(t), g.on_input, shot.remote / seat / aim). The script is foogolf-ota-cloud/extension-store/extensions/mini_golf.lua, published as CFZW5. course.draw was never needed - the script draws the course itself, which is the point of the API. Still not ported from the native game: nothing.

10. Sim integrations (GSPro Direct, GSPro port 8888, Sim sharing, Garmin R10)

Out of scope for scripting: they forward shots to external software over the network and USB HID, hold sockets and pairing state, and the user has said the USB host transport stays as it is. They are not drills or games in the sense of this API and no script facility is planned for them.


11. Common functionality the survey found

What every experience (or most) does the same way - these became the runtime’s shared parts rather than script code.

Lifecycle. All nine implement the same Game contract: activate / deactivate, forward(msg) (cache the widget fields), onShot(speed, face) with a null shot meaning “take the screen” (Play, Resume, boot), tick returning true while busy, forceStop for HOME. Every drill defers a screen swap out of a key handler with lv_async_call. => hooks in section 2 of the reference; deferral is the runtime’s.

The shot. Every experience reads at most seven numbers of the message: club speed, face angle at the ball, rotation rate, attack angle (games, for the wire), the two inner proportions, the impact ratio. Everything else in SensorMessage::calculations (heights, pivot, turf contact, timing) is unused by any experience. => the shot table.

The combined widget. Seven of nine build PathLieMini_Build(screen, BOTTOM_RIGHT, -6, 0, ...) with the same five inputs and the device’s speed display, stimp, impact ratio and skill sensitivity, rebuilt per putt. => hud.widget(shot).

Physics. One roll model (computeEsp32DistanceFt), one cup predicate (isCapturedAtRim), one rim rebound (25 %), used by the perspective page, the side page, wouldBallBeHoled, maxMakeDistanceFt, MakePattern and the mini golf stepper. => putt.*.

The perspective green. Lag putting, Close-in and Golf course share PerspectiveShotPage + PerspectiveGreenScene (frame, drawScene, overlay hook, halo, flash, reticle, make pattern) and the instant / real-time switch. => green.*.

Own canvas. Face angle, Face rotation, Arc strength, Make distance and Mini golf each allocate one 320 x 170 PSRAM canvas and draw with float anti-aliasing (two of them with a verbatim copy of the same renderer). => canvas.* with the AA primitives; one canvas per experience.

Drill scaffolding. DrillCommon: the focus proxy, the results screen (name + “Drill over” + two rows), the tick/cross indicator row, the 10 s restart countdown, and Drill_RecordTrend (append + flush). => ui.results, hud.indicators, trends.record.

Menus. Every experience is one GameMenuSpec row: Play or Resume + New, then Settings / Trends / About; settings rows are numeric pages, list pages or in-place toggles whose labels carry the live value; every About page is the five-section content page. => the manifest, settings.*, about.

Audio. Every experience uses the fixed clip pack through the same helpers (name + stimp opener, distance, face angle, rotation, the verdict words, the turn announcement) and the same six tones; the universal rule is draw, lv_refr_now, then speak from the next tick, and hold busy until the speech drains (2.5 s cap). => audio.* and the busy rule.

Settings and guest cfg. Every own setting is an NVS key in foogolf, a GuestMode::noteLocalSettingChange() on a hand change, and an exportConfig / importConfig pair in the 16-slot integer cfg. => settings.* with the same three effects.

Round engine and online. The two games share seats, holes, honour, away-player, lock-step, the three scorers, the player card, the results card, the scorecard and the OnlineRound::State relay. => round.*, hud.player_card, ui.scorecard, ui.results_card, section 13.

Keys. During play HOME and OK leave to the menu in every drill; on every results page any key but HOME restarts; UP/DOWN are raw only when the page asks for editing mode. => on_key, keys_editing.


12. API decisions the survey forced

  • shot_speech = "device" | "none" because two drills kept the device cue and five owned their audio. Gone in effect since fw 0.8.672 (user, 2026-10-13): the Audio feedback setting is removed, the device speaks nothing per shot and shot_speech is ignored; Close-in v6 and Swing path v6 speak the face angle themselves (audio.say_face_angle).
  • speaks_stimp because two drills omit it and Arc strength keeps it.
  • trend is mandatory on a drill except for a metrics view (Metrics only), which is why trend = false is allowed and documented as a special case.
  • traj.cue carries the Close-in tiers and putt.dominant_miss the Lag putting rule, because both are policy the built-in drills expose to players and a port must reproduce them exactly.
  • green.show{ pattern = ... } and overlay because Close-in draws the make pattern under the ball and over the roll.
  • hud.move because Face angle’s value labels track the picture’s ends.
  • canvas.save/restore because Make distance’s frame is a backdrop memcpy plus a few strokes, and Mini golf’s patch redraw is what keeps a cast cheap; canvas.invalidate for the same reason.
  • canvas.image("putter_clubhead", ...) because Arc strength draws the clubhead asset; canvas.scenery / canvas.grass because Make distance and Swing path paint the device’s scenery and grass.
  • round.new{ deck = ... } because the Close-in deck and the mini golf seed are “host deals, guests adopt” values.
  • r:undo(), ui.popup, ui.box, net.send with sequencing because Mini golf’s mulligan, OK menu, notices and any-device aiming need them.
  • settings.action because Regenerate course and Scorecard are menu rows that are not values.
  • pick_up, one_putt and par = function because Mini golf, Close-in and Mini golf again differ from Golf course only in those.
  • No handicap feed: a user rule, kept as an open decision rather than an API.

13. Facility matrix

Face angleFace rot.Arc str.Swing pathLagClose-inMake dist.Golf courseMini golf
shot.face_degxshownxxxxx
shot.rotationxx
shot.path_*x
shot.speed_mpsshownxxxxx
putt.simulate / green.rollxxown stripxcourse
putt.make_distance_ftx
putt.make_patternx
green.*xxx
canvas.*xxxxxx
hud.widgetxxStripesxxxx
hud.indicatorsxxx
settings.* own122143
trends.recordxxxxxxx
round.*xx
onlinexx
audio.say*facerateratecue+distcue+distdistturnturn
device shot speechxx
fanfare at endxx
instant/real timen/an/an/an/axxxalways rollalways roll

Rendered from golfclaude/docs/lua/porting-catalogue.md at 8ef01dd, 2026-10-26. The Markdown in the repository is the source; this page follows it on every push.