Initial sauer
[SauerbratenRemote.git] / src / xcode / Launcher.m
blob6f764f61d7a6c11f4d3408694f2d0fea37049bef
1 #import "Launcher.h"\r
2 #import "ConsoleView.h"\r
3 #include <stdlib.h>\r
4 #include <unistd.h> /* _exit() */\r
5 #include <util.h> /* forkpty() */\r
6 \r
7 // User default keys\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
27 @end\r
29 @implementation NSUserDefaults(Extras)\r
30 - (NSString*)nonNullStringForKey:(NSString*)key {\r
31     NSString *result = [self stringForKey:key];\r
32     return (result ? result : @"");\r
33 }\r
34 @end\r
36 @interface Map : NSObject {\r
37     NSString *path;\r
38     BOOL demo, user;\r
39 }\r
40 @end\r
42 @implementation Map\r
43 - (id)initWithPath:(NSString*)aPath user:(BOOL)aUser \r
44 {\r
45     if((self = [super init])) \r
46     {\r
47         demo = [aPath hasSuffix:@".dmo"];\r
48         path = [[aPath stringByDeletingPathExtension] retain];\r
49         user = aUser;\r
50     }\r
51     return self;\r
52 }\r
53 - (void)dealloc \r
54 {\r
55     [path release];\r
56     [super dealloc];\r
57 }\r
58 - (NSString*)path { return (demo ? [NSString stringWithFormat:@"-xdemo \"%@\"", path] : path); } // minor hack\r
59 - (NSString*)name { return [path lastPathComponent]; }\r
60 - (NSImage*)image \r
61 \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
65     return image;\r
66 }\r
67 - (NSString*)text \r
68 {\r
69     NSString *text = [NSString alloc];\r
70     NSError *error;\r
71     if([text respondsToSelector:@selector(initWithContentsOfFile:encoding:error:)])\r
72         text = [text initWithContentsOfFile:[path stringByAppendingString:@".txt"] encoding:NSASCIIStringEncoding error:&error];\r
73     else\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
78     }\r
79     if(!text) return @"";\r
80     return text;\r
81 }\r
82 - (void)setText:(NSString*)text { } // wtf? - damn textfield believes it's editable\r
83 - (NSString*)tickIfExists:(NSString*)ext \r
84 {\r
85     unichar tickCh = 0x2713; \r
86     return ([[NSFileManager defaultManager] fileExistsAtPath:[path stringByAppendingString:ext]] ? [NSString stringWithCharacters:&tickCh length:1] : @"");\r
87 }\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
91 @end\r
95 static int numberForKey(CFDictionaryRef desc, CFStringRef key) \r
96 {\r
97     CFNumberRef value;\r
98     int num = 0;\r
99     if ((value = CFDictionaryGetValue(desc, key)) == NULL)\r
100         return 0;\r
101     CFNumberGetValue(value, kCFNumberIntType, &num);\r
102     return num;\r
105 @implementation Launcher\r
107 - (void)switchViews:(NSToolbarItem *)item \r
109     NSView *prefsView = nil;\r
110     switch([item tag]) \r
111     {\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
118         default: return;\r
119     }\r
120     \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
128     \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
134     \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
146     [item setTag: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
152     [item release];\r
153     return item;\r
156 - (void)initViews \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
167     [item release];\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
176     [toolbar release];\r
177     if([window respondsToSelector:@selector(setShowsToolbarButton:)]) [window setShowsToolbarButton:NO]; //10.4+\r
178     \r
179     //Make it select the first by default\r
180     [toolbar setSelectedItemIdentifier:[first itemIdentifier]];\r
181     [self switchViews:first]; \r
184 /*\r
185  * toolbar delegate methods\r
186  */\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
214     CFIndex i, cnt;\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
222     }   \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
231     {\r
232         CGDisplayCount i;\r
233         for (i = 0; i < numDisplays; i++)\r
234             [self addResolutionsForDisplay:display[i]];\r
235     }\r
236     [resolutions selectItemAtIndex: [[NSUserDefaults standardUserDefaults] integerForKey:dkRESOLUTION]];        \r
242 /* directory where the executable lives */\r
243 -(NSString *)cwd \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
252     FSRef folder;\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
257         CFRelease(url);\r
258         path = [path stringByAppendingPathComponent:@"sauerbraten"];\r
259     }\r
260     return path;\r
263 /* build key array from config data */\r
264 -(NSArray *)getKeys:(NSDictionary *)dict \r
265 {       \r
266     NSMutableArray *arr = [[NSMutableArray alloc] init];\r
267     NSEnumerator *e = [dict keyEnumerator];\r
268     NSString *key;\r
269     while ((key = [e nextObject])) \r
270     {\r
271         NSString *trig;\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
276         else \r
277             continue;\r
278         [arr addObject:[NSDictionary dictionaryWithObjectsAndKeys: //keys used in nib\r
279             trig, @"key",\r
280             ([key hasPrefix:@"editbind"] ? @"edit" : @""), @"mode",\r
281             [dict objectForKey:key], @"action",\r
282             nil]];\r
283     }\r
284     return arr;\r
288 /*\r
289  * extract a dictionary from the config files containing:\r
290  * - name, team, gamma strings\r
291  * - bind/editbind '.' key strings\r
292  */\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
298     \r
299     NSString *files[] = {@"config.cfg", @"autoexec.cfg"};\r
300     int i;\r
301     for(i = 0; i < sizeof(files)/sizeof(NSString*); i++) \r
302     {\r
303         NSString *file = [[self userdir] stringByAppendingPathComponent:files[i]];\r
304         \r
305         NSArray *lines = [[NSString stringWithContentsOfFile:file] componentsSeparatedByString:@"\n"];\r
306         \r
307         if(i==0 && !lines)  // ugh - special case when first run...\r
308         { \r
309             file = [[self cwd] stringByAppendingPathComponent:@"data/defaults.cfg"];\r
310             lines = [[NSString stringWithContentsOfFile:file] componentsSeparatedByString:@"\n"];\r
311         }\r
312                 \r
313         NSString *line; \r
314         NSEnumerator *e = [lines objectEnumerator];\r
315         while(line = [e nextObject]) \r
316         {\r
317             NSRange r; // more flexible to do this manually rather than via NSScanner...\r
318             int j = 0;\r
319             while(j < [line length] && [line characterAtIndex:j] <= ' ') j++; //skip white\r
320             r.location = j;\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
324                         \r
325             while(j < [line length] && [line characterAtIndex:j] <= ' ') j++; //skip white\r
326             if(j < [line length] && [line characterAtIndex:j] == '"') \r
327             {\r
328                 r.location = ++j;\r
329                 while(j < [line length] && [line characterAtIndex:j] != '"') j++; //until close quote\r
330                 r.length = (j++) - r.location;\r
331             } else {\r
332                 r.location = j;\r
333                 while(j < [line length] && [line characterAtIndex:j] > ' ') j++; //until white\r
334                 r.length = j - r.location;\r
335             }\r
336             NSString *value = [line substringWithRange:r];\r
337             \r
338             while(j < [line length] && [line characterAtIndex:j] <= ' ') j++; //skip white\r
339             NSString *remainder = [line substringFromIndex:j];\r
340                         \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
345         }\r
346     }\r
347     return dict;\r
350 - (void)serverTerminated\r
352     if(server==-1) return;\r
353     server = -1;\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
361         \r
362     if(!start)\r
363     {   //STOP\r
364                 \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
368     } \r
369     else\r
370     {   //START\r
371         NSString *cwd = [self cwd];\r
372         NSUserDefaults *defs = [NSUserDefaults standardUserDefaults];\r
373                        \r
374         NSArray *opts = [[defs nonNullStringForKey:dkSERVEROPTS] componentsSeparatedByString:@" "];\r
375                 \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
380                 \r
381         args[argc++] = childPath;\r
382         args[argc++] = "-d";\r
383         \r
384         for(i = 0; i < [opts count]; i++)\r
385         {\r
386             NSString *opt = [opts objectAtIndex:i];\r
387             if([opt length] == 0) continue; //skip empty\r
388             args[argc++] = [opt UTF8String];\r
389         }\r
390         \r
391         NSString *desc = [defs nonNullStringForKey:dkDESCRIPTION];\r
392         if (![desc isEqual:@""]) args[argc++] = [[NSString stringWithFormat:@"-n%@", desc] UTF8String];\r
393         \r
394         NSString *pass = [defs nonNullStringForKey:dkPASSWORD];\r
395         if (![pass isEqual:@""]) args[argc++] = [[NSString stringWithFormat:@"-p%@", pass] UTF8String];\r
396                 \r
397         int clients = [defs integerForKey:dkMAXCLIENTS];\r
398         if (clients > 0) args[argc++] = [[NSString stringWithFormat:@"-c%d", clients] UTF8String];\r
399         \r
400         if([defs boolForKey:dkUSERDIR]) args[argc++] = [[NSString stringWithFormat:@"-q%@", [self userdir]] UTF8String];\r
401         \r
402         args[argc++] = NULL;\r
403                 \r
404         switch ( (server = forkpty(&fdm, NULL, NULL, NULL)) ) // forkpty so we can reliably grab SDL console\r
405         { \r
406             case -1:\r
407                 [console appendLine:@"Error - can't launch server"];\r
408                 [self serverTerminated];\r
409                 break;\r
410             case 0: // child\r
411                 chdir(childCwd);\r
412                 execv(childPath, (char*const*)args);\r
413                 fprintf(stderr, "Error - can't launch server\n");\r
414                 _exit(0);\r
415             default: // parent\r
416                 free(args);\r
417                 //fprintf(stderr, "fdm=%d\n", slave_name, fdm);\r
418                 [multiplayer setTitle:@"Stop"];\r
419                                 \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
424                 break;\r
425         }\r
426     }\r
429 - (void)serverDataAvailable:(NSNotification *)note\r
431     NSFileHandle *taskOutput = [note object];\r
432     NSData *data = [[note userInfo] objectForKey:NSFileHandleNotificationDataItem];\r
433         \r
434     if (data && [data length])\r
435     {\r
436         NSString *text = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];           \r
437         [console appendText:text];\r
438         [text release];                                 \r
439         [taskOutput readInBackgroundAndNotify]; //wait for more data\r
440     }\r
441     else\r
442     {\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
447     }\r
450 /*\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
454  */\r
455 - (BOOL)playFile:(id)filename \r
456 {       \r
457     NSUserDefaults *defs = [NSUserDefaults standardUserDefaults];\r
458     \r
459     NSArray *res = [[resolutions titleOfSelectedItem] componentsSeparatedByString:@" x "];      \r
460     NSMutableArray *args = [[NSMutableArray alloc] init];\r
461     NSString *cwd = [self cwd];\r
462         \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
466         \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
470     \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
476     \r
477     if(filename) \r
478     {\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
484         else \r
485             [args addObject:[NSString stringWithFormat:@"-l%@", filename]];\r
486         }\r
487     \r
488     if([cmds count] > 0) \r
489     {\r
490         NSString *script = [cmds objectAtIndex:0];\r
491         int i;\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
494     }\r
495     \r
496     NSString *adv = [defs nonNullStringForKey:dkADVANCEDOPTS];\r
497     if(![adv isEqual:@""]) [args addObjectsFromArray:[adv componentsSeparatedByString:@" "]];\r
499     //NSLog(@"%@", args);\r
500     \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
509     [args release];\r
510     [cmds release];\r
511         \r
512     BOOL okay = YES;\r
513         \r
514     NS_DURING\r
515         [task launch];\r
516         if(server==-1) [NSApp terminate:self];  //if there is a server then don't exit!\r
517     NS_HANDLER\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
523         okay = NO;\r
524     NS_ENDHANDLER\r
525         \r
526     return okay;\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
534     int i;\r
535     for(i = 0; i < len; i++) \r
536     {\r
537         NSString *dir = (i==0) ? [self cwd] : [self userdir];\r
538         NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtPath:dir];\r
539         NSString *file;\r
540         while(file = [enumerator nextObject]) \r
541         {\r
542             if([file hasSuffix:@".ogz"] || [file hasSuffix:@".dmo"]) \r
543             {   \r
544                 Map *map = [[Map alloc] initWithPath:[dir stringByAppendingPathComponent:file] user:(i==1)];\r
545                 [maps performSelectorOnMainThread:@selector(addObject:) withObject:map waitUntilDone:NO];\r
546             }\r
547         }\r
548     }\r
549     [prog performSelectorOnMainThread:@selector(stopAnimation:) withObject:nil waitUntilDone:NO];\r
550     [pool release];\r
553 - (void)initMaps \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
563     [self initViews];\r
564     [window setBackgroundColor:[NSColor colorWithDeviceRed:0.90 green:0.90 blue:0.90 alpha:1.0]]; //Apples 'mercury' crayon color\r
565         \r
566     NSUserDefaults *defs = [NSUserDefaults standardUserDefaults];\r
567     NSFileManager *fm = [NSFileManager defaultManager];\r
568     \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
572     {\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
577     }\r
578         [defs setObject:appVersion forKey:dkVERSION];\r
579     \r
580     NSDictionary *dict = [self readConfigFiles];\r
581     [keys addObjects:[self getKeys:dict]];\r
582     \r
583     if([[defs nonNullStringForKey:dkNAME] isEqual:@""]) \r
584     {\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
588     }\r
589     \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
593         \r
594     [self initMaps];\r
595     [self initResolutions];\r
596     server = -1;\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
599     \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
607     [self initMaps];\r
610 #pragma mark -\r
611 #pragma mark application delegate\r
613 -(BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication {\r
614     return YES;\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
629     int i;\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
635     }\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
641     return NO;\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
654     if(!url) return;\r
655     [self playFile:[NSString stringWithFormat:@"-xconnect %@", [url host]]]; \r
658 #pragma mark -\r
659 #pragma mark interface actions\r
661 - (IBAction)multiplayerAction:(id)sender \r
662 \r
663     [window makeFirstResponder:window]; //ensure fields are exited and committed\r
664     [self setServerActive:(server==-1)]; \r
667 - (IBAction)playAction:(id)sender \r
668 \r
669     [window makeFirstResponder:window]; //ensure fields are exited and committed\r
670     [self playFile:nil]; \r
673 - (IBAction)playRpg:(id)sender \r
674 \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
698 @end\r