Oolite 1.91.0.7644-241112-7f5034b
Loading...
Searching...
No Matches
ResourceManager.m
Go to the documentation of this file.
1/*
2
3ResourceManager.m
4
5Oolite
6Copyright (C) 2004-2013 Giles C Williams and contributors
7
8This program is free software; you can redistribute it and/or
9modify it under the terms of the GNU General Public License
10as published by the Free Software Foundation; either version 2
11of the License, or (at your option) any later version.
12
13This program is distributed in the hope that it will be useful,
14but WITHOUT ANY WARRANTY; without even the implied warranty of
15MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16GNU General Public License for more details.
17
18You should have received a copy of the GNU General Public License
19along with this program; if not, write to the Free Software
20Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
21MA 02110-1301, USA.
22
23*/
24
25#import "ResourceManager.h"
29#import "OOSound.h"
30#import "OOCacheManager.h"
31#import "Universe.h"
32#import "OOStringParsing.h"
33#import "OOPListParsing.h"
34#import "MyOpenGLView.h"
39#import "OOOXZManager.h"
40#import "unzip.h"
41#import "HeadUpDisplay.h"
42#import "OODebugStandards.h"
44
45#import "OOJSScript.h"
46#import "OOPListScript.h"
47
49
50static NSString * const kOOLogCacheUpToDate = @"dataCache.upToDate";
51static NSString * const kOOLogCacheExplicitFlush = @"dataCache.rebuild.explicitFlush";
52static NSString * const kOOLogCacheStalePaths = @"dataCache.rebuild.pathsChanged";
53static NSString * const kOOLogCacheStaleDates = @"dataCache.rebuild.datesChanged";
54static NSString * const kOOCacheSearchPathModDates = @"search path modification dates";
55static NSString * const kOOCacheKeySearchPaths = @"search paths";
56static NSString * const kOOCacheKeyModificationDates = @"modification dates";
57
58
59
60extern NSDictionary* ParseOOSScripts(NSString* script);
61
62
63@interface ResourceManager (OOPrivate)
64
65+ (void) checkOXPMessagesInPath:(NSString *)path;
66+ (void) checkPotentialPath:(NSString *)path :(NSMutableArray *)searchPaths;
67+ (BOOL) validateManifest:(NSDictionary*)manifest forOXP:(NSString *)path;
68+ (BOOL) areRequirementsFulfilled:(NSDictionary*)requirements forOXP:(NSString *)path andFile:(NSString *)file;
69+ (void) filterSearchPathsForConflicts:(NSMutableArray *)searchPaths;
70+ (BOOL) filterSearchPathsForRequirements:(NSMutableArray *)searchPaths;
71+ (void) filterSearchPathsToExcludeScenarioOnlyPaths:(NSMutableArray *)searchPaths;
72+ (void) filterSearchPathsByScenario:(NSMutableArray *)searchPaths;
73+ (BOOL) manifestAllowedByScenario:(NSDictionary *)manifest;
74+ (BOOL) manifestAllowedByScenario:(NSDictionary *)manifest withIdentifier:(NSString *)identifier;
75+ (BOOL) manifestAllowedByScenario:(NSDictionary *)manifest withTag:(NSString *)tag;
76
77+ (void) addErrorWithKey:(NSString *)descriptionKey param1:(id)param1 param2:(id)param2;
78+ (BOOL) checkCacheUpToDateForPaths:(NSArray *)searchPaths;
79+ (void) logPaths;
80+ (void) mergeRoleCategories:(NSDictionary *)catData intoDictionary:(NSMutableDictionary *)category;
81+ (void) preloadFileLists;
82+ (void) preloadFileListFromOXZ:(NSString *)path forFolders:(NSArray *)folders;
83+ (void) preloadFileListFromFolder:(NSString *)path forFolders:(NSArray *)folders;
84+ (void) preloadFilePathFor:(NSString *)fileName inFolder:(NSString *)subFolder atPath:(NSString *)path;
85
86@end
87
88
89static NSMutableArray *sSearchPaths;
90static NSString *sUseAddOns;
91static NSArray *sUseAddOnsParts;
92static BOOL sFirstRun = YES;
93static BOOL sAllMet = NO;
94static NSMutableArray *sOXPsWithMessagesFound;
95static NSMutableArray *sExternalPaths;
96static NSMutableArray *sErrors;
97static NSMutableDictionary *sOXPManifests;
98
99
100
101// caches allow us to load any given file once only
102//
103static NSMutableDictionary *sSoundCache;
104static NSMutableDictionary *sStringCache;
105
106
107
108@implementation ResourceManager
109
110+ (void) reset
111{
112 sFirstRun = YES;
120}
121
122
123+ (void) resetManifestKnowledgeForOXZManager
124{
130}
131
132
133+ (NSString *) errors
134{
135 NSArray *error = nil;
136 NSUInteger i, count;
137 NSMutableArray *result = nil;
138 NSString *errStr = nil;
139
140 count = [sErrors count];
141 if (count == 0) return nil;
142
143 // Expand error messages. This is deferred for localizability.
144 result = [NSMutableArray arrayWithCapacity:count];
145 for (i = 0; i != count; ++i)
146 {
147 error = [sErrors objectAtIndex:i];
148 errStr = [UNIVERSE descriptionForKey:[error oo_stringAtIndex:0]];
149 if (errStr != nil)
150 {
151 errStr = [NSString stringWithFormat:errStr, [error objectAtIndex:1], [error objectAtIndex:2]];
152 [result addObject:errStr];
153 }
154 }
155
156 [sErrors release];
157 sErrors = nil;
158
159 return [result componentsJoinedByString:@"\n"];
160}
161
162
163+ (NSArray *)rootPaths
164{
165 static NSArray *sRootPaths = nil;
166 if (sRootPaths == nil) {
167 /* Built-in data, then managed OXZs, then manually installed ones,
168 * which may be useful for debugging/testing purposes. */
169 sRootPaths = [NSArray arrayWithObjects:[self builtInPath], [[OOOXZManager sharedManager] installPath], nil];
170 sRootPaths = [[sRootPaths arrayByAddingObjectsFromArray:[self userRootPaths]] retain];
171 }
172 return sRootPaths;
173}
174
175
176+ (NSArray *)userRootPaths
177{
178 static NSArray *sUserRootPaths = nil;
179
180 if (sUserRootPaths == nil)
181 {
182 // the paths are now in order of preference as per yesterday's talk. -- Kaks 2010-05-05
183
184 sUserRootPaths = [[NSArray alloc] initWithObjects:
185
186#if OOLITE_MAC_OS_X
187 [[[[NSHomeDirectory() stringByAppendingPathComponent:@"Library"]
188 stringByAppendingPathComponent:@"Application Support"]
189 stringByAppendingPathComponent:@"Oolite"]
190 stringByAppendingPathComponent:@"AddOns"],
191 [[[[NSBundle mainBundle] bundlePath]
192 stringByDeletingLastPathComponent]
193 stringByAppendingPathComponent:@"AddOns"],
194
195#elif OOLITE_WINDOWS
196 @"../AddOns",
197#else
198 @"AddOns",
199#endif
200
201#if !OOLITE_WINDOWS
202 [[NSHomeDirectory()
203 stringByAppendingPathComponent:@".Oolite"]
204 stringByAppendingPathComponent:@"AddOns"],
205#endif
206
207 nil];
208 }
209 OOLog(@"searchPaths.debug",@"%@",sUserRootPaths);
210 return sUserRootPaths;
211}
212
213
214+ (NSString *)builtInPath
215{
216#if OOLITE_WINDOWS
217 /* [[NSBundle mainBundle] resourcePath] causes complaints under Windows,
218 because we don't have a properly-built bundle.
219 */
220 return @"Resources";
221#else
222 return [[NSBundle mainBundle] resourcePath];
223#endif
224}
225
226
227+ (NSArray *)pathsWithAddOns
228{
229 if ([sSearchPaths count] > 0) return sSearchPaths;
230
231 if (sUseAddOns == nil)
232 {
233 sUseAddOns = [[NSString alloc] initWithString:SCENARIO_OXP_DEFINITION_ALL];
234 sUseAddOnsParts = [[sUseAddOns componentsSeparatedByString:@";"] retain];
235 }
236
237 /* Handle special case of 'strict mode' efficiently */
238 // testing actual string
239 if ([sUseAddOns isEqualToString:SCENARIO_OXP_DEFINITION_NONE])
240 {
241 return (NSArray *)[NSArray arrayWithObject:[self builtInPath]];
242 }
243
244 [sErrors release];
245 sErrors = nil;
246
247 NSFileManager *fmgr = [NSFileManager defaultManager];
248 NSArray *rootPaths = nil;
249 NSMutableArray *existingRootPaths = nil;
250 NSEnumerator *pathEnum = nil;
251 NSString *root = nil;
252 NSDirectoryEnumerator *dirEnum = nil;
253 NSString *subPath = nil;
254 NSString *path = nil;
255 BOOL isDirectory;
256
257 // Copy those root paths that actually exist to search paths.
258 rootPaths = [self rootPaths];
259 existingRootPaths = [NSMutableArray arrayWithCapacity:[rootPaths count]];
260 for (pathEnum = [rootPaths objectEnumerator]; (root = [pathEnum nextObject]); )
261 {
262 if ([fmgr fileExistsAtPath:root isDirectory:&isDirectory] && isDirectory)
263 {
264 [existingRootPaths addObject:root];
265 }
266 }
267
268 // validate default search paths
270 sSearchPaths = [NSMutableArray new];
271 foreach(path, existingRootPaths)
272 {
273 [self checkPotentialPath:path :sSearchPaths];
274 }
275
276 // Iterate over root paths.
277 for (pathEnum = [existingRootPaths objectEnumerator]; (root = [pathEnum nextObject]); )
278 {
279 // Iterate over each root path's contents.
280 if ([fmgr fileExistsAtPath:root isDirectory:&isDirectory] && isDirectory)
281 {
282 for (dirEnum = [fmgr enumeratorAtPath:root]; (subPath = [dirEnum nextObject]); )
283 {
284 // Check if it's a directory.
285 path = [root stringByAppendingPathComponent:subPath];
286 if ([fmgr fileExistsAtPath:path isDirectory:&isDirectory])
287 {
288 if (isDirectory)
289 {
290 // If it is, is it an OXP?.
291 if ([[[path pathExtension] lowercaseString] isEqualToString:@"oxp"])
292 {
293 [self checkPotentialPath:path :sSearchPaths];
294 if ([sSearchPaths containsObject:path]) [self checkOXPMessagesInPath:path];
295 }
296 else
297 {
298 // If not, don't search subdirectories.
299 [dirEnum skipDescendents];
300 }
301 }
302 else
303 {
304 // If not a directory, is it an OXZ?
305 if ([[[path pathExtension] lowercaseString] isEqualToString:@"oxz"])
306 {
307 [self checkPotentialPath:path :sSearchPaths];
308 if ([sSearchPaths containsObject:path]) [self checkOXPMessagesInPath:path];
309 }
310 }
311 }
312 }
313 }
314 }
315
316 for (pathEnum = [sExternalPaths objectEnumerator]; (path = [pathEnum nextObject]); )
317 {
318 [self checkPotentialPath:path :sSearchPaths];
319 if ([sSearchPaths containsObject:path]) [self checkOXPMessagesInPath:path];
320 }
321
322 /* If a scenario restriction is *not* in place, remove
323 * scenario-only OXPs. */
324 // test string
325 if ([sUseAddOns isEqualToString:SCENARIO_OXP_DEFINITION_ALL])
326 {
327 [self filterSearchPathsToExcludeScenarioOnlyPaths:sSearchPaths];
328 }
329
330 /* This is a conservative filter. It probably gets rid of more
331 * OXPs than it technically needs to in certain situations with
332 * dependency chains, but really any conflict here needs to be
333 * resolved by the user rather than Oolite. The point is to avoid
334 * loading OXPs which we shouldn't; if doing so takes out other
335 * OXPs which would have been safe, that's not important. */
336 [self filterSearchPathsForConflicts:sSearchPaths];
337
338 /* This one needs to be run repeatedly to be sure. Take the chain
339 * A depends on B depends on C. A and B are installed. A is
340 * checked first, and depends on B, which is thought to be
341 * okay. So A is kept. Then B is checked and removed. A must then
342 * be rechecked. This function therefore is run repeatedly until a
343 * run of it removes no further items.
344 *
345 * There may well be more elegant and efficient ways to do this
346 * but this is already fast enough for most purposes.
347 */
348 while (![self filterSearchPathsForRequirements:sSearchPaths]) {}
349
350 /* If a scenario restriction is in place, restrict OXPs to the
351 * ones valid for the scenario only. */
352 // test string
353 if (![sUseAddOns isEqualToString:SCENARIO_OXP_DEFINITION_ALL])
354 {
355 [self filterSearchPathsByScenario:sSearchPaths];
356 }
357
358 [self checkCacheUpToDateForPaths:sSearchPaths];
359
360 return sSearchPaths;
361}
362
363
364+ (void) preloadFileLists
365{
366 NSString *path = nil;
367 NSEnumerator *pathEnum = nil;
368
369 // folders which may contain files to be cached
370 NSArray *folders = [NSArray arrayWithObjects:@"AIs",@"Images",@"Models",@"Music",@"Scenarios",@"Scripts",@"Shaders",@"Sounds",@"Textures",nil];
371
372 for (pathEnum = [[ResourceManager paths] reverseObjectEnumerator]; (path = [pathEnum nextObject]); )
373 {
374 if ([path hasSuffix:@".oxz"])
375 {
376 [self preloadFileListFromOXZ:path forFolders:folders];
377 }
378 else
379 {
380 [self preloadFileListFromFolder:path forFolders:folders];
381 }
382 }
383}
384
385
386+ (void) preloadFileListFromOXZ:(NSString *)path forFolders:(NSArray *)folders
387{
388 unzFile uf = NULL;
389 const char* zipname = [path UTF8String];
390 char componentName[512];
391
392 if (zipname != NULL)
393 {
394 uf = unzOpen64(zipname);
395 }
396 if (uf == NULL)
397 {
398 OOLog(@"resourceManager.error",@"Could not open .oxz at %@ as zip file",path);
399 return;
400 }
401 if (unzGoToFirstFile(uf) == UNZ_OK)
402 {
403 do
404 {
406 componentName, 512,
407 NULL, 0,
408 NULL, 0);
409 NSString *zipEntry = [NSString stringWithUTF8String:componentName];
410 NSArray *pathBits = [zipEntry pathComponents];
411 if ([pathBits count] >= 2)
412 {
413 NSString *folder = [pathBits oo_stringAtIndex:0];
414 if ([folders containsObject:folder])
415 {
416 NSRange bitRange;
417 bitRange.location = 1;
418 bitRange.length = [pathBits count]-1;
419 NSString *file = [NSString pathWithComponents:[pathBits subarrayWithRange:bitRange]];
420 NSString *fullPath = [[path stringByAppendingPathComponent:folder] stringByAppendingPathComponent:file];
421
422 [self preloadFilePathFor:file inFolder:folder atPath:fullPath];
423 }
424 }
425
426 }
427 while (unzGoToNextFile(uf) == UNZ_OK);
428 }
429 unzClose(uf);
430
431}
432
433
434+ (void) preloadFileListFromFolder:(NSString *)path forFolders:(NSArray *)folders
435{
436 NSFileManager *fmgr = [NSFileManager defaultManager];
437 NSString *subFolder = nil;
438 NSString *subFolderPath = nil;
439 NSArray *fileList = nil;
440 NSString *fileName = nil;
441
442 // search each subfolder for files
443 foreach (subFolder, folders)
444 {
445 subFolderPath = [path stringByAppendingPathComponent:subFolder];
446 fileList = [fmgr oo_directoryContentsAtPath:subFolderPath];
447 foreach (fileName, fileList)
448 {
449 [self preloadFilePathFor:fileName inFolder:subFolder atPath:[subFolderPath stringByAppendingPathComponent:fileName]];
450 }
451 }
452
453}
454
455
456+ (void) preloadFilePathFor:(NSString *)fileName inFolder:(NSString *)subFolder atPath:(NSString *)path
457{
459 NSString *cacheKey = [NSString stringWithFormat:@"%@/%@", subFolder, fileName];
460 NSString *result = [cache objectForKey:cacheKey inCache:@"resolved paths"];
461 // if nil, not found in another OXP already
462 if (result == nil)
463 {
464 OOLog(@"resourceManager.foundFile.preLoad", @"Found %@/%@ at %@", subFolder, fileName, path);
465 [cache setObject:path forKey:cacheKey inCache:@"resolved paths"];
466 }
467}
468
469
470
471+ (NSArray *)paths
472{
474 {
475 sSearchPaths = [[NSMutableArray alloc] init];
476 }
477 return [self pathsWithAddOns];
478}
479
480
481+ (NSString *)useAddOns
482{
483 return sUseAddOns;
484}
485
486
487+ (void)setUseAddOns:(NSString *)useAddOns
488{
489 if (sFirstRun || ![useAddOns isEqualToString:sUseAddOns])
490 {
491 [self reset];
492 sFirstRun = NO;
495 sUseAddOns = [useAddOns retain];
496 sUseAddOnsParts = [[sUseAddOns componentsSeparatedByString:@";"] retain];
497
500
502 /* only allow cache writes for the "all OXPs" default
503 *
504 * cache should be less necessary for restricted sets anyway */
505 // testing the actual string here
506 if ([sUseAddOns isEqualToString:SCENARIO_OXP_DEFINITION_ALL])
507 {
508 [cmgr reloadAllCaches];
509 [cmgr setAllowCacheWrites:YES];
510 }
511 else
512 {
513 [cmgr clearAllCaches];
514 [cmgr setAllowCacheWrites:NO];
515 }
516
517 [self checkCacheUpToDateForPaths:[self paths]];
518 [self logPaths];
519 /* preloading the file lists at this stage helps efficiency a
520 * lot when many OXZs are installed */
521 [self preloadFileLists];
522
523 }
524}
525
526
527+ (void) addExternalPath:(NSString *)path
528{
529 if (sSearchPaths == nil) sSearchPaths = [[NSMutableArray alloc] init];
530 if (![sSearchPaths containsObject:path])
531 {
532 [sSearchPaths addObject:path];
533
534 if (sExternalPaths == nil) sExternalPaths = [[NSMutableArray alloc] init];
535 [sExternalPaths addObject:path];
536 }
537}
538
539
540+ (NSEnumerator *)pathEnumerator
541{
542 return [[self paths] objectEnumerator];
543}
544
545
546+ (NSEnumerator *)reversePathEnumerator
547{
548 return [[self paths] reverseObjectEnumerator];
549}
550
551
552+ (NSArray *)OXPsWithMessagesFound
553{
554 return [[sOXPsWithMessagesFound copy] autorelease];
555}
556
557
558+ (NSDictionary *)manifestForIdentifier:(NSString *)identifier
559{
560 return [sOXPManifests objectForKey:identifier];
561}
562
563
564+ (void) checkOXPMessagesInPath:(NSString *)path
565{
566 NSArray *OXPMessageArray = OOArrayFromFile([path stringByAppendingPathComponent:@"OXPMessages.plist"]);
567
568 if ([OXPMessageArray count] > 0)
569 {
570 unsigned i;
571 for (i = 0; i < [OXPMessageArray count]; i++)
572 {
573 NSString *oxpMessage = [OXPMessageArray oo_stringAtIndex:i];
574 if (oxpMessage)
575 {
576 OOLog(@"oxp.message", @"%@: %@", path, oxpMessage);
577 }
578 }
579 if (sOXPsWithMessagesFound == nil) sOXPsWithMessagesFound = [[NSMutableArray alloc] init];
580 [sOXPsWithMessagesFound addObject:[path lastPathComponent]];
581 }
582}
583
584
585// Given a path to an assumed OXP (or other location where files are permissible), check for a requires.plist or manifest.plist and add to search paths if acceptable.
586+ (void)checkPotentialPath:(NSString *)path :(NSMutableArray *)searchPaths
587{
588 NSDictionary *requirements = nil;
589 NSDictionary *manifest = nil;
590 BOOL requirementsMet = YES;
591
592 if (![[[path pathExtension] lowercaseString] isEqualToString:@"oxz"])
593 {
594 // OXZ format ignores requires.plist
595 requirements = OODictionaryFromFile([path stringByAppendingPathComponent:@"requires.plist"]);
596 requirementsMet = [self areRequirementsFulfilled:requirements forOXP:path andFile:@"requires.plist"];
597 }
598 if (!requirementsMet)
599 {
600 NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"];
601 OOLog(@"oxp.versionMismatch", @"OXP %@ is incompatible with version %@ of Oolite.", path, version);
602 [self addErrorWithKey:@"oxp-is-incompatible" param1:[path lastPathComponent] param2:version];
603 return;
604 }
605
606 manifest = OODictionaryFromFile([path stringByAppendingPathComponent:@"manifest.plist"]);
607 if (manifest == nil)
608 {
609 if ([[[path pathExtension] lowercaseString] isEqualToString:@"oxz"])
610 {
611 OOLog(@"oxp.noManifest", @"OXZ %@ has no manifest.plist", path);
612 [self addErrorWithKey:@"oxz-lacks-manifest" param1:[path lastPathComponent] param2:nil];
613 return;
614 }
615 else
616 {
617 if ([[[path pathExtension] lowercaseString] isEqualToString:@"oxp"])
618 {
619 OOStandardsError([NSString stringWithFormat:@"OXP %@ has no manifest.plist", path]);
620 if (OOEnforceStandards())
621 {
622 [self addErrorWithKey:@"oxp-lacks-manifest" param1:[path lastPathComponent] param2:nil];
623 return;
624 }
625 }
626 // make up a basic manifest in relaxed mode or for base folders
627 manifest = [NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithFormat:@"__oolite.tmp.%@",path],kOOManifestIdentifier,@"1",kOOManifestVersion,@"OXP without manifest",kOOManifestTitle,@"1",kOOManifestRequiredOoliteVersion,nil];
628 }
629 }
630
631 requirementsMet = [self validateManifest:manifest forOXP:path];
632
633
634 if (requirementsMet)
635 {
636 [searchPaths addObject:path];
637 }
638}
639
640
641+ (BOOL) validateManifest:(NSDictionary*)manifest forOXP:(NSString *)path
642{
644 {
645 sOXPManifests = [[NSMutableDictionary alloc] initWithCapacity:32];
646 }
647
648 BOOL OK = YES;
649 NSString *identifier = [manifest oo_stringForKey:kOOManifestIdentifier defaultValue:nil];
650 NSString *version = [manifest oo_stringForKey:kOOManifestVersion defaultValue:nil];
651 NSString *required = [manifest oo_stringForKey:kOOManifestRequiredOoliteVersion defaultValue:nil];
652 NSString *title = [manifest oo_stringForKey:kOOManifestTitle defaultValue:nil];
653
654 if (identifier == nil)
655 {
656 OOLog(@"oxp.noManifest", @"OXZ %@ manifest.plist has no '%@' field.", path, kOOManifestIdentifier);
657 [self addErrorWithKey:@"oxp-manifest-incomplete" param1:title param2:kOOManifestIdentifier];
658 OK = NO;
659 }
660 if (version == nil)
661 {
662 OOLog(@"oxp.noManifest", @"OXZ %@ manifest.plist has no '%@' field.", path, kOOManifestVersion);
663 [self addErrorWithKey:@"oxp-manifest-incomplete" param1:title param2:kOOManifestVersion];
664 OK = NO;
665 }
666 if (required == nil)
667 {
668 OOLog(@"oxp.noManifest", @"OXZ %@ manifest.plist has no '%@' field.", path, kOOManifestRequiredOoliteVersion);
669 [self addErrorWithKey:@"oxp-manifest-incomplete" param1:title param2:kOOManifestRequiredOoliteVersion];
670 OK = NO;
671 }
672 if (title == nil)
673 {
674 OOLog(@"oxp.noManifest", @"OXZ %@ manifest.plist has no '%@' field.", path, kOOManifestTitle);
675 [self addErrorWithKey:@"oxp-manifest-incomplete" param1:title param2:kOOManifestTitle];
676 OK = NO;
677 }
678 if (!OK)
679 {
680 return NO;
681 }
682 OK = [self checkVersionCompatibility:manifest forOXP:title];
683
684 if (!OK)
685 {
686 NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"];
687 OOLog(@"oxp.versionMismatch", @"OXP %@ is incompatible with version %@ of Oolite.", path, version);
688 [self addErrorWithKey:@"oxp-is-incompatible" param1:[path lastPathComponent] param2:version];
689 return NO;
690 }
691
692 NSDictionary *duplicate = [sOXPManifests objectForKey:identifier];
693 if (duplicate != nil)
694 {
695 OOLog(@"oxp.duplicate", @"OXP %@ has the same identifier (%@) as %@ which has already been loaded.",path,identifier,[duplicate oo_stringForKey:kOOManifestFilePath]);
696 [self addErrorWithKey:@"oxp-manifest-duplicate" param1:path param2:[duplicate oo_stringForKey:kOOManifestFilePath]];
697 return NO;
698 }
699 NSMutableDictionary *mData = [NSMutableDictionary dictionaryWithDictionary:manifest];
700 [mData setObject:path forKey:kOOManifestFilePath];
701 // add an extra key
702 [sOXPManifests setObject:mData forKey:identifier];
703 return YES;
704}
705
706
707+ (BOOL) checkVersionCompatibility:(NSDictionary *)manifest forOXP:(NSString *)title
708{
709 NSString *required = [manifest oo_stringForKey:kOOManifestRequiredOoliteVersion defaultValue:nil];
710 NSString *maxRequired = [manifest oo_stringForKey:kOOManifestMaximumOoliteVersion defaultValue:nil];
711 // ignore empty max version string rather than treating as "version 0"
712 if (maxRequired == nil || [maxRequired length] == 0)
713 {
714 return [self areRequirementsFulfilled:[NSDictionary dictionaryWithObjectsAndKeys:required, @"version", nil] forOXP:title andFile:@"manifest.plist"];
715 }
716 else
717 {
718 return [self areRequirementsFulfilled:[NSDictionary dictionaryWithObjectsAndKeys:required, @"version", maxRequired, @"max_version", nil] forOXP:title andFile:@"manifest.plist"];
719 }
720}
721
722
723+ (BOOL) areRequirementsFulfilled:(NSDictionary*)requirements forOXP:(NSString *)path andFile:(NSString *)file
724{
725 BOOL OK = YES;
726 NSString *requiredVersion = nil;
727 NSString *maxVersion = nil;
728 unsigned conditionsHandled = 0;
729 static NSArray *ooVersionComponents = nil;
730 NSArray *oxpVersionComponents = nil;
731
732 if (requirements == nil) return YES;
733
734 if (ooVersionComponents == nil)
735 {
736 ooVersionComponents = ComponentsFromVersionString([[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]);
737 [ooVersionComponents retain];
738 }
739
740 // Check "version" (minimum version)
741 if (OK)
742 {
743 // Not oo_stringForKey:, because we need to be able to complain about non-strings.
744 requiredVersion = [requirements objectForKey:@"version"];
745 if (requiredVersion != nil)
746 {
747 ++conditionsHandled;
748 if ([requiredVersion isKindOfClass:[NSString class]])
749 {
750 oxpVersionComponents = ComponentsFromVersionString(requiredVersion);
751 if (NSOrderedAscending == CompareVersions(ooVersionComponents, oxpVersionComponents)) OK = NO;
752 }
753 else
754 {
755 OOLog(@"requirements.wrongType", @"Expected %@ entry \"%@\" to be string, but got %@ in OXP %@.", file, @"version", [requirements class], [path lastPathComponent]);
756 OK = NO;
757 }
758 }
759 }
760
761 // Check "max_version" (minimum max_version)
762 if (OK)
763 {
764 // Not oo_stringForKey:, because we need to be able to complain about non-strings.
765 maxVersion = [requirements objectForKey:@"max_version"];
766 if (maxVersion != nil)
767 {
768 ++conditionsHandled;
769 if ([maxVersion isKindOfClass:[NSString class]])
770 {
771 oxpVersionComponents = ComponentsFromVersionString(maxVersion);
772 if (NSOrderedDescending == CompareVersions(ooVersionComponents, oxpVersionComponents)) OK = NO;
773 }
774 else
775 {
776 OOLog(@"requirements.wrongType", @"Expected %@ entry \"%@\" to be string, but got %@ in OXP %@.", file, @"max_version", [requirements class], [path lastPathComponent]);
777 OK = NO;
778 }
779 }
780 }
781
782 if (OK && conditionsHandled < [requirements count])
783 {
784 // There are unknown requirement keys - don't support. NOTE: this check was not made pre 1.69!
785 OOLog(@"requirements.unknown", @"requires.plist for OXP %@ contains unknown keys, rejecting.", [path lastPathComponent]);
786 OK = NO;
787 }
788
789 return OK;
790}
791
792
793+ (BOOL) manifestHasConflicts:(NSDictionary *)manifest logErrors:(BOOL)logErrors
794{
795 NSDictionary *conflicting = nil;
796 NSDictionary *conflictManifest = nil;
797 NSString *conflictID = nil;
798 NSArray *conflicts = nil;
799
800 conflicts = [manifest oo_arrayForKey:kOOManifestConflictOXPs defaultValue:nil];
801 // if it has a non-empty conflict_oxps list
802 if (conflicts != nil && [conflicts count] > 0)
803 {
804 // iterate over that list
805 foreach (conflicting, conflicts)
806 {
807 conflictID = [conflicting oo_stringForKey:kOOManifestRelationIdentifier];
808 conflictManifest = [sOXPManifests objectForKey:conflictID];
809 // if the other OXP is in the list
810 if (conflictManifest != nil)
811 {
812 // then check versions
813 if ([self matchVersions:conflicting withVersion:[conflictManifest oo_stringForKey:kOOManifestVersion]])
814 {
815 if (logErrors)
816 {
817 [self addErrorWithKey:@"oxp-conflict" param1:[manifest oo_stringForKey:kOOManifestTitle] param2:[conflictManifest oo_stringForKey:kOOManifestTitle]];
818 OOLog(@"oxp.conflict",@"OXP %@ conflicts with %@ and was removed from the loading list",[[manifest oo_stringForKey:kOOManifestFilePath] lastPathComponent],[[conflictManifest oo_stringForKey:kOOManifestFilePath] lastPathComponent]);
819 }
820 return YES;
821 }
822 }
823 }
824 }
825 return NO;
826}
827
828
829+ (void) filterSearchPathsForConflicts:(NSMutableArray *)searchPaths
830{
831 NSDictionary *manifest = nil;
832 NSString *identifier = nil;
833 NSArray *identifiers = [sOXPManifests allKeys];
834
835 // take a copy because we'll mutate the original
836 // foreach identified add-on
837 foreach (identifier, identifiers)
838 {
839 manifest = [sOXPManifests objectForKey:identifier];
840 if (manifest != nil)
841 {
842 if ([self manifestHasConflicts:manifest logErrors:YES])
843 {
844 // then we have a conflict, so remove this path
845 [searchPaths removeObject:[manifest oo_stringForKey:kOOManifestFilePath]];
846 [sOXPManifests removeObjectForKey:identifier];
847 }
848 }
849 }
850}
851
852
853+ (BOOL) manifestHasMissingDependencies:(NSDictionary *)manifest logErrors:(BOOL)logErrors
854{
855 NSDictionary *required = nil;
856 NSArray *requireds = nil;
857
858 requireds = [manifest oo_arrayForKey:kOOManifestRequiresOXPs defaultValue:nil];
859 // if it has a non-empty required_oxps list
860 if (requireds != nil && [requireds count] > 0)
861 {
862 // iterate over that list
863 foreach (required, requireds)
864 {
865 if ([ResourceManager manifest:manifest HasUnmetDependency:required logErrors:logErrors])
866 {
867 return YES;
868 }
869 }
870 }
871 return NO;
872}
873
874
875+ (BOOL) manifest:(NSDictionary *)manifest HasUnmetDependency:(NSDictionary *)required logErrors:(BOOL)logErrors
876{
877 NSString *requiredID = [required oo_stringForKey:kOOManifestRelationIdentifier];
878 NSMutableDictionary *requiredManifest = [sOXPManifests objectForKey:requiredID];
879 // if the other OXP is in the list
880 BOOL requirementsMet = NO;
881 if (requiredManifest != nil)
882 {
883 // then check versions
884 if ([self matchVersions:required withVersion:[requiredManifest oo_stringForKey:kOOManifestVersion]])
885 {
886 requirementsMet = YES;
887 /* Mark the requiredManifest as a dependency of the
888 * requiring manifest */
889 NSSet *reqby = [requiredManifest oo_setForKey:kOOManifestRequiredBy defaultValue:[NSSet set]];
890 NSUInteger reqbycount = [reqby count];
891 /* then add this manifest to its required set. This is
892 * done without checking if it's already there, because
893 * the list of nested requirements may have changed. */
894 reqby = [reqby setByAddingObject:[manifest oo_stringForKey:kOOManifestIdentifier]];
895 // *and* anything that requires this OXP to be installed
896 reqby = [reqby setByAddingObjectsFromSet:[manifest oo_setForKey:kOOManifestRequiredBy]];
897 if (reqbycount < [reqby count])
898 {
899 /* Then the set has increased in size. To handle
900 * potential cases with nested dependencies, need to
901 * re-run the requirement filter until all the sets
902 * stabilise. */
903 sAllMet = NO;
904 }
905 // and push back into the requiring manifest
906 [requiredManifest setObject:reqby forKey:kOOManifestRequiredBy];
907 }
908 }
909 if (!requirementsMet)
910 {
911 if (logErrors)
912 {
913 [self addErrorWithKey:@"oxp-required" param1:[manifest oo_stringForKey:kOOManifestTitle] param2:[required oo_stringForKey:kOOManifestRelationDescription defaultValue:[required oo_stringForKey:kOOManifestRelationIdentifier]]];
914 OOLog(@"oxp.requirementMissing",@"OXP %@ had unmet requirements and was removed from the loading list",[[manifest oo_stringForKey:kOOManifestFilePath] lastPathComponent]);
915 }
916 return YES;
917 }
918 return NO;
919}
920
921
922+ (BOOL) filterSearchPathsForRequirements:(NSMutableArray *)searchPaths
923{
924 NSDictionary *manifest = nil;
925 NSString *identifier = nil;
926 NSArray *identifiers = [sOXPManifests allKeys];
927
928 sAllMet = YES;
929
930 // take a copy because we'll mutate the original
931 // foreach identified add-on
932 foreach (identifier, identifiers)
933 {
934 manifest = [sOXPManifests objectForKey:identifier];
935 if (manifest != nil)
936 {
937 if ([self manifestHasMissingDependencies:manifest logErrors:YES])
938 {
939 // then we have a missing requirement, so remove this path
940 [searchPaths removeObject:[manifest oo_stringForKey:kOOManifestFilePath]];
941 [sOXPManifests removeObjectForKey:identifier];
942 sAllMet = NO;
943 }
944 }
945 }
946
947 return sAllMet;
948}
949
950
951+ (BOOL) matchVersions:(NSDictionary *)rangeDict withVersion:(NSString *)version
952{
953 NSString *minimum = [rangeDict oo_stringForKey:kOOManifestRelationVersion defaultValue:nil];
954 NSString *maximum = [rangeDict oo_stringForKey:kOOManifestRelationMaxVersion defaultValue:nil];
955 NSArray *isVersionComponents = ComponentsFromVersionString(version);
956 NSArray *reqVersionComponents = nil;
957 if (minimum != nil)
958 {
959 reqVersionComponents = ComponentsFromVersionString(minimum);
960 if (NSOrderedAscending == CompareVersions(isVersionComponents, reqVersionComponents))
961 {
962 // earlier than minimum version
963 return NO;
964 }
965 }
966 if (maximum != nil)
967 {
968 reqVersionComponents = ComponentsFromVersionString(maximum);
969 if (NSOrderedDescending == CompareVersions(isVersionComponents, reqVersionComponents))
970 {
971 // later than maximum version
972 return NO;
973 }
974 }
975 // either version was okay, or no version info so an unconditional match
976 return YES;
977}
978
979
980+ (void) filterSearchPathsToExcludeScenarioOnlyPaths:(NSMutableArray *)searchPaths
981{
982 NSDictionary *manifest = nil;
983 NSString *identifier = nil;
984 NSArray *identifiers = [sOXPManifests allKeys];
985
986 // take a copy because we'll mutate the original
987 // foreach identified add-on
988 foreach (identifier, identifiers)
989 {
990 manifest = [sOXPManifests objectForKey:identifier];
991 if (manifest != nil)
992 {
993 if ([[manifest oo_arrayForKey:kOOManifestTags] containsObject:kOOManifestTagScenarioOnly])
994 {
995 [searchPaths removeObject:[manifest oo_stringForKey:kOOManifestFilePath]];
996 [sOXPManifests removeObjectForKey:identifier];
997 }
998 }
999 }
1000}
1001
1002
1003
1004+ (void) filterSearchPathsByScenario:(NSMutableArray *)searchPaths
1005{
1006 NSDictionary *manifest = nil;
1007 NSString *identifier = nil;
1008 NSArray *identifiers = [sOXPManifests allKeys];
1009
1010 // take a copy because we'll mutate the original
1011 // foreach identified add-on
1012 foreach (identifier, identifiers)
1013 {
1014 manifest = [sOXPManifests objectForKey:identifier];
1015 if (manifest != nil)
1016 {
1017 if (![ResourceManager manifestAllowedByScenario:manifest])
1018 {
1019 // then we don't need this one
1020 [searchPaths removeObject:[manifest oo_stringForKey:kOOManifestFilePath]];
1021 [sOXPManifests removeObjectForKey:identifier];
1022 }
1023 }
1024 }
1025}
1026
1027
1028+ (BOOL) manifestAllowedByScenario:(NSDictionary *)manifest
1029{
1030 /* Checks for a couple of "never happens" cases */
1031#ifndef NDEBUG
1032 // test string
1033 if ([sUseAddOns isEqualToString:SCENARIO_OXP_DEFINITION_ALL])
1034 {
1035 OOLog(@"scenario.check", @"%@", @"Checked scenario allowances in all state - this is an internal error; please report this");
1036 return YES;
1037 }
1038 if ([sUseAddOns isEqualToString:SCENARIO_OXP_DEFINITION_NONE])
1039 {
1040 OOLog(@"scenario.check", @"%@", @"Checked scenario allowances in none state - this is an internal error; please report this");
1041 return NO;
1042 }
1043#endif
1044 if ([[manifest oo_stringForKey:kOOManifestIdentifier] isEqualToString:@"org.oolite.oolite"])
1045 {
1046 // the core data is always allowed!
1047 return YES;
1048 }
1049
1050 NSString *uaoBit = nil;
1051 BOOL result = NO;
1052 foreach (uaoBit, sUseAddOnsParts)
1053 {
1054 if ([uaoBit hasPrefix:SCENARIO_OXP_DEFINITION_BYID])
1055 {
1056 result |= [ResourceManager manifestAllowedByScenario:manifest withIdentifier:[uaoBit substringFromIndex:[SCENARIO_OXP_DEFINITION_BYID length]]];
1057 }
1058 else if ([uaoBit hasPrefix:SCENARIO_OXP_DEFINITION_BYTAG])
1059 {
1060 result |= [ResourceManager manifestAllowedByScenario:manifest withTag:[uaoBit substringFromIndex:[SCENARIO_OXP_DEFINITION_BYTAG length]]];
1061 }
1062 }
1063 return result;
1064}
1065
1066
1067+ (BOOL) manifestAllowedByScenario:(NSDictionary *)manifest withIdentifier:(NSString *)identifier
1068{
1069 if ([[manifest oo_stringForKey:kOOManifestIdentifier] isEqualToString:identifier])
1070 {
1071 // manifest has the identifier - easy
1072 return YES;
1073 }
1074 // manifest is also allowed if a manifest with that identifier
1075 // requires it to be installed
1076 if ([[manifest oo_setForKey:kOOManifestRequiredBy] containsObject:identifier])
1077 {
1078 return YES;
1079 }
1080 // otherwise, no
1081 return NO;
1082}
1083
1084
1085+ (BOOL) manifestAllowedByScenario:(NSDictionary *)manifest withTag:(NSString *)tag
1086{
1087 if ([[manifest oo_arrayForKey:kOOManifestTags] containsObject:tag])
1088 {
1089 // manifest has the tag - easy
1090 return YES;
1091 }
1092 // manifest is also allowed if a manifest with that tag
1093 // requires it to be installed
1094 NSSet *reqby = [manifest oo_setForKey:kOOManifestRequiredBy];
1095 if (reqby != nil)
1096 {
1097 NSString *identifier = nil;
1098 foreach (identifier, reqby)
1099 {
1100 NSDictionary *reqManifest = [sOXPManifests oo_dictionaryForKey:identifier defaultValue:nil];
1101 // need to check for nil as this one may already have been ruled out
1102 if (reqManifest != nil && [[reqManifest oo_arrayForKey:kOOManifestTags] containsObject:tag])
1103 {
1104 return YES;
1105 }
1106 }
1107 }
1108 // otherwise, no
1109 return NO;
1110}
1111
1112
1113
1114+ (void) addErrorWithKey:(NSString *)descriptionKey param1:(id)param1 param2:(id)param2
1115{
1116 if (descriptionKey != nil)
1117 {
1118 if (sErrors == nil) sErrors = [[NSMutableArray alloc] init];
1119 [sErrors addObject:[NSArray arrayWithObjects:descriptionKey, param1 ?: (id)@"", param2 ?: (id)@"", nil]];
1120 }
1121}
1122
1123
1124+ (BOOL)checkCacheUpToDateForPaths:(NSArray *)searchPaths
1125{
1126 /* Check if caches are up to date.
1127 The strategy is to use a two-entry cache. One entry is an array
1128 containing the search paths, the other an array of modification dates
1129 (in the same order). If either fails to match the correct settings,
1130 we delete both.
1131 */
1133 NSFileManager *fmgr = [NSFileManager defaultManager];
1134 BOOL upToDate = YES;
1135 id oldPaths = nil;
1136 NSMutableArray *modDates = nil;
1137 NSEnumerator *pathEnum = nil;
1138 NSString *path = nil;
1139 id modDate = nil;
1140
1141 if (EXPECT_NOT([[NSUserDefaults standardUserDefaults] boolForKey:@"always-flush-cache"]))
1142 {
1143 OOLog(kOOLogCacheExplicitFlush, @"%@", @"Cache explicitly flushed with always-flush-cache preference. Rebuilding from scratch.");
1144 upToDate = NO;
1145 }
1146 else if ([MyOpenGLView pollShiftKey])
1147 {
1148 OOLog(kOOLogCacheExplicitFlush, @"%@", @"Cache explicitly flushed with shift key. Rebuilding from scratch.");
1149 upToDate = NO;
1150 }
1151
1152 oldPaths = [cacheMgr objectForKey:kOOCacheKeySearchPaths inCache:kOOCacheSearchPathModDates];
1153 if (upToDate && ![oldPaths isEqual:searchPaths])
1154 {
1155 // OXPs added/removed
1156 if (oldPaths != nil) OOLog(kOOLogCacheStalePaths, @"%@", @"Cache is stale (search paths have changed). Rebuilding from scratch.");
1157 upToDate = NO;
1158 }
1159
1160 // Build modification date list. (We need this regardless of whether the search paths matched.)
1161
1162 modDates = [NSMutableArray arrayWithCapacity:[searchPaths count]];
1163 for (pathEnum = [searchPaths objectEnumerator]; (path = [pathEnum nextObject]); )
1164 {
1165 modDate = [[fmgr oo_fileAttributesAtPath:path traverseLink:YES] objectForKey:NSFileModificationDate];
1166 if (modDate != nil)
1167 {
1168 // Converts to double because I'm not sure the cache can deal with dates under GNUstep.
1169 modDate = [NSNumber numberWithDouble:[modDate timeIntervalSince1970]];
1170 [modDates addObject:modDate];
1171 }
1172 }
1173
1174 if (upToDate && ![[cacheMgr objectForKey:kOOCacheKeyModificationDates inCache:kOOCacheSearchPathModDates] isEqual:modDates])
1175 {
1176 OOLog(kOOLogCacheStaleDates, @"%@", @"Cache is stale (modification dates have changed). Rebuilding from scratch.");
1177 upToDate = NO;
1178 }
1179
1180 if (!upToDate)
1181 {
1182 [cacheMgr clearAllCaches];
1183 [cacheMgr setObject:searchPaths forKey:kOOCacheKeySearchPaths inCache:kOOCacheSearchPathModDates];
1184 [cacheMgr setObject:modDates forKey:kOOCacheKeyModificationDates inCache:kOOCacheSearchPathModDates];
1185 }
1186 else OOLog(kOOLogCacheUpToDate, @"%@", @"Data cache is up to date.");
1187
1188 return upToDate;
1189}
1190
1191
1192/* This method allows the exclusion of particular files from the plist
1193 * building when they're in builtInPath. The point of this is to allow
1194 * scenarios to avoid merging in core files without having to override
1195 * every individual entry (which may not always be possible
1196 * anyway). It only works on plists, but of course worldscripts can be
1197 * excluded by not including the plists which reference them, and
1198 * everything else can be excluded by not referencing it from a plist.
1199 */
1200+ (BOOL) corePlist:(NSString *)fileName excludedAt:(NSString *)path
1201{
1202 if (![path isEqualToString:[self builtInPath]])
1203 {
1204 // non-core paths always okay
1205 return NO;
1206 }
1207 NSString *uaoBit = nil;
1208 foreach (uaoBit, sUseAddOnsParts)
1209 {
1210 if ([uaoBit hasPrefix:SCENARIO_OXP_DEFINITION_NOPLIST])
1211 {
1212 NSString *plist = [uaoBit substringFromIndex:[SCENARIO_OXP_DEFINITION_NOPLIST length]];
1213 if ([plist isEqualToString:fileName])
1214 {
1215 // this core plist file should not be loaded at all
1216 return YES;
1217 }
1218 }
1219 }
1220 // then not excluded
1221 return NO;
1222}
1223
1224
1225+ (NSDictionary *)dictionaryFromFilesNamed:(NSString *)fileName
1226 inFolder:(NSString *)folderName
1227 andMerge:(BOOL) mergeFiles
1228{
1229 return [ResourceManager dictionaryFromFilesNamed:fileName inFolder:folderName mergeMode:mergeFiles ? MERGE_BASIC : MERGE_NONE cache:YES];
1230}
1231
1232
1233+ (NSDictionary *)dictionaryFromFilesNamed:(NSString *)fileName
1234 inFolder:(NSString *)folderName
1235 mergeMode:(OOResourceMergeMode)mergeMode
1236 cache:(BOOL)cache
1237{
1238 id result = nil;
1239 NSMutableArray *results = nil;
1240 NSString *cacheKey = nil;
1241 NSString *mergeType = nil;
1243 NSEnumerator *enumerator = nil;
1244 NSString *path = nil;
1245 NSString *dictPath = nil;
1246 NSDictionary *dict = nil;
1247
1248 if (fileName == nil) return nil;
1249
1250 switch (mergeMode)
1251 {
1252 case MERGE_NONE:
1253 mergeType = @"none";
1254 break;
1255
1256 case MERGE_BASIC:
1257 mergeType = @"basic";
1258 break;
1259
1260 case MERGE_SMART:
1261 mergeType = @"smart";
1262 break;
1263 }
1264 if (mergeType == nil)
1265 {
1266 OOLog(kOOLogParameterError, @"Unknown dictionary merge mode %u for %@. (This is an internal programming error, please report it.)", mergeMode, fileName);
1267 return nil;
1268 }
1269
1270 if (cache)
1271 {
1272
1273 if (folderName != nil)
1274 {
1275 cacheKey = [NSString stringWithFormat:@"%@/%@ merge:%@", folderName, fileName, mergeType];
1276 }
1277 else
1278 {
1279 cacheKey = [NSString stringWithFormat:@"%@ merge:%@", fileName, mergeType];
1280 }
1281 result = [cacheMgr objectForKey:cacheKey inCache:@"dictionaries"];
1282 if (result != nil) return result;
1283 }
1284
1285 if (mergeMode == MERGE_NONE)
1286 {
1287 // Find "last" matching dictionary
1288 for (enumerator = [ResourceManager reversePathEnumerator]; (path = [enumerator nextObject]); )
1289 {
1290 if (folderName != nil)
1291 {
1292 dictPath = [[path stringByAppendingPathComponent:folderName] stringByAppendingPathComponent:fileName];
1293 dict = OODictionaryFromFile(dictPath);
1294 if (dict != nil) break;
1295 }
1296 dictPath = [path stringByAppendingPathComponent:fileName];
1297 dict = OODictionaryFromFile(dictPath);
1298 if (dict != nil) break;
1299 }
1300 result = dict;
1301 }
1302 else
1303 {
1304 // Find all matching dictionaries
1305 results = [NSMutableArray array];
1306 for (enumerator = [ResourceManager pathEnumerator]; (path = [enumerator nextObject]); )
1307 {
1308 if ([ResourceManager corePlist:fileName excludedAt:path])
1309 {
1310 continue;
1311 }
1312 dictPath = [path stringByAppendingPathComponent:fileName];
1313 dict = OODictionaryFromFile(dictPath);
1314 if (dict != nil) [results addObject:dict];
1315 if (folderName != nil)
1316 {
1317 dictPath = [[path stringByAppendingPathComponent:folderName] stringByAppendingPathComponent:fileName];
1318 dict = OODictionaryFromFile(dictPath);
1319 if (dict != nil) [results addObject:dict];
1320 }
1321 }
1322
1323 if ([results count] == 0) return nil;
1324
1325 // Merge result
1326 result = [NSMutableDictionary dictionary];
1327
1328 for (enumerator = [results objectEnumerator]; (dict = [enumerator nextObject]); )
1329 {
1330 if (mergeMode == MERGE_SMART) [result mergeEntriesFromDictionary:dict];
1331 else [result addEntriesFromDictionary:dict];
1332 }
1333 result = [[result copy] autorelease]; // Make immutable
1334 }
1335
1336 if (cache && result != nil) [cacheMgr setObject:result forKey:cacheKey inCache:@"dictionaries"];
1337
1338 return result;
1339}
1340
1341
1342+ (NSArray *) arrayFromFilesNamed:(NSString *)fileName inFolder:(NSString *)folderName andMerge:(BOOL) mergeFiles
1343{
1344 return [self arrayFromFilesNamed:fileName inFolder:folderName andMerge:mergeFiles cache:YES];
1345}
1346
1347
1348+ (NSArray *) arrayFromFilesNamed:(NSString *)fileName inFolder:(NSString *)folderName andMerge:(BOOL) mergeFiles cache:(BOOL)useCache
1349{
1350 id result = nil;
1351 NSMutableArray *results = nil;
1352 NSString *cacheKey = nil;
1354 NSEnumerator *enumerator = nil;
1355 NSString *path = nil;
1356 NSString *arrayPath = nil;
1357 NSMutableArray *array = nil;
1358 NSArray *arrayNonEditable = nil;
1359
1360 if (fileName == nil) return nil;
1361
1362 if (useCache)
1363 {
1364 cacheKey = [NSString stringWithFormat:@"%@%@ merge:%@", (folderName != nil) ? [folderName stringByAppendingString:@"/"] : (NSString *)@"", fileName, mergeFiles ? @"yes" : @"no"];
1365 result = [cache objectForKey:cacheKey inCache:@"arrays"];
1366 if (result != nil) return result;
1367 }
1368
1369 if (!mergeFiles)
1370 {
1371 // Find "last" matching array
1372 for (enumerator = [ResourceManager reversePathEnumerator]; (path = [enumerator nextObject]); )
1373 {
1374 if (folderName != nil)
1375 {
1376 arrayPath = [[path stringByAppendingPathComponent:folderName] stringByAppendingPathComponent:fileName];
1377 arrayNonEditable = OOArrayFromFile(arrayPath);
1378 if (arrayNonEditable != nil) break;
1379 }
1380 arrayPath = [path stringByAppendingPathComponent:fileName];
1381 arrayNonEditable = OOArrayFromFile(arrayPath);
1382 if (arrayNonEditable != nil) break;
1383 }
1384 result = arrayNonEditable;
1385 }
1386 else
1387 {
1388 // Find all matching arrays
1389 results = [NSMutableArray array];
1390 for (enumerator = [ResourceManager pathEnumerator]; (path = [enumerator nextObject]); )
1391 {
1392 if ([ResourceManager corePlist:fileName excludedAt:path])
1393 {
1394 continue;
1395 }
1396
1397 arrayPath = [path stringByAppendingPathComponent:fileName];
1398 array = [[OOArrayFromFile(arrayPath) mutableCopy] autorelease];
1399 if (array != nil) [results addObject:array];
1400
1401 // Special handling for arrays merging. Currently, only equipment.plist, nebulatextures.plist and
1402 // startextures.plist gets their objects merged.
1403 // A lookup index is required. For the equipment.plist items, this is the index corresponding to the
1404 // EQ_* string, which describes the role of an equipment item and is unique.
1405 // For nebula and star textures, this is the texture filename, although it can be overridden with a
1406 // "key" property
1407 if ([array count] != 0 && ([[fileName lowercaseString] isEqualToString:@"nebulatextures.plist"] ||
1408 [[fileName lowercaseString] isEqualToString:@"startextures.plist"]))
1409 [self handleStarNebulaListMerging:results];
1410
1411 if ([array count] != 0 && [[array objectAtIndex:0] isKindOfClass:[NSArray class]])
1412 {
1413 if ([[fileName lowercaseString] isEqualToString:@"equipment.plist"])
1414 [self handleEquipmentListMerging:results forLookupIndex:3]; // Index 3 is the role string (EQ_*).
1415 }
1416 if (folderName != nil)
1417 {
1418 arrayPath = [[path stringByAppendingPathComponent:folderName] stringByAppendingPathComponent:fileName];
1419 array = [[OOArrayFromFile(arrayPath) mutableCopy] autorelease];
1420 if (array != nil) [results addObject:array];
1421
1422 if ([array count] != 0 && ([[fileName lowercaseString] isEqualToString:@"nebulatextures.plist"] ||
1423 [[fileName lowercaseString] isEqualToString:@"startextures.plist"]))
1424 [self handleStarNebulaListMerging:results];
1425
1426 if ([array count] != 0 && [[array objectAtIndex:0] isKindOfClass:[NSArray class]])
1427 {
1428 if ([[fileName lowercaseString] isEqualToString:@"equipment.plist"])
1429 [self handleEquipmentListMerging:results forLookupIndex:3]; // Index 3 is the role string (EQ_*).
1430 }
1431 }
1432 }
1433
1434 if ([results count] == 0) return nil;
1435
1436 // Merge result
1437 result = [NSMutableArray array];
1438
1439 for (enumerator = [results objectEnumerator]; (array = [enumerator nextObject]); )
1440 {
1441 [result addObjectsFromArray:array];
1442 }
1443 // if we're doing equipment.plist, do equipment overrides now, while the array is still mutable
1444 if ([[fileName lowercaseString] isEqualToString:@"equipment.plist"])
1445 {
1446 [self handleEquipmentOverrides:result];
1447 }
1448 result = [[result copy] autorelease]; // Make immutable
1449 }
1450
1451 if (useCache && result != nil) [cache setObject:result forKey:cacheKey inCache:@"arrays"];
1452
1453 return [NSArray arrayWithArray:result];
1454}
1455
1456
1457// A method for handling merging of arrays. Currently used with the equipment.plist entries.
1458// The arrayToProcess array is scanned for repetitions of the item at lookup index location and, if found,
1459// the latest entry replaces the earliest.
1460+ (void) handleEquipmentListMerging: (NSMutableArray *)arrayToProcess forLookupIndex:(unsigned)lookupIndex
1461{
1462 NSUInteger i,j,k;
1463 NSMutableArray *refArray = [arrayToProcess objectAtIndex:[arrayToProcess count] - 1];
1464
1465 // Any change to refArray will directly modify arrayToProcess.
1466
1467 for (i = 0; i < [refArray count]; i++)
1468 {
1469 for (j = 0; j < [arrayToProcess count] - 1; j++)
1470 {
1471 NSUInteger count = [[arrayToProcess oo_arrayAtIndex:j] count];
1472 if (count == 0) continue;
1473
1474 for (k=0; k < count; k++)
1475 {
1476 id processValue = [[[arrayToProcess oo_arrayAtIndex:j] oo_arrayAtIndex:k] oo_objectAtIndex:lookupIndex defaultValue:nil];
1477 id refValue = [[refArray oo_arrayAtIndex:i] oo_objectAtIndex:lookupIndex defaultValue:nil];
1478
1479 if ([processValue isEqual:refValue])
1480 {
1481 [[arrayToProcess objectAtIndex:j] replaceObjectAtIndex:k withObject:[refArray objectAtIndex:i]];
1482 [refArray removeObjectAtIndex:i];
1483 }
1484 }
1485 }
1486 }
1487 // arrayToProcess has been processed at this point. Any necessary merging has been done.
1488}
1489
1490
1491// handles processing of equipment-overrides.plist files, updating the source array with values found.
1492// format of file is slightly different to the standard equipment.plist file, in that it is a
1493// dictionary of dictionary objects (rather than an array of arrays). this allows properties like
1494// techlevel, price, name/short_description and description/long_description to be updated via the overrides file.
1495+ (void) handleEquipmentOverrides: (NSMutableArray *)arrayToProcess
1496{
1497 NSEnumerator *equipKeyEnum = nil;
1498 NSString *equipKey = nil;
1499 NSDictionary *overrides = nil;
1500 NSDictionary *overridesEntry = nil;
1501 int i;
1502
1503 overrides = [ResourceManager dictionaryFromFilesNamed:@"equipment-overrides.plist"
1504 inFolder:@"Config"
1505 mergeMode:MERGE_SMART
1506 cache:NO];
1507
1508 // cycle through all the equipment keys found in override files
1509 for (equipKeyEnum = [overrides keyEnumerator]; (equipKey = [equipKeyEnum nextObject]); )
1510 {
1511 overridesEntry = [overrides objectForKey:equipKey];
1512 // loop through our data array to find a match
1513 for (i = 0; i < [arrayToProcess count]; i++)
1514 {
1515 NSMutableArray *equipArray = [[[arrayToProcess objectAtIndex:i] mutableCopy] autorelease];
1516 id refValue = [equipArray oo_objectAtIndex:EQUIPMENT_KEY_INDEX defaultValue:nil];
1517 // does the overridden equipment item exist in the equipment array? if so, get working
1518 if ([equipKey isEqual:refValue])
1519 {
1520 NSEnumerator *infoKeyEnum = nil;
1521 NSString *infoKey;
1522 // cycle through all the properties found for this equipment key in the overrides file
1523 for (infoKeyEnum = [overridesEntry keyEnumerator]; (infoKey = [infoKeyEnum nextObject]); )
1524 {
1525 // special cases for the array items that don't have a direct keyname
1526 if ([infoKey isEqualToString:@"techlevel"])
1527 [equipArray replaceObjectAtIndex:EQUIPMENT_TECH_LEVEL_INDEX withObject:[overridesEntry objectForKey:infoKey]];
1528 else if ([infoKey isEqualToString:@"price"])
1529 [equipArray replaceObjectAtIndex:EQUIPMENT_PRICE_INDEX withObject:[overridesEntry objectForKey:infoKey]];
1530 else if ([infoKey isEqualToString:@"short_description"])
1531 [equipArray replaceObjectAtIndex:EQUIPMENT_SHORT_DESC_INDEX withObject:[overridesEntry objectForKey:infoKey]];
1532 else if ([infoKey isEqualToString:@"name"])
1533 [equipArray replaceObjectAtIndex:EQUIPMENT_SHORT_DESC_INDEX withObject:[overridesEntry objectForKey:infoKey]];
1534 else if ([infoKey isEqualToString:@"long_description"])
1535 [equipArray replaceObjectAtIndex:EQUIPMENT_LONG_DESC_INDEX withObject:[overridesEntry objectForKey:infoKey]];
1536 else if ([infoKey isEqualToString:@"description"])
1537 [equipArray replaceObjectAtIndex:EQUIPMENT_LONG_DESC_INDEX withObject:[overridesEntry objectForKey:infoKey]];
1538 else
1539 {
1540 NSMutableDictionary *extra = nil;
1541 // for everything else
1542 // do we actually have an extras dictionary?
1543 if (![equipArray oo_dictionaryAtIndex:EQUIPMENT_EXTRA_INFO_INDEX])
1544 {
1545 // if not, create a blank one we can add to
1546 extra = [NSMutableDictionary dictionary];
1547 }
1548 else
1549 {
1550 extra = [[equipArray oo_dictionaryAtIndex:EQUIPMENT_EXTRA_INFO_INDEX] mutableCopy];
1551 }
1552 // special case for weapon_info && script_info, which are child dictionaries
1553 if ([infoKey isEqualToString:@"weapon_info"] || [infoKey isEqualToString:@"script_info"])
1554 {
1555 NSEnumerator *subEnum = nil;
1556 NSString *subKey;
1557 NSMutableDictionary *subInfo = nil;
1558 NSDictionary *subOverrides = nil;
1559 // do we actually have a weapon_info/script_info dictionary?
1560 if (![extra oo_dictionaryForKey:infoKey])
1561 {
1562 // if not, create a blank dictionary we can add to
1563 subInfo = [NSMutableDictionary dictionary];
1564 }
1565 else
1566 {
1567 subInfo = [[extra oo_dictionaryForKey:infoKey] mutableCopy];
1568 }
1569 subOverrides = [overridesEntry oo_dictionaryForKey:infoKey];
1570 // cycle through all the sub keys found in the overrides file for this equipment key item
1571 for (subEnum = [subOverrides keyEnumerator]; (subKey = [subEnum nextObject]); )
1572 {
1573 [subInfo setObject:[subOverrides objectForKey:subKey] forKey:subKey];
1574 }
1575 [extra setObject:[[subInfo copy] autorelease] forKey:infoKey];
1576 }
1577 else
1578 {
1579 // for all other keys in the extras dictionary
1580 [extra setObject:[overridesEntry objectForKey:infoKey] forKey:infoKey];
1581 }
1582 [equipArray replaceObjectAtIndex:EQUIPMENT_EXTRA_INFO_INDEX withObject:[[extra copy] autorelease]];
1583 }
1584 }
1585 [arrayToProcess replaceObjectAtIndex:i withObject:equipArray];
1586 }
1587 }
1588 }
1589}
1590
1591
1592// A method for handling merging of arrays. Currently used with the nebulatextures.plist and startextures.plist entries.
1593// uses the "texture" filename as the key value, or "key" if found
1594+ (void) handleStarNebulaListMerging: (NSMutableArray *)arrayToProcess
1595{
1596 NSUInteger i,j,k;
1597 NSMutableArray *refArray = [arrayToProcess objectAtIndex:[arrayToProcess count] - 1];
1598
1599 // Any change to refArray will directly modify arrayToProcess.
1600 for (i = 0; i < [refArray count]; i++)
1601 {
1602 for (j = 0; j < [arrayToProcess count] - 1; j++)
1603 {
1604 NSUInteger count = [[arrayToProcess oo_arrayAtIndex:j] count];
1605 if (count == 0) continue;
1606
1607 for (k = 0; k < count; k++)
1608 {
1609 id processValue = [[arrayToProcess oo_arrayAtIndex:j] oo_objectAtIndex:k defaultValue:nil];
1610 id refValue = [refArray oo_objectAtIndex:i defaultValue:nil];
1611 NSString *key1 = nil;
1612 NSString *key2 = nil;
1613
1614 if ([processValue isKindOfClass:[NSString class]])
1615 {
1616 key1 = processValue;
1617 }
1618 else if ([processValue isKindOfClass:[NSDictionary class]])
1619 {
1620 if (![processValue objectForKey:@"key"])
1621 {
1622 key1 = [processValue objectForKey:@"texture"];
1623 }
1624 else
1625 {
1626 key1 = [processValue objectForKey:@"key"];
1627 }
1628 }
1629 if (!key1) continue;
1630
1631 if ([refValue isKindOfClass:[NSString class]])
1632 {
1633 key2 = refValue;
1634 }
1635 else if ([refValue isKindOfClass:[NSDictionary class]])
1636 {
1637 if (![refValue objectForKey:@"key"])
1638 {
1639 key2 = [refValue objectForKey:@"texture"];
1640 }
1641 else
1642 {
1643 key2 = [refValue objectForKey:@"key"];
1644 }
1645 }
1646 if (!key2) continue;
1647
1648 if ([key1 isEqual:key2])
1649 {
1650 [[arrayToProcess objectAtIndex:j] replaceObjectAtIndex:k withObject:[refArray objectAtIndex:i]];
1651 [refArray removeObjectAtIndex:i];
1652 }
1653
1654 }
1655 }
1656 }
1657 // arrayToProcess has been processed at this point. Any necessary merging has been done.
1658}
1659
1660
1661+ (NSDictionary *) whitelistDictionary
1662{
1663 static id whitelistDictionary = nil;
1664
1665 if (whitelistDictionary == nil)
1666 {
1667 NSString *path = [[[ResourceManager builtInPath] stringByAppendingPathComponent:@"Config"] stringByAppendingPathComponent:@"whitelist.plist"];
1668 whitelistDictionary = [NSDictionary dictionaryWithContentsOfFile:path];
1669 if (whitelistDictionary == nil) whitelistDictionary = [NSNull null];
1670
1671 [whitelistDictionary retain];
1672 }
1673
1674 if (whitelistDictionary == [NSNull null]) return nil;
1675 return whitelistDictionary;
1676}
1677
1678
1679static NSString *LogClassKeyRoot(NSString *key)
1680{
1681 NSRange dot = [key rangeOfString:@"."];
1682 if (dot.location != NSNotFound)
1683 {
1684 return [key substringToIndex:dot.location];
1685 }
1686 else
1687 {
1688 return key;
1689 }
1690}
1691
1692
1693+ (NSDictionary *) logControlDictionary
1694{
1695 // Load built-in copy of logcontrol.plist.
1696 NSString *path = [[[ResourceManager builtInPath] stringByAppendingPathComponent:@"Config"]
1697 stringByAppendingPathComponent:@"logcontrol.plist"];
1698 NSMutableDictionary *logControl = [NSMutableDictionary dictionaryWithDictionary:OODictionaryFromFile(path)];
1699 if (logControl == nil) logControl = [NSMutableDictionary dictionary];
1700
1701 // Build list of root log message classes that appear in the built-in list.
1702 NSMutableSet *coreRoots = [NSMutableSet set];
1703 NSString *key = nil;
1704 foreachkey(key, logControl)
1705 {
1706 [coreRoots addObject:LogClassKeyRoot(key)];
1707 }
1708
1709 NSArray *rootPaths = [self rootPaths];
1710 NSString *configPath = nil;
1711 NSDictionary *dict = nil;
1712
1713 // Look for logcontrol.plists inside OXPs (but not in root paths). These are not allowed to define keys in hierarchies used by the build-in one.
1714 NSEnumerator *pathEnum = [self pathEnumerator];
1715 while ((path = [pathEnum nextObject]))
1716 {
1717 if ([rootPaths containsObject:path]) continue;
1718
1719 configPath = [[path stringByAppendingPathComponent:@"Config"]
1720 stringByAppendingPathComponent:@"logcontrol.plist"];
1721 dict = OODictionaryFromFile(configPath);
1722 if (dict == nil)
1723 {
1724 configPath = [path stringByAppendingPathComponent:@"logcontrol.plist"];
1725 dict = OODictionaryFromFile(configPath);
1726 }
1727 foreachkey (key, dict)
1728 {
1729 if (![coreRoots containsObject:LogClassKeyRoot(key)])
1730 {
1731 [logControl setObject:[dict objectForKey:key] forKey:key];
1732 }
1733 }
1734 }
1735
1736 // Now, look for logcontrol.plists in root paths, i.e. not within OXPs. These are allowed to override the built-in copy.
1737 pathEnum = [rootPaths objectEnumerator];
1738 while ((path = [pathEnum nextObject]))
1739 {
1740 configPath = [[path stringByAppendingPathComponent:@"Config"]
1741 stringByAppendingPathComponent:@"logcontrol.plist"];
1742 dict = OODictionaryFromFile(configPath);
1743 if (dict == nil)
1744 {
1745 configPath = [path stringByAppendingPathComponent:@"logcontrol.plist"];
1746 dict = OODictionaryFromFile(configPath);
1747 }
1748 foreachkey (key, dict)
1749 {
1750 [logControl setObject:[dict objectForKey:key] forKey:key];
1751 }
1752 }
1753
1754 // Finally, look in preferences, which can override all of the above.
1755 dict = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"logging-enable"];
1756 if (dict != nil) [logControl addEntriesFromDictionary:dict];
1757
1758 return logControl;
1759}
1760
1761
1762+ (NSDictionary *) roleCategoriesDictionary
1763{
1764 NSMutableDictionary *roleCategories = [NSMutableDictionary dictionaryWithCapacity:16];
1765
1766 NSString *path = nil;
1767 NSString *configPath = nil;
1768 NSDictionary *categories = nil;
1769
1770 NSEnumerator *pathEnum = [self pathEnumerator];
1771 while ((path = [pathEnum nextObject]))
1772 {
1773 if ([ResourceManager corePlist:@"role-categories.plist" excludedAt:path])
1774 {
1775 continue;
1776 }
1777
1778 configPath = [[path stringByAppendingPathComponent:@"Config"]
1779 stringByAppendingPathComponent:@"role-categories.plist"];
1780 categories = OODictionaryFromFile(configPath);
1781 if (categories != nil)
1782 {
1783 [ResourceManager mergeRoleCategories:categories intoDictionary:roleCategories];
1784 }
1785 }
1786
1787 /* If the old pirate-victim-roles files exist, merge them in */
1788 NSArray *pirateVictims = [ResourceManager arrayFromFilesNamed:@"pirate-victim-roles.plist" inFolder:@"Config" andMerge:YES];
1789 if (OOEnforceStandards() && [pirateVictims count] > 0)
1790 {
1791 OOStandardsDeprecated(@"pirate-victim-roles.plist is still being used.");
1792 }
1793 [ResourceManager mergeRoleCategories:[NSDictionary dictionaryWithObject:pirateVictims forKey:@"oolite-pirate-victim"] intoDictionary:roleCategories];
1794
1795 return [[roleCategories copy] autorelease];
1796}
1797
1798
1799+ (void) mergeRoleCategories:(NSDictionary *)catData intoDictionary:(NSMutableDictionary *)categories
1800{
1801 NSMutableSet *contents = nil;
1802 NSArray *catDataEntry = nil;
1803 NSString *key;
1804 foreachkey(key, catData)
1805 {
1806 contents = [categories objectForKey:key];
1807 if (contents == nil)
1808 {
1809 contents = [NSMutableSet setWithCapacity:16];
1810 [categories setObject:contents forKey:key];
1811 }
1812 catDataEntry = [catData oo_arrayForKey:key];
1813 OOLog(@"shipData.load.roleCategories", @"Adding %ld entries for category %@", (unsigned long)[catDataEntry count], key);
1814 [contents addObjectsFromArray:catDataEntry];
1815 }
1816}
1817
1818
1819+ (OOSystemDescriptionManager *) systemDescriptionManager
1820{
1821 OOLog(@"resourceManager.planetinfo.load", @"%@", @"Initialising manager");
1823
1824 NSString *path = nil;
1825 NSString *configPath = nil;
1826 NSDictionary *categories = nil;
1827 NSString *systemKey = nil;
1828
1829 NSEnumerator *pathEnum = [self pathEnumerator];
1830 while ((path = [pathEnum nextObject]))
1831 {
1832 if ([ResourceManager corePlist:@"planetinfo.plist" excludedAt:path])
1833 {
1834 continue;
1835 }
1836 configPath = [[path stringByAppendingPathComponent:@"Config"]
1837 stringByAppendingPathComponent:@"planetinfo.plist"];
1838 categories = OODictionaryFromFile(configPath);
1839 if (categories != nil)
1840 {
1841 foreachkey(systemKey,categories)
1842 {
1843 NSDictionary *values = [categories oo_dictionaryForKey:systemKey defaultValue:nil];
1844 if (values != nil)
1845 {
1846 if ([systemKey isEqualToString:PLANETINFO_UNIVERSAL_KEY])
1847 {
1848 [manager setUniversalProperties:values];
1849 }
1850 else if ([systemKey isEqualToString:PLANETINFO_INTERSTELLAR_KEY])
1851 {
1852 [manager setInterstellarProperties:values];
1853 }
1854 else
1855 {
1856 [manager setProperties:values forSystemKey:systemKey];
1857 }
1858 }
1859 }
1860 }
1861 }
1862 OOLog(@"resourceManager.planetinfo.load", @"%@", @"Caching routes");
1863 [manager buildRouteCache];
1864 OOLog(@"resourceManager.planetinfo.load", @"%@", @"Initialised manager");
1865 return [manager autorelease];
1866}
1867
1868
1869
1870+ (NSDictionary *) shaderBindingTypesDictionary
1871{
1872 static id shaderBindingTypesDictionary = nil;
1873
1874 if (shaderBindingTypesDictionary == nil)
1875 {
1876 NSAutoreleasePool *pool = [NSAutoreleasePool new];
1877
1878 NSString *path = [[[ResourceManager builtInPath] stringByAppendingPathComponent:@"Config"] stringByAppendingPathComponent:@"shader-uniform-bindings.plist"];
1879 NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfFile:path];
1880 NSArray *keys = [dict allKeys];
1881
1882 // Resolve all $inherit keys.
1883 unsigned changeCount = 0;
1884 do {
1885 changeCount = 0;
1886 NSString *key = nil;
1887 foreach (key, keys)
1888 {
1889 NSDictionary *value = [dict oo_dictionaryForKey:key];
1890 NSString *inheritKey = [value oo_stringForKey:@"$w299"];
1891 if (inheritKey != nil)
1892 {
1893 changeCount++;
1894 NSMutableDictionary *mutableValue = [[value mutableCopy] autorelease];
1895 [mutableValue removeObjectForKey:@"$w300"];
1896 NSDictionary *inherited = [dict oo_dictionaryForKey:inheritKey];
1897 if (inherited != nil)
1898 {
1899 [mutableValue addEntriesFromDictionary:inherited];
1900 }
1901
1902 [dict setObject:[[mutableValue copy] autorelease] forKey:key];
1903 }
1904 }
1905 } while (changeCount != 0);
1906
1907 shaderBindingTypesDictionary = [dict copy];
1908
1909 [pool release];
1910 }
1911
1912 return shaderBindingTypesDictionary;
1913}
1914
1915
1916+ (NSString *) pathForFileNamed:(NSString *)fileName inFolder:(NSString *)folderName
1917{
1918 return [self pathForFileNamed:fileName inFolder:folderName cache:YES];
1919}
1920
1921
1922/* This is extremely expensive to call with useCache:NO */
1923+ (NSString *) pathForFileNamed:(NSString *)fileName inFolder:(NSString *)folderName cache:(BOOL)useCache
1924{
1925 NSString *result = nil;
1926 NSString *cacheKey = nil;
1928 NSEnumerator *pathEnum = nil;
1929 NSString *path = nil;
1930 NSString *filePath = nil;
1931 NSFileManager *fmgr = nil;
1932
1933 if (fileName == nil) return nil;
1934
1935 if (cache)
1936 {
1937 if (folderName != nil) cacheKey = [NSString stringWithFormat:@"%@/%@", folderName, fileName];
1938 else cacheKey = fileName;
1939 result = [cache objectForKey:cacheKey inCache:@"resolved paths"];
1940 if (result != nil) return result;
1941 }
1942
1943 // Search for file
1944 fmgr = [NSFileManager defaultManager];
1945 // reverse object enumerator allows OXPs to override core
1946 for (pathEnum = [[ResourceManager paths] reverseObjectEnumerator]; (path = [pathEnum nextObject]); )
1947 {
1948 filePath = [[path stringByAppendingPathComponent:folderName] stringByAppendingPathComponent:fileName];
1949 if ([fmgr oo_oxzFileExistsAtPath:filePath])
1950 {
1951 result = filePath;
1952 break;
1953 }
1954
1955 filePath = [path stringByAppendingPathComponent:fileName];
1956 if ([fmgr oo_oxzFileExistsAtPath:filePath])
1957 {
1958 result = filePath;
1959 break;
1960 }
1961 }
1962
1963 if (result != nil)
1964 {
1965 OOLog(@"resourceManager.foundFile", @"Found %@/%@ at %@", folderName, fileName, filePath);
1966 if (useCache)
1967 {
1968 [cache setObject:result forKey:cacheKey inCache:@"resolved paths"];
1969 }
1970 }
1971 return result;
1972}
1973
1974
1975/* use extreme caution in calling with usePathCache:NO - this can be
1976 * an extremely expensive operation */
1977+ (id) retrieveFileNamed:(NSString *)fileName
1978 inFolder:(NSString *)folderName
1979 cache:(NSMutableDictionary **)ioCache
1980 key:(NSString *)key
1981 class:(Class)class
1982 usePathCache:(BOOL)useCache
1983{
1984 id result = nil;
1985 NSString *path = nil;
1986
1987 if (ioCache)
1988 {
1989 if (key == nil) key = [NSString stringWithFormat:@"%@:%@", folderName, fileName];
1990 if (*ioCache != nil)
1991 {
1992 // return the cached object, if any
1993 result = [*ioCache objectForKey:key];
1994 if (result) return result;
1995 }
1996 }
1997
1998 path = [self pathForFileNamed:fileName inFolder:folderName cache:useCache];
1999 if (path != nil) result = [[[class alloc] initWithContentsOfFile:path] autorelease];
2000
2001 if (result != nil && ioCache != NULL)
2002 {
2003 if (*ioCache == nil) *ioCache = [[NSMutableDictionary alloc] init];
2004 [*ioCache setObject:result forKey:key];
2005 }
2006
2007 return result;
2008}
2009
2010
2011+ (OOMusic *) ooMusicNamed:(NSString *)fileName inFolder:(NSString *)folderName
2012{
2013 return [self retrieveFileNamed:fileName
2014 inFolder:folderName
2015 cache:NULL // Don't cache music objects; minimizing latency isn't really important.
2016 key:[NSString stringWithFormat:@"OOMusic:%@:%@", folderName, fileName]
2017 class:[OOMusic class]
2018 usePathCache:YES];
2019}
2020
2021
2022+ (OOSound *) ooSoundNamed:(NSString *)fileName inFolder:(NSString *)folderName
2023{
2024 return [self retrieveFileNamed:fileName
2025 inFolder:folderName
2026 cache:&sSoundCache
2027 key:[NSString stringWithFormat:@"OOSound:%@:%@", folderName, fileName]
2028 class:[OOSound class]
2029 usePathCache:YES];
2030}
2031
2032
2033+ (NSString *) stringFromFilesNamed:(NSString *)fileName inFolder:(NSString *)folderName
2034{
2035 return [self stringFromFilesNamed:fileName inFolder:folderName cache:YES];
2036}
2037
2038
2039+ (NSString *) stringFromFilesNamed:(NSString *)fileName inFolder:(NSString *)folderName cache:(BOOL)useCache
2040{
2041 id result = nil;
2042 NSString *path = nil;
2043 NSString *key = nil;
2044
2045 if (useCache)
2046 {
2047 key = [NSString stringWithFormat:@"%@:%@", folderName, fileName];
2048 if (sStringCache != nil)
2049 {
2050 // return the cached object, if any
2051 result = [sStringCache objectForKey:key];
2052 if (result) return result;
2053 }
2054 }
2055
2056 path = [self pathForFileNamed:fileName inFolder:folderName cache:YES];
2057 if (path != nil) result = [NSString stringWithContentsOfUnicodeFile:path];
2058
2059 if (result != nil && useCache)
2060 {
2061 if (sStringCache == nil) sStringCache = [[NSMutableDictionary alloc] init];
2062 [sStringCache setObject:result forKey:key];
2063 }
2064
2065 return result;
2066}
2067
2068
2069+ (NSDictionary *)loadScripts
2070{
2071 NSMutableDictionary *loadedScripts = nil;
2072 NSArray *results = nil;
2073 NSArray *paths = nil;
2074 NSEnumerator *pathEnum = nil;
2075 NSString *path = nil;
2076 NSEnumerator *scriptEnum = nil;
2077 OOScript *script = nil;
2078 NSString *name = nil;
2079 NSAutoreleasePool *pool = nil;
2080
2081 OOLog(@"script.load.world.begin", @"%@", @"Loading world scripts...");
2082
2083 loadedScripts = [NSMutableDictionary dictionary];
2084 paths = [ResourceManager paths];
2085 for (pathEnum = [paths objectEnumerator]; (path = [pathEnum nextObject]); )
2086 {
2087 // excluding world-scripts.plist also excludes script.js / script.plist
2088 // though as those core files don't and won't exist this is not
2089 // a problem.
2090 if (![ResourceManager corePlist:@"world-scripts.plist" excludedAt:path])
2091 {
2092 pool = [[NSAutoreleasePool alloc] init];
2093
2094 @try
2095 {
2096 results = [OOScript worldScriptsAtPath:[path stringByAppendingPathComponent:@"Config"]];
2097 if (results == nil) results = [OOScript worldScriptsAtPath:path];
2098 if (results != nil)
2099 {
2100 for (scriptEnum = [results objectEnumerator]; (script = [scriptEnum nextObject]); )
2101 {
2102 name = [script name];
2103 if (name != nil) [loadedScripts setObject:script forKey:name];
2104 else OOLog(@"script.load.unnamed", @"Discarding anonymous script %@", script);
2105 }
2106 }
2107 }
2108 @catch (NSException *exception)
2109 {
2110 OOLog(@"script.load.exception", @"***** %s encountered exception %@ (%@) while trying to load script from %@ -- ignoring this location.", __PRETTY_FUNCTION__, [exception name], [exception reason], path);
2111 // Ignore exception and keep loading other scripts.
2112 }
2113
2114 [pool release];
2115 }
2116 }
2117
2118 if (OOLogWillDisplayMessagesInClass(@"script.load.world.listAll"))
2119 {
2120 NSUInteger count = [loadedScripts count];
2121 if (count != 0)
2122 {
2123 NSMutableArray *displayNames = nil;
2124 NSEnumerator *scriptEnum = nil;
2125 OOScript *script = nil;
2126 NSString *displayString = nil;
2127
2128 displayNames = [NSMutableArray arrayWithCapacity:count];
2129
2130 for (scriptEnum = [loadedScripts objectEnumerator]; (script = [scriptEnum nextObject]); )
2131 {
2132 [displayNames addObject:[script displayName]];
2133 }
2134
2135 displayString = [[displayNames sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)] componentsJoinedByString:@"\n "];
2136 OOLog(@"script.load.world.listAll", @"Loaded %lu world scripts:\n %@", count, displayString);
2137 }
2138 else
2139 {
2140 OOLog(@"script.load.world.listAll", @"%@", @"*** No world scripts loaded.");
2141 }
2142 }
2143
2144 return loadedScripts;
2145}
2146
2147
2148+ (BOOL) writeDiagnosticData:(NSData *)data toFileNamed:(NSString *)name
2149{
2150 if (data == nil || name == nil) return NO;
2151
2152 NSString *directory = [self diagnosticFileLocation];
2153 if (directory == nil) return NO;
2154
2155 NSArray *nameComponents = [name componentsSeparatedByString:@"/"];
2156 NSUInteger count = [nameComponents count];
2157 if (count > 1)
2158 {
2159 name = [nameComponents lastObject];
2160
2161 for (NSUInteger i = 0; i < count - 1; i++)
2162 {
2163 NSString *component = [nameComponents objectAtIndex:i];
2164 if ([component hasPrefix:@"."])
2165 {
2166 component = [@"!" stringByAppendingString:[component substringFromIndex:1]];
2167 }
2168 directory = [directory stringByAppendingPathComponent:component];
2169 [[NSFileManager defaultManager] oo_createDirectoryAtPath:directory attributes:nil];
2170 }
2171 }
2172
2173 return [data writeToFile:[directory stringByAppendingPathComponent:name] atomically:YES];
2174}
2175
2176
2177+ (BOOL) writeDiagnosticString:(NSString *)string toFileNamed:(NSString *)name
2178{
2179 return [self writeDiagnosticData:[string dataUsingEncoding:NSUTF8StringEncoding] toFileNamed:name];
2180}
2181
2182
2183+ (BOOL) writeDiagnosticPList:(id)plist toFileNamed:(NSString *)name
2184{
2185 NSData *data = [plist oldSchoolPListFormatWithErrorDescription:NULL];
2186 if (data == nil) [NSPropertyListSerialization dataFromPropertyList:plist format:NSPropertyListXMLFormat_v1_0 errorDescription:NULL];
2187 if (data == nil) return NO;
2188
2189 return [self writeDiagnosticData:data toFileNamed:name];
2190}
2191
2192
2193+ (NSDictionary *) materialDefaults
2194{
2195 return [self dictionaryFromFilesNamed:@"material-defaults.plist" inFolder:@"Config" andMerge:YES];
2196}
2197
2198
2199+ (BOOL)directoryExists:(NSString *)inPath create:(BOOL)inCreate
2200{
2201 BOOL exists, directory;
2202 NSFileManager *fmgr = [NSFileManager defaultManager];
2203
2204 exists = [fmgr fileExistsAtPath:inPath isDirectory:&directory];
2205
2206 if (exists && !directory)
2207 {
2208 OOLog(@"resourceManager.write.buildPath.failed", @"Expected %@ to be a folder, but it is a file.", inPath);
2209 return NO;
2210 }
2211 if (!exists)
2212 {
2213 if (!inCreate) return NO;
2214 if (![fmgr oo_createDirectoryAtPath:inPath attributes:nil])
2215 {
2216 OOLog(@"resourceManager.write.buildPath.failed", @"Could not create folder %@.", inPath);
2217 return NO;
2218 }
2219 }
2220
2221 return YES;
2222}
2223
2224
2225+ (NSString *) diagnosticFileLocation
2226{
2228}
2229
2230
2231+ (void) logPaths
2232{
2233 NSMutableArray *displayPaths = nil;
2234 NSEnumerator *pathEnum = nil;
2235 NSString *path = nil;
2236
2237 // Prettify paths for logging.
2238 displayPaths = [NSMutableArray arrayWithCapacity:[sSearchPaths count]];
2239 for (pathEnum = [sSearchPaths objectEnumerator]; (path = [pathEnum nextObject]); )
2240 {
2241 [displayPaths addObject:[[path stringByStandardizingPath] stringByAbbreviatingWithTildeInPath]];
2242 }
2243
2244 OOLog(@"searchPaths.dumpAll", @"Resource paths: %@\n %@", sUseAddOns, [displayPaths componentsJoinedByString:@"\n "]);
2245
2246}
2247
2248
2249+ (void) clearCaches
2250{
2251 [sSoundCache release];
2252 sSoundCache = nil;
2253 [sStringCache release];
2254 sStringCache = nil;
2255}
2256
2257@end
void OOHUDResetTextEngine(void)
#define DESTROY(x)
Definition OOCocoa.h:77
#define foreachkey(VAR, DICT)
Definition OOCocoa.h:366
void OOStandardsDeprecated(NSString *message)
BOOL OOEnforceStandards(void)
void OOStandardsError(NSString *message)
#define EXPECT_NOT(x)
NSString * OOLogHandlerGetLogBasePath(void)
BOOL OOLogWillDisplayMessagesInClass(NSString *inMessageClass)
Definition OOLogging.m:144
#define OOLog(class, format,...)
Definition OOLogging.h:88
NSString *const kOOLogParameterError
Definition OOLogging.m:647
static NSString *const kOOManifestTags
static NSString *const kOOManifestRequiredOoliteVersion
static NSString *const kOOManifestTitle
static NSString *const kOOManifestTagScenarioOnly
static NSString *const kOOManifestIdentifier
static NSString *const kOOManifestVersion
static NSString *const kOOManifestRequiredBy
static NSString *const kOOManifestFilePath
NSDictionary * OODictionaryFromFile(NSString *path)
NSArray * OOArrayFromFile(NSString *path)
unsigned count
return nil
NSArray * ComponentsFromVersionString(NSString *string)
NSComparisonResult CompareVersions(NSArray *version1, NSArray *version2)
#define SCENARIO_OXP_DEFINITION_BYID
#define SCENARIO_OXP_DEFINITION_NONE
#define SCENARIO_OXP_DEFINITION_NOPLIST
OOResourceMergeMode
@ MERGE_SMART
@ MERGE_NONE
@ MERGE_BASIC
#define SCENARIO_OXP_DEFINITION_ALL
#define SCENARIO_OXP_DEFINITION_BYTAG
static NSMutableArray * sErrors
static NSString *const kOOLogCacheUpToDate
static NSArray * sUseAddOnsParts
static NSString *const kOOLogCacheStalePaths
static NSMutableArray * sSearchPaths
static NSString *const kOOCacheSearchPathModDates
static NSString *const kOOLogCacheStaleDates
static NSString *const kOOCacheKeySearchPaths
static NSMutableDictionary * sOXPManifests
static BOOL sAllMet
static NSMutableDictionary * sSoundCache
static NSMutableArray * sOXPsWithMessagesFound
static NSString * sUseAddOns
static NSMutableDictionary * sStringCache
static BOOL sFirstRun
static NSString *const kOOLogCacheExplicitFlush
static NSString *const kOOCacheKeyModificationDates
NSDictionary * ParseOOSScripts(NSString *script)
static NSMutableArray * sExternalPaths
#define PLANETINFO_UNIVERSAL_KEY
Definition Universe.h:153
@ EQUIPMENT_EXTRA_INFO_INDEX
Definition Universe.h:84
#define PLANETINFO_INTERSTELLAR_KEY
Definition Universe.h:154
void setObject:forKey:inCache:(id inElement,[forKey] NSString *inKey,[inCache] NSString *inCacheKey)
void setAllowCacheWrites:(BOOL flag)
id objectForKey:inCache:(NSString *inKey,[inCache] NSString *inCacheKey)
OOCacheManager * sharedCache()
NSString * installPath()
OOOXZManager * sharedManager()
NSString * name()
Definition OOScript.m:256
NSArray * worldScriptsAtPath:(NSString *path)
Definition OOScript.m:45
NSString * displayName()
Definition OOScript.m:277
void setUniversalProperties:(NSDictionary *properties)
void setProperties:forSystemKey:(NSDictionary *properties,[forSystemKey] NSString *key)
void setInterstellarProperties:(NSDictionary *properties)
NSArray * pathsWithAddOns()
BOOL manifestAllowedByScenario:withTag:(NSDictionary *manifest, [withTag] NSString *tag)
NSString * builtInPath()
NSArray * arrayFromFilesNamed:inFolder:andMerge:(NSString *fileName,[inFolder] NSString *folderName,[andMerge] BOOL mergeFiles)
void mergeRoleCategories:intoDictionary:(NSDictionary *catData, [intoDictionary] NSMutableDictionary *categories)
BOOL manifestAllowedByScenario:withIdentifier:(NSDictionary *manifest, [withIdentifier] NSString *identifier)
NSDictionary * dictionaryFromFilesNamed:inFolder:mergeMode:cache:(NSString *fileName,[inFolder] NSString *folderName,[mergeMode] OOResourceMergeMode mergeMode,[cache] BOOL useCache)
int ZEXPORT unzGetCurrentFileInfo64(unzFile file, unz_file_info64 *pfile_info, char *szFileName, uLong fileNameBufferSize, void *extraField, uLong extraFieldBufferSize, char *szComment, uLong commentBufferSize)
Definition unzip.c:1130
int ZEXPORT unzGoToFirstFile(unzFile file)
Definition unzip.c:1184
unzFile ZEXPORT unzOpen64(const void *path)
Definition unzip.c:801
int ZEXPORT unzGoToNextFile(unzFile file)
Definition unzip.c:1205
int ZEXPORT unzClose(unzFile file)
Definition unzip.c:811
voidp unzFile
Definition unzip.h:70
#define UNZ_OK
Definition unzip.h:74