- Game Maker Language 57.7%
- Rust 12.9%
- Python 9.9%
- C 8.6%
- C++ 5.5%
- Other 5.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| abbrev | ||
| ext | ||
| gml | ||
| installer | ||
| patch | ||
| patreon | ||
| recipes | ||
| sounds/Voices | ||
| tools | ||
| vendor | ||
| .gitignore | ||
| 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. 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) + status |
| 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 (order_detail per active order) |
| 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, help |
| ctl.gml | development control commands, ctl state (JSON for tools/drive.py) |
| access_menu.gml | Options > Gameplay > Accessibility: speech rate, subtitles, station keys, board sort, order number voice and volume (patch.csx 2c/2d) |
| sounds.gml | the mod's own sounds (sounds/ → <game>/accessdelicious/sounds/, streamed, played one at a time): order number when an order is ready, in a voice 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, 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) |
| 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 - 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+ (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 wants (recipe lines; twice within 250 ms: only the lines still to do,
from the pane's placed slots
stackCKarray) · Alt+K keys spoken when a pane opens on/off (inispeech/keys) · Shift+M medals (today's count, then targets; also in the pause menu; territory map: totals) · Shift+H keys for this screen - During a day, buffers: Left/Right switch (Menu Board, Orders: one item per Shift+O group) 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; Up/Down walk the results list, most important first (the game only uses Left/Right there). Enter skips the tally, then continues.
- 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 the Menu Board sort: holding
station order, or greatest need, which also orders Shift+B; Order Number Voice, Off or a voice,
default Samantha; and Order Number Volume, 0–100%, default 50%. Changing either previews the
game's ready sound with a random number, cutting off the last preview). Saved in
accessdelicious.ini. - 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). 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. - 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). 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>,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>. 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, 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.- 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_abbrev(the food) small on the title row, which may equal an ingredient ("Crab Legs"). -
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. -
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. -
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.
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: upgrade/random-day 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".