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.
- mpf/_version.py +1 -1
- mpf/config_spec.yaml +3 -3
- mpf/core/device_manager.py +14 -1
- mpf/core/events.py +9 -6
- mpf/core/placeholder_manager.py +21 -1
- mpf/core/utility_functions.py +11 -6
- mpf/devices/light_group.py +45 -13
- mpf/devices/magnet.py +1 -1
- mpf/modes/match/code/match.py +15 -5
- mpf/parsers/event_reference_parser.py +10 -6
- mpf/platforms/fast/fast.py +7 -4
- mpf/platforms/fast/fast_led.py +17 -7
- mpf/platforms/pinproc/windows/pinproc.cp313-win32.pyd +0 -0
- mpf/platforms/pinproc/windows/pinproc.cp313-win_amd64.pyd +0 -0
- mpf/platforms/pinproc/windows/pinproc.cp314-win32.pyd +0 -0
- mpf/platforms/pinproc/windows/pinproc.cp314-win_amd64.pyd +0 -0
- mpf/platforms/virtual_pinball/virtual_pinball.py +22 -1
- mpf/tests/machine_files/auditor/config/config.yaml +1 -1
- mpf/tests/machine_files/custom_code/code/test_code.py +4 -0
- mpf/tests/machine_files/event_manager/config/test_event_manager.yaml +6 -0
- mpf/tests/machine_files/event_manager/modes/game_mode/config/game_mode.yaml +5 -0
- mpf/tests/machine_files/light/config/light_groups.yaml +12 -0
- mpf/tests/machine_files/settings/config/config.yaml +27 -0
- mpf/tests/test_CustomCode.py +7 -0
- mpf/tests/test_DeviceManager.py +63 -0
- mpf/tests/test_EventManager.py +45 -0
- mpf/tests/test_Fast_Led.py +61 -0
- mpf/tests/test_LightGroups.py +48 -16
- mpf/tests/test_LightNumbering.py +2 -3
- mpf/tests/test_MatchMode.py +16 -4
- mpf/tests/test_RandomEventPlayer.py +4 -4
- mpf/tests/test_Settings.py +16 -0
- mpf/tests/test_Utility_Functions.py +54 -7
- mpf/tests/test_VPX.py +71 -0
- {mpf-0.58.0.dev1.dist-info → mpf-0.58.0.dev3.dist-info}/METADATA +2 -2
- {mpf-0.58.0.dev1.dist-info → mpf-0.58.0.dev3.dist-info}/RECORD +41 -40
- {mpf-0.58.0.dev1.dist-info → mpf-0.58.0.dev3.dist-info}/WHEEL +1 -1
- mpf/platforms/pinproc/windows/pinproc.cp38-win32.pyd +0 -0
- mpf/platforms/pinproc/windows/pinproc.cp38-win_amd64.pyd +0 -0
- mpf/platforms/pinproc/windows/pinproc.cp39-win32.pyd +0 -0
- mpf/platforms/pinproc/windows/pinproc.cp39-win_amd64.pyd +0 -0
- {mpf-0.58.0.dev1.dist-info → mpf-0.58.0.dev3.dist-info}/entry_points.txt +0 -0
- {mpf-0.58.0.dev1.dist-info → mpf-0.58.0.dev3.dist-info}/licenses/AUTHORS.md +0 -0
- {mpf-0.58.0.dev1.dist-info → mpf-0.58.0.dev3.dist-info}/licenses/LICENSE.md +0 -0
- {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.
|
|
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
|
mpf/core/device_manager.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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):
|
mpf/core/placeholder_manager.py
CHANGED
|
@@ -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."""
|
mpf/core/utility_functions.py
CHANGED
|
@@ -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
|
|
592
|
-
return
|
|
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))
|
mpf/devices/light_group.py
CHANGED
|
@@ -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 =
|
|
99
|
-
|
|
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 =
|
|
109
|
-
|
|
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
|
-
|
|
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']
|
|
214
|
+
if self.config['size'] in ('8digit', '8digit-rg-swapped'):
|
|
184
215
|
count = 120
|
|
185
|
-
elif self.config['size']
|
|
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",
|
|
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."""
|
mpf/modes/match/code/match.py
CHANGED
|
@@ -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(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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)
|
|
35
|
-
|
|
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.
|
|
40
|
+
class_label = str(statement.value.value)
|
|
38
41
|
elif statement.targets[0].id == "config_section":
|
|
39
|
-
config_section = [str(statement.value.
|
|
42
|
+
config_section = [str(statement.value.value)]
|
|
40
43
|
|
|
41
44
|
for y in ast.walk(x):
|
|
42
|
-
if not (isinstance(y, ast.
|
|
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.
|
|
67
|
+
for line in string.value.split('\n'))
|
|
64
68
|
|
|
65
69
|
string = string.replace('Event:', 'event:')
|
|
66
70
|
string = string.replace('Desc:', 'desc:')
|
mpf/platforms/fast/fast.py
CHANGED
|
@@ -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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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)
|
mpf/platforms/fast/fast_led.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
#
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
if brightness < 0:
|
|
132
|
-
brightness
|
|
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
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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,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"
|
mpf/tests/test_CustomCode.py
CHANGED
|
@@ -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'])
|