mpf 0.58.0.dev1__py3-none-any.whl → 0.58.0.dev3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. mpf/_version.py +1 -1
  2. mpf/config_spec.yaml +3 -3
  3. mpf/core/device_manager.py +14 -1
  4. mpf/core/events.py +9 -6
  5. mpf/core/placeholder_manager.py +21 -1
  6. mpf/core/utility_functions.py +11 -6
  7. mpf/devices/light_group.py +45 -13
  8. mpf/devices/magnet.py +1 -1
  9. mpf/modes/match/code/match.py +15 -5
  10. mpf/parsers/event_reference_parser.py +10 -6
  11. mpf/platforms/fast/fast.py +7 -4
  12. mpf/platforms/fast/fast_led.py +17 -7
  13. mpf/platforms/pinproc/windows/pinproc.cp313-win32.pyd +0 -0
  14. mpf/platforms/pinproc/windows/pinproc.cp313-win_amd64.pyd +0 -0
  15. mpf/platforms/pinproc/windows/pinproc.cp314-win32.pyd +0 -0
  16. mpf/platforms/pinproc/windows/pinproc.cp314-win_amd64.pyd +0 -0
  17. mpf/platforms/virtual_pinball/virtual_pinball.py +22 -1
  18. mpf/tests/machine_files/auditor/config/config.yaml +1 -1
  19. mpf/tests/machine_files/custom_code/code/test_code.py +4 -0
  20. mpf/tests/machine_files/event_manager/config/test_event_manager.yaml +6 -0
  21. mpf/tests/machine_files/event_manager/modes/game_mode/config/game_mode.yaml +5 -0
  22. mpf/tests/machine_files/light/config/light_groups.yaml +12 -0
  23. mpf/tests/machine_files/settings/config/config.yaml +27 -0
  24. mpf/tests/test_CustomCode.py +7 -0
  25. mpf/tests/test_DeviceManager.py +63 -0
  26. mpf/tests/test_EventManager.py +45 -0
  27. mpf/tests/test_Fast_Led.py +61 -0
  28. mpf/tests/test_LightGroups.py +48 -16
  29. mpf/tests/test_LightNumbering.py +2 -3
  30. mpf/tests/test_MatchMode.py +16 -4
  31. mpf/tests/test_RandomEventPlayer.py +4 -4
  32. mpf/tests/test_Settings.py +16 -0
  33. mpf/tests/test_Utility_Functions.py +54 -7
  34. mpf/tests/test_VPX.py +71 -0
  35. {mpf-0.58.0.dev1.dist-info → mpf-0.58.0.dev3.dist-info}/METADATA +2 -2
  36. {mpf-0.58.0.dev1.dist-info → mpf-0.58.0.dev3.dist-info}/RECORD +41 -40
  37. {mpf-0.58.0.dev1.dist-info → mpf-0.58.0.dev3.dist-info}/WHEEL +1 -1
  38. mpf/platforms/pinproc/windows/pinproc.cp38-win32.pyd +0 -0
  39. mpf/platforms/pinproc/windows/pinproc.cp38-win_amd64.pyd +0 -0
  40. mpf/platforms/pinproc/windows/pinproc.cp39-win32.pyd +0 -0
  41. mpf/platforms/pinproc/windows/pinproc.cp39-win_amd64.pyd +0 -0
  42. {mpf-0.58.0.dev1.dist-info → mpf-0.58.0.dev3.dist-info}/entry_points.txt +0 -0
  43. {mpf-0.58.0.dev1.dist-info → mpf-0.58.0.dev3.dist-info}/licenses/AUTHORS.md +0 -0
  44. {mpf-0.58.0.dev1.dist-info → mpf-0.58.0.dev3.dist-info}/licenses/LICENSE.md +0 -0
  45. {mpf-0.58.0.dev1.dist-info → mpf-0.58.0.dev3.dist-info}/top_level.txt +0 -0
mpf/_version.py CHANGED
@@ -10,7 +10,7 @@ PyPI.
10
10
 
