- Game Maker Language 43.8%
- C 34.3%
- Python 8.1%
- Rust 7.5%
- C++ 3.1%
- Other 3.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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> |
||
| abbrev | ||
| ext | ||
| gml | ||
| installer | ||
| patch | ||
| patreon | ||
| recipes | ||
| sounds/Voices | ||
| tools | ||
| vendor | ||
| .gitignore | ||
| changes.md | ||
| game.mk | ||
| Makefile | ||
| README.md | ||
| run.sh | ||
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 fromO_initvarsCreate, 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_clearbefore the game's Step events see them), key-injection queue. - Step_2 (End Step): the announcers, then
text_end_frame().
- Create: binds the DLL (
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:- imports the mod code (lib concatenated into Create);
- appends the controller's creation to
O_initvarsCreate; 2b. prependswith (obj_csd3a) hotkeys_once();toO_foodbrainBegin 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; - before/after every
instance_deactivate_all(...):obj_csd3a.before_pause();andinstance_activate_object(obj_csd3a);(tutorial popups deactivate everything; gameplay objects can't be read while deactivated, so state is snapshotted just before); - rewrites every
draw_text*call (exact variants only; notdraw_texture_*) toobj_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, inispeech/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: …" fromstackCKarray[i][0]instead of their instruction text) · buttons the recipe needs (RCP_ar[slot][2]times, whatO_NEWingredientpane_Other_12checks) 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, inispeech/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 (inispeech/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 insplash_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 inaccessdelicious.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 theirgenerate.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 have8.ogg(a missing file is logged and skipped). Voices are the folders with a1.ogg, found at runtime (sounds_voices): a new folder needs no rebuild. Gain = volume^0.67 × the game's UI sound volume, so 50% ≈ plainsayand 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_soundplays it, cached per file and pan. The buffer must bebuffer_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 todist\data.win, a releaseaccessdelicious.dllbuilt with MSVC, PRISM, the voices). The original data.win comes fromCSD3A_ORIG, default the Steam install'sdata.win.orig(elsedata.win).--cli, or--install/--uninstall [--game DIR]for scripts. It refuses to run while CSD3.exe is open.cargo test --release -- --ignoredwithCSD3A_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 fromvendor/xdeltacompiled in, GPLv2),accessdelicious.xdelta(ORIG → patched data.win, ~450 KB), a releaseaccessdelicious.dll(-DCSD3A_RELEASE: control server only ifaccessdelicious.devexists in the game folder) and the PRISM files. The installer checks both SHA-256s (build/release/hashes.h), keepsdata.win.orig, and offers update/remove when run again. UI is message boxes, the folder picker and a wait window;--yesskips them and prints to stdout (tests under wine). Under Wine it hard-linksprism-crossover-ttsd(CopyFile drops the executable bit). The xdelta encode (-9, 1 GB window) takes over 10 minutes. IfPATCHED_SHA256inhashes.his unchanged, the oldbuild/release/accessdelicious.xdeltastill fits: check it decodes to that hash,touchit, rerun. ORIG lives in the SteamCMD depot folder; if it's gone, re-rundownload_depot 1000030 1000031(the cached login expires: the user logs in withsteamcmd.sh +login guilevi +quitfirst) and check its SHA-256 againstORIG_SHA256. A game update changes ORIG.make check-gamecompares Steam's public build withgame.mk;make update(tools/update_game.sh) downloads the new depot with SteamCMD (cached loginSTEAMUSER, default guilevi), keeps the old data.win inbackup/, syncs the CrossOver install, re-decompiles intore/, regenerates abbrev/recipes, writesgame.mk, then deploys and builds the release. Test before shipping; add the game's version number toGAMEVERingame.mkby 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, userguilevicached) plus a hand-written appmanifest andsteam_appid.txt. - The bottle's graphics backend must be DXMT (D3DMetal fails GameMaker's
CheckMultisampleQualityLevelswith E_INVALIDARG dialogs). CrossOver'swineCLI needsCX_BOTTLE_PATH=/Volumes/T7/Bottles; the bottle config overridesCX_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:portoverrides) —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: whileplay.pyruns it holds the connection andctl.pytimes out (the game isn't hung — check the log is still growing). tools/drive.py start|play|day: the test driver. Readsctl 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 NAMEmakes 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|ordersstill drives the tutorial;start_day.shandplay.pyare the old text-scraping drivers.tools/scan.py <decompiled-gml-dir>: static scan of theSCRIPT_foodpopulate_*recipe scripts for phantom buttons, instructions naming buttons a variant doesn't use, andLS_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
getstill work). If keys do nothing, runtools/focus.shfirst. The user typing in Terminal takes focus away. - Screenshots:
tools/winshot.sh [out.png](CGWindowList, ownerCSD3.exe, thenscreencapture -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>.namenot 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. modis 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_backcan't be mod methods: the framework runs them withscript_execute, which in this runtime takes script indices only (a method ran ascamera_create: Code Error). Route through a game script patched to hand off to the mod (patch.csx 2d:options_menu_callbackwith user data [100+, event]). - Never a built-in global as a struct key:
{ room: ... }assigns the built-inroom, i.e.room_goto;ctl statesent 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
refvalues — compare/sort withreal(id);ds_map_keys_to_arrayreturns undefined for an empty map (map_keys()); errors insidearray_sortcallbacks escape try/catch.
Game facts worth knowing
-
Widget arrays:
[0]type,[1]internal name,[11]parent,[12]id,[22..]per type (table inlib/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_HSfreshalarmsframes, 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 andO_foodbar_Other_14counts 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 fromLRCHK_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 whenLV_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_nextin 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_NEWingredientbuttonshelperpair per key;LVT_lockdown0 usable, 2 greyed (step recipes). Pages: other pages' buttons are moved ~2000+ px off screen; the page-turn helper isLV_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_aralias, "(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 drawsLS_foodname(the food) small on the title row, 11 px below the title, which may equal an ingredient ("Crab Legs"). It is not alwaysLS_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_foodnameis the food. -
Ingredient names: each has a button label
LS_ingredient("Onion Sht.") and an aliasLS_ingredalias(the recipe line; sometimes spelled out, "Onion Shoot") inSCRIPT_foodpopulate_*.LV_ingredientkeycodeis only the icon subimage: there is no master ingredient table, so ~330 abbreviations ("P.Sugar") are expanded fromabbrev/hand.tsv. -
Recipe lines draw
O_recipepane.RCP_ar[slot][1]aliases; each button helper'sLV_attachedtois 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 > -1creates a button even for a slot with noSCRIPT_foodpopulatebranch (Roulade, Oysters, Haemul Pajeon, Htapothi, Tamales). It has noLS_ingredientand keeps the helper's default key"\", andO_recipepane_Alarm_0fillsRCP_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 withoutLS_ingredient(cook_real_button) and prefer slots the recipe needs. -
F9 (Today's Menu) opens a prep pane with
LV_whichprepst109: 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_foodbraininstance 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_17takes 0 or 6) and goes back to 0 when its food turns up (Other_23). Holding station key on a cooking/ready station:LVT_trashnow1 = "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,Odeliciousare per day. -
HUD sign:
O_foodbrain.LVTT_LCDnumb + LVTT_LCD("12 MI. Away", "5 Remaining"); stop counterLVCSD_stopdisplayofCSD3routes[rest][(stage-1)*20+10]. -
O_autoservebutton(drawn "CTRL") exists while Whisk and Cleaver can auto-serve: silent, reported by Shift+O. -
LS_foodnameis copy-pasted wrong for some foods inSCRIPT_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 fromglobal.FOODGLOSS[LV_foodcode][0](the name the menus show) for codes below 400 (order_food). Station codes are food + 1000. -
Saves:
O_savinggame(MainMenu onglobal.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_isthisanHSfood0 = 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 inmenu_text_objects(menus.gml). The buy price is a sprite (food_item[14]CC); balance isglobal.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: cleangame_end, no ScreenKey flag, Alt not held. The user's real F4 works fine. Test withCSD3A_STATIONS=1,2,3 tools/play.py. -
Rival trucks (
O3_foodtruckrival, spawned byO_foodbrain_Alarm_8on set days/stops; the attack runs in its Alarm_0/Alarm_4). DisableHS/Disable2HS setglobal.HSdamaged[n] = 1for the rest of the day (reset only inO_foodbrainCreate) and send the food away (O_holdingstationbar_Other_15:LVT_trashnow3); 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 sameOther_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, elseFoodtruckLockedAlert): a Menu whosefocus_widgetis a tree node (like routeday/territory). 18 nodes built inupgrade_menu_create; dataglobal.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]FTupgrademodsrow. 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; risingpadc_01loop, then_15_truck_upgraded; too poor: error sound + token highlight only). S toggles a bought station on/off (upgrade_menu_toggle_upgrade). Currencyglobal.CURRENCY_upgradetokens(truck parts). The panel draws name/description withdraw_text*(captured). Not voiced by the mod yet. -
Customize Truck = trinkets (
TrinketsMenu; YUM level ≥ 3, elseTrinketsLockedAlert; entered viaO_loadingscreen, deactivates Menu). Cosmetic only.trinket_state: 0 mode list (Up/Down overmode_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_areasTop (32×5; statues only on its bottom row), Left/Right (posters only), Bottom (statues only); each area's[11]ds_grid holdsFTD_objectplaceindices. 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 useget_save_filename/get_open_filenamein 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_controlkeycode1 draws the button, 1001 takes the key and draws its label); a Practice rebind changes only the 1001 half.LS_keyboardinputis a letter,chr(code)(custom binds: arrows are "%&'(", keyboard types setLV_bypassdispkeyandLS_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.tsvare 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".