Line data Source code
1 0 : /*
2 :
3 : ResourceManager.m
4 :
5 : Oolite
6 : Copyright (C) 2004-2013 Giles C Williams and contributors
7 :
8 : This program is free software; you can redistribute it and/or
9 : modify it under the terms of the GNU General Public License
10 : as published by the Free Software Foundation; either version 2
11 : of the License, or (at your option) any later version.
12 :
13 : This program is distributed in the hope that it will be useful,
14 : but WITHOUT ANY WARRANTY; without even the implied warranty of
15 : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 : GNU General Public License for more details.
17 :
18 : You should have received a copy of the GNU General Public License
19 : along with this program; if not, write to the Free Software
20 : Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
21 : MA 02110-1301, USA.
22 :
23 : */
24 :
25 : #import "ResourceManager.h"
26 : #import "NSScannerOOExtensions.h"
27 : #import "NSMutableDictionaryOOExtensions.h"
28 : #import "NSStringOOExtensions.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"
35 : #import "OOCollectionExtractors.h"
36 : #import "OOLogOutputHandler.h"
37 : #import "NSFileManagerOOExtensions.h"
38 : #import "OldSchoolPropertyListWriting.h"
39 : #import "OOOXZManager.h"
40 : #import "unzip.h"
41 : #import "HeadUpDisplay.h"
42 : #import "OODebugStandards.h"
43 : #import "OOSystemDescriptionManager.h"
44 :
45 : #import "OOJSScript.h"
46 : #import "OOPListScript.h"
47 :
48 : #import "OOManifestProperties.h"
49 :
50 0 : static NSString * const kOOLogCacheUpToDate = @"dataCache.upToDate";
51 0 : static NSString * const kOOLogCacheExplicitFlush = @"dataCache.rebuild.explicitFlush";
52 0 : static NSString * const kOOLogCacheStalePaths = @"dataCache.rebuild.pathsChanged";
53 0 : static NSString * const kOOLogCacheStaleDates = @"dataCache.rebuild.datesChanged";
54 0 : static NSString * const kOOCacheSearchPathModDates = @"search path modification dates";
55 0 : static NSString * const kOOCacheKeySearchPaths = @"search paths";
56 0 : static NSString * const kOOCacheKeyModificationDates = @"modification dates";
57 :
58 :
59 :
60 0 : extern NSDictionary* ParseOOSScripts(NSString* script);
61 :
62 :
63 : @interface ResourceManager (OOPrivate)
64 :
65 0 : + (void) checkOXPMessagesInPath:(NSString *)path;
66 0 : + (void) checkPotentialPath:(NSString *)path :(NSMutableArray *)searchPaths;
67 0 : + (BOOL) validateManifest:(NSDictionary*)manifest forOXP:(NSString *)path;
68 0 : + (BOOL) areRequirementsFulfilled:(NSDictionary*)requirements forOXP:(NSString *)path andFile:(NSString *)file;
69 0 : + (void) filterSearchPathsForConflicts:(NSMutableArray *)searchPaths;
70 0 : + (BOOL) filterSearchPathsForRequirements:(NSMutableArray *)searchPaths;
71 0 : + (void) filterSearchPathsToExcludeScenarioOnlyPaths:(NSMutableArray *)searchPaths;
72 0 : + (void) filterSearchPathsByScenario:(NSMutableArray *)searchPaths;
73 0 : + (BOOL) manifestAllowedByScenario:(NSDictionary *)manifest;
74 0 : + (BOOL) manifestAllowedByScenario:(NSDictionary *)manifest withIdentifier:(NSString *)identifier;
75 0 : + (BOOL) manifestAllowedByScenario:(NSDictionary *)manifest withTag:(NSString *)tag;
76 :
77 0 : + (void) addErrorWithKey:(NSString *)descriptionKey param1:(id)param1 param2:(id)param2;
78 0 : + (BOOL) checkCacheUpToDateForPaths:(NSArray *)searchPaths;
79 0 : + (void) logPaths;
80 0 : + (void) mergeRoleCategories:(NSDictionary *)catData intoDictionary:(NSMutableDictionary *)category;
81 0 : + (void) preloadFileLists;
82 0 : + (void) preloadFileListFromOXZ:(NSString *)path forFolders:(NSArray *)folders;
83 0 : + (void) preloadFileListFromFolder:(NSString *)path forFolders:(NSArray *)folders;
84 0 : + (void) preloadFilePathFor:(NSString *)fileName inFolder:(NSString *)subFolder atPath:(NSString *)path;
85 :
86 : @end
87 :
88 :
89 0 : static NSMutableArray *sSearchPaths;
90 0 : static NSString *sUseAddOns;
91 0 : static NSArray *sUseAddOnsParts;
92 0 : static BOOL sFirstRun = YES;
93 0 : static BOOL sAllMet = NO;
94 0 : static NSMutableArray *sOXPsWithMessagesFound;
95 0 : static NSMutableArray *sExternalPaths;
96 0 : static NSMutableArray *sErrors;
97 0 : static NSMutableDictionary *sOXPManifests;
98 :
99 :
100 :
101 : // caches allow us to load any given file once only
102 : //
103 0 : static NSMutableDictionary *sSoundCache;
104 0 : static NSMutableDictionary *sStringCache;
105 :
106 :
107 :
108 : @implementation ResourceManager
109 :
110 : + (void) reset
111 : {
112 : sFirstRun = YES;
113 : DESTROY(sUseAddOns);
114 : DESTROY(sUseAddOnsParts);
115 : DESTROY(sSearchPaths);
116 : DESTROY(sOXPsWithMessagesFound);
117 : DESTROY(sExternalPaths);
118 : DESTROY(sErrors);
119 : DESTROY(sOXPManifests);
120 : }
121 :
122 :
123 : + (void) resetManifestKnowledgeForOXZManager
124 : {
125 : DESTROY(sUseAddOns);
126 : DESTROY(sUseAddOnsParts);
127 : DESTROY(sSearchPaths);
128 : DESTROY(sOXPManifests);
129 : [ResourceManager pathsWithAddOns];
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
269 : DESTROY(sSearchPaths);
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 0 : + (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 0 : + (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 : {
405 : unzGetCurrentFileInfo64(uf, NULL,
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 0 : + (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 0 : + (void) preloadFilePathFor:(NSString *)fileName inFolder:(NSString *)subFolder atPath:(NSString *)path
457 : {
458 : OOCacheManager *cache = [OOCacheManager sharedCache];
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 : {
473 : if (EXPECT_NOT(sSearchPaths == nil))
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;
493 : DESTROY(sUseAddOnsParts);
494 : DESTROY(sUseAddOns);
495 : sUseAddOns = [useAddOns retain];
496 : sUseAddOnsParts = [[sUseAddOns componentsSeparatedByString:@";"] retain];
497 :
498 : [ResourceManager clearCaches];
499 : OOHUDResetTextEngine();
500 :
501 : OOCacheManager *cmgr = [OOCacheManager sharedCache];
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 0 : + (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 0 : + (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 0 : + (BOOL) validateManifest:(NSDictionary*)manifest forOXP:(NSString *)path
642 : {
643 : if (EXPECT_NOT(sOXPManifests == nil))
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 0 : + (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 0 : + (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 0 : + (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 0 : + (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 0 : + (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 0 : + (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 0 : + (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 0 : + (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 0 : + (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 0 : + (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 : */
1132 : OOCacheManager *cacheMgr = [OOCacheManager sharedCache];
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;
1242 : OOCacheManager *cacheMgr = [OOCacheManager sharedCache];
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;
1353 : OOCacheManager *cache = [OOCacheManager sharedCache];
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 :
1679 0 : static 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 0 : + (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");
1822 : OOSystemDescriptionManager *manager = [[OOSystemDescriptionManager alloc] init];
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:@"$inherit"];
1891 : if (inheritKey != nil)
1892 : {
1893 : changeCount++;
1894 : NSMutableDictionary *mutableValue = [[value mutableCopy] autorelease];
1895 : [mutableValue removeObjectForKey:@"$inherit"];
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;
1927 : OOCacheManager *cache = [OOCacheManager sharedCache];
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 0 : + (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 0 : + (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 : {
2227 : return OOLogHandlerGetLogBasePath();
2228 : }
2229 :
2230 :
2231 0 : + (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
|