11
11
  """
12
12
 
13
- __version__ = '0.58.0.dev1' # Also consider whether MPF-MC pyproject.toml should be updated
13
+ __version__ = '0.58.0.dev3' # Also consider whether MPF-MC pyproject.toml should be updated
14
14
  '''The full version of MPF.'''
15
15
 
16
16
  __short_version__ = '0.58'
mpf/config_spec.yaml CHANGED
@@ -622,7 +622,7 @@ fast:
622
622
  __type__: config
623
623
  net: single|subconfig(fast_net)|None
624
624
  exp: single|subconfig(fast_exp)|None
625
- exp_int: single|subconfig(fast_exp)|None
625
+ exp_int: single|subconfig(fast_exp)|None
626
626
  aud: single|subconfig(fast_aud)|None
627
627
  seg: single|subconfig(fast_seg)|None
628
628
  dmd: single|subconfig(fast_dmd)|None
@@ -944,7 +944,7 @@ neoseg_displays:
944
944
  number_template: single|str|None
945
945
  start_x: single|float|None
946
946
  start_y: single|float|None
947
- size: single|enum(2digit,8digit)|8digit
947
+ size: single|enum(2digit,8digit,2digit-rg-swapped,8digit-rg-swapped)|8digit
948
948
  light_template: single|subconfig(lights,device)|
949
949
  lights:
950
950
  __valid_in__: machine
@@ -1573,7 +1573,7 @@ settings:
1573
1573
  label: single|str|
1574
1574
  sort: single|int|
1575
1575
  values: dict|str:str|
1576
- key_type: single|enum(str,float,int)|str
1576
+ key_type: single|enum(str,float,int,bool)|str
1577
1577
  default: single|str|
1578
1578
  machine_var: single|str|None
1579
1579
  settingType: single|str|standard
@@ -172,7 +172,20 @@ class DeviceManager(MpfController):
172
172
  for device_name in config:
173
173
  futures.append(collection[device_name].device_added_system_wide())
174
174
 
175
- await asyncio.wait([asyncio.create_task(futures) for futures in futures])
175
+ fs = [asyncio.create_task(future) for future in futures]
176
+ done, pending = await asyncio.wait(fs, return_when=asyncio.FIRST_EXCEPTION)
177
+
178
+ # Check if any of the futures returned an exception and throw it
179
+ for task in done:
180
+ if task.cancelled():
181
+ continue
182
+
183
+ exc = task.exception()
184
+ if exc is not None:
185
+ for pending_task in pending:
186
+ pending_task.cancel()
187
+ await asyncio.gather(*pending, return_exceptions=True)
188
+ raise exc
176
189
 
177
190
  # pylint: disable-msg=too-many-nested-blocks
178
191
  def get_device_control_events(self, config) -> Generator[Tuple[str, Callable, int, "Device"], None, None]:
mpf/core/events.py CHANGED
@@ -224,14 +224,15 @@ class EventManager(MpfController):
224
224
  'handler you passed?'.format(handler, event))
225
225
 
226
226
  sig = inspect.signature(handler)
227
- if 'kwargs' not in sig.parameters:
228
- raise AssertionError("Handler {} for event '{}' is missing **kwargs. Actual signature: {}".format(
229
- handler, event, sig))
230
-
231
- if sig.parameters['kwargs'].kind != inspect.Parameter.VAR_KEYWORD:
232
- raise AssertionError("Handler {} for event '{}' param kwargs is missing '**'. "
227
+ catchall_name = next((name for name in ('kwargs', '_kwargs') if name in sig.parameters), None)
228
+ if catchall_name is None:
229
+ raise AssertionError("Handler {} for event '{}' is missing **kwargs. "
233
230
  "Actual signature: {}".format(handler, event, sig))
234
231
 
232
+ if sig.parameters[catchall_name].kind != inspect.Parameter.VAR_KEYWORD:
233
+ raise AssertionError("Handler {} for event '{}' param {} is missing '**'. "
234
+ "Actual signature: {}".format(handler, event, catchall_name, sig))
235
+
235
236
  event, condition, additional_priority = self.get_event_and_condition_from_string(event)
236
237
  priority += additional_priority
237
238
 
@@ -457,6 +458,8 @@ class EventManager(MpfController):
457
458
  _future=future,
458
459
  _keys=keys,
459
460
  event=event_name)))
461
+
462
+ future.add_done_callback(lambda f: [self.remove_handler_by_key(k) for k in keys])
460
463
  return future
461
464
 
462
465
  def _wait_handler(self, _future: asyncio.Future, _keys: List[EventHandlerKey], **kwargs):
@@ -4,7 +4,7 @@ import string
4
4
  import asyncio
5
5
  import operator as op
6
6
  import abc
7
- from functools import lru_cache
7
+ from functools import lru_cache, partial
8
8
 
9
9
  import re
10
10
  from typing import Tuple, List, Any, Union
@@ -860,8 +860,28 @@ class BasePlaceholderManager(MpfController):
860
860
  subscriptions.append(self.machine.wait_for_stop())
861
861
  future = Util.any(subscriptions)
862
862
  future = asyncio.ensure_future(future)
863
+
864
+ future.add_done_callback(
865
+ partial(self._cleanup_placeholder_subscriptions, subscriptions=subscriptions)
866
+ )
863
867
  return value, future
864
868
 
869
+ def _cleanup_placeholder_subscriptions(self, future, subscriptions):
870
+ """Cancel child placeholder subscriptions when the parent template frame finishes."""
871
+ del future
872
+
873
+ for sub in subscriptions:
874
+ if isinstance(sub, asyncio.Future):
875
+ if not sub.done():
876
+ # Cancel outstanding asyncio Future/Task instance
877
+ sub.cancel()
878
+ elif asyncio.iscoroutine(sub):
879
+ try:
880
+ # close raw coroutines (like sleep)
881
+ sub.close()
882
+ except RuntimeWarning:
883
+ pass
884
+
865
885
  @lru_cache(typed=True)
866
886
  def parse_conditional_template(self, template, default_number=None):
867
887
  """Parse a template for condition and number and return a dict."""
@@ -57,6 +57,8 @@ class Util:
57
57
  return float(value)
58
58
  if type_name == "str":
59
59
  return str(value)
60
+ if type_name == "bool":
61
+ return str(value).lower() in ['true', 't', 'yes', 'enable', 'on', '1']
60
62
 
61
63
  raise AssertionError("Unknown type {}".format(type_name))
62
64
 
@@ -586,13 +588,16 @@ class Util:
586
588
  if isinstance(time_string, (int, float)):
587
589
  return int(time_string)
588
590
 
589
- time_string = str(time_string).upper()
591
+ time_string = str(time_string).upper().strip()
590
592
 
591
- if time_string.endswith('MS') or time_string.endswith('MSEC'):
592
- return int(time_string[:-2])
593
+ if time_string == 'NONE':
594
+ return 0
595
+
596
+ if time_string.endswith('MS'):
597
+ return int(float(time_string[:-2]))
593
598
 
594
599
  if time_string.endswith('MSEC'):
595
- return int(time_string[:-4])
600
+ return int(float(time_string[:-4]))
596
601
 
597
602
  if time_string.endswith('D'):
598
603
  return int(float(time_string[:-1]) * 86400 * 1000)
@@ -609,7 +614,7 @@ class Util:
609
614
  if time_string.endswith('SEC'):
610
615
  return int(float(time_string[:-3]) * 1000)
611
616
 
612
- return int(time_string)
617
+ return int(float(time_string))
613
618
 
614
619
  @staticmethod
615
620
  def string_to_secs(time_string: str) -> float:
@@ -847,4 +852,4 @@ class Util:
847
852
  bool: True if the bit is set, False otherwise
848
853
  """
