Files
assaultcube-/source/xcode/Launcher.m
T
core-code 292799aa26 mac updates
git-svn-id: svn://svn.code.sf.net/p/actiongame/code/trunk/ac@1135 e016b1c5-ed68-44aa-a2f7-31cac88cffde
2007-06-04 21:01:25 +00:00

530 lines
19 KiB
Objective-C

#import "Launcher.h"
#import "ConsoleView.h"
#include <stdlib.h>
#include <unistd.h> /* unlink */
#include <util.h> /* forkpty */
#define kMaxDisplays 16
// unless you want strings with "(null)" in them :-/
@interface NSUserDefaults(Extras)
- (NSString*)nonNullStringForKey:(NSString*)key;
@end
@implementation NSUserDefaults(Extras)
- (NSString*)nonNullStringForKey:(NSString*)key {
NSString *result = [self stringForKey:key];
return (result?result:@"");
}
@end
@interface Map : NSObject {
NSString *path;
}
@end
@implementation Map
- (id)initWithPath:(NSString*)aPath
{
if((self = [super init]))
path = [[aPath stringByDeletingPathExtension] retain];
return self;
}
- (void)dealloc
{
[path release];
[super dealloc];
}
- (NSString*)path { return path; }
- (NSString*)name { return [path lastPathComponent]; }
- (NSImage*)image { return [[NSImage alloc] initWithContentsOfFile:[path stringByAppendingString:@".jpg"]]; }
- (NSString*)text
{
NSString *text = [NSString alloc];
if(![text respondsToSelector:@selector(initWithContentsOfFile:encoding:error:)])
return [text initWithContentsOfFile:[path stringByAppendingString:@".txt"]]; //deprecated in 10.4
NSError *error;
return [text initWithContentsOfFile:[path stringByAppendingString:@".txt"] encoding:NSASCIIStringEncoding error:&error];
}
- (NSString*)tickIfExists:(NSString*)ext
{
unichar tickCh = 0x2713;
return [[NSFileManager defaultManager] fileExistsAtPath:[path stringByAppendingString:ext]] ? [NSString stringWithCharacters:&tickCh length:1] : @"";
}
- (NSString*)hasImage { return [self tickIfExists:@".jpg"]; }
- (NSString*)hasText { return [self tickIfExists:@".txt"]; }
- (NSString*)hasCfg { return [self tickIfExists:@".cfg"]; }
@end
static int numberForKey(CFDictionaryRef desc, CFStringRef key)
{
CFNumberRef value;
int num = 0;
if ((value = CFDictionaryGetValue(desc, key)) == NULL)
return 0;
CFNumberGetValue(value, kCFNumberIntType, &num);
return num;
}
@implementation Launcher
- (void)switchViews:(NSToolbarItem *)item
{
NSView *prefsView;
switch([item tag])
{
case 1: prefsView = view1; break;
case 2: prefsView = view2; break;
case 3: prefsView = view3; break;
//extend as see fit...
default: return;
}
//to stop flicker, we make a temp blank view.
NSView *tempView = [[NSView alloc] initWithFrame:[[window contentView] frame]];
[window setContentView:tempView];
[tempView release];
//mojo to get the right frame for the new window.
NSRect newFrame = [window frame];
newFrame.size.height = [prefsView frame].size.height + ([window frame].size.height - [[window contentView] frame].size.height);
newFrame.size.width = [prefsView frame].size.width;
newFrame.origin.y += ([[window contentView] frame].size.height - [prefsView frame].size.height);
//set the frame to newFrame and animate it.
[window setFrame:newFrame display:YES animate:YES];
//set the main content view to the new view we have picked through the if structure above.
[window setContentView:prefsView];
[window setContentMinSize:[prefsView bounds].size];
}
- (NSToolbarItem*)addToolBarItem:(NSString*)name
{
int n = [toolBarItems count] + 1;
NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:[NSString stringWithFormat:@"%0d", n]];
[item setTag:n];
[item setTarget:self];
[item setAction:@selector(switchViews:)];
[toolBarItems setObject:item forKey:[item itemIdentifier]];
[item setLabel:NSLocalizedString(name, @"")];
[item setImage:[NSImage imageNamed:name]];
[item release];
return item;
}
- (void)initViews
{
toolBarItems = [[NSMutableDictionary alloc] init];
NSToolbarItem *first = [self addToolBarItem:@"Main"];
[self addToolBarItem:@"Keys"];
[self addToolBarItem:@"Server"];
NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:NSToolbarFlexibleSpaceItemIdentifier];
[toolBarItems setObject:item forKey:[item itemIdentifier]];
[item release];
[[self addToolBarItem:@"Help"] setAction:@selector(helpAction:)];
NSToolbar *toolbar = [[NSToolbar alloc] initWithIdentifier:@"PreferencePanes"];
[toolbar setDelegate:self];
[toolbar setAllowsUserCustomization:NO];
[toolbar setAutosavesConfiguration:NO];
[window setToolbar:toolbar];
[toolbar release];
if([window respondsToSelector:@selector(setShowsToolbarButton:)]) [window setShowsToolbarButton:NO]; //10.4+
//Make it select the first by default
[toolbar setSelectedItemIdentifier:[first itemIdentifier]];
[self switchViews:first];
}
/*
* toolbar delegate methods
*/
- (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag
{
return [toolBarItems objectForKey:itemIdentifier];
}
- (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar*)theToolbar
{
return [self toolbarDefaultItemIdentifiers:theToolbar];
}
- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar*)theToolbar
{
return [NSArray arrayWithObjects:@"1", @"2", @"3", NSToolbarFlexibleSpaceItemIdentifier, @"5", nil];
}
- (NSArray *)toolbarSelectableItemIdentifiers: (NSToolbar *)toolbar {
return [NSArray arrayWithObjects:@"1", @"2", @"3", nil];
}
- (void)addResolutionsForDisplay:(CGDirectDisplayID)dspy
{
CFIndex i, cnt;
CFArrayRef modeList = CGDisplayAvailableModes(dspy);
if(modeList == NULL) return;
cnt = CFArrayGetCount(modeList);
for(i = 0; i < cnt; i++) {
CFDictionaryRef mode = CFArrayGetValueAtIndex(modeList, i);
NSString *title = [NSString stringWithFormat:@"%i x %i", numberForKey(mode, kCGDisplayWidth), numberForKey(mode, kCGDisplayHeight)];
if(![resolutions itemWithTitle:title]) [resolutions addItemWithTitle:title];
}
}
- (void)initResolutions
{
CGDirectDisplayID display[kMaxDisplays];
CGDisplayCount numDisplays;
[resolutions removeAllItems];
if(CGGetActiveDisplayList(kMaxDisplays, display, &numDisplays) == CGDisplayNoErr)
{
CGDisplayCount i;
for (i = 0; i < numDisplays; i++)
[self addResolutionsForDisplay:display[i]];
}
[resolutions selectItemAtIndex: [[NSUserDefaults standardUserDefaults] integerForKey:@"resolution"]];
}
/* directory where the executable lives */
-(NSString *)cwd
{
return [[[[NSBundle mainBundle] bundlePath] stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"assaultcube"];
}
/* build key array from config data */
-(NSArray *)getKeys:(NSDictionary *)dict
{
NSMutableArray *arr = [[NSMutableArray alloc] init];
NSEnumerator *e = [dict keyEnumerator];
NSString *key;
while ((key = [e nextObject]))
{
NSString *trig;
if([key hasPrefix:@"editbind"])
trig = [key substringFromIndex:9];
else if([key hasPrefix:@"bind"])
trig = [key substringFromIndex:5];
else
continue;
[arr addObject:[NSDictionary dictionaryWithObjectsAndKeys: //keys used in nib
trig, @"key",
[key hasPrefix:@"editbind"]?@"edit":@"", @"mode",
[dict objectForKey:key], @"action",
nil]];
}
return arr;
}
/*
* extract a dictionary from the config files containing:
* - name, gamma strings
* - bind/editbind '.' key strings
*/
-(NSDictionary *)readConfigFiles
{
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
[dict setObject:@"" forKey:@"name"]; //ensure these entries are never nil
NSString *files[] = {@"config.cfg", @"autoexec.cfg"};
int i;
for(i = 0; i < sizeof(files)/sizeof(NSString*); i++)
{
NSString *file = [[self cwd] stringByAppendingPathComponent:files[i]];
NSArray *lines = [[NSString stringWithContentsOfFile:file] componentsSeparatedByString:@"\n"];
if(i==0 && !lines) // ugh - special case when first run...
{
file = [[self cwd] stringByAppendingPathComponent:@"config/defaults.cfg"];
lines = [[NSString stringWithContentsOfFile:file] componentsSeparatedByString:@"\n"];
}
NSString *line;
NSEnumerator *e = [lines objectEnumerator];
while(line = [e nextObject])
{
NSRange r; // more flexible to do this manually rather than via NSScanner...
int j = 0;
while(j < [line length] && [line characterAtIndex:j] <= ' ') j++; //skip white
r.location = j;
while(j < [line length] && [line characterAtIndex:j] > ' ') j++; //until white
r.length = j - r.location;
NSString *type = [line substringWithRange:r];
while(j < [line length] && [line characterAtIndex:j] <= ' ') j++; //skip white
if(j < [line length] && [line characterAtIndex:j] == '"')
{
r.location = ++j;
while(j < [line length] && [line characterAtIndex:j] != '"') j++; //until close quote
r.length = (j++) - r.location;
} else {
r.location = j;
while(j < [line length] && [line characterAtIndex:j] > ' ') j++; //until white
r.length = j - r.location;
}
NSString *value = [line substringWithRange:r];
while(j < [line length] && [line characterAtIndex:j] <= ' ') j++; //skip white
NSString *remainder = [line substringFromIndex:j];
if([type isEqual:@"name"] || [type isEqual:@"gamma"])
[dict setObject:value forKey:type];
else if([type isEqual:@"bind"] || [type isEqual:@"editbind"])
[dict setObject:remainder forKey:[NSString stringWithFormat:@"%@.%@", type,value]];
}
}
return dict;
}
-(void)updateAutoexecFile:(NSDictionary *)updates
{
NSString *file = [[self cwd] stringByAppendingPathComponent:@"config/autoexec.cfg"];
//build the data
NSString *result = nil;
NSArray *lines = [[NSString stringWithContentsOfFile:file] componentsSeparatedByString:@"\n"];
if(lines)
{
NSString *line;
NSEnumerator *e = [lines objectEnumerator];
while(line = [e nextObject])
{
NSScanner *scanner = [NSScanner scannerWithString:line];
NSString *type;
if([scanner scanCharactersFromSet:[NSCharacterSet letterCharacterSet] intoString:&type])
if([updates objectForKey:type]) continue; //skip things declared in updates
result = (result) ? [NSString stringWithFormat:@"%@\n%@", result, line] : line;
}
}
NSEnumerator *e = [updates keyEnumerator];
NSString *type;
while(type = [e nextObject])
{
id value = [updates objectForKey:type];
if([type isEqual:@"name"]) value = [NSString stringWithFormat:@"\"%@\"", value];
NSString *line = [NSString stringWithFormat:@"%@ %@", type, value];
result = (result) ? [NSString stringWithFormat:@"%@\n%@", result, line] : line;
}
//backup
NSFileManager *fm = [NSFileManager defaultManager];
NSString *backupfile = nil;
if([fm fileExistsAtPath:file])
{
backupfile = [file stringByAppendingString:@".bak"];
if(![fm movePath:file toPath:backupfile handler:nil]) return; //can't create backup
}
//write the new file
if(![fm createFileAtPath:file contents:[result dataUsingEncoding:NSASCIIStringEncoding] attributes:nil]) return; //can't create new file
//remove the backup
if(backupfile) [fm removeFileAtPath:backupfile handler:nil];
}
- (void)serverTerminated
{
if(server==-1) return;
server = -1;
[multiplayer setTitle:@"Start"];
[console appendText:@"\n \n"];
}
- (void)setServerActive:(BOOL)start
{
if((server==-1) != start) return;
if(!start)
{ //STOP
//damn server, terminate isn't good enough for you - die, die, die...
if((server!=-1) && (server!=0)) kill(server, SIGKILL); //@WARNING - you do not want a 0 or -1 to be accidentally sent a kill!
[self serverTerminated];
}
else
{ //START
NSString *cwd = [self cwd];
NSUserDefaults *defs = [NSUserDefaults standardUserDefaults];
NSArray *opts = [[defs nonNullStringForKey:@"server_options"] componentsSeparatedByString:@" "];
const char *childCwd = [cwd fileSystemRepresentation];
const char *childPath = [[cwd stringByAppendingPathComponent:@"actioncube.app/Contents/MacOS/actioncube"] fileSystemRepresentation];
const char **args = (const char**)malloc(sizeof(char*)*([opts count] + 3 + 3)); //3 = {path, -d, NULL}, and +3 again for optional settings...
int i, fdm, argc = 0;
args[argc++] = childPath;
args[argc++] = "-d";
for(i = 0; i < [opts count]; i++)
{
NSString *opt = [opts objectAtIndex:i];
if([opt length] == 0) continue; //skip empty
args[argc++] = [opt UTF8String];
}
NSString *desc = [[NSUserDefaults standardUserDefaults] nonNullStringForKey:@"server_description"];
if (![desc isEqualToString:@""]) args[argc++] = [[NSString stringWithFormat:@"-n%@", desc] UTF8String];
NSString *pass = [[NSUserDefaults standardUserDefaults] nonNullStringForKey:@"server_password"];
if (![pass isEqualToString:@""]) args[argc++] = [[NSString stringWithFormat:@"-x%@", pass] UTF8String];
int clients = [[NSUserDefaults standardUserDefaults] integerForKey:@"server_maxclients"];
if (clients > 0) args[argc++] = [[NSString stringWithFormat:@"-c%d", clients] UTF8String];
args[argc++] = NULL;
switch ( (server = forkpty(&fdm, NULL, NULL, NULL)) ) // forkpty so we can reliably grab SDL console
{
case -1:
[console appendLine:@"Error - can't launch server"];
[self serverTerminated];
break;
case 0: // child
chdir(childCwd);
execv(childPath, (char*const*)args);
fprintf(stderr, "Error - can't launch server\n");
_exit(0);
default: // parent
free(args);
//fprintf(stderr, "fdm=%d\n", slave_name, fdm);
[multiplayer setTitle:@"Stop"];
NSFileHandle *taskOutput = [[NSFileHandle alloc] initWithFileDescriptor:fdm];
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self selector:@selector(serverDataAvailable:) name:NSFileHandleReadCompletionNotification object:taskOutput];
[taskOutput readInBackgroundAndNotify];
break;
}
}
}
- (void)serverDataAvailable:(NSNotification *)note
{
NSFileHandle *taskOutput = [note object];
NSData *data = [[note userInfo] objectForKey:NSFileHandleNotificationDataItem];
if (data && [data length])
{
NSString *text = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
[console appendText:text];
[text release];
[taskOutput readInBackgroundAndNotify]; //wait for more data
}
else
{
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc removeObserver:self name:NSFileHandleReadCompletionNotification object:taskOutput];
close([taskOutput fileDescriptor]);
[self setServerActive:NO];
}
}
- (BOOL)playFile
{
NSUserDefaults *defs = [NSUserDefaults standardUserDefaults];
NSArray *res = [[resolutions titleOfSelectedItem] componentsSeparatedByString:@" x "];
NSMutableArray *args = [[NSMutableArray alloc] init];
NSString *cwd = [self cwd];
//suppose could use this to update gamma and keys too, but can't be bothered...
[self updateAutoexecFile:[NSDictionary dictionaryWithObjectsAndKeys:
[defs nonNullStringForKey:@"name"], @"name",
nil]];
[args addObject:[NSString stringWithFormat:@"-w%@", [res objectAtIndex:0]]];
[args addObject:[NSString stringWithFormat:@"-h%@", [res objectAtIndex:1]]];
[args addObject:@"-z32"];
if([defs integerForKey:@"fullscreen"] == 0) [args addObject:@"-t"];
[args addObject:[NSString stringWithFormat:@"-a%d", [defs integerForKey:@"fsaa"]]];
[args addObject:[NSString stringWithFormat:@"-f%d", [defs integerForKey:@"shader"]]];
NSString *adv = [defs nonNullStringForKey:@"advancedOptions"];
if(![adv isEqual:@""]) [args addObjectsFromArray:[adv componentsSeparatedByString:@" "]];
NSTask *task = [[NSTask alloc] init];
[task setCurrentDirectoryPath:cwd];
[task setLaunchPath:[cwd stringByAppendingPathComponent:@"actioncube.app/Contents/MacOS/actioncube"]];
[task setArguments:args];
[task setEnvironment:[NSDictionary dictionaryWithObjectsAndKeys:
@"1", @"SDL_SINGLEDISPLAY",
@"1", @"SDL_ENABLEAPPEVENTS", nil
]]; // makes Command-H, Command-M and Command-Q work at least when not in fullscreen
[args release];
BOOL okay = YES;
NS_DURING
[task launch];
if(server==-1) [NSApp terminate:self]; //if there is a server then don't exit!
NS_HANDLER
//NSLog(@"%@", localException);
NSBeginCriticalAlertSheet(
@"Can't start AssaultCube", nil, nil, nil,
window, nil, nil, nil, nil,
@"Please move the directory containing AssaultCube to a path that doesn't contain weird characters or start AssaultCube manually.");
okay = NO;
NS_ENDHANDLER
return okay;
}
- (void)awakeFromNib
{
[self initViews];
NSDictionary *dict = [self readConfigFiles];
[keys addObjects:[self getKeys:dict]];
NSUserDefaults *defs = [NSUserDefaults standardUserDefaults];
if([[defs nonNullStringForKey:@"name"] isEqual:@""])
{
NSString *name = [dict objectForKey:@"name"];
if([name isEqual:@""] || [name isEqual:@"unnamed"]) name = NSUserName();
[defs setValue:name forKey:@"name"];
}
[self initResolutions];
server = -1;
[window setDelegate:self]; // so can catch the window close
[NSApp setDelegate:self]; //so can catch the double-click & dropped files
}
-(void)windowWillClose:(NSNotification *)notification
{
[self setServerActive:NO];
[NSApp terminate:self];
}
/*
* Interface actions
*/
- (IBAction)multiplayerAction:(id)sender
{
[window makeFirstResponder:window]; //ensure fields are exited and committed
[self setServerActive:(server==-1)];
}
- (IBAction)playAction:(id)sender
{
[window makeFirstResponder:window]; //ensure fields are exited and committed
[self playFile];
}
- (IBAction)helpAction:(id)sender
{
NSString *file = [[[[NSBundle mainBundle] bundlePath] stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"README.html"];
[[NSWorkspace sharedWorkspace] openURL:[NSURL fileURLWithPath:file]];
}
@end