Ejemplo n.º 1
0
    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)
Ejemplo n.º 2
0
    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'])
Ejemplo n.º 3
0
    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'])
Ejemplo n.º 4
0
    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')
Ejemplo n.º 5
0
    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')
Ejemplo n.º 6
0
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)
Ejemplo n.º 7
0
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)
Ejemplo n.º 8
0
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.
Ejemplo n.º 9
0
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)