849
854
  num = int(hex_string, 16)
850
- return bool(num & (1 << bit))
855
+ return bool(num & (1 << bit))
@@ -14,6 +14,21 @@ from mpf.core.system_wide_device import SystemWideDevice
14
14
  from mpf.devices.light import Light
15
15
 
16
16
 
17
+ NEOSEG_8DIGIT_ORDER = [
18
+ 95, 90, 93, 82, 85, 89, 86, 91, 88, 87, 81, 92, 83, 84, 94,
19
+ 104, 76, 79, 96, 99, 103, 100, 77, 102, 101, 75, 78, 97, 98, 80,
20
+ 110, 105, 108, 67, 70, 74, 71, 106, 73, 72, 66, 107, 68, 69, 109,
21
+ 119, 61, 64, 111, 114, 118, 115, 62, 117, 116, 60, 63, 112, 113, 65,
22
+ 5, 0, 3, 52, 55, 59, 56, 1, 58, 57, 51, 2, 53, 54, 4,
23
+ 14, 46, 49, 6, 9, 13, 10, 47, 12, 11, 45, 48, 7, 8, 50,
24
+ 20, 15, 18, 37, 40, 44, 41, 16, 43, 42, 36, 17, 38, 39, 19,
25
+ 29, 31, 34, 21, 24, 28, 25, 32, 27, 26, 30, 33, 22, 23, 35]
26
+
27
+ NEOSEG_2DIGIT_ORDER = [
28
+ 5, 0, 3, 22, 25, 29, 26, 1, 28, 27, 21, 2, 23, 24, 4,
29
+ 14, 16, 19, 6, 9, 13, 10, 17, 12, 11, 15, 18, 7, 8, 20]
30
+
31
+
17
32
  class LightGroup(SystemWideDevice):
