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