forked from mjocean/proc-GenericLauncher
-
Notifications
You must be signed in to change notification settings - Fork 0
/
GameLauncher.py
505 lines (432 loc) · 20.4 KB
/
GameLauncher.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
# GameLauncher
#
# An all-in-one front-end for PyProcGame to launch games from
# your P-ROC enabled Williams pinball machine. Use the yaml
# to define games to launch pinmame (Williams, FreeWPC), procgame
# or PyProcGame games
#
# Authors:
# Tom Collins (tomlogic)
# Michael Ocean (mocean)
# Based on the F-14 Loader by Mark (Snux) which was based on
# Original code from Jim (myPinballs) and Koen (DutchPinball)
#
# This is intended to be a one-size-fits-all launcher; in theory all
# you need to supply is a Loader.yaml and this launcher will work.
# Everything is smashed into this single file (which may
# not be terribly 'Pythonic', but that's not the point...)
# Honestly, Mark Jim and Koen did the hard part by writing the original
# launching code (starting and stopping the P-ROC). I haven't done much
# other than moving everything into one file and moving all of the
# would-be options out of the code and into a yaml file.
#
# Clearly none of this would make any sense without the brilliant work
# of Gerry Stellenberg and Adam Premble
#
# This is a work in progress.
# Please report issues on the PinballControllers forum.
#
from procgame import *
from procgame.dmd import font_named
import os
import shutil
import sys
import locale
import yaml
# use py-pinmame-nvram project to extract high scores from PinMAME .nv files
sys.path.append('py-pinmame-nvmaps')
from nvram_parser import ParseNVRAM
# Loader specific file is required; and expected to live in the same
# directory. Gross to load it globally? Sure.
loader_config_path = 'Loader.yaml'
loaderconfig = None
def format_score(number):
if sys.version_info >= (2,7,0):
return '{0:,}'.format(number)
s = '%d' % number
groups = []
while s and s[-1].isdigit():
groups.append(s[-3:])
s = s[:-3]
return s + ','.join(reversed(groups))
def getnested(node, key):
""" Helper function to access a deep element of a dict/list using dot notation. """
keys = key.split('.')
try:
for key in keys:
if isinstance(node, list):
key = int(key)
node = node[key]
return node
except TypeError:
pass
class Loader(game.Mode):
"""
A Mode derived class to actually deal with prompting for a player's
game selection and actually loading the game
"""
def __init__(self, game, priority):
super(Loader, self).__init__(game, priority)
global loaderconfig
self.game_index = 0 # number of selected game (indexed from 0)
self.config_index = 0 # number of selected game's configuration
self.configs = None
self.selected_config = None
self.selected_game = None
# pull out some config values that we know we'll need up
# front. Should any of these fail, it's OK to fail loudly
self.title = loaderconfig['title']
self.instructions_line_1 = loaderconfig['instructions_line_1']
self.instructions_line_2 = loaderconfig['instructions_line_2']
self.games = loaderconfig['games']
self.pinmame_path = loaderconfig['pinmame']['path']
self.pinmame_cmd = loaderconfig['pinmame']['cmd']
self.pinmame_nvram = loaderconfig['pinmame']['nvram']
self.python_path = loaderconfig['python']['cmdpath']
# extra args for pinmame are totally optional
self.pinmame_extra_args = ""
if loaderconfig['pinmame'].has_key('extra_args'):
self.pinmame_extra_args = loaderconfig['pinmame']['extra_args']
# print(self.games)
self.reset()
def reset(self):
# if changing this font, adjust the row as necessary to avoid clipping top of title
self.title_layer = dmd.TextLayer(64, -2, font_named('Font_CC_12px_az.dmd'), "center", opaque=False)
font = font_named("04B-03-7px.dmd")
self.text1_layer = dmd.TextLayer(64, 0, font, "center", opaque=False)
self.text2_layer = dmd.TextLayer(64, 8, font, "center", opaque=False)
self.text3_layer = dmd.TextLayer(64, 16, font, "center", opaque=False)
self.text4_layer = dmd.TextLayer(64, 24, font, "center", opaque=False)
self.layer = dmd.GroupedLayer(128, 32, [self.text4_layer, self.text3_layer,
self.text2_layer, self.text1_layer, self.title_layer])
def mode_started(self):
self.show_title()
self.delay(name='gi_dim', event_type=None, delay=1.0, handler=self.gi_dim)
def mode_tick(self):
pass
def nvram_path(self):
if self.selected_config.has_key('filename'):
basename = self.selected_config['filename']
else:
basename = self.selected_game['ROM']
return self.pinmame_nvram + basename + '.nv'
# Return a string with the Grand Champion initials and score for the
# selected configuration of the selected game.
def load_gc(self):
scores = []
score_file = None
if self.selected_config.has_key('fileprefix'):
score_file = self.selected_game['gamepath'] + self.selected_config['fileprefix'] \
+ '-scores.yaml'
elif self.selected_game.has_key('scores'):
score_file = self.selected_game['gamepath'] + self.selected_game['scores']
if score_file is not None:
try:
gcdict = self.selected_game['gc']
yamlfile = open(score_file, 'r')
scores_yaml = yaml.load(yamlfile)
yamlfile.close()
initials = getnested(scores_yaml, gcdict['initials'])
score = int(getnested(scores_yaml, gcdict['score']))
scores.append('GC: ' + initials + ' ' + format_score(score))
except Exception:
# ignore exceptions (like when nvram file doesn't exist yet)
pass
else:
p = ParseNVRAM(None, None);
p.load_nvram(self.nvram_path())
if self.selected_game.has_key('nv_json'):
p.load_json(self.selected_game['nv_json']);
scores = p.high_scores(short_labels = True)
if p.nv_json.has_key('mode_champions'):
scores.extend(p.high_scores(section = 'mode_champions',
short_labels = True))
lp = p.last_played()
if lp is not None:
scores.append("Last played: " + lp)
elif self.selected_game.has_key('gc'):
gcdict = self.selected_game['gc']
score = 'GC:'
if gcdict.has_key('initials'):
score += ' ' + p.format(
{'encoding': 'ch', 'start': gcdict['initials'],
'length': 3})
if gcdict.has_key('score') and gcdict.has_key('bcd_bytes'):
score += ' ' + p.format(
{'encoding': 'bcd', 'start': gcdict['score'],
'length': gcdict['bcd_bytes']})
scores = [score]
return scores
def show_title(self):
self.title_layer.set_text(self.title)
self.text3_layer.set_text(self.instructions_line_1)
self.text4_layer.set_text(self.instructions_line_2)
self.layer = dmd.GroupedLayer(128, 32, [self.text4_layer, self.text3_layer,
self.title_layer])
self.selected_game = None
def gi_enable(self, dim=False):
global loaderconfig
if loaderconfig.has_key('gi_enable'):
for lamp in loaderconfig['gi_enable']:
if dim:
self.game.lamps[lamp].patter(on_time=1, off_time=2)
else:
self.game.lamps[lamp].enable()
if loaderconfig.has_key('lampshow'):
if dim:
self.game.lampctrl.stop_show()
else:
if not self.game.lampctrl.show_playing:
self.game.lampctrl.register_show('lampshow', loaderconfig['lampshow'])
self.game.lampctrl.play_show('lampshow', repeat=True)
if not dim:
self.cancel_delayed('gi_dim')
self.delay(name='gi_dim', event_type=None, delay=60.0, handler=self.gi_dim)
def gi_dim(self):
self.gi_enable(dim=True)
def show_next_game(self,direction=0):
self.gi_enable(dim=False)
if self.selected_game is None:
# from title, go to either 0 (first) or -1 (last) game/config
self.game_index = self.config_index = 0 if direction > 0 else -1
if self.game.lamps.has_key('startButton'):
self.game.lamps.startButton.schedule(schedule=0xff00ff00, cycle_seconds=0, now=False)
else:
self.config_index += direction
# If the user goes "left" of the first config, roll to the last
# config of the previous game
if self.config_index == -1:
self.game_index -= 1
# note that we need to load the configs for this new game_index
# before we can set config_index to the last configuration
# If we have gone past the last config, roll over to the first
# configuration of the next game.
elif self.config_index == len(self.configs):
self.game_index += 1
self.config_index = 0
# use the modulus operator to wrap-around game_index
self.game_index = self.game_index % len(self.games)
self.selected_game = self.games[self.game_index]
if self.selected_game.has_key('configuration'):
self.configs = self.selected_game['configuration']
else:
self.configs = [{'description': 'default configuration'}]
if self.config_index == -1:
# We can now select the last configuration of the newly-selected game.
self.config_index = len(self.configs) - 1
self.selected_config = self.configs[self.config_index]
# Update the DMD screen to describe the current game/config option.
self.text1_layer.set_text(self.selected_game['line1'])
self.text2_layer.set_text(self.selected_game['line2'])
self.text3_layer.set_text(self.selected_config['description'])
scores = self.load_gc()
if len(scores) == 0:
scores = ['']
layer_script = []
for s in scores:
l = dmd.TextLayer(64, 24, font_named("04B-03-7px.dmd"), "center", opaque=False)
l.set_text(s)
layer_script.append({'seconds':2.5,
'layer':dmd.GroupedLayer(128, 32, [l, self.text3_layer,
self.text2_layer, self.text1_layer])})
self.layer = dmd.ScriptedLayer(width=128, height=32, script=layer_script)
def sw_startButton_active(self, sw):
global loaderconfig
# Ignore start button if we're on the title/startup screen.
if self.selected_game is None:
return
if self.selected_game.has_key('ROM'):
# args for launching PinMAME directly
machine_config = loaderconfig['machine_config_file']
if self.selected_game.has_key('machine_config_file'):
machine_config = self.selected_game['machine_config_file']
args = self.selected_game['ROM'] + " -p-roc " \
+ machine_config + " " + self.pinmame_extra_args
# args for launching PinMAME via a shell script (runpinmame) or batch file
# args = self.selected_game['ROM'] + " " + machine_config
pinmame_nvfile = self.pinmame_nvram + self.selected_game['ROM'] + '.nv'
config_nvfile = None
if self.selected_config.has_key('filename'):
config_nvfile = self.pinmame_nvram + self.selected_config['filename'] + '.nv'
if os.path.isfile(config_nvfile):
# copy configuration's nvram file to PinMAME's version of the file
print 'Copying ' + config_nvfile + ' to ' + pinmame_nvfile
shutil.copyfile(config_nvfile, pinmame_nvfile)
# actually run PinMAME
self.launch_ext(self.pinmame_cmd, args, self.pinmame_path)
# upon return, copy the modified nvram file back (if necessary)
if config_nvfile is not None:
# copy PinMAME's version of the file back to the configuration's file
print 'Copying ' + pinmame_nvfile + ' back to ' + config_nvfile
shutil.copyfile(pinmame_nvfile, config_nvfile)
else:
config_settings = None
config_scores = None
settings_yaml = None
scores_yaml = None
# copy configuration's settings and scores YAML files to the game's path
try:
settings_yaml = self.selected_game['gamepath'] + self.selected_game['settings']
config_settings = self.selected_game['gamepath'] + self.selected_config['fileprefix'] \
+ '-settings.yaml'
if os.path.isfile(config_settings):
print 'Copying ' + config_settings + ' to ' + settings_yaml
shutil.copyfile(config_settings, settings_yaml)
except Exception as detail:
# ignore exceptions (like when there aren't files to copy)
print "failed to copy config settings: ", detail
try:
scores_yaml = self.selected_game['gamepath'] + self.selected_game['scores']
config_scores = self.selected_game['gamepath'] + self.selected_config['fileprefix'] \
+ '-scores.yaml'
if os.path.isfile(config_scores):
print 'Copying ' + config_scores + ' to ' + scores_yaml
shutil.copyfile(config_scores, scores_yaml)
except Exception as detail:
# ignore exceptions (like when there aren't files to copy)
print "failed to copy config scores: ", detail
self.launch_ext(self.python_path,self.selected_game['gamefile'],self.selected_game['gamepath'])
# self.launch_python(self.selected_game['gamefile'],self.selected_game['gamepath'])
# upon return, copy the modified settings and scores files back (if necessary)
try:
if config_settings is not None and settings_yaml is not None:
print 'Copying ' + settings_yaml + ' back to ' + config_settings
shutil.copyfile(settings_yaml, config_settings)
except Exception as detail:
# ignore exceptions (like when there aren't files to copy)
print "failed to save config settings: ", detail
try:
if config_scores is not None and scores_yaml is not None:
print 'Copying ' + scores_yaml + ' back to ' + config_scores
shutil.copyfile(scores_yaml, config_scores)
except Exception as detail:
# ignore exceptions (like when there aren't files to copy)
print "failed to save config scores: ", detail
def sw_flipperLwL_active(self, sw):
self.show_next_game(direction=-1)
def sw_flipperLwR_active(self, sw):
self.show_next_game(direction=1)
def launch_ext(self, cmd, args, path):
"""
launch a either a pinmame based game or a PyProc-based game by
running the executable and corresponding arguments. In the case
of Python-based games, this /could/ be done by launching the game class,
but for my needs, I want to be able to run multiple games
that are different, but have the same module names (e.g., many
games will have a BaseGameMode but that mode will be different)
"""
self.cancel_delayed('gi_dim')
self.game.lampctrl.stop_show()
self.stop_proc()
# remember current directory
cwd = os.getcwd()
os.chdir(path)
# Call an executable to take over from here, further execution of Python code is halted.
print(cmd+" "+args)
os.system(cmd+" "+args)
# return to original directory
os.chdir(cwd)
# Pinmame/PyProcGame executable was:
# - Quit by a delete on the keyboard
# - Interupted by flipper buttons + start button combo
# - died
def get_class( self, kls, path_adj='/.' ):
"""Returns a class for the given fully qualified class name, *kls*.
Source: http://stackoverflow.com/questions/452969/does-python-have-an-equivalent-to-java-class-forname
this is left here if someone wants to try to find an elegant recursive
reload solution for classes; otherwise launching two different PyProcGame
based games that have module names in common will not work (will just use
the previously loaded modules)
"""
sys.path.append(sys.path[0]+path_adj)
parts = kls.split('.')
module = ".".join(parts[:-1])
m = __import__( module )
reload(m)
for comp in parts[1:]:
m = getattr(m, comp)
return m
def launch_python(self, gameclass, gamepath):
"""
this is left here if someone wants to try to find an elegant recursive
reload solution for classes; otherwise launching two different PyProcGame
based games that have module names in common will not work (will just use
the previously loaded modules)
"""
self.stop_proc()
print("launching [" + gameclass + "] from [" + gamepath + "]")
klass = self.get_class(gameclass,gamepath)
game = klass()
# launch the game
game.run_loop()
# game exited so
del game
def stop_proc(self):
self.game.end_run_loop()
while len(self.game.dmd.frame_handlers) > 0:
del self.game.dmd.frame_handlers[0]
del self.game.proc
def restart_proc(self):
self.game.proc = self.game.create_pinproc()
self.game.proc.reset(1)
self.game.load_config(self.game.yamlpath)
self.game.enable_flippers(False)
self.game.dmd.frame_handlers.append(self.game.proc.dmd_draw)
self.game.dmd.frame_handlers.append(self.game.set_last_frame)
#########################################
##
#########################################
class Game(game.BasicGame):
""" the 'game' portion of the Loader """
def __init__(self, machine_type):
super(Game, self).__init__(machine_type)
self.lampctrl = lamps.LampController(self)
def setup(self):
"""docstring for setup"""
self.load_config(self.yamlpath)
self.loader = Loader(self,2)
# Instead of resetting everything here as well as when a user
# initiated reset occurs, do everything in self.reset() and call it
# now and during a user initiated reset.
self.reset()
def enable_flippers(self,enable):
""" enables flippers on pre-fliptronics machines by checking for the
presence of a coil with name flipperEnable which is mapped to the
flipper enable relay (G08)
"""
if self.coils.has_key('flipperEnable'):
enable = True
self.coils.flipperEnable.pulse(0)
super(game.BasicGame, self).enable_flippers(enable)
def reset(self):
# Reset the entire game framework
super(Game, self).reset()
# Add the basic modes to the mode queue
self.modes.add(self.loader)
# Make sure flippers are off
self.enable_flippers(False)
def main():
# actually load the Loader.yaml file
print("Using Loader config at: %s "%(loader_config_path))
global loaderconfig
loaderconfig = yaml.load(open(loader_config_path, 'r'))
# find the appropriate machine specific yaml file from
# Loader.yaml
machine_config_file = loaderconfig['machine_config_file']
config = yaml.load(open(machine_config_file, 'r'))
print("Using machine config at: %s "%(machine_config_file))
machine_type = config['PRGame']['machineType']
config = 0
game = None
try:
game = Game(machine_type)
game.yamlpath = machine_config_file
game.setup()
while 1:
game.run_loop()
# Reset mode & restart P-ROC / pyprocgame
game.loader.mode_started()
game.loader.restart_proc()
finally:
del game
if __name__ == '__main__': main()