1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-20 20:23:03 +02:00
vcmi/osx/osx-vcmibuilder/vcmibuilder/AppDelegate.m

396 lines
14 KiB
Objective-C

#import "AppDelegate.h"
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification*)aNotification
{
installationCompleted = NO;
outputDir = [[[NSBundle mainBundle] bundlePath] stringByAppendingString:@"/../../Data"];
tempDir = NSTemporaryDirectory();
// Output to Application Support
NSArray* appSupportDirs = [[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask];
outputDir = [[appSupportDirs[0] path] stringByAppendingString:@"/vcmi"];
}
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)sender
{
return YES;
}
- (void)download:(NSURLDownload*)download didReceiveResponse:(NSURLResponse*)response
{
self->bytesRecieved = 0;
self->bytesExpected = [response expectedContentLength];
}
- (void)download:(NSURLDownload*)download didReceiveDataOfLength:(NSUInteger)length
{
self->bytesRecieved += length;
[self showProgressText:[NSString stringWithFormat:@"Downloading %@ archive: %3.1f Mb / %3.1f Mb", self->currentArchiveName,
self->bytesRecieved / 1024.0f / 1024.0f, self->bytesExpected / 1024.0f / 1024.0f]];
}
- (void)download:(NSURLDownload*)download decideDestinationWithSuggestedFilename:(NSString*)filename
{
[download setDestination:[tempDir stringByAppendingString:currentArchiveFilename] allowOverwrite:YES];
}
- (void)downloadDidFinish:(NSURLDownload*)download
{
[self showProgressText:[NSString stringWithFormat:@"Downloading %@ archive: completed", self->currentArchiveName]];
[self nextAction];
}
- (void)download:(NSURLDownload*)download didFailWithError:(NSError*)error
{
[self showProgressText:[NSString stringWithFormat:@"Downloading %@ archive: failed", self->currentArchiveName]];
[self showErrorText:[error localizedDescription]];
}
- (void)nextAction
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if ([actions count] > 0) {
SEL sel = NSSelectorFromString(actions[0]);
[actions removeObjectAtIndex:0];
@try {
[self performSelector:sel];
}
@catch (NSException* e) {
[self showErrorText:[e name]];
}
}
});
}
- (int)runTask:(NSString*)executable withArgs:(NSArray*)args withWorkingDir:(NSString*)workingDir withPipe:(NSPipe*)pipe
{
if (![executable hasPrefix:@"/usr/"]) {
executable = [[[NSBundle mainBundle] resourcePath] stringByAppendingString:executable];
}
NSTask* task = [[NSTask alloc] init];
[task setLaunchPath:executable];
if (workingDir != nil) {
[task setCurrentDirectoryPath:workingDir];
}
if (pipe != nil) {
[task setStandardOutput:pipe];
}
[task setArguments:args];
[task launch];
[task waitUntilExit];
return [task terminationStatus];
}
- (void)validateAction
{
// Before starting anything run validations
if (![[NSFileManager defaultManager] fileExistsAtPath:[self.cd1TextField stringValue]]) {
return [self showErrorText:@"Please select existing file"];
}
// Show progress controls
[self.progressIndicator setHidden:NO];
[self.progressIndicator startAnimation:self];
[self showProgressText:@"Installing VCMI..."];
[self nextAction];
}
- (void)downloadWogArchive
{
// First of all we need to download WoG archive
// Downloading should be done on main thread because of callbacks
dispatch_async(dispatch_get_main_queue(), ^{
self->currentArchiveName = @"WoG";
self->currentArchiveFilename = @"/wog.zip";
NSURL* url = [NSURL URLWithString:@"http://download.vcmi.eu/WoG/wog.zip"];
self.download = [[NSURLDownload alloc] initWithRequest:[NSURLRequest requestWithURL:url] delegate:self];
});
}
- (void)unzipWogArchive
{
// Then we unzip downloaded WoG archive
[self showProgressText:@"Unzipping WoG archive"];
if ([self runTask:@"/usr/bin/unzip" withArgs:@[@"-qo", [tempDir stringByAppendingString:currentArchiveFilename], @"-d", outputDir] withWorkingDir:nil withPipe:nil] != 0) {
return [self showErrorText:@"Failed to unzip WoG archive"];
}
[self nextAction];
}
- (void)downloadVcmiArchive
{
// Than we need to download VCMI archive
// Downloading should be done on main thread because of callbacks
dispatch_async(dispatch_get_main_queue(), ^{
self->currentArchiveName = @"VCMI";
self->currentArchiveFilename = @"/core.zip";
NSURL* url = [NSURL URLWithString:@"http://download.vcmi.eu/core.zip"];
self.download = [[NSURLDownload alloc] initWithRequest:[NSURLRequest requestWithURL:url] delegate:self];
});
}
- (void)unzipVcmiArchive
{
// Then we unzip downloaded VCMI archive
[self showProgressText:@"Unzipping VCMI archive"];
if ([self runTask:@"/usr/bin/unzip" withArgs:@[@"-qo", [tempDir stringByAppendingString:currentArchiveFilename], @"-d", outputDir, @"-x", @"*.json", @"*.txt", @"*.PAL"] withWorkingDir:nil withPipe:nil] != 0) {
return [self showErrorText:@"Failed to unzip VCMI archive"];
}
[self nextAction];
}
- (void)extractGameData
{
// Then we extract game data from provided iso files using unshield or from innosetup exe
if ([[self.cd1TextField stringValue] hasSuffix:@".exe"]) {
[self innoexctract];
} else {
[self unshield];
}
[self nextAction];
}
- (void)innoexctract
{
// Extraction via innoextact is pretty straightforward
[self showProgressText:@"Extracting game data using innoextract..."];
if ([self runTask:@"/innoextract" withArgs:@[[self.cd1TextField stringValue]] withWorkingDir:tempDir withPipe:nil] != 0) {
[self showErrorText:@"Failed to exctract game data using innoextract"];
}
dataDir = [tempDir stringByAppendingString:@"/app"];
}
- (NSString*)attachDiskImage:(NSString*)path
{
[self showProgressText:[NSString stringWithFormat:@"Mounting image \"%@\"", path]];
// Run hdiutil to mount specified disk image
NSPipe* pipe = [NSPipe pipe];
if ([self runTask:@"/usr/bin/hdiutil" withArgs:@[@"attach", path] withWorkingDir:nil withPipe:pipe] != 0) {
[NSException raise:[NSString stringWithFormat:@"Failed to mount \"%@\"", path] format:nil];
}
// Capture hdiutil output to get mounted disk image filesystem path
NSFileHandle* file = [pipe fileHandleForReading];
NSString* output = [[NSString alloc] initWithData:[file readDataToEndOfFile] encoding:NSUTF8StringEncoding];
NSRegularExpression* regex = [NSRegularExpression regularExpressionWithPattern:@"(/Volumes/.*)$" options:0 error:nil];
NSTextCheckingResult* match = [regex firstMatchInString:output options:0 range:NSMakeRange(0, [output length])];
return [output substringWithRange:[match range]];
}
- (void)detachDiskImage:(NSString*)mountedPath
{
if ([self runTask:@"/usr/bin/hdiutil" withArgs:@[@"detach", mountedPath] withWorkingDir:nil withPipe:nil] != 0) {
[NSException raise:[NSString stringWithFormat:@"Failed to unmount \"%@\"", mountedPath] format:nil];
}
}
- (void)unshield
{
// In case of iso files we should mount them first
// If CD2 is not specified use the same path as for CD1
NSString* cd1 = [self attachDiskImage:[self.cd1TextField stringValue]];
NSString* cd2 = [[self.cd2TextField stringValue] isEqualToString:@""] ? cd1 : [self attachDiskImage:[self.cd2TextField stringValue]];
// Extract
[self showProgressText:@"Extracting game data using unshield..."];
NSArray* knownLocations = @[
@"/_setup/data1.cab",
@"/Autorun/Setup/data1.cab"
];
bool success = false;
for (NSString* location in knownLocations) {
NSString* cabLocation = [cd1 stringByAppendingString:location];
if ([[NSFileManager defaultManager] fileExistsAtPath:cabLocation]) {
int result = [self runTask:@"/unshield" withArgs:@[@"-d", tempDir, @"x", cabLocation] withWorkingDir:tempDir withPipe:nil];
if (result == 0) {
success = true;
break;
}
}
}
if (!success) {
return [self showErrorText:@"Failed to extract game data using unshield"];
}
NSArray* knownDataDirs = @[
@"/Heroes3",
@"/Program_Files",
@"/Data",
];
success = false;
for (NSString* knownDir in knownDataDirs) {
dataDir = [tempDir stringByAppendingString:knownDir];
if ([[NSFileManager defaultManager] fileExistsAtPath:dataDir]) {
success = true;
break;
}
}
if (!success) {
return [self showErrorText:@"Failed to extract game data using unshield"];
}
// Unmount CD1. Unmount CD2 if needed
[self detachDiskImage:cd1];
if (![cd1 isEqualToString:cd2]) {
[self detachDiskImage:cd2];
}
}
- (void)extractionCompleted
{
// After game data is extracted we should move it to destination place
[self showProgressText:@"Moving items into place"];
NSFileManager* fileManager = [NSFileManager defaultManager];
[fileManager moveItemAtPath:[dataDir stringByAppendingString:@"/Data"] toPath:[outputDir stringByAppendingString:@"/Data"] error:nil];
[fileManager moveItemAtPath:[dataDir stringByAppendingString:@"/Maps"] toPath:[outputDir stringByAppendingString:@"/Maps"] error:nil];
if ([fileManager fileExistsAtPath:[dataDir stringByAppendingString:@"/MP3"] isDirectory:nil]) {
[fileManager moveItemAtPath:[dataDir stringByAppendingString:@"/MP3"] toPath:[outputDir stringByAppendingString:@"/Mp3"] error:nil];
} else {
[fileManager moveItemAtPath:[dataDir stringByAppendingString:@"/Mp3"] toPath:[outputDir stringByAppendingString:@"/Mp3"] error:nil];
}
// After everythin is complete we create marker file. VCMI will look for this file to exists on startup and
// will run this setup otherwise
system([[NSString stringWithFormat:@"touch \"%@/game_data_prepared\"", outputDir] UTF8String]);
[self showProgressText:@"Installation complete"];
[self.installButton setTitle:@"Run VCMI"];
[self.progressIndicator stopAnimation:self];
// Notify user that installation completed
[self showNotification:@"Installation completed"];
// Hide all progress related controls
[self.progressIndicator setHidden:YES];
[self.progressIndicator stopAnimation:self];
[self.progressLabel setHidden:YES];
[self.installButton setEnabled:YES];
installationCompleted = YES;
}
- (void)selectFile:(NSArray*)fileTypes withTextField:(NSTextField*)textField
{
NSOpenPanel* openPanel = [NSOpenPanel openPanel];
[openPanel setCanChooseFiles:YES];
[openPanel setAllowedFileTypes:fileTypes];
[openPanel setAllowsMultipleSelection:NO];
if ([openPanel runModal] == NSOKButton) {
NSString* path = [[openPanel URL] path];
[textField setStringValue:path];
}
}
- (IBAction)selectCD1:(id)sender
{
[self selectFile:@[@"iso", @"exe"] withTextField:self.cd1TextField];
}
- (IBAction)selectCD2:(id)sender
{
[self selectFile:@[@"iso"] withTextField:self.cd2TextField];
}
- (IBAction)install:(id)sender
{
if (installationCompleted) {
// Run vcmi
system([[NSString stringWithFormat:@"open %@/../../..", [[NSBundle mainBundle] bundlePath]] UTF8String]);
[NSApp terminate: nil];
} else {
// Run installation
[self.cd1Button setEnabled:NO];
[self.cd2Button setEnabled:NO];
[self.installButton setEnabled:NO];
actions = [NSMutableArray arrayWithObjects:
@"validateAction",
@"downloadWogArchive",
@"unzipWogArchive",
@"downloadVcmiArchive",
@"unzipVcmiArchive",
@"extractGameData",
@"extractionCompleted",
nil
];
[self nextAction];
}
}
- (void)showNotification:(NSString*)text
{
// Notification Center is supported only on OS X 10.8 and newer
NSUserNotification* notification = [[NSUserNotification alloc] init];
if (notification != nil) {
notification.title = @"VCMI";
notification.informativeText = text;
notification.deliveryDate = [NSDate dateWithTimeInterval:0 sinceDate:[NSDate date]];
notification.soundName = NSUserNotificationDefaultSoundName;
[[NSUserNotificationCenter defaultUserNotificationCenter] scheduleNotification:notification];
} else {
// On older OS X version force dock icon to jump
[NSApp requestUserAttention:NSCriticalRequest];
}
}
- (void)showProgressText:(NSString*)text
{
// All GUI updates should be done on main thread
dispatch_async(dispatch_get_main_queue(), ^{
[self.progressLabel setHidden:NO];
[self.progressLabel setStringValue:text];
});
}
- (void)showErrorText:(NSString*)text
{
// All GUI updates should be done on main thread
dispatch_async(dispatch_get_main_queue(), ^{
[self showNotification:@"Installation failed"];
// Show error alert
NSAlert *alert = [[NSAlert alloc] init];
[alert setMessageText:@"Error"];
[alert setInformativeText:text];
[alert beginSheetModalForWindow:self.window modalDelegate:nil didEndSelector:nil contextInfo:nil];
// Enable select file buttons again
[self.cd1Button setEnabled:YES];
[self.cd2Button setEnabled:YES];
[self.installButton setEnabled:YES];
// Hide all progress related controls
[self.progressIndicator setHidden:YES];
[self.progressIndicator stopAnimation:self];
[self.progressLabel setHidden:YES];
});
}
@end