diff --git a/.github/workflows/ppuc.yml b/.github/workflows/ppuc.yml index 03ce1ea..d348647 100644 --- a/.github/workflows/ppuc.yml +++ b/.github/workflows/ppuc.yml @@ -1,6 +1,6 @@ name: ppuc on: - workflow_dispatch: + pull_request: defaults: run: diff --git a/.gitignore b/.gitignore index 2ce5455..46cec1e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ third-party/runtime-libs/ third-party/build-libs/ third-party/include/ third-party/pinmame-nvram-maps +third-party/lua-src diff --git a/CMakeLists.txt b/CMakeLists.txt index 576ecd2..de3d0ae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -92,6 +92,85 @@ if(EXISTS "${PPUC_STAGED_INCLUDE_DIR}") list(APPEND PPUC_INCLUDE_DIRS "${PPUC_STAGED_INCLUDE_DIR}") endif() +set(PPUC_LUA_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/third-party/lua-src") +set(PPUC_LUA_INCLUDE_DIR "") +set(PPUC_LUA_LIBRARY "") +set(PPUC_LUA_SOURCES "") +if(EXISTS "${PPUC_LUA_SOURCE_DIR}/lua.h") + set(PPUC_LUA_INCLUDE_DIR "${PPUC_LUA_SOURCE_DIR}") + set(PPUC_LUA_SOURCES + "${PPUC_LUA_SOURCE_DIR}/lapi.c" + "${PPUC_LUA_SOURCE_DIR}/lauxlib.c" + "${PPUC_LUA_SOURCE_DIR}/lbaselib.c" + "${PPUC_LUA_SOURCE_DIR}/lcode.c" + "${PPUC_LUA_SOURCE_DIR}/lcorolib.c" + "${PPUC_LUA_SOURCE_DIR}/lctype.c" + "${PPUC_LUA_SOURCE_DIR}/ldblib.c" + "${PPUC_LUA_SOURCE_DIR}/ldebug.c" + "${PPUC_LUA_SOURCE_DIR}/ldo.c" + "${PPUC_LUA_SOURCE_DIR}/ldump.c" + "${PPUC_LUA_SOURCE_DIR}/lfunc.c" + "${PPUC_LUA_SOURCE_DIR}/lgc.c" + "${PPUC_LUA_SOURCE_DIR}/linit.c" + "${PPUC_LUA_SOURCE_DIR}/liolib.c" + "${PPUC_LUA_SOURCE_DIR}/llex.c" + "${PPUC_LUA_SOURCE_DIR}/lmathlib.c" + "${PPUC_LUA_SOURCE_DIR}/lmem.c" + "${PPUC_LUA_SOURCE_DIR}/loadlib.c" + "${PPUC_LUA_SOURCE_DIR}/lobject.c" + "${PPUC_LUA_SOURCE_DIR}/lopcodes.c" + "${PPUC_LUA_SOURCE_DIR}/loslib.c" + "${PPUC_LUA_SOURCE_DIR}/lparser.c" + "${PPUC_LUA_SOURCE_DIR}/lstate.c" + "${PPUC_LUA_SOURCE_DIR}/lstring.c" + "${PPUC_LUA_SOURCE_DIR}/lstrlib.c" + "${PPUC_LUA_SOURCE_DIR}/ltable.c" + "${PPUC_LUA_SOURCE_DIR}/ltablib.c" + "${PPUC_LUA_SOURCE_DIR}/ltm.c" + "${PPUC_LUA_SOURCE_DIR}/lundump.c" + "${PPUC_LUA_SOURCE_DIR}/lutf8lib.c" + "${PPUC_LUA_SOURCE_DIR}/lvm.c" + "${PPUC_LUA_SOURCE_DIR}/lzio.c" + ) + message(STATUS "Lua source dir: ${PPUC_LUA_SOURCE_DIR}") +else() + find_path(PPUC_LUA_INCLUDE_DIR + NAMES lua.h + PATHS + "${PPUC_STAGED_INCLUDE_DIR}" + "${PPUC_STAGED_INCLUDE_DIR}/lua" + "${PPUC_STAGED_INCLUDE_DIR}/lua5.4" + "${PPUC_STAGED_INCLUDE_DIR}/lua54" + ) + if(NOT PPUC_LUA_INCLUDE_DIR) + find_path(PPUC_LUA_INCLUDE_DIR + NAMES lua.h + PATH_SUFFIXES lua5.4 lua54 lua + ) + endif() + + find_library(PPUC_LUA_LIBRARY + NAMES lua54 lua5.4 lua + PATHS + "${PPUC_STAGED_BUILD_LIB_DIR}" + "${PPUC_STAGED_RUNTIME_LIB_DIR}" + ) + if(NOT PPUC_LUA_LIBRARY) + find_library(PPUC_LUA_LIBRARY + NAMES lua54 lua5.4 lua + ) + endif() + + if(NOT PPUC_LUA_INCLUDE_DIR OR NOT PPUC_LUA_LIBRARY) + message(FATAL_ERROR "Could not find Lua source, headers, or library. Run the platform external.sh script first.") + endif() + + message(STATUS "Lua include dir: ${PPUC_LUA_INCLUDE_DIR}") + message(STATUS "Lua library: ${PPUC_LUA_LIBRARY}") +endif() + +list(APPEND PPUC_INCLUDE_DIRS "${PPUC_LUA_INCLUDE_DIR}") + if(CMAKE_BUILD_TYPE STREQUAL "Debug") if(ENABLE_SANITIZERS AND (PLATFORM STREQUAL "macos" OR PLATFORM STREQUAL "linux")) set(SANITIZER_FLAGS -fsanitize=address,undefined) @@ -123,9 +202,9 @@ endif() add_executable(ppuc-pinmame src/AudioOutput.cpp - src/PUPTriggerEngine.cpp + src/LuaRulesEngine.cpp + ${PPUC_LUA_SOURCES} src/SpeechCliSupport.cpp - src/SpeechTriggerMap.cpp src/SpeechService.cpp src/ppuc.cpp ) @@ -280,6 +359,7 @@ if(PLATFORM STREQUAL "win" OR PLATFORM STREQUAL "win-mingw") SDL364 SDL3_image64 yaml-cpp + ${PPUC_LUA_LIBRARY} ws2_32 ) else() @@ -292,6 +372,7 @@ if(PLATFORM STREQUAL "win" OR PLATFORM STREQUAL "win-mingw") SDL3 SDL3_image yaml-cpp + ${PPUC_LUA_LIBRARY} ws2_32 ) endif() @@ -322,6 +403,7 @@ else() SDL3 SDL3_image ${PPUC_YAML_CPP_LINK_LIBRARY} + ${PPUC_LUA_LIBRARY} ) else() target_link_libraries(ppuc-pinmame LINK_PUBLIC @@ -333,6 +415,7 @@ else() SDL3 SDL3_image ${PPUC_YAML_CPP_LINK_LIBRARY} + ${PPUC_LUA_LIBRARY} ) endif() diff --git a/INTERCEPTOR.md b/INTERCEPTOR.md new file mode 100644 index 0000000..42ab493 --- /dev/null +++ b/INTERCEPTOR.md @@ -0,0 +1,137 @@ +# Interceptor Lua Rules + +The interceptor feature lets `ppuc-pinmame` react to physical machine events +before they are forwarded to PinMAME. Interceptor logic lives in the Lua rules +file loaded with: + +```text +--rules +``` + +The path can be one Lua file or a directory of Lua files. The same Lua rules can +emit DMD/PUP triggers, speech callouts, board effect triggers, and host-side +interceptor actions. + +## Runtime API + +Interceptor rules use the `ppuc` Lua namespace: + +```lua +function ppuc.onSwitchChanged(number, state) + if ppuc.stateActive("ballSave") and number == 9 and state == 1 then + ppuc.suppressSwitch(9) + ppuc.pulseCoil(7, 120) + end +end +``` + +Useful functions: + +- `ppuc.switchState(number)` +- `ppuc.switchGroupState(name)` +- `ppuc.switchGroupClosing(name)` +- `ppuc.switchGroupOpening(name)` +- `number` and `state` inside `ppuc.onSwitchChanged(number, state)` +- `ppuc.setState(name)` and `ppuc.setState(name, durationMs)` +- `ppuc.clearState(name)` +- `ppuc.stateActive(name)` +- `ppuc.after(delayMs, function() ... end)` +- `ppuc.suppressSwitch(number)` +- `ppuc.sendSwitchToCpu(number, state)` +- `ppuc.pulseCoil(number, durationMs)` +- `ppuc.blinkLamp(number, onMs, offMs)` +- `ppuc.stopBlinkLamp(number)` + +## Switch Groups + +Switch groups are declared in the game YAML and loaded by `libppuc`: + +```yaml +switchGroups: + playfield: + switches: [10, 11, 12, 13] +``` + +The group name `buttons` is reserved. It is built automatically from switches +marked with `button: true` in `switches` or `switchMatrix.switches`. + +## Runtime Output Overrides + +PinMAME remains the normal owner of lamps and coils. Interceptor output actions +temporarily override a single output number: + +- `ppuc.pulseCoil(...)` forces the coil on until the pulse timer expires, then + restores the latest PinMAME coil state for that coil. +- `ppuc.blinkLamp(...)` forces a lamp blink until `ppuc.stopBlinkLamp(...)`, + then restores the latest PinMAME lamp state for that lamp. + +If PinMAME changes the same output while the override is active, the new PinMAME +state is remembered and restored when the override ends. + +## Non-Blocking Delays + +Use `ppuc.after(delayMs, function() ... end)` when a rule needs delayed work. +This schedules the function on the Lua rules update tick; it does not sleep and +does not block `ppuc-pinmame`. + +```lua +function ppuc.onSwitchChanged(number, state) + if number == 16 and state == 1 then + ppuc.after(500, function() + ppuc.speech("Test") + end) + end +end +``` + +Suppressed switches can be sent to PinMAME later by scheduling explicit CPU +switch states: + +```lua +function ppuc.onSwitchChanged(number, state) + if number == 16 and state == 1 then + ppuc.suppressSwitch(16) + ppuc.after(500, function() + ppuc.sendSwitchToCpu(16, 1) + end) + ppuc.after(650, function() + ppuc.sendSwitchToCpu(16, 0) + end) + end +end +``` + +## Ball Save Example + +This example arms ball save on switch 15, starts a 5 second save window when any +playfield switch closes, blinks lamp 8 while ball save is ready or active, and +suppresses the outhole switch while pulsing coil 7. + +```lua +function ppuc.onSwitchChanged(number, state) + if number == 15 and state == 1 then + ppuc.setState("ballSaveReady") + end + + if ppuc.stateActive("ballSaveReady") and ppuc.switchGroupClosing("playfield") then + ppuc.clearState("ballSaveReady") + ppuc.setState("ballSave", 5000) + end + + if ppuc.stateActive("ballSave") and number == 9 and state == 1 then + ppuc.suppressSwitch(9) + ppuc.pulseCoil(7, 120) + end +end + +function ppuc.onRulesUpdate() + if ppuc.stateActive("ballSaveReady") or ppuc.stateActive("ballSave") then + ppuc.blinkLamp(8, 250, 250) + else + ppuc.stopBlinkLamp(8) + end +end +``` + +The feature is intentionally host-side. It does not change the RS485 wire +protocol or move rules into the IO-board firmware. diff --git a/README.md b/README.md index 3d9da2a..6d5a1b7 100644 --- a/README.md +++ b/README.md @@ -61,11 +61,8 @@ These components are still in an early development stage and the documentation w * -p VALUE * Serum ignore number of unknown frames * optional -* --pup-triggers path - * path to a lightweight PUP trigger rules file - * optional -* --speech-file path - * path to a speech trigger text file used with speech trigger rules +* --rules path + * path to one Lua rules file or a directory containing Lua rule files * optional * --music-files csv * comma-separated MP3 playlist for gameplay background music @@ -161,82 +158,109 @@ pwmOutput: ballSearch: true ``` -### PUP Trigger Rules - -Use `--pup-triggers ` to map switch/lamp/coil conditions to calls of `SetPUPTrigger(source, id, value)`. -This trigger feature is independent from `--pup`, and can also drive speech callouts and board-local PPUC effects. +### Lua Rules + +Use `--rules ` to run Lua rules. The path can point to one `.lua` file or +to a directory. Directory loading is non-recursive, loads top-level `*.lua` +files in filename order, and fails on the first load or runtime error. Rules +are independent from `--pup`, and can also drive speech callouts, board-local +PPUC effects, and host-side interceptor behavior. + +Rules define handlers on the `ppuc` namespace: + +```lua +function ppuc.onSwitchChanged(number, state) + if number == 13 and state == 1 and ppuc.lampState(42) then + ppuc.pupTrigger("P", 100, 1) + end + + if ppuc.stateActive("ballSave") and number == 9 and state == 1 then + ppuc.suppressSwitch(9) + ppuc.pulseCoil(7, 120) + end +end + +function ppuc.onLampChanged(number, state) + if number == 23 and state == 1 and not ppuc.attractMode() then + ppuc.speech("New highscore!") + end +end +``` -Rule syntax: +When multiple rule files define the same handler, all handlers run in load +order. Rule files share one Lua state, so use `local` helper functions and +variables unless cross-file globals are intentional. + +Supported handlers: +* `ppuc.onSwitchChanged(number, state)` +* `ppuc.onLampChanged(number, state)` +* `ppuc.onCoilChanged(number, state)` +* `ppuc.onBallChanged(ball)` +* `ppuc.onPlayerChanged(player)` +* `ppuc.onRulesUpdate()` + +State helpers and handler values: +* `ppuc.switchState(number)`, `ppuc.lampState(number)`, `ppuc.coilState(number)` +* `ppuc.currentBall()`, `ppuc.currentPlayer()`, `ppuc.attractMode()` +* Changed handlers receive `number` and `state`; use `state == 1` for active/closed/on and `state == 0` for inactive/open/off. + +Named states, history, and switch groups: +* `ppuc.setState(name)` and `ppuc.setState(name, durationMs)` +* `ppuc.clearState(name)` and `ppuc.stateActive(name)` +* `ppuc.triggerHistory(id)` and `ppuc.triggerHistory(id, windowMs)` +* `ppuc.triggerSequence(windowMs, id1, id2, id3)` +* `ppuc.onlyOnceEvery(name, durationMs)` returns true only once per named time window +* `ppuc.switchGroupState(name)`, `ppuc.switchGroupClosing(name)`, `ppuc.switchGroupOpening(name)` + +Switch groups can be declared in the game YAML: -```text - [value] [cooldown=] [delay=] : +```yaml +switchGroups: + playfield: + switches: [10, 11, 12, 13] ``` -Expression functions: -* `switch()` -* `lamp()` -* `coil()` -* `attract` or `attract()` -* `switch_rising()` -* `switch_falling()` -* `lamp_rising()` -* `lamp_falling()` -* `coil_rising()` -* `coil_falling()` - -Operators: -* `!` -* `&&` -* `||` -* parentheses `(...)` - -Rule options: -* `cooldown=` - * suppress retriggering until the cooldown window has elapsed -* `delay=` - * wait before firing after the expression matches - * for state-based expressions, the condition must still be true when the delay expires - * for edge expressions like `switch_rising(...)`, the matching edge arms the delayed trigger once - -Example: - -```text -P 100 1 : switch_rising(13) && lamp(42) -P 101 1 cooldown=500 : switch_rising(13) && attract -P 102 1 delay=750 : switch(13) && attract -O 60010 1 : lamp_rising(23) && !attract -F cabinet-attract 1 : lamp_rising(5) && attract +The group `buttons` is built in from switches marked `button: true` and cannot +be overridden in YAML. + +Outputs and integrations: +* `ppuc.after(delayMs, function() ... end)` schedules non-blocking delayed Lua work +* `ppuc.pupTrigger(source, id, value)` +* `ppuc.speech(text)` +* `ppuc.effectTrigger(id, value)` or `ppuc.effectTrigger(name, value)` +* `ppuc.suppressSwitch(number)` +* `ppuc.sendSwitchToCpu(number, state)` +* `ppuc.pulseCoil(number, durationMs)` +* `ppuc.blinkLamp(number, onMs, offMs)` and `ppuc.stopBlinkLamp(number)` + +`ppuc.after(...)` does not sleep inside the PinMAME loop. It stores the callback +and runs it from the normal rules update tick after the requested delay: + +```lua +function ppuc.onSwitchChanged(number, state) + if number == 16 and state == 1 then + ppuc.after(500, function() + ppuc.speech("Test") + end) + end +end ``` -A ready-to-use sample file is available at `examples/pup-triggers.rules`. +A ready-to-use sample file is available at `examples/rules.lua`. +Interceptor-specific behavior is documented in `INTERCEPTOR.md`. -Speech trigger source: -* `O` - * spoken callout target - * the trigger `id` is looked up in a `--speech-file` +Board effect trigger source: * `F` * board-local effect trigger * forwarded to `libppuc` as a runtime event with source `EVENT_SOURCE_EFFECT` * use matching `trigger.source: F` plus `trigger.name` or `trigger.number` in the game YAML effect block -### Speech Trigger Text Files - -Use `--speech-file ` together with `--speech` and `--pup-triggers` to map speech trigger IDs to spoken text. - -Syntax: +Speech callouts use the configured speech backend directly from Lua: -```text - : +```lua +ppuc.speech("New highscore!") ``` -Example: - -```text -60010: New highscore! -``` - -If a trigger rule emits source `O` with id `60010`, the speech backend will speak that text. - Speech backends: * `auto` * prefer `espeak-ng` when available, otherwise `flite` @@ -252,12 +276,20 @@ Examples: * `--speech-backend flite --speech-voice kal` Ready-to-use samples are available at: -* `examples/flash.rules` -* `examples/flash.speech` +* `examples/rules.lua` ### Compiling +The platform build scripts stage pinned third-party dependencies into +`third-party`. For local development, they automatically prefer sibling +checkouts named `../libppuc` and `../libsdldmd` when those directories exist, +then fall back to the pinned GitHub archives from `platforms/config.sh`. + +Set `PPUC_USE_LOCAL_DEPS=0` to force the pinned archive path, or set +`PPUC_LOCAL_DEPS_ROOT=/path/to/workspace` to look for local dependency +checkouts somewhere other than the parent directory. + #### Windows (x64) ```shell diff --git a/RULES_AND_EFFECTS.md b/RULES_AND_EFFECTS.md index cbb5bf3..54be937 100644 --- a/RULES_AND_EFFECTS.md +++ b/RULES_AND_EFFECTS.md @@ -1,348 +1,205 @@ -# Flash Rules And Effects +# Rules And Effects -This document describes the behavior added by the PPUC layer on top of the original ROM when the game is run through PinMAME. +PPUC rules are Lua scripts loaded by `ppuc-pinmame` with: -The original ROM still owns the game rules, scoring, lamp logic, solenoid timing, switch matrix behavior, ball flow, and attract/game modes. -PPUC adds extra outputs and trigger-driven presentation around that baseline. - -## What PPUC Adds - -PPUC currently adds four kinds of enhancements: - -1. Extra addressable LEDs that do not exist in the original machine. -2. A shaker motor effect layer. -3. Trigger rules that emit DMD or PUP-style events from ROM state changes. -4. Optional speech callouts driven by those trigger rules. - -PPUC does not replace the ROM logic here. -It observes ROM-driven switch, lamp, and coil activity and uses that activity to drive modern extras. - -## Flash Example -### Cabinet LED String - -The Flash game config defines an addressable LED string named `Cabinet` on IO board `1`, port `29`. - -Source: -[Flash_877888bc-06a9-428a-91ab-6d821e104107.yml](../ppuc_games/flash/Flash_877888bc-06a9-428a-91ab-6d821e104107.yml) - -#### Physical Layout - -- LEDs `0-7` are assigned individually. -- LEDs `8-93` are grouped into segment `1`. +```text +--rules +``` -#### LEDs 0-7 +The path can be one `.lua` file or a directory containing `.lua` files. When a +directory is used, `ppuc-pinmame` loads top-level `.lua` files in filename order. -These are configured as GI-linked cabinet/button/status lights: +The old trigger-rule file format and separate speech text file are no longer +supported. Speech callouts are written directly in Lua with `ppuc.speech(...)`. -- LED `0`: left flipper button bottom, `FF00FF` -- LED `1`: left flipper button bottom, `004AFF` -- LED `2`: start button, `FF7F00` -- LED `3`: right flipper button top, `FF00FF` -- LED `4`: right flipper button bottom, `004AFF` -- LED `5`: coin 1, `FFFFFF` -- LED `6`: coin 2, `FFFFFF` -- LED `7`: coin 3, `FFFFFF` +## What Rules Can Do -All of these map to GI string `1`, so they follow ROM-driven GI state rather than acting as independent PinMAME lamps. +Lua rules observe PinMAME and physical-machine state, then trigger extra PPUC +behavior: -#### LEDs 8-93 +- send DMD/PUP triggers with `ppuc.pupTrigger(...)` +- trigger board-local effects with `ppuc.effectTrigger(...)` +- speak text with `ppuc.speech(...)` +- delay work without blocking with `ppuc.after(...)` +- suppress or later send physical switches to PinMAME +- pulse coils and blink lamps as host-side interceptor output overrides -This range is configured as `segment 1`. -The intent of this segment is effect playback, not direct PinMAME lamp ownership. +The original ROM still owns scoring, original lamp logic, solenoid timing, +switch matrix behavior, ball flow, and attract/game mode. PPUC adds extra output +and presentation behavior around that baseline. -Current configured effect: +## Lua API -- Description: `Exprerssion Lights` -- Segment: `1` -- Color: `0000FF` -- WS2812FX effect: `22` -- Duration: `0` (unlimited) -- Speed: `0` -- Priority: `1` -- Repeat: `-1` +Common state helpers: -Under the new model, this segment should be defined in YAML only as an effect target. -The logic deciding when it starts should live only in the rule file. +```lua +ppuc.switchState(number) +ppuc.lampState(number) +ppuc.coilState(number) +ppuc.currentBall() +ppuc.currentPlayer() +ppuc.attractMode() +``` -### Shaker Motor +Named states, timing, and switch groups: + +```lua +ppuc.setState(name) +ppuc.setState(name, durationMs) +ppuc.clearState(name) +ppuc.stateActive(name) +ppuc.onlyOnceEvery(name, durationMs) +ppuc.switchGroupState(name) +ppuc.switchGroupClosing(name) +ppuc.switchGroupOpening(name) +``` -The Flash game config also defines a shaker on IO board `1`, port `19`. +Outputs and integrations: + +```lua +ppuc.after(delayMs, function() ... end) +ppuc.pupTrigger(source, id, value) +ppuc.speech(text) +ppuc.effectTrigger(id, value) +ppuc.effectTrigger(name, value) +ppuc.suppressSwitch(number) +ppuc.sendSwitchToCpu(number, state) +ppuc.pulseCoil(number, durationMs) +ppuc.blinkLamp(number, onMs, offMs) +ppuc.stopBlinkLamp(number) +``` -It is declared as: +Handlers: -- Type: `shaker` -- Number: `100` -- Power limit: `128` +```lua +function ppuc.onSwitchChanged(number, state) +end -This is not ROM-native hardware. -PPUC adds it as a modern feedback device. +function ppuc.onLampChanged(number, state) +end -### Configured Shaker Effects +function ppuc.onCoilChanged(number, state) +end -Two PWM effects should be defined in the Flash YAML. +function ppuc.onBallChanged(ball) +end -#### Jet Bumper Shaker +function ppuc.onPlayerChanged(player) +end -- Description: `Jet Bumper Shaker` -- PWM effect: `3` -- Frequency: `4` -- Max intensity: `128` -- Min intensity: `0` -- Duration: `0` -- Priority: `1` -- Repeat: `0` +function ppuc.onRulesUpdate() +end +``` -Under the new model, this effect remains defined in YAML, but its trigger logic belongs only in the rule file. +## Interceptor Rules -#### Flash Shaker +Interceptor behavior is part of the Lua rules engine. It lets `ppuc-pinmame` +react to physical machine events before they are forwarded to PinMAME. -- Description: `Flash Shaker` -- PWM effect: `1` -- Frequency: `4` -- Max intensity: `192` -- Min intensity: `0` -- Duration: `1000 ms` -- Priority: `2` -- Repeat: `0` +```lua +function ppuc.onSwitchChanged(number, state) + if ppuc.stateActive("ballSave") and number == 9 and state == 1 then + ppuc.suppressSwitch(9) + ppuc.pulseCoil(7, 120) + end +end +``` -This is a second shaker pattern that should be triggered from the rule file instead of from YAML-generated trigger entries. +Suppressed switches can be sent to PinMAME later: + +```lua +function ppuc.onSwitchChanged(number, state) + if number == 16 and state == 1 then + ppuc.suppressSwitch(16) + ppuc.after(500, function() + ppuc.sendSwitchToCpu(16, 1) + end) + ppuc.after(650, function() + ppuc.sendSwitchToCpu(16, 0) + end) + end +end +``` -## Trigger Rules From `flash.rules` +`ppuc.after(...)` schedules work on the rules update tick. It does not sleep and +does not block `ppuc-pinmame`. -Rules file: -[flash.rules](/Volumes/data/workspace/PPUC/ppuc_combined/ppuc/examples/flash.rules) +See `INTERCEPTOR.md` for a focused interceptor reference. -These rules watch ROM-visible switch, lamp, and coil changes and emit additional actions. +## Switch Groups -### Terminology +Switch groups are declared in game YAML and loaded by `libppuc`: -The word `source` comes from the older effect-trigger vocabulary inherited from DOF, the Direct Output Framework. +```yaml +switchGroups: + playfield: + switches: [10, 11, 12, 13] +``` -That old name is still visible in some code and protocol constants, but in the first field of a rule line it is better understood as a target channel, not as the source of the condition. +The `buttons` group name is reserved. It is built automatically from switches +marked with `button: true` in `switches` or `switchMatrix.switches`. -Example: +In the config-tool, switch group names are entered on the game node as one name +per line: ```text -F cabinet-flash-attract 1 : lamp_rising(5) && attract +playfield +standups ``` -Here: - -- `F` is the target channel for the effect event that will be emitted. It means effect. -- `cabinet-flash-attract` is the effect trigger ID or name -- `lamp_rising(5) && attract` is the condition - -There is also a silent channel: - -- `S` means evaluate the rule and apply state/history side effects, but emit no external trigger. - -Inside the rule expression itself we no longer use those old single-character codes. -The conditions are written with readable words: - -- `ball(...)` -- `player(...)` -- `history(...)` -- `sequence(...)` -- `switch(...)` -- `lamp(...)` -- `coil(...)` -- `switch_rising(...)` -- `lamp_rising(...)` -- `coil_rising(...)` -- `attract` - -Current limitation: -`ball(...)` is currently populated from PinMAME CPU RAM only for Williams System 3, System 4, and System 6 style games. -`player(...)` can now be supplied either by runtime state updates or by rules that use `set_player=`. -Trigger history is retained per player for a rolling time window and is cleared when the table returns to attract mode. -Rules can also use `clear_player_history=` to drop stored history for a specific player before the new trigger is recorded. - -So the intended reading is: - -- rule header: target/output channel plus trigger ID -- rule body: readable boolean logic over ball, switch, lamp, coil, and attract state - -### DMD / PUP Trigger Rules - -Target channel `D` emits extra trigger IDs for display/video/DMD-side integrations. - -Current `D` triggers: - -- `60001`: Ball 1 -- `60002`: Ball 2 -- `60003`: Ball 3 -- `60005`: Bonus x2 -- `60006`: Bonus x3 -- `60007`: Extra Ball -- `60008`: Flash attract event -- `60009`: Game Over -- `60010`: Highscore -- `60012`: progress-1.mp4 trigger -- `60013`: progress-1.mp4 trigger -- `60014`: progress-1.mp4 trigger -- `60015`: progress-1.mp4 trigger -- `60016`: Shoot Again off -- `60017`: Special -- `60018`: Tilt - -These are not original ROM outputs. -They are interpretation layers built from ROM state transitions. - -### Speech Trigger Rules - -Target channel `O` is used for speech. - -Current speech-triggered rule: - -- `60010`: Highscore - -The speech text is defined in: -[flash.speech](/Volumes/data/workspace/PPUC/ppuc_combined/ppuc/examples/flash.speech) +Switches are added to those groups with the checkboxes on each switch edit page. -Current speech lines: +## SYS11 Coil-To-GI Mapping -- `60010`: `New highscore!` -- `60009`: `Game over.` -- `60007`: `Extra ball!` +Some Williams System 11 games use one or more coils to control GI strings or +parts of GI. PPUC supports this with `coilGiMappings` in game YAML: -Only IDs emitted by an `O` rule are spoken. -Right now the rules file explicitly emits `O 60010`, so `New highscore!` is the currently wired speech callout from the sample rule set. - -## New Rule-Driven Board Effects - -PPUC now also supports board-local effect triggering directly from the trigger-rule engine. - -This is the new path intended for: - -- named WS2812FX segment effects -- named shaker / WavePWM effects -- any effect that should run on the IO board and not be treated like a normal PinMAME lamp or coil - -### Rule Source - -Target channel `F` in a rules file means: - -- evaluate the rule in `ppuc` -- send a runtime `EVENT_SOURCE_EFFECT` -- let the firmware match that event against configured LED or PWM effects - -### Named Trigger IDs +```yaml +coilGiMappings: + - coil: 12 + gi: 1 + onBrightness: 8 + offBrightness: 0 + - coil: 12 + gi: 2 + onBrightness: 8 + offBrightness: 0 +``` -Rules and named YAML effect definitions can now use names instead of only numeric IDs. +When PinMAME reports the mapped coil as active, `ppuc-pinmame` sets the mapped +GI string to `onBrightness`. When the coil becomes inactive, it sets the GI +string to `offBrightness`. -Example rule: +In the config-tool, coil/GI mappings are entered on the game node as one mapping +per line: ```text -F cabinet-flash-attract 1 : lamp_rising(5) && attract +12: 1,2 = 8/0 +13: 3 = 0/8 ``` -Matching YAML effect definition: +The format is: -```yaml -- name: cabinet-flash-attract - effect: running_random - segment: 1 - color: 0000FF - speed: 0 - duration: 0 - mode: 0 - priority: 1 - repeat: -1 +```text +coil: gi[,gi...] = onBrightness/offBrightness ``` -The name is hashed deterministically on both sides, so the rule file and YAML can refer to the same effect without a hand-maintained numeric ID table. -No YAML `trigger:` block is required for named effects anymore. - -### Sample `F` Rules Added In The Repo - -The sample `flash.rules` now also contains: - -- `F cabinet-flash-attract 1 cooldown=20000 : lamp_rising(5) && attract` -- `F shaker-flash-hit 1 : coil_rising(6) && !attract` - -Rules can also use `delay=` when an effect should fire after a hold time instead of immediately. - -These demonstrate the intended future direction: - -- cabinet segment effects triggered by game state -- shaker effects triggered by ROM activity - -Important current status: +Brightness values are clamped to the PPUC GI range `0..8`. -- the runtime `F` transport path is implemented -- the sample `F` rules exist -- named YAML effects can now auto-register themselves as `F` targets +## Board Effect Triggers -So the intended setup is now: +Board-local PWM and LED effects are defined in YAML as effect targets. Lua rules +start those effects with `ppuc.effectTrigger(...)`. -- YAML defines effect targets and parameters -- `flash.rules` defines all boolean logic and trigger conditions -- YAML effect `trigger:` blocks are no longer needed for named effects - -## Config Tool Status - -`../config-tool` now needs one explicit machine effect name per LED or PWM effect. - -- That field is exported as YAML `name:`. -- The old effect-level `trigger:` field has been removed from LED and PWM effect content types. -- Generated YAML now emits only static effect definitions plus the machine effect name. -- All trigger logic now belongs exclusively in the `*.rules` file. - -## Effect Names - -PPUC now accepts named effect values in addition to numeric IDs. - -### WS2812FX Names - -Examples of accepted LED effect names: - -- `static` -- `blink` -- `breath` -- `color_wipe` -- `scan` -- `running_lights` -- `sparkle` -- `strobe` -- `chase_rainbow` -- `running_color` -- `running_random` -- `larson_scanner` -- `comet` -- `fireworks` -- `fire_flicker` -- `tricolor_chase` -- `twinklefox` -- `rain` -- `heartbeat` -- `multi_comet` -- `popcorn` -- `oscillator` - -### PWM / WavePWM Names - -Accepted PWM effect names: - -- `sine` -- `ramp_down_stop` -- `impulse` - -These names make the Flash YAML easier to read than raw numeric effect IDs. - -## Summary - -For Flash, the PPUC layer currently adds: - -- cabinet button/coin/start lighting on an external addressable LED string -- one large cabinet LED segment reserved for effect playback -- shaker motor feedback effects -- extra DMD/PUP trigger events derived from ROM activity -- optional speech callouts - -And it now supports a cleaner next step: - -- rule-driven named board effects via target channel `F` -- named trigger IDs -- named LED and PWM effect modes +```lua +function ppuc.onSwitchChanged(number, state) + if number == 18 and state == 1 then + ppuc.effectTrigger("jet-bumper", 1) + end +end +``` -That combination is the intended mechanism for cabinet animations and shaker patterns that should exist beside the original ROM rather than inside it. +The board effect trigger source is `F` internally. In YAML effect definitions, +use matching `trigger.source: F` plus `trigger.name` or `trigger.number` when a +simple board-side trigger is needed. For new rule-driven behavior, prefer Lua +logic and named effects. diff --git a/examples/flash.rules b/examples/flash.rules deleted file mode 100644 index b6328b6..0000000 --- a/examples/flash.rules +++ /dev/null @@ -1,67 +0,0 @@ -# Format: -# [value] [cooldown=] [delay=] : -# -# Expression functions: -# - switch() current switch state (true when closed) -# - lamp() current lamp state (true when lit) -# - coil() current coil state (true when active) -# - attract / attract() true if table is in attract mode (no game running) -# - switch_rising() switch transitioned 0 -> 1 in this event -# - switch_falling() -# - lamp_rising() -# - lamp_falling() -# - coil_rising() -# - coil_falling() -# -# Operators: !, &&, || and parentheses - -# Ball 1 -D 60001 1 : coil_rising(13) && lamp(42) && !attract - -# Ball 2 -D 60002 1 : coil_rising(13) && lamp(43) && !attract - -# Ball 3 -D 60003 1 : coil_rising(13) && lamp(44) && !attract - -# Extra Ball -D 60007 1 : lamp_rising(22) && !attract - -# Shoot Again -D 60016 1 : lamp_falling(22) && !attract - -# Bonus x2 -D 60005 1 : lamp_rising(20) && !attract - -# Bonus x3 -D 60006 1 : lamp_rising(21) && !attract - -# Flash -D 60008 1 cooldown=20000 : lamp_rising(5) && attract -F cabinet-flash-attract 1 cooldown=20000 : lamp_rising(5) && attract - -# Game Over -D 60009 1 : lamp_rising(23) && !attract - -# Highscore -D 60010 1 : lamp_rising(23) && !attract -O 60010 1 : lamp_rising(23) && !attract - -# progress-1.mp4, 30fps -D 60012 1 : lamp_rising(24) && !attract - -# progress-1.mp4, 30fps -D 60013 1 : lamp_rising(25) && !attract - -# progress-1.mp4, 30fps -D 60014 1 : lamp_rising(26) && !attract - -# progress-1.mp4, 30fps -D 60015 1 : lamp_rising(27) && !attract - -# Special -D 60017 1 : (switch_rising(13) || switch_rising(12)) && (lamp(2) || lamp(23)) && !attract - -# Tilt -D 60018 1 : lamp_rising(27) && !attract -F shaker-flash-hit 1 : coil_rising(6) && !attract diff --git a/examples/flash.speech b/examples/flash.speech deleted file mode 100644 index 224ff8a..0000000 --- a/examples/flash.speech +++ /dev/null @@ -1,15 +0,0 @@ -# Format: -# : -# -# Use with trigger rules that emit source "O". -# Example in flash.rules: -# O 60010 1 : lamp_rising(23) && !attract - -# Highscore -60010: New highscore! - -# Game Over -60009: Game over. - -# Extra Ball -60007: Extra ball! diff --git a/examples/ppuc-pinmame.ini b/examples/ppuc-pinmame.ini index 0e676bf..684f766 100644 --- a/examples/ppuc-pinmame.ini +++ b/examples/ppuc-pinmame.ini @@ -20,11 +20,8 @@ Serial = # Windows example: D:\PinMAME\ PinmamePath = -# Optional PUP trigger rules file. -PUPTriggers = - -# Optional speech trigger text file. -SpeechFile = +# Optional Lua rules file or directory. +Rules = # Optional comma-separated MP3 playlist for gameplay background music. MusicFiles = diff --git a/examples/pup-triggers.rules b/examples/pup-triggers.rules deleted file mode 100644 index b0ff00b..0000000 --- a/examples/pup-triggers.rules +++ /dev/null @@ -1,60 +0,0 @@ -# Format: -# [value] [cooldown=] [delay=] [set_player=] -# [clear_player_history=] : -# Special source: -# - S silent rule: apply state/history side effects, but emit no external trigger -# -# Expression functions: -# - ball() true if the current ball-in-play number matches -# - player() true if the current player matches -# - history() true if this trigger id fired before for the current player -# - history(, ) same, limited to the last milliseconds -# - sequence(, , , ...) true if those trigger ids fired in order within -# - switch() current switch state (true when closed) -# - lamp() current lamp state (true when lit) -# - coil() current coil state (true when active) -# - attract / attract() true if table is in attract mode (no game running) -# - switch_rising() switch transitioned 0 -> 1 in this event -# - switch_falling() -# - lamp_rising() -# - lamp_falling() -# - coil_rising() -# - coil_falling() -# -# Operators: !, &&, || and parentheses - -# Example from request: -# Trigger when switch 13 closes while lamp 42 is lit. -P 100 1 : switch_rising(13) && lamp(42) - -# Trigger when lamp 77 turns on and either switch 25 or 26 is closed. -P 101 1 : lamp_rising(77) && (switch(25) || switch(26)) - -# Trigger when switch 54 opens while lamp 77 is off. -P 102 0 : switch_falling(54) && !lamp(77) - -# Trigger when coil 20 becomes active while lamp 42 is lit. -P 103 1 : coil_rising(20) && lamp(42) - -# Trigger when the game advances to ball 2. -P 106 1 : ball(2) - -# Example: player selection from an external lamp-driven event without emitting anything. -S 201 1 set_player=1 : lamp_rising(101) - -# Example: clear all stored history for player 1 without emitting anything. -S 205 1 clear_player_history=1 : player(1) && ball(1) - -# Example: emit once per player, then emit a "ball save" style trigger if it repeats within 2 seconds. -P 202 1 : player(1) && !history(202) -P 203 1 : player(1) && history(202, 2000) - -# Example: detect "A then B then C" within 2 seconds for the current player. -P 204 1 : sequence(2000, 301, 302, 303) - -# Trigger at most every 500ms while in attract mode. -P 104 1 cooldown=500 : switch_rising(13) && attract - -# Trigger 750ms after switch 13 closes in attract mode. -# If switch 13 opens again before 750ms, this delayed trigger is cancelled. -P 105 1 delay=750 : switch(13) && attract diff --git a/examples/rules.lua b/examples/rules.lua new file mode 100644 index 0000000..ee427e1 --- /dev/null +++ b/examples/rules.lua @@ -0,0 +1,55 @@ +-- PPUC Lua rules example. +-- +-- Define any of these handlers as needed: +-- ppuc.onSwitchChanged(number, state) +-- ppuc.onLampChanged(number, state) +-- ppuc.onCoilChanged(number, state) +-- ppuc.onBallChanged(ball) +-- ppuc.onPlayerChanged(player) +-- ppuc.onRulesUpdate() + +function ppuc.onSwitchChanged(number, state) + if number == 13 and state == 1 and ppuc.lampState(42) then + ppuc.pupTrigger("P", 100, 1) + end + + if number == 13 and state == 1 and ppuc.attractMode() then + ppuc.pupTrigger("P", 101, 1) + ppuc.after(500, function() + ppuc.speech("Delayed attract switch callout") + end) + end + + -- Host-side ball-save interceptor example. + if number == 15 and state == 1 then + ppuc.setState("ballSaveReady") + end + + if ppuc.stateActive("ballSaveReady") and ppuc.switchGroupClosing("playfield") then + ppuc.clearState("ballSaveReady") + ppuc.setState("ballSave", 5000) + end + + if ppuc.stateActive("ballSave") and number == 9 and state == 1 then + ppuc.suppressSwitch(9) + ppuc.pulseCoil(7, 120) + end +end + +function ppuc.onLampChanged(number, state) + if number == 23 and state == 1 and not ppuc.attractMode() then + ppuc.speech("New highscore!") + end + + if number == 5 and state == 1 and ppuc.attractMode() then + ppuc.effectTrigger(1000, 1) + end +end + +function ppuc.onRulesUpdate() + if ppuc.stateActive("ballSaveReady") or ppuc.stateActive("ballSave") then + ppuc.blinkLamp(8, 250, 250) + else + ppuc.stopBlinkLamp(8) + end +end diff --git a/platforms/config.sh b/platforms/config.sh index 9d7aa4e..4dbfdfa 100644 --- a/platforms/config.sh +++ b/platforms/config.sh @@ -6,10 +6,93 @@ SDL_IMAGE_SHA=bec9134a26c7d0f31b36d6083c25296e04cabff5 SDL_MIXER_SHA=72a81869b45e249e8e67102db4e98dd2441f05a1 FLITE_SHA=6c9f20dc915b17f5619340069889db0aa007fcdc ESPEAK_NG_SHA=1.52.0 +LUA_VERSION=5.4.8 PINMAME_SHA=bf74d40ef837bdfc377c0266c0ef71b3ed59a751 PINMAME_NVRAM_MAPS_SHA=fa1086d57118e12f4802f3a9683c1e6acfb6ec6d -LIBPPUC_SHA=715dd76055e2552b31dcff35508721bf0445a396 -LIBSDLDMD_SHA=87c804e1ae53b86846dfb2e8a8fafb908e417e82 +LIBPPUC_SHA=93afce7bfcef68a7766283fb129b0d85f6838229 +LIBSDLDMD_SHA=72a7e9777af59fe430c6f6ae77159e0b8c7301b4 + +PPUC_SOURCE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PPUC_LOCAL_DEPS_ROOT="${PPUC_LOCAL_DEPS_ROOT:-$(cd "${PPUC_SOURCE_ROOT}/.." && pwd)}" +PPUC_USE_LOCAL_DEPS="${PPUC_USE_LOCAL_DEPS:-1}" +PPUC_LOCAL_DEPS_CACHE_BUSTER="${PPUC_LOCAL_DEPS_CACHE_BUSTER:-$(date +%s)}" + +ppuc_local_dependency_dir() { + local name="$1" + local dir="${PPUC_LOCAL_DEPS_ROOT}/${name}" + + if [ "${PPUC_USE_LOCAL_DEPS}" != "0" ] && [ -d "${dir}" ] && [ "${dir}" != "${PPUC_SOURCE_ROOT}" ]; then + echo "${dir}" + fi +} + +ppuc_dependency_cache_key() { + local name="$1" + local sha="$2" + local local_dir + + local_dir="$(ppuc_local_dependency_dir "${name}")" + if [ -n "${local_dir}" ]; then + echo "local:${local_dir}:${PPUC_LOCAL_DEPS_CACHE_BUSTER}" + else + echo "${sha}" + fi +} + +ppuc_print_dependency_source() { + local label="$1" + local name="$2" + local sha="$3" + local local_dir + + local_dir="$(ppuc_local_dependency_dir "${name}")" + if [ -n "${local_dir}" ]; then + echo " ${label}_SOURCE: local ${local_dir}" + else + echo " ${label}_SOURCE: archive ${sha}" + fi +} + +ppuc_prepare_dependency_source() { + local name="$1" + local sha="$2" + local url="$3" + local local_dir + + local_dir="$(ppuc_local_dependency_dir "${name}")" + if [ -n "${local_dir}" ]; then + echo "Using local ${name}: ${local_dir}" + ln -s "${local_dir}" "${name}" + else + curl -sL "${url}" -o "${name}-${sha}.tar.gz" + tar xzf "${name}-${sha}.tar.gz" + mv "${name}-${sha}" "${name}" + fi +} + +ppuc_stage_lua_source() { + local expected="${LUA_VERSION}" + local found + + found="$([ -f lua/cache.txt ] && cat lua/cache.txt || echo "")" + if [ "${expected}" != "${found}" ] || [ ! -f lua/lua/src/lua.h ]; then + echo "Preparing Lua. Expected: ${expected}, Found: ${found}" + + rm -rf lua + mkdir lua + cd lua + + ppuc_prepare_dependency_source lua "${LUA_VERSION}" "https://www.lua.org/ftp/lua-${LUA_VERSION}.tar.gz" + echo "${expected}" > cache.txt + + cd .. + fi + + mkdir -p ../third-party/include/lua ../third-party/lua-src + cp lua/lua/src/*.h ../third-party/include/lua/ + cp lua/lua/src/*.h ../third-party/lua-src/ + cp lua/lua/src/*.c ../third-party/lua-src/ +} if [ -z "${BUILD_TYPE}" ]; then BUILD_TYPE="Release" diff --git a/platforms/linux/aarch64/external.sh b/platforms/linux/aarch64/external.sh index e07d900..5e66b79 100755 --- a/platforms/linux/aarch64/external.sh +++ b/platforms/linux/aarch64/external.sh @@ -11,10 +11,13 @@ echo " SDL_IMAGE_SHA: ${SDL_IMAGE_SHA}" echo " SDL_MIXER_SHA: ${SDL_MIXER_SHA}" echo " FLITE_SHA: ${FLITE_SHA}" echo " ESPEAK_NG_SHA: ${ESPEAK_NG_SHA}" +echo " LUA_VERSION: ${LUA_VERSION}" echo " PINMAME_SHA: ${PINMAME_SHA}" echo " PINMAME_NVRAM_MAPS_SHA: ${PINMAME_NVRAM_MAPS_SHA}" echo " LIBPPUC_SHA: ${LIBPPUC_SHA}" +ppuc_print_dependency_source LIBPPUC libppuc "${LIBPPUC_SHA}" echo " LIBSDLDMD_SHA: ${LIBSDLDMD_SHA}" +ppuc_print_dependency_source LIBSDLDMD libsdldmd "${LIBSDLDMD_SHA}" echo "" if [ -z "${CACHE_DIR}" ]; then @@ -29,11 +32,13 @@ echo "" mkdir -p external ${CACHE_DIR} cd external +ppuc_stage_lua_source + # # build libsdldmd, SDL3_image, SDL3_mixer # -LIBSDLDMD_EXPECTED_SHA="${LIBSDLDMD_SHA}" +LIBSDLDMD_EXPECTED_SHA="$(ppuc_dependency_cache_key libsdldmd "${LIBSDLDMD_SHA}")" LIBSDLDMD_FOUND_SHA="$([ -f libsdldmd/cache.txt ] && cat libsdldmd/cache.txt || echo "")" if [ -n "${LIBSDLDMD_SHA}" ] && [ "${LIBSDLDMD_EXPECTED_SHA}" != "${LIBSDLDMD_FOUND_SHA}" ]; then @@ -43,9 +48,7 @@ if [ -n "${LIBSDLDMD_SHA}" ] && [ "${LIBSDLDMD_EXPECTED_SHA}" != "${LIBSDLDMD_FO mkdir libsdldmd cd libsdldmd - curl -sL https://github.com/PPUC/libsdldmd/archive/${LIBSDLDMD_SHA}.tar.gz -o libsdldmd-${LIBSDLDMD_SHA}.tar.gz - tar xzf libsdldmd-${LIBSDLDMD_SHA}.tar.gz - mv libsdldmd-${LIBSDLDMD_SHA} libsdldmd + ppuc_prepare_dependency_source libsdldmd "${LIBSDLDMD_SHA}" "https://github.com/PPUC/libsdldmd/archive/${LIBSDLDMD_SHA}.tar.gz" cd libsdldmd BUILD_TYPE=${BUILD_TYPE} platforms/linux/aarch64/external.sh @@ -279,7 +282,7 @@ fi # libppuc # -LIBPPUC_EXPECTED_SHA="${LIBPPUC_SHA}" +LIBPPUC_EXPECTED_SHA="$(ppuc_dependency_cache_key libppuc "${LIBPPUC_SHA}")" LIBPPUC_FOUND_SHA="$([ -f libppuc/cache.txt ] && cat libppuc/cache.txt || echo "")" if [ "${LIBPPUC_EXPECTED_SHA}" != "${LIBPPUC_FOUND_SHA}" ]; then @@ -289,9 +292,7 @@ if [ "${LIBPPUC_EXPECTED_SHA}" != "${LIBPPUC_FOUND_SHA}" ]; then mkdir libppuc cd libppuc - curl -sL https://github.com/PPUC/libppuc/archive/${LIBPPUC_SHA}.tar.gz -o libppuc-${LIBPPUC_SHA}.tar.gz - tar xzf libppuc-${LIBPPUC_SHA}.tar.gz - mv libppuc-${LIBPPUC_SHA} libppuc + ppuc_prepare_dependency_source libppuc "${LIBPPUC_SHA}" "https://github.com/PPUC/libppuc/archive/${LIBPPUC_SHA}.tar.gz" cd libppuc BUILD_TYPE=${BUILD_TYPE} platforms/linux/aarch64/external.sh diff --git a/platforms/linux/x64/external.sh b/platforms/linux/x64/external.sh index 1a5e5f0..c7e254e 100755 --- a/platforms/linux/x64/external.sh +++ b/platforms/linux/x64/external.sh @@ -11,10 +11,13 @@ echo " SDL_IMAGE_SHA: ${SDL_IMAGE_SHA}" echo " SDL_MIXER_SHA: ${SDL_MIXER_SHA}" echo " ESPEAK_NG_SHA: ${ESPEAK_NG_SHA}" echo " FLITE_SHA: ${FLITE_SHA}" +echo " LUA_VERSION: ${LUA_VERSION}" echo " PINMAME_SHA: ${PINMAME_SHA}" echo " PINMAME_NVRAM_MAPS_SHA: ${PINMAME_NVRAM_MAPS_SHA}" echo " LIBPPUC_SHA: ${LIBPPUC_SHA}" +ppuc_print_dependency_source LIBPPUC libppuc "${LIBPPUC_SHA}" echo " LIBSDLDMD_SHA: ${LIBSDLDMD_SHA}" +ppuc_print_dependency_source LIBSDLDMD libsdldmd "${LIBSDLDMD_SHA}" echo "" if [ -z "${CACHE_DIR}" ]; then @@ -29,11 +32,13 @@ echo "" mkdir -p external ${CACHE_DIR} cd external +ppuc_stage_lua_source + # # build libsdldmd, SDL3_image, SDL3_mixer # -LIBSDLDMD_EXPECTED_SHA="${LIBSDLDMD_SHA}" +LIBSDLDMD_EXPECTED_SHA="$(ppuc_dependency_cache_key libsdldmd "${LIBSDLDMD_SHA}")" LIBSDLDMD_FOUND_SHA="$([ -f libsdldmd/cache.txt ] && cat libsdldmd/cache.txt || echo "")" if [ -n "${LIBSDLDMD_SHA}" ] && [ "${LIBSDLDMD_EXPECTED_SHA}" != "${LIBSDLDMD_FOUND_SHA}" ]; then @@ -43,9 +48,7 @@ if [ -n "${LIBSDLDMD_SHA}" ] && [ "${LIBSDLDMD_EXPECTED_SHA}" != "${LIBSDLDMD_FO mkdir libsdldmd cd libsdldmd - curl -sL https://github.com/PPUC/libsdldmd/archive/${LIBSDLDMD_SHA}.tar.gz -o libsdldmd-${LIBSDLDMD_SHA}.tar.gz - tar xzf libsdldmd-${LIBSDLDMD_SHA}.tar.gz - mv libsdldmd-${LIBSDLDMD_SHA} libsdldmd + ppuc_prepare_dependency_source libsdldmd "${LIBSDLDMD_SHA}" "https://github.com/PPUC/libsdldmd/archive/${LIBSDLDMD_SHA}.tar.gz" cd libsdldmd BUILD_TYPE=${BUILD_TYPE} platforms/linux/x64/external.sh @@ -277,7 +280,7 @@ fi # libppuc # -LIBPPUC_EXPECTED_SHA="${LIBPPUC_SHA}" +LIBPPUC_EXPECTED_SHA="$(ppuc_dependency_cache_key libppuc "${LIBPPUC_SHA}")" LIBPPUC_FOUND_SHA="$([ -f libppuc/cache.txt ] && cat libppuc/cache.txt || echo "")" if [ "${LIBPPUC_EXPECTED_SHA}" != "${LIBPPUC_FOUND_SHA}" ]; then @@ -287,9 +290,7 @@ if [ "${LIBPPUC_EXPECTED_SHA}" != "${LIBPPUC_FOUND_SHA}" ]; then mkdir libppuc cd libppuc - curl -sL https://github.com/PPUC/libppuc/archive/${LIBPPUC_SHA}.tar.gz -o libppuc-${LIBPPUC_SHA}.tar.gz - tar xzf libppuc-${LIBPPUC_SHA}.tar.gz - mv libppuc-${LIBPPUC_SHA} libppuc + ppuc_prepare_dependency_source libppuc "${LIBPPUC_SHA}" "https://github.com/PPUC/libppuc/archive/${LIBPPUC_SHA}.tar.gz" cd libppuc BUILD_TYPE=${BUILD_TYPE} platforms/linux/x64/external.sh diff --git a/platforms/macos/arm64/external.sh b/platforms/macos/arm64/external.sh index 7ac3df0..ab238d5 100755 --- a/platforms/macos/arm64/external.sh +++ b/platforms/macos/arm64/external.sh @@ -11,10 +11,13 @@ echo " SDL_IMAGE_SHA: ${SDL_IMAGE_SHA}" echo " SDL_MIXER_SHA: ${SDL_MIXER_SHA}" echo " FLITE_SHA: ${FLITE_SHA}" echo " ESPEAK_NG_SHA: ${ESPEAK_NG_SHA}" +echo " LUA_VERSION: ${LUA_VERSION}" echo " PINMAME_SHA: ${PINMAME_SHA}" echo " PINMAME_NVRAM_MAPS_SHA: ${PINMAME_NVRAM_MAPS_SHA}" echo " LIBPPUC_SHA: ${LIBPPUC_SHA}" +ppuc_print_dependency_source LIBPPUC libppuc "${LIBPPUC_SHA}" echo " LIBSDLDMD_SHA: ${LIBSDLDMD_SHA}" +ppuc_print_dependency_source LIBSDLDMD libsdldmd "${LIBSDLDMD_SHA}" echo "" if [ -z "${CACHE_DIR}" ]; then @@ -29,6 +32,8 @@ echo "" mkdir -p external ${CACHE_DIR} cd external +ppuc_stage_lua_source + # # flite # @@ -69,7 +74,7 @@ if [ "${FLITE_EXPECTED_SHA}" != "${FLITE_FOUND_SHA}" ] || [ "${FLITE_ARTIFACTS_O cd .. fi -LIBSDLDMD_EXPECTED_SHA="${LIBSDLDMD_SHA}" +LIBSDLDMD_EXPECTED_SHA="$(ppuc_dependency_cache_key libsdldmd "${LIBSDLDMD_SHA}")" LIBSDLDMD_FOUND_SHA="$([ -f libsdldmd/cache.txt ] && cat libsdldmd/cache.txt || echo "")" if [ -n "${LIBSDLDMD_SHA}" ] && [ "${LIBSDLDMD_EXPECTED_SHA}" != "${LIBSDLDMD_FOUND_SHA}" ]; then @@ -79,9 +84,7 @@ if [ -n "${LIBSDLDMD_SHA}" ] && [ "${LIBSDLDMD_EXPECTED_SHA}" != "${LIBSDLDMD_FO mkdir libsdldmd cd libsdldmd - curl -sL https://github.com/PPUC/libsdldmd/archive/${LIBSDLDMD_SHA}.tar.gz -o libsdldmd-${LIBSDLDMD_SHA}.tar.gz - tar xzf libsdldmd-${LIBSDLDMD_SHA}.tar.gz - mv libsdldmd-${LIBSDLDMD_SHA} libsdldmd + ppuc_prepare_dependency_source libsdldmd "${LIBSDLDMD_SHA}" "https://github.com/PPUC/libsdldmd/archive/${LIBSDLDMD_SHA}.tar.gz" cd libsdldmd BUILD_TYPE=${BUILD_TYPE} platforms/macos/arm64/external.sh @@ -280,7 +283,7 @@ fi # libppuc # -LIBPPUC_EXPECTED_SHA="${LIBPPUC_SHA}" +LIBPPUC_EXPECTED_SHA="$(ppuc_dependency_cache_key libppuc "${LIBPPUC_SHA}")" LIBPPUC_FOUND_SHA="$([ -f libppuc/cache.txt ] && cat libppuc/cache.txt || echo "")" if [ "${LIBPPUC_EXPECTED_SHA}" != "${LIBPPUC_FOUND_SHA}" ]; then @@ -290,9 +293,7 @@ if [ "${LIBPPUC_EXPECTED_SHA}" != "${LIBPPUC_FOUND_SHA}" ]; then mkdir libppuc cd libppuc - curl -sL https://github.com/PPUC/libppuc/archive/${LIBPPUC_SHA}.tar.gz -o libppuc-${LIBPPUC_SHA}.tar.gz - tar xzf libppuc-${LIBPPUC_SHA}.tar.gz - mv libppuc-${LIBPPUC_SHA} libppuc + ppuc_prepare_dependency_source libppuc "${LIBPPUC_SHA}" "https://github.com/PPUC/libppuc/archive/${LIBPPUC_SHA}.tar.gz" cd libppuc BUILD_TYPE=${BUILD_TYPE} platforms/macos/arm64/external.sh diff --git a/platforms/macos/x64/external.sh b/platforms/macos/x64/external.sh index 7dab260..aede570 100755 --- a/platforms/macos/x64/external.sh +++ b/platforms/macos/x64/external.sh @@ -10,10 +10,13 @@ echo "Building libraries..." echo " SDL_IMAGE_SHA: ${SDL_IMAGE_SHA}" echo " SDL_MIXER_SHA: ${SDL_MIXER_SHA}" echo " ESPEAK_NG_SHA: ${ESPEAK_NG_SHA}" +echo " LUA_VERSION: ${LUA_VERSION}" echo " PINMAME_SHA: ${PINMAME_SHA}" echo " PINMAME_NVRAM_MAPS_SHA: ${PINMAME_NVRAM_MAPS_SHA}" echo " LIBPPUC_SHA: ${LIBPPUC_SHA}" +ppuc_print_dependency_source LIBPPUC libppuc "${LIBPPUC_SHA}" echo " LIBSDLDMD_SHA: ${LIBSDLDMD_SHA}" +ppuc_print_dependency_source LIBSDLDMD libsdldmd "${LIBSDLDMD_SHA}" echo "" if [ -z "${CACHE_DIR}" ]; then @@ -28,11 +31,13 @@ echo "" mkdir -p external ${CACHE_DIR} cd external +ppuc_stage_lua_source + # # build libsdldmd, SDL3_image, SDL3_mixer # -LIBSDLDMD_EXPECTED_SHA="${LIBSDLDMD_SHA}" +LIBSDLDMD_EXPECTED_SHA="$(ppuc_dependency_cache_key libsdldmd "${LIBSDLDMD_SHA}")" LIBSDLDMD_FOUND_SHA="$([ -f libsdldmd/cache.txt ] && cat libsdldmd/cache.txt || echo "")" if [ -n "${LIBSDLDMD_SHA}" ] && [ "${LIBSDLDMD_EXPECTED_SHA}" != "${LIBSDLDMD_FOUND_SHA}" ]; then @@ -42,9 +47,7 @@ if [ -n "${LIBSDLDMD_SHA}" ] && [ "${LIBSDLDMD_EXPECTED_SHA}" != "${LIBSDLDMD_FO mkdir libsdldmd cd libsdldmd - curl -sL https://github.com/PPUC/libsdldmd/archive/${LIBSDLDMD_SHA}.tar.gz -o libsdldmd-${LIBSDLDMD_SHA}.tar.gz - tar xzf libsdldmd-${LIBSDLDMD_SHA}.tar.gz - mv libsdldmd-${LIBSDLDMD_SHA} libsdldmd + ppuc_prepare_dependency_source libsdldmd "${LIBSDLDMD_SHA}" "https://github.com/PPUC/libsdldmd/archive/${LIBSDLDMD_SHA}.tar.gz" cd libsdldmd BUILD_TYPE=${BUILD_TYPE} platforms/macos/x64/external.sh @@ -239,7 +242,7 @@ fi # libppuc # -LIBPPUC_EXPECTED_SHA="${LIBPPUC_SHA}" +LIBPPUC_EXPECTED_SHA="$(ppuc_dependency_cache_key libppuc "${LIBPPUC_SHA}")" LIBPPUC_FOUND_SHA="$([ -f libppuc/cache.txt ] && cat libppuc/cache.txt || echo "")" if [ "${LIBPPUC_EXPECTED_SHA}" != "${LIBPPUC_FOUND_SHA}" ]; then @@ -249,9 +252,7 @@ if [ "${LIBPPUC_EXPECTED_SHA}" != "${LIBPPUC_FOUND_SHA}" ]; then mkdir libppuc cd libppuc - curl -sL https://github.com/PPUC/libppuc/archive/${LIBPPUC_SHA}.tar.gz -o libppuc-${LIBPPUC_SHA}.tar.gz - tar xzf libppuc-${LIBPPUC_SHA}.tar.gz - mv libppuc-${LIBPPUC_SHA} libppuc + ppuc_prepare_dependency_source libppuc "${LIBPPUC_SHA}" "https://github.com/PPUC/libppuc/archive/${LIBPPUC_SHA}.tar.gz" cd libppuc BUILD_TYPE=${BUILD_TYPE} platforms/macos/x64/external.sh diff --git a/platforms/win-mingw/x64/external.sh b/platforms/win-mingw/x64/external.sh index c4153a6..7e8d2b1 100755 --- a/platforms/win-mingw/x64/external.sh +++ b/platforms/win-mingw/x64/external.sh @@ -9,10 +9,13 @@ NUM_PROCS=$(nproc) echo "Building libraries..." echo " SDL_IMAGE_SHA: ${SDL_IMAGE_SHA}" echo " SDL_MIXER_SHA: ${SDL_MIXER_SHA}" +echo " LUA_VERSION: ${LUA_VERSION}" echo " PINMAME_SHA: ${PINMAME_SHA}" echo " PINMAME_NVRAM_MAPS_SHA: ${PINMAME_NVRAM_MAPS_SHA}" echo " LIBPPUC_SHA: ${LIBPPUC_SHA}" +ppuc_print_dependency_source LIBPPUC libppuc "${LIBPPUC_SHA}" echo " LIBSDLDMD_SHA: ${LIBSDLDMD_SHA}" +ppuc_print_dependency_source LIBSDLDMD libsdldmd "${LIBSDLDMD_SHA}" echo "" if [ -z "${CACHE_DIR}" ]; then @@ -32,7 +35,9 @@ mkdir -p \ third-party/runtime-libs/win-mingw-x64 cd external -LIBSDLDMD_EXPECTED_SHA="${LIBSDLDMD_SHA}" +ppuc_stage_lua_source + +LIBSDLDMD_EXPECTED_SHA="$(ppuc_dependency_cache_key libsdldmd "${LIBSDLDMD_SHA}")" LIBSDLDMD_FOUND_SHA="$([ -f libsdldmd/cache.txt ] && cat libsdldmd/cache.txt || echo "")" if [ -n "${LIBSDLDMD_SHA}" ] && [ "${LIBSDLDMD_EXPECTED_SHA}" != "${LIBSDLDMD_FOUND_SHA}" ]; then @@ -42,9 +47,7 @@ if [ -n "${LIBSDLDMD_SHA}" ] && [ "${LIBSDLDMD_EXPECTED_SHA}" != "${LIBSDLDMD_FO mkdir libsdldmd cd libsdldmd - curl -sL https://github.com/PPUC/libsdldmd/archive/${LIBSDLDMD_SHA}.tar.gz -o libsdldmd-${LIBSDLDMD_SHA}.tar.gz - tar xzf libsdldmd-${LIBSDLDMD_SHA}.tar.gz - mv libsdldmd-${LIBSDLDMD_SHA} libsdldmd + ppuc_prepare_dependency_source libsdldmd "${LIBSDLDMD_SHA}" "https://github.com/PPUC/libsdldmd/archive/${LIBSDLDMD_SHA}.tar.gz" cd libsdldmd BUILD_TYPE=${BUILD_TYPE} platforms/win-mingw/x64/external.sh @@ -187,7 +190,7 @@ if [ "${PINMAME_NVRAM_MAPS_EXPECTED_SHA}" != "${PINMAME_NVRAM_MAPS_FOUND_SHA}" ] cd .. fi -LIBPPUC_EXPECTED_SHA="${LIBPPUC_SHA}" +LIBPPUC_EXPECTED_SHA="$(ppuc_dependency_cache_key libppuc "${LIBPPUC_SHA}")" LIBPPUC_FOUND_SHA="$([ -f libppuc/cache.txt ] && cat libppuc/cache.txt || echo "")" if [ "${LIBPPUC_EXPECTED_SHA}" != "${LIBPPUC_FOUND_SHA}" ]; then @@ -197,9 +200,7 @@ if [ "${LIBPPUC_EXPECTED_SHA}" != "${LIBPPUC_FOUND_SHA}" ]; then mkdir libppuc cd libppuc - curl -sL https://github.com/PPUC/libppuc/archive/${LIBPPUC_SHA}.tar.gz -o libppuc-${LIBPPUC_SHA}.tar.gz - tar xzf libppuc-${LIBPPUC_SHA}.tar.gz - mv libppuc-${LIBPPUC_SHA} libppuc + ppuc_prepare_dependency_source libppuc "${LIBPPUC_SHA}" "https://github.com/PPUC/libppuc/archive/${LIBPPUC_SHA}.tar.gz" cd libppuc BUILD_TYPE=${BUILD_TYPE} platforms/win-mingw/x64/external.sh diff --git a/platforms/win/x64/external.sh b/platforms/win/x64/external.sh index 41cae2e..e015fa6 100755 --- a/platforms/win/x64/external.sh +++ b/platforms/win/x64/external.sh @@ -7,10 +7,13 @@ source ./platforms/config.sh echo "Building libraries..." echo " SDL_IMAGE_SHA: ${SDL_IMAGE_SHA}" echo " SDL_MIXER_SHA: ${SDL_MIXER_SHA}" +echo " LUA_VERSION: ${LUA_VERSION}" echo " PINMAME_SHA: ${PINMAME_SHA}" echo " PINMAME_NVRAM_MAPS_SHA: ${PINMAME_NVRAM_MAPS_SHA}" echo " LIBPPUC_SHA: ${LIBPPUC_SHA}" +ppuc_print_dependency_source LIBPPUC libppuc "${LIBPPUC_SHA}" echo " LIBSDLDMD_SHA: ${LIBSDLDMD_SHA}" +ppuc_print_dependency_source LIBSDLDMD libsdldmd "${LIBSDLDMD_SHA}" echo "" if [ -z "${CACHE_DIR}" ]; then @@ -24,11 +27,13 @@ echo "" mkdir -p external ${CACHE_DIR} cd external +ppuc_stage_lua_source + # # build libsdldmd, SDL3_image, SDL3_mixer # -LIBSDLDMD_EXPECTED_SHA="${LIBSDLDMD_SHA}" +LIBSDLDMD_EXPECTED_SHA="$(ppuc_dependency_cache_key libsdldmd "${LIBSDLDMD_SHA}")" LIBSDLDMD_FOUND_SHA="$([ -f libsdldmd/cache.txt ] && cat libsdldmd/cache.txt || echo "")" if [ -n "${LIBSDLDMD_SHA}" ] && [ "${LIBSDLDMD_EXPECTED_SHA}" != "${LIBSDLDMD_FOUND_SHA}" ]; then @@ -38,9 +43,7 @@ if [ -n "${LIBSDLDMD_SHA}" ] && [ "${LIBSDLDMD_EXPECTED_SHA}" != "${LIBSDLDMD_FO mkdir libsdldmd cd libsdldmd - curl -sL https://github.com/PPUC/libsdldmd/archive/${LIBSDLDMD_SHA}.tar.gz -o libsdldmd-${LIBSDLDMD_SHA}.tar.gz - tar xzf libsdldmd-${LIBSDLDMD_SHA}.tar.gz - mv libsdldmd-${LIBSDLDMD_SHA} libsdldmd + ppuc_prepare_dependency_source libsdldmd "${LIBSDLDMD_SHA}" "https://github.com/PPUC/libsdldmd/archive/${LIBSDLDMD_SHA}.tar.gz" cd libsdldmd BUILD_TYPE=${BUILD_TYPE} platforms/win/x64/external.sh @@ -190,7 +193,7 @@ fi # libppuc # -LIBPPUC_EXPECTED_SHA="${LIBPPUC_SHA}" +LIBPPUC_EXPECTED_SHA="$(ppuc_dependency_cache_key libppuc "${LIBPPUC_SHA}")" LIBPPUC_FOUND_SHA="$([ -f libppuc/cache.txt ] && cat libppuc/cache.txt || echo "")" if [ "${LIBPPUC_EXPECTED_SHA}" != "${LIBPPUC_FOUND_SHA}" ]; then @@ -200,9 +203,7 @@ if [ "${LIBPPUC_EXPECTED_SHA}" != "${LIBPPUC_FOUND_SHA}" ]; then mkdir libppuc cd libppuc - curl -sL https://github.com/PPUC/libppuc/archive/${LIBPPUC_SHA}.tar.gz -o libppuc-${LIBPPUC_SHA}.tar.gz - tar xzf libppuc-${LIBPPUC_SHA}.tar.gz - mv libppuc-${LIBPPUC_SHA} libppuc + ppuc_prepare_dependency_source libppuc "${LIBPPUC_SHA}" "https://github.com/PPUC/libppuc/archive/${LIBPPUC_SHA}.tar.gz" cd libppuc platforms/win/x64/external.sh diff --git a/platforms/win/x86/external.sh b/platforms/win/x86/external.sh index 58af0e6..b197092 100755 --- a/platforms/win/x86/external.sh +++ b/platforms/win/x86/external.sh @@ -7,10 +7,13 @@ source ./platforms/config.sh echo "Building libraries..." echo " SDL_IMAGE_SHA: ${SDL_IMAGE_SHA}" echo " SDL_MIXER_SHA: ${SDL_MIXER_SHA}" +echo " LUA_VERSION: ${LUA_VERSION}" echo " PINMAME_SHA: ${PINMAME_SHA}" echo " PINMAME_NVRAM_MAPS_SHA: ${PINMAME_NVRAM_MAPS_SHA}" echo " LIBPPUC_SHA: ${LIBPPUC_SHA}" +ppuc_print_dependency_source LIBPPUC libppuc "${LIBPPUC_SHA}" echo " LIBSDLDMD_SHA: ${LIBSDLDMD_SHA}" +ppuc_print_dependency_source LIBSDLDMD libsdldmd "${LIBSDLDMD_SHA}" echo "" if [ -z "${CACHE_DIR}" ]; then @@ -24,11 +27,13 @@ echo "" mkdir -p external ${CACHE_DIR} cd external +ppuc_stage_lua_source + # # build libsdldmd, SDL3_image, SDL3_mixer # -LIBSDLDMD_EXPECTED_SHA="${LIBSDLDMD_SHA}" +LIBSDLDMD_EXPECTED_SHA="$(ppuc_dependency_cache_key libsdldmd "${LIBSDLDMD_SHA}")" LIBSDLDMD_FOUND_SHA="$([ -f libsdldmd/cache.txt ] && cat libsdldmd/cache.txt || echo "")" if [ -n "${LIBSDLDMD_SHA}" ] && [ "${LIBSDLDMD_EXPECTED_SHA}" != "${LIBSDLDMD_FOUND_SHA}" ]; then @@ -38,9 +43,7 @@ if [ -n "${LIBSDLDMD_SHA}" ] && [ "${LIBSDLDMD_EXPECTED_SHA}" != "${LIBSDLDMD_FO mkdir libsdldmd cd libsdldmd - curl -sL https://github.com/PPUC/libsdldmd/archive/${LIBSDLDMD_SHA}.tar.gz -o libsdldmd-${LIBSDLDMD_SHA}.tar.gz - tar xzf libsdldmd-${LIBSDLDMD_SHA}.tar.gz - mv libsdldmd-${LIBSDLDMD_SHA} libsdldmd + ppuc_prepare_dependency_source libsdldmd "${LIBSDLDMD_SHA}" "https://github.com/PPUC/libsdldmd/archive/${LIBSDLDMD_SHA}.tar.gz" cd libsdldmd BUILD_TYPE=${BUILD_TYPE} platforms/win/x86/external.sh @@ -192,7 +195,7 @@ fi # libppuc # -LIBPPUC_EXPECTED_SHA="${LIBPPUC_SHA}" +LIBPPUC_EXPECTED_SHA="$(ppuc_dependency_cache_key libppuc "${LIBPPUC_SHA}")" LIBPPUC_FOUND_SHA="$([ -f libppuc/cache.txt ] && cat libppuc/cache.txt || echo "")" if [ "${LIBPPUC_EXPECTED_SHA}" != "${LIBPPUC_FOUND_SHA}" ]; then @@ -202,9 +205,7 @@ if [ "${LIBPPUC_EXPECTED_SHA}" != "${LIBPPUC_FOUND_SHA}" ]; then mkdir libppuc cd libppuc - curl -sL https://github.com/PPUC/libppuc/archive/${LIBPPUC_SHA}.tar.gz -o libppuc-${LIBPPUC_SHA}.tar.gz - tar xzf libppuc-${LIBPPUC_SHA}.tar.gz - mv libppuc-${LIBPPUC_SHA} libppuc + ppuc_prepare_dependency_source libppuc "${LIBPPUC_SHA}" "https://github.com/PPUC/libppuc/archive/${LIBPPUC_SHA}.tar.gz" cd libppuc platforms/win/x86/external.sh diff --git a/src/LuaRulesEngine.cpp b/src/LuaRulesEngine.cpp new file mode 100644 index 0000000..fd2d624 --- /dev/null +++ b/src/LuaRulesEngine.cpp @@ -0,0 +1,931 @@ +#include "LuaRulesEngine.h" + +#include +#include +#include +#include + +extern "C" +{ +#include +#include +#include +} + +namespace +{ +constexpr uint32_t kHistoryRetentionMs = 60000; +constexpr char kBoardEffectTriggerSource = 'F'; +constexpr const char* kHandlerNames[] = { + "onSwitchChanged", + "onLampChanged", + "onCoilChanged", + "onBallChanged", + "onPlayerChanged", + "onRulesUpdate", +}; + +using LuaFn = int (*)(lua_State*); + +void OpenLuaLibrary(lua_State* L, const char* name, lua_CFunction fn) +{ + luaL_requiref(L, name, fn, 1); + lua_pop(L, 1); +} + +void SetPpucFunction(lua_State* L, LuaRulesEngine* engine, const char* name, LuaFn fn) +{ + lua_pushlightuserdata(L, engine); + lua_pushcclosure(L, fn, 1); + lua_setfield(L, -2, name); +} + +uint16_t HashNamedTriggerId(const char* name) +{ + if (name == nullptr) + { + return 0; + } + + uint32_t hash = 2166136261u; + while (*name != '\0') + { + hash ^= static_cast(*name++); + hash *= 16777619u; + } + + hash ^= (hash >> 16); + const uint16_t reduced = static_cast(hash & 0xFFFFu); + return reduced == 0 ? 1 : reduced; +} +} // namespace + +LuaRulesEngine::LuaRulesEngine() = default; + +LuaRulesEngine::~LuaRulesEngine() +{ + if (m_lua != nullptr) + { + ClearRegisteredHandlers(); + ClearScheduledCallbacks(); + lua_close(m_lua); + m_lua = nullptr; + } +} + +void LuaRulesEngine::SetDebug(bool debug) +{ + std::lock_guard lock(m_mutex); + m_debug = debug; +} + +void LuaRulesEngine::SetTriggerCallback(TriggerCallback callback) +{ + std::lock_guard lock(m_mutex); + m_triggerCallback = std::move(callback); +} + +void LuaRulesEngine::SetSpeechCallback(SpeechCallback callback) +{ + std::lock_guard lock(m_mutex); + m_speechCallback = std::move(callback); +} + +void LuaRulesEngine::SetActionCallback(ActionCallback callback) +{ + std::lock_guard lock(m_mutex); + m_actionCallback = std::move(callback); +} + +void LuaRulesEngine::SetSwitchGroups(const std::unordered_map>& switchGroups) +{ + std::lock_guard lock(m_mutex); + m_switchGroups = switchGroups; +} + +bool LuaRulesEngine::InitializeLua(std::string& error) +{ + if (m_lua != nullptr) + { + lua_close(m_lua); + m_lua = nullptr; + } + + m_lua = luaL_newstate(); + if (m_lua == nullptr) + { + error = "Unable to create Lua state"; + return false; + } + + OpenLuaLibrary(m_lua, LUA_GNAME, luaopen_base); + OpenLuaLibrary(m_lua, LUA_TABLIBNAME, luaopen_table); + OpenLuaLibrary(m_lua, LUA_STRLIBNAME, luaopen_string); + OpenLuaLibrary(m_lua, LUA_MATHLIBNAME, luaopen_math); + lua_pushnil(m_lua); + lua_setglobal(m_lua, "dofile"); + lua_pushnil(m_lua); + lua_setglobal(m_lua, "loadfile"); + RegisterApi(); + return true; +} + +void LuaRulesEngine::RegisterApi() +{ + lua_newtable(m_lua); + + SetPpucFunction(m_lua, this, "switchState", LuaSwitchState); + SetPpucFunction(m_lua, this, "lampState", LuaLampState); + SetPpucFunction(m_lua, this, "coilState", LuaCoilState); + SetPpucFunction(m_lua, this, "currentBall", LuaCurrentBall); + SetPpucFunction(m_lua, this, "currentPlayer", LuaCurrentPlayer); + SetPpucFunction(m_lua, this, "attractMode", LuaAttractMode); + + SetPpucFunction(m_lua, this, "switchGroupState", LuaSwitchGroupState); + SetPpucFunction(m_lua, this, "switchGroupClosing", LuaSwitchGroupClosing); + SetPpucFunction(m_lua, this, "switchGroupOpening", LuaSwitchGroupOpening); + + SetPpucFunction(m_lua, this, "setState", LuaSetState); + SetPpucFunction(m_lua, this, "clearState", LuaClearState); + SetPpucFunction(m_lua, this, "stateActive", LuaStateActive); + SetPpucFunction(m_lua, this, "triggerHistory", LuaTriggerHistory); + SetPpucFunction(m_lua, this, "triggerSequence", LuaTriggerSequence); + SetPpucFunction(m_lua, this, "onlyOnceEvery", LuaOnlyOnceEvery); + SetPpucFunction(m_lua, this, "after", LuaAfter); + + SetPpucFunction(m_lua, this, "pupTrigger", LuaPupTrigger); + SetPpucFunction(m_lua, this, "speech", LuaSpeech); + SetPpucFunction(m_lua, this, "effectTrigger", LuaEffectTrigger); + SetPpucFunction(m_lua, this, "suppressSwitch", LuaSuppressSwitch); + SetPpucFunction(m_lua, this, "sendSwitchToCpu", LuaSendSwitchToCpu); + SetPpucFunction(m_lua, this, "pulseCoil", LuaPulseCoil); + SetPpucFunction(m_lua, this, "blinkLamp", LuaBlinkLamp); + SetPpucFunction(m_lua, this, "stopBlinkLamp", LuaStopBlinkLamp); + + lua_setglobal(m_lua, "ppuc"); +} + +bool LuaRulesEngine::LoadScript(const char* path, std::string& error) +{ + return LoadScripts(std::vector{path}, error); +} + +bool LuaRulesEngine::LoadScripts(const std::vector& paths, std::string& error) +{ + std::lock_guard lock(m_mutex); + m_fatalError = false; + m_fatalErrorMessage.clear(); + ClearRegisteredHandlers(); + ClearScheduledCallbacks(); + + if (!InitializeLua(error)) + { + return false; + } + + for (const std::string& path : paths) + { + if (!LoadScriptIntoState(path.c_str(), error)) + { + return false; + } + } + + return true; +} + +void LuaRulesEngine::ClearRegisteredHandlers() +{ + if (m_lua == nullptr) + { + m_handlers.clear(); + return; + } + + for (auto& entry : m_handlers) + { + for (const int ref : entry.second) + { + luaL_unref(m_lua, LUA_REGISTRYINDEX, ref); + } + } + m_handlers.clear(); +} + +void LuaRulesEngine::ClearScheduledCallbacks() +{ + if (m_lua != nullptr) + { + for (const ScheduledCallback& callback : m_scheduledCallbacks) + { + luaL_unref(m_lua, LUA_REGISTRYINDEX, callback.ref); + } + } + m_scheduledCallbacks.clear(); + m_nextScheduledSequence = 0; +} + +void LuaRulesEngine::ClearPpucHandlers() +{ + lua_getglobal(m_lua, "ppuc"); + for (const char* name : kHandlerNames) + { + lua_pushnil(m_lua); + lua_setfield(m_lua, -2, name); + } + lua_pop(m_lua, 1); +} + +void LuaRulesEngine::CapturePpucHandlers() +{ + lua_getglobal(m_lua, "ppuc"); + for (const char* name : kHandlerNames) + { + lua_getfield(m_lua, -1, name); + if (lua_isfunction(m_lua, -1)) + { + m_handlers[name].push_back(luaL_ref(m_lua, LUA_REGISTRYINDEX)); + } + else + { + lua_pop(m_lua, 1); + } + } + lua_pop(m_lua, 1); +} + +bool LuaRulesEngine::LoadScriptIntoState(const char* path, std::string& error) +{ + ClearPpucHandlers(); + + if (luaL_loadfile(m_lua, path) != LUA_OK) + { + error = lua_tostring(m_lua, -1); + lua_pop(m_lua, 1); + return false; + } + + if (lua_pcall(m_lua, 0, 0, 0) != LUA_OK) + { + error = lua_tostring(m_lua, -1); + lua_pop(m_lua, 1); + return false; + } + + CapturePpucHandlers(); + return true; +} + +bool LuaRulesEngine::CallHandler(const char* name) +{ + const auto it = m_handlers.find(name); + if (it == m_handlers.end()) + { + return true; + } + + for (const int handlerRef : it->second) + { + if (!CallRegisteredHandler(handlerRef, name, {})) + { + return false; + } + } + return true; +} + +bool LuaRulesEngine::CallHandler(const char* name, int arg1) +{ + const auto it = m_handlers.find(name); + if (it == m_handlers.end()) + { + return true; + } + + for (const int handlerRef : it->second) + { + if (!CallRegisteredHandler(handlerRef, name, {arg1})) + { + return false; + } + } + return true; +} + +bool LuaRulesEngine::CallHandler(const char* name, int arg1, int arg2) +{ + const auto it = m_handlers.find(name); + if (it == m_handlers.end()) + { + return true; + } + + for (const int handlerRef : it->second) + { + if (!CallRegisteredHandler(handlerRef, name, {arg1, arg2})) + { + return false; + } + } + return true; +} + +bool LuaRulesEngine::CallRegisteredHandler(int handlerRef, const char* name, const std::vector& args) +{ + lua_rawgeti(m_lua, LUA_REGISTRYINDEX, handlerRef); + for (const int arg : args) + { + lua_pushinteger(m_lua, arg); + } + + if (lua_pcall(m_lua, static_cast(args.size()), 0, 0) != LUA_OK) + { + std::string error = lua_tostring(m_lua, -1); + if (name != nullptr && name[0] != '\0') + { + error = std::string(name) + ": " + error; + } + SetFatalError(error); + lua_pop(m_lua, 1); + return false; + } + return true; +} + +bool LuaRulesEngine::CallScheduledCallback(int callbackRef) +{ + lua_rawgeti(m_lua, LUA_REGISTRYINDEX, callbackRef); + luaL_unref(m_lua, LUA_REGISTRYINDEX, callbackRef); + + if (lua_pcall(m_lua, 0, 0, 0) != LUA_OK) + { + std::string error = lua_tostring(m_lua, -1); + SetFatalError(std::string("scheduled callback: ") + error); + lua_pop(m_lua, 1); + return false; + } + return true; +} + +void LuaRulesEngine::RunDueScheduledCallbacks(uint64_t nowMs) +{ + std::vector due; + for (auto it = m_scheduledCallbacks.begin(); it != m_scheduledCallbacks.end();) + { + if (it->dueMs <= nowMs) + { + due.push_back(*it); + it = m_scheduledCallbacks.erase(it); + } + else + { + ++it; + } + } + + std::sort(due.begin(), due.end(), [](const ScheduledCallback& a, const ScheduledCallback& b) { + if (a.dueMs != b.dueMs) + { + return a.dueMs < b.dueMs; + } + return a.sequence < b.sequence; + }); + + for (const ScheduledCallback& callback : due) + { + if (!CallScheduledCallback(callback.ref) || m_fatalError) + { + return; + } + } +} + +void LuaRulesEngine::Update() +{ + std::lock_guard lock(m_mutex); + if (m_lua == nullptr || m_fatalError) + { + return; + } + + const uint64_t nowMs = GetNowMs(); + PruneHistoryLocked(nowMs); + PruneNamedStatesLocked(nowMs); + m_currentEvent = CurrentEvent{}; + RunDueScheduledCallbacks(nowMs); + if (m_fatalError) + { + return; + } + CallHandler("onRulesUpdate"); +} + +LuaRulesEngine::SwitchProcessResult LuaRulesEngine::ProcessSwitchState(int number, uint8_t state) +{ + std::lock_guard lock(m_mutex); + SwitchProcessResult result; + const uint8_t normalized = state == 0 ? 0 : 1; + const uint8_t old = GetState(m_switchStates, number); + m_switchStates[number] = normalized; + m_currentEvent = CurrentEvent{EventType::Switch, number, old, normalized}; + + if (m_lua != nullptr && !m_fatalError) + { + CallHandler("onSwitchChanged", number, normalized); + } + + if (normalized == 0 && m_suppressedSwitchOpen.erase(number) > 0) + { + result.forwardToCpu = false; + } + else if (m_suppressedSwitchOpen.find(number) != m_suppressedSwitchOpen.end()) + { + result.forwardToCpu = false; + } + + m_currentEvent = CurrentEvent{}; + return result; +} + +void LuaRulesEngine::OnLampState(int number, uint8_t state) +{ + std::lock_guard lock(m_mutex); + const uint8_t normalized = state == 0 ? 0 : 1; + const uint8_t old = GetState(m_lampStates, number); + m_lampStates[number] = normalized; + m_currentEvent = CurrentEvent{EventType::Lamp, number, old, normalized}; + if (m_lua != nullptr && !m_fatalError) + { + CallHandler("onLampChanged", number, normalized); + } + m_currentEvent = CurrentEvent{}; +} + +void LuaRulesEngine::OnCoilState(int number, uint8_t state) +{ + std::lock_guard lock(m_mutex); + const uint8_t normalized = state == 0 ? 0 : 1; + const uint8_t old = GetState(m_coilStates, number); + m_coilStates[number] = normalized; + m_currentEvent = CurrentEvent{EventType::Coil, number, old, normalized}; + if (m_lua != nullptr && !m_fatalError) + { + CallHandler("onCoilChanged", number, normalized); + } + m_currentEvent = CurrentEvent{}; +} + +void LuaRulesEngine::SetCurrentBall(uint8_t currentBall) +{ + std::lock_guard lock(m_mutex); + if (m_currentBall == currentBall) + { + return; + } + m_currentBall = currentBall; + if (m_lua != nullptr && !m_fatalError) + { + CallHandler("onBallChanged", currentBall); + } +} + +void LuaRulesEngine::SetCurrentPlayer(uint8_t currentPlayer) +{ + std::lock_guard lock(m_mutex); + if (m_currentPlayer == currentPlayer) + { + return; + } + m_currentPlayer = currentPlayer; + if (m_lua != nullptr && !m_fatalError) + { + CallHandler("onPlayerChanged", currentPlayer); + } +} + +void LuaRulesEngine::SetAttractMode(bool attractMode) +{ + std::lock_guard lock(m_mutex); + m_attractMode = attractMode; +} + +bool LuaRulesEngine::HasFatalError() const +{ + std::lock_guard lock(m_mutex); + return m_fatalError; +} + +const std::string& LuaRulesEngine::GetFatalError() const +{ + return m_fatalErrorMessage; +} + +uint8_t LuaRulesEngine::GetState(const std::unordered_map& states, int number) const +{ + const auto it = states.find(number); + return it == states.end() ? 0 : it->second; +} + +bool LuaRulesEngine::IsRising(EventType type, int number) const +{ + return m_currentEvent.type == type && m_currentEvent.number == number && m_currentEvent.oldValue == 0 && + m_currentEvent.newValue != 0; +} + +bool LuaRulesEngine::IsFalling(EventType type, int number) const +{ + return m_currentEvent.type == type && m_currentEvent.number == number && m_currentEvent.oldValue != 0 && + m_currentEvent.newValue == 0; +} + +bool LuaRulesEngine::SwitchGroupStateActive(const std::string& name) const +{ + const auto it = m_switchGroups.find(name); + if (it == m_switchGroups.end()) + { + return false; + } + + for (const uint16_t number : it->second) + { + if (GetState(m_switchStates, number) != 0) + { + return true; + } + } + return false; +} + +bool LuaRulesEngine::SwitchGroupEdge(const std::string& name, bool rising) const +{ + if (m_currentEvent.type != EventType::Switch) + { + return false; + } + + const auto it = m_switchGroups.find(name); + if (it == m_switchGroups.end()) + { + return false; + } + + if (std::find(it->second.begin(), it->second.end(), static_cast(m_currentEvent.number)) == it->second.end()) + { + return false; + } + return rising ? IsRising(EventType::Switch, m_currentEvent.number) : IsFalling(EventType::Switch, m_currentEvent.number); +} + +bool LuaRulesEngine::HistoryContains(uint16_t id, uint32_t windowMs, uint64_t nowMs) const +{ + for (auto it = m_history.rbegin(); it != m_history.rend(); ++it) + { + if (it->player != m_currentPlayer || it->id != id) + { + continue; + } + if (windowMs == 0 || nowMs - it->timestampMs <= windowMs) + { + return true; + } + } + return false; +} + +bool LuaRulesEngine::SequenceOccurred(const std::vector& ids, uint32_t windowMs, uint64_t nowMs) const +{ + if (ids.empty()) + { + return false; + } + + size_t next = ids.size(); + uint64_t newest = 0; + uint64_t oldest = 0; + for (auto it = m_history.rbegin(); it != m_history.rend(); ++it) + { + if (it->player != m_currentPlayer) + { + continue; + } + if (it->id != ids[next - 1]) + { + continue; + } + if (newest == 0) + { + newest = it->timestampMs; + } + oldest = it->timestampMs; + next--; + if (next == 0) + { + return windowMs == 0 || (nowMs - newest <= windowMs && newest - oldest <= windowMs); + } + } + return false; +} + +bool LuaRulesEngine::NamedStateActive(const std::string& name, uint64_t nowMs) const +{ + const auto it = m_namedStates.find(name); + return it != m_namedStates.end() && (it->second == 0 || it->second > nowMs); +} + +bool LuaRulesEngine::StartOnceEvery(const std::string& name, uint32_t durationMs, uint64_t nowMs) +{ + PruneNamedStatesLocked(nowMs); + if (NamedStateActive(name, nowMs)) + { + return false; + } + + m_namedStates[name] = durationMs == 0 ? 0 : nowMs + durationMs; + return true; +} + +uint64_t LuaRulesEngine::GetNowMs() const +{ + return static_cast( + std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()) + .count()); +} + +void LuaRulesEngine::PruneHistoryLocked(uint64_t nowMs) +{ + while (!m_history.empty() && nowMs - m_history.front().timestampMs > kHistoryRetentionMs) + { + m_history.pop_front(); + } +} + +void LuaRulesEngine::PruneNamedStatesLocked(uint64_t nowMs) +{ + for (auto it = m_namedStates.begin(); it != m_namedStates.end();) + { + if (it->second != 0 && it->second <= nowMs) + { + it = m_namedStates.erase(it); + } + else + { + ++it; + } + } +} + +void LuaRulesEngine::RecordTriggerLocked(uint16_t id, uint64_t nowMs) +{ + m_history.push_back(HistoryEntry{id, m_currentPlayer, nowMs}); + PruneHistoryLocked(nowMs); +} + +void LuaRulesEngine::SetFatalError(const std::string& error) +{ + m_fatalError = true; + m_fatalErrorMessage = error.empty() ? "Lua rules runtime error" : error; +} + +LuaRulesEngine* LuaRulesEngine::FromLua(lua_State* L) +{ + return static_cast(lua_touserdata(L, lua_upvalueindex(1))); +} + +int LuaRulesEngine::LuaSwitchState(lua_State* L) +{ + auto* engine = FromLua(L); + lua_pushboolean(L, engine->GetState(engine->m_switchStates, static_cast(luaL_checkinteger(L, 1))) != 0); + return 1; +} + +int LuaRulesEngine::LuaLampState(lua_State* L) +{ + auto* engine = FromLua(L); + lua_pushboolean(L, engine->GetState(engine->m_lampStates, static_cast(luaL_checkinteger(L, 1))) != 0); + return 1; +} + +int LuaRulesEngine::LuaCoilState(lua_State* L) +{ + auto* engine = FromLua(L); + lua_pushboolean(L, engine->GetState(engine->m_coilStates, static_cast(luaL_checkinteger(L, 1))) != 0); + return 1; +} + +int LuaRulesEngine::LuaCurrentBall(lua_State* L) +{ + lua_pushinteger(L, FromLua(L)->m_currentBall); + return 1; +} + +int LuaRulesEngine::LuaCurrentPlayer(lua_State* L) +{ + lua_pushinteger(L, FromLua(L)->m_currentPlayer); + return 1; +} + +int LuaRulesEngine::LuaAttractMode(lua_State* L) +{ + lua_pushboolean(L, FromLua(L)->m_attractMode); + return 1; +} + +int LuaRulesEngine::LuaSwitchGroupState(lua_State* L) +{ + lua_pushboolean(L, FromLua(L)->SwitchGroupStateActive(luaL_checkstring(L, 1))); + return 1; +} + +int LuaRulesEngine::LuaSwitchGroupClosing(lua_State* L) +{ + lua_pushboolean(L, FromLua(L)->SwitchGroupEdge(luaL_checkstring(L, 1), true)); + return 1; +} + +int LuaRulesEngine::LuaSwitchGroupOpening(lua_State* L) +{ + lua_pushboolean(L, FromLua(L)->SwitchGroupEdge(luaL_checkstring(L, 1), false)); + return 1; +} + +int LuaRulesEngine::LuaSetState(lua_State* L) +{ + auto* engine = FromLua(L); + const std::string name = luaL_checkstring(L, 1); + const uint32_t durationMs = lua_gettop(L) >= 2 ? static_cast(luaL_checkinteger(L, 2)) : 0; + engine->m_namedStates[name] = durationMs == 0 ? 0 : engine->GetNowMs() + durationMs; + return 0; +} + +int LuaRulesEngine::LuaClearState(lua_State* L) +{ + FromLua(L)->m_namedStates.erase(luaL_checkstring(L, 1)); + return 0; +} + +int LuaRulesEngine::LuaStateActive(lua_State* L) +{ + auto* engine = FromLua(L); + lua_pushboolean(L, engine->NamedStateActive(luaL_checkstring(L, 1), engine->GetNowMs())); + return 1; +} + +int LuaRulesEngine::LuaTriggerHistory(lua_State* L) +{ + auto* engine = FromLua(L); + const uint16_t id = static_cast(luaL_checkinteger(L, 1)); + const uint32_t windowMs = lua_gettop(L) >= 2 ? static_cast(luaL_checkinteger(L, 2)) : 0; + lua_pushboolean(L, engine->HistoryContains(id, windowMs, engine->GetNowMs())); + return 1; +} + +int LuaRulesEngine::LuaTriggerSequence(lua_State* L) +{ + auto* engine = FromLua(L); + const int argc = lua_gettop(L); + const uint32_t windowMs = static_cast(luaL_checkinteger(L, 1)); + std::vector ids; + ids.reserve(argc > 1 ? static_cast(argc - 1) : 0); + for (int i = 2; i <= argc; ++i) + { + ids.push_back(static_cast(luaL_checkinteger(L, i))); + } + lua_pushboolean(L, engine->SequenceOccurred(ids, windowMs, engine->GetNowMs())); + return 1; +} + +int LuaRulesEngine::LuaOnlyOnceEvery(lua_State* L) +{ + auto* engine = FromLua(L); + const std::string name = luaL_checkstring(L, 1); + const uint32_t durationMs = lua_gettop(L) >= 2 ? static_cast(luaL_checkinteger(L, 2)) : 0; + lua_pushboolean(L, engine->StartOnceEvery(name, durationMs, engine->GetNowMs())); + return 1; +} + +int LuaRulesEngine::LuaAfter(lua_State* L) +{ + auto* engine = FromLua(L); + const lua_Integer delayArg = luaL_checkinteger(L, 1); + luaL_checktype(L, 2, LUA_TFUNCTION); + const uint64_t delayMs = delayArg <= 0 ? 0 : static_cast(delayArg); + lua_pushvalue(L, 2); + const int callbackRef = luaL_ref(L, LUA_REGISTRYINDEX); + engine->m_scheduledCallbacks.push_back( + ScheduledCallback{engine->GetNowMs() + delayMs, engine->m_nextScheduledSequence++, callbackRef}); + return 0; +} + +int LuaRulesEngine::LuaPupTrigger(lua_State* L) +{ + auto* engine = FromLua(L); + const char* source = luaL_checkstring(L, 1); + const uint16_t id = static_cast(luaL_checkinteger(L, 2)); + const uint8_t value = lua_gettop(L) >= 3 ? static_cast(luaL_checkinteger(L, 3)) : 1; + if (engine->m_triggerCallback) + { + engine->m_triggerCallback(source != nullptr && source[0] != '\0' ? source[0] : 'P', id, value); + } + engine->RecordTriggerLocked(id, engine->GetNowMs()); + return 0; +} + +int LuaRulesEngine::LuaSpeech(lua_State* L) +{ + auto* engine = FromLua(L); + const char* text = luaL_checkstring(L, 1); + if (engine->m_speechCallback && text != nullptr && text[0] != '\0') + { + engine->m_speechCallback(text); + } + return 0; +} + +int LuaRulesEngine::LuaEffectTrigger(lua_State* L) +{ + auto* engine = FromLua(L); + uint16_t id = 0; + if (lua_type(L, 1) == LUA_TSTRING) + { + id = HashNamedTriggerId(lua_tostring(L, 1)); + } + else + { + id = static_cast(luaL_checkinteger(L, 1)); + } + const uint8_t value = lua_gettop(L) >= 2 ? static_cast(luaL_checkinteger(L, 2)) : 1; + if (engine->m_triggerCallback) + { + engine->m_triggerCallback(kBoardEffectTriggerSource, id, value); + } + engine->RecordTriggerLocked(id, engine->GetNowMs()); + return 0; +} + +int LuaRulesEngine::LuaSuppressSwitch(lua_State* L) +{ + auto* engine = FromLua(L); + const int number = static_cast(luaL_checkinteger(L, 1)); + if (engine->m_currentEvent.type == EventType::Switch && engine->m_currentEvent.number == number && + engine->m_currentEvent.newValue != 0) + { + engine->m_suppressedSwitchOpen.insert(number); + } + return 0; +} + +int LuaRulesEngine::LuaSendSwitchToCpu(lua_State* L) +{ + auto* engine = FromLua(L); + if (engine->m_actionCallback) + { + engine->m_actionCallback(RulesAction{RulesActionType::SendSwitchToCpu, + static_cast(luaL_checkinteger(L, 1)), + luaL_checkinteger(L, 2) == 0 ? static_cast(0) : static_cast(1), + 0, + 0, + 0}); + } + return 0; +} + +int LuaRulesEngine::LuaPulseCoil(lua_State* L) +{ + auto* engine = FromLua(L); + if (engine->m_actionCallback) + { + engine->m_actionCallback(RulesAction{RulesActionType::PulseCoil, + static_cast(luaL_checkinteger(L, 1)), + 0, + static_cast(luaL_checkinteger(L, 2)), + 0, + 0}); + } + return 0; +} + +int LuaRulesEngine::LuaBlinkLamp(lua_State* L) +{ + auto* engine = FromLua(L); + if (engine->m_actionCallback) + { + engine->m_actionCallback(RulesAction{RulesActionType::StartBlinkLamp, + static_cast(luaL_checkinteger(L, 1)), + 0, + 0, + static_cast(luaL_checkinteger(L, 2)), + static_cast(luaL_checkinteger(L, 3))}); + } + return 0; +} + +int LuaRulesEngine::LuaStopBlinkLamp(lua_State* L) +{ + auto* engine = FromLua(L); + if (engine->m_actionCallback) + { + engine->m_actionCallback( + RulesAction{RulesActionType::StopBlinkLamp, static_cast(luaL_checkinteger(L, 1)), 0, 0, 0, 0}); + } + return 0; +} diff --git a/src/LuaRulesEngine.h b/src/LuaRulesEngine.h new file mode 100644 index 0000000..21da01f --- /dev/null +++ b/src/LuaRulesEngine.h @@ -0,0 +1,161 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "RulesAction.h" + +struct lua_State; + +class LuaRulesEngine +{ + public: + using TriggerCallback = std::function; + using SpeechCallback = std::function; + using ActionCallback = std::function; + + struct SwitchProcessResult + { + bool forwardToCpu = true; + }; + + LuaRulesEngine(); + ~LuaRulesEngine(); + + LuaRulesEngine(const LuaRulesEngine&) = delete; + LuaRulesEngine& operator=(const LuaRulesEngine&) = delete; + + void SetDebug(bool debug); + void SetTriggerCallback(TriggerCallback callback); + void SetSpeechCallback(SpeechCallback callback); + void SetActionCallback(ActionCallback callback); + void SetSwitchGroups(const std::unordered_map>& switchGroups); + bool LoadScript(const char* path, std::string& error); + bool LoadScripts(const std::vector& paths, std::string& error); + void Update(); + + SwitchProcessResult ProcessSwitchState(int number, uint8_t state); + void OnLampState(int number, uint8_t state); + void OnCoilState(int number, uint8_t state); + void SetCurrentBall(uint8_t currentBall); + void SetCurrentPlayer(uint8_t currentPlayer); + void SetAttractMode(bool attractMode); + + bool HasFatalError() const; + const std::string& GetFatalError() const; + + private: + enum class EventType + { + None, + Switch, + Lamp, + Coil + }; + + struct CurrentEvent + { + EventType type = EventType::None; + int number = 0; + uint8_t oldValue = 0; + uint8_t newValue = 0; + }; + + struct HistoryEntry + { + uint16_t id = 0; + uint8_t player = 0; + uint64_t timestampMs = 0; + }; + + struct ScheduledCallback + { + uint64_t dueMs = 0; + uint64_t sequence = 0; + int ref = 0; + }; + + uint8_t GetState(const std::unordered_map& states, int number) const; + bool IsRising(EventType type, int number) const; + bool IsFalling(EventType type, int number) const; + bool SwitchGroupStateActive(const std::string& name) const; + bool SwitchGroupEdge(const std::string& name, bool rising) const; + bool HistoryContains(uint16_t id, uint32_t windowMs, uint64_t nowMs) const; + bool SequenceOccurred(const std::vector& ids, uint32_t windowMs, uint64_t nowMs) const; + bool NamedStateActive(const std::string& name, uint64_t nowMs) const; + bool StartOnceEvery(const std::string& name, uint32_t durationMs, uint64_t nowMs); + uint64_t GetNowMs() const; + void PruneHistoryLocked(uint64_t nowMs); + void PruneNamedStatesLocked(uint64_t nowMs); + void RecordTriggerLocked(uint16_t id, uint64_t nowMs); + + bool InitializeLua(std::string& error); + void RegisterApi(); + void ClearRegisteredHandlers(); + void ClearScheduledCallbacks(); + void ClearPpucHandlers(); + void CapturePpucHandlers(); + bool LoadScriptIntoState(const char* path, std::string& error); + bool CallHandler(const char* name); + bool CallHandler(const char* name, int arg1); + bool CallHandler(const char* name, int arg1, int arg2); + bool CallRegisteredHandler(int handlerRef, const char* name, const std::vector& args); + bool CallScheduledCallback(int callbackRef); + void RunDueScheduledCallbacks(uint64_t nowMs); + void SetFatalError(const std::string& error); + + static LuaRulesEngine* FromLua(lua_State* L); + static int LuaSwitchState(lua_State* L); + static int LuaLampState(lua_State* L); + static int LuaCoilState(lua_State* L); + static int LuaCurrentBall(lua_State* L); + static int LuaCurrentPlayer(lua_State* L); + static int LuaAttractMode(lua_State* L); + static int LuaSwitchGroupState(lua_State* L); + static int LuaSwitchGroupClosing(lua_State* L); + static int LuaSwitchGroupOpening(lua_State* L); + static int LuaSetState(lua_State* L); + static int LuaClearState(lua_State* L); + static int LuaStateActive(lua_State* L); + static int LuaTriggerHistory(lua_State* L); + static int LuaTriggerSequence(lua_State* L); + static int LuaOnlyOnceEvery(lua_State* L); + static int LuaAfter(lua_State* L); + static int LuaPupTrigger(lua_State* L); + static int LuaSpeech(lua_State* L); + static int LuaEffectTrigger(lua_State* L); + static int LuaSuppressSwitch(lua_State* L); + static int LuaSendSwitchToCpu(lua_State* L); + static int LuaPulseCoil(lua_State* L); + static int LuaBlinkLamp(lua_State* L); + static int LuaStopBlinkLamp(lua_State* L); + + lua_State* m_lua = nullptr; + std::unordered_map m_switchStates; + std::unordered_map m_lampStates; + std::unordered_map m_coilStates; + std::unordered_map m_namedStates; + std::unordered_map> m_switchGroups; + std::unordered_map> m_handlers; + std::unordered_set m_suppressedSwitchOpen; + std::deque m_history; + std::deque m_scheduledCallbacks; + CurrentEvent m_currentEvent; + TriggerCallback m_triggerCallback; + SpeechCallback m_speechCallback; + ActionCallback m_actionCallback; + uint8_t m_currentBall = 0; + uint8_t m_currentPlayer = 0; + bool m_attractMode = true; + bool m_debug = false; + bool m_fatalError = false; + std::string m_fatalErrorMessage; + uint64_t m_nextScheduledSequence = 0; + mutable std::mutex m_mutex; +}; diff --git a/src/PUPTriggerEngine.cpp b/src/PUPTriggerEngine.cpp deleted file mode 100644 index c8964b0..0000000 --- a/src/PUPTriggerEngine.cpp +++ /dev/null @@ -1,1353 +0,0 @@ -#include "PUPTriggerEngine.h" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "io-boards/Event.h" - -namespace -{ -constexpr uint32_t kHistoryRetentionMs = 60000; -constexpr char kSilentTriggerSource = 'S'; - -std::string Trim(const std::string& input) -{ - size_t start = 0; - while (start < input.size() && ::isspace(static_cast(input[start]))) - { - start++; - } - - size_t end = input.size(); - while (end > start && ::isspace(static_cast(input[end - 1]))) - { - end--; - } - - return input.substr(start, end - start); -} - -bool ParseUInt16Token(const std::string& token, uint16_t& out) -{ - if (token.empty()) - { - return false; - } - - int value = 0; - for (char c : token) - { - if (!::isdigit(static_cast(c))) - { - return false; - } - - value = (value * 10) + (c - '0'); - if (value > 65535) - { - return false; - } - } - - out = static_cast(value); - return true; -} - -bool ParseTriggerIdToken(const std::string& token, uint16_t& out) -{ - if (ParseUInt16Token(token, out)) - { - return true; - } - - if (token.empty()) - { - return false; - } - - for (char c : token) - { - const unsigned char uc = static_cast(c); - if (!(::isalnum(uc) || c == '_' || c == '-' || c == '.')) - { - return false; - } - } - - out = HashNamedTriggerId(token.c_str()); - return true; -} - -bool ParseUInt8Token(const std::string& token, uint8_t& out) -{ - if (token.empty()) - { - return false; - } - - int value = 0; - for (char c : token) - { - if (!::isdigit(static_cast(c))) - { - return false; - } - - value = (value * 10) + (c - '0'); - if (value > 255) - { - return false; - } - } - - out = static_cast(value); - return true; -} - -bool ParseUInt32Token(const std::string& token, uint32_t& out) -{ - if (token.empty()) - { - return false; - } - - uint64_t value = 0; - for (char c : token) - { - if (!::isdigit(static_cast(c))) - { - return false; - } - - value = (value * 10) + static_cast(c - '0'); - if (value > 4294967295ULL) - { - return false; - } - } - - out = static_cast(value); - return true; -} - -class TriggerExpressionParser -{ - public: - explicit TriggerExpressionParser(const std::string& input) : m_input(input), m_position(0) {} - - std::unique_ptr Parse() - { - auto root = ParseOr(); - if (!root) - { - return nullptr; - } - - SkipWhitespace(); - if (!IsEnd()) - { - m_error = "unexpected trailing token"; - return nullptr; - } - - return root; - } - - const std::string& Error() const { return m_error; } - - private: - std::unique_ptr ParseOr() - { - auto left = ParseAnd(); - if (!left) - { - return nullptr; - } - - while (true) - { - SkipWhitespace(); - if (!Match("||")) - { - break; - } - - auto right = ParseAnd(); - if (!right) - { - m_error = "expected expression after ||"; - return nullptr; - } - - auto node = std::make_unique(); - node->type = PUPTriggerEngine::ExprNodeType::Or; - node->left = std::move(left); - node->right = std::move(right); - left = std::move(node); - } - - return left; - } - - std::unique_ptr ParseAnd() - { - auto left = ParseUnary(); - if (!left) - { - return nullptr; - } - - while (true) - { - SkipWhitespace(); - if (!Match("&&")) - { - break; - } - - auto right = ParseUnary(); - if (!right) - { - m_error = "expected expression after &&"; - return nullptr; - } - - auto node = std::make_unique(); - node->type = PUPTriggerEngine::ExprNodeType::And; - node->left = std::move(left); - node->right = std::move(right); - left = std::move(node); - } - - return left; - } - - std::unique_ptr ParseUnary() - { - SkipWhitespace(); - if (Match("!")) - { - auto operand = ParseUnary(); - if (!operand) - { - m_error = "expected expression after !"; - return nullptr; - } - - auto node = std::make_unique(); - node->type = PUPTriggerEngine::ExprNodeType::Not; - node->left = std::move(operand); - return node; - } - - return ParsePrimary(); - } - - std::unique_ptr ParsePrimary() - { - SkipWhitespace(); - if (Match("(")) - { - auto expression = ParseOr(); - if (!expression) - { - return nullptr; - } - - SkipWhitespace(); - if (!Match(")")) - { - m_error = "missing closing )"; - return nullptr; - } - - return expression; - } - - std::string identifier = ParseIdentifier(); - if (identifier.empty()) - { - m_error = "expected expression"; - return nullptr; - } - - for (char& character : identifier) - { - character = static_cast(::tolower(character)); - } - - if (identifier == "true" || identifier == "false") - { - auto node = std::make_unique(); - node->type = PUPTriggerEngine::ExprNodeType::Literal; - node->literal = identifier == "true"; - return node; - } - - if (identifier == "attract") - { - SkipWhitespace(); - if (Match("(")) - { - SkipWhitespace(); - if (!Match(")")) - { - m_error = "missing ) in function 'attract'"; - return nullptr; - } - } - - auto node = std::make_unique(); - node->type = PUPTriggerEngine::ExprNodeType::AttractState; - return node; - } - - PUPTriggerEngine::ExprNodeType functionType; - if (identifier == "ball") - { - functionType = PUPTriggerEngine::ExprNodeType::BallState; - } - else if (identifier == "player") - { - functionType = PUPTriggerEngine::ExprNodeType::PlayerState; - } - else if (identifier == "history") - { - return ParseHistoryFunction(); - } - else if (identifier == "sequence") - { - return ParseSequenceFunction(); - } - else if (identifier == "switch") - { - functionType = PUPTriggerEngine::ExprNodeType::SwitchState; - } - else if (identifier == "lamp") - { - functionType = PUPTriggerEngine::ExprNodeType::LampState; - } - else if (identifier == "coil") - { - functionType = PUPTriggerEngine::ExprNodeType::CoilState; - } - else if (identifier == "switch_rising") - { - functionType = PUPTriggerEngine::ExprNodeType::SwitchRising; - } - else if (identifier == "switch_falling") - { - functionType = PUPTriggerEngine::ExprNodeType::SwitchFalling; - } - else if (identifier == "lamp_rising") - { - functionType = PUPTriggerEngine::ExprNodeType::LampRising; - } - else if (identifier == "lamp_falling") - { - functionType = PUPTriggerEngine::ExprNodeType::LampFalling; - } - else if (identifier == "coil_rising") - { - functionType = PUPTriggerEngine::ExprNodeType::CoilRising; - } - else if (identifier == "coil_falling") - { - functionType = PUPTriggerEngine::ExprNodeType::CoilFalling; - } - else - { - m_error = "unknown identifier '" + identifier + "'"; - return nullptr; - } - - SkipWhitespace(); - if (!Match("(")) - { - m_error = "missing ( after function '" + identifier + "'"; - return nullptr; - } - - int number = 0; - if (!ParseUnsignedNumber(number)) - { - m_error = "expected numeric argument in function '" + identifier + "'"; - return nullptr; - } - - SkipWhitespace(); - if (!Match(")")) - { - m_error = "missing ) in function '" + identifier + "'"; - return nullptr; - } - - auto node = std::make_unique(); - node->type = functionType; - node->number = number; - return node; - } - - std::unique_ptr ParseHistoryFunction() - { - SkipWhitespace(); - if (!Match("(")) - { - m_error = "missing ( after function 'history'"; - return nullptr; - } - - std::string triggerToken = ParseArgumentToken(); - uint16_t triggerId = 0; - if (!ParseTriggerIdToken(triggerToken, triggerId)) - { - m_error = "expected trigger id in function 'history'"; - return nullptr; - } - - uint32_t windowMs = 0; - SkipWhitespace(); - if (Match(",")) - { - int window = 0; - if (!ParseUnsignedNumber(window)) - { - m_error = "expected numeric window in function 'history'"; - return nullptr; - } - windowMs = static_cast(window); - SkipWhitespace(); - } - - if (!Match(")")) - { - m_error = "missing ) in function 'history'"; - return nullptr; - } - - auto node = std::make_unique(); - node->type = PUPTriggerEngine::ExprNodeType::HistoryState; - node->triggerIds.push_back(triggerId); - node->windowMs = windowMs; - return node; - } - - std::unique_ptr ParseSequenceFunction() - { - SkipWhitespace(); - if (!Match("(")) - { - m_error = "missing ( after function 'sequence'"; - return nullptr; - } - - int window = 0; - if (!ParseUnsignedNumber(window)) - { - m_error = "expected numeric window in function 'sequence'"; - return nullptr; - } - - std::vector triggerIds; - while (true) - { - SkipWhitespace(); - if (!Match(",")) - { - break; - } - - std::string triggerToken = ParseArgumentToken(); - uint16_t triggerId = 0; - if (!ParseTriggerIdToken(triggerToken, triggerId)) - { - m_error = "expected trigger id in function 'sequence'"; - return nullptr; - } - triggerIds.push_back(triggerId); - } - - if (triggerIds.size() < 2) - { - m_error = "function 'sequence' requires at least two trigger ids"; - return nullptr; - } - - SkipWhitespace(); - if (!Match(")")) - { - m_error = "missing ) in function 'sequence'"; - return nullptr; - } - - auto node = std::make_unique(); - node->type = PUPTriggerEngine::ExprNodeType::SequenceState; - node->windowMs = static_cast(window); - node->triggerIds = std::move(triggerIds); - return node; - } - - std::string ParseIdentifier() - { - SkipWhitespace(); - const size_t start = m_position; - while (!IsEnd()) - { - const char c = m_input[m_position]; - if (::isalnum(static_cast(c)) || c == '_') - { - m_position++; - } - else - { - break; - } - } - - if (m_position == start) - { - return ""; - } - - return m_input.substr(start, m_position - start); - } - - std::string ParseArgumentToken() - { - SkipWhitespace(); - const size_t start = m_position; - while (!IsEnd()) - { - const char c = m_input[m_position]; - const unsigned char uc = static_cast(c); - if (::isalnum(uc) || c == '_' || c == '-' || c == '.') - { - m_position++; - } - else - { - break; - } - } - - if (m_position == start) - { - return ""; - } - - return m_input.substr(start, m_position - start); - } - - bool ParseUnsignedNumber(int& out) - { - SkipWhitespace(); - if (IsEnd() || !::isdigit(static_cast(m_input[m_position]))) - { - return false; - } - - int value = 0; - while (!IsEnd() && ::isdigit(static_cast(m_input[m_position]))) - { - value = (value * 10) + (m_input[m_position] - '0'); - m_position++; - } - - out = value; - return true; - } - - bool Match(const char* token) - { - size_t idx = 0; - while (token[idx] != '\0') - { - if (m_position + idx >= m_input.size() || m_input[m_position + idx] != token[idx]) - { - return false; - } - idx++; - } - - m_position += idx; - return true; - } - - void SkipWhitespace() - { - while (!IsEnd() && ::isspace(static_cast(m_input[m_position]))) - { - m_position++; - } - } - - bool IsEnd() const { return m_position >= m_input.size(); } - - std::string m_input; - size_t m_position; - std::string m_error; -}; -} // namespace - -bool PUPTriggerEngine::LoadRules(const char* path, std::string& error) -{ - std::ifstream input(path); - if (!input.is_open()) - { - error = "Unable to open PUP trigger file"; - return false; - } - - std::vector loadedRules; - std::string line; - size_t lineNo = 0; - - while (std::getline(input, line)) - { - lineNo++; - const size_t comment = line.find('#'); - if (comment != std::string::npos) - { - line = line.substr(0, comment); - } - - line = Trim(line); - if (line.empty()) - { - continue; - } - - const size_t separator = line.find(':'); - if (separator == std::string::npos) - { - error = "Invalid PUP trigger line " + std::to_string(lineNo) + ": missing ':' separator."; - return false; - } - - const std::string triggerPart = Trim(line.substr(0, separator)); - const std::string expressionPart = Trim(line.substr(separator + 1)); - if (triggerPart.empty() || expressionPart.empty()) - { - error = "Invalid PUP trigger line " + std::to_string(lineNo) + ": missing trigger or expression."; - return false; - } - - std::istringstream triggerStream(triggerPart); - std::vector tokens; - std::string token; - while (triggerStream >> token) - { - tokens.push_back(token); - } - - if (tokens.size() < 2) - { - error = "Invalid PUP trigger line " + std::to_string(lineNo) + - ": expected ' [value] : '."; - return false; - } - - const std::string& sourceToken = tokens[0]; - const std::string& idToken = tokens[1]; - - if (sourceToken.size() != 1) - { - error = "Invalid PUP trigger line " + std::to_string(lineNo) + ": source must be a single character."; - return false; - } - - Rule rule; - rule.source = sourceToken[0]; - rule.line = lineNo; - - if (!ParseTriggerIdToken(idToken, rule.id)) - { - error = "Invalid PUP trigger line " + std::to_string(lineNo) + ": id must be uint16 or a named token."; - return false; - } - - bool hasValue = false; - for (size_t i = 2; i < tokens.size(); i++) - { - const std::string& option = tokens[i]; - if (option.rfind("cooldown=", 0) == 0) - { - const std::string msToken = option.substr(9); - if (!ParseUInt32Token(msToken, rule.cooldownMs)) - { - error = "Invalid PUP trigger line " + std::to_string(lineNo) + ": cooldown must be uint32 milliseconds."; - return false; - } - continue; - } - - if (option.rfind("delay=", 0) == 0) - { - const std::string msToken = option.substr(6); - if (!ParseUInt32Token(msToken, rule.delayMs)) - { - error = "Invalid PUP trigger line " + std::to_string(lineNo) + ": delay must be uint32 milliseconds."; - return false; - } - continue; - } - - if (option.rfind("set_player=", 0) == 0) - { - uint8_t player = 0; - if (!ParseUInt8Token(option.substr(11), player)) - { - error = "Invalid PUP trigger line " + std::to_string(lineNo) + ": set_player must be uint8."; - return false; - } - rule.setPlayer = player; - continue; - } - - if (option.rfind("clear_player_history=", 0) == 0) - { - uint8_t player = 0; - if (!ParseUInt8Token(option.substr(21), player)) - { - error = "Invalid PUP trigger line " + std::to_string(lineNo) + - ": clear_player_history must be uint8."; - return false; - } - rule.clearPlayerHistory = player; - continue; - } - - if (!hasValue) - { - if (!ParseUInt8Token(option, rule.value)) - { - error = "Invalid PUP trigger line " + std::to_string(lineNo) + ": value must be uint8."; - return false; - } - hasValue = true; - continue; - } - - error = "Invalid PUP trigger line " + std::to_string(lineNo) + ": unknown option '" + option + "'."; - return false; - } - - TriggerExpressionParser parser(expressionPart); - rule.expression = parser.Parse(); - if (!rule.expression) - { - error = "Invalid PUP trigger expression on line " + std::to_string(lineNo) + ": " + parser.Error(); - return false; - } - - rule.eventTriggered = UsesEventEdges(rule.expression.get()); - - loadedRules.push_back(std::move(rule)); - } - - std::lock_guard lock(m_mutex); - m_rules = std::move(loadedRules); - m_switchStates.clear(); - m_lampStates.clear(); - m_coilStates.clear(); - m_history.clear(); - m_currentBall = 0; - m_currentPlayer = 0; - return true; -} - -void PUPTriggerEngine::SetDebug(bool debug) -{ - std::lock_guard lock(m_mutex); - m_debug = debug; -} - -void PUPTriggerEngine::SetTriggerCallback(TriggerCallback callback) -{ - std::lock_guard lock(m_mutex); - m_triggerCallback = std::move(callback); -} - -void PUPTriggerEngine::SetAttractMode(bool attractMode) -{ - std::lock_guard lock(m_mutex); - if (attractMode && !m_attractMode) - { - m_history.clear(); - m_currentBall = 0; - m_currentPlayer = 0; - } - m_attractMode = attractMode; -} - -size_t PUPTriggerEngine::GetRuleCount() const -{ - std::lock_guard lock(m_mutex); - return m_rules.size(); -} - -void PUPTriggerEngine::Update() -{ - const uint64_t nowMs = GetNowMs(); - std::vector matched; - TriggerCallback triggerCallback; - bool debug = false; - { - std::lock_guard lock(m_mutex); - PruneHistoryLocked(nowMs); - CollectDueTriggers(nowMs, matched); - RecordMatchedTriggers(matched, nowMs); - triggerCallback = m_triggerCallback; - debug = m_debug; - } - - for (const auto& match : matched) - { - if (debug) - { - printf("PUP trigger matched (line=%zu player=%u ball=%u): source=%c id=%u value=%u\n", match.line, - match.player, match.ball, match.source, match.id, match.value); - } - - if (match.source != kSilentTriggerSource && triggerCallback) - { - triggerCallback(match.source, match.id, match.value); - } - } -} - -uint8_t PUPTriggerEngine::GetState(const std::unordered_map& states, int number) const -{ - const auto it = states.find(number); - return it == states.end() ? 0 : it->second; -} - -bool PUPTriggerEngine::EvaluateExpression(const ExprNode* node, const TriggerEvent& event) const -{ - if (!node) - { - return false; - } - - switch (node->type) - { - case ExprNodeType::Literal: - return node->literal; - case ExprNodeType::AttractState: - return m_attractMode; - case ExprNodeType::Not: - return !EvaluateExpression(node->left.get(), event); - case ExprNodeType::And: - return EvaluateExpression(node->left.get(), event) && EvaluateExpression(node->right.get(), event); - case ExprNodeType::Or: - return EvaluateExpression(node->left.get(), event) || EvaluateExpression(node->right.get(), event); - case ExprNodeType::BallState: - return m_currentBall == static_cast(node->number); - case ExprNodeType::PlayerState: - return m_currentPlayer == static_cast(node->number); - case ExprNodeType::HistoryState: - return !node->triggerIds.empty() && HistoryContains(node->triggerIds[0], node->windowMs, GetNowMs()); - case ExprNodeType::SequenceState: - return SequenceOccurred(node->triggerIds, node->windowMs, GetNowMs()); - case ExprNodeType::SwitchState: - return GetState(m_switchStates, node->number) != 0; - case ExprNodeType::LampState: - return GetState(m_lampStates, node->number) != 0; - case ExprNodeType::CoilState: - return GetState(m_coilStates, node->number) != 0; - case ExprNodeType::SwitchRising: - return event.type == EventType::Switch && event.number == node->number && event.oldValue == 0 && - event.newValue != 0; - case ExprNodeType::SwitchFalling: - return event.type == EventType::Switch && event.number == node->number && event.oldValue != 0 && - event.newValue == 0; - case ExprNodeType::LampRising: - return event.type == EventType::Lamp && event.number == node->number && event.oldValue == 0 && - event.newValue != 0; - case ExprNodeType::LampFalling: - return event.type == EventType::Lamp && event.number == node->number && event.oldValue != 0 && - event.newValue == 0; - case ExprNodeType::CoilRising: - return event.type == EventType::Coil && event.number == node->number && event.oldValue == 0 && - event.newValue != 0; - case ExprNodeType::CoilFalling: - return event.type == EventType::Coil && event.number == node->number && event.oldValue != 0 && - event.newValue == 0; - } - - return false; -} - -bool PUPTriggerEngine::HistoryContains(const uint16_t triggerId, const uint32_t windowMs, const uint64_t nowMs) const -{ - for (auto it = m_history.rbegin(); it != m_history.rend(); ++it) - { - if (windowMs > 0 && nowMs > it->timestampMs && nowMs - it->timestampMs > static_cast(windowMs)) - { - break; - } - if (m_currentPlayer != 0 && it->player != m_currentPlayer) - { - continue; - } - if (it->id == triggerId) - { - return true; - } - } - return false; -} - -bool PUPTriggerEngine::SequenceOccurred(const std::vector& triggerIds, const uint32_t windowMs, - const uint64_t nowMs) const -{ - if (triggerIds.empty()) - { - return false; - } - - size_t matchedCount = 0; - const uint64_t earliestAllowedMs = - windowMs == 0 || nowMs < static_cast(windowMs) ? 0 : nowMs - static_cast(windowMs); - - for (const auto& entry : m_history) - { - if (entry.timestampMs < earliestAllowedMs) - { - continue; - } - if (m_currentPlayer != 0 && entry.player != m_currentPlayer) - { - continue; - } - if (entry.id == triggerIds[matchedCount]) - { - matchedCount++; - if (matchedCount == triggerIds.size()) - { - return true; - } - } - } - - return false; -} - -bool PUPTriggerEngine::UsesEventEdges(const ExprNode* node) const -{ - if (!node) - { - return false; - } - - switch (node->type) - { - case ExprNodeType::SwitchRising: - case ExprNodeType::SwitchFalling: - case ExprNodeType::LampRising: - case ExprNodeType::LampFalling: - case ExprNodeType::CoilRising: - case ExprNodeType::CoilFalling: - return true; - default: - return UsesEventEdges(node->left.get()) || UsesEventEdges(node->right.get()); - } -} - -uint64_t PUPTriggerEngine::GetNowMs() const -{ - return static_cast( - std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()) - .count()); -} - -bool PUPTriggerEngine::CanTriggerNow(const Rule& rule, uint64_t nowMs) const -{ - return rule.cooldownMs == 0 || nowMs >= (rule.lastTriggeredMs + static_cast(rule.cooldownMs)); -} - -void PUPTriggerEngine::CollectDueTriggers(uint64_t nowMs, std::vector& matched) -{ - for (auto& rule : m_rules) - { - if (!rule.pending || nowMs < rule.pendingTriggerMs) - { - continue; - } - - if (!CanTriggerNow(rule, nowMs)) - { - continue; - } - - rule.lastTriggeredMs = nowMs; - rule.pending = false; - rule.pendingTriggerMs = 0; - matched.push_back({rule.source, rule.id, rule.value, rule.line, - rule.setPlayer != 0 ? rule.setPlayer : m_currentPlayer, m_currentBall}); - } -} - -void PUPTriggerEngine::RecordMatchedTriggers(const std::vector& matched, const uint64_t nowMs) -{ - for (const auto& match : matched) - { - m_history.push_back({match.source, match.id, match.value, match.player, nowMs}); - if (match.player != 0) - { - m_currentPlayer = match.player; - } - } -} - -void PUPTriggerEngine::PruneHistoryLocked(const uint64_t nowMs) -{ - while (!m_history.empty() && nowMs > m_history.front().timestampMs && - nowMs - m_history.front().timestampMs > static_cast(kHistoryRetentionMs)) - { - m_history.pop_front(); - } -} - -void PUPTriggerEngine::ClearPlayerHistoryLocked(const uint8_t player) -{ - if (player == 0) - { - return; - } - - auto it = m_history.begin(); - while (it != m_history.end()) - { - if (it->player == player) - { - it = m_history.erase(it); - } - else - { - ++it; - } - } -} - -void PUPTriggerEngine::HandleStateChange(EventType type, int number, uint8_t state, std::unordered_map& states) -{ - const uint64_t nowMs = GetNowMs(); - std::vector matched; - TriggerCallback triggerCallback; - bool debug = false; - { - std::lock_guard lock(m_mutex); - PruneHistoryLocked(nowMs); - - const uint8_t oldState = GetState(states, number); - states[number] = state; - - const TriggerEvent event = {type, number, oldState, state}; - for (auto& rule : m_rules) - { - const bool expressionMatched = EvaluateExpression(rule.expression.get(), event); - - if (rule.delayMs > 0) - { - if (rule.eventTriggered) - { - if (expressionMatched && !rule.pending) - { - rule.pending = true; - rule.pendingTriggerMs = nowMs + static_cast(rule.delayMs); - } - } - else - { - if (expressionMatched) - { - if (!rule.conditionActive) - { - rule.conditionActive = true; - if (!rule.pending) - { - rule.pending = true; - rule.pendingTriggerMs = nowMs + static_cast(rule.delayMs); - } - } - } - else - { - rule.conditionActive = false; - rule.pending = false; - rule.pendingTriggerMs = 0; - } - } - continue; - } - - if (!expressionMatched) - { - continue; - } - - if (!CanTriggerNow(rule, nowMs)) - { - continue; - } - - rule.lastTriggeredMs = nowMs; - const uint8_t matchedPlayer = rule.setPlayer != 0 ? rule.setPlayer : m_currentPlayer; - if (rule.clearPlayerHistory != 0) - { - ClearPlayerHistoryLocked(rule.clearPlayerHistory == 255 ? matchedPlayer : rule.clearPlayerHistory); - } - matched.push_back({rule.source, rule.id, rule.value, rule.line, matchedPlayer, m_currentBall}); - } - - CollectDueTriggers(nowMs, matched); - RecordMatchedTriggers(matched, nowMs); - - triggerCallback = m_triggerCallback; - debug = m_debug; - } - - for (const auto& match : matched) - { - if (debug) - { - printf("PUP trigger matched (line=%zu player=%u ball=%u): source=%c id=%u value=%u\n", match.line, - match.player, match.ball, match.source, match.id, match.value); - } - - if (match.source != kSilentTriggerSource && triggerCallback) - { - triggerCallback(match.source, match.id, match.value); - } - } -} - -void PUPTriggerEngine::HandleBallChange(uint8_t currentBall) -{ - const uint64_t nowMs = GetNowMs(); - std::vector matched; - TriggerCallback triggerCallback; - bool debug = false; - { - std::lock_guard lock(m_mutex); - PruneHistoryLocked(nowMs); - - const uint8_t oldBall = m_currentBall; - if (oldBall == currentBall) - { - return; - } - - m_currentBall = currentBall; - - const TriggerEvent event = {EventType::Ball, static_cast(currentBall), oldBall, currentBall}; - for (auto& rule : m_rules) - { - const bool expressionMatched = EvaluateExpression(rule.expression.get(), event); - - if (rule.delayMs > 0) - { - if (rule.eventTriggered) - { - if (expressionMatched && !rule.pending) - { - rule.pending = true; - rule.pendingTriggerMs = nowMs + static_cast(rule.delayMs); - } - } - else - { - if (expressionMatched) - { - if (!rule.conditionActive) - { - rule.conditionActive = true; - if (!rule.pending) - { - rule.pending = true; - rule.pendingTriggerMs = nowMs + static_cast(rule.delayMs); - } - } - } - else - { - rule.conditionActive = false; - rule.pending = false; - rule.pendingTriggerMs = 0; - } - } - continue; - } - - if (!expressionMatched) - { - continue; - } - - if (!CanTriggerNow(rule, nowMs)) - { - continue; - } - - rule.lastTriggeredMs = nowMs; - const uint8_t matchedPlayer = rule.setPlayer != 0 ? rule.setPlayer : m_currentPlayer; - if (rule.clearPlayerHistory != 0) - { - ClearPlayerHistoryLocked(rule.clearPlayerHistory == 255 ? matchedPlayer : rule.clearPlayerHistory); - } - matched.push_back({rule.source, rule.id, rule.value, rule.line, matchedPlayer, m_currentBall}); - } - - CollectDueTriggers(nowMs, matched); - RecordMatchedTriggers(matched, nowMs); - - triggerCallback = m_triggerCallback; - debug = m_debug; - } - - for (const auto& match : matched) - { - if (debug) - { - printf("PUP trigger matched (line=%zu player=%u ball=%u): source=%c id=%u value=%u\n", match.line, - match.player, match.ball, match.source, match.id, match.value); - } - - if (match.source != kSilentTriggerSource && triggerCallback) - { - triggerCallback(match.source, match.id, match.value); - } - } -} - -void PUPTriggerEngine::HandlePlayerChange(uint8_t currentPlayer) -{ - const uint64_t nowMs = GetNowMs(); - std::vector matched; - TriggerCallback triggerCallback; - bool debug = false; - { - std::lock_guard lock(m_mutex); - PruneHistoryLocked(nowMs); - - const uint8_t oldPlayer = m_currentPlayer; - if (oldPlayer == currentPlayer) - { - return; - } - - m_currentPlayer = currentPlayer; - - const TriggerEvent event = {EventType::Player, static_cast(currentPlayer), oldPlayer, currentPlayer}; - for (auto& rule : m_rules) - { - const bool expressionMatched = EvaluateExpression(rule.expression.get(), event); - - if (rule.delayMs > 0) - { - if (rule.eventTriggered) - { - if (expressionMatched && !rule.pending) - { - rule.pending = true; - rule.pendingTriggerMs = nowMs + static_cast(rule.delayMs); - } - } - else - { - if (expressionMatched) - { - if (!rule.conditionActive) - { - rule.conditionActive = true; - if (!rule.pending) - { - rule.pending = true; - rule.pendingTriggerMs = nowMs + static_cast(rule.delayMs); - } - } - } - else - { - rule.conditionActive = false; - rule.pending = false; - rule.pendingTriggerMs = 0; - } - } - continue; - } - - if (!expressionMatched) - { - continue; - } - - if (!CanTriggerNow(rule, nowMs)) - { - continue; - } - - rule.lastTriggeredMs = nowMs; - const uint8_t matchedPlayer = rule.setPlayer != 0 ? rule.setPlayer : m_currentPlayer; - if (rule.clearPlayerHistory != 0) - { - ClearPlayerHistoryLocked(rule.clearPlayerHistory == 255 ? matchedPlayer : rule.clearPlayerHistory); - } - matched.push_back({rule.source, rule.id, rule.value, rule.line, matchedPlayer, m_currentBall}); - } - - CollectDueTriggers(nowMs, matched); - RecordMatchedTriggers(matched, nowMs); - - triggerCallback = m_triggerCallback; - debug = m_debug; - } - - for (const auto& match : matched) - { - if (debug) - { - printf("PUP trigger matched (line=%zu player=%u ball=%u): source=%c id=%u value=%u\n", match.line, - match.player, match.ball, match.source, match.id, match.value); - } - - if (match.source != kSilentTriggerSource && triggerCallback) - { - triggerCallback(match.source, match.id, match.value); - } - } -} - -void PUPTriggerEngine::OnSwitchState(int number, uint8_t state) -{ - HandleStateChange(EventType::Switch, number, state == 0 ? 0 : 1, m_switchStates); -} - -void PUPTriggerEngine::OnLampState(int number, uint8_t state) -{ - HandleStateChange(EventType::Lamp, number, state == 0 ? 0 : 1, m_lampStates); -} - -void PUPTriggerEngine::OnCoilState(int number, uint8_t state) -{ - HandleStateChange(EventType::Coil, number, state == 0 ? 0 : 1, m_coilStates); -} - -void PUPTriggerEngine::SetCurrentBall(uint8_t currentBall) -{ - HandleBallChange(currentBall); -} - -void PUPTriggerEngine::SetCurrentPlayer(uint8_t currentPlayer) -{ - HandlePlayerChange(currentPlayer); -} diff --git a/src/PUPTriggerEngine.h b/src/PUPTriggerEngine.h deleted file mode 100644 index b0cebc7..0000000 --- a/src/PUPTriggerEngine.h +++ /dev/null @@ -1,146 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class PUPTriggerEngine -{ - public: - using TriggerCallback = std::function; - - void SetDebug(bool debug); - void SetTriggerCallback(TriggerCallback callback); - bool LoadRules(const char* path, std::string& error); - size_t GetRuleCount() const; - void Update(); - - void OnSwitchState(int number, uint8_t state); - void OnLampState(int number, uint8_t state); - void OnCoilState(int number, uint8_t state); - void SetCurrentBall(uint8_t currentBall); - void SetCurrentPlayer(uint8_t currentPlayer); - void SetAttractMode(bool attractMode); - - // Internal types are public to keep parser implementation simple in .cpp. - enum class EventType - { - Switch, - Lamp, - Coil, - Ball, - Player - }; - - struct TriggerEvent - { - EventType type; - int number; - uint8_t oldValue; - uint8_t newValue; - }; - - enum class ExprNodeType - { - Literal, - AttractState, - Not, - And, - Or, - BallState, - PlayerState, - HistoryState, - SequenceState, - SwitchState, - LampState, - CoilState, - SwitchRising, - SwitchFalling, - LampRising, - LampFalling, - CoilRising, - CoilFalling - }; - - struct ExprNode - { - ExprNodeType type; - int number = 0; - bool literal = false; - uint32_t windowMs = 0; - std::vector triggerIds; - std::unique_ptr left; - std::unique_ptr right; - }; - - struct Rule - { - char source = 'P'; - uint16_t id = 0; - uint8_t value = 1; - size_t line = 0; - uint32_t cooldownMs = 0; - uint32_t delayMs = 0; - uint64_t lastTriggeredMs = 0; - uint64_t pendingTriggerMs = 0; - bool pending = false; - bool conditionActive = false; - bool eventTriggered = false; - uint8_t setPlayer = 0; - uint8_t clearPlayerHistory = 0; - std::unique_ptr expression; - }; - - struct MatchedTrigger - { - char source = 'P'; - uint16_t id = 0; - uint8_t value = 1; - size_t line = 0; - uint8_t player = 0; - uint8_t ball = 0; - }; - - struct HistoryEntry - { - char source = 'P'; - uint16_t id = 0; - uint8_t value = 1; - uint8_t player = 0; - uint64_t timestampMs = 0; - }; - - private: - uint8_t GetState(const std::unordered_map& states, int number) const; - bool EvaluateExpression(const ExprNode* node, const TriggerEvent& event) const; - bool UsesEventEdges(const ExprNode* node) const; - bool HistoryContains(uint16_t triggerId, uint32_t windowMs, uint64_t nowMs) const; - bool SequenceOccurred(const std::vector& triggerIds, uint32_t windowMs, uint64_t nowMs) const; - uint64_t GetNowMs() const; - bool CanTriggerNow(const Rule& rule, uint64_t nowMs) const; - void CollectDueTriggers(uint64_t nowMs, std::vector& matched); - void RecordMatchedTriggers(const std::vector& matched, uint64_t nowMs); - void PruneHistoryLocked(uint64_t nowMs); - void ClearPlayerHistoryLocked(uint8_t player); - void HandleStateChange(EventType type, int number, uint8_t state, std::unordered_map& states); - void HandleBallChange(uint8_t currentBall); - void HandlePlayerChange(uint8_t currentPlayer); - - std::unordered_map m_switchStates; - std::unordered_map m_lampStates; - std::unordered_map m_coilStates; - std::vector m_rules; - std::deque m_history; - TriggerCallback m_triggerCallback; - uint8_t m_currentBall = 0; - uint8_t m_currentPlayer = 0; - bool m_attractMode = true; - bool m_debug = false; - mutable std::mutex m_mutex; -}; diff --git a/src/RulesAction.h b/src/RulesAction.h new file mode 100644 index 0000000..ec5cdc5 --- /dev/null +++ b/src/RulesAction.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +enum class RulesActionType +{ + SendSwitchToCpu, + PulseCoil, + StartBlinkLamp, + StopBlinkLamp +}; + +struct RulesAction +{ + RulesActionType type; + int number = 0; + uint8_t state = 0; + uint32_t durationMs = 0; + uint32_t onMs = 0; + uint32_t offMs = 0; +}; diff --git a/src/SpeechCliSupport.cpp b/src/SpeechCliSupport.cpp index 0dcff12..380b570 100644 --- a/src/SpeechCliSupport.cpp +++ b/src/SpeechCliSupport.cpp @@ -145,7 +145,6 @@ bool ParseSpeechCliOptions(const char* backendArg, bool ValidateSpeechAudioUsage(bool noSound, bool speechEnabled, bool greetingEnabled, - const char* speechFile, std::string* errorMessage) { if (errorMessage != nullptr) @@ -153,16 +152,6 @@ bool ValidateSpeechAudioUsage(bool noSound, errorMessage->clear(); } - if (speechFile != nullptr && speechFile[0] != '\0' && noSound) - { - if (errorMessage != nullptr) - { - *errorMessage = - "--speech-file requires audio output and cannot be used with --no-sound"; - } - return false; - } - if (noSound && (speechEnabled || greetingEnabled)) { if (errorMessage != nullptr) diff --git a/src/SpeechCliSupport.h b/src/SpeechCliSupport.h index a67ace0..9dd2690 100644 --- a/src/SpeechCliSupport.h +++ b/src/SpeechCliSupport.h @@ -18,7 +18,6 @@ bool ParseSpeechCliOptions(const char* backendArg, bool ValidateSpeechAudioUsage(bool noSound, bool speechEnabled, bool greetingEnabled, - const char* speechFile, std::string* errorMessage); bool CreateConfiguredSpeechService(AudioOutput& audioOutput, diff --git a/src/SpeechTriggerMap.cpp b/src/SpeechTriggerMap.cpp deleted file mode 100644 index 89f4c52..0000000 --- a/src/SpeechTriggerMap.cpp +++ /dev/null @@ -1,107 +0,0 @@ -#include "SpeechTriggerMap.h" - -#include -#include - -namespace -{ -std::string Trim(const std::string& input) -{ - const size_t first = input.find_first_not_of(" \t\r\n"); - if (first == std::string::npos) - { - return {}; - } - - const size_t last = input.find_last_not_of(" \t\r\n"); - return input.substr(first, last - first + 1); -} - -bool ParseUInt16(const std::string& token, uint16_t& value) -{ - try - { - const unsigned long parsed = std::stoul(token, nullptr, 10); - if (parsed > 0xFFFFul) - { - return false; - } - value = static_cast(parsed); - return true; - } - catch (...) - { - return false; - } -} -} // namespace - -bool SpeechTriggerMap::Load(const char* path, std::string& error) -{ - std::ifstream input(path); - if (!input.is_open()) - { - error = "Unable to open speech trigger file"; - return false; - } - - std::unordered_map loaded; - std::string line; - size_t lineNo = 0; - while (std::getline(input, line)) - { - lineNo++; - const size_t comment = line.find('#'); - if (comment != std::string::npos) - { - line = line.substr(0, comment); - } - - line = Trim(line); - if (line.empty()) - { - continue; - } - - const size_t separator = line.find(':'); - if (separator == std::string::npos) - { - error = "Invalid speech trigger line " + std::to_string(lineNo) + - ": missing ':' separator."; - return false; - } - - const std::string idPart = Trim(line.substr(0, separator)); - const std::string textPart = Trim(line.substr(separator + 1)); - if (idPart.empty() || textPart.empty()) - { - error = "Invalid speech trigger line " + std::to_string(lineNo) + - ": missing id or text."; - return false; - } - - uint16_t id = 0; - if (!ParseUInt16(idPart, id)) - { - error = "Invalid speech trigger line " + std::to_string(lineNo) + - ": id must be uint16."; - return false; - } - - loaded[id] = textPart; - } - - m_entries = std::move(loaded); - return true; -} - -const std::string* SpeechTriggerMap::Find(uint16_t id) const -{ - const auto it = m_entries.find(id); - return it == m_entries.end() ? nullptr : &it->second; -} - -size_t SpeechTriggerMap::GetEntryCount() const -{ - return m_entries.size(); -} diff --git a/src/SpeechTriggerMap.h b/src/SpeechTriggerMap.h deleted file mode 100644 index 72b3b1d..0000000 --- a/src/SpeechTriggerMap.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include -#include -#include - -class SpeechTriggerMap -{ -public: - bool Load(const char* path, std::string& error); - const std::string* Find(uint16_t id) const; - size_t GetEntryCount() const; - -private: - std::unordered_map m_entries; -}; diff --git a/src/menu.cpp b/src/menu.cpp index d619ef0..6718fe3 100644 --- a/src/menu.cpp +++ b/src/menu.cpp @@ -1109,8 +1109,7 @@ int main(int argc, char* argv[]) } std::string speechError; - if (!ValidateSpeechAudioUsage(optNoSound, optSpeech, optGreeting, nullptr, - &speechError)) + if (!ValidateSpeechAudioUsage(optNoSound, optSpeech, optGreeting, &speechError)) { fprintf(stderr, "%s\n", speechError.c_str()); return 1; diff --git a/src/ppuc.cpp b/src/ppuc.cpp index b4a123c..1ab5735 100644 --- a/src/ppuc.cpp +++ b/src/ppuc.cpp @@ -44,13 +44,12 @@ #include "SDLDMD/SDLDMD.h" #endif #include "AudioOutput.h" -#include "PUPTriggerEngine.h" +#include "LuaRulesEngine.h" #include "SDL3/SDL.h" #include "SDL3/SDL_filesystem.h" #include "SDL3_image/SDL_image.h" #include "SpeechCliSupport.h" #include "SpeechService.h" -#include "SpeechTriggerMap.h" #include "cargs.h" #include "io-boards/Event.h" #include "io-boards/PPUCPlatforms.h" @@ -74,12 +73,10 @@ constexpr uint32_t kBallSearchCoilPulseMs = 200; DMDUtil::DMD* pDmd; PPUC* ppuc; -std::unique_ptr pPUPTriggerEngine; -std::unique_ptr pSpeechTriggerMap; +std::unique_ptr pLuaRulesEngine; std::unique_ptr pAudioOutput; std::unique_ptr pSpeechService; -constexpr char kSpeechTriggerSource = 'O'; constexpr char kBoardEffectTriggerSource = 'F'; enum class PinmameMapEncoding @@ -307,7 +304,6 @@ bool opt_no_serial = false; bool opt_no_sound = false; bool opt_speech = false; bool opt_greeting = false; -const char* opt_speech_file = NULL; const char* opt_music_files = NULL; uint32_t opt_music_gap_ms = 2000; const char* opt_speech_backend = "auto"; @@ -384,6 +380,150 @@ static void PrintFlushedLogLine(const char* prefix, const char* message) fflush(stdout); } +static void SendSwitchToCpu(int number, uint8_t state) +{ + const int switchNumber = (number < 241) ? number : 240 - number; + PinmameSetSwitch(switchNumber, state == 0 ? 0 : 1); +} + +struct InterceptorOutputOverrides +{ + struct CoilPulse + { + std::chrono::steady_clock::time_point until{}; + }; + + struct LampBlink + { + uint32_t onMs = 250; + uint32_t offMs = 250; + bool outputOn = false; + std::chrono::steady_clock::time_point nextToggle{}; + }; + + std::unordered_map pinmameCoils; + std::unordered_map pinmameLamps; + std::unordered_map coilPulses; + std::unordered_map lampBlinks; + + void ApplyPinmameCoil(PPUC* controller, int number, uint8_t state) + { + pinmameCoils[number] = state == 0 ? 0 : 1; + const auto now = std::chrono::steady_clock::now(); + const auto pulseIt = coilPulses.find(number); + if (pulseIt != coilPulses.end() && now < pulseIt->second.until) + { + controller->SetSolenoidState(number, 1); + return; + } + controller->SetSolenoidState(number, state == 0 ? 0 : 1); + } + + void ApplyPinmameLamp(PPUC* controller, int number, uint8_t state) + { + pinmameLamps[number] = state == 0 ? 0 : 1; + if (lampBlinks.find(number) != lampBlinks.end()) + { + return; + } + controller->SetLampState(number, state == 0 ? 0 : 1); + } + + void PulseCoil(PPUC* controller, int number, uint32_t durationMs) + { + const auto now = std::chrono::steady_clock::now(); + auto& pulse = coilPulses[number]; + const auto until = now + std::chrono::milliseconds(durationMs == 0 ? 1 : durationMs); + if (until > pulse.until) + { + pulse.until = until; + } + controller->SetSolenoidState(number, 1); + } + + void StartBlinkLamp(PPUC* controller, int number, uint32_t onMs, uint32_t offMs) + { + const uint32_t normalizedOnMs = onMs == 0 ? 250 : onMs; + const uint32_t normalizedOffMs = offMs == 0 ? 250 : offMs; + const auto existing = lampBlinks.find(number); + if (existing != lampBlinks.end()) + { + existing->second.onMs = normalizedOnMs; + existing->second.offMs = normalizedOffMs; + return; + } + + auto& blink = lampBlinks[number]; + blink.onMs = normalizedOnMs; + blink.offMs = normalizedOffMs; + blink.outputOn = true; + blink.nextToggle = std::chrono::steady_clock::now() + std::chrono::milliseconds(blink.onMs); + controller->SetLampState(number, 1); + } + + void StopBlinkLamp(PPUC* controller, int number) + { + lampBlinks.erase(number); + const uint8_t restore = pinmameLamps.count(number) == 0 ? 0 : pinmameLamps[number]; + controller->SetLampState(number, restore); + } + + void Service(PPUC* controller) + { + const auto now = std::chrono::steady_clock::now(); + + auto coilIt = coilPulses.begin(); + while (coilIt != coilPulses.end()) + { + if (now >= coilIt->second.until) + { + const int number = coilIt->first; + coilIt = coilPulses.erase(coilIt); + const uint8_t restore = pinmameCoils.count(number) == 0 ? 0 : pinmameCoils[number]; + controller->SetSolenoidState(number, restore); + } + else + { + ++coilIt; + } + } + + for (auto& entry : lampBlinks) + { + LampBlink& blink = entry.second; + if (now < blink.nextToggle) + { + continue; + } + + blink.outputOn = !blink.outputOn; + blink.nextToggle = now + std::chrono::milliseconds(blink.outputOn ? blink.onMs : blink.offMs); + controller->SetLampState(entry.first, blink.outputOn ? 1 : 0); + } + } + + void HandleAction(PPUC* controller, const RulesAction& action) + { + switch (action.type) + { + case RulesActionType::SendSwitchToCpu: + SendSwitchToCpu(action.number, action.state); + break; + case RulesActionType::PulseCoil: + PulseCoil(controller, action.number, action.durationMs); + break; + case RulesActionType::StartBlinkLamp: + StartBlinkLamp(controller, action.number, action.onMs, action.offMs); + break; + case RulesActionType::StopBlinkLamp: + StopBlinkLamp(controller, action.number); + break; + } + } +}; + +InterceptorOutputOverrides g_interceptorOutputs; + static uint64_t CurrentUnixMs() { return static_cast( @@ -1130,6 +1270,59 @@ static const char* DuplicateOptionalIniString(const std::string& value) return DuplicateIniString(value); } +static bool CollectRulesScripts(const char* pathArg, std::vector& scripts, std::string& error) +{ + scripts.clear(); + error.clear(); + + if (!HasOptionValue(pathArg)) + { + error = "Rules path is empty"; + return false; + } + + std::error_code ec; + const std::filesystem::path rulesPath(pathArg); + if (std::filesystem::is_regular_file(rulesPath, ec)) + { + scripts.push_back(rulesPath.string()); + return true; + } + + if (std::filesystem::is_directory(rulesPath, ec)) + { + for (const std::filesystem::directory_entry& entry : std::filesystem::directory_iterator(rulesPath, ec)) + { + if (ec) + { + error = "Unable to read Lua rules directory"; + return false; + } + if (!entry.is_regular_file(ec)) + { + ec.clear(); + continue; + } + const std::filesystem::path filePath = entry.path(); + if (filePath.extension() == ".lua") + { + scripts.push_back(filePath.string()); + } + } + + std::sort(scripts.begin(), scripts.end()); + if (scripts.empty()) + { + error = "Lua rules directory contains no .lua files"; + return false; + } + return true; + } + + error = "Rules path is not a file or directory"; + return false; +} + static const char* kAnsiStrikeOn = "\033[9m"; static const char* kAnsiStrikeOff = "\033[0m"; @@ -2170,10 +2363,6 @@ static struct cag_option options[] = { .access_name = "greeting", .value_name = NULL, .description = "Speak a startup greeting for speech debugging (optional)"}, - {.identifier = '9', - .access_name = "speech-file", - .value_name = "VALUE", - .description = "Path to speech trigger text file (optional)"}, {.identifier = 'o', .access_name = "music-files", .value_name = "VALUE", @@ -2217,9 +2406,9 @@ static struct cag_option options[] = { .value_name = "VALUE", .description = "Enable PUP videos (optional)"}, {.identifier = 'y', - .access_name = "pup-triggers", + .access_name = "rules", .value_name = "VALUE", - .description = "Path to PUP trigger rules file (optional)"}, + .description = "Path to Lua rules file (optional)"}, {.identifier = 'i', .access_letters = "i", .access_name = "console-display", @@ -2615,7 +2804,22 @@ void PINMAMECALLBACK OnSolenoidUpdated(PinmameSolenoidState* p_solenoidState, co printf("OnSolenoidUpdated: solenoid=%d, state=%d\n", p_solenoidState->solNo, coilState); } - ppuc->SetSolenoidState(p_solenoidState->solNo, coilState); + g_interceptorOutputs.ApplyPinmameCoil(ppuc, p_solenoidState->solNo, coilState); + + for (const PPUCCoilGiMapping& mapping : ppuc->GetCoilGiMappings()) + { + if (mapping.coil != p_solenoidState->solNo) + { + continue; + } + const uint8_t brightness = coilState != 0 ? mapping.onBrightness : mapping.offBrightness; + if (opt_debug || opt_debug_coils) + { + printf("Coil GI mapping: solenoid=%d, state=%d, gi=%u, brightness=%u\n", p_solenoidState->solNo, coilState, + mapping.gi, brightness); + } + ppuc->SetGIState(mapping.gi, brightness); + } if (isGameOnCoil) { @@ -2643,18 +2847,18 @@ void PINMAMECALLBACK OnSolenoidUpdated(PinmameSolenoidState* p_solenoidState, co } } - if (pPUPTriggerEngine) + if (pLuaRulesEngine) { if (isGameOnCoil) { - pPUPTriggerEngine->SetAttractMode(coilState == 0); + pLuaRulesEngine->SetAttractMode(coilState == 0); if (coilState == 0) { - pPUPTriggerEngine->SetCurrentBall(0); - pPUPTriggerEngine->SetCurrentPlayer(0); + pLuaRulesEngine->SetCurrentBall(0); + pLuaRulesEngine->SetCurrentPlayer(0); } } - pPUPTriggerEngine->OnCoilState(p_solenoidState->solNo, coilState); + pLuaRulesEngine->OnCoilState(p_solenoidState->solNo, coilState); } } @@ -2713,7 +2917,7 @@ int main(int argc, char** argv) cag_option_context cag_context; const char* config_file = NULL; const char* opt_ini_file = NULL; - const char* opt_pup_triggers = NULL; + const char* opt_rules = NULL; const char* opt_backbox_address = NULL; uint16_t opt_backbox_port = 6789; const char* opt_serial = NULL; @@ -2831,10 +3035,8 @@ int main(int argc, char** argv) opt_serial = DuplicateOptionalIniString(value); else if (key == "PinmamePath") opt_pinmame_path = DuplicateOptionalIniString(value); - else if (key == "PUPTriggers") - opt_pup_triggers = DuplicateOptionalIniString(value); - else if (key == "SpeechFile") - opt_speech_file = DuplicateOptionalIniString(value); + else if (key == "Rules") + opt_rules = DuplicateOptionalIniString(value); else if (key == "MusicFiles") opt_music_files = DuplicateOptionalIniString(value); else if (key == "MusicGapMs") @@ -3018,9 +3220,6 @@ int main(int argc, char** argv) case 'Y': opt_greeting = true; break; - case '9': - opt_speech_file = cag_option_get_value(&cag_context); - break; case 'o': opt_music_files = cag_option_get_value(&cag_context); break; @@ -3055,7 +3254,7 @@ int main(int argc, char** argv) opt_pup = true; break; case 'y': - opt_pup_triggers = cag_option_get_value(&cag_context); + opt_rules = cag_option_get_value(&cag_context); break; case 'i': opt_console_display = true; @@ -3302,7 +3501,7 @@ int main(int argc, char** argv) if (!ValidateSpeechAudioUsage(opt_no_sound, (opt_speech || HasOptionValue(opt_speech_voice) || HasOptionValue(opt_speech_rate_arg) || HasOptionValue(opt_speech_pitch_arg)), - opt_greeting, opt_speech_file, &speechValidationError)) + opt_greeting, &speechValidationError)) { fprintf(stderr, "%s\n", speechValidationError.c_str()); return 1; @@ -3345,7 +3544,7 @@ int main(int argc, char** argv) const auto initializeSpeechIfNeeded = [&]() -> bool { - if (pSpeechService != nullptr || !(opt_speech || opt_greeting || opt_speech_file)) + if (pSpeechService != nullptr || !(opt_speech || opt_greeting)) { return true; } @@ -3613,11 +3812,28 @@ int main(int argc, char** argv) } dmdConfig->SetRoundedCorners(opt_rounded_corners); - if (opt_pup_triggers) + if (opt_rules) { - pPUPTriggerEngine = std::make_unique(); - pPUPTriggerEngine->SetDebug(opt_debug || opt_debug_effects); - pPUPTriggerEngine->SetTriggerCallback( + pLuaRulesEngine = std::make_unique(); + pLuaRulesEngine->SetDebug(opt_debug || opt_debug_effects); + pLuaRulesEngine->SetSwitchGroups(ppuc->GetSwitchGroups()); + pLuaRulesEngine->SetActionCallback( + [](const RulesAction& action) + { + if (ppuc != nullptr) + { + g_interceptorOutputs.HandleAction(ppuc, action); + } + }); + pLuaRulesEngine->SetSpeechCallback( + [](const std::string& text) + { + if (pSpeechService != nullptr) + { + pSpeechService->SpeakText(text); + } + }); + pLuaRulesEngine->SetTriggerCallback( [](const char source, const uint16_t id, const uint8_t value) { if (source == kBoardEffectTriggerSource) @@ -3633,45 +3849,30 @@ int main(int argc, char** argv) return; } - if (source == kSpeechTriggerSource) - { - if (pSpeechService && pSpeechTriggerMap) - { - const std::string* text = pSpeechTriggerMap->Find(id); - if (text) - { - pSpeechService->SpeakText(*text); - } - } - return; - } - if (pDmd) { pDmd->SetPUPTrigger(source, id, value); } }); + std::vector ruleScripts; std::string error; - if (!pPUPTriggerEngine->LoadRules(opt_pup_triggers, error)) + if (!CollectRulesScripts(opt_rules, ruleScripts, error)) { - printf("%s: %s\n", error.c_str(), opt_pup_triggers); + printf("%s: %s\n", error.c_str(), opt_rules); return 1; } - printf("Loaded %zu PUP trigger rule(s) from %s\n", pPUPTriggerEngine->GetRuleCount(), opt_pup_triggers); - } - if (opt_speech_file) - { - pSpeechTriggerMap = std::make_unique(); - std::string error; - if (!pSpeechTriggerMap->Load(opt_speech_file, error)) + if (!pLuaRulesEngine->LoadScripts(ruleScripts, error)) { - printf("%s: %s\n", error.c_str(), opt_speech_file); + printf("%s: %s\n", error.c_str(), opt_rules); return 1; } - printf("Loaded %zu speech trigger text entr%s from %s\n", pSpeechTriggerMap->GetEntryCount(), - pSpeechTriggerMap->GetEntryCount() == 1 ? "y" : "ies", opt_speech_file); + + for (const std::string& ruleScript : ruleScripts) + { + printf("Loaded Lua rules from %s\n", ruleScript.c_str()); + } } PinmameConfig config = { @@ -3937,8 +4138,8 @@ int main(int argc, char** argv) } } - trackCurrentBall = pPUPTriggerEngine != nullptr && trackingConfig.currentBall.available; - trackCurrentPlayer = pPUPTriggerEngine != nullptr && trackingConfig.currentPlayer.available; + trackCurrentBall = pLuaRulesEngine != nullptr && trackingConfig.currentBall.available; + trackCurrentPlayer = pLuaRulesEngine != nullptr && trackingConfig.currentPlayer.available; nextTrackedStatePollAt = std::chrono::steady_clock::now(); } @@ -3950,10 +4151,16 @@ int main(int argc, char** argv) if (game_state.load(std::memory_order_acquire) == 0) { - if (pPUPTriggerEngine) + if (pLuaRulesEngine) { - pPUPTriggerEngine->Update(); + pLuaRulesEngine->Update(); + if (pLuaRulesEngine->HasFatalError()) + { + printf("Lua rules error: %s\n", pLuaRulesEngine->GetFatalError().c_str()); + running = false; + } } + g_interceptorOutputs.Service(ppuc); continue; } @@ -3967,7 +4174,7 @@ int main(int argc, char** argv) uint8_t currentBall = 0; if (TryDecodeTrackedPinmameValue(trackingConfig.currentBall, ¤tBall)) { - pPUPTriggerEngine->SetCurrentBall(currentBall); + pLuaRulesEngine->SetCurrentBall(currentBall); } else if (!loggedMissingCurrentBallApi && (opt_debug || opt_debug_errors)) { @@ -3981,7 +4188,7 @@ int main(int argc, char** argv) uint8_t currentPlayer = 0; if (TryDecodeTrackedPinmameValue(trackingConfig.currentPlayer, ¤tPlayer)) { - pPUPTriggerEngine->SetCurrentPlayer(currentPlayer); + pLuaRulesEngine->SetCurrentPlayer(currentPlayer); } else if (!loggedMissingCurrentPlayerApi && (opt_debug || opt_debug_errors)) { @@ -3998,12 +4205,22 @@ int main(int argc, char** argv) NoteBallSearchSwitchUpdate(ppuc, ballSearchRunner, switchState->number, newSwitchState, opt_ball_search_delay_ms); + LuaRulesEngine::SwitchProcessResult switchProcess; + if (pLuaRulesEngine) + { + switchProcess = pLuaRulesEngine->ProcessSwitchState(switchState->number, newSwitchState); + if (pLuaRulesEngine->HasFatalError()) + { + printf("Lua rules error: %s\n", pLuaRulesEngine->GetFatalError().c_str()); + running = false; + } + } + // Switches between 200 and 240 are custom switches within the io-boards which should not be sent to // pinmame. Switches above 240 will become negative values, for example 243 => -3. - if (switchState->number < 200 || switchState->number > 241) + if (switchProcess.forwardToCpu && (switchState->number < 200 || switchState->number > 241)) { - const int switchNumber = (switchState->number < 241) ? switchState->number : 240 - switchState->number; - PinmameSetSwitch(switchNumber, newSwitchState); + SendSwitchToCpu(switchState->number, newSwitchState); } if (opt_debug || opt_debug_switches) @@ -4011,10 +4228,7 @@ int main(int argc, char** argv) printf("Switch updated: #%d, %d\n", switchState->number, newSwitchState); } - if (pPUPTriggerEngine) - { - pPUPTriggerEngine->OnSwitchState(switchState->number, newSwitchState); - } + delete switchState; } ServiceBallSearchRunner(ppuc, ballSearchRunner, @@ -4032,11 +4246,16 @@ int main(int argc, char** argv) printf("Lamp updated: #%d, %d\n", lampNo, lampState); } - ppuc->SetLampState(lampNo, lampState); + g_interceptorOutputs.ApplyPinmameLamp(ppuc, static_cast(lampNo), lampState); - if (pPUPTriggerEngine) + if (pLuaRulesEngine) { - pPUPTriggerEngine->OnLampState(static_cast(lampNo), lampState); + pLuaRulesEngine->OnLampState(static_cast(lampNo), lampState); + if (pLuaRulesEngine->HasFatalError()) + { + printf("Lua rules error: %s\n", pLuaRulesEngine->GetFatalError().c_str()); + running = false; + } } } @@ -4057,10 +4276,16 @@ int main(int argc, char** argv) } } - if (pPUPTriggerEngine) + if (pLuaRulesEngine) { - pPUPTriggerEngine->Update(); + pLuaRulesEngine->Update(); + if (pLuaRulesEngine->HasFatalError()) + { + printf("Lua rules error: %s\n", pLuaRulesEngine->GetFatalError().c_str()); + running = false; + } } + g_interceptorOutputs.Service(ppuc); { // Needs to be a separate scope for the lock_guard // Process any pending render requests