AppDelegate.m 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. #import "AppDelegate.h"
  2. @implementation AppDelegate
  3. - (void)applicationDidFinishLaunching:(NSNotification*)aNotification
  4. {
  5. installationCompleted = NO;
  6. outputDir = [[[NSBundle mainBundle] bundlePath] stringByAppendingString:@"/../../Data"];
  7. tempDir = NSTemporaryDirectory();
  8. }
  9. - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)sender
  10. {
  11. return YES;
  12. }
  13. - (void)download:(NSURLDownload*)download didReceiveResponse:(NSURLResponse*)response
  14. {
  15. self->bytesRecieved = 0;
  16. self->bytesExpected = [response expectedContentLength];
  17. }
  18. - (void)download:(NSURLDownload*)download didReceiveDataOfLength:(NSUInteger)length
  19. {
  20. self->bytesRecieved += length;
  21. [self showProgressText:[NSString stringWithFormat:@"Downloading %@ archive: %3.1f Mb / %3.1f Mb", self->currentArchiveName,
  22. self->bytesRecieved / 1024.0f / 1024.0f, self->bytesExpected / 1024.0f / 1024.0f]];
  23. }
  24. - (void)download:(NSURLDownload*)download decideDestinationWithSuggestedFilename:(NSString*)filename
  25. {
  26. [download setDestination:[tempDir stringByAppendingString:currentArchiveFilename] allowOverwrite:YES];
  27. }
  28. - (void)downloadDidFinish:(NSURLDownload*)download
  29. {
  30. [self showProgressText:[NSString stringWithFormat:@"Downloading %@ archive: completed", self->currentArchiveName]];
  31. [self nextAction];
  32. }
  33. - (void)download:(NSURLDownload*)download didFailWithError:(NSError*)error
  34. {
  35. [self showProgressText:[NSString stringWithFormat:@"Downloading %@ archive: failed", self->currentArchiveName]];
  36. [self showErrorText:[error localizedDescription]];
  37. }
  38. - (void)nextAction
  39. {
  40. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  41. if ([actions count] > 0) {
  42. SEL sel = NSSelectorFromString(actions[0]);
  43. [actions removeObjectAtIndex:0];
  44. @try {
  45. [self performSelector:sel];
  46. }
  47. @catch (NSException* e) {
  48. [self showErrorText:[e name]];
  49. }
  50. }
  51. });
  52. }
  53. - (int)runTask:(NSString*)executable withArgs:(NSArray*)args withWorkingDir:(NSString*)workingDir withPipe:(NSPipe*)pipe
  54. {
  55. if (![executable hasPrefix:@"/usr/"]) {
  56. executable = [[[NSBundle mainBundle] resourcePath] stringByAppendingString:executable];
  57. }
  58. NSTask* task = [[NSTask alloc] init];
  59. [task setLaunchPath:executable];
  60. if (workingDir != nil) {
  61. [task setCurrentDirectoryPath:workingDir];
  62. }
  63. if (pipe != nil) {
  64. [task setStandardOutput:pipe];
  65. }
  66. [task setArguments:args];
  67. [task launch];
  68. [task waitUntilExit];
  69. return [task terminationStatus];
  70. }
  71. - (void)validateAction
  72. {
  73. // Before starting anything run validations
  74. if (![[NSFileManager defaultManager] fileExistsAtPath:[self.cd1TextField stringValue]]) {
  75. return [self showErrorText:@"Please select existing file"];
  76. }
  77. // Show progress controls
  78. [self.progressIndicator setHidden:NO];
  79. [self.progressIndicator startAnimation:self];
  80. [self showProgressText:@"Installing VCMI..."];
  81. [self nextAction];
  82. }
  83. - (void)downloadWogArchive
  84. {
  85. // First of all we need to download WoG archive
  86. // Downloading should be done on main thread because of callbacks
  87. dispatch_async(dispatch_get_main_queue(), ^{
  88. self->currentArchiveName = @"WoG";
  89. self->currentArchiveFilename = @"/wog.zip";
  90. NSURL* url = [NSURL URLWithString:@"http://download.vcmi.eu/WoG/wog.zip"];
  91. self.download = [[NSURLDownload alloc] initWithRequest:[NSURLRequest requestWithURL:url] delegate:self];
  92. });
  93. }
  94. - (void)unzipWogArchive
  95. {
  96. // Then we unzip downloaded WoG archive
  97. [self showProgressText:@"Unzipping WoG archive"];
  98. if ([self runTask:@"/usr/bin/unzip" withArgs:@[@"-qo", [tempDir stringByAppendingString:currentArchiveFilename], @"-d", outputDir] withWorkingDir:nil withPipe:nil] != 0) {
  99. return [self showErrorText:@"Failed to unzip WoG archive"];
  100. }
  101. [self nextAction];
  102. }
  103. - (void)downloadVcmiArchive
  104. {
  105. // Than we need to download VCMI archive
  106. // Downloading should be done on main thread because of callbacks
  107. dispatch_async(dispatch_get_main_queue(), ^{
  108. self->currentArchiveName = @"VCMI";
  109. self->currentArchiveFilename = @"/core.zip";
  110. NSURL* url = [NSURL URLWithString:@"http://download.vcmi.eu/core.zip"];
  111. self.download = [[NSURLDownload alloc] initWithRequest:[NSURLRequest requestWithURL:url] delegate:self];
  112. });
  113. }
  114. - (void)unzipVcmiArchive
  115. {
  116. // Then we unzip downloaded VCMI archive
  117. [self showProgressText:@"Unzipping VCMI archive"];
  118. if ([self runTask:@"/usr/bin/unzip" withArgs:@[@"-qo", [tempDir stringByAppendingString:currentArchiveFilename], @"-d", outputDir, @"-x", @"*.json", @"*.txt", @"*.PAL"] withWorkingDir:nil withPipe:nil] != 0) {
  119. return [self showErrorText:@"Failed to unzip VCMI archive"];
  120. }
  121. [self nextAction];
  122. }
  123. - (void)extractGameData
  124. {
  125. // Then we extract game data from provided iso files using unshield or from innosetup exe
  126. if ([[self.cd1TextField stringValue] hasSuffix:@".exe"]) {
  127. [self innoexctract];
  128. } else {
  129. [self unshield];
  130. }
  131. [self nextAction];
  132. }
  133. - (void)innoexctract
  134. {
  135. // Extraction via innoextact is pretty straightforward
  136. [self showProgressText:@"Extracting game data using innoextract..."];
  137. if ([self runTask:@"/innoextract" withArgs:@[[self.cd1TextField stringValue]] withWorkingDir:tempDir withPipe:nil] != 0) {
  138. [self showErrorText:@"Failed to exctract game data using innoextract"];
  139. }
  140. dataDir = [tempDir stringByAppendingString:@"/app"];
  141. }
  142. - (NSString*)attachDiskImage:(NSString*)path
  143. {
  144. [self showProgressText:[NSString stringWithFormat:@"Mounting image \"%@\"", path]];
  145. // Run hdiutil to mount specified disk image
  146. NSPipe* pipe = [NSPipe pipe];
  147. if ([self runTask:@"/usr/bin/hdiutil" withArgs:@[@"attach", path] withWorkingDir:nil withPipe:pipe] != 0) {
  148. [NSException raise:[NSString stringWithFormat:@"Failed to mount \"%@\"", path] format:nil];
  149. }
  150. // Capture hdiutil output to get mounted disk image filesystem path
  151. NSFileHandle* file = [pipe fileHandleForReading];
  152. NSString* output = [[NSString alloc] initWithData:[file readDataToEndOfFile] encoding:NSUTF8StringEncoding];
  153. NSRegularExpression* regex = [NSRegularExpression regularExpressionWithPattern:@"(/Volumes/.*)$" options:0 error:nil];
  154. NSTextCheckingResult* match = [regex firstMatchInString:output options:0 range:NSMakeRange(0, [output length])];
  155. return [output substringWithRange:[match range]];
  156. }
  157. - (void)detachDiskImage:(NSString*)mountedPath
  158. {
  159. if ([self runTask:@"/usr/bin/hdiutil" withArgs:@[@"detach", mountedPath] withWorkingDir:nil withPipe:nil] != 0) {
  160. [NSException raise:[NSString stringWithFormat:@"Failed to unmount \"%@\"", mountedPath] format:nil];
  161. }
  162. }
  163. - (void)unshield
  164. {
  165. // In case of iso files we should mount them first
  166. // If CD2 is not specified use the same path as for CD1
  167. NSString* cd1 = [self attachDiskImage:[self.cd1TextField stringValue]];
  168. NSString* cd2 = [[self.cd2TextField stringValue] isEqualToString:@""] ? cd1 : [self attachDiskImage:[self.cd2TextField stringValue]];
  169. // Extract
  170. [self showProgressText:@"Extracting game data using unshield..."];
  171. if ([self runTask:@"/unshield" withArgs:@[@"-d", tempDir, @"x", [cd1 stringByAppendingString:@"/_setup/data1.cab"]] withWorkingDir:tempDir withPipe:nil] != 0) {
  172. return [self showErrorText:@"Failed to extract game data using unshield"];
  173. }
  174. dataDir = [tempDir stringByAppendingString:@"/Heroes3"];
  175. if (![[NSFileManager defaultManager] fileExistsAtPath:dataDir]) {
  176. // Some releases have "Program_Files" folder instead of "Heroes3"
  177. dataDir = [tempDir stringByAppendingString:@"/Program_Files"];
  178. }
  179. // Unmount CD1. Unmount CD2 if needed
  180. [self detachDiskImage:cd1];
  181. if (![cd1 isEqualToString:cd2]) {
  182. [self detachDiskImage:cd2];
  183. }
  184. }
  185. - (void)extractionCompleted
  186. {
  187. // After game data is extracted we should move it to destination place
  188. [self showProgressText:@"Moving items into place"];
  189. NSFileManager* fileManager = [NSFileManager defaultManager];
  190. [fileManager moveItemAtPath:[dataDir stringByAppendingString:@"/Data"] toPath:[outputDir stringByAppendingString:@"/Data"] error:nil];
  191. [fileManager moveItemAtPath:[dataDir stringByAppendingString:@"/Maps"] toPath:[outputDir stringByAppendingString:@"/Maps"] error:nil];
  192. if ([fileManager fileExistsAtPath:[dataDir stringByAppendingString:@"/MP3"] isDirectory:nil]) {
  193. [fileManager moveItemAtPath:[dataDir stringByAppendingString:@"/MP3"] toPath:[outputDir stringByAppendingString:@"/Mp3"] error:nil];
  194. } else {
  195. [fileManager moveItemAtPath:[dataDir stringByAppendingString:@"/Mp3"] toPath:[outputDir stringByAppendingString:@"/Mp3"] error:nil];
  196. }
  197. // After everythin is complete we create marker file. VCMI will look for this file to exists on startup and
  198. // will run this setup otherwise
  199. system([[NSString stringWithFormat:@"touch %@/game_data_prepared", outputDir] UTF8String]);
  200. [self showProgressText:@"Installation complete"];
  201. [self.installButton setTitle:@"Run VCMI"];
  202. [self.progressIndicator stopAnimation:self];
  203. [NSApp requestUserAttention:NSCriticalRequest];
  204. // Hide all progress related controls
  205. [self.progressIndicator setHidden:YES];
  206. [self.progressIndicator stopAnimation:self];
  207. [self.progressLabel setHidden:YES];
  208. [self.installButton setEnabled:YES];
  209. installationCompleted = YES;
  210. }
  211. - (void)selectFile:(NSArray*)fileTypes withTextField:(NSTextField*)textField
  212. {
  213. NSOpenPanel* openPanel = [NSOpenPanel openPanel];
  214. [openPanel setCanChooseFiles:YES];
  215. [openPanel setAllowedFileTypes:fileTypes];
  216. [openPanel setAllowsMultipleSelection:NO];
  217. if ([openPanel runModal] == NSOKButton) {
  218. NSString* path = [[openPanel URL] path];
  219. [textField setStringValue:path];
  220. }
  221. }
  222. - (IBAction)selectCD1:(id)sender
  223. {
  224. [self selectFile:@[@"iso", @"exe"] withTextField:self.cd1TextField];
  225. }
  226. - (IBAction)selectCD2:(id)sender
  227. {
  228. [self selectFile:@[@"iso"] withTextField:self.cd2TextField];
  229. }
  230. - (IBAction)install:(id)sender
  231. {
  232. if (installationCompleted) {
  233. // Run vcmi
  234. system([[NSString stringWithFormat:@"open %@/../../..", [[NSBundle mainBundle] bundlePath]] UTF8String]);
  235. [NSApp terminate: nil];
  236. } else {
  237. // Run installation
  238. [self.cd1Button setEnabled:NO];
  239. [self.cd2Button setEnabled:NO];
  240. [self.installButton setEnabled:NO];
  241. actions = [NSMutableArray arrayWithObjects:
  242. @"validateAction",
  243. @"downloadWogArchive",
  244. @"unzipWogArchive",
  245. @"downloadVcmiArchive",
  246. @"unzipVcmiArchive",
  247. @"extractGameData",
  248. @"extractionCompleted",
  249. nil
  250. ];
  251. [self nextAction];
  252. }
  253. }
  254. - (void)showProgressText:(NSString*)text
  255. {
  256. // All GUI updates should be done on main thread
  257. dispatch_async(dispatch_get_main_queue(), ^{
  258. [self.progressLabel setHidden:NO];
  259. [self.progressLabel setStringValue:text];
  260. });
  261. }
  262. - (void)showErrorText:(NSString*)text
  263. {
  264. // All GUI updates should be done on main thread
  265. dispatch_async(dispatch_get_main_queue(), ^{
  266. // Show error alert
  267. NSAlert *alert = [[NSAlert alloc] init];
  268. [alert setMessageText:@"Error"];
  269. [alert setInformativeText:text];
  270. [alert beginSheetModalForWindow:self.window modalDelegate:nil didEndSelector:nil contextInfo:nil];
  271. // Enable select file buttons again
  272. [self.cd1Button setEnabled:YES];
  273. [self.cd2Button setEnabled:YES];
  274. [self.installButton setEnabled:YES];
  275. // Hide all progress related controls
  276. [self.progressIndicator setHidden:YES];
  277. [self.progressIndicator stopAnimation:self];
  278. [self.progressLabel setHidden:YES];
  279. });
  280. }
  281. @end