-
Notifications
You must be signed in to change notification settings - Fork 140
/
mode.py
executable file
·582 lines (436 loc) · 22 KB
/
mode.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
"""Contains the Mode base class."""
import copy
from typing import Any
from typing import Callable
from typing import Dict
from typing import List
from typing import Set
from typing import Tuple
from mpf.core.case_insensitive_dict import CaseInsensitiveDict
from mpf.core.delays import DelayManager
from mpf.core.device import Device
from mpf.core.events import EventHandlerKey
from mpf.core.events import QueuedEvent
from mpf.core.player import Player
from mpf.core.switch_controller import SwitchHandler
from mpf.core.timer import Timer
from mpf.core.utility_functions import Util
from mpf.core.logging import LogMixin
# pylint: disable-msg=too-many-instance-attributes
class Mode(LogMixin):
"""Parent class for in-game mode code."""
def __init__(self, machine, config: dict, name: str, path) -> None:
"""Initialise mode.
Args:
machine(mpf.core.machine.MachineController): the machine controller
config: config dict for mode
name: name of mode
path: path of mode
Returns:
"""
super().__init__()
self.machine = machine
self.config = config
self.name = name.lower()
self.path = path
self.priority = 0
self._active = False
self._mode_start_wait_queue = None # type: QueuedEvent
self.stop_methods = list() # type: List[Tuple[Callable, Any]]
self.timers = dict() # type: Dict[str, Timer]
self.start_callback = None # type: callable
self.stop_callback = None # type: callable
self.event_handlers = set() # type: Set[EventHandlerKey]
self.switch_handlers = list() # type: List[SwitchHandler]
self.mode_stop_kwargs = dict() # type: Dict[str, Any]
self.mode_devices = set() # type: Set[Device]
self.start_event_kwargs = None # type: Dict[str, Any]
self.stopping = False
self.delay = DelayManager(self.machine.delayRegistry)
self.player = None # type: Player
'''Reference to the current player object.'''
self._validate_mode_config()
self.configure_logging('Mode.' + name,
self.config['mode']['console_log'],
self.config['mode']['file_log'])
self._create_mode_devices()
self._initialise_mode_devices()
self.configure_mode_settings(config.get('mode', dict()))
self.auto_stop_on_ball_end = self.config['mode']['stop_on_ball_end']
'''Controls whether this mode is stopped when the ball ends,
regardless of its stop_events settings.
'''
self.restart_on_next_ball = self.config['mode']['restart_on_next_ball']
'''Controls whether this mode will restart on the next ball. This only
works if the mode was running when the ball ended. It's tracked per-
player in the 'restart_modes_on_next_ball' player variable.
'''
# Call registered remote loader methods
for item in self.machine.mode_controller.loader_methods:
if (item.config_section and
item.config_section in self.config and
self.config[item.config_section]):
item.method(config=self.config[item.config_section],
mode_path=self.path,
mode=self,
root_config_dict=self.config,
**item.kwargs)
elif not item.config_section:
item.method(config=self.config, mode_path=self.path,
**item.kwargs)
self.mode_init()
@staticmethod
def get_config_spec():
"""Return config spec for mode_settings."""
return '''
__valid_in__: mode
__allow_others__:
'''
def __repr__(self):
"""Return string representation."""
return '<Mode.{}>'.format(self.name)
@property
def active(self):
"""Return true if mode is active."""
return self._active
@active.setter
def active(self, active):
"""Setter for _active."""
if self._active != active:
self._active = active
self.machine.mode_controller.set_mode_state(self, self._active)
def configure_mode_settings(self, config):
"""Process this mode's configuration settings from a config dictionary."""
self.config['mode'] = self.machine.config_validator.validate_config(
config_spec='mode', source=config, section_name='mode')
for event in self.config['mode']['start_events']:
self.machine.events.add_handler(event=event, handler=self.start,
priority=self.config['mode']['priority'] +
self.config['mode']['start_priority'])
def _validate_mode_config(self):
"""Validate mode config."""
for section in self.machine.config['mpf']['mode_config_sections']:
this_section = self.config.get(section, None)
# do not double validate devices
if section in self.machine.device_manager.device_classes:
continue
if this_section:
if isinstance(this_section, dict):
for device, settings in this_section.items():
self.config[section][device] = (
self.machine.config_validator.validate_config(
section, settings, "mode:" + self.name))
else:
self.config[section] = (self.machine.config_validator.validate_config(section, this_section))
def _get_merged_settings(self, section_name):
"""Return a dict of a config section from the machine-wide config with the mode-specific config merged in."""
if section_name in self.machine.config:
return_dict = copy.deepcopy(self.machine.config[section_name])
else:
return_dict = CaseInsensitiveDict()
if section_name in self.config:
return_dict = Util.dict_merge(return_dict,
self.config[section_name],
combine_lists=False)
return return_dict
def start(self, mode_priority=None, callback=None, **kwargs):
"""Start this mode.
Args:
mode_priority: Integer value of what you want this mode to run at. If you
don't specify one, it will use the "Mode: priority" setting from
this mode's configuration file.
**kwargs: Catch-all since this mode might start from events with
who-knows-what keyword arguments.
Warning: You can safely call this method, but do not override it in your
mode code. If you want to write your own mode code by subclassing Mode,
put whatever code you want to run when this mode starts in the
mode_start method which will be called automatically.
"""
self.debug_log("Received request to start")
if self.config['mode']['game_mode'] and not self.machine.game:
self.warning_log("Can only start mode %s during a game. Aborting start.", self.name)
return
if self._active:
self.debug_log("Mode is already active. Aborting start.")
return
if self.config['mode']['use_wait_queue'] and 'queue' in kwargs:
self.debug_log("Registering a mode start wait queue")
self._mode_start_wait_queue = kwargs['queue']
self._mode_start_wait_queue.wait()
if isinstance(mode_priority, int):
self.priority = mode_priority
else:
self.priority = self.config['mode']['priority']
self.start_event_kwargs = kwargs
self._add_mode_devices()
self.debug_log("Registering mode_stop handlers")
# register mode stop events
if 'stop_events' in self.config['mode']:
for event in self.config['mode']['stop_events']:
# stop priority is +1 so if two modes of the same priority
# start and stop on the same event, the one will stop before
# the other starts
self.add_mode_event_handler(event=event, handler=self.stop,
priority=self.config['mode']['stop_priority'] + 1)
self.start_callback = callback
self.debug_log("Calling mode_start handlers")
for item in self.machine.mode_controller.start_methods:
if item.config_section in self.config or not item.config_section:
self.stop_methods.append(
item.method(config=self.config.get(item.config_section,
self.config),
priority=self.priority,
mode=self,
**item.kwargs))
self._setup_device_control_events()
self.machine.events.post_queue(event='mode_' + self.name + '_starting',
callback=self._started)
'''event: mode_(name)_starting
desc: The mode called "name" is starting.
This is a queue event. The mode will not fully start until the queue is
cleared.
'''
def _started(self):
"""Called after the mode_<name>_starting queue event has finished."""
self.info_log('Started. Priority: %s', self.priority)
self.active = True
if 'timers' in self.config:
self._setup_timers()
self._start_timers()
self.machine.events.post('mode_' + self.name + '_started',
callback=self._mode_started_callback)
'''event: mode_(name)_started
desc: Posted when a mode has started. The "name" part is replaced
with the actual name of the mode, so the actual event posted is
something like *mode_attract_started*, *mode_base_started*, etc.
This is posted after the "mode_(name)_starting" event.
'''
def _mode_started_callback(self, **kwargs):
"""Called after the mode_<name>_started queue event has finished."""
del kwargs
self.mode_start(**self.start_event_kwargs)
self.start_event_kwargs = dict()
if self.start_callback:
self.start_callback()
self.debug_log('Mode Start process complete.')
def stop(self, callback=None, **kwargs):
"""Stop this mode.
Args:
**kwargs: Catch-all since this mode might start from events with
who-knows-what keyword arguments.
Warning: You can safely call this method, but do not override it in your
mode code. If you want to write your own mode code by subclassing Mode,
put whatever code you want to run when this mode stops in the
mode_stop method which will be called automatically.
"""
if not self._active:
return
self.stopping = True
self.mode_stop_kwargs = kwargs
self.debug_log('Mode Stopping.')
self._remove_mode_switch_handlers()
self.stop_callback = callback
self._kill_timers()
self.delay.clear()
self.machine.events.post_queue(event='mode_' + self.name + '_stopping',
callback=self._stopped)
'''event: mode_(name)_stopping
desc: The mode called "name" is stopping. This is a queue event. The
mode won't actually stop until the queue is cleared.
'''
def _stopped(self):
self.info_log('Stopped.')
self.priority = 0
self.active = False
self.stopping = False
for callback in self.machine.mode_controller.stop_methods:
callback[0](self)
for item in self.stop_methods:
item[0](item[1])
self.stop_methods = list()
self.machine.events.post('mode_' + self.name + '_stopped',
callback=self._mode_stopped_callback)
'''event: mode_(name)_stopped
desc: Posted when a mode has stopped. The "name" part is replaced
with the actual name of the mode, so the actual event posted is
something like *mode_attract_stopped*, *mode_base_stopped*, etc.
'''
self.machine.events.post('clear', key=self.name)
'''event: clear
args:
key: string name of the configs to clear
desc: Posted to cause config players to clear whatever they're running
based on the key passed. Typically posted when a show or mode ends.
'''
if self._mode_start_wait_queue:
self.debug_log("Clearing wait queue")
self._mode_start_wait_queue.clear()
self._mode_start_wait_queue = None
def _mode_stopped_callback(self, **kwargs):
del kwargs
self._remove_mode_event_handlers()
self._remove_mode_devices()
self.mode_stop(**self.mode_stop_kwargs)
self.mode_stop_kwargs = dict()
if self.stop_callback:
self.stop_callback()
def _add_mode_devices(self):
# adds and initializes mode devices which get removed at the end of the mode
for collection_name, device_class in (
iter(self.machine.device_manager.device_classes.items())):
# check if there is config for the device type
if device_class.config_section in self.config:
for device_name in self.config[device_class.config_section]:
collection = getattr(self.machine, collection_name)
# get device
device = collection[device_name]
# Track that this device was added via this mode so we
# can remove it when the mode ends.
self.mode_devices.add(device)
if not self.config['mode']['game_mode'] and not device.can_exist_outside_of_game:
raise AssertionError("Device {} cannot exist in non game-mode {}.".format(
device, self.name
))
# This lets the device know it was added to a mode
device.device_added_to_mode(mode=self,
player=self.player)
def _create_mode_devices(self):
"""Create new devices that are specified in a mode config that haven't been created in the machine-wide."""
self.debug_log("Scanning config for mode-based devices")
for collection_name, device_class in iter(self.machine.device_manager.device_classes.items()):
# check if there is config for the device type
if device_class.config_section not in self.config:
continue
# check if it is supposed to be used in mode
if collection_name not in self.machine.config['mpf']['mode_config_sections']:
raise AssertionError("Found config for device {} in mode {} which may not be used in modes".format(
collection_name, self.name
))
for device, settings in iter(self.config[device_class.config_section].items()):
collection = getattr(self.machine, collection_name)
if device not in collection: # no existing device, create
self.debug_log("Creating mode-based device: %s",
device)
self.machine.device_manager.create_devices(
collection.name, {device: settings})
def _initialise_mode_devices(self):
"""Initialise new devices that are specified in a mode config."""
for collection_name, device_class in iter(self.machine.device_manager.device_classes.items()):
# check if there is config for the device type
if device_class.config_section not in self.config:
continue
for device, settings in iter(self.config[device_class.config_section].items()):
collection = getattr(self.machine, collection_name)
device = collection[device]
settings = device.prepare_config(settings, True)
settings = device.validate_and_parse_config(settings, True)
if device.config:
self.debug_log("Overwrite mode-based device: %s", device)
# overload
device.overload_config_in_mode(self, settings)
else:
self.debug_log("Initializing mode-based device: %s", device)
# load config
device.load_config(settings)
def _remove_mode_devices(self):
for device in self.mode_devices:
device.device_removed_from_mode(self)
self.mode_devices = set()
def _setup_device_control_events(self):
# registers mode handlers for control events for all devices specified
# in this mode's config (not just newly-created devices)
self.debug_log("Scanning mode-based config for device control_events")
for event, method, delay, device in (
self.machine.device_manager.get_device_control_events(
self.config)):
try:
event, priority = event.split('|')
except ValueError:
priority = 0
if not delay:
self.add_mode_event_handler(
event=event,
handler=method,
priority=int(priority) + 2)
else:
self.add_mode_event_handler(
event=event,
handler=self._control_event_handler,
priority=int(priority) + 2,
callback=method,
ms_delay=delay)
# get all devices in the mode
device_list = set()
for collection in self.machine.device_manager.collections:
if self.machine.device_manager.collections[collection].config_section in self.config:
for device, _ in \
iter(self.config[self.machine.device_manager.collections[collection].config_section].items()):
device_list.add(self.machine.device_manager.collections[collection][device])
for device in device_list:
device.add_control_events_in_mode(self)
def _control_event_handler(self, callback, ms_delay=0, **kwargs):
del kwargs
self.debug_log("_control_event_handler: callback: %s,", callback)
self.delay.add(name=callback, ms=ms_delay, callback=callback, mode=self)
def add_mode_event_handler(self, event, handler, priority=0, **kwargs):
"""Register an event handler which is automatically removed when this mode stops.
This method is similar to the Event Manager's add_handler() method,
except this method automatically unregisters the handlers when the mode
ends.
Args:
event: String name of the event you're adding a handler for. Since
events are text strings, they don't have to be pre-defined.
handler: The method that will be called when the event is fired.
priority: An arbitrary integer value that defines what order the
handlers will be called in. The default is 1, so if you have a
handler that you want to be called first, add it here with a
priority of 2. (Or 3 or 10 or 100000.) The numbers don't matter.
They're called from highest to lowest. (i.e. priority 100 is
called before priority 1.)
**kwargs: Any any additional keyword/argument pairs entered here
will be attached to the handler and called whenever that handler
is called. Note these are in addition to kwargs that could be
passed as part of the event post. If there's a conflict, the
event-level ones will win.
Returns:
A GUID reference to the handler which you can use to later remove
the handler via ``remove_handler_by_key``. Though you don't need to
remove the handler since the whole point of this method is they're
automatically removed when the mode stops.
Note that if you do add a handler via this method and then remove it
manually, that's ok too.
"""
key = self.machine.events.add_handler(event, handler, self.priority + priority, mode=self, **kwargs)
self.event_handlers.add(key)
return key
def _remove_mode_event_handlers(self):
for key in self.event_handlers:
self.machine.events.remove_handler_by_key(key)
self.event_handlers = set()
def _remove_mode_switch_handlers(self):
for handler in self.switch_handlers:
self.machine.switch_controller.remove_switch_handler_by_key(handler)
self.switch_handlers = list()
def _setup_timers(self):
# config is localized
for timer, settings in self.config['timers'].items():
self.timers[timer] = Timer(machine=self.machine, mode=self,
name=timer, config=settings)
return self._kill_timers
def _start_timers(self):
for timer in list(self.timers.values()):
if timer.config['start_running']:
timer.start()
def _kill_timers(self, ):
for timer in list(self.timers.values()):
timer.kill()
self.timers = dict()
def mode_init(self):
"""User-overrideable method which will be called when this mode initializes as part of the MPF boot process."""
pass
def mode_start(self, **kwargs):
"""User-overrideable method which will be called whenever this mode starts (i.e. whenever it becomes active)."""
pass
def mode_stop(self, **kwargs):
"""User-overrideable method which will be called whenever this mode stops."""
pass