2 #import "ConsoleView.h"
\r
4 #include <unistd.h> /* _exit() */
\r
5 #include <util.h> /* forkpty() */
\r
8 #define dkVERSION @"version"
\r
9 #define dkUSERDIR @"userdir"
\r
10 #define dkNAME @"name"
\r
11 #define dkFULLSCREEN @"fullscreen"
\r
12 #define dkFSAA @"fsaa"
\r
13 #define dkSHADER @"shader"
\r
14 #define dkRESOLUTION @"resolution"
\r
15 #define dkADVANCEDOPTS @"advancedOptions"
\r
16 #define dkSERVEROPTS @"server_options"
\r
17 #define dkDESCRIPTION @"server_description"
\r
18 #define dkPASSWORD @"server_password"
\r
19 #define dkMAXCLIENTS @"server_maxclients"
\r
22 #define kMaxDisplays 16
\r
24 // unless you want strings with "(null)" in them :-/
\r
25 @interface NSUserDefaults(Extras)
\r
26 - (NSString*)nonNullStringForKey:(NSString*)key;
\r
29 @implementation NSUserDefaults(Extras)
\r
30 - (NSString*)nonNullStringForKey:(NSString*)key {
\r
31 NSString *result = [self stringForKey:key];
\r
32 return (result ? result : @"");
\r
36 @interface Map : NSObject {
\r
43 - (id)initWithPath:(NSString*)aPath user:(BOOL)aUser
\r
45 if((self = [super init]))
\r
47 demo = [aPath hasSuffix:@".dmo"];
\r
48 path = [[aPath stringByDeletingPathExtension] retain];
\r
58 - (NSString*)path { return (demo ? [NSString stringWithFormat:@"-xdemo \"%@\"", path] : path); } // minor hack
\r
59 - (NSString*)name { return [path lastPathComponent]; }
\r
62 NSImage *image = [[NSImage alloc] initWithContentsOfFile:[path stringByAppendingString:@".jpg"]];
\r
63 if(!image && demo) image = [NSImage imageNamed:@"Main"];
\r
64 if(!image) image = [NSImage imageNamed:@"Nomap"];
\r
69 NSString *text = [NSString alloc];
\r
71 if([text respondsToSelector:@selector(initWithContentsOfFile:encoding:error:)])
\r
72 text = [text initWithContentsOfFile:[path stringByAppendingString:@".txt"] encoding:NSASCIIStringEncoding error:&error];
\r
74 text = [text initWithContentsOfFile:[path stringByAppendingString:@".txt"]]; //deprecated in 10.4
\r
75 if(!text && (demo || user)) {
\r
76 text = user ? @"user " : @"";
\r
77 if(demo) text = [text stringByAppendingString:@"demo"];
\r
79 if(!text) return @"";
\r
82 - (void)setText:(NSString*)text { } // wtf? - damn textfield believes it's editable
\r
83 - (NSString*)tickIfExists:(NSString*)ext
\r
85 unichar tickCh = 0x2713;
\r
86 return ([[NSFileManager defaultManager] fileExistsAtPath:[path stringByAppendingString:ext]] ? [NSString stringWithCharacters:&tickCh length:1] : @"");
\r
88 - (NSString*)hasImage { return [self tickIfExists:@".jpg"]; }
\r
89 - (NSString*)hasText { return [self tickIfExists:@".txt"]; }
\r
90 - (NSString*)hasCfg { return [self tickIfExists:@".cfg"]; }
\r
95 static int numberForKey(CFDictionaryRef desc, CFStringRef key)
\r
99 if ((value = CFDictionaryGetValue(desc, key)) == NULL)
\r
101 CFNumberGetValue(value, kCFNumberIntType, &num);
\r
105 @implementation Launcher
\r
107 - (void)switchViews:(NSToolbarItem *)item
\r
109 NSView *prefsView = nil;
\r
110 switch([item tag])
\r
112 case 1: prefsView = view1; break;
\r
113 case 2: prefsView = view2; break;
\r
114 case 3: prefsView = view3; break;
\r
115 case 4: prefsView = view4; break;
\r
116 case 5: prefsView = view5; break;
\r
117 //extend as see fit...
\r
121 //to stop flicker, we make a temp blank view.
\r
122 NSView *tempView = [[NSView alloc] initWithFrame:[[window contentView] frame]];
\r
123 [window setContentView:tempView];
\r
124 [tempView release];
\r
126 //if no view then keep the blank one
\r
127 if(!prefsView) prefsView = tempView;
\r
129 //mojo to get the right frame for the new window.
\r
130 NSRect newFrame = [window frame];
\r
131 newFrame.size.height = [prefsView frame].size.height + ([window frame].size.height - [[window contentView] frame].size.height);
\r
132 newFrame.size.width = [prefsView frame].size.width;
\r
133 newFrame.origin.y += ([[window contentView] frame].size.height - [prefsView frame].size.height);
\r
135 //set the frame to newFrame and animate it.
\r
136 [window setFrame:newFrame display:YES animate:YES];
\r
137 //set the main content view to the new view we have picked through the if structure above.
\r
138 [window setContentView:prefsView];
\r
139 [window setContentMinSize:[prefsView bounds].size];
\r
142 - (NSToolbarItem*)addToolBarItem:(NSString*)name
\r
144 int n = [toolBarItems count] + 1;
\r
145 NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:[NSString stringWithFormat:@"%0d", n]];
\r
147 [item setTarget:self];
\r
148 [item setAction:@selector(switchViews:)];
\r
149 [toolBarItems setObject:item forKey:[item itemIdentifier]];
\r
150 [item setLabel:NSLocalizedString(name, @"")];
\r
151 [item setImage:[NSImage imageNamed:name]];
\r
158 toolBarItems = [[NSMutableDictionary alloc] init];
\r
159 NSToolbarItem *first = [self addToolBarItem:@"Main"];
\r
160 [self addToolBarItem:@"Maps"];
\r
161 [self addToolBarItem:@"Keys"];
\r
162 [self addToolBarItem:@"Server"];
\r
163 [self addToolBarItem:@"EisenStern"];
\r
165 NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:NSToolbarFlexibleSpaceItemIdentifier];
\r
166 [toolBarItems setObject:item forKey:[item itemIdentifier]];
\r
169 [[self addToolBarItem:@"Help"] setAction:@selector(helpAction:)];
\r
171 NSToolbar *toolbar = [[NSToolbar alloc] initWithIdentifier:@"PreferencePanes"];
\r
172 [toolbar setDelegate:self];
\r
173 [toolbar setAllowsUserCustomization:NO];
\r
174 [toolbar setAutosavesConfiguration:NO];
\r
175 [window setToolbar:toolbar];
\r
177 if([window respondsToSelector:@selector(setShowsToolbarButton:)]) [window setShowsToolbarButton:NO]; //10.4+
\r
179 //Make it select the first by default
\r
180 [toolbar setSelectedItemIdentifier:[first itemIdentifier]];
\r
181 [self switchViews:first];
\r
185 * toolbar delegate methods
\r
187 - (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag
\r
189 return [toolBarItems objectForKey:itemIdentifier];
\r
192 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar*)theToolbar
\r
194 return [self toolbarDefaultItemIdentifiers:theToolbar];
\r
197 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar*)theToolbar
\r
199 return [NSArray arrayWithObjects:@"1", @"2", @"3", @"4", @"5", NSToolbarFlexibleSpaceItemIdentifier, @"7", nil];
\r
202 - (NSArray *)toolbarSelectableItemIdentifiers: (NSToolbar *)toolbar
\r
204 return [NSArray arrayWithObjects:@"1", @"2", @"3", @"4", @"5", nil];
\r
212 - (void)addResolutionsForDisplay:(CGDirectDisplayID)dspy
\r
215 CFArrayRef modeList = CGDisplayAvailableModes(dspy);
\r
216 if(modeList == NULL) return;
\r
217 cnt = CFArrayGetCount(modeList);
\r
218 for(i = 0; i < cnt; i++) {
\r
219 CFDictionaryRef mode = CFArrayGetValueAtIndex(modeList, i);
\r
220 NSString *title = [NSString stringWithFormat:@"%i x %i", numberForKey(mode, kCGDisplayWidth), numberForKey(mode, kCGDisplayHeight)];
\r
221 if(![resolutions itemWithTitle:title]) [resolutions addItemWithTitle:title];
\r
225 - (void)initResolutions
\r
227 CGDirectDisplayID display[kMaxDisplays];
\r
228 CGDisplayCount numDisplays;
\r
229 [resolutions removeAllItems];
\r
230 if(CGGetActiveDisplayList(kMaxDisplays, display, &numDisplays) == CGDisplayNoErr)
\r
233 for (i = 0; i < numDisplays; i++)
\r
234 [self addResolutionsForDisplay:display[i]];
\r
236 [resolutions selectItemAtIndex: [[NSUserDefaults standardUserDefaults] integerForKey:dkRESOLUTION]];
\r
242 /* directory where the executable lives */
\r
245 return [[[[NSBundle mainBundle] bundlePath] stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"sauerbraten"];
\r
248 /* directory where user files are kept */
\r
249 - (NSString*)userdir {
\r
250 if(![[NSUserDefaults standardUserDefaults] boolForKey:dkUSERDIR]) return [self cwd];
\r
251 // /Users/<name>/Application Support/sauerbraten
\r
253 NSString *path = nil;
\r
254 if(FSFindFolder(kUserDomain, kApplicationSupportFolderType, NO, &folder) == noErr) {
\r
255 CFURLRef url = CFURLCreateFromFSRef(kCFAllocatorDefault, &folder);
\r
256 path = [(NSURL *)url path];
\r
258 path = [path stringByAppendingPathComponent:@"sauerbraten"];
\r
263 /* build key array from config data */
\r
264 -(NSArray *)getKeys:(NSDictionary *)dict
\r
266 NSMutableArray *arr = [[NSMutableArray alloc] init];
\r
267 NSEnumerator *e = [dict keyEnumerator];
\r
269 while ((key = [e nextObject]))
\r
272 if([key hasPrefix:@"editbind"])
\r
273 trig = [key substringFromIndex:9];
\r
274 else if([key hasPrefix:@"bind"])
\r
275 trig = [key substringFromIndex:5];
\r
278 [arr addObject:[NSDictionary dictionaryWithObjectsAndKeys: //keys used in nib
\r
280 ([key hasPrefix:@"editbind"] ? @"edit" : @""), @"mode",
\r
281 [dict objectForKey:key], @"action",
\r
289 * extract a dictionary from the config files containing:
\r
290 * - name, team, gamma strings
\r
291 * - bind/editbind '.' key strings
\r
293 -(NSDictionary *)readConfigFiles
\r
295 NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
\r
296 [dict setObject:@"" forKey:@"name"]; //ensure these entries are never nil
\r
297 [dict setObject:@"" forKey:@"team"];
\r
299 NSString *files[] = {@"config.cfg", @"autoexec.cfg"};
\r
301 for(i = 0; i < sizeof(files)/sizeof(NSString*); i++)
\r
303 NSString *file = [[self userdir] stringByAppendingPathComponent:files[i]];
\r
305 NSArray *lines = [[NSString stringWithContentsOfFile:file] componentsSeparatedByString:@"\n"];
\r
307 if(i==0 && !lines) // ugh - special case when first run...
\r
309 file = [[self cwd] stringByAppendingPathComponent:@"data/defaults.cfg"];
\r
310 lines = [[NSString stringWithContentsOfFile:file] componentsSeparatedByString:@"\n"];
\r
314 NSEnumerator *e = [lines objectEnumerator];
\r
315 while(line = [e nextObject])
\r
317 NSRange r; // more flexible to do this manually rather than via NSScanner...
\r
319 while(j < [line length] && [line characterAtIndex:j] <= ' ') j++; //skip white
\r
321 while(j < [line length] && [line characterAtIndex:j] > ' ') j++; //until white
\r
322 r.length = j - r.location;
\r
323 NSString *type = [line substringWithRange:r];
\r
325 while(j < [line length] && [line characterAtIndex:j] <= ' ') j++; //skip white
\r
326 if(j < [line length] && [line characterAtIndex:j] == '"')
\r
329 while(j < [line length] && [line characterAtIndex:j] != '"') j++; //until close quote
\r
330 r.length = (j++) - r.location;
\r
333 while(j < [line length] && [line characterAtIndex:j] > ' ') j++; //until white
\r
334 r.length = j - r.location;
\r
336 NSString *value = [line substringWithRange:r];
\r
338 while(j < [line length] && [line characterAtIndex:j] <= ' ') j++; //skip white
\r
339 NSString *remainder = [line substringFromIndex:j];
\r
341 if([type isEqual:@"name"] || [type isEqual:@"team"] || [type isEqual:@"gamma"])
\r
342 [dict setObject:value forKey:type];
\r
343 else if([type isEqual:@"bind"] || [type isEqual:@"editbind"])
\r
344 [dict setObject:remainder forKey:[NSString stringWithFormat:@"%@.%@", type,value]];
\r
350 - (void)serverTerminated
\r
352 if(server==-1) return;
\r
354 [multiplayer setTitle:@"Start"];
\r
355 [console appendText:@"\n \n"];
\r
358 - (void)setServerActive:(BOOL)start
\r
360 if((server==-1) != start) return;
\r
365 //damn server, terminate isn't good enough for you - die, die, die...
\r
366 if((server!=-1) && (server!=0)) kill(server, SIGKILL); //@WARNING - you do not want a 0 or -1 to be accidentally sent a kill!
\r
367 [self serverTerminated];
\r
371 NSString *cwd = [self cwd];
\r
372 NSUserDefaults *defs = [NSUserDefaults standardUserDefaults];
\r
374 NSArray *opts = [[defs nonNullStringForKey:dkSERVEROPTS] componentsSeparatedByString:@" "];
\r
376 const char *childCwd = [cwd fileSystemRepresentation];
\r
377 const char *childPath = [[cwd stringByAppendingPathComponent:@"sauerbraten.app/Contents/MacOS/sauerbraten"] fileSystemRepresentation];
\r
378 const char **args = (const char**)malloc(sizeof(char*)*([opts count] + 3 + 4)); //3 = {path, -d, NULL}, and +4 again for optional settings...
\r
379 int i, fdm, argc = 0;
\r
381 args[argc++] = childPath;
\r
382 args[argc++] = "-d";
\r
384 for(i = 0; i < [opts count]; i++)
\r
386 NSString *opt = [opts objectAtIndex:i];
\r
387 if([opt length] == 0) continue; //skip empty
\r
388 args[argc++] = [opt UTF8String];
\r
391 NSString *desc = [defs nonNullStringForKey:dkDESCRIPTION];
\r
392 if (![desc isEqual:@""]) args[argc++] = [[NSString stringWithFormat:@"-n%@", desc] UTF8String];
\r
394 NSString *pass = [defs nonNullStringForKey:dkPASSWORD];
\r
395 if (![pass isEqual:@""]) args[argc++] = [[NSString stringWithFormat:@"-p%@", pass] UTF8String];
\r
397 int clients = [defs integerForKey:dkMAXCLIENTS];
\r
398 if (clients > 0) args[argc++] = [[NSString stringWithFormat:@"-c%d", clients] UTF8String];
\r
400 if([defs boolForKey:dkUSERDIR]) args[argc++] = [[NSString stringWithFormat:@"-q%@", [self userdir]] UTF8String];
\r
402 args[argc++] = NULL;
\r
404 switch ( (server = forkpty(&fdm, NULL, NULL, NULL)) ) // forkpty so we can reliably grab SDL console
\r
407 [console appendLine:@"Error - can't launch server"];
\r
408 [self serverTerminated];
\r
412 execv(childPath, (char*const*)args);
\r
413 fprintf(stderr, "Error - can't launch server\n");
\r
417 //fprintf(stderr, "fdm=%d\n", slave_name, fdm);
\r
418 [multiplayer setTitle:@"Stop"];
\r
420 NSFileHandle *taskOutput = [[NSFileHandle alloc] initWithFileDescriptor:fdm];
\r
421 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
\r
422 [nc addObserver:self selector:@selector(serverDataAvailable:) name:NSFileHandleReadCompletionNotification object:taskOutput];
\r
423 [taskOutput readInBackgroundAndNotify];
\r
429 - (void)serverDataAvailable:(NSNotification *)note
\r
431 NSFileHandle *taskOutput = [note object];
\r
432 NSData *data = [[note userInfo] objectForKey:NSFileHandleNotificationDataItem];
\r
434 if (data && [data length])
\r
436 NSString *text = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
\r
437 [console appendText:text];
\r
439 [taskOutput readInBackgroundAndNotify]; //wait for more data
\r
443 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
\r
444 [nc removeObserver:self name:NSFileHandleReadCompletionNotification object:taskOutput];
\r
445 close([taskOutput fileDescriptor]);
\r
446 [self setServerActive:NO];
\r
451 * nil will just launch the fps game
\r
452 * "-rpg" will launch the rpg demo
\r
453 * otherwise we are specifying a map to play
\r
455 - (BOOL)playFile:(id)filename
\r
457 NSUserDefaults *defs = [NSUserDefaults standardUserDefaults];
\r
459 NSArray *res = [[resolutions titleOfSelectedItem] componentsSeparatedByString:@" x "];
\r
460 NSMutableArray *args = [[NSMutableArray alloc] init];
\r
461 NSString *cwd = [self cwd];
\r
463 [args addObject:[NSString stringWithFormat:@"-w%@", [res objectAtIndex:0]]];
\r
464 [args addObject:[NSString stringWithFormat:@"-h%@", [res objectAtIndex:1]]];
\r
465 [args addObject:@"-z32"]; //otherwise seems to have a fondness to use -z16 which looks crap
\r
467 if([defs integerForKey:dkFULLSCREEN] == 0) [args addObject:@"-t"];
\r
468 [args addObject:[NSString stringWithFormat:@"-a%d", [defs integerForKey:dkFSAA]]];
\r
469 [args addObject:[NSString stringWithFormat:@"-f%d", [defs integerForKey:dkSHADER]]];
\r
471 if([defs boolForKey:dkUSERDIR]) [args addObject:[NSString stringWithFormat:@"-q%@", [self userdir]]];
\r
473 NSMutableArray *cmds = [[NSMutableArray alloc] init];
\r
474 NSString *name = [defs nonNullStringForKey:dkNAME];
\r
475 if(name) [cmds addObject:[NSString stringWithFormat:@"name \"%@\"", name]];
\r
479 if([filename isEqual:@"-rpg"]) {
\r
480 [cmds removeAllObjects]; // rpg current doesn't require name/team
\r
481 [args addObject:@"-grpg"]; //demo the rpg game
\r
482 } else if([filename hasPrefix:@"-x"])
\r
483 [cmds addObject:[filename substringFromIndex:2]];
\r
485 [args addObject:[NSString stringWithFormat:@"-l%@", filename]];
\r
488 if([cmds count] > 0)
\r
490 NSString *script = [cmds objectAtIndex:0];
\r
492 for(i = 1; i < [cmds count]; i++) script = [NSString stringWithFormat:@"%@;%@", script, [cmds objectAtIndex:i]];
\r
493 [args addObject:[NSString stringWithFormat:@"-x%@", script]];
\r
496 NSString *adv = [defs nonNullStringForKey:dkADVANCEDOPTS];
\r
497 if(![adv isEqual:@""]) [args addObjectsFromArray:[adv componentsSeparatedByString:@" "]];
\r
499 //NSLog(@"%@", args);
\r
501 NSTask *task = [[NSTask alloc] init];
\r
502 [task setCurrentDirectoryPath:cwd];
\r
503 [task setLaunchPath:[cwd stringByAppendingPathComponent:@"sauerbraten.app/Contents/MacOS/sauerbraten"]];
\r
504 [task setArguments:args];
\r
505 [task setEnvironment:[NSDictionary dictionaryWithObjectsAndKeys:
\r
506 @"1", @"SDL_SINGLEDISPLAY",
\r
507 @"1", @"SDL_ENABLEAPPEVENTS", nil
\r
508 ]]; // makes Command-H, Command-M and Command-Q work at least when not in fullscreen
\r
516 if(server==-1) [NSApp terminate:self]; //if there is a server then don't exit!
\r
518 //NSLog(@"%@", localException);
\r
519 NSBeginCriticalAlertSheet(
\r
520 @"Can't start Sauerbraten", nil, nil, nil,
\r
521 window, nil, nil, nil, nil,
\r
522 @"Please move the directory containing Sauerbraten to a path that doesn't contain weird characters or start Sauerbraten manually.");
\r
530 - (void)scanMaps:(id)obj //@note threaded!
\r
532 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
\r
533 int len = [[NSUserDefaults standardUserDefaults] boolForKey:dkUSERDIR] ? 2 : 1;
\r
535 for(i = 0; i < len; i++)
\r
537 NSString *dir = (i==0) ? [self cwd] : [self userdir];
\r
538 NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtPath:dir];
\r
540 while(file = [enumerator nextObject])
\r
542 if([file hasSuffix:@".ogz"] || [file hasSuffix:@".dmo"])
\r
544 Map *map = [[Map alloc] initWithPath:[dir stringByAppendingPathComponent:file] user:(i==1)];
\r
545 [maps performSelectorOnMainThread:@selector(addObject:) withObject:map waitUntilDone:NO];
\r
549 [prog performSelectorOnMainThread:@selector(stopAnimation:) withObject:nil waitUntilDone:NO];
\r
555 [prog startAnimation:nil];
\r
556 [maps removeObjects:[maps arrangedObjects]];
\r
557 [NSThread detachNewThreadSelector: @selector(scanMaps:) toTarget:self withObject:nil];
\r
561 - (void)awakeFromNib
\r
564 [window setBackgroundColor:[NSColor colorWithDeviceRed:0.90 green:0.90 blue:0.90 alpha:1.0]]; //Apples 'mercury' crayon color
\r
566 NSUserDefaults *defs = [NSUserDefaults standardUserDefaults];
\r
567 NSFileManager *fm = [NSFileManager defaultManager];
\r
569 NSString *appVersion = [[[NSBundle bundleForClass:[self class]] infoDictionary] objectForKey:@"CFBundleVersion"];
\r
570 NSString *version = [defs stringForKey:dkVERSION];
\r
571 if(!version || ![version isEqual:appVersion])
\r
573 NSLog(@"Upgraded Version...");
\r
574 //need to flush lurking config files - they're automatically generated, so no big deal...
\r
575 [fm removeFileAtPath:[[self userdir] stringByAppendingPathComponent:@"init.cfg"] handler:nil];
\r
576 [fm removeFileAtPath:[[self userdir] stringByAppendingPathComponent:@"config.cfg"] handler:nil];
\r
578 [defs setObject:appVersion forKey:dkVERSION];
\r
580 NSDictionary *dict = [self readConfigFiles];
\r
581 [keys addObjects:[self getKeys:dict]];
\r
583 if([[defs nonNullStringForKey:dkNAME] isEqual:@""])
\r
585 NSString *name = [dict objectForKey:@"name"];
\r
586 if([name isEqual:@""] || [name isEqual:@"unnamed"]) name = NSUserName();
\r
587 [defs setValue:name forKey:dkNAME];
\r
590 NSString *dir = [self cwd];
\r
591 if(![fm fileExistsAtPath:dir]) NSLog(@"Missing sauebraten?!"); // @TODO error indicator?
\r
592 else if(![fm isWritableFileAtPath:dir]) [defs setBool:YES forKey:dkUSERDIR]; // auto-enable userdir
\r
595 [self initResolutions];
\r
597 [NSApp setDelegate:self]; //so can catch the double-click, dropped files, termination
\r
598 [[NSAppleEventManager sharedAppleEventManager] setEventHandler:self andSelector:@selector(getUrl:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL];
\r
600 //Listen for changes in the preferences that require the gui to refresh
\r
601 [defs addObserver:self forKeyPath:dkUSERDIR options:NSKeyValueObservingOptionNew context:nil];
\r
605 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
\r
606 if(![keyPath isEqual:dkUSERDIR]) return;
\r
611 #pragma mark application delegate
\r
613 -(BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication {
\r
617 - (void)applicationWillTerminate: (NSNotification *)note {
\r
618 [self setServerActive:NO];
\r
622 //we register 'ogz' and 'dmo' as doc types
\r
623 - (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename
\r
625 NSString *dirs[] = {[self cwd], [self userdir]};
\r
626 BOOL demo = [filename hasSuffix:@".dmo"];
\r
627 if(!demo && ![filename hasSuffix:@".ogz"]) return NO;
\r
628 filename = [filename substringToIndex:[filename length]-4]; //chop off extension
\r
630 for(i = 0; i < 2; i++) {
\r
631 NSString *pkg = dirs[i];
\r
632 if(!demo) pkg = [pkg stringByAppendingPathComponent:@"packages"];
\r
633 if([filename hasPrefix:pkg])
\r
634 return [self playFile:(demo ? [NSString stringWithFormat:@"-xdemo \"%@\"", filename] : filename)];
\r
636 NSBeginCriticalAlertSheet(
\r
637 @"Invalid file location", @"Ok", @"Cancel", nil,
\r
638 window, self, @selector(openPackageFolder:returnCode:contextInfo:), nil, nil,
\r
639 @"Can only load files that are within the sauerbraten/packages/ folder. Do you want to show this folder?");
\r
640 //@TODO give user option to copy it into the packages folder?
\r
644 - (void)openPackageFolder:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo
\r
646 if(returnCode == 0) return;
\r
647 [self openUserdir:nil]; //close enough... otherwise need to ensure that sauerbraten/packages folder exists
\r
650 //we register 'sauerbraten' as a url scheme
\r
651 - (void)getUrl:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent
\r
653 NSURL *url = [NSURL URLWithString:[[event paramDescriptorForKeyword:keyDirectObject] stringValue]];
\r
655 [self playFile:[NSString stringWithFormat:@"-xconnect %@", [url host]]];
\r
659 #pragma mark interface actions
\r
661 - (IBAction)multiplayerAction:(id)sender
\r
663 [window makeFirstResponder:window]; //ensure fields are exited and committed
\r
664 [self setServerActive:(server==-1)];
\r
667 - (IBAction)playAction:(id)sender
\r
669 [window makeFirstResponder:window]; //ensure fields are exited and committed
\r
670 [self playFile:nil];
\r
673 - (IBAction)playRpg:(id)sender
\r
675 [self playFile:@"-rpg"];
\r
678 - (IBAction)playMap:(id)sender
\r
680 NSArray *sel = [maps selectedObjects];
\r
681 if(sel && [sel count] > 0) [self playFile:[[sel objectAtIndex:0] path]];
\r
684 - (IBAction)helpAction:(id)sender
\r
686 NSString *file = [[[[NSBundle mainBundle] bundlePath] stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"README.html"];
\r
687 [[NSWorkspace sharedWorkspace] openURL:[NSURL fileURLWithPath:file]];
\r
690 - (IBAction)openUserdir:(id)sender
\r
692 NSString *dir = [self userdir];
\r
693 NSFileManager *fm = [NSFileManager defaultManager];
\r
694 if(![fm fileExistsAtPath:dir]) [fm createDirectoryAtPath:dir attributes:nil]; //ensure there is a folder to open
\r
695 [[NSWorkspace sharedWorkspace] openFile:dir];
\r