Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ppuc.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: ppuc
on:
workflow_dispatch:
pull_request:

defaults:
run:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ third-party/runtime-libs/
third-party/build-libs/
third-party/include/
third-party/pinmame-nvram-maps
third-party/lua-src
87 changes: 85 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
)
Expand Down Expand Up @@ -280,6 +359,7 @@ if(PLATFORM STREQUAL "win" OR PLATFORM STREQUAL "win-mingw")
SDL364
SDL3_image64
yaml-cpp
${PPUC_LUA_LIBRARY}
ws2_32
)
else()
Expand All @@ -292,6 +372,7 @@ if(PLATFORM STREQUAL "win" OR PLATFORM STREQUAL "win-mingw")
SDL3
SDL3_image
yaml-cpp
${PPUC_LUA_LIBRARY}
ws2_32
)
endif()
Expand Down Expand Up @@ -322,6 +403,7 @@ else()
SDL3
SDL3_image
${PPUC_YAML_CPP_LINK_LIBRARY}
${PPUC_LUA_LIBRARY}
)
else()
target_link_libraries(ppuc-pinmame LINK_PUBLIC
Expand All @@ -333,6 +415,7 @@ else()
SDL3
SDL3_image
${PPUC_YAML_CPP_LINK_LIBRARY}
${PPUC_LUA_LIBRARY}
)
endif()

Expand Down
137 changes: 137 additions & 0 deletions INTERCEPTOR.md
Original file line number Diff line number Diff line change
@@ -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 <path>
```

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.
Loading
Loading