AppDelegate.m 13 KB

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