18
33
 
19
34
  """An abstract group of lights."""
@@ -92,25 +107,41 @@ class LightGroup(SystemWideDevice):
92
107
  for light in self.lights:
93
108
  light.color(color, fade_ms, priority, key)
94
109
 
110
+ def _swap_grb_rgb_channel_offsets(self, offsets):
111
+ """Swap every first and second out of sets of three (GRB->RGB)."""
112
+ adjustments = {0: 1, 1: -1, 2: 0}
113
+ return [
114
+ x + adjustments[x % 3]
115
+ for x in offsets
116
+ ]
117
+
95
118
  def _reorder_lights(self):
96
119
  if self.config['size'] == '8digit':
97
120
  #Magic order for 8 digit NeoSeg displays from CobraPin
98
- order = [95, 90, 93, 82, 85, 89, 86, 91, 88, 87, 81, 92, 83, 84, 94,
99
- 104, 76, 79, 96, 99, 103, 100, 77, 102, 101, 75, 78, 97, 98, 80,
100
- 110, 105, 108, 67, 70, 74, 71, 106, 73, 72, 66, 107, 68, 69, 109,
101
- 119, 61, 64, 111, 114, 118, 115, 62, 117, 116, 60, 63, 112, 113, 65,
102
- 5, 0, 3, 52, 55, 59, 56, 1, 58, 57, 51, 2, 53, 54, 4,
103
- 14, 46, 49, 6, 9, 13, 10, 47, 12, 11, 45, 48, 7, 8, 50,
104
- 20, 15, 18, 37, 40, 44, 41, 16, 43, 42, 36, 17, 38, 39, 19,
105
- 29, 31, 34, 21, 24, 28, 25, 32, 27, 26, 30, 33, 22, 23, 35]
121
+ order = NEOSEG_8DIGIT_ORDER
122
+
106
123
  elif self.config['size'] == '2digit':
107
124
  #Magic order for 2 digit NeoSeg displays from CobraPin
108
- order = [5, 0, 3, 22, 25, 29, 26, 1, 28, 27, 21, 2, 23, 24, 4,
109
- 14, 16, 19, 6, 9, 13, 10, 17, 12, 11, 15, 18, 7, 8, 20]
125
+ order = NEOSEG_2DIGIT_ORDER
126
+
127
+ elif self.config['size'] == '8digit-rg-swapped':
128
+ #Magic order for 8 digit NeoSeg displays from CobraPin when platform numbering uses RGB channel order
129
+ order = self._swap_grb_rgb_channel_offsets(NEOSEG_8DIGIT_ORDER)
130
+
131
+ elif self.config['size'] == '2digit-rg-swapped':
132
+ #Magic order for 2 digit NeoSeg displays from CobraPin when platform numbering uses RGB channel order
133
+ order = self._swap_grb_rgb_channel_offsets(NEOSEG_2DIGIT_ORDER)
110
134
  else:
