2
#import "ConsoleView.h"
4
#include <unistd.h> /* _exit() */
5
#include <util.h> /* forkpty() */
8
#define dkVERSION @"version"
9
#define dkFULLSCREEN @"fullscreen"
10
#define dkFSAA @"fsaa"
12
#define dkRESOLUTION @"resolution"
13
#define dkADVANCEDOPTS @"advancedOptions"
14
#define dkSERVEROPTS @"server_options"
15
#define dkDESCRIPTION @"server_description"
16
#define dkPASSWORD @"server_password"
17
#define dkMAXCLIENTS @"server_maxclients"
19
#define kMaxDisplays 16
21
//If you make a MOD then please change this, the bundle indentifier, the file extensions (.ogz, .dmo), and the url registration.
22
#define kSAUERBRATEN @"assaultcube"
24
//tab names, i.e. image names (text is localised)
25
#define tkMAIN @"Main"
26
#define tkKEYS @"Keys"
27
#define tkSERVER @"Server"
30
@interface NSString(Extras)
32
@implementation NSString(Extras)
34
NSMutableString *str = [NSMutableString string];
36
[str replaceOccurrencesOfString:@":s" withString:kSAUERBRATEN options:0 range:NSMakeRange(0, [str length])];
42
@interface NSUserDefaults(Extras) // unless you want strings with "(null)" in them :-/
43
- (NSString*)nonNullStringForKey:(NSString*)key;
45
@implementation NSUserDefaults(Extras)
46
- (NSString*)nonNullStringForKey:(NSString*)key {
47
NSString *result = [self stringForKey:key];
48
return (result ? result : @"");
53
@interface Map : NSObject {
59
- (id)initWithPath:(NSString*)aPath user:(BOOL)aUser demo:(BOOL)aDemo
61
if((self = [super init]))
63
path = [[aPath stringByDeletingPathExtension] retain];
74
- (NSString*)path { return (demo ? [NSString stringWithFormat:@"-xdemo \"%@\"", path] : path); } // minor hack
75
- (NSString*)name { return [path lastPathComponent]; }
78
NSImage *image = [[NSImage alloc] initWithContentsOfFile:[path stringByAppendingString:@".jpg"]];
79
if(!image && demo) image = [NSImage imageNamed:tkMAIN];
80
if(!image) image = [NSImage imageNamed:@"Nomap"];
85
NSString *text = [NSString alloc];
87
if([text respondsToSelector:@selector(initWithContentsOfFile:encoding:error:)])
88
text = [text initWithContentsOfFile:[path stringByAppendingString:@".txt"] encoding:NSASCIIStringEncoding error:&error];
90
text = [text initWithContentsOfFile:[path stringByAppendingString:@".txt"]]; //deprecated in 10.4
91
if(!text) text = (demo)?@"Recorded demo data":@"";
94
- (void)setText:(NSString*)text { } // wtf? - damn textfield believes it's editable
95
- (NSString*)tickIfExists:(NSString*)ext
97
unichar tickCh = 0x2713;
98
return ([[NSFileManager defaultManager] fileExistsAtPath:[path stringByAppendingString:ext]] ? [NSString stringWithCharacters:&tickCh length:1] : @"");
100
- (NSString*)hasImage { return [self tickIfExists:@".jpg"]; }
101
- (NSString*)hasText { return [self tickIfExists:@".txt"]; }
102
- (NSString*)hasCfg { return [self tickIfExists:@".cfg"]; }
104
unichar tickCh = 0x2713;
105
return (user ? [NSString stringWithCharacters:&tickCh length:1] : @"");
110
static int numberForKey(CFDictionaryRef desc, CFStringRef key)
114
if ((value = CFDictionaryGetValue(desc, key)) == NULL)
116
CFNumberGetValue(value, kCFNumberIntType, &num);
121
@interface Launcher(ToolBar)
123
@implementation Launcher(ToolBar)
125
- (void)switchViews:(NSToolbarItem *)item
127
NSView *views[] = {view1, view3, view4};
128
NSView *prefsView = views[[item tag]-1];
130
//to stop flicker, we make a temp blank view.
131
NSView *tempView = [[NSView alloc] initWithFrame:[[window contentView] frame]];
132
[window setContentView:tempView];
135
//mojo to get the right frame for the new window.
136
NSRect newFrame = [window frame];
137
newFrame.size.height = [prefsView frame].size.height + ([window frame].size.height - [[window contentView] frame].size.height);
138
newFrame.size.width = [prefsView frame].size.width;
139
newFrame.origin.y += ([[window contentView] frame].size.height - [prefsView frame].size.height);
141
//set the frame to newFrame and animate it.
142
[window setFrame:newFrame display:YES animate:YES];
143
//set the main content view to the new view we have picked through.
144
[window setContentView:prefsView];
145
[window setContentMinSize:[prefsView bounds].size];
150
toolBarItems = [[NSMutableDictionary alloc] init];
151
NSEnumerator *e = [[self toolbarDefaultItemIdentifiers:nil] objectEnumerator];
152
NSString *identifier;
153
while(identifier = [e nextObject])
155
NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:identifier];
156
int tag = [identifier intValue];
157
NSString *name = identifier;
158
SEL action = @selector(displayHelp:);
161
NSString *names[] = {tkMAIN, tkKEYS, tkSERVER};
163
action = @selector(switchViews:);
167
[item setTarget:target];
168
[item setAction:action];
169
[item setLabel:NSLocalizedString(name, @"")];
170
[item setImage:[NSImage imageNamed:name]];
171
[toolBarItems setObject:item forKey:identifier];
174
NSToolbar *toolbar = [[NSToolbar alloc] initWithIdentifier:@""];
175
[toolbar setDelegate:self];
176
[toolbar setAllowsUserCustomization:NO];
177
[toolbar setAutosavesConfiguration:NO];
178
[window setToolbar:toolbar];
180
if([window respondsToSelector:@selector(setShowsToolbarButton:)]) [window setShowsToolbarButton:NO]; //10.4+
182
//select the first by default
183
NSToolbarItem *first = [toolBarItems objectForKey:[[self toolbarDefaultItemIdentifiers:nil] objectAtIndex:0]];
184
[toolbar setSelectedItemIdentifier:[first itemIdentifier]];
185
[self switchViews:first];
188
#pragma mark toolbar delegate methods
190
- (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag
192
return [toolBarItems objectForKey:itemIdentifier];
195
- (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar*)theToolbar
197
return [self toolbarDefaultItemIdentifiers:theToolbar];
200
- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar*)toolbar
202
NSMutableArray *array = (NSMutableArray *)[self toolbarSelectableItemIdentifiers:toolbar];
203
[array addObject:NSToolbarFlexibleSpaceItemIdentifier];
204
[array addObject:@"Help"];
208
- (NSArray *)toolbarSelectableItemIdentifiers: (NSToolbar *)toolbar
210
NSMutableArray *array = [NSMutableArray array];
211
NSView *views[] = {view1, view3, view4};
213
for(i = 0; i < sizeof(views)/sizeof(NSView*); i++) if(views[i]) [array addObject:[NSString stringWithFormat:@"%d", i+1]];
217
- (void)displayHelp:(id)sender
219
NSString *path = [[[NSBundle mainBundle] resourcePath] stringByAppendingString:@"/help/README.html"];
221
if (![[NSWorkspace sharedWorkspace] openFile:path])
222
NSLog(@"Warning: [[NSWorkspace sharedWorkspace] openFile:path] failed");
228
@implementation Launcher
230
/* directory where the executable lives */
233
return [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"Contents/gamedata"];
237
/* directory where user files are kept - typically /Users/<name>/Application Support/sauerbraten */
241
NSString *path = nil;
242
if(FSFindFolder(kUserDomain, kApplicationSupportFolderType, NO, &folder) == noErr) {
243
CFURLRef url = CFURLCreateFromFSRef(kCFAllocatorDefault, &folder);
244
path = [(NSURL *)url path];
246
path = [path stringByAppendingPathComponent:kSAUERBRATEN];
247
NSFileManager *fm = [NSFileManager defaultManager];
248
if(![fm fileExistsAtPath:path]) [fm createDirectoryAtPath:path attributes:nil]; //ensure it exists
253
- (void)addResolutionsForDisplay:(CGDirectDisplayID)dspy
256
CFArrayRef modeList = CGDisplayAvailableModes(dspy);
257
if(modeList == NULL) return;
258
cnt = CFArrayGetCount(modeList);
259
for(i = 0; i < cnt; i++) {
260
CFDictionaryRef mode = CFArrayGetValueAtIndex(modeList, i);
261
NSString *title = [NSString stringWithFormat:@"%i x %i", numberForKey(mode, kCGDisplayWidth), numberForKey(mode, kCGDisplayHeight)];
262
if(![resolutions itemWithTitle:title]) [resolutions addItemWithTitle:title];
266
- (void)initResolutions
268
CGDirectDisplayID display[kMaxDisplays];
269
CGDisplayCount numDisplays;
270
[resolutions removeAllItems];
271
if(CGGetActiveDisplayList(kMaxDisplays, display, &numDisplays) == CGDisplayNoErr)
274
for (i = 0; i < numDisplays; i++)
275
[self addResolutionsForDisplay:display[i]];
277
[resolutions selectItemAtIndex: [[NSUserDefaults standardUserDefaults] integerForKey:dkRESOLUTION]];
280
/* build key array from config data */
281
-(NSArray *)getKeys:(NSDictionary *)dict
283
NSMutableArray *arr = [NSMutableArray array];
284
NSEnumerator *e = [dict keyEnumerator];
286
while ((key = [e nextObject]))
288
int pos = [key rangeOfString:@"bind."].location;
289
if(pos == NSNotFound || pos > 5) continue;
290
[arr addObject:[NSDictionary dictionaryWithObjectsAndKeys: //keys used in nib
291
[key substringFromIndex:pos+5], @"key",
292
[key substringToIndex:pos], @"mode",
293
[dict objectForKey:key], @"action",
300
* extract a dictionary from the config files containing:
301
* - name, team, gamma strings
302
* - bind/editbind '.' key strings
304
-(NSDictionary *)readConfigFiles
306
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
307
[dict setObject:@"" forKey:@"name"]; //ensure these entries are never nil
308
[dict setObject:@"" forKey:@"team"];
310
NSString *files[] = {@"config.cfg", @"autoexec.cfg"};
312
for(i = 0; i < sizeof(files)/sizeof(NSString*); i++)
314
NSString *file = [Launcher userdir];
315
file = [file stringByAppendingPathComponent:files[i]];
317
NSArray *lines = [[NSString stringWithContentsOfFile:file] componentsSeparatedByString:@"\n"];
319
if(i==0 && !lines) // ugh - special case when first run...
321
file = [[Launcher cwd] stringByAppendingPathComponent:@"config/defaults.cfg"];
322
lines = [[NSString stringWithContentsOfFile:file] componentsSeparatedByString:@"\n"];
326
NSEnumerator *e = [lines objectEnumerator];
327
while(line = [e nextObject])
329
NSRange r; // more flexible to do this manually rather than via NSScanner...
331
while(j < [line length] && [line characterAtIndex:j] <= ' ') j++; //skip white
332
if(j != 0) continue; // shouldn't be indented
334
while(j < [line length] && [line characterAtIndex:j] > ' ') j++; //until white
335
r.length = j - r.location;
336
NSString *type = [line substringWithRange:r];
338
while(j < [line length] && [line characterAtIndex:j] <= ' ') j++; //skip white
339
if(j < [line length] && [line characterAtIndex:j] == '"')
342
while(j < [line length] && [line characterAtIndex:j] != '"') j++; //until close quote
343
r.length = (j++) - r.location;
346
while(j < [line length] && [line characterAtIndex:j] > ' ') j++; //until white
347
r.length = j - r.location;
349
if(r.location+r.length >= [line length]) continue; //missing value
350
NSString *value = [line substringWithRange:r];
352
while(j < [line length] && [line characterAtIndex:j] <= ' ') j++; //skip white
353
NSString *remainder = [line substringFromIndex:j];
355
if([type isEqual:@"name"] || [type isEqual:@"team"] || [type isEqual:@"gamma"])
356
[dict setObject:value forKey:type];
357
else if([type isEqual:@"bind"] || [type isEqual:@"editbind"] || [type isEqual:@"specbind"])
358
[dict setObject:remainder forKey:[NSString stringWithFormat:@"%@.%@", type,value]];
365
if(server > 0) kill(server, SIGKILL); //@WARNING - you do NOT want a 0 or -1 to be accidentally sent a kill!
367
[multiplayer setTitle:NSLocalizedString(@"Start", @"")];
368
[console appendText:@"\n \n"];
371
- (void)serverDataAvailable:(NSNotification *)note
373
NSFileHandle *taskOutput = [note object];
374
NSData *data = [[note userInfo] objectForKey:NSFileHandleNotificationDataItem];
376
if (data && [data length])
378
NSString *text = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
379
[console appendText:text];
381
[taskOutput readInBackgroundAndNotify]; //wait for more data
385
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
386
[nc removeObserver:self name:NSFileHandleReadCompletionNotification object:taskOutput];
387
close([taskOutput fileDescriptor]);
392
- (BOOL)launchGame:(NSArray *)args {
393
NSString *cwd = [Launcher cwd];
394
NSString *exe = [[NSBundle bundleWithPath:[cwd stringByAppendingPathComponent:[@":s.app" expand]]] executablePath];
398
if([args containsObject:@"-d"])
400
if(server != -1) return NO; // server is already running
402
const char **argv = (const char**)malloc(sizeof(char*)*([args count] + 2)); //{path, <args>, NULL};
403
argv[0] = [exe fileSystemRepresentation];
404
argv[[args count]+1] = NULL;
406
for(i = 0; i < [args count]; i++) argv[i+1] = [[args objectAtIndex:i] UTF8String];
409
NSString *fail = [NSLocalizedString(@"ServerAlertMesg", nil) expand];
410
switch ( (server = forkpty(&fdm, NULL, NULL, NULL)) ) // forkpty so we can reliably grab SDL console
413
[console appendLine:fail];
418
chdir([cwd fileSystemRepresentation]);
419
if(execv([exe fileSystemRepresentation], (char*const*)argv) == -1) fprintf(stderr, "%s\n", [fail UTF8String]);
422
[multiplayer setTitle:NSLocalizedString(@"Stop", @"")];
423
NSFileHandle *taskOutput = [[NSFileHandle alloc] initWithFileDescriptor:fdm];
424
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
425
[nc addObserver:self selector:@selector(serverDataAvailable:) name:NSFileHandleReadCompletionNotification object:taskOutput];
426
[taskOutput readInBackgroundAndNotify];
434
NSTask *task = [[NSTask alloc] init];
435
[task setCurrentDirectoryPath:cwd];
436
[task setLaunchPath:exe];
437
[task setArguments:args]; NSLog([args description]);
438
[task setEnvironment:[NSDictionary dictionaryWithObjectsAndKeys:
439
@"1", @"SDL_SINGLEDISPLAY",
440
@"1", @"SDL_ENABLEAPPEVENTS", nil
441
]]; // makes Command-H, Command-M and Command-Q work at least when not in fullscreen
444
if(server == -1) [NSApp terminate:self]; //if there is a server then don't exit!
446
//NSLog(@"%@", localException);
447
NSBeginCriticalAlertSheet(
448
[NSLocalizedString(@"ClientAlertTitle", @"") expand] , nil, nil, nil,
449
window, nil, nil, nil, nil,
450
[NSLocalizedString(@"ClientAlertMesg", @"") expand]);
459
* nil will just launch the fps game
460
* "-rpg" will launch the rpg demo
461
* "-x.." will launch and run commands
462
* otherwise we are specifying a map to play
464
- (BOOL)playFile:(id)filename
466
NSUserDefaults *defs = [NSUserDefaults standardUserDefaults];
468
NSArray *res = [[resolutions titleOfSelectedItem] componentsSeparatedByString:@" x "];
469
NSMutableArray *args = [NSMutableArray array];
471
[args addObject:[NSString stringWithFormat:@"--home=%@", [Launcher userdir]]];
472
[args addObject:@"--init"];
474
[args addObject:[NSString stringWithFormat:@"-w%@", [res objectAtIndex:0]]];
475
[args addObject:[NSString stringWithFormat:@"-h%@", [res objectAtIndex:1]]];
476
[args addObject:@"-z32"]; //otherwise seems to have a fondness to use -z16 which looks crap
478
if([defs integerForKey:dkFULLSCREEN] == 0)
479
[args addObject:@"-t0"];
481
[args addObject:@"-t1"];
482
[args addObject:[NSString stringWithFormat:@"-a%d", [defs integerForKey:dkFSAA]]];
484
if ([stencil state] == NSOnState) [args addObject:@"-s8"];
486
NSEnumerator *e = [[[defs nonNullStringForKey:dkADVANCEDOPTS] componentsSeparatedByString:@" "] objectEnumerator];
488
while(opt = [e nextObject]) if([opt length] != 0) [args addObject:opt]; //skip empty ones
490
return [self launchGame:args];
493
- (void)scanMaps:(id)obj //@note threaded!
495
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
497
for(i = 0; i < 2; i++)
499
NSString *dir = (i==0) ? [Launcher cwd] : [Launcher userdir];
500
NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtPath:dir];
502
while(file = [enumerator nextObject])
504
NSString *role = [fileRoles objectForKey:[file pathExtension]];
507
Map *map = [[Map alloc] initWithPath:[dir stringByAppendingPathComponent:file] user:(i==1) demo:[role isEqual:@"Viewer"]];
508
[maps performSelectorOnMainThread:@selector(addObject:) withObject:map waitUntilDone:NO];
512
[prog performSelectorOnMainThread:@selector(stopAnimation:) withObject:nil waitUntilDone:NO];
518
[prog startAnimation:nil];
519
[maps removeObjects:[maps arrangedObjects]];
520
[NSThread detachNewThreadSelector: @selector(scanMaps:) toTarget:self withObject:nil];
525
//generate some pretty icons if they are missing
526
NSRect region = NSMakeRect(0, 0, 64, 64);
527
NSImage *image = [NSImage imageNamed:tkMAIN];
529
image = [[NSImage imageNamed:@"NSApplicationIcon"] copy];
530
[image setSize:region.size];
531
[image setName:tkMAIN]; //one less image to include
535
[window setBackgroundColor:[NSColor colorWithDeviceRed:0.90 green:0.90 blue:0.90 alpha:1.0]]; //Apples 'mercury' crayon color
537
//from the plist we determine that dmo->Viewer, and ogz->Editor
538
fileRoles = [[NSMutableDictionary dictionary] retain];
539
NSEnumerator *types = [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDocumentTypes"] objectEnumerator];
541
while((type = [types nextObject])) {
542
NSString *role = [type objectForKey:@"CFBundleTypeRole"];
543
NSEnumerator *exts = [[type objectForKey:@"CFBundleTypeExtensions"] objectEnumerator];
545
while((ext = [exts nextObject])) [fileRoles setObject:role forKey:ext];
548
NSUserDefaults *defs = [NSUserDefaults standardUserDefaults];
549
NSFileManager *fm = [NSFileManager defaultManager];
551
NSString *appVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
552
NSString *version = [defs stringForKey:dkVERSION];
553
if(!version || ![version isEqual:appVersion])
555
NSLog(@"Upgraded Version...");
556
//need to flush lurking config files - they're automatically generated, so no big deal...
557
NSString *dir = [Launcher userdir];
558
[fm removeFileAtPath:[dir stringByAppendingPathComponent:@"init.cfg"] handler:nil];
559
[fm removeFileAtPath:[dir stringByAppendingPathComponent:@"config.cfg"] handler:nil];
561
[defs setObject:appVersion forKey:dkVERSION];
563
NSDictionary *dict = [self readConfigFiles];
564
[keys addObjects:[self getKeys:dict]];
568
[self initResolutions];
570
[NSApp setDelegate:self]; //so can catch the double-click, dropped files, termination
571
[[NSAppleEventManager sharedAppleEventManager] setEventHandler:self andSelector:@selector(getUrl:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL];
576
#pragma mark application delegate
578
- (void)applicationDidFinishLaunching:(NSNotification *)note {
579
NSFileManager *fm = [NSFileManager defaultManager];
580
NSString *dir = [Launcher cwd];
581
if(![fm fileExistsAtPath:dir])
582
NSBeginCriticalAlertSheet(
583
[NSLocalizedString(@"InitAlertTitle", @"") expand], nil, nil, nil,
584
window, self, nil, nil, nil,
585
[NSLocalizedString(@"InitAlertMesg", @"") expand]);
588
-(BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication {
592
- (void)applicationWillTerminate: (NSNotification *)note {
596
//we register 'ogz' and 'dmo' as doc types
597
- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename
599
NSString *role = [fileRoles objectForKey:[filename pathExtension]];
601
BOOL demo = [role isEqual:@"Viewer"];
602
filename = [filename stringByDeletingPathExtension]; //chop off extension
604
for(i = 0; i < 2; i++) {
605
NSString *pkg = (i == 0) ? [Launcher cwd] : [Launcher userdir];
606
if(!demo) pkg = [pkg stringByAppendingPathComponent:@"packages"];
607
if([filename hasPrefix:pkg])
608
return [self playFile:(demo ? [NSString stringWithFormat:@"-xdemo \"%@\"", filename] : filename)];
610
NSBeginCriticalAlertSheet(
611
[NSLocalizedString(@"FileAlertMesg", @"") expand], NSLocalizedString(@"Ok", @""), NSLocalizedString(@"Cancel", @""), nil,
612
window, self, @selector(openPackageFolder:returnCode:contextInfo:), nil, nil,
613
[NSLocalizedString(@"FileAlertMesg", @"") expand]);
617
- (void)openPackageFolder:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo
619
if(returnCode == 0) return;
620
[self openUserdir:nil];
623
//we register 'sauerbraten' as a url scheme
624
- (void)getUrl:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent
626
NSURL *url = [NSURL URLWithString:[[event paramDescriptorForKeyword:keyDirectObject] stringValue]];
628
[self playFile:[NSString stringWithFormat:@"-xconnect %@", [url host]]];
631
#pragma mark interface actions
633
- (IBAction)multiplayerAction:(id)sender
635
[window makeFirstResponder:window]; //ensure fields are exited and committed
642
NSUserDefaults *defs = [NSUserDefaults standardUserDefaults];
644
NSMutableArray *args = [NSMutableArray arrayWithObject:@"-d"];
646
NSEnumerator *e = [[[defs nonNullStringForKey:dkSERVEROPTS] componentsSeparatedByString:@" "] objectEnumerator];
648
while(opt = [e nextObject]) if([opt length] != 0) [args addObject:opt]; //skip empty ones
650
NSString *desc = [defs nonNullStringForKey:dkDESCRIPTION];
651
if (![desc isEqual:@""]) [args addObject:[NSString stringWithFormat:@"-n%@", desc]];
653
NSString *pass = [defs nonNullStringForKey:dkPASSWORD];
654
if (![pass isEqual:@""]) [args addObject:[NSString stringWithFormat:@"-p%@", pass]];
656
if (![[admin_password stringValue] isEqual:@""]) [args addObject:[NSString stringWithFormat:@"-x%@", pass]];
658
int clients = [defs integerForKey:dkMAXCLIENTS];
659
if (clients > 0) [args addObject:[NSString stringWithFormat:@"-c%d", clients]];
661
[args addObject:[NSString stringWithFormat:@"-q%@", [Launcher userdir]]];
663
[self launchGame:args];
667
- (IBAction)playAction:(id)sender
669
[window makeFirstResponder:window]; //ensure fields are exited and committed
673
- (IBAction)playRpg:(id)sender
675
[self playFile:@"-rpg"];
678
- (IBAction)playMap:(id)sender
680
NSArray *sel = [maps selectedObjects];
681
if(sel && [sel count] > 0) [self playFile:[[sel objectAtIndex:0] path]];
684
- (IBAction)openUserdir:(id)sender
686
[[NSWorkspace sharedWorkspace] openFile:[Launcher userdir]];