FreeWPC is mostly an event-driven system, which only acts when there are new inputs to the system. This section describes the basic interface to the application layer software, and then explains how that is implemented internally.
When any module wants to be notified when a particular event occurs, it declares an event catcher function, using the CALLSET_ENTRY macro. Here is an example:
CALLSET_ENTRY (strobe_multiball, sw_forcefield_target) { ... }
This declares a new catcher for the module named strobe_multiball
,
which is to be called whenever the event sw_forcefield_target
occurs.
The name of the module is arbitrary, but the convention is to use the name of the source code file or something similar. If you need to write two different handlers in the same source file for the same event, they need to have different module names.
The event name must exactly match the name used by the generator of that event.
There is a list of predefined system events which are often caught (see System Events).
Switch names can also be used as events (e.g. sw_free_kick_target). Normally a switch
event is generated on an inactive-to-active transition; you can cause an event
on any transition by declaring the switch as ‘edge’ in the machine configuration file.
Ball devices also generate events for several reasons (see Ball Tracking), for example,
dev_lock_enter
.
If there are multiple catchers for the same event, then all of them will be invoked, in some random order. For example, another mode might also make use of the same target shown above:
CALLSET_ENTRY (mode_start, sw_forcefield_target) { ... }
Then both of these handlers would be called when the switch is closed.
To generate an event, use the callset_invoke()
API, passing
it the name of the event. You can create your own events for any
purpose; event names do not need to be declared. Such events are
as full-featured as system-defined events. When defining custom events
in a game, it is customary to prefix the event name with the short
name of the game (e.g. ‘tz’).
Event catchers are allowed to throw new events. This will result in nested function calls.
Most event handlers do not return a value. However, in some cases it is desirable to stop calling event handlers once the event has been claimed. You can do this with boolean events.
To throw a boolean event, use callset_invoke_boolean()
. This
returns TRUE if all of the event handlers return TRUE; else it
returns FALSE.
The event handlers are declared using CALLSET_BOOL_ENTRY
instead
of CALLSET_ENTRY
, and they must return either TRUE or FALSE.
The invocation of boolean handlers will immediately stop if one of them returns FALSE (so-called short-circuit evaluation).
The runtime code generated by gencallset will call the macro callset_debug
before invoking each of the handlers. This macro is passed one argument, which is a 16-bit
integer that is unique to every handler. The default implementation is to save this in
persistent memory; if a crash occurs, after rebooting the value can be examined to help
determine the last function that caused the problem.
Previously, debugging required a compile-time flag set in CALLSET_FLAGS, but this is no longer needed.
For more thorough debugging, you can rewrite the implementation of callset_debug
to do something else with those IDs, such as print them or set a breakpoint.
When you write a CALLSET_ENTRY
, the module name and event name are
concatenated to form the name of a function, separated by an underscore.
When you write a callset_invoke
, it calls a function named callset_
event.
gencallset scans all of the source code and creates these functions; you can
see the output in build/callset.c. So there is no queueing, memory allocation, or
anything complicated — these are just ordinary function calls. The trick is to do all
of the work at compile-time.
Because event handlers are just function calls, they can sometimes become deeply
nested. For example, a start button press can cause many other events to be
thrown. On the 6809 hardware, the stack size is limited and a stack overflow can
occur if functions nest too deeply, AND a call to task_sleep
is made. As long
as you don't sleep, there is no problem; the limitation is in the size of the per-task
stack area. start_game
and test_start
are particularly problematic
sometimes. Avoid sleeping in such handlers to be safe; if necessary, use a periodic
function instead.