111
135
  order = range(0, len(self.lights))
112
136
 
113
- self.lights = [self.lights[i] for i in order]
137
+ mapped_lights = []
138
+ try:
139
+ for i in order:
140
+ mapped_lights.append(self.lights[i])
141
+ self.lights = mapped_lights
142
+ except IndexError:
143
+ self.error_log("Attempted to use light index %s but source list only had %s items", i, len(self.lights))
144
+ raise
114
145
 
115
146
  def wait_for_loaded(self):
116
147
  """Return future."""
@@ -180,11 +211,12 @@ class NeoSegDisplay(LightGroup):
180
211
  __slots__ = [] # type: List[str]
181
212
 
182
213
  def _create_lights(self):
183
- if self.config['size'] == '8digit':
214
+ if self.config['size'] in ('8digit', '8digit-rg-swapped'):
184
215
  count = 120
185
- elif self.config['size'] == '2digit':
216
+ elif self.config['size'] in ('2digit', '2digit-rg-swapped'):
186
217
  count = 30
187
218
  else:
219
+ self.warning_log("NeoSegDisplay %s could not determine number of lights to create.", self.name)
188
220
  count = 0
189
221
 
190
222
  for index in range(self.config['number_start'], self.config['number_start'] + count):
mpf/devices/magnet.py CHANGED
@@ -9,7 +9,7 @@ from mpf.core.device_monitor import DeviceMonitor
9
9
  from mpf.core.system_wide_device import SystemWideDevice
10
10
 
11
11
 
12
- @DeviceMonitor(_active="active", _release_in_progress="release_in_progress", _enabled="enabled")
12
+ @DeviceMonitor(_active="active", _release_in_progress="release_in_progress", enabled="enabled")
13
13
  class Magnet(EnableDisableMixinSystemWideDevice, SystemWideDevice):
14
14
 
15
15
  """Controls a playfield magnet in a pinball machine."""
@@ -18,10 +18,11 @@ class Match(AsyncMode):
18
18
  """Initialize match mode."""
19
19
  super().__init__(*args, **kwargs)
20
20
  # add setting
21
- self.machine.settings.add_setting(SettingEntry("match_percentage", "Match Percentage", 500,
22
- "match_percentage", 10,
23
- {0: "Never", 2: "2%", 5: "5%", 10: "10%", 15: "15%", 30: "30%",
24
- 50: "50%"}, "standard"))
21
+ self.machine.settings.add_setting(
22
+ SettingEntry("match_percentage",
23
+ "Match Percentage", 500, "match_percentage", 10,
24
+ {0: "Never", 2: "2%", 5: "5%", 10: "10%", 15: "15%", 30: "30%", 50: "50%"},
25
+ "standard"))
25
26
 
26
27
  def _get_match_numbers(self):
27
28
  """Calculate match numbers."""
@@ -66,7 +67,12 @@ class Match(AsyncMode):
66
67
  self.machine.variables.set_machine_var('match_number', winner_number)
67
68
 
68
69
  for i in range(0, self.machine.game.max_players):
69
- event_args["match_number{}".format(i)] = match_numbers[i] if len(match_numbers) > i else ""
70
+ player_value = ""
71
+ if len(match_numbers) > i:
72
+ player_value = match_numbers[i]
73
+
74
+ event_args["match_number{}".format(i)] = player_value
75
+ event_args["match_number{}_won".format(i)] = player_value == winner_number
70
76
 
71
77
  if not winners:
72
78
  # no winner
@@ -80,7 +86,9 @@ class Match(AsyncMode):
80
86
  winners: Number of winners (always 0 here)
81
87
  match_number0: Match number for player 0
82
88
  match_number1: Match number for player 1
89
+ match_number1_won: True/False for if player 1 matched
83
90
  match_numberX: Match number for player X (up to max players)
