Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MPF 0.57.3 Release #1834

Merged
merged 18 commits into from
Sep 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mpf/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"""

__version__ = '0.57.2' # Also consider whether MPF-MC pyproject.toml should be updated
__version__ = '0.57.3.dev1' # Also consider whether MPF-MC pyproject.toml should be updated
'''The full version of MPF.'''

__short_version__ = '0.57'
Expand Down
3 changes: 3 additions & 0 deletions mpf/config_spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,7 @@ fast_exp_board:
led_ports: list|subconfig(fast_led_port)|None
led_fade_time: single|ms|0
led_hz: single|float|30
ignore_led_errors: single|bool|false
fast_breakout:
port: single|enum(1,2,3)|
model: single|str|
Expand Down Expand Up @@ -835,6 +836,7 @@ high_score:
award_slide_display_time: single|ms|4s
categories: dict|str:list|
defaults: dict|str:list|None
filler_initials: list|str|None
enter_initials_timeout: single|secs|20s
reverse_sort: list|str|None
reset_high_scores_events: list|event_handler|high_scores_reset,factory_reset
Expand Down Expand Up @@ -1111,6 +1113,7 @@ mpf:
core_modules: ignore
config_players: ignore
device_modules: ignore
min_mpf_version: single|str|None
plugins: ignore
platforms: ignore
paths: ignore
Expand Down
17 changes: 16 additions & 1 deletion mpf/core/machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import threading
from typing import Any, Callable, Dict, List, Set, Optional

from packaging import version
from pkg_resources import iter_entry_points

from mpf._version import __version__
Expand Down Expand Up @@ -368,6 +369,16 @@ def _validate_config(self) -> None:
self.validate_machine_config_section('machine')
self.validate_machine_config_section('game')
self.validate_machine_config_section('mpf')
self._validate_version()

def _validate_version(self):
if not self.config['mpf']['min_mpf_version']:
return
min_version = version.parse(self.config['mpf']['min_mpf_version'])
mpf_version = version.parse(__version__)
if mpf_version < min_version:
raise AssertionError(f'MPF version mismatch. MPF version {mpf_version} found but game config '
f'requires at least {min_version} ')

def validate_machine_config_section(self, section: str) -> None:
"""Validate a config section."""
Expand Down Expand Up @@ -685,7 +696,11 @@ def run(self) -> None:
if not self.initialize_mpf():
return

self.info_log("Starting the main run loop.")
self.info_log("Starting the main run loop with active modes: %s",
self.mode_controller.active_modes)
if not self.modes['attract'] in self.mode_controller.active_modes:
self.warning_log("Attract mode is not active, game will not be playable. "
"Please check your attract mode configuration.")
self._run_loop()

def stop_with_exception(self, exception) -> None:
Expand Down
9 changes: 4 additions & 5 deletions mpf/modes/bonus/code/bonus.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,10 @@ def _bonus_next_item(self):
self._subtotal()
return

# Calling player.vars.get() instead of player.get() bypasses the
# auto-fill zero and will throw if there is no player variable.
# The fallback value of 1 is used for bonus entries that don't use
# a player score, which are multiplied by one to get the bonus.
hits = self.player.vars.get(entry['player_score_entry'], 1)
# If a player_score_entry is provided, use player getattr to get a
# fallback value of zero if the variable is not set. Otherwise
# use 1 as the multiplier for non-player-score bonuses.
hits = self.player[entry['player_score_entry']] if entry['player_score_entry'] else 1
score = entry['score'].evaluate([]) * hits

if (not score and entry['skip_if_zero']) or (score < 0 and entry['skip_if_negative']):
Expand Down
19 changes: 16 additions & 3 deletions mpf/modes/high_score/code/high_score.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Contains the High Score mode code."""
import asyncio
from random import choice

from mpf.core.async_mode import AsyncMode
from mpf.core.player import Player
Expand Down Expand Up @@ -195,7 +196,8 @@ async def _run(self) -> None:
# ask player for initials if we do not know them
if not player.initials:
try:
player.initials = await self._ask_player_for_initials(player, award_names[i], value)
player.initials = await self._ask_player_for_initials(player, award_names[i],
value, category_name)
except asyncio.TimeoutError:
del new_list[i]
# no entry when the player missed the timeout
Expand Down Expand Up @@ -241,7 +243,7 @@ def _assign_vars(self, category_name, player):
return var_dict

