def __init__(self, machine): """Base class for the auditor. Args: machine: A refence to the machine controller object. """ if 'auditor' not in machine.config: machine.log.debug('"Auditor:" section not found in machine ' 'configuration, so the auditor will not be ' 'used.') return self.log = logging.getLogger('Auditor') self.machine = machine self.machine.auditor = self self.switchnames_to_audit = set() self.enabled = False """Attribute that's viewed by other system components to let them know they should send auditing events. Set this via the enable() and disable() methods. """ self.data_manager = DataManager(self.machine, 'audits') self.machine.events.add_handler('init_phase_4', self._initialize)
def _load_machine_vars(self): self.machine_var_data_manager = DataManager(self, 'machine_vars') current_time = time.time() for name, settings in ( self.machine_var_data_manager.get_data().iteritems()): if ('expire' in settings and settings['expire'] and settings['expire'] < current_time): settings['value'] = 0 self.create_machine_var(name=name, value=settings['value'])
def _load_machine_vars(self): self.machine_var_data_manager = DataManager(self, 'machine_vars') current_time = time.time() for name, settings in ( self.machine_var_data_manager.get_data().iteritems()): if ('expire' in settings and settings['expire'] and settings['expire'] < current_time): settings['value'] = 0 self.create_machine_var(name=name, value=settings['value'])
def mode_init(self): self.data_manager = DataManager(self.machine, 'earnings') self.earnings = self.data_manager.get_data() self.credit_units_per_game = 0 self.credit_units_inserted = 0 self.credit_unit = 0 self.max_credit_units = 0 self.pricing_tiers = set() self.credit_units_for_pricing_tiers = 0 self.credits_config = self.machine.config_processor.process_config2( config_spec='credits', source=self._get_merged_settings('credits'), section_name='credits')
def mode_init(self): self.data_manager = DataManager(self.machine, 'earnings') self.earnings = self.data_manager.get_data() self.credit_units_per_game = 0 self.credit_units_inserted = 0 self.credit_unit = 0 self.max_credit_units = 0 self.pricing_tiers = set() self.credit_units_for_pricing_tiers = 0 self.credits_config = self.machine.config['credits'] if 'credits' in self.config: self.credits_config.update(self.config['credits']) self.credits_config = self.machine.config_processor.process_config2( 'credits', self.credits_config, 'credits')
class MachineController(object): """Base class for the Machine Controller object. The machine controller is the main entity of the entire framework. It's the main part that's in charge and makes things happen. Args: options: Dictionary of options the machine controller uses to configure itself. Attributes: options: A dictionary of options built from the command line options used to launch mpf.py. config: A dictionary of machine's configuration settings, merged from various sources. done: Boolean. Set to True and MPF exits. machine_path: The root path of this machine_files folder display: plugins: scriptlets: platform: events: """ def __init__(self, options): self.options = options self.log = logging.getLogger("Machine") self.log.info("Mission Pinball Framework v%s", version.__version__) self.log.debug("Command line arguments: {}".format(self.options)) self.verify_system_info() self.loop_start_time = 0 self.tick_num = 0 self.done = False self.machine_path = None # Path to this machine's folder root self.monitors = dict() self.plugins = list() self.scriptlets = list() self.modes = list() self.asset_managers = dict() self.game = None self.active_debugger = dict() self.machine_vars = CaseInsensitiveDict() self.machine_var_monitor = False self.machine_var_data_manager = None self.flag_bcp_reset_complete = False self.asset_loader_complete = False self.delay = DelayManager() self.crash_queue = Queue.Queue() Task.create(self._check_crash_queue) FileManager.init() self.config = dict() self._load_mpf_config() self._set_machine_path() self._load_machine_config() self.configure_debugger() self.hardware_platforms = dict() self.default_platform = None if not self.options['force_platform']: for section, platform in self.config['hardware'].iteritems(): if platform.lower() != 'default' and section != 'driverboards': self.add_platform(platform) self.set_default_platform(self.config['hardware']['platform']) else: self.add_platform(self.options['force_platform']) self.set_default_platform(self.options['force_platform']) # Do this here so there's a credit_string var even if they're not using # the credits mode try: credit_string = self.config['credits']['free_play_string'] except KeyError: credit_string = 'FREE PLAY' self.create_machine_var('credits_string', credit_string, silent=True) self._load_system_modules() # This is called so hw platforms have a change to register for events, # and/or anything else they need to do with system modules since # they're not set up yet when the hw platforms are constructed. for platform in self.hardware_platforms.values(): platform.initialize() self.validate_machine_config_section('machine') self.validate_machine_config_section('timing') self.validate_machine_config_section('hardware') self.validate_machine_config_section('game') self._register_system_events() self._load_machine_vars() self.events.post("init_phase_1") self.events._process_event_queue() self.events.post("init_phase_2") self.events._process_event_queue() self._load_plugins() self.events.post("init_phase_3") self.events._process_event_queue() self._load_scriptlets() self.events.post("init_phase_4") self.events._process_event_queue() self.events.post("init_phase_5") self.events._process_event_queue() self.reset() def validate_machine_config_section(self, section): if section not in self.config['config_validator']: return if section not in self.config: self.config[section] = dict() self.config[section] = self.config_processor.process_config2( section, self.config[section], section) def _register_system_events(self): self.events.add_handler('shutdown', self.power_off) self.events.add_handler( self.config['mpf']['switch_tag_event'].replace('%', 'shutdown'), self.power_off) self.events.add_handler('quit', self.quit) self.events.add_handler( self.config['mpf']['switch_tag_event'].replace('%', 'quit'), self.quit) self.events.add_handler('timer_tick', self._loading_tick) def _load_machine_vars(self): self.machine_var_data_manager = DataManager(self, 'machine_vars') current_time = time.time() for name, settings in ( self.machine_var_data_manager.get_data().iteritems()): if ('expire' in settings and settings['expire'] and settings['expire'] < current_time): settings['value'] = 0 self.create_machine_var(name=name, value=settings['value']) def _check_crash_queue(self): try: crash = self.crash_queue.get(block=False) except Queue.Empty: yield 1000 else: self.log.critical("MPF Shutting down due to child thread crash") self.log.critical("Crash details: %s", crash) self.done = True def _load_mpf_config(self): self.config = Config.load_config_file(self.options['mpfconfigfile']) def _set_machine_path(self): # If the machine folder value passed starts with a forward or # backward slash, then we assume it's from the mpf root. Otherwise we # assume it's in the mpf/machine_files folder if (self.options['machine_path'].startswith('/') or self.options['machine_path'].startswith('\\')): machine_path = self.options['machine_path'] else: machine_path = os.path.join( self.config['mpf']['paths']['machine_files'], self.options['machine_path']) self.machine_path = os.path.abspath(machine_path) self.log.debug("Machine path: {}".format(self.machine_path)) # Add the machine folder to sys.path so we can import modules from it sys.path.append(self.machine_path) def _load_machine_config(self): for num, config_file in enumerate(self.options['configfile']): if not (config_file.startswith('/') or config_file.startswith('\\')): config_file = os.path.join( self.machine_path, self.config['mpf']['paths']['config'], config_file) self.log.info("Machine config file #%s: %s", num + 1, config_file) self.config = Util.dict_merge(self.config, Config.load_config_file(config_file)) def verify_system_info(self): """Dumps information about the Python installation to the log. Information includes Python version, Python executable, platform, and system architecture. """ python_version = sys.version_info if python_version[0] != 2 or python_version[1] != 7: self.log.error( "Incorrect Python version. MPF requires Python 2.7." "x. You have Python %s.%s.%s.", python_version[0], python_version[1], python_version[2]) sys.exit() self.log.debug("Python version: %s.%s.%s", python_version[0], python_version[1], python_version[2]) self.log.debug("Platform: %s", sys.platform) self.log.debug("Python executable location: %s", sys.executable) self.log.debug("32-bit Python? %s", sys.maxsize < 2**32) def _load_system_modules(self): self.log.info("Loading system modules...") for module in self.config['mpf']['system_modules']: self.log.debug("Loading '%s' system module", module[1]) m = self.string_to_class(module[1])(self) setattr(self, module[0], m) def _load_plugins(self): self.log.info("Loading plugins...") # TODO: This should be cleaned up. Create a Plugins superclass and # classmethods to determine if the plugins should be used. for plugin in Util.string_to_list(self.config['mpf']['plugins']): self.log.debug("Loading '%s' plugin", plugin) pluginObj = self.string_to_class(plugin)(self) self.plugins.append(pluginObj) def _load_scriptlets(self): if 'scriptlets' in self.config: self.config['scriptlets'] = self.config['scriptlets'].split(' ') self.log.info("Loading scriptlets...") for scriptlet in self.config['scriptlets']: self.log.debug("Loading '%s' scriptlet", scriptlet) i = __import__(self.config['mpf']['paths']['scriptlets'] + '.' + scriptlet.split('.')[0], fromlist=['']) self.scriptlets.append( getattr(i, scriptlet.split('.')[1])( machine=self, name=scriptlet.split('.')[1])) def _prepare_to_reset(self): pass # wipe all event handlers def reset(self): """Resets the machine. This method is safe to call. It essentially sets up everything from scratch without reloading the config files and assets from disk. This method is called after a game ends and before attract mode begins. Note: This method is not yet implemented. """ self.events.post('Resetting...') self.events._process_event_queue() self.events.post('machine_reset_phase_1') self.events._process_event_queue() self.events.post('machine_reset_phase_2') self.events._process_event_queue() self.events.post('machine_reset_phase_3') self.events._process_event_queue() self.log.debug('Reset Complete') def add_platform(self, name): """Makes an additional hardware platform interface available to MPF. Args: name: String name of the platform to add. Must match the name of a platform file in the mpf/platforms folder (without the .py extension). """ if name not in self.hardware_platforms: hardware_platform = __import__('mpf.platform.%s' % name, fromlist=["HardwarePlatform"]) self.hardware_platforms[name] = ( hardware_platform.HardwarePlatform(self)) def set_default_platform(self, name): """Sets the default platform which is used if a device class-specific or device-specific platform is not specified. The default platform also controls whether a platform timer or MPF's timer is used. Args: name: String name of the platform to set to default. """ try: self.default_platform = self.hardware_platforms[name] self.log.debug("Setting default platform to '%s'", name) except KeyError: self.log.error( "Cannot set default platform to '%s', as that's not" " a currently active platform", name) def string_to_class(self, class_string): """Converts a string like mpf.system.events.EventManager into a python class. Args: class_string(str): The input string Returns: A reference to the python class object This function came from here: http://stackoverflow.com/questions/452969/ does-python-have-an-equivalent-to-java-class-forname """ parts = class_string.split('.') module = ".".join(parts[:-1]) m = __import__(module) for comp in parts[1:]: m = getattr(m, comp) return m def register_monitor(self, monitor_class, monitor): """Registers a monitor. Args: monitor_class: String name of the monitor class for this monitor that's being registered. monitor: String name of the monitor. MPF uses monitors to allow components to monitor certain internal elements of MPF. For example, a player variable monitor could be setup to be notified of any changes to a player variable, or a switch monitor could be used to allow a plugin to be notified of any changes to any switches. The MachineController's list of registered monitors doesn't actually do anything. Rather it's a dictionary of sets which the monitors themselves can reference when they need to do something. We just needed a central registry of monitors. """ if monitor_class not in self.monitors: self.monitors[monitor_class] = set() self.monitors[monitor_class].add(monitor) def run(self): """Starts the main machine run loop.""" self.log.debug("Starting the main run loop.") self.default_platform.timer_initialize() self.loop_start_time = time.time() if self.default_platform.features['hw_timer']: self.default_platform.run_loop() else: self._mpf_timer_run_loop() def _mpf_timer_run_loop(self): #Main machine run loop with when the default platform interface #specifies the MPF should control the main timer start_time = time.time() loops = 0 secs_per_tick = timing.Timing.secs_per_tick sleep_sec = self.config['timing']['hw_thread_sleep_ms'] / 1000.0 self.default_platform.next_tick_time = time.time() try: while self.done is False: time.sleep(sleep_sec) self.default_platform.tick() loops += 1 if self.default_platform.next_tick_time <= time.time( ): # todo change this self.timer_tick() self.default_platform.next_tick_time += secs_per_tick except KeyboardInterrupt: pass self.log_loop_rate() self._platform_stop() try: self.log.info("Hardware loop rate: %s Hz", round(loops / (time.time() - start_time), 2)) except ZeroDivisionError: self.log.info("Hardware loop rate: 0 Hz") def timer_tick(self): """Called to "tick" MPF at a rate specified by the machine Hz setting. This method is called by the MPF run loop or the platform run loop, depending on the platform. (Some platforms drive the loop, and others let MPF drive.) """ self.tick_num += 1 # used to calculate the loop rate when MPF exits self.timing.timer_tick() # notifies the timing module self.events.post('timer_tick') # sends the timer_tick system event tasks.Task.timer_tick() # notifies tasks tasks.DelayManager.timer_tick(self) self.events._process_event_queue() def _platform_stop(self): for platform in self.hardware_platforms.values(): platform.stop() def power_off(self): """Attempts to perform a power down of the pinball machine and ends MPF. This method is not yet implemented. """ pass def quit(self): """Performs a graceful exit of MPF.""" self.log.info("Shutting down...") self.events.post('shutdown') self.events._process_event_queue() self.done = True def log_loop_rate(self): self.log.info("Target MPF loop rate: %s Hz", timing.Timing.HZ) try: self.log.info( "Actual MPF loop rate: %s Hz", round(self.tick_num / (time.time() - self.loop_start_time), 2)) except ZeroDivisionError: self.log.info("Actual MPF loop rate: 0 Hz") def _loading_tick(self): if not self.asset_loader_complete: if AssetManager.loader_queue.qsize(): self.log.debug( "Holding Attract start while MPF assets load. " "Remaining: %s", AssetManager.loader_queue.qsize()) self.bcp.bcp_trigger( 'assets_to_load', total=AssetManager.total_assets, remaining=AssetManager.loader_queue.qsize()) else: self.bcp.bcp_trigger('assets_to_load', total=AssetManager.total_assets, remaining=0) self.asset_loader_complete = True elif self.bcp.active_connections and not self.flag_bcp_reset_complete: if self.tick_num % Timing.HZ == 0: self.log.info("Waiting for BCP reset_complete...") else: self.log.debug("Asset loading complete") self._reset_complete() def bcp_reset_complete(self): self.flag_bcp_reset_complete = True def _reset_complete(self): self.log.debug('Reset Complete') self.events.post('reset_complete') self.events.remove_handler(self._loading_tick) def configure_debugger(self): pass def get_debug_status(self, debug_path): if (self.options['loglevel'] > 10 or self.options['consoleloglevel'] > 10): return True class_, module = debug_path.split('|') try: if module in self.active_debugger[class_]: return True else: return False except KeyError: return False def set_machine_var(self, name, value, force_events=False): """Sets the value of a machine variable. Args: name: String name of the variable you're setting the value for. value: The value you're setting. This can be any Type. force_events: Boolean which will force the event posting, the machine monitor callback, and writing the variable to disk (if it's set to persist). By default these things only happen if the new value is different from the old value. """ if name not in self.machine_vars: self.log.warning( "Received request to set machine_var '%s', but " "that is not a valid machine_var.", name) return prev_value = self.machine_vars[name]['value'] self.machine_vars[name]['value'] = value try: change = value - prev_value except TypeError: if prev_value != value: change = True else: change = False if change or force_events: if self.machine_vars[name]['persist']: disk_var = CaseInsensitiveDict() disk_var['value'] = value if self.machine_vars[name]['expire_secs']: disk_var['expire'] = ( time.time() + self.machine_vars[name]['expire_secs']) self.machine_var_data_manager.save_key(name, disk_var) self.log.debug( "Setting machine_var '%s' to: %s, (prior: %s, " "change: %s)", name, value, prev_value, change) self.events.post('machine_var_' + name, value=value, prev_value=prev_value, change=change) if self.machine_var_monitor: for callback in self.monitors['machine_vars']: callback(name=name, value=value, prev_value=prev_value, change=change) def get_machine_var(self, name): """Returns the value of a machine variable. Args: name: String name of the variable you want to get that value for. Returns: The value of the variable if it exists, or None if the variable does not exist. """ try: return self.machine_vars[name]['value'] except KeyError: return None def is_machine_var(self, name): if name in self.machine_vars: return True else: return False def create_machine_var(self, name, value=0, persist=False, expire_secs=None, silent=False): """Creates a new machine variable: Args: name: String name of the variable. value: The value of the variable. This can be any Type. persist: Boolean as to whether this variable should be saved to disk so it's available the next time MPF boots. expire_secs: Optional number of seconds you'd like this variable to persist on disk for. When MPF boots, if the expiration time of the variable is in the past, it will be loaded with a value of 0. For example, this lets you write the number of credits on the machine to disk to persist even during power off, but you could set it so that those only stay persisted for an hour. """ var = CaseInsensitiveDict() var['value'] = value var['persist'] = persist var['expire_secs'] = expire_secs self.machine_vars[name] = var if not silent: self.set_machine_var(name, value, force_events=True) def remove_machine_var(self, name): """Removes a machine variable by name. If this variable persists to disk, it will remove it from there too. Args: name: String name of the variable you want to remove. """ try: del self.machine_vars[name] self.machine_var_data_manager.remove_key(name) except KeyError: pass def remove_machine_var_search(self, startswith='', endswith=''): """Removes a machine variable by matching parts of its name. Args: startswith: Optional start of the variable name to match. endswith: Optional end of the variable name to match. For example, if you pass startswit='player' and endswith='score', this method will match and remove player1_score, player2_score, etc. """ for var in self.machine_vars.keys(): if var.startswith(startswith) and var.endswith(endswith): del self.machine_vars[var] self.machine_var_data_manager.remove_key(var)
class MachineController(object): """Base class for the Machine Controller object. The machine controller is the main entity of the entire framework. It's the main part that's in charge and makes things happen. Args: options: Dictionary of options the machine controller uses to configure itself. Attributes: options: A dictionary of options built from the command line options used to launch mpf.py. config: A dictionary of machine's configuration settings, merged from various sources. done: Boolean. Set to True and MPF exits. machine_path: The root path of this machine_files folder display: plugins: scriptlets: platform: events: """ def __init__(self, options): self.options = options self.log = logging.getLogger("Machine") self.log.info("Mission Pinball Framework v%s", version.__version__) self.log.debug("Command line arguments: {}".format(self.options)) self.verify_system_info() self.loop_start_time = 0 self.tick_num = 0 self.done = False self.machine_path = None # Path to this machine's folder root self.monitors = dict() self.plugins = list() self.scriptlets = list() self.modes = list() self.asset_managers = dict() self.game = None self.active_debugger = dict() self.machine_vars = CaseInsensitiveDict() self.machine_var_monitor = False self.machine_var_data_manager = None self.flag_bcp_reset_complete = False self.asset_loader_complete = False self.delay = DelayManager() self.crash_queue = Queue.Queue() Task.create(self._check_crash_queue) FileManager.init() self.config = dict() self._load_mpf_config() self._set_machine_path() self._load_machine_config() self.configure_debugger() self.hardware_platforms = dict() self.default_platform = None if not self.options['force_platform']: for section, platform in self.config['hardware'].iteritems(): if platform.lower() != 'default' and section != 'driverboards': self.add_platform(platform) self.set_default_platform(self.config['hardware']['platform']) else: self.add_platform(self.options['force_platform']) self.set_default_platform(self.options['force_platform']) # Do this here so there's a credit_string var even if they're not using # the credits mode try: credit_string = self.config['credits']['free_play_string'] except KeyError: credit_string = 'FREE PLAY' self.create_machine_var('credits_string', credit_string, silent=True) self._load_system_modules() # This is called so hw platforms have a change to register for events, # and/or anything else they need to do with system modules since # they're not set up yet when the hw platforms are constructed. for platform in self.hardware_platforms.values(): platform.initialize() self.validate_machine_config_section('machine') self.validate_machine_config_section('timing') self.validate_machine_config_section('hardware') self.validate_machine_config_section('game') self._register_system_events() self._load_machine_vars() self.events.post("init_phase_1") self.events._process_event_queue() self.events.post("init_phase_2") self.events._process_event_queue() self._load_plugins() self.events.post("init_phase_3") self.events._process_event_queue() self._load_scriptlets() self.events.post("init_phase_4") self.events._process_event_queue() self.events.post("init_phase_5") self.events._process_event_queue() self.reset() def validate_machine_config_section(self, section): if section not in self.config['config_validator']: return if section not in self.config: self.config[section] = dict() self.config[section] = self.config_processor.process_config2( section, self.config[section], section) def _register_system_events(self): self.events.add_handler('shutdown', self.power_off) self.events.add_handler(self.config['mpf']['switch_tag_event']. replace('%', 'shutdown'), self.power_off) self.events.add_handler('quit', self.quit) self.events.add_handler(self.config['mpf']['switch_tag_event']. replace('%', 'quit'), self.quit) self.events.add_handler('timer_tick', self._loading_tick) def _load_machine_vars(self): self.machine_var_data_manager = DataManager(self, 'machine_vars') current_time = time.time() for name, settings in ( self.machine_var_data_manager.get_data().iteritems()): if ('expire' in settings and settings['expire'] and settings['expire'] < current_time): settings['value'] = 0 self.create_machine_var(name=name, value=settings['value']) def _check_crash_queue(self): try: crash = self.crash_queue.get(block=False) except Queue.Empty: yield 1000 else: self.log.critical("MPF Shutting down due to child thread crash") self.log.critical("Crash details: %s", crash) self.done = True def _load_mpf_config(self): self.config = Config.load_config_file(self.options['mpfconfigfile']) def _set_machine_path(self): # If the machine folder value passed starts with a forward or # backward slash, then we assume it's from the mpf root. Otherwise we # assume it's in the mpf/machine_files folder if (self.options['machine_path'].startswith('/') or self.options['machine_path'].startswith('\\')): machine_path = self.options['machine_path'] else: machine_path = os.path.join(self.config['mpf']['paths'] ['machine_files'], self.options['machine_path']) self.machine_path = os.path.abspath(machine_path) self.log.debug("Machine path: {}".format(self.machine_path)) # Add the machine folder to sys.path so we can import modules from it sys.path.append(self.machine_path) def _load_machine_config(self): for num, config_file in enumerate(self.options['configfile']): if not (config_file.startswith('/') or config_file.startswith('\\')): config_file = os.path.join(self.machine_path, self.config['mpf']['paths']['config'], config_file) self.log.info("Machine config file #%s: %s", num+1, config_file) self.config = Util.dict_merge(self.config, Config.load_config_file(config_file)) def verify_system_info(self): """Dumps information about the Python installation to the log. Information includes Python version, Python executable, platform, and system architecture. """ python_version = sys.version_info if python_version[0] != 2 or python_version[1] != 7: self.log.error("Incorrect Python version. MPF requires Python 2.7." "x. You have Python %s.%s.%s.", python_version[0], python_version[1], python_version[2]) sys.exit() self.log.debug("Python version: %s.%s.%s", python_version[0], python_version[1], python_version[2]) self.log.debug("Platform: %s", sys.platform) self.log.debug("Python executable location: %s", sys.executable) self.log.debug("32-bit Python? %s", sys.maxsize < 2**32) def _load_system_modules(self): self.log.info("Loading system modules...") for module in self.config['mpf']['system_modules']: self.log.debug("Loading '%s' system module", module[1]) m = self.string_to_class(module[1])(self) setattr(self, module[0], m) def _load_plugins(self): self.log.info("Loading plugins...") # TODO: This should be cleaned up. Create a Plugins superclass and # classmethods to determine if the plugins should be used. for plugin in Util.string_to_list( self.config['mpf']['plugins']): self.log.debug("Loading '%s' plugin", plugin) pluginObj = self.string_to_class(plugin)(self) self.plugins.append(pluginObj) def _load_scriptlets(self): if 'scriptlets' in self.config: self.config['scriptlets'] = self.config['scriptlets'].split(' ') self.log.info("Loading scriptlets...") for scriptlet in self.config['scriptlets']: self.log.debug("Loading '%s' scriptlet", scriptlet) i = __import__(self.config['mpf']['paths']['scriptlets'] + '.' + scriptlet.split('.')[0], fromlist=['']) self.scriptlets.append(getattr(i, scriptlet.split('.')[1]) (machine=self, name=scriptlet.split('.')[1])) def _prepare_to_reset(self): pass # wipe all event handlers def reset(self): """Resets the machine. This method is safe to call. It essentially sets up everything from scratch without reloading the config files and assets from disk. This method is called after a game ends and before attract mode begins. Note: This method is not yet implemented. """ self.events.post('Resetting...') self.events._process_event_queue() self.events.post('machine_reset_phase_1') self.events._process_event_queue() self.events.post('machine_reset_phase_2') self.events._process_event_queue() self.events.post('machine_reset_phase_3') self.events._process_event_queue() self.log.debug('Reset Complete') def add_platform(self, name): """Makes an additional hardware platform interface available to MPF. Args: name: String name of the platform to add. Must match the name of a platform file in the mpf/platforms folder (without the .py extension). """ if name not in self.hardware_platforms: hardware_platform = __import__('mpf.platform.%s' % name, fromlist=["HardwarePlatform"]) self.hardware_platforms[name] = ( hardware_platform.HardwarePlatform(self)) def set_default_platform(self, name): """Sets the default platform which is used if a device class-specific or device-specific platform is not specified. The default platform also controls whether a platform timer or MPF's timer is used. Args: name: String name of the platform to set to default. """ try: self.default_platform = self.hardware_platforms[name] self.log.debug("Setting default platform to '%s'", name) except KeyError: self.log.error("Cannot set default platform to '%s', as that's not" " a currently active platform", name) def string_to_class(self, class_string): """Converts a string like mpf.system.events.EventManager into a python class. Args: class_string(str): The input string Returns: A reference to the python class object This function came from here: http://stackoverflow.com/questions/452969/ does-python-have-an-equivalent-to-java-class-forname """ parts = class_string.split('.') module = ".".join(parts[:-1]) m = __import__(module) for comp in parts[1:]: m = getattr(m, comp) return m def register_monitor(self, monitor_class, monitor): """Registers a monitor. Args: monitor_class: String name of the monitor class for this monitor that's being registered. monitor: String name of the monitor. MPF uses monitors to allow components to monitor certain internal elements of MPF. For example, a player variable monitor could be setup to be notified of any changes to a player variable, or a switch monitor could be used to allow a plugin to be notified of any changes to any switches. The MachineController's list of registered monitors doesn't actually do anything. Rather it's a dictionary of sets which the monitors themselves can reference when they need to do something. We just needed a central registry of monitors. """ if monitor_class not in self.monitors: self.monitors[monitor_class] = set() self.monitors[monitor_class].add(monitor) def run(self): """Starts the main machine run loop.""" self.log.debug("Starting the main run loop.") self.default_platform.timer_initialize() self.loop_start_time = time.time() if self.default_platform.features['hw_timer']: self.default_platform.run_loop() else: self._mpf_timer_run_loop() def _mpf_timer_run_loop(self): #Main machine run loop with when the default platform interface #specifies the MPF should control the main timer start_time = time.time() loops = 0 secs_per_tick = timing.Timing.secs_per_tick sleep_sec = self.config['timing']['hw_thread_sleep_ms'] / 1000.0 self.default_platform.next_tick_time = time.time() try: while self.done is False: time.sleep(sleep_sec) self.default_platform.tick() loops += 1 if self.default_platform.next_tick_time <= time.time(): # todo change this self.timer_tick() self.default_platform.next_tick_time += secs_per_tick except KeyboardInterrupt: pass self.log_loop_rate() self._platform_stop() try: self.log.info("Hardware loop rate: %s Hz", round(loops / (time.time() - start_time), 2)) except ZeroDivisionError: self.log.info("Hardware loop rate: 0 Hz") def timer_tick(self): """Called to "tick" MPF at a rate specified by the machine Hz setting. This method is called by the MPF run loop or the platform run loop, depending on the platform. (Some platforms drive the loop, and others let MPF drive.) """ self.tick_num += 1 # used to calculate the loop rate when MPF exits self.timing.timer_tick() # notifies the timing module self.events.post('timer_tick') # sends the timer_tick system event tasks.Task.timer_tick() # notifies tasks tasks.DelayManager.timer_tick(self) self.events._process_event_queue() def _platform_stop(self): for platform in self.hardware_platforms.values(): platform.stop() def power_off(self): """Attempts to perform a power down of the pinball machine and ends MPF. This method is not yet implemented. """ pass def quit(self): """Performs a graceful exit of MPF.""" self.log.info("Shutting down...") self.events.post('shutdown') self.events._process_event_queue() self.done = True def log_loop_rate(self): self.log.info("Target MPF loop rate: %s Hz", timing.Timing.HZ) try: self.log.info("Actual MPF loop rate: %s Hz", round(self.tick_num / (time.time() - self.loop_start_time), 2)) except ZeroDivisionError: self.log.info("Actual MPF loop rate: 0 Hz") def _loading_tick(self): if not self.asset_loader_complete: if AssetManager.loader_queue.qsize(): self.log.debug("Holding Attract start while MPF assets load. " "Remaining: %s", AssetManager.loader_queue.qsize()) self.bcp.bcp_trigger('assets_to_load', total=AssetManager.total_assets, remaining=AssetManager.loader_queue.qsize()) else: self.bcp.bcp_trigger('assets_to_load', total=AssetManager.total_assets, remaining=0) self.asset_loader_complete = True elif self.bcp.active_connections and not self.flag_bcp_reset_complete: if self.tick_num % Timing.HZ == 0: self.log.info("Waiting for BCP reset_complete...") else: self.log.debug("Asset loading complete") self._reset_complete() def bcp_reset_complete(self): self.flag_bcp_reset_complete = True def _reset_complete(self): self.log.debug('Reset Complete') self.events.post('reset_complete') self.events.remove_handler(self._loading_tick) def configure_debugger(self): pass def get_debug_status(self, debug_path): if (self.options['loglevel'] > 10 or self.options['consoleloglevel'] > 10): return True class_, module = debug_path.split('|') try: if module in self.active_debugger[class_]: return True else: return False except KeyError: return False def set_machine_var(self, name, value, force_events=False): """Sets the value of a machine variable. Args: name: String name of the variable you're setting the value for. value: The value you're setting. This can be any Type. force_events: Boolean which will force the event posting, the machine monitor callback, and writing the variable to disk (if it's set to persist). By default these things only happen if the new value is different from the old value. """ if name not in self.machine_vars: self.log.warning("Received request to set machine_var '%s', but " "that is not a valid machine_var.", name) return prev_value = self.machine_vars[name]['value'] self.machine_vars[name]['value'] = value try: change = value-prev_value except TypeError: if prev_value != value: change = True else: change = False if change or force_events: if self.machine_vars[name]['persist']: disk_var = CaseInsensitiveDict() disk_var['value'] = value if self.machine_vars[name]['expire_secs']: disk_var['expire'] = (time.time() + self.machine_vars[name]['expire_secs']) self.machine_var_data_manager.save_key(name, disk_var) self.log.debug("Setting machine_var '%s' to: %s, (prior: %s, " "change: %s)", name, value, prev_value, change) self.events.post('machine_var_' + name, value=value, prev_value=prev_value, change=change) if self.machine_var_monitor: for callback in self.monitors['machine_vars']: callback(name=name, value=value, prev_value=prev_value, change=change) def get_machine_var(self, name): """Returns the value of a machine variable. Args: name: String name of the variable you want to get that value for. Returns: The value of the variable if it exists, or None if the variable does not exist. """ try: return self.machine_vars[name]['value'] except KeyError: return None def is_machine_var(self, name): if name in self.machine_vars: return True else: return False def create_machine_var(self, name, value=0, persist=False, expire_secs=None, silent=False): """Creates a new machine variable: Args: name: String name of the variable. value: The value of the variable. This can be any Type. persist: Boolean as to whether this variable should be saved to disk so it's available the next time MPF boots. expire_secs: Optional number of seconds you'd like this variable to persist on disk for. When MPF boots, if the expiration time of the variable is in the past, it will be loaded with a value of 0. For example, this lets you write the number of credits on the machine to disk to persist even during power off, but you could set it so that those only stay persisted for an hour. """ var = CaseInsensitiveDict() var['value'] = value var['persist'] = persist var['expire_secs'] = expire_secs self.machine_vars[name] = var if not silent: self.set_machine_var(name, value, force_events=True) def remove_machine_var(self, name): """Removes a machine variable by name. If this variable persists to disk, it will remove it from there too. Args: name: String name of the variable you want to remove. """ try: del self.machine_vars[name] self.machine_var_data_manager.remove_key(name) except KeyError: pass def remove_machine_var_search(self, startswith='', endswith=''): """Removes a machine variable by matching parts of its name. Args: startswith: Optional start of the variable name to match. endswith: Optional end of the variable name to match. For example, if you pass startswit='player' and endswith='score', this method will match and remove player1_score, player2_score, etc. """ for var in self.machine_vars.keys(): if var.startswith(startswith) and var.endswith(endswith): del self.machine_vars[var] self.machine_var_data_manager.remove_key(var)
class Credits(Mode): def mode_init(self): self.data_manager = DataManager(self.machine, 'earnings') self.earnings = self.data_manager.get_data() self.credit_units_per_game = 0 self.credit_units_inserted = 0 self.credit_unit = 0 self.max_credit_units = 0 self.pricing_tiers = set() self.credit_units_for_pricing_tiers = 0 self.credits_config = self.machine.config['credits'] if 'credits' in self.config: self.credits_config.update(self.config['credits']) self.credits_config = self.machine.config_processor.process_config2( 'credits', self.credits_config, 'credits') def mode_start(self, **kwargs): self.add_mode_event_handler('enable_free_play', self.enable_free_play) self.add_mode_event_handler('enable_credit_play', self.enable_credit_play) self.add_mode_event_handler('toggle_credit_play', self.toggle_credit_play) self.add_mode_event_handler('slam_tilt', self.clear_all_credits) if self.credits_config['free_play']: self.enable_free_play(post_event=False) else: self._calculate_credit_units() self._calculate_pricing_tiers() self.enable_credit_play(post_event=False) def mode_stop(self, **kwargs): self.enable_free_play() def _calculate_credit_units(self): # "credit units" are how we handle fractional credits (since most # pinball machines show credits as fractions instead of decimals). # We convert everything to the smallest coin unit and then track # how many of those a game takes. So price of $0.75 per game with a # quarter slot means a credit unit is 0.25 and the game needs 3 credit # units to start. This is all hidden from the player # We need to calculate it differently depending on how the coin switch # values relate to game cost. if self.credits_config['switches']: min_currency_value = min(x['value'] for x in self.credits_config['switches']) else: min_currency_value = ( self.credits_config['pricing_tiers'][0]['price']) price_per_game = self.credits_config['pricing_tiers'][0]['price'] if min_currency_value == price_per_game: self.credit_unit = min_currency_value elif min_currency_value < price_per_game: self.credit_unit = price_per_game - min_currency_value if self.credit_unit > min_currency_value: self.credit_unit = min_currency_value elif min_currency_value > price_per_game: self.credit_unit = min_currency_value - price_per_game if self.credit_unit > price_per_game: self.credit_unit = price_per_game self.log.debug("Calculated the credit unit to be %s based on a minimum" "currency value of %s and a price per game of %s", self.credit_unit, min_currency_value, price_per_game) self.credit_units_per_game = ( int(self.credits_config['pricing_tiers'][0]['price'] / self.credit_unit)) self.log.debug("Credit units per game: %s", self.credit_units_per_game) if self.credits_config['max_credits']: self.max_credit_units = (self.credit_units_per_game * self.credits_config['max_credits']) def _calculate_pricing_tiers(self): # pricing tiers are calculated with a set of tuples which indicate the # credit units for the price break as well as the "bump" in credit # units that should be added once that break is passed. for pricing_tier in self.credits_config['pricing_tiers']: credit_units = pricing_tier['price'] / self.credit_unit actual_credit_units = self.credit_units_per_game * pricing_tier['credits'] bonus = actual_credit_units - credit_units self.log.debug("Pricing Tier Bonus. Price: %s, Credits: %s. " "Credit units for this tier: %s, Credit units this " "tier buys: %s, Bonus bump needed: %s", pricing_tier['price'], pricing_tier['credits'], credit_units, actual_credit_units, bonus) self.pricing_tiers.add((credit_units, bonus)) def enable_credit_play(self, post_event=True, **kwargs): self.credits_config['free_play'] = False if self.machine.is_machine_var('credit_units'): credit_units = self.machine.get_machine_var('credit_units') else: credit_units = 0 if self.credits_config['persist_credits_while_off_time']: self.machine.create_machine_var(name='credit_units', value=credit_units, persist=True, expire_secs=self.credits_config[ 'persist_credits_while_off_time']) else: self.machine.create_machine_var(name='credit_units', value=credit_units) self.machine.create_machine_var('credits_string', ' ') self.machine.create_machine_var('credits_value', '0') self.machine.create_machine_var('credits_whole_num', 0) self.machine.create_machine_var('credits_numerator', 0) self.machine.create_machine_var('credits_denominator', 0) self._update_credit_strings() self._enable_credit_switch_handlers() # setup switch handlers self.machine.events.add_handler('player_add_request', self._player_add_request) self.machine.events.add_handler('request_to_start_game', self._request_to_start_game) self.machine.events.add_handler('player_add_success', self._player_add_success) self.machine.events.add_handler('mode_game_started', self._game_ended) self.machine.events.add_handler('mode_game_ended', self._game_started) self.machine.events.add_handler('ball_starting', self._ball_starting) if post_event: self.machine.events.post('enabling_credit_play') def enable_free_play(self, post_event=True, **kwargs): self.credits_config['free_play'] = True self.machine.events.remove_handler(self._player_add_request) self.machine.events.remove_handler(self._request_to_start_game) self.machine.events.remove_handler(self._player_add_success) self.machine.events.remove_handler(self._game_ended) self.machine.events.remove_handler(self._game_started) self.machine.events.remove_handler(self._ball_starting) self._disable_credit_switch_handlers() self._update_credit_strings() if post_event: self.machine.events.post('enabling_free_play') def toggle_credit_play(self, **kwargs): if self.credits_config['free_play']: self.enable_credit_play() else: self.enable_free_play() def _player_add_request(self): if (self.machine.get_machine_var('credit_units') >= self.credit_units_per_game): self.log.debug("Received request to add player. Request Approved") return True else: self.log.debug("Received request to add player. Request Denied") self.machine.events.post("not_enough_credits") return False def _request_to_start_game(self): if (self.machine.get_machine_var('credit_units') >= self.credit_units_per_game): self.log.debug("Received request to start game. Request Approved") return True else: self.log.debug("Received request to start game. Request Denied") self.machine.events.post("not_enough_credits") return False def _player_add_success(self, **kwargs): new_credit_units = (self.machine.get_machine_var('credit_units') - self.credit_units_per_game) if new_credit_units < 0: self.log.warning("Somehow credit units went below 0?!? Resetting " "to 0.") new_credit_units = 0 self.machine.set_machine_var('credit_units', new_credit_units) self._update_credit_strings() def _enable_credit_switch_handlers(self): for switch_settings in self.credits_config['switches']: self.machine.switch_controller.add_switch_handler( switch_name=switch_settings['switch'].name, callback=self._credit_switch_callback, callback_kwargs={'value': switch_settings['value'], 'audit_class': switch_settings['type']}) for switch in self.credits_config['service_credits_switch']: self.machine.switch_controller.add_switch_handler( switch_name=switch.name, callback=self._service_credit_callback) def _disable_credit_switch_handlers(self): for switch_settings in self.credits_config['switches']: self.machine.switch_controller.remove_switch_handler( switch_name=switch_settings['switch'].name, callback=self._credit_switch_callback) for switch in self.credits_config['service_credits_switch']: self.machine.switch_controller.remove_switch_handler( switch_name=switch.name, callback=self._service_credit_callback) def _credit_switch_callback(self, value, audit_class): self._add_credit_units(credit_units=value/self.credit_unit) self._audit(value, audit_class) def _service_credit_callback(self): self.log.debug("Service Credit Added") self.add_credit(price_tiering=False) self._audit(1, 'service_credit') def _add_credit_units(self, credit_units, price_tiering=True): self.log.debug("Adding %s credit_units. Price tiering: %s", credit_units, price_tiering) previous_credit_units = self.machine.get_machine_var('credit_units') total_credit_units = credit_units + previous_credit_units # check for pricing tier if price_tiering: self.credit_units_for_pricing_tiers += credit_units bonus_credit_units = 0 for tier_credit_units, bonus in self.pricing_tiers: if self.credit_units_for_pricing_tiers % tier_credit_units == 0: bonus_credit_units += bonus total_credit_units += bonus_credit_units max_credit_units = (self.credits_config['max_credits'] * self.credit_units_per_game) if max_credit_units and total_credit_units > max_credit_units: self.log.debug("Max credits reached") self._update_credit_strings() self.machine.events.post('max_credits_reached') self.machine.set_machine_var('credit_units', max_credit_units) if max_credit_units > previous_credit_units: self.log.debug("Credit units added") self.machine.set_machine_var('credit_units', total_credit_units) self._update_credit_strings() self.machine.events.post('credits_added') def add_credit(self, price_tiering=True): """Adds a single credit to the machine. Args: price_tiering: Boolean which controls whether this credit will be eligible for the pricing tier bonuses. Default is True. """ self._add_credit_units(self.credit_units_per_game, price_tiering) def _reset_pricing_tier_credits(self): if not self.reset_pricing_tier_count_this_game: self.log.debug("Resetting pricing tier credit count") self.credit_units_for_pricing_tiers = 0 self.reset_pricing_tier_count_this_game = True def _ball_starting(self, **kwargs): if self.player.number == 1 and self.player.ball == 2: self._reset_pricing_tier_credits() def _update_credit_strings(self): machine_credit_units = self.machine.get_machine_var('credit_units') whole_num = int(floor(machine_credit_units / self.credit_units_per_game)) numerator = int(machine_credit_units % self.credit_units_per_game) denominator = int(self.credit_units_per_game) if numerator: if whole_num: display_fraction = '{} {}/{}'.format(whole_num, numerator, denominator) else: display_fraction = '{}/{}'.format(numerator, denominator) else: display_fraction = str(whole_num) if self.credits_config['free_play']: display_string = self.credits_config['free_play_string'] else: display_string = '{} {}'.format( self.credits_config['credits_string'], display_fraction) self.machine.set_machine_var('credits_string', display_string) self.machine.set_machine_var('credits_value', display_fraction) self.machine.set_machine_var('credits_whole_num', whole_num) self.machine.set_machine_var('credits_numerator', numerator) self.machine.set_machine_var('credits_denominator', denominator) def _audit(self, value, audit_class): if audit_class not in self.earnings: self.earnings[audit_class] = dict() self.earnings[audit_class]['total_value'] = 0 self.earnings[audit_class]['count'] = 0 self.earnings[audit_class]['total_value'] += value self.earnings[audit_class]['count'] += 1 self.data_manager.save_all(data=self.earnings) def _game_started(self): self.log.debug("Removing credit clearing delays") self.delay.remove('clear_fractional_credits') self.delay.remove('clear_all_credits') def _game_ended(self): if self.credits_config['fractional_credit_expiration_time']: self.log.debug("Adding delay to clear fractional credits") self.delay.add( ms=self.credits_config['fractional_credit_expiration_time'], callback=self._clear_fractional_credits, name='clear_fractional_credits') if self.credits_config['credit_expiration_time']: self.log.debug("Adding delay to clear credits") self.delay.add( ms=self.credits_config['credit_expiration_time'], callback=self.clear_all_credits, name='clear_all_credits') self.reset_pricing_tier_count_this_game = False def _clear_fractional_credits(self): self.log.debug("Clearing fractional credits") credit_units = self.machine.get_machine_var('credit_units') credit_units -= credit_units % self.credit_units_per_game self.machine.set_machine_var('credit_units', credit_units) self._update_credit_strings() def clear_all_credits(self): self.log.debug("Clearing all credits") self.machine.set_machine_var('credit_units', 0) self._update_credit_strings() # The MIT License (MIT) # Copyright (c) 2013-2015 Brian Madden and Gabe Knuth # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE.
class Auditor(object): def __init__(self, machine): """Base class for the auditor. Args: machine: A refence to the machine controller object. """ if 'auditor' not in machine.config: machine.log.debug('"Auditor:" section not found in machine ' 'configuration, so the auditor will not be ' 'used.') return self.log = logging.getLogger('Auditor') self.machine = machine self.machine.auditor = self self.switchnames_to_audit = set() self.enabled = False """Attribute that's viewed by other system components to let them know they should send auditing events. Set this via the enable() and disable() methods. """ self.data_manager = DataManager(self.machine, 'audits') self.machine.events.add_handler('init_phase_4', self._initialize) def __repr__(self): return '<Auditor>' def _initialize(self): # Initializes the auditor. We do this separate from __init__() since # we need everything else to be setup first. config = ''' save_events: list|ball_ended audit: list|None events: list|None player: list|None num_player_top_records: int|10 ''' self.config = Config.process_config(config, self.machine.config['auditor']) self.current_audits = self.data_manager.get_data() if not self.current_audits: self.current_audits = dict() # Make sure we have all the sections we need in our audit dict if 'switches' not in self.current_audits: self.current_audits['switches'] = dict() if 'events' not in self.current_audits: self.current_audits['events'] = dict() if 'player' not in self.current_audits: self.current_audits['player'] = dict() # Make sure we have all the switches in our audit dict for switch in self.machine.switches: if (switch.name not in self.current_audits['switches'] and 'no_audit' not in switch.tags): self.current_audits['switches'][switch.name] = 0 # build the list of switches we should audit self.switchnames_to_audit = {x.name for x in self.machine.switches if 'no_audit' not in x.tags} # Make sure we have all the player stuff in our audit dict if 'player' in self.config['audit']: for item in self.config['player']: if item not in self.current_audits['player']: self.current_audits['player'][item] = dict() self.current_audits['player'][item]['top'] = list() self.current_audits['player'][item]['average'] = 0 self.current_audits['player'][item]['total'] = 0 # Register for the events the auditor needs to do its job self.machine.events.add_handler('game_starting', self.enable) self.machine.events.add_handler('game_ended', self.disable) if 'player' in self.config['audit']: self.machine.events.add_handler('game_ending', self.audit_player) # Enable the shots monitor Shot.monitor_enabled = True self.machine.register_monitor('shots', self.audit_shot) # Add the switches monitor self.machine.switch_controller.add_monitor(self.audit_switch) def audit(self, audit_class, event, **kwargs): """Called to log an auditable event. Args: audit_class: A string of the section we want this event to be logged to. event: A string name of the event we're auditing. **kawargs: Not used, but included since some of the audit events might include random kwargs. """ if audit_class not in self.current_audits: self.current_audits[audit_class] = dict() if event not in self.current_audits[audit_class]: self.current_audits[audit_class][event] = 0 self.current_audits[audit_class][event] += 1 def audit_switch(self, switch_name, state): if state and switch_name in self.switchnames_to_audit: self.audit('switches', switch_name) def audit_shot(self, name, profile, state): self.audit('shots', name) def audit_event(self, eventname, **kwargs): """Registered as an event handlers to log an event to the audit log. Args: eventname: The string name of the event. **kwargs, not used, but included since some types of events include kwargs. """ self.current_audits['events'][eventname] += 1 def audit_player(self, **kwargs): """Called to write player data to the audit log. Typically this is only called at the end of a game. Args: **kwargs, not used, but included since some types of events include kwargs. """ for item in self.config['player']: for player in self.machine.game.player_list: self.current_audits['player'][item]['top'] = ( self._merge_into_top_list( player[item], self.current_audits['player'][item]['top'], self.config['num_player_top_records'])) self.current_audits['player'][item]['average'] = ( ((self.current_audits['player'][item]['total'] * self.current_audits['player'][item]['average']) + self.machine.game.player[item]) / (self.current_audits['player'][item]['total'] + 1)) self.current_audits['player'][item]['total'] += 1 def _merge_into_top_list(self, new_item, current_list, num_items): # takes a list of top integers and a new item and merges the new item # into the list, then trims it based on the num_items specified current_list.append(new_item) current_list.sort(reverse=True) return current_list[0:num_items] def enable(self, **kwags): """Enables the auditor. This method lets you enable the auditor so it only records things when you want it to. Typically this is called at the beginning of a game. Args: **kwargs: No function here. They just exist to allow this method to be registered as a handler for events that might contain keyword arguments. """ if self.enabled: return # this will happen if we get a mid game restart self.log.debug("Enabling the Auditor") self.enabled = True # Register for the events we're auditing if 'events' in self.config['audit']: for event in self.config['events']: self.machine.events.add_handler(event, self.audit_event, eventname=event, priority=2) # Make sure we have an entry in our audit file for this event if event not in self.current_audits['events']: self.current_audits['events'][event] = 0 for event in self.config['save_events']: self.machine.events.add_handler(event, self._save_audits, priority=0) def _save_audits(self, delay_secs=3): self.data_manager.save_all(data=self.current_audits, delay_secs=delay_secs) def disable(self, **kwargs): """Disables the auditor.""" self.log.debug("Disabling the Auditor") self.enabled = False # remove switch and event handlers self.machine.events.remove_handler(self.audit_event) self.machine.events.remove_handler(self._save_audits) for switch in self.machine.switches: if 'no_audit' not in switch.tags: self.machine.switch_controller.remove_switch_handler( switch.name, self.audit_switch, 1, 0)