91
+ match_numberX_won: T/F for if player X matched (up to max players)
84
92
  '''
85
93
  else:
86
94
  # we got a winner
@@ -94,6 +102,8 @@ class Match(AsyncMode):
94
102
  winners: Number of winners (always more than 0 here)
95
103
  match_number0: Match number for player 0
96
104
  match_number1: Match number for player 1
105
+ match_number1_won: True/False for if player 1 matched
97
106
  match_numberX: Match number for player X (up to max players)
107
+ match_numberX_won: T/F for if player X matched (up to max players)
98
108
  '''
99
109
  # that is it. credits mode should hook into the match_has_match event and award credits
@@ -31,15 +31,19 @@ class EventReferenceParser:
31
31
  class_label = None
32
32
  config_section = None
33
33
  for statement in x.body:
34
- if isinstance(statement, ast.Assign) and len(statement.targets) == 1 and \
35
- isinstance(statement.targets[0], ast.Name) and isinstance(statement.value, ast.Str):
34
+ if (isinstance(statement, ast.Assign)
35
+ and len(statement.targets) == 1
36
+ and isinstance(statement.targets[0], ast.Name)
37
+ and isinstance(statement.value, ast.Constant)
38
+ and isinstance(statement.value.value, str)):
36
39
  if statement.targets[0].id == "class_label":
37
- class_label = str(statement.value.s)
40
+ class_label = str(statement.value.value)
38
41
  elif statement.targets[0].id == "config_section":
39
- config_section = [str(statement.value.s)]
42
+ config_section = [str(statement.value.value)]
40
43
 
41
44
  for y in ast.walk(x):
42
- if not (isinstance(y, ast.Str) and (y.s.strip().lower().startswith('event:'))):
45
+ if not (isinstance(y, ast.Constant) and isinstance(y.value, str)
46
+ and y.value.strip().lower().startswith('event:')):
43
47
  continue
44
48
 
45
49
  event_dict = self._parse_string(y)
@@ -60,7 +64,7 @@ class EventReferenceParser:
60
64
 
61
65
  def _parse_string(self, string):
62
66
  string = '\n'.join(' '.join(line.split())
63
- for line in string.s.split('\n'))
67
+ for line in string.value.split('\n'))
64
68
 
65
69
  string = string.replace('Event:', 'event:')
66
70
  string = string.replace('Desc:', 'desc:')
