Blind accessibility mod for Cook, Serve, Delicious! 3!?
  • Game Maker Language 43.8%
  • C 34.3%
  • Python 8.1%
  • Rust 7.5%
  • C++ 3.1%
  • Other 3.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
guilevi 19c62a40e1 Recipe guidance: page turns, repeat counts, stray steps; keybinds, buffers, voices
Paged and repeated recipes are guided step by step, phantom backslash keys
and stray food-name steps are gone, recipe notes only ask for keys the order
needs. Also: accessible keybinding screens, holding-station buffer, buffer
settings, number panning, new Guy/Jenny voices (Shannon/Sophie removed),
installer changelog. See changes.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 17:58:30 +02:00
abbrev Stations to 8, cooking/board/settings fixes; release notes for PRISM licence and SmartScreen 2026-10-01 21:50:55 +02:00
ext Recipe guidance: page turns, repeat counts, stray steps; keybinds, buffers, voices 2026-10-19 17:58:30 +02:00
gml Recipe guidance: page turns, repeat counts, stray steps; keybinds, buffers, voices 2026-10-19 17:58:30 +02:00
installer Recipe guidance: page turns, repeat counts, stray steps; keybinds, buffers, voices 2026-10-19 17:58:30 +02:00
patch Recipe guidance: page turns, repeat counts, stray steps; keybinds, buffers, voices 2026-10-19 17:58:30 +02:00
patreon add windows installer 2026-09-26 08:49:02 +02:00
recipes Buffers, verbosity keys, correct food names, autosave 2026-09-24 00:25:09 +02:00
sounds/Voices Recipe guidance: page turns, repeat counts, stray steps; keybinds, buffers, voices 2026-10-19 17:58:30 +02:00
tools tools: scan.py (recipe-script scan) and mk.py (make practice orders) 2026-10-19 17:58:30 +02:00
vendor Access Delicious: screen reader accessibility for Cook, Serve, Delicious! 3?! 2026-09-23 21:36:26 +02:00
.gitignore add windows installer 2026-09-26 08:49:02 +02:00
changes.md Recipe guidance: page turns, repeat counts, stray steps; keybinds, buffers, voices 2026-10-19 17:58:30 +02:00
game.mk Orders grouping, Shift+D, trash/perfect-stop voicing, fixes; state-based test driver 2026-09-24 13:15:00 +02:00
Makefile Recipe guidance: page turns, repeat counts, stray steps; keybinds, buffers, voices 2026-10-19 17:58:30 +02:00
README.md Recipe guidance: page turns, repeat counts, stray steps; keybinds, buffers, voices 2026-10-19 17:58:30 +02:00
run.sh loads of shit 2026-09-25 18:05:16 +02:00

Access Delicious

Screen-reader accessibility mod for Cook, Serve, Delicious! 3?! (Windows, Steam build 14196546 / v1.02.03). Speech goes through PRISM (NVDA, JAWS, UIA, SAPI… on Windows; under CrossOver, the prism_crossover backend + macOS helper). Windows is the only target; development and testing happen on a Mac under CrossOver. (A Mac-native prototype was archived outside the repo, in ~/src/claude/access-delicious-mac-archive.)

Read this before working on the mod. Log each day's user-visible changes in changes.md. Game logic questions: read the decompiled source in re/win_gml/CodeEntries/ first (local only: re/ holds the game's decompiled code and is not in git), then go to the screen in-game and screenshot it to see what a sighted player actually sees.

How it works

The Windows data.win is GameMaker bytecode (not YYC), so UndertaleModTool can decompile and recompile it. The mod is GML injected into the game plus a small DLL.

  • gml/gml_Object_obj_csd3a_*.gml — the controller object's events. Created from O_initvars Create, persistent.
    • Create: binds the DLL (external_define), then the lib methods run (see below).
    • Step_1 (Begin Step): control-socket commands, mod hotkeys (cleared with keyboard_clear before the game's Step events see them), key-injection queue.
    • Step_2 (End Step): the announcers, then text_end_frame().
  • gml/lib/*.gml — all mod functions, as methods on the controller (name = function(...) {...};), concatenated alphabetically into Create by the patcher. Create-time statements in a lib file may only use things defined in files that sort before it.
  • ext/accessdelicious.c → accessdelicious.dll (mingw): PRISM speech, a log file, and the development TCP control server.
  • patch/patch.csx — the UndertaleModCli script that builds the patched data.win:
    1. imports the mod code (lib concatenated into Create);
    2. appends the controller's creation to O_initvars Create; 2b. prepends with (obj_csd3a) hotkeys_once(); to O_foodbrain Begin Step: the game reads order/station keys there, before the persistent controller's Begin Step, so mod hotkeys (Shift+number) must be handled and cleared from inside it;
    3. before/after every instance_deactivate_all(...): obj_csd3a.before_pause(); and instance_activate_object(obj_csd3a); (tutorial popups deactivate everything; gameplay objects can't be read while deactivated, so state is snapshotted just before);
    4. rewrites every draw_text* call (exact variants only; not draw_texture_*) to obj_csd3a.dt_text*(object_index, ...) so the mod sees every string drawn, by which object and where (lib/text.gml).

Modules (gml/lib)

file covers
text.gml text capture model (text_of(obj), text_new), grace 12 frames
fallback.gml speaks unhandled drawn text (queued); silent list; subtitle objects
menus.gml the game's widget framework (all Menu children): focus, values, positions, locked items, alerts, descriptions
routeday.gml / territory.gml route/day and territory map menus (custom-drawn); day medals and road-closed gates from global.MODEchefforhire
pause.gml in-day pause menu (O_pausemenu)
todaysmenu.gml Today's Menu and the main menu's Food Catalog (FoodCatalog): slots, Begin, setup/filters carousels, food grid, food card (actions, stats) — state machine documented at the top
splash.gml day card (O3_newsplashstartend): start (route, day, hint pages) and end: a results list walked with Up/Down (splash_items: medal, YUM, % to next level, money, CC, orders, points breakdown, menu, route), medal rules
cook.gml recipe/prep pane: title, recipe lines, keys (per page; locked steps), placed ingredients, board info, batch size
orders.gml customer tickets (O_foodbar): new, cooking/ready/about to burn/burnt/leaving, walked out, boxed, served (result), impatience; truck status (driving/arriving/stopped)
holding.gml holding stations (O_holdingstationbar): lifecycle (incl. ruined by mistakes), rival attacks (station destroyed, food rammed), the dead key of a destroyed station, status, Shift+H summary
rival.gml rival truck attack banners (O_foodbrain.LVTT_FTAtext): "Truck attacked! Incoming Orders Hidden!"; the holding-station ones are left to holding.gml
saving.gml "Autosaving" while O_savinggame (the spinning pickle) writes the save; queued after the menu change that starts it
recipes.gml generated (recipes/gen.py, from re/): food/stage recipes that vary per customer; those orders say "Wants" instead of "Recipe"
buffers.gml arrow-key buffers during a day: Menu Board (board_items), Orders (orders_groups), Holding Stations (holding_status per station); each can be off (Accessibility > Buffers)
board.gml Shift+B: the menu board computed like the prep pane draws it (also read when F9 opens it)
settings.gml ini settings, hotkeys, pause snapshot, per-handler error isolation
ctl.gml development control commands, ctl state (JSON for tools/drive.py)
access_menu.gml Options > Gameplay > Accessibility: speech rate, subtitles, station keys, already placed keys, Buffers submenu (each buffer Off or its order), order, stale-station and destroyed-station number voices, number volume (patch.csx 2c/2d)
sounds.gml the mod's own sounds (sounds/ → <game>/accessdelicious/sounds/, decoded and panned by the DLL, played one at a time): order number when an order is ready, station number when a holding station goes stale or is destroyed, in voices from sounds/Voices
upgrades.gml Upgrade Truck (UpgradeMenu): focused node (name, cost/level, on/off, grid row/side, "needs the one above"), description once per node, key summary on open, purchases/toggles/newly unlocked, refusals (locked, max, too few parts, S on unowned); focuses the first node on open (the game ignores Enter until an arrow)
trinkets.gml Customize Truck (TrinketsMenu): modes, trinket list (name, poster/statue, price, owned/new/placed, unlock condition), filter row, refusals (locked, can't afford, truck full, nothing to edit), purchases, placing/choosing with area, row/column and the nearest trinket per direction (left/right/above/below; the bottom counter's rows are behind/in front; "overlapping" within 90 px)
keybinds.gml key rebinding: Options > Keyboard key buttons (modal wait, refused keys, keys shared by two actions), Import/Export file dialogs (patch.csx 2e) and Revert, Practice per-food binding by keyboard (F1, ingredient's key, new key; F5), ing_key_name (an ingredient button's key as its cap shows it: custom, universal, keyboard-type and arrow/punctuation binds)
abbrev.gml generated (abbrev/gen.py, run by make): full names for abbreviated ingredients/foods, expand_name(name, food); edit abbrev/hand.tsv

Keys (mod)

  • Shift + holding-station key (F1..F8 by default, global.KEY_HS<n>): station status ("1, 3 Classic Pretzels ready, freshness 4 of 6": servings before the food; no "Holding station"; the number is left out when Speak Numbers on Shift+Station Keys is off, ini speech/numbers, which also covers Shift+)
  • Shift+O orders, grouped by food and state, most urgent first ("Falafel leaving 1, 2; Blancmange ready 3") · Shift+I order numbers, most impatient first ("3, 1, 5": leaving, then patience stage, then time left before walking out) · Shift+U order numbers about to burn or spoil ("3, 5") · Shift+L order numbers leaving · Shift+ (1..7) one order in detail (stage, station, mistakes, patience) · Shift+B menu board (needed/makes/cooking, any time) · Shift+D truck: distance to the next stop or customers remaining, stop N of M · Shift+R re-read the open cooking pane + placed so far (keys always included) · Shift+K the pane's keys · Shift+W what the open order still needs (the recipe lines left to do, from the pane's placed slots stackCKarray[i][1]; twice within 250 ms: the whole recipe. An instruction naming several buttons gets each key after its name, "Add Dough D, then Flatten F"; stacked recipes (LV_recipestack: Moussaka, lasagnas, Baklava, Tiramisu…) say "In order: …" from stackCKarray[i][0] instead of their instruction text) · buttons the recipe needs (RCP_ar[slot][2] times, what O_NEWingredientpane_Other_12 checks) that no line names (Ratatouille's 22 Veg.Slices under "Sliced Vegetables", chores) are said from that data, by page, in recipe-text then button order ("Now Vegetable Slices R, A, T…; page 2: …"); pressing one says the next ("Now A", "Space: page 2"); a greyed-out key says "A already placed" (Accessibility > Speak Already Placed Keys, ini speech/already_placed)/"S locked"; a used-up page with work left says "Space: next page" · Alt+K keys spoken when a pane opens on/off (ini speech/keys) · Shift+M medals (the medal today's counts earn so far, the counts, then targets; also in the pause menu; territory map: totals) · Shift+H empty holding stations, then expired ones, then destroyed ones ("3, expired 1, 4, destroyed 2")
  • During a day, buffers: Left/Right switch (Menu Board, Orders: one item per Shift+O group, Holding Stations: one per station as Shift+F says it, by number or empty ones first; buffers turned off are skipped, all off leaves the arrows to the game) and read ": "; Up/Down move through items (built on each press; "empty" if none). Arrows pass to the game while paused, on the day card, in tutorial popups, and when bound to Page Turn or an open pane's ingredient (buffers.gml).
  • End-of-day card: the headline (medal) is announced, then each results line as the card's animation reveals it (LV_endgameanistage, stages in splash_reveal); Up/Down walk only the revealed lines, in screen order (the game only uses Left/Right there). Enter skips the tally (money, CC and level are then read at once), then continues.
  • Upgrade Truck (the game's keys, summarized when the menu opens): arrows move between upgrades · hold Enter (or Space) to buy, or buy the next level · S switches a bought upgrade on or off · Escape leaves. Pressing Enter on one you can't buy says why (locked, max level, too few parts).
  • Shift+T subtitles on/off · Shift+= / Shift+- (or Shift+PgUp/PgDn) speech rate, only if the PRISM backend reports SUPPORTS_SET_RATE (csd3a_can_rate; else "Speech rate is set in your screen reader") · Left Ctrl stop speech (not swallowed: Ctrl is the game's default auto-serve key)
  • Options > Gameplay > Accessibility holds the same settings (plus Buffers, a submenu with one carousel per buffer: Menu Board Off / holding station order / greatest need: most batches needed first, then most servings, then holding-station-only foods, which also orders Shift+B, kept while the buffer is off; Orders Off / On; Holding Stations Off / by number / empty stations first; ini buffers/board, buffers/orders, buffers/holding, board/sort; Order Number Voice, Stale Station Voice and Destroyed Station Voice, each Off or a voice, default Samantha; Number Volume for both, 0–100%, default 50%; and Number Pan Width, 0–100%, default 50%: changing it plays 1 then 7. Changing a voice previews the game's sound (order ready / stale alert / rival gunfire) with a random number; changing the volume plays just a number; each cuts off the last preview). Saved in accessdelicious.ini (sounds/voice, sounds/stale_voice, sounds/damaged_voice, sounds/order = the volume).
  • Order ready: alongside the game's cooked sound, the order's number (sounds/Voices/<voice>/N.ogg; Evan and Samantha made by their generate.sh: say, normalized, about 4 dB louder). Holding station going stale (final warning, SND3_freshalertnew): the station's number, same files. Holding station destroyed by a rival truck: "Holding station 1 destroyed, Classic Pretzels lost" and its number. A rammed truck (food gone, stations intact): "Food destroyed in holding stations 2, 3". Pressing a destroyed station's key (the game does nothing): "Holding station 1 destroyed". Stations go up to 8: only Evan and Samantha have 8.ogg (a missing file is logged and skipped). Voices are the folders with a 1.ogg, found at runtime (sounds_voices): a new folder needs no rebuild. Gain = volume^0.67 × the game's UI sound volume, so 50% ≈ plain say and 100% = +4 dB. Streams are in no audio group: the master volume applies, group mutes don't. Panning: pan runs -1 for number 1 to +1 for the last order slot (O_foodbrain.FB_LV_PScount) or holding station (FB_LV_HScount), times the width. GameMaker's emitters (audio_play_sound_on) left the streamed files centred under every falloff model, so the DLL decodes the .ogg (ext/stb_vorbis.c), mixes it to mono and writes 16-bit stereo with a balance law into a GML buffer (csd3a_ogg_load/rate/pan); audio_create_buffer_sound plays it, cached per file and pan. The buffer must be buffer_filled from GML first, or the runner calls it empty and makes no sound.
  • The user prefers letter keys over F-keys for mod functions.

Build, deploy, run, test

make deploy          # build DLL + patched data.win from the ORIGINAL (depot copy), copy into the bottle
./run.sh             # relaunch CSD3 in the CrossOver "Steam" bottle (Steam must be running there)
./run.sh --quiet     # the same for test drivers: speech only logged ("(quiet)"), master volume 5%
tools/ctl.py quiet off   # hand a quiet game to the player (speech on, their volume); "on" goes back
make restore         # put the original data.win back
  • Windows single-exe installer: installer\build_installer.bat [version] (Rust, MSVC, CMake, Ninja) → build\AccessDeliciousInstaller.exe, a wxWidgets window like the other mods' installers but with no download: it carries this mod version (the xdelta patch from the untouched data.win to dist\data.win, a release accessdelicious.dll built with MSVC, PRISM, the voices). The original data.win comes from CSD3A_ORIG, default the Steam install's data.win.orig (else data.win). --cli, or --install/--uninstall [--game DIR] for scripts. It refuses to run while CSD3.exe is open. cargo test --release -- --ignored with CSD3A_E2E_DIR (a folder with a CSD3.exe and the original data.win) runs the real patch.
  • Release: make release VERSION=x.y → build/AccessDelicious-x.y.zip (~1.1 MB, no game data): access-delicious-setup.exe (installer/setup.c, xdelta3 decoder from vendor/xdelta compiled in, GPLv2), accessdelicious.xdelta (ORIG → patched data.win, ~450 KB), a release accessdelicious.dll (-DCSD3A_RELEASE: control server only if accessdelicious.dev exists in the game folder) and the PRISM files. The installer checks both SHA-256s (build/release/hashes.h), keeps data.win.orig, and offers update/remove when run again. UI is message boxes, the folder picker and a wait window; --yes skips them and prints to stdout (tests under wine). Under Wine it hard-links prism-crossover-ttsd (CopyFile drops the executable bit). The xdelta encode (-9, 1 GB window) takes over 10 minutes. If PATCHED_SHA256 in hashes.h is unchanged, the old build/release/accessdelicious.xdelta still fits: check it decodes to that hash, touch it, rerun. ORIG lives in the SteamCMD depot folder; if it's gone, re-run download_depot 1000030 1000031 (the cached login expires: the user logs in with steamcmd.sh +login guilevi +quit first) and check its SHA-256 against ORIG_SHA256. A game update changes ORIG. make check-game compares Steam's public build with game.mk; make update (tools/update_game.sh) downloads the new depot with SteamCMD (cached login STEAMUSER, default guilevi), keeps the old data.win in backup/, syncs the CrossOver install, re-decompiles into re/, regenerates abbrev/recipes, writes game.mk, then deploys and builds the release. Test before shipping; add the game's version number to GAMEVER in game.mk by hand (it isn't in the game data).
  • Game install used for testing: CrossOver bottle /Volumes/T7/Bottles/Steam (external SSD "T7"), game copied from the SteamCMD depot (~/src/claude/tools/steamcmd, user guilevi cached) plus a hand-written appmanifest and steam_appid.txt.
  • The bottle's graphics backend must be DXMT (D3DMetal fails GameMaker's CheckMultisampleQualityLevels with E_INVALIDARG dialogs). CrossOver's wine CLI needs CX_BOTTLE_PATH=/Volumes/T7/Bottles; the bottle config overrides CX_GRAPHICS_BACKEND.
  • Log: <game folder>/accessdelicious.log (previous run .log.1); game stdout: ~/Library/Logs/access-delicious/wine.log.
  • Control channel: tools/ctl.py <cmd> (127.0.0.1:47700; CSD3A_TCP=host:port overrides) — ping, room, insts, vars <Obj|id|global>, get <target> <var>, screen [obj], ui, hs <n>, rival <attack> (spawns a rival truck: DisableHS, Disable2HS, RamHoldingStations, HackingTypeA…), orders, key <k>, keys <k>..., chord shift 1 (keys held together: modifier tests), hold enter 130 (one key held N frames: hold-to-buy), upgrades (the upgrade tree), board, order <key>, expand <name>, mods, say <text>, practice <food code> (Food Catalog practice for any food, owned or not, from the main menu; Escape leaves it). One client at a time: while play.py runs it holds the connection and ctl.py times out (the game isn't hung — check the log is still growing).
  • tools/drive.py start|play|day: the test driver. Reads ctl state (JSON: screen, menus, Today's Menu, stations, orders, board, the open pane's recipe as data), never spoken text, so wording changes don't break it. Loads save slot 0 (1S_…, the test save; --slot), fills Today's Menu to the day's points (--food NAME makes room for that food first), plays (serves, makes specials, preps stations the board needs), and recovers: actions with no effect are blocked for a while; a screen that doesn't change gets Enter (Escape only in panes/cutscenes; on the front end it returns to the title). The day-1 tutorial needs F4, which it can't press. tools/nav.py mainmenu|prep|orders still drives the tutorial; start_day.sh and play.py are the old text-scraping drivers.
  • tools/scan.py <decompiled-gml-dir>: static scan of the SCRIPT_foodpopulate_* recipe scripts for phantom buttons, instructions naming buttons a variant doesn't use, and LS_foodname ≠ LS_abbrev.
  • tools/mk.py [--until REGEX] [--max N] [--shiftw]: in practice, opens waiting orders one by one, prints what the mod says, makes each from the pane's plan and follows multi-stage orders. Takes focus like drive.py.
  • The user's own game is in another save slot (2S): never load it from a driver.
  • Injected keys only work while the game window has focus (the runner drops simulated key state otherwise; control commands like get still work). If keys do nothing, run tools/focus.sh first. The user typing in Terminal takes focus away.
  • Screenshots: tools/winshot.sh [out.png] (CGWindowList, owner CSD3.exe, then screencapture -x -o -l <id>).
  • A GML runtime error shows a blocking "Code Error" dialog: check for it (window title) after launches. Keep waits short (~30–45 s).

UndertaleModTool compiler traps (all hit in practice)

  • Calls to newly added global script functions bind wrongly (runtime "<unknown_object>.name not set") → everything is controller methods.
  • Indexing a call result (f(x)[0]) fails: "Invalid expression on accessor" → a temp variable.
  • [$ key] compiles as an array index → variable_struct_get/set.
  • Avoid ?? and [? key] → nz(), ds_map_find_value/ds_map_set.
  • mod is an operator (not a variable name).
  • A function can't call itself or others in its own script (moot with methods).
  • UndertaleModCli hangs unless stdin is /dev/null (the Makefile does this).
  • Menu callbacks and on_back can't be mod methods: the framework runs them with script_execute, which in this runtime takes script indices only (a method ran as camera_create: Code Error). Route through a game script patched to hand off to the mod (patch.csx 2d: options_menu_callback with user data [100+, event]).
  • Never a built-in global as a struct key: { room: ... } assigns the built-in room, i.e. room_goto; ctl state sent the game to ROOM_initvars on every call, which re-ran O_initvars and stacked menu music players (music heard several times). Instance built-ins (x, y, visible) are fine.
  • Runtime: instance ids are ref values — compare/sort with real(id); ds_map_keys_to_array returns undefined for an empty map (map_keys()); errors inside array_sort callbacks escape try/catch.

Game facts worth knowing

  • Widget arrays: [0] type, [1] internal name, [11] parent, [12] id, [22..] per type (table in lib/menus.gml). Lists are [count, items]. visible = on screen.

  • Holding station status: 5 prep open, 1 cooking, 10 ready, 11 expired, 12 ruined (made with mistakes: only a black flash, an icon and a sound); freshness 6..0 every LV_HSfreshalarms frames, then a doubled final warning (O_holdingstationbar_Alarm_3).

  • Board: O_recipepane.LVCSD3_HSsprites[1..] = [sprite, food, ?, name, servings per batch, ?, batches needed]; "Needed" = batches × servings per batch.

  • Medals (O3_newsplashstartend_Other_11): bronze <15 bad; silver <8 bad+average+sick; gold none.

  • Order LV_status: 0 waiting, 5 pane open, 1 cooking, 2 cooked, 3 burnt (already lost: the ticket slides off and O_foodbar_Other_14 counts it bad and destroys it), 6 leaving, 100 served. While cooked, LV_cooktimeangle < -270 is the red flashing timer (~5 s before it burns). Lost orders (burnt, walk-outs) are destroyed without ever showing 100: the mod notices them vanish; result from LRCHK_errors (0 perfect, 1–2 average, 3+ bad). Multi-stage special orders (ice cream: Cone Prep → waffle iron → scoops) go 5→1→2→5→… Cooked (2) means "serve now" only when LV_stagephase + 1 == LV_endingstagephase (O_foodbar_Other_17); otherwise the key opens the next pane (Puff Puff special, ending 8: a topping step). The mod says "cooked, needs prep" for those (serve_next in orders.gml).

  • Truck global.CSD3FTstatus: Begin → SetupTravel → Travel → Arriving → Stop → (queue empty) Wrapup/SetupTravel. An order finished while not stopped is boxed (LV_takeout, status back to 0) and served with its key at the stop.

  • Ingredient buttons: one O_NEWingredientbuttonshelper pair per key; LVT_lockdown 0 usable, 2 greyed (step recipes). Pages: other pages' buttons are moved ~2000+ px off screen; the page-turn helper is LV_controlkeycode 2002 (LVT_pagecurrent), Space turns.

  • An opened order can't be cancelled: Enter (button 2001) stays locked until LVNEW_ingredientcount >= LV_ingredientminimum (Alarm_5), and station keys are ignored. The mod voices a locked Enter with the usable keys ("Enter locked: place an ingredient first. Now S Butter").

  • Opening an order while it cooks (status 1) pulls it out raw: LRCHK_errors += LV_rawfoodtilt (O_foodbar_Other_10) → the mod says "pulled early". Wait for "Order N ready".

  • Recipe box rows: ingredient columns (each an RCP_ar alias, "(2)" = twice) or an instruction ("After placing the ingredients, Dunk & Cook.") drawn in fragments; the instruction's action is a step button (label contained in the line). The pane also draws LS_foodname (the food) small on the title row, 11 px below the title, which may equal an ingredient ("Crab Legs"). It is not always LS_abbrev ("Fried Fish Basket" / "Fish Basket"): skip both.

  • Instruction lines can merely mention a button the order doesn't need ("Milk ingredients are always a custom order", "No ketchup or mustard"): when RCP_ar[slot][2] lists the needs, only needed buttons get keys in a line.

  • Pane titles are the recipe variant ("German Pretzels") or a step ("Standard Prep", "Cone Prep"); LS_foodname is the food.

  • Ingredient names: each has a button label LS_ingredient ("Onion Sht.") and an alias LS_ingredalias (the recipe line; sometimes spelled out, "Onion Shoot") in SCRIPT_foodpopulate_*. LV_ingredientkeycode is only the icon subimage: there is no master ingredient table, so ~330 abbreviations ("P.Sugar") are expanded from abbrev/hand.tsv.

  • Recipe lines draw O_recipepane.RCP_ar[slot][1] aliases; each button helper's LV_attachedto is its slot, so each line maps exactly to a key; slots 1–8 page 1, 9–16 page 2, 17+ page 3.

  • Placeholder buttons: a stage's LVT_<A|B|C><n>button > -1 creates a button even for a slot with no SCRIPT_foodpopulate branch (Roulade, Oysters, Haemul Pajeon, Htapothi, Tamales). It has no LS_ingredient and keeps the helper's default key "\", and O_recipepane_Alarm_0 fills RCP_ar[1..24][1] by calling the populate script per slot, so slots without a branch repeat the previous alias ("Pistachios" on 7, then 8–24). Name → slot lookups must skip buttons without LS_ingredient (cook_real_button) and prefer slots the recipe needs.

  • F9 (Today's Menu) opens a prep pane with LV_whichprepst 109: the board only, no keys.

  • Quitting a day from the pause menu destroys the day's objects while deactivated: per-day state keyed to the O_foodbrain instance resets when a new one appears.

  • Boxed orders (LV_takeout) switch to status 6 the moment the truck stops and walk out ~9 s later: serve them first at each stop.

  • Order status 6 (leaving) can still be served (O_foodbar_Other_17 takes 0 or 6) and goes back to 0 when its food turns up (Other_23). Holding station key on a cooking/ready station: LVT_trashnow 1 = "trash?" (sound only, 120 frames), second press = 2, thrown out.

  • Stop end: O3_stopwordsflash.LVW_texttype "PerfectStop" when a stop had no average/bad. Day totals = global.O3perfecttmp/averagetmp/badtmp (finished stops) + Operfect/Oaverage/Obad (current stop, reset per stop); Osick, Odelicious are per day.

  • HUD sign: O_foodbrain.LVTT_LCDnumb + LVTT_LCD ("12 MI. Away", "5 Remaining"); stop counter LVCSD_stopdisplay of CSD3routes[rest][(stage-1)*20+10].

  • O_autoservebutton (drawn "CTRL") exists while Whisk and Cleaver can auto-serve: silent, reported by Shift+O.

  • LS_foodname is copy-pasted wrong for some foods in SCRIPT_foodpopulate_*: Satay (282), Food Truck Beans (292) and Food Truck Fries (291) are "Fries"; Chicken Tikka, Soto Ayam and Seafood Boil are "Brownies"; Egg Rolls is "Pretzel". Orders are named from global.FOODGLOSS[LV_foodcode][0] (the name the menus show) for codes below 400 (order_food). Station codes are food + 1000.

  • Saves: O_savinggame (MainMenu on global.do_save, e.g. backing out of route select; the loading screen after a day; quitting). Quitting a day from the pause menu does not save.

  • Orders: LV_isthisanHSfood 0 = special order (SO badge); "either" foods come both ways, so announcements add "special". An HS order whose station is empty opens a prep pane.

  • End card LV_tempvarsplash: [0] delicious [1] perfect [2] average [3] bad [4..9] orders by menu points [10] money [11] bonus mult [12] CC [13] levels gained [14] YUM level.

  • Prompts built as menu_create(..., "<owner> <name>", obj) (clear all, buy, unlock, add to menu, not enough CC) draw their own text: listed in menu_text_objects (menus.gml). The buy price is a sprite (food_item[14] CC); balance is global.CURRENCY_foodtokens.

  • Test-rig only: an injected F4 quits the game (keyboard_key_press(vk_f4), or an osascript key code 118), modded or not: clean game_end, no ScreenKey flag, Alt not held. The user's real F4 works fine. Test with CSD3A_STATIONS=1,2,3 tools/play.py.

  • Rival trucks (O3_foodtruckrival, spawned by O_foodbrain_Alarm_8 on set days/stops; the attack runs in its Alarm_0/Alarm_4). DisableHS/Disable2HS set global.HSdamaged[n] = 1 for the rest of the day (reset only in O_foodbrain Create) and send the food away (O_holdingstationbar_Other_15: LVT_trashnow 3); the key then does nothing (O_holdingstationreserve_Other_10). RamHoldingStations destroys every station's food. An emptied station (Alarm_8: ready with no servings) and a cancelled prep (LV_cancelHS) leave through the same Other_15, so trash 3 alone is not a ram. The banner (O_foodbrain.LVTT_FTAtext) is drawn by the silenced O_foodbrain. The armour upgrade (FTupgrademods[9]) blocks them (not on route 12). The other attacks (hacks, BlockStop, HSHeating, UpgradeShot) are voiced from the banner (rival.gml); a hacked Today's Menu (global.HACK_TypeA) hides the board, so Shift+B and the Menu Board buffer say "Menu board hidden, hacked".

  • Silent failures the mod must voice: station keys ignored while a pane is open; orders unservable from empty/expired stations; opening an expired station throws it out.

  • Upgrade Truck (UpgradeMenu, the "upgrade menu"; YUM level ≥ 5, else FoodtruckLockedAlert): a Menu whose focus_widget is a tree node (like routeday/territory). 18 nodes built in upgrade_menu_create; data global.upgrades[key]: [2] name, [3] description, [4] cost (number or per-level array), [5] level (negative = toggled off), [6] level cap, [7] kind (1 prep station, 2 holding station), [8] FTupgrademods row. Node [6]: 0 locked, 1 available, 2 bought. Arrows move along the node's grid row/column, wrapping (__upgrade_scroll_x/y). Buy = hold Enter/Space (held_duration; rising padc_01 loop, then _15_truck_upgraded; too poor: error sound + token highlight only). S toggles a bought station on/off (upgrade_menu_toggle_upgrade). Currency global.CURRENCY_upgradetokens (truck parts). The panel draws name/description with draw_text* (captured). Not voiced by the mod yet.

  • Customize Truck = trinkets (TrinketsMenu; YUM level ≥ 3, else TrinketsLockedAlert; entered via O_loadingscreen, deactivates Menu). Cosmetic only. trinket_state: 0 mode list (Up/Down over mode_labels: Show All / For Sale / Inventory / Edit-Decorate / Screenshot Mode), 1 trinket grid (6 columns, trinket_selected; filter row above the top row), 2 edit/place, 3 screenshot. global.TRINKET_catalog[i] (99): [0] status (0 locked, 1/3 owned, −N = N CC, −(1000+N) = N parts), [3] name, [4] unlock condition text, [5] "Poster"/"Statue". Buying: BuyTrinketAlertMenu (a Menu). Placement is grid-based: trinket_areas Top (32×5; statues only on its bottom row), Left/Right (posters only), Bottom (statues only); each area's [11] ds_grid holds FTD_objectplace indices. Edit keys: arrows step to the next empty cell (wrap), Shift next area, Space pick up (edit select), Enter/Escape confirm, Backspace delete, Ctrl grid, F1 clear all. New trinkets auto-place at a random free spot. Not voiced yet; plan: modes, grid (name, status, cost/unlock), buy, then spoken positions ("Left side, row 4 of 17, column 2") while placing.

  • Key rebinding (keybinds.gml has the details): many Options widgets have no internal name ([1] undefined: every key button, Import/Export), so menus.gml tracks focus by widget id [12]. A key button's modal takes any next key, Escape included. Export/Import use get_save_filename/get_open_filename in the game folder, but GameMaker's file sandbox writes the file by name into the save folder (%LOCALAPPDATA%\CSD3_Foodtruck, game_save_id). Ingredient buttons are pairs (LV_controlkeycode 1 draws the button, 1001 takes the key and draws its label); a Practice rebind changes only the 1001 half. LS_keyboardinput is a letter, chr(code) (custom binds: arrows are "%&'(", keyboard types set LV_bypassdispkey and LS_keyboardinputdisp), or a key code (Universal).

Next

  • End card results list (splash_items: YUM, % to next level, perfect day, money/CC after an Enter skip, computed as Other_10 does): not yet heard live (replay a day, once skipping the tally).
  • Auto-serve (Ctrl) feedback. Standard mode: an HS order whose station is empty leaves almost at once; each walk-out is said twice ("leaving!", then "walked out"): maybe drop one.
  • Abbreviations: built, not yet heard live. 40 guesses in abbrev/hand.tsv are marked ? for the user to check. Only ingredient/food names are expanded (keys, recipe lines, placed, Now:, orders, holding stations); recipe instruction lines and board names are not.
  • An HS order whose station ran out opens as a make-to-order pane: maybe say so up front.
  • Speaker names for dialogue; save-slot wording ("00, date (time)").
  • Customer walk-outs; chores (later days).
  • Today's Menu random foods (R, YUM ≥ 20): tested on save 3 with YUM set in memory; a random-only day (filter -1005) not yet seen. Tres Leches' "Choc. Milk" match untested (random variant).
  • Test rig: an injected Space (ctl key space) opens Wine's window menu (Restore/Move/Close) and freezes the game until it's closed (focus.sh, then an Escape via osascript). The user heard system beeps with the window focused during the same runs. Not investigated yet.
  • Today's Menu: upgrade panel text, unlock prompt (no locked food found yet), actually buying (only opened and cancelled), not-enough-CC alert untested. Day 3 needs a 3-point menu.
  • Upgrade Truck and Customize Truck: done and tested on save 3 (3S); not yet heard by the user. Buy prompt price reads "Buy X for 5 CC, you have 86".