# pylint: disable-msg=too-many-arguments
async def _ask_player_for_initials(self, player: Player, award_label: str, value: int) -> str:
async def _ask_player_for_initials(self, player: Player, award_label: str, value: int, category_name: str) -> str:
"""Show text widget to ask player for initials."""
self.info_log("New high score. Player: %s, award_label: %s"
", Value: %s", player, award_label, value)
Expand All @@ -256,7 +258,18 @@ async def _ask_player_for_initials(self, player: Player, award_label: str, value
timeout=self.high_score_config['enter_initials_timeout']
) # type: dict

return event_result["text"] if "text" in event_result else ''
input_initials = event_result["text"] if "text" in event_result else ''

# If no initials were input, some can be randomly chosen from the 'filler_initials' config section
if not input_initials and self.high_score_config["filler_initials"]:
# High scores are stored as an array of [name, score]
existing_initials = [n[0] for n in self.high_scores[category_name]]
unused_initials = [i for i in self.high_score_config["filler_initials"] if i not in existing_initials]
# If there aren't enough to choose something unique, just pick any from the fillers
if not unused_initials:
unused_initials = self.high_score_config["filler_initials"]
input_initials = choice(unused_initials)
return input_initials

async def _show_award_slide(self, player_num, player_name: str, category_name: str, award: str, value: int) -> None:
if not self.high_score_config['award_slide_display_time']:
Expand Down
2 changes: 1 addition & 1 deletion mpf/platforms/fast/communicators/net_neuron.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ def update_switches_from_hw_data(self):
This will silently sync the switch.hw_state. If the logical state changes,
it will process it like any switch change.
"""
for switch in self.machine.switches:
for switch in self.machine.switches.values():
hw_state = self.platform.hw_switch_data[switch.hw_switch.number]

if hw_state != switch.hw_state:
Expand Down
8 changes: 5 additions & 3 deletions mpf/platforms/fast/fast_exp_board.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import asyncio
from base64 import b16decode
from binascii import Error as binasciiError
from importlib import import_module

from packaging import version
Expand Down Expand Up @@ -176,11 +177,12 @@ def update_leds(self):

try:
self.communicator.send_bytes(b16decode(f'{msg_header}{msg}'), log_msg)
except Exception as e:
except binasciiError as e:
self.log.error(
f"Error decoding the following message for board {breakout_address} : {msg_header}{msg}")
self.log.debug("Attempted update that caused this error: %s", dirty_leds)
raise e
self.log.info("Attempted update that caused this error: %s", dirty_leds)
if not self.config['ignore_led_errors']:
raise e

def set_led_fade(self, rate: int) -> None:
"""Set LED fade rate in ms."""
Expand Down
12 changes: 6 additions & 6 deletions mpf/platforms/virtual.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,16 @@ async def get_hw_switch_states(self):

if 'virtual_platform_start_active_switches' in self.machine.config:
initial_active_switches = []
for switch in Util.string_to_list(self.machine.config['virtual_platform_start_active_switches']):
if switch not in self.machine.switches:
if " " in switch:
for switch_name in Util.string_to_list(self.machine.config['virtual_platform_start_active_switches']):
if switch_name not in self.machine.switches.keys():
if " " in switch_name:
self.raise_config_error("MPF no longer supports lists separated by space in "
"virtual_platform_start_active_switches. Please separate "
"switches by comma: {}.".format(switch), 1)
"switches by comma: {}.".format(switch_name), 1)
else:
self.raise_config_error("Switch {} used in virtual_platform_start_active_switches was not "
"found in switches section.".format(switch), 1)
initial_active_switches.append(self.machine.switches[switch].hw_switch.number)
"found in switches section.".format(switch_name), 1)
initial_active_switches.append(self.machine.switches[switch_name].hw_switch.number)

for k in self.hw_switches:
if k in initial_active_switches:
Expand Down
18 changes: 13 additions & 5 deletions mpf/plugins/platform_integration_test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,12 +207,16 @@ async def start_game(self, num_players=1):
async def eject_and_plunge_ball(self, plunger_switch_name, plunger_lane_settle_time=2, **kwargs):
"""Shuffle the trough switches and plunger to simulate an eject."""
del kwargs
self.info_log("Ejecting and plunging ball...")
self.set_switch_sync(self.trough_switches[0], 0)
# Find all the trough switches that currently have balls
active_trough_switches = [s for s in self.trough_switches if self.machine.switches[s].state]
assert len(active_trough_switches), "Unable to eject a ball. Trough is empty."
self.info_log("Ejecting and plunging ball from trough %s to plunger switch %s...",
active_trough_switches[-1], plunger_switch_name)
self.set_switch_sync(active_trough_switches[0], 0)
await asyncio.sleep(0.03)
self.set_switch_sync(self.trough_switches[-1], 0)
self.set_switch_sync(active_trough_switches[-1], 0)
await asyncio.sleep(0.1)
self.set_switch_sync(self.trough_switches[0], 1)
self.set_switch_sync(active_trough_switches[0], 1)
await asyncio.sleep(0.25)
await self.set_switch(plunger_switch_name, 1, duration_secs=plunger_lane_settle_time)
await asyncio.sleep(1)
Expand All @@ -221,7 +225,11 @@ async def move_ball_from_drain_to_trough(self, **kwargs):
"""Move a ball from the drain device to the trough device."""
del kwargs
drain_switches = self.machine.ball_devices.items_tagged('drain')[0].config.get('ball_switches')
self.set_switch_sync(drain_switches[-1], 0)
self.info_log("Found drain switches: %s of type %s", drain_switches, type(drain_switches))
# If there's only one drain switch it might be a single value, rather than a list
drain_switch = drain_switches if isinstance(drain_switches, str) else drain_switches[-1]
self.info_log("Setting drain switch '%s' to zero", drain_switch)
self.set_switch_sync(drain_switch, 0)
await asyncio.sleep(0.25)
self.set_switch_sync(self.trough_switches[-1], 1)
await asyncio.sleep(0.25)
6 changes: 6 additions & 0 deletions mpf/tests/machine_files/bonus/modes/bonus/config/bonus.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,9 @@ mode_settings:
score: 5000
player_score_entry: modes
reset_player_score_entry: False
- event: bonus_undefined_var
score: 5000
skip_if_zero: false
player_score_entry: undefined_var
- event: bonus_static
score: 2000
4 changes: 4 additions & 0 deletions mpf/tests/machine_files/shots/config/test_shot_groups.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ switches:
number:
switch_4:
number:
switch_5:
number:
switch_6:
number:
s_rotate_l:
number:
s_rotate_r:
Expand Down
4 changes: 4 additions & 0 deletions mpf/tests/machine_files/shots/modes/base2/config/base2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ shots:
light: tag1
shot_4:
switch: switch_1
shot_5:
switch: switch_5
shot_6:
switch: switch_6
led_1:
switch: switch_1
show_tokens:
Expand Down
19 changes: 18 additions & 1 deletion mpf/tests/machine_files/shots/modes/mode1/config/mode1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ shots:
mode1_shot_3:
switch: switch_3
profile: mode1_shot_3
mode1_shot_5:
switch: switch_5
profile: mode1_shot_5
mode1_shot_6:
switch: switch_6
profile: mode1_shot_6

shot_profiles:
mode1_shot_2:
Expand All @@ -32,10 +38,21 @@ shot_profiles:
- name: mode1_one
- name: mode1_two
- name: mode1_three
mode1_shot_3:
mode1_shot_3: # Test block: True
show: rainbow2
block: True
states:
- name: mode1_one
- name: mode1_two
- name: mode1_three
mode1_shot_5: # Test block: False
show: rainbow2
block: False
states:
- name: mode1_one
- name: mode1_two
mode1_shot_6: # Test block default
show: rainbow2
states:
- name: mode1_one
- name: mode1_two
32 changes: 26 additions & 6 deletions mpf/tests/test_Bonus.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ def test_slam_tilt_in_service(self):
def testBonus(self):
self.mock_event("bonus_ramps")
self.mock_event("bonus_modes")
self.mock_event("bonus_undefined_var")
self.mock_event("bonus_static")
self.mock_event("bonus_subtotal")
self.mock_event("bonus_multiplier")
self.mock_event("bonus_total")
Expand Down Expand Up @@ -78,10 +80,14 @@ def testBonus(self):
self.assertEqual(3, self._last_event_kwargs["bonus_ramps"]["hits"])
self.assertEqual(10000, self._last_event_kwargs["bonus_modes"]["score"])
self.assertEqual(2, self._last_event_kwargs["bonus_modes"]["hits"])
self.assertEqual(13000, self._last_event_kwargs["bonus_subtotal"]["score"])
self.assertEqual(0, self._last_event_kwargs["bonus_undefined_var"]["score"])
self.assertEqual(0, self._last_event_kwargs["bonus_undefined_var"]["hits"])
self.assertEqual(2000, self._last_event_kwargs["bonus_static"]["score"])
self.assertEqual(1, self._last_event_kwargs["bonus_static"]["hits"])
self.assertEqual(15000, self._last_event_kwargs["bonus_subtotal"]["score"])
self.assertEqual(5, self._last_event_kwargs["bonus_multiplier"]["multiplier"])
self.assertEqual(65000, self._last_event_kwargs["bonus_total"]["score"])
self.assertEqual(66337, self.machine.game.player.score)
self.assertEqual(75000, self._last_event_kwargs["bonus_total"]["score"])
self.assertEqual(76337, self.machine.game.player.score)

# check resets
self.assertEqual(0, self.machine.game.player.ramps)
Expand All @@ -102,10 +108,10 @@ def testBonus(self):
self.assertEqual(0, self._last_event_kwargs["bonus_ramps"]["hits"])
self.assertEqual(10000, self._last_event_kwargs["bonus_modes"]["score"])
self.assertEqual(2, self._last_event_kwargs["bonus_modes"]["hits"])
self.assertEqual(10000, self._last_event_kwargs["bonus_subtotal"]["score"])
self.assertEqual(12000, self._last_event_kwargs["bonus_subtotal"]["score"])
self.assertEqual(5, self._last_event_kwargs["bonus_multiplier"]["multiplier"])
self.assertEqual(50000, self._last_event_kwargs["bonus_total"]["score"])
self.assertEqual(116337, self.machine.game.player.score)
self.assertEqual(60000, self._last_event_kwargs["bonus_total"]["score"])
self.assertEqual(136337, self.machine.game.player.score)

# multiplier should stay the same
self.assertEqual(0, self.machine.game.player.ramps)
Expand All @@ -128,6 +134,8 @@ def testBonus(self):
self.mock_event("bonus_start")
self.mock_event("bonus_ramps")
self.mock_event("bonus_modes")
self.mock_event("bonus_undefined_var")
self.mock_event("bonus_static")
self.mock_event("bonus_subtotal")
self.mock_event("bonus_multiplier")
self.mock_event("bonus_total")
Expand Down Expand Up @@ -157,6 +165,18 @@ def testBonus(self):
self.assertEventNotCalled('bonus_multiplier')
self.assertEventNotCalled('bonus_total')

self.advance_time_and_run(.5)
self.assertEventCalled('bonus_undefined_var')
self.assertEventNotCalled('bonus_subtotal')
self.assertEventNotCalled('bonus_multiplier')
self.assertEventNotCalled('bonus_total')

self.advance_time_and_run(.5)
self.assertEventCalled('bonus_static')
self.assertEventNotCalled('bonus_subtotal')
self.assertEventNotCalled('bonus_multiplier')
self.assertEventNotCalled('bonus_total')

self.advance_time_and_run(.5)
self.assertEventCalled('bonus_subtotal')
self.assertEventNotCalled('bonus_multiplier')
Expand Down
Loading
Loading