home *** CD-ROM | disk | FTP | other *** search
/ Maximum CD 2007 September / maximum-cd-2007-09.iso / Assets / data / AssaultCube_v0.93.exe / source / xcode / Launcher.m < prev    next >
Encoding:
Text File  |  2007-06-04  |  19.4 KB  |  530 lines

  1. #import "Launcher.h"
  2. #import "ConsoleView.h"
  3. #include <stdlib.h>
  4. #include <unistd.h> /* unlink */
  5. #include <util.h> /* forkpty */
  6.  
  7. #define kMaxDisplays    16
  8.  
  9. // unless you want strings with "(null)" in them :-/
  10. @interface NSUserDefaults(Extras)
  11. - (NSString*)nonNullStringForKey:(NSString*)key;
  12. @end
  13.  
  14. @implementation NSUserDefaults(Extras)
  15. - (NSString*)nonNullStringForKey:(NSString*)key {
  16.     NSString *result = [self stringForKey:key];
  17.     return (result?result:@"");
  18. }
  19. @end
  20.  
  21.  
  22.  
  23. @interface Map : NSObject {
  24.     NSString *path;
  25. }
  26. @end
  27.  
  28. @implementation Map
  29. - (id)initWithPath:(NSString*)aPath 
  30. {
  31.     if((self = [super init])) 
  32.         path = [[aPath stringByDeletingPathExtension] retain];
  33.     return self;
  34. }
  35. - (void)dealloc 
  36. {
  37.     [path release];
  38.     [super dealloc];
  39. }
  40. - (NSString*)path { return path; }
  41. - (NSString*)name { return [path lastPathComponent]; }
  42. - (NSImage*)image { return [[NSImage alloc] initWithContentsOfFile:[path stringByAppendingString:@".jpg"]]; }
  43. - (NSString*)text 
  44. {
  45.     NSString *text = [NSString alloc];
  46.     if(![text respondsToSelector:@selector(initWithContentsOfFile:encoding:error:)])
  47.         return [text initWithContentsOfFile:[path stringByAppendingString:@".txt"]]; //deprecated in 10.4
  48.     NSError *error;
  49.     return [text initWithContentsOfFile:[path stringByAppendingString:@".txt"] encoding:NSASCIIStringEncoding error:&error]; 
  50. }
  51. - (NSString*)tickIfExists:(NSString*)ext 
  52. {
  53.     unichar tickCh = 0x2713; 
  54.     return [[NSFileManager defaultManager] fileExistsAtPath:[path stringByAppendingString:ext]] ? [NSString stringWithCharacters:&tickCh length:1] : @"";
  55. }
  56. - (NSString*)hasImage { return [self tickIfExists:@".jpg"]; }
  57. - (NSString*)hasText { return [self tickIfExists:@".txt"]; }
  58. - (NSString*)hasCfg { return [self tickIfExists:@".cfg"]; }
  59. @end
  60.  
  61.  
  62.  
  63. static int numberForKey(CFDictionaryRef desc, CFStringRef key) 
  64. {
  65.     CFNumberRef value;
  66.     int num = 0;
  67.     if ((value = CFDictionaryGetValue(desc, key)) == NULL)
  68.         return 0;
  69.     CFNumberGetValue(value, kCFNumberIntType, &num);
  70.     return num;
  71. }
  72.  
  73. @implementation Launcher
  74.  
  75. - (void)switchViews:(NSToolbarItem *)item 
  76. {
  77.     NSView *prefsView;
  78.     switch([item tag]) 
  79.     {
  80.         case 1: prefsView = view1; break;
  81.         case 2: prefsView = view2; break;
  82.         case 3: prefsView = view3; break;
  83.             //extend as see fit...
  84.         default: return;
  85.     }
  86.     
  87.     //to stop flicker, we make a temp blank view.
  88.     NSView *tempView = [[NSView alloc] initWithFrame:[[window contentView] frame]];
  89.     [window setContentView:tempView];
  90.     [tempView release];
  91.     
  92.     //mojo to get the right frame for the new window.
  93.     NSRect newFrame = [window frame];
  94.     newFrame.size.height = [prefsView frame].size.height + ([window frame].size.height - [[window contentView] frame].size.height);
  95.     newFrame.size.width = [prefsView frame].size.width;
  96.     newFrame.origin.y += ([[window contentView] frame].size.height - [prefsView frame].size.height);
  97.     
  98.     //set the frame to newFrame and animate it. 
  99.     [window setFrame:newFrame display:YES animate:YES];
  100.     //set the main content view to the new view we have picked through the if structure above.
  101.     [window setContentView:prefsView];
  102.     [window setContentMinSize:[prefsView bounds].size];
  103. }
  104.  
  105. - (NSToolbarItem*)addToolBarItem:(NSString*)name 
  106. {
  107.     int n = [toolBarItems count] + 1;
  108.     NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:[NSString stringWithFormat:@"%0d", n]];
  109.     [item setTag:n];
  110.     [item setTarget:self];
  111.     [item setAction:@selector(switchViews:)];
  112.     [toolBarItems setObject:item forKey:[item itemIdentifier]];
  113.     [item setLabel:NSLocalizedString(name, @"")];
  114.     [item setImage:[NSImage imageNamed:name]];
  115.     [item release];
  116.     return item;
  117. }
  118.  
  119. - (void)initViews 
  120. {
  121.     toolBarItems = [[NSMutableDictionary alloc] init];
  122.     NSToolbarItem *first = [self addToolBarItem:@"Main"];
  123.     [self addToolBarItem:@"Keys"];
  124.     [self addToolBarItem:@"Server"];
  125.  
  126.     NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:NSToolbarFlexibleSpaceItemIdentifier];
  127.     [toolBarItems setObject:item forKey:[item itemIdentifier]];
  128.     [item release];
  129.  
  130.     [[self addToolBarItem:@"Help"] setAction:@selector(helpAction:)];
  131.  
  132.     NSToolbar *toolbar = [[NSToolbar alloc] initWithIdentifier:@"PreferencePanes"];
  133.     [toolbar setDelegate:self]; 
  134.     [toolbar setAllowsUserCustomization:NO]; 
  135.     [toolbar setAutosavesConfiguration:NO];  
  136.     [window setToolbar:toolbar]; 
  137.     [toolbar release];
  138.     if([window respondsToSelector:@selector(setShowsToolbarButton:)]) [window setShowsToolbarButton:NO]; //10.4+
  139.     
  140.     //Make it select the first by default
  141.     [toolbar setSelectedItemIdentifier:[first itemIdentifier]];
  142.     [self switchViews:first]; 
  143. }
  144.  
  145. /*
  146.  * toolbar delegate methods
  147.  */
  148. - (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag 
  149. {
  150.     return [toolBarItems objectForKey:itemIdentifier];
  151. }
  152.  
  153. - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar*)theToolbar 
  154. {
  155.     return [self toolbarDefaultItemIdentifiers:theToolbar];
  156. }
  157.  
  158. - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar*)theToolbar 
  159. {
  160.     return [NSArray arrayWithObjects:@"1", @"2", @"3", NSToolbarFlexibleSpaceItemIdentifier, @"5", nil];
  161. }
  162.  
  163. - (NSArray *)toolbarSelectableItemIdentifiers: (NSToolbar *)toolbar {
  164.     return [NSArray arrayWithObjects:@"1", @"2", @"3", nil];
  165. }
  166.  
  167.  
  168.  
  169.  
  170.  
  171.  
  172. - (void)addResolutionsForDisplay:(CGDirectDisplayID)dspy 
  173. {
  174.     CFIndex i, cnt;
  175.     CFArrayRef modeList = CGDisplayAvailableModes(dspy);
  176.     if(modeList == NULL) return;
  177.     cnt = CFArrayGetCount(modeList);
  178.     for(i = 0; i < cnt; i++) {
  179.         CFDictionaryRef mode = CFArrayGetValueAtIndex(modeList, i);
  180.         NSString *title = [NSString stringWithFormat:@"%i x %i", numberForKey(mode, kCGDisplayWidth), numberForKey(mode, kCGDisplayHeight)];
  181.         if(![resolutions itemWithTitle:title]) [resolutions addItemWithTitle:title];
  182.     }    
  183. }
  184.  
  185. - (void)initResolutions 
  186. {
  187.     CGDirectDisplayID display[kMaxDisplays];
  188.     CGDisplayCount numDisplays;
  189.     [resolutions removeAllItems];
  190.     if(CGGetActiveDisplayList(kMaxDisplays, display, &numDisplays) == CGDisplayNoErr) 
  191.     {
  192.         CGDisplayCount i;
  193.         for (i = 0; i < numDisplays; i++)
  194.             [self addResolutionsForDisplay:display[i]];
  195.     }
  196.     [resolutions selectItemAtIndex: [[NSUserDefaults standardUserDefaults] integerForKey:@"resolution"]];    
  197. }
  198.  
  199.  
  200.  
  201.  
  202. /* directory where the executable lives */
  203. -(NSString *)cwd 
  204. {
  205.     return [[[[NSBundle mainBundle] bundlePath] stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"assaultcube"];
  206. }
  207.  
  208. /* build key array from config data */
  209. -(NSArray *)getKeys:(NSDictionary *)dict 
  210. {    
  211.     NSMutableArray *arr = [[NSMutableArray alloc] init];
  212.     NSEnumerator *e = [dict keyEnumerator];
  213.     NSString *key;
  214.     while ((key = [e nextObject])) 
  215.     {
  216.         NSString *trig;
  217.         if([key hasPrefix:@"editbind"]) 
  218.             trig = [key substringFromIndex:9];
  219.         else if([key hasPrefix:@"bind"]) 
  220.             trig = [key substringFromIndex:5];
  221.         else 
  222.             continue;
  223.         [arr addObject:[NSDictionary dictionaryWithObjectsAndKeys: //keys used in nib
  224.             trig, @"key",
  225.             [key hasPrefix:@"editbind"]?@"edit":@"", @"mode",
  226.             [dict objectForKey:key], @"action",
  227.             nil]];
  228.     }
  229.     return arr;
  230. }
  231.  
  232.  
  233. /*
  234.  * extract a dictionary from the config files containing:
  235.  * - name, gamma strings
  236.  * - bind/editbind '.' key strings
  237.  */
  238. -(NSDictionary *)readConfigFiles 
  239. {
  240.     NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
  241.     [dict setObject:@"" forKey:@"name"]; //ensure these entries are never nil
  242.     
  243.     NSString *files[] = {@"config.cfg", @"autoexec.cfg"};
  244.     int i;
  245.     for(i = 0; i < sizeof(files)/sizeof(NSString*); i++) 
  246.     {
  247.         NSString *file = [[self cwd] stringByAppendingPathComponent:files[i]];
  248.         NSArray *lines = [[NSString stringWithContentsOfFile:file] componentsSeparatedByString:@"\n"];
  249.         
  250.         if(i==0 && !lines)  // ugh - special case when first run...
  251.         { 
  252.             file = [[self cwd] stringByAppendingPathComponent:@"config/defaults.cfg"];
  253.             lines = [[NSString stringWithContentsOfFile:file] componentsSeparatedByString:@"\n"];
  254.         }
  255.         
  256.         NSString *line; 
  257.         NSEnumerator *e = [lines objectEnumerator];
  258.         while(line = [e nextObject]) 
  259.         {
  260.             NSRange r; // more flexible to do this manually rather than via NSScanner...
  261.             int j = 0;
  262.             while(j < [line length] && [line characterAtIndex:j] <= ' ') j++; //skip white
  263.             r.location = j;
  264.             while(j < [line length] && [line characterAtIndex:j] > ' ') j++; //until white
  265.             r.length = j - r.location;
  266.             NSString *type = [line substringWithRange:r];
  267.             
  268.             while(j < [line length] && [line characterAtIndex:j] <= ' ') j++; //skip white
  269.             if(j < [line length] && [line characterAtIndex:j] == '"') 
  270.             {
  271.                 r.location = ++j;
  272.                 while(j < [line length] && [line characterAtIndex:j] != '"') j++; //until close quote
  273.                 r.length = (j++) - r.location;
  274.             } else {
  275.                 r.location = j;
  276.                 while(j < [line length] && [line characterAtIndex:j] > ' ') j++; //until white
  277.                 r.length = j - r.location;
  278.             }
  279.             NSString *value = [line substringWithRange:r];
  280.             
  281.             while(j < [line length] && [line characterAtIndex:j] <= ' ') j++; //skip white
  282.             NSString *remainder = [line substringFromIndex:j];
  283.             
  284.             if([type isEqual:@"name"] || [type isEqual:@"gamma"]) 
  285.                 [dict setObject:value forKey:type];
  286.             else if([type isEqual:@"bind"] || [type isEqual:@"editbind"]) 
  287.                 [dict setObject:remainder forKey:[NSString stringWithFormat:@"%@.%@", type,value]];
  288.         }
  289.     }
  290.     return dict;
  291. }
  292.  
  293. -(void)updateAutoexecFile:(NSDictionary *)updates 
  294. {
  295.     NSString *file = [[self cwd] stringByAppendingPathComponent:@"config/autoexec.cfg"];
  296.     //build the data 
  297.     NSString *result = nil;
  298.     NSArray *lines = [[NSString stringWithContentsOfFile:file] componentsSeparatedByString:@"\n"];
  299.     if(lines) 
  300.     {
  301.         NSString *line; 
  302.         NSEnumerator *e = [lines objectEnumerator];
  303.         while(line = [e nextObject]) 
  304.         {
  305.             NSScanner *scanner = [NSScanner scannerWithString:line];
  306.             NSString *type;
  307.             if([scanner scanCharactersFromSet:[NSCharacterSet letterCharacterSet] intoString:&type])
  308.                 if([updates objectForKey:type]) continue; //skip things declared in updates
  309.             result = (result) ? [NSString stringWithFormat:@"%@\n%@", result, line] : line;
  310.         }
  311.     }
  312.     NSEnumerator *e = [updates keyEnumerator];
  313.     NSString *type;
  314.     while(type = [e nextObject]) 
  315.     {
  316.         id value = [updates objectForKey:type];
  317.         if([type isEqual:@"name"]) value = [NSString stringWithFormat:@"\"%@\"", value];
  318.         NSString *line = [NSString stringWithFormat:@"%@ %@", type, value];
  319.         result = (result) ? [NSString stringWithFormat:@"%@\n%@", result, line] : line;
  320.     }
  321.     //backup
  322.     NSFileManager *fm = [NSFileManager defaultManager];
  323.     NSString *backupfile = nil;
  324.     if([fm fileExistsAtPath:file]) 
  325.     {
  326.         backupfile = [file stringByAppendingString:@".bak"];
  327.         if(![fm movePath:file toPath:backupfile handler:nil]) return; //can't create backup
  328.     }    
  329.     //write the new file
  330.     if(![fm createFileAtPath:file contents:[result dataUsingEncoding:NSASCIIStringEncoding] attributes:nil]) return; //can't create new file        
  331.                                                                                                                      //remove the backup
  332.     if(backupfile) [fm removeFileAtPath:backupfile handler:nil];
  333. }
  334.  
  335. - (void)serverTerminated
  336. {
  337.     if(server==-1) return;
  338.     server = -1;
  339.     [multiplayer setTitle:@"Start"];
  340.     [console appendText:@"\n \n"];
  341. }
  342.  
  343. - (void)setServerActive:(BOOL)start
  344. {
  345.     if((server==-1) != start) return;
  346.     
  347.     if(!start)
  348.     {    //STOP
  349.         
  350.         //damn server, terminate isn't good enough for you - die, die, die...
  351.         if((server!=-1) && (server!=0)) kill(server, SIGKILL); //@WARNING - you do not want a 0 or -1 to be accidentally sent a  kill!
  352.         [self serverTerminated];
  353.     } 
  354.     else
  355.     {    //START
  356.         NSString *cwd = [self cwd];
  357.         NSUserDefaults *defs = [NSUserDefaults standardUserDefaults];
  358.                        
  359.         NSArray *opts = [[defs nonNullStringForKey:@"server_options"] componentsSeparatedByString:@" "];
  360.         
  361.         const char *childCwd  = [cwd fileSystemRepresentation];
  362.         const char *childPath = [[cwd stringByAppendingPathComponent:@"actioncube.app/Contents/MacOS/actioncube"] fileSystemRepresentation];
  363.         const char **args = (const char**)malloc(sizeof(char*)*([opts count] + 3 + 3)); //3 = {path, -d, NULL}, and +3 again for optional settings...
  364.         int i, fdm, argc = 0;
  365.         
  366.         args[argc++] = childPath;
  367.         args[argc++] = "-d";
  368.         
  369.         for(i = 0; i < [opts count]; i++)
  370.         {
  371.             NSString *opt = [opts objectAtIndex:i];
  372.             if([opt length] == 0) continue; //skip empty
  373.             args[argc++] = [opt UTF8String];
  374.         }
  375.         
  376.         NSString *desc = [[NSUserDefaults standardUserDefaults] nonNullStringForKey:@"server_description"];
  377.         if (![desc isEqualToString:@""]) args[argc++] = [[NSString stringWithFormat:@"-n%@", desc] UTF8String];
  378.         
  379.         NSString *pass = [[NSUserDefaults standardUserDefaults] nonNullStringForKey:@"server_password"];
  380.         if (![pass isEqualToString:@""]) args[argc++] = [[NSString stringWithFormat:@"-x%@", pass] UTF8String];
  381.         
  382.         int clients = [[NSUserDefaults standardUserDefaults] integerForKey:@"server_maxclients"];
  383.         if (clients > 0) args[argc++] = [[NSString stringWithFormat:@"-c%d", clients] UTF8String];
  384.         
  385.         args[argc++] = NULL;
  386.                 
  387.         switch ( (server = forkpty(&fdm, NULL, NULL, NULL)) ) // forkpty so we can reliably grab SDL console
  388.         { 
  389.             case -1:
  390.                 [console appendLine:@"Error - can't launch server"];
  391.                 [self serverTerminated];
  392.                 break;
  393.             case 0: // child
  394.                 chdir(childCwd);
  395.                 execv(childPath, (char*const*)args);
  396.                 fprintf(stderr, "Error - can't launch server\n");
  397.                 _exit(0);
  398.             default: // parent
  399.                 free(args);
  400.                 //fprintf(stderr, "fdm=%d\n", slave_name, fdm);
  401.                 [multiplayer setTitle:@"Stop"];
  402.                 
  403.                 NSFileHandle *taskOutput = [[NSFileHandle alloc] initWithFileDescriptor:fdm];
  404.                 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
  405.                 [nc addObserver:self selector:@selector(serverDataAvailable:) name:NSFileHandleReadCompletionNotification object:taskOutput];
  406.                 [taskOutput readInBackgroundAndNotify];
  407.                 break;
  408.         }
  409.     }
  410. }
  411.  
  412. - (void)serverDataAvailable:(NSNotification *)note
  413. {
  414.     NSFileHandle *taskOutput = [note object];
  415.     NSData *data = [[note userInfo] objectForKey:NSFileHandleNotificationDataItem];
  416.     
  417.     if (data && [data length])
  418.     {
  419.         NSString *text = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];        
  420.         [console appendText:text];
  421.         [text release];                    
  422.         [taskOutput readInBackgroundAndNotify]; //wait for more data
  423.     }
  424.     else
  425.     {
  426.         NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
  427.         [nc removeObserver:self name:NSFileHandleReadCompletionNotification object:taskOutput];
  428.         close([taskOutput fileDescriptor]);
  429.         [self setServerActive:NO];
  430.     }
  431. }
  432.  
  433. - (BOOL)playFile
  434. {    
  435.     NSUserDefaults *defs = [NSUserDefaults standardUserDefaults];
  436.     
  437.     NSArray *res = [[resolutions titleOfSelectedItem] componentsSeparatedByString:@" x "];    
  438.     NSMutableArray *args = [[NSMutableArray alloc] init];
  439.     NSString *cwd = [self cwd];
  440.     
  441.     //suppose could use this to update gamma and keys too, but can't be bothered...
  442.     [self updateAutoexecFile:[NSDictionary dictionaryWithObjectsAndKeys:
  443.         [defs nonNullStringForKey:@"name"], @"name",
  444.         nil]];
  445.     
  446.     [args addObject:[NSString stringWithFormat:@"-w%@", [res objectAtIndex:0]]];
  447.     [args addObject:[NSString stringWithFormat:@"-h%@", [res objectAtIndex:1]]];
  448.     [args addObject:@"-z32"]; 
  449.     
  450.     if([defs integerForKey:@"fullscreen"] == 0) [args addObject:@"-t"];
  451.     [args addObject:[NSString stringWithFormat:@"-a%d", [defs integerForKey:@"fsaa"]]];
  452.     [args addObject:[NSString stringWithFormat:@"-f%d", [defs integerForKey:@"shader"]]];
  453.     
  454.     NSString *adv = [defs nonNullStringForKey:@"advancedOptions"];
  455.     if(![adv isEqual:@""]) [args addObjectsFromArray:[adv componentsSeparatedByString:@" "]];
  456.     
  457.     NSTask *task = [[NSTask alloc] init];
  458.     [task setCurrentDirectoryPath:cwd];
  459.     [task setLaunchPath:[cwd stringByAppendingPathComponent:@"actioncube.app/Contents/MacOS/actioncube"]];
  460.     [task setArguments:args];
  461.     [task setEnvironment:[NSDictionary dictionaryWithObjectsAndKeys: 
  462.         @"1", @"SDL_SINGLEDISPLAY",
  463.         @"1", @"SDL_ENABLEAPPEVENTS", nil
  464.         ]]; // makes Command-H, Command-M and Command-Q work at least when not in fullscreen
  465.     [args release];
  466.     
  467.     BOOL okay = YES;
  468.     
  469.     NS_DURING
  470.         [task launch];
  471.         if(server==-1) [NSApp terminate:self];    //if there is a server then don't exit!
  472.     NS_HANDLER
  473.         //NSLog(@"%@", localException);
  474.         NSBeginCriticalAlertSheet(
  475.             @"Can't start AssaultCube", nil, nil, nil,
  476.             window, nil, nil, nil, nil,
  477.             @"Please move the directory containing AssaultCube to a path that doesn't contain weird characters or start AssaultCube manually.");
  478.         okay = NO;
  479.     NS_ENDHANDLER
  480.         
  481.     return okay;
  482. }
  483.  
  484. - (void)awakeFromNib 
  485. {
  486.     [self initViews];
  487.     
  488.     NSDictionary *dict = [self readConfigFiles];
  489.     [keys addObjects:[self getKeys:dict]];
  490.     NSUserDefaults *defs = [NSUserDefaults standardUserDefaults];
  491.     if([[defs nonNullStringForKey:@"name"] isEqual:@""]) 
  492.     {
  493.         NSString *name = [dict objectForKey:@"name"];
  494.         if([name isEqual:@""] || [name isEqual:@"unnamed"]) name = NSUserName();
  495.         [defs setValue:name forKey:@"name"];
  496.     }
  497.         
  498.     [self initResolutions];
  499.     server = -1;
  500.     [window setDelegate:self]; // so can catch the window close    
  501.     [NSApp setDelegate:self]; //so can catch the double-click & dropped files
  502. }
  503.  
  504. -(void)windowWillClose:(NSNotification *)notification 
  505. {
  506.     [self setServerActive:NO];
  507.     [NSApp terminate:self];
  508. }
  509.  
  510. /*
  511.  * Interface actions
  512.  */
  513. - (IBAction)multiplayerAction:(id)sender 
  514.     [window makeFirstResponder:window]; //ensure fields are exited and committed
  515.     [self setServerActive:(server==-1)]; 
  516. }
  517.  
  518. - (IBAction)playAction:(id)sender 
  519.     [window makeFirstResponder:window]; //ensure fields are exited and committed
  520.     [self playFile]; 
  521. }
  522.  
  523. - (IBAction)helpAction:(id)sender 
  524. {
  525.     NSString *file = [[[[NSBundle mainBundle] bundlePath] stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"README.html"];
  526.     [[NSWorkspace sharedWorkspace] openURL:[NSURL fileURLWithPath:file]];
  527. }
  528. @end