@@ -593,10 +593,13 @@ class FastHardwarePlatform(ServoPlatform, LightsPlatform, RgbDmdPlatform,
593
593
  return FASTMatrixLight(number, self.serial_connections['net'], self.machine,
594
594
  int(1 / self.config['net']['lamp_hz'] * 1000), self)
595
595
  if not subtype or subtype == "led":
596
- # make everything lowercase and strip trailing channel number
597
- parts, channel = number.lower().rsplit('-', 1)
598
- # split into board name, breakout, port, led
599
- parts = parts.split('-')
596
+ try:
597
+ # make everything lowercase and strip trailing channel number
598
+ parts, channel = number.lower().rsplit('-', 1)
599
+ # split into board name, breakout, port, led
600
+ parts = parts.split('-')
601
+ except ValueError as e:
602
+ raise ValueError(f"Unable to parse FAST light '{config.name}' address {number}") from e
600
603
 
601
604
  if parts[0] in self.exp_boards_by_name: # this is an expansion board LED in config file format
602
605
  return self._add_exp_led_with_config_format(parts, channel, config.name)
@@ -40,7 +40,11 @@ class FASTRGBLED:
40
40
  channel = self.channels[index]
41
41
  if channel:
42
42
  brightness, _, done = channel.get_fade_and_brightness(current_time)
43
- result += f'{int(brightness * 255):02X}'
43
+ # Clamp to a single byte: an out-of-range brightness here would
44
+ # format to a 1- or 3-char hex channel and crash the downstream
45
+ # binascii unpacking with an "Odd-length string" error.
46
+ clamped_brightness = min(255, max(0, int(brightness * 255)))
47
+ result += f'{clamped_brightness:02X}'
44
48
  if not done:
45
49
  self.dirty = True
46
50
  else:
@@ -116,6 +120,12 @@ class FASTLEDChannel(LightPlatformInterface):
116
120
  fade_ms = max_fade_ms
117
121
  ratio = ((current_time + (fade_ms / 1000.0) - start_time) /
118
122
  (target_time - start_time))
123
+ # A fade's start_time and the current_time passed in come from
124
+ # separate clock reads, so current_time can land a hair before
125
+ # start_time, making this ratio slightly out of [0, 1]. That would
126
+ # push brightness below 0 (fade up) or above 1 (fade down); clamp
127
+ # the ratio so the interpolation stays in range for both directions.
128
+ ratio = max(0.0, min(1.0, ratio))
119
129
  brightness = start_brightness + (target_brightness - start_brightness) * ratio
120
130
  done = False
121
131
  else:
@@ -125,15 +135,15 @@ class FASTLEDChannel(LightPlatformInterface):
125
135
  self._last_brightness = brightness
126
136
  done = True
127
137
 
128
- # There is a bug that can sometimes cause the start_time to be ahead of the current_time,
129
- # resulting in a negative brightness value. This may be a floating-point rounding error,
130
- # or maybe something else. I can't figure it out, so just floor the value to be non-negative.
131
- if brightness < 0:
132
- brightness = 0
133
- self.led.log.warning("Calculated a negative brightness (%s) for led %s channel %s. current_time: %s "
138
+ # Final guard in case start/target brightness themselves are ever out
139
+ # of [0, 1]: an out-of-range value would crash the hex formatter in
140
+ # current_color, so clamp both ends.
141
+ if brightness < 0 or brightness > 1:
142
+ self.led.log.warning("Calculated an out-of-range brightness (%s) for led %s channel %s. current_time: %s "
134
143
  "start_brightness: %s, start_time: %s, target_brightness: %s, target_time: %s",
135
144
  brightness, self.led, self.channel, current_time, start_brightness, start_time,
136
145
  target_brightness, target_time)
146
+ brightness = max(0.0, min(1.0, brightness))
137
147
 
138
148
  return brightness, fade_ms, done
139
149
 
@@ -1,5 +1,6 @@
1
1
  """VPX platform."""
2
2
  import asyncio
3
+ import inspect
3
4
  from typing import Dict, List, Optional
4
5
 
5
6
  import logging
@@ -159,7 +160,7 @@ class VirtualPinballPlatform(LightsPlatform, SwitchPlatform, DriverPlatform, Seg
159
160
 
160
161
  """VPX platform."""
161
162
 
162
- __slots__ = ["_lights", "_switches", "_drivers", "_last_drivers", "_last_lights",
163
+ __slots__ = ["_lights", "_switches", "_drivers", "_last_drivers", "_last_lights",
163
164
  "_started", "rules", "_configured_segment_displays", "_last_segment_text"]
164
165
 
165
166
  def __init__(self, machine):
@@ -200,6 +201,8 @@ class VirtualPinballPlatform(LightsPlatform, SwitchPlatform, DriverPlatform, Seg
200
201
 
201
202
  try:
202
203
  result = method(**kwargs)
204
+ if inspect.iscoroutine(result):
205
+ result = await result
203
206
  # pylint: disable-msg=broad-except
204
207
  except Exception as e:
205
208
  self.machine.bcp.transport.send_to_client(client, "vpcom_bridge_response",
@@ -212,6 +215,24 @@ class VirtualPinballPlatform(LightsPlatform, SwitchPlatform, DriverPlatform, Seg
212
215
  self._started.set()
213
216
  return True
214
217
 
218
+ async def vpx_reset(self):
219
+ """Wipe platform-side switch mirror and reset the machine.
220
+
221
+ Called by the VPX plugin on every (re)connect to give VPX a clean
222
+ MPF to talk to. Mechanisms (servos, motors, drop targets, diverters)
223
+ auto-reset because they subscribe to machine_reset_phase_3.
224
+
225
+ Order matters: clear switches first so reset-phase handlers see the
226
+ cleaned state instead of stale readings from the prior VPX session.
227
+ """
228
+ for num, sw in list(self._switches.items()):
229
+ if sw.state:
230
+ sw.state = False
231
+ self.machine.switch_controller.process_switch_by_num(
232
+ state=0, num=num, platform=self)
233
+ await self.machine.reset()
234
+ return True
235
+
215
236
  def vpx_get_switch(self, number):
216
237
  """Return switch value."""
217
238
  # pylint: disable-msg=no-else-return
@@ -6,7 +6,7 @@ game:
6
6
  player_vars:
7
7
  score:
8
8
  initial_value: 0
9
- my_var:
9
+ my_var:
10
10
  initial_value: 0
11
11
 
12
12
  auditor:
@@ -6,7 +6,11 @@ class TestCustomCode(CustomCode):
6
6
  self.log.debug("Loaded!")
7
7
 
8
8
  self.machine.events.add_handler('test_event', self._update)
9
+ self.machine.events.add_handler('test_event_2', self._alternative_kwargs)
9
10
 
10
11
  def _update(self, **kwargs):
11
12
  del kwargs
12
13
  self.machine.events.post("test_response")
14
+
15
+ def _alternative_kwargs(self, **_kwargs):
16
+ self.machine.events.post("test_response_2")
@@ -7,6 +7,12 @@ event_player:
7
7
  test_event_player_delayed:
8
8
  - test_event_player2|2s
9
9
  - test_event_player3:2s
10
+ "{machine.lifetime_earnings >= 500}": never_happens_machine
11
+ "{machine.switches.s_test_1.state == 1}": sometimes_happens_switch
12
+
13
+ switches:
14
+ s_test_1:
15
+ number:
10
16
 
11
17
  random_event_player:
12
18
  test_random_event_player1:
@@ -3,6 +3,11 @@ mode:
3
3
  start_events: game_mode_start
4
4
  stop_events: game_mode_end
5
5
 
6
+ event_player:
7
+ "{players[0].score>=99999999}": never_happens_players
8
+ "{current_player.score>=999999999}": never_happens_player
9
+ "{current_player.score<=current_player.score + 1 and current_player.score > -1}": always_happens_multiple_ast
10
+
6
11
  random_event_player:
7
12
  test_random_event_player_mode2:
8
13
  - out1
@@ -50,3 +50,15 @@ neoseg_displays:
50
50
  light_template:
51
51
  type: w
52
52
  subtype: led
53
+ neoSeg_2:
54
+ start_channel: 0-1-0
55
+ size: 8digit-rg-swapped
56
+ light_template:
57
+ type: w
58
+ subtype: led
59
+ neoSeg_3:
60
+ start_channel: 0-1-120
61
+ size: 2digit-rg-swapped
62
+ light_template:
63
+ type: w
64
+ subtype: led
@@ -19,3 +19,30 @@ settings:
19
19
  zero: "Zero"
20
20
  one: "One"
21
21
  two: "Two"
22
+ custom_setting_float:
23
+ label: "Float Setting"
24
+ key_type: float
25
+ default: 0.1
26
+ sort: 3
27
+ values:
28
+ 0: "Zero"
29
+ 0.1: "Point One"
30
+ 1.0: "One"
31
+ 2.5: "Two Point Five"
32
+ custom_setting_bool_t:
33
+ label: "Boolean Setting Default True"
34
+ key_type: bool
35
+ default: true
36
+ sort: 4
37
+ values:
38
+ true: "yes"
39
+ false: "off"
40
+
41
+ custom_setting_bool_f:
42
+ label: "Boolean Setting Default False"
43
+ key_type: bool
44
+ default: false
45
+ sort: 5
46
+ values:
47
+ true: "on"
48
+ false: "no"
@@ -15,3 +15,10 @@ class TestCustomCode(MpfTestCase):
15
15
  self.machine_run()
16
16
 
17
17
  self.assertEqual(1, self._events['test_response'])
18
+
19
+ def test_scoring_2(self):
20
+ self.mock_event("test_response_2")
21
+ self.post_event("test_event_2")
22
+ self.machine_run()
23
+
24
+ self.assertEqual(1, self._events['test_response_2'])