Hopefully this all makes vague sense to people who haven't coded a pinball machine before:
I also am going to try to take a different route from how (to my knowledge) all other pinball games are coded. I've worked with/explored a lot of systems (early williams/bally/gottlieb, wpc, MPF, skeleton game, etc) and they're all very much focused around a central concept: events come in (from switches, timers, etc), and then the code listens to those events, and sets some state (light on/off), fires some coils, etc. It seems to often lead to bugs where lamps get stuck on, balls get stuck in holes, etc, because there's state where there shouldn't be, or there isn't state where there should be.
For instance, on Demolition Man, when you collect the third claw award, it lights the 'car chase' inserts on both ramps. When you finish the mode, it turns them off again. But there's a bug where sometimes, if you drain while in car chase, on your next game, those lights will still be lit! Clearly, it's storing the 'state' of the lamp globally, and then forgetting to turn the state back to 'off' in some edge case. In my mind, that implies something wrong with the methodology they're structuring their code around. The lamp's state should be directly tied to being in car chase mode, it shouldn't be possible to 'forget' to turn if off. I'm sure there were tons more bugs like this that were found and fixed during development we never saw, if this one was able to get through multiple software revisions. I've even seen similar bugs on modern games, like Alice Cooper. On an older game like DM where they were writing in assembly and bytecode, and modes were a new concept, it's understandable that their handling of all this wasn't the cleanest, but with modern games where there can be tons of stacking going on, I want to come up with a cleaner solution for the lights.
Another example of the reverse case is something I've run into on my Taxi. In certain edge cases, the ball will go into one of the eject holes, and then won't kick back out. The game knows the ball is in there, since it registers in switch test, and it doesn't trigger ball search. Somewhere in the complex code surrounding the kickouts, there must be an edge case where it forgets to fire the eject coil. But again, that shouldn't be possible! We have a piece of set continuous 'state' here, which is that the ball is in the hole (the switch is closed), but its tied to a momentary input and output (the switch closing, and the solenoid firing). Since one of those momentary events was missed, the continuous state is now stuck.
In my mind, whenever possible, you want to match up these types of events/states. If a the eject switch is closed, the ball needs to be ejected. That's a 1 to 1 issue. The fact that sometimes, this might result in multiple momentary firings of the eject coil should be abstracted away. Similarly, imagine you had a shot where there's a down post and an opto. The game knows the ball is behind the post, since the opto is blocked, therefore it needs to release it. The same exact situation from a logical perspective: ball in hole/etc, ball needs to be released. But this time it's a down post, which isn't a momentary coil. You just need to energize the post until the ball leaves, no need to repeatedly fire the eject coil like in the first case. So I want to have a system where, in both cases, the game/mode's code is exactly the same. All it would say is something like "coil on if switch closed". Then a separate layer, which actually interacts with the hardware, can take care of things like, is this coil momentary? If so, fire it, then wait a bit. If the game is still requesting the coil to be on, fire it again. Increase the strength if necessary, or maybe trigger an operator alert if too many attempts have failed, etc.
With this in mind, my code works like this:
I have three layers: the game code, the machine driver code, and the actual hardware code. The game code is going to support stacking modes, sub-'modes', etc with their own priorities to override each other, etc. The machine driver code handles turning the 'wants' of the game code into commands for the hardware. In the simple cases this is just stuff like sending the 'turn on coil', 'turn off coil' commands to the hardware, but in cases of more complicated devices, it'll also manage that. For instance, my ramp has a switch to tell when it's up, and I put a switch underneath, to tell if a ball has gotten stuck under the ramp by rolling in from behind while it was down. The 'driver' will handle raising the ramp temporarily if it detects a stuck ball, or giving the lift coil another pulse if it detects the ramp has fallen down when it's supposed to be up. The hardware code will be very simple. It'll just handle reading the switch matrix, toggling IOs, etc, based on what the driver tells it to do.
The driver layer is going to have a list of every 'output' (coils, lamps, etc) the game has. Each mode will be able to specify its own values for any lamp or coil it wants to control. I'm then going to have a system that watches all the modes for when one of those values changes. It'll figure out which modes have priority, etc, and propagate the final value down to the driver. This way, I can have one mode say 'eject the ball from this scoop', but then, if needed, a sub mode that's playing an animation could say 'don't eject the ball right now', and block the coil from activating. When that sub-mode ends, the system will take care of automatically reverting all the outputs' values to remove any effects the sub-mode was having. In the case of lights, I'll also add support for combining values instead of overriding, so for instance I could have one mode say an arrow is green, and another say the arrow is white, but since they're stacked together at the same priority, the driver will take care of flashing the arrow green-white-green-white for them.
Hopefully this is all a good idea....