oolite/src/Core/OOShipRegistry.m

1824 lines
58 KiB
Objective-C

/*
OOShipRegistry.m
Copyright (C) 2008-2013 Jens Ayton and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#import "OOShipRegistry.h"
#import "OOCacheManager.h"
#import "ResourceManager.h"
#import "OOCollectionExtractors.h"
#import "NSDictionaryOOExtensions.h"
#import "OOProbabilitySet.h"
#import "OORoleSet.h"
#import "OOStringParsing.h"
#import "OOMesh.h"
#import "GameController.h"
#import "OOLegacyScriptWhitelist.h"
#import "OODeepCopy.h"
#import "OOColor.h"
#import "OOStringExpander.h"
#import "OOShipLibraryDescriptions.h"
#import "Universe.h"
#import "OOJSScript.h"
#import "OODebugStandards.h"
#define PRELOAD 0
static void DumpStringAddrs(NSDictionary *dict, NSString *context);
static NSComparisonResult SortDemoShipsByName (id a, id b, void* context);
static NSComparisonResult SortDemoCategoriesByName (id a, id b, void* context);
static OOShipRegistry *sSingleton = nil;
static NSString * const kShipRegistryCacheName = @"ship registry";
static NSString * const kShipDataCacheKey = @"ship data";
static NSString * const kPlayerShipsCacheKey = @"player ships";
static NSString * const kRoleWeightsCacheKey = @"role weights";
static NSString * const kDefaultDemoShip = @"coriolis-station";
static NSString * const kVisualEffectRegistryCacheName = @"visual effect registry";
static NSString * const kVisualEffectDataCacheKey = @"visual effect data";
@interface OOShipRegistry (OODataLoader)
- (void) loadShipData;
- (void) loadDemoShipConditions;
- (void) loadDemoShips;
- (void) loadCachedRoleProbabilitySets;
- (void) buildRoleProbabilitySets;
- (BOOL) applyLikeShips:(NSMutableDictionary *)ioData withKey:(NSString *)likeKey;
- (BOOL) loadAndMergeShipyard:(NSMutableDictionary *)ioData;
- (BOOL) stripPrivateKeys:(NSMutableDictionary *)ioData;
- (BOOL) makeShipEntriesMutable:(NSMutableDictionary *)ioData;
- (BOOL) loadAndApplyShipDataOverrides:(NSMutableDictionary *)ioData;
- (BOOL) canonicalizeAndTagSubentities:(NSMutableDictionary *)ioData;
- (BOOL) removeUnusableEntries:(NSMutableDictionary *)ioData shipMode:(BOOL)shipMode;
- (BOOL) sanitizeConditions:(NSMutableDictionary *)ioData;
#if PRELOAD
- (BOOL) preloadShipMeshes:(NSMutableDictionary *)ioData;
#endif
- (NSMutableDictionary *) mergeShip:(NSDictionary *)child withParent:(NSDictionary *)parent;
- (void) mergeShipRoles:(NSString *)roles forShipKey:(NSString *)shipKey intoProbabilityMap:(NSMutableDictionary *)probabilitySets;
- (NSDictionary *) canonicalizeSubentityDeclaration:(id)declaration
forShip:(NSString *)shipKey
shipData:(NSDictionary *)shipData
fatalError:(BOOL *)outFatalError;
- (NSDictionary *) translateOldStyleSubentityDeclaration:(NSString *)declaration
forShip:(NSString *)shipKey
shipData:(NSDictionary *)shipData
fatalError:(BOOL *)outFatalError;
- (NSDictionary *) translateOldStyleFlasherDeclaration:(NSArray *)tokens
forShip:(NSString *)shipKey
fatalError:(BOOL *)outFatalError;
- (NSDictionary *) translateOldStandardBasicSubentityDeclaration:(NSArray *)tokens
forShip:(NSString *)shipKey
shipData:(NSDictionary *)shipData
fatalError:(BOOL *)outFatalError;
- (NSDictionary *) validateNewStyleSubentityDeclaration:(NSDictionary *)declaration
forShip:(NSString *)shipKey
fatalError:(BOOL *)outFatalError;
- (NSDictionary *) validateNewStyleFlasherDeclaration:(NSDictionary *)declaration
forShip:(NSString *)shipKey
fatalError:(BOOL *)outFatalError;
- (NSDictionary *) validateNewStyleStandardSubentityDeclaration:(NSDictionary *)declaration
forShip:(NSString *)shipKey
fatalError:(BOOL *)outFatalError;
- (BOOL) shipIsBallTurretForKey:(NSString *)shipKey inShipData:(NSDictionary *)shipData;
@end
@implementation OOShipRegistry
+ (OOShipRegistry *) sharedRegistry
{
if (sSingleton == nil)
{
sSingleton = [[self alloc] init];
}
return sSingleton;
}
+ (void) reload
{
if (sSingleton != nil)
{
/* CIM: 'release' doesn't work - the class definition
* overrides it, so this leaks memory. Needs a proper reset
* method for reloading the ship registry data instead */
[sSingleton release];
sSingleton = nil;
(void) [self sharedRegistry];
}
}
- (id) init
{
if ((self = [super init]))
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
OOCacheManager *cache = [OOCacheManager sharedCache];
_shipData = [[cache objectForKey:kShipDataCacheKey inCache:kShipRegistryCacheName] retain];
_playerShips = [[cache objectForKey:kPlayerShipsCacheKey inCache:kShipRegistryCacheName] retain];
_effectData = [[cache objectForKey:kVisualEffectDataCacheKey inCache:kVisualEffectRegistryCacheName] retain];
if ([_shipData count] == 0) // Don't accept nil or empty
{
[self loadShipData];
if ([_shipData count] == 0)
{
[NSException raise:@"OOShipRegistryLoadFailure" format:@"Could not load any ship data."];
}
if ([_playerShips count] == 0)
{
[NSException raise:@"OOShipRegistryLoadFailure" format:@"Could not load any player ships."];
}
}
[self loadDemoShipConditions];
[self loadDemoShips]; // testing only
if ([_demoShips count] == 0)
{
[NSException raise:@"OOShipRegistryLoadFailure" format:@"Could not load or synthesize any demo ships."];
}
[self loadCachedRoleProbabilitySets];
if (_probabilitySets == nil)
{
[self buildRoleProbabilitySets];
if ([_probabilitySets count] == 0)
{
[NSException raise:@"OOShipRegistryLoadFailure" format:@"Could not load or synthesize role probability sets."];
}
}
[pool release];
}
return self;
}
- (void) dealloc
{
[_shipData release];
[_demoShips release];
[_playerShips release];
[_probabilitySets release];
[super dealloc];
}
- (NSDictionary *) shipInfoForKey:(NSString *)key
{
return [_shipData objectForKey:key];
}
- (void) setShipInfoForKey:(NSString *)key with:(NSDictionary *)newShipData
{
NSMutableDictionary *mutableDict = [NSMutableDictionary dictionaryWithDictionary:_shipData];
[mutableDict setObject:OODeepCopy(newShipData) forKey:key];
DESTROY(_shipData);
_shipData = [[NSDictionary dictionaryWithDictionary:mutableDict] retain];
}
- (NSDictionary *) effectInfoForKey:(NSString *)key
{
return [_effectData objectForKey:key];
}
- (NSDictionary *) shipyardInfoForKey:(NSString *)key
{
return [[self shipInfoForKey:key] objectForKey:@"_oo_shipyard"];
}
- (OOProbabilitySet *) probabilitySetForRole:(NSString *)role
{
if (role == nil) return nil;
return [_probabilitySets objectForKey:role];
}
- (NSArray *) demoShipKeys
{
// with condition scripts in use, can't cache this value
[self loadDemoShips];
return [[_demoShips copy] autorelease];
}
- (NSArray *) playerShipKeys
{
return _playerShips;
}
@end
@implementation OOShipRegistry (OOConveniences)
- (NSArray *) shipKeys
{
return [_shipData allKeys];
}
- (NSArray *) shipRoles
{
return [_probabilitySets allKeys];
}
- (NSArray *) shipKeysWithRole:(NSString *)role
{
return [[self probabilitySetForRole:role] allObjects];
}
- (NSString *) randomShipKeyForRole:(NSString *)role
{
return [[self probabilitySetForRole:role] randomObject];
}
@end
@implementation OOShipRegistry (OODataLoader)
/* -loadShipData
Load the data for all ships. This consists of five stages:
* Load merges shipdata.plist dictionary.
* Apply all like_ship entries.
* Load shipdata-overrides.plist and apply patches.
* Load shipyard.plist, add shipyard data into ship dictionaries, and
create _playerShips array.
* Build role->ship type probability sets.
*/
- (void) loadShipData
{
NSMutableDictionary *result = nil;
[_shipData release];
_shipData = nil;
[_playerShips release];
_playerShips = nil;
// Load shipdata.plist.
result = [[[ResourceManager dictionaryFromFilesNamed:@"shipdata.plist"
inFolder:@"Config"
mergeMode:MERGE_BASIC
cache:NO] mutableCopy] autorelease];
if (result == nil) return;
DumpStringAddrs(result, @"shipdata.plist");
// Make each entry mutable to simplify later stages. Also removes any entries that aren't dictionaries.
if (![self makeShipEntriesMutable:result]) return;
OOLog(@"shipData.load.progress", @"%@", @"Finished initial cleanup...");
// Apply patches.
if (![self loadAndApplyShipDataOverrides:result]) return;
OOLog(@"shipData.load.progress", @"%@", @"Finished applying patches...");
// Strip private keys (anything starting with _oo_).
if (![self stripPrivateKeys:result]) return;
OOLog(@"shipData.load.progress", @"%@", @"Finished stripping private keys...");
// Resolve like_ship entries.
if (![self applyLikeShips:result withKey:@"like_ship"]) return;
OOLog(@"shipData.load.progress", @"%@", @"Finished resolving like_ships...");
// Clean up subentity declarations and tag subentities so they won't be pruned.
if (![self canonicalizeAndTagSubentities:result]) return;
OOLog(@"shipData.load.progress", @"%@", @"Finished cleaning up subentities...");
// Clean out templates and invalid entries.
if (![self removeUnusableEntries:result shipMode:YES]) return;
OOLog(@"shipData.load.progress", @"%@", @"Finished removing invalid entries...");
// Add shipyard entries into shipdata entries.
if (![self loadAndMergeShipyard:result]) return;
OOLog(@"shipData.load.progress", @"%@", @"Finished adding shipyard entries...");
// Sanitize conditions.
if (![self sanitizeConditions:result]) return;
OOLog(@"shipData.load.progress", @"%@", @"Finished validating data...");
#if PRELOAD
// Preload and cache meshes.
if (![self preloadShipMeshes:result]) return;
OOLog(@"shipData.load.progress", @"%@", @"Finished loading meshes...");
#endif
_shipData = OODeepCopy(result);
[[OOCacheManager sharedCache] setObject:_shipData forKey:kShipDataCacheKey inCache:kShipRegistryCacheName];
OOLog(@"shipData.load.done", @"%@", @"Ship data loaded.");
[_effectData release];
_effectData = nil;
result = [[[ResourceManager dictionaryFromFilesNamed:@"effectdata.plist"
inFolder:@"Config"
mergeMode:MERGE_BASIC
cache:NO] mutableCopy] autorelease];
if (result == nil) return;
// Make each entry mutable to simplify later stages. Also removes any entries that aren't dictionaries.
if (![self makeShipEntriesMutable:result]) return;
OOLog(@"effectData.load.progress", @"%@", @"Finished initial cleanup...");
// Strip private keys (anything starting with _oo_).
if (![self stripPrivateKeys:result]) return;
OOLog(@"effectData.load.progress", @"%@", @"Finished stripping private keys...");
// Resolve like_effect entries.
if (![self applyLikeShips:result withKey:@"like_effect"]) return;
OOLog(@"effectData.load.progress", @"%@", @"Finished resolving like_effects...");
// Clean up subentity declarations and tag subentities so they won't be pruned.
if (![self canonicalizeAndTagSubentities:result]) return;
OOLog(@"effectData.load.progress", @"%@", @"Finished cleaning up subentities...");
// Clean out templates and invalid entries.
if (![self removeUnusableEntries:result shipMode:NO]) return;
OOLog(@"effectData.load.progress", @"%@", @"Finished removing invalid entries...");
_effectData = OODeepCopy(result);
[[OOCacheManager sharedCache] setObject:_effectData forKey:kVisualEffectDataCacheKey inCache:kVisualEffectRegistryCacheName];
OOLog(@"effectData.load.done", @"%@", @"Effect data loaded.");
}
- (void) loadDemoShipConditions
{
NSMutableArray *conditionScripts = [[NSMutableArray alloc] init];
NSEnumerator *enumerator = nil;
NSDictionary *key = nil;
NSArray *initialDemoShips = nil;
initialDemoShips = [ResourceManager arrayFromFilesNamed:@"shiplibrary.plist"
inFolder:@"Config"
andMerge:YES
cache:NO];
for (enumerator = [initialDemoShips objectEnumerator]; (key = [enumerator nextObject]); )
{
NSString *conditions = [key oo_stringForKey:kOODemoShipConditions defaultValue:nil];
if (conditions != nil)
{
[conditionScripts addObject:conditions];
}
}
[[OOCacheManager sharedCache] setObject:conditionScripts forKey:@"demoship conditions" inCache:@"condition scripts"];
[conditionScripts release];
}
/* -loadDemoShips
Load demoships.plist, and filter out non-existent ships. If no existing
ships remain, try adding coriolis; if this fails, add any ship in
shipdata.
*/
- (void) loadDemoShips
{
NSEnumerator *enumerator = nil;
NSDictionary *key = nil;
NSArray *initialDemoShips = nil;
NSMutableArray *demoShips = nil;
DESTROY(_demoShips);
initialDemoShips = [ResourceManager arrayFromFilesNamed:@"shiplibrary.plist"
inFolder:@"Config"
andMerge:YES
cache:NO];
demoShips = [NSMutableArray arrayWithArray:initialDemoShips];
// Note: iterate over initialDemoShips to avoid mutating the collection being enu,erated.
for (enumerator = [initialDemoShips objectEnumerator]; (key = [enumerator nextObject]); )
{
NSString *shipKey = [key oo_stringForKey:kOODemoShipKey];
if (![key isKindOfClass:[NSDictionary class]] || [self shipInfoForKey:shipKey] == nil)
{
[demoShips removeObject:key];
}
else
{
NSString *conditions = [key oo_stringForKey:kOODemoShipConditions defaultValue:nil];
if (conditions != nil)
{
if ([PLAYER status] == STATUS_START_GAME)
{
// conditions always false here
[demoShips removeObject:key];
}
else
{
OOJSScript *condScript = [UNIVERSE getConditionScript:conditions];
if (condScript != nil) // should always be non-nil, but just in case
{
JSContext *context = OOJSAcquireContext();
BOOL OK;
JSBool allow_use;
jsval result;
jsval args[] = { OOJSValueFromNativeObject(context, shipKey) };
OK = [condScript callMethod:OOJSID("allowShowLibraryShip")
inContext:context
withArguments:args count:sizeof args / sizeof *args
result:&result];
if (OK) OK = JS_ValueToBoolean(context, result, &allow_use);
OOJSRelinquishContext(context);
if (OK && !allow_use)
{
/* if the script exists, the function exists, the function
* returns a bool, and that bool is false, hide the
* ship. Otherwise allow it as default */
[demoShips removeObject:key];
}
}
}
}
}
}
if ([demoShips count] == 0)
{
NSString *shipKey = nil;
if ([self shipInfoForKey:kDefaultDemoShip] != nil)
{
shipKey = kDefaultDemoShip;
}
else
{
shipKey = [[_shipData allKeys] objectAtIndex:0];
}
[demoShips addObject:[NSDictionary dictionaryWithObject:shipKey forKey:kOODemoShipKey]];
}
// now separate out the demoships by class, and add some extra keys
NSMutableDictionary *demoList = [NSMutableDictionary dictionaryWithCapacity:8];
NSMutableArray *demoClass = nil;
foreach(key, demoShips)
{
NSString *class = [key oo_stringForKey:kOODemoShipClass defaultValue:@"ship"];
if ([OOShipLibraryCategoryPlural(class) length] == 0)
{
OOLog(@"shipdata.load.warning",@"Unexpected class '%@' in shiplibrary.plist for '%@'",class,[key oo_stringForKey:kOODemoShipKey]);
class = @"ship";
}
demoClass = [demoList objectForKey:class];
if (demoClass == nil)
{
[demoList setObject:[NSMutableArray array] forKey:class];
demoClass = [demoList objectForKey:class];
}
NSMutableDictionary *demoEntry = [NSMutableDictionary dictionaryWithDictionary:key];
// add "name" object to dictionary from ship definition
[demoEntry setObject:[[self shipInfoForKey:[demoEntry oo_stringForKey:@"ship"]] oo_stringForKey:kOODemoShipName] forKey:kOODemoShipName];
// set "class" object to standard ship if not otherwise set
if (![[demoEntry oo_stringForKey:kOODemoShipClass defaultValue:nil] isEqualToString:class])
{
[demoEntry setObject:class forKey:kOODemoShipClass];
}
[demoClass addObject:demoEntry];
}
// sort each ship list by name
NSString *demoClassName = nil;
foreach(demoClassName, demoList)
{
[[demoList objectForKey:demoClassName] sortUsingFunction:SortDemoShipsByName context:NULL];
}
// and then sort the ship list list by class name
_demoShips = [[[demoList allValues] sortedArrayUsingFunction:SortDemoCategoriesByName context:NULL] retain];
}
- (void) loadCachedRoleProbabilitySets
{
NSDictionary *cachedSets = nil;
NSMutableDictionary *restoredSets = nil;
NSEnumerator *roleEnum = nil;
NSString *role = nil;
cachedSets = [[OOCacheManager sharedCache] objectForKey:kRoleWeightsCacheKey inCache:kShipRegistryCacheName];
if (cachedSets == nil) return;
restoredSets = [NSMutableDictionary dictionaryWithCapacity:[cachedSets count]];
for (roleEnum = [cachedSets keyEnumerator]; (role = [roleEnum nextObject]); )
{
[restoredSets setObject:[OOProbabilitySet probabilitySetWithPropertyListRepresentation:[cachedSets objectForKey:role]] forKey:role];
}
_probabilitySets = [restoredSets copy];
}
- (void) buildRoleProbabilitySets
{
NSMutableDictionary *probabilitySets = nil;
NSEnumerator *shipEnum = nil;
NSString *shipKey = nil;
NSDictionary *shipEntry = nil;
NSString *roles = nil;
NSEnumerator *roleEnum = nil;
NSString *role = nil;
OOProbabilitySet *pset = nil;
NSMutableDictionary *cacheEntry = nil;
probabilitySets = [NSMutableDictionary dictionary];
// Build role sets
for (shipEnum = [_shipData keyEnumerator]; (shipKey = [shipEnum nextObject]); )
{
shipEntry = [_shipData objectForKey:shipKey];
roles = [shipEntry oo_stringForKey:@"roles"];
[self mergeShipRoles:roles forShipKey:shipKey intoProbabilityMap:probabilitySets];
}
// Convert role sets to immutable form, and build cache entry.
// Note: we iterate over a copy of the keys to avoid mutating while iterating.
cacheEntry = [NSMutableDictionary dictionaryWithCapacity:[probabilitySets count]];
for (roleEnum = [[probabilitySets allKeys] objectEnumerator]; (role = [roleEnum nextObject]); )
{
pset = [probabilitySets objectForKey:role];
pset = [[pset copy] autorelease];
[probabilitySets setObject:pset forKey:role];
[cacheEntry setObject:[pset propertyListRepresentation] forKey:role];
}
_probabilitySets = [probabilitySets copy];
[[OOCacheManager sharedCache] setObject:cacheEntry forKey:kRoleWeightsCacheKey inCache:kShipRegistryCacheName];
}
/* -applyLikeShips:
Implement like_ship by copying inherited ship and overwriting with child
ship values. Done iteratively to report recursive references of arbitrary
depth. Also removes and reports ships whose like_ship entry does not
resolve, and handles reference loops by removing all ships involved.
We start with a set of keys all ships that have a like_ships entry. In
each iteration, every ship whose like_ship entry does not refer to a ship
which itself has a like_ship entry is finalized. If the set of pending
ships does not shrink in an iteration, the remaining ships cannot be
resolved (either their like_ships do not exist, or they form reference
cycles) so we stop looping and report it.
*/
- (BOOL) applyLikeShips:(NSMutableDictionary *)ioData withKey:(NSString *)likeKey
{
NSMutableSet *remainingLikeShips = nil;
NSEnumerator *enumerator = nil;
NSString *key = nil;
NSString *parentKey = nil;
NSDictionary *shipEntry = nil;
NSDictionary *parentEntry = nil;
NSUInteger count, lastCount;
NSMutableArray *reportedBadShips = nil;
// Build set of ships with like_ship references
remainingLikeShips = [NSMutableSet set];
for (enumerator = [ioData keyEnumerator]; (key = [enumerator nextObject]); )
{
shipEntry = [ioData objectForKey:key];
if ([shipEntry oo_stringForKey:likeKey] != nil)
{
[remainingLikeShips addObject:key];
}
}
count = lastCount = [remainingLikeShips count];
while (count != 0)
{
for (enumerator = [[[remainingLikeShips copy] autorelease] objectEnumerator]; (key = [enumerator nextObject]); )
{
// Look up like_ship entry
shipEntry = [ioData objectForKey:key];
parentKey = [shipEntry objectForKey:likeKey];
if (![remainingLikeShips containsObject:parentKey])
{
// If parent is fully resolved, we can resolve this child.
parentEntry = [ioData objectForKey:parentKey];
shipEntry = [self mergeShip:shipEntry withParent:parentEntry];
if (shipEntry != nil)
{
[remainingLikeShips removeObject:key];
[ioData setObject:shipEntry forKey:key];
}
}
}
count = [remainingLikeShips count];
if (count == lastCount)
{
/* Fail: we couldn't resolve all like_ship entries.
Remove unresolved entries, building a list of the ones that
don't have is_external_dependency set.
*/
reportedBadShips = [NSMutableArray array];
for (enumerator = [remainingLikeShips objectEnumerator]; (key = [enumerator nextObject]); )
{
if (![[ioData oo_dictionaryForKey:key] oo_boolForKey:@"is_external_dependency"])
{
[reportedBadShips addObject:key];
}
[ioData removeObjectForKey:key];
}
if ([reportedBadShips count] != 0)
{
[reportedBadShips sortUsingSelector:@selector(caseInsensitiveCompare:)];
OOLogERR(@"shipData.merge.failed", @"one or more shipdata.plist entries have %@ references that cannot be resolved: %@", likeKey, [reportedBadShips componentsJoinedByString:@", "]); // FIXME: distinguish shipdata and effectdata
OOStandardsError(@"Likely missing a dependency in a manifest.plist");
}
break;
}
lastCount = count;
}
return YES;
}
- (NSMutableDictionary *) mergeShip:(NSDictionary *)child withParent:(NSDictionary *)parent
{
NSMutableDictionary *result = [[parent mutableCopy] autorelease];
if (result == nil) return nil;
[result addEntriesFromDictionary:child];
[result removeObjectForKey:@"like_ship"];
// Certain properties cannot be inherited.
if ([child oo_stringForKey:@"display_name"] == nil) [result removeObjectForKey:@"display_name"];
if ([child oo_stringForKey:@"is_template"] == nil) [result removeObjectForKey:@"is_template"];
// Since both 'scanClass' and 'scan_class' are accepted as valid keys for the scanClass property,
// we may end up with conflicting scanClass and scan_class keys from like_ship relationships getting
// merged in the result dictionary. We want to always have the child overriding the parent setting
// and we do that by determining which of the two keys belongs to the child dictionary and removing
// the other one from the result - Nikos 20100512
if ([result oo_stringForKey:@"scan_class"] != nil && [result oo_stringForKey:@"scanClass"] != nil)
{
if ([child oo_stringForKey:@"scanClass"] != nil)
[result removeObjectForKey:@"scan_class"];
else
[result removeObjectForKey:@"scanClass"];
}
// TODO: all normalised/non-normalised value name pairs need to be catered for. - Kaks 2010-05-13
if ([result oo_stringForKey:@"escort_role"] != nil && [result oo_stringForKey:@"escort-role"] != nil)
{
if ([child oo_stringForKey:@"escort-role"] != nil)
[result removeObjectForKey:@"escort_role"];
else
[result removeObjectForKey:@"escort-role"];
}
if ([result oo_stringForKey:@"escort_ship"] != nil && [result oo_stringForKey:@"escort-ship"] != nil)
{
if ([child oo_stringForKey:@"escort-ship"] != nil)
[result removeObjectForKey:@"escort_ship"];
else
[result removeObjectForKey:@"escort-ship"];
}
if ([result oo_stringForKey:@"is_carrier"] != nil && [result oo_stringForKey:@"isCarrier"] != nil)
{
if ([child oo_stringForKey:@"isCarrier"] != nil)
[result removeObjectForKey:@"is_carrier"];
else
[result removeObjectForKey:@"isCarrier"];
}
if ([result oo_stringForKey:@"has_shipyard"] != nil && [result oo_stringForKey:@"hasShipyard"] != nil)
{
if ([child oo_stringForKey:@"hasShipyard"] != nil)
[result removeObjectForKey:@"has_shipyard"];
else
[result removeObjectForKey:@"hasShipyard"];
}
return result;
}
- (BOOL) makeShipEntriesMutable:(NSMutableDictionary *)ioData
{
NSEnumerator *shipKeyEnum = nil;
NSString *shipKey = nil;
NSDictionary *shipEntry = nil;
for (shipKeyEnum = [[ioData allKeys] objectEnumerator]; (shipKey = [shipKeyEnum nextObject]); )
{
shipEntry = [ioData objectForKey:shipKey];
if (![shipEntry isKindOfClass:[NSDictionary class]])
{
OOLogERR(@"shipData.load.badEntry", @"the shipdata.plist entry \"%@\" is not a dictionary.", shipKey);
[ioData removeObjectForKey:shipKey];
}
else
{
shipEntry = [shipEntry mutableCopy];
[ioData setObject:shipEntry forKey:shipKey];
[shipEntry release];
}
}
return YES;
}
- (BOOL) loadAndApplyShipDataOverrides:(NSMutableDictionary *)ioData
{
NSEnumerator *shipKeyEnum = nil;
NSString *shipKey = nil;
NSMutableDictionary *shipEntry = nil;
NSDictionary *overrides = nil;
NSDictionary *overridesEntry = nil;
overrides = [ResourceManager dictionaryFromFilesNamed:@"shipdata-overrides.plist"
inFolder:@"Config"
mergeMode:MERGE_SMART
cache:NO];
for (shipKeyEnum = [overrides keyEnumerator]; (shipKey = [shipKeyEnum nextObject]); )
{
shipEntry = [ioData objectForKey:shipKey];
if (shipEntry != nil)
{
overridesEntry = [overrides objectForKey:shipKey];
if (![overridesEntry isKindOfClass:[NSDictionary class]])
{
OOLogERR(@"shipData.load.error", @"the shipdata-overrides.plist entry \"%@\" is not a dictionary.", shipKey);
}
else
{
[shipEntry addEntriesFromDictionary:overridesEntry];
}
}
}
return YES;
}
- (BOOL) stripPrivateKeys:(NSMutableDictionary *)ioData
{
NSEnumerator *shipKeyEnum = nil;
NSString *shipKey = nil;
NSMutableDictionary *shipEntry = nil;
NSEnumerator *attrKeyEnum = nil;
NSString *attrKey = nil;
for (shipKeyEnum = [ioData keyEnumerator]; (shipKey = [shipKeyEnum nextObject]); )
{
shipEntry = [ioData objectForKey:shipKey];
for (attrKeyEnum = [shipEntry keyEnumerator]; (attrKey = [attrKeyEnum nextObject]); )
{
if ([attrKey hasPrefix:@"_oo_"])
{
[shipEntry removeObjectForKey:attrKey];
}
}
}
return YES;
}
/* -loadAndMergeShipyard:
Load shipyard.plist, add its entries to appropriate shipyard entries as
a dictionary under the key "shipyard", and build list of player ships.
Before that, we strip out any "shipyard" entries already in shipdata, and
apply any shipyard-overrides.plist stuff to shipyard.
*/
- (BOOL) loadAndMergeShipyard:(NSMutableDictionary *)ioData
{
NSEnumerator *shipKeyEnum = nil;
NSString *shipKey = nil;
NSMutableDictionary *shipEntry = nil;
NSDictionary *shipyard = nil;
NSDictionary *shipyardOverrides = nil;
NSDictionary *shipyardEntry = nil;
NSDictionary *shipyardOverridesEntry = nil;
NSMutableArray *playerShips = nil;
// Strip out any shipyard stuff in shipdata (there shouldn't be any).
for (shipKeyEnum = [ioData keyEnumerator]; (shipKey = [shipKeyEnum nextObject]); )
{
shipEntry = [ioData objectForKey:shipKey];
if ([shipEntry objectForKey:@"_oo_shipyard"] != nil)
{
[shipEntry removeObjectForKey:@"_oo_shipyard"];
}
}
shipyard = [ResourceManager dictionaryFromFilesNamed:@"shipyard.plist"
inFolder:@"Config"
mergeMode:MERGE_BASIC
cache:NO];
shipyardOverrides = [ResourceManager dictionaryFromFilesNamed:@"shipyard-overrides.plist"
inFolder:@"Config"
mergeMode:MERGE_SMART
cache:NO];
playerShips = [NSMutableArray arrayWithCapacity:[shipyard count]];
// Insert merged shipyard and shipyardOverrides entries.
for (shipKeyEnum = [shipyard keyEnumerator]; (shipKey = [shipKeyEnum nextObject]); )
{
shipEntry = [ioData objectForKey:shipKey];
if (shipEntry != nil)
{
shipyardEntry = [shipyard objectForKey:shipKey];
shipyardOverridesEntry = [shipyardOverrides objectForKey:shipKey];
shipyardEntry = [shipyardEntry dictionaryByAddingEntriesFromDictionary:shipyardOverridesEntry];
[shipEntry setObject:shipyardEntry forKey:@"_oo_shipyard"];
[playerShips addObject:shipKey];
}
else
{
OOLogWARN(@"shipData.load.shipyard.unknown", @"the shipyard.plist entry \"%@\" does not have a corresponding shipdata.plist entry, ignoring.", shipKey);
}
}
_playerShips = [playerShips copy];
[[OOCacheManager sharedCache] setObject:_playerShips forKey:kPlayerShipsCacheKey inCache:kShipRegistryCacheName];
return YES;
}
- (BOOL) canonicalizeAndTagSubentities:(NSMutableDictionary *)ioData
{
NSEnumerator *shipKeyEnum = nil;
NSString *shipKey = nil;
NSMutableDictionary *shipEntry = nil;
NSArray *subentityDeclarations = nil;
NSEnumerator *subentityEnum = nil;
id subentityDecl = nil;
NSDictionary *subentityDict = nil;
NSString *subentityKey = nil;
NSMutableDictionary *subentityShipEntry = nil;
NSMutableSet *badSubentities = nil;
NSString *badSubentitiesList = nil;
NSMutableArray *okSubentities = nil;
BOOL remove, fatal;
// Convert all subentity declarations to dictionaries and add
// _oo_is_subentity=YES to all entries used as subentities.
// Iterate over all ships. (Iterates over a copy of keys since it mutates the dictionary.)
for (shipKeyEnum = [[ioData allKeys] objectEnumerator]; (shipKey = [shipKeyEnum nextObject]); )
{
shipEntry = [ioData objectForKey:shipKey];
remove = NO;
badSubentities = nil;
// Iterate over each subentity declaration of each ship
subentityDeclarations = [shipEntry oo_arrayForKey:@"subentities"];
if (subentityDeclarations != nil)
{
okSubentities = [NSMutableArray arrayWithCapacity:[subentityDeclarations count]];
for (subentityEnum = [subentityDeclarations objectEnumerator]; (subentityDecl = [subentityEnum nextObject]); )
{
subentityDict = [self canonicalizeSubentityDeclaration:subentityDecl forShip:shipKey shipData:ioData fatalError:&fatal];
// If entry is broken, we need to kill this ship.
if (fatal)
{
OOStandardsError(@"Bad subentity definition found");
remove = YES;
}
else if (subentityDict != nil)
{
[okSubentities addObject:subentityDict];
// Tag subentities.
if (![[subentityDict oo_stringForKey:@"type"] isEqualToString:@"flasher"])
{
subentityKey = [subentityDict oo_stringForKey:@"subentity_key"];
subentityShipEntry = [ioData objectForKey:subentityKey];
if (subentityKey == nil || subentityShipEntry == nil)
{
// Oops, reference to non-existent subent.
if (badSubentities == nil) badSubentities = [NSMutableSet set];
[badSubentities addObject:subentityKey];
}
else
{
// Subent exists, add _oo_is_subentity so roles aren't required.
[subentityShipEntry oo_setBool:YES forKey:@"_oo_is_subentity"];
}
}
}
}
// Set updated subentity list.
if ([okSubentities count] != 0)
{
[shipEntry setObject:okSubentities forKey:@"subentities"];
}
else
{
[shipEntry removeObjectForKey:@"subentities"];
}
if (badSubentities != nil)
{
if (![shipEntry oo_boolForKey:@"is_external_dependency"])
{
badSubentitiesList = [[[badSubentities allObjects] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)] componentsJoinedByString:@", "];
OOLogERR(@"shipData.load.error", @"the shipdata.plist entry \"%@\" has unresolved subentit%@ %@.", shipKey, ([badSubentities count] == 1) ? @"y" : @"ies", badSubentitiesList);
OOStandardsError(@"Bad subentity definition found");
}
remove = YES;
}
if (remove)
{
// Removal is deferred to avoid bogus "entry doesn't exist" errors.
[shipEntry oo_setBool:YES forKey:@"_oo_deferred_remove"];
}
}
}
return YES;
}
- (BOOL) removeUnusableEntries:(NSMutableDictionary *)ioData shipMode:(BOOL)shipMode
{
NSEnumerator *shipKeyEnum = nil;
NSString *shipKey = nil;
NSMutableDictionary *shipEntry = nil;
BOOL remove;
NSString *modelName = nil;
// Clean out invalid entries and templates. (Iterates over a copy of keys since it mutates the dictionary.)
for (shipKeyEnum = [[ioData allKeys] objectEnumerator]; (shipKey = [shipKeyEnum nextObject]); )
{
shipEntry = [ioData objectForKey:shipKey];
remove = NO;
if ([shipEntry oo_boolForKey:@"is_template"] || [shipEntry oo_boolForKey:@"_oo_deferred_remove"]) remove = YES;
else if (shipMode && [[shipEntry oo_stringForKey:@"roles"] length] == 0 && ![shipEntry oo_boolForKey:@"_oo_is_subentity"] && ![shipEntry oo_boolForKey:@"_oo_is_effect"])
{
OOLogERR(@"shipData.load.error", @"the shipdata.plist entry \"%@\" specifies no %@.", shipKey, @"roles");
remove = YES;
OOStandardsError(@"Error in shipdata.plist");
}
else
{
modelName = [shipEntry oo_stringForKey:@"model"];
if (shipMode && [modelName length] == 0)
{
OOLogERR(@"shipData.load.error", @"the shipdata.plist entry \"%@\" specifies no %@.", shipKey, @"model");
OOStandardsError(@"Error in shipdata.plist");
remove = YES;
}
else if ([modelName length] != 0 && [ResourceManager pathForFileNamed:modelName inFolder:@"Models"] == nil)
{
OOLogERR(@"shipData.load.error", @"the shipdata.plist entry \"%@\" specifies non-existent model \"%@\".", shipKey, modelName);
OOStandardsError(@"Error in shipdata.plist");
remove = YES;
}
}
if (remove) [ioData removeObjectForKey:shipKey];
}
return YES;
}
/* Transform conditions, determinant (if conditions array) and
shipyard.conditions from hasShipyard to sanitized form.
Also get list of condition_scripts
*/
- (BOOL) sanitizeConditions:(NSMutableDictionary *)ioData
{
NSEnumerator *shipKeyEnum = nil;
NSString *shipKey = nil;
NSMutableDictionary *shipEntry = nil;
NSMutableDictionary *mutableShipyard = nil;
NSArray *conditions = nil;
NSArray *hasShipyard = nil;
NSArray *shipyardConditions = nil;
NSString *condition_script = nil;
NSString *shipyard_condition_script = nil;
NSMutableArray *conditionScripts = [[NSMutableArray alloc] init];
for (shipKeyEnum = [[ioData allKeys] objectEnumerator]; (shipKey = [shipKeyEnum nextObject]); )
{
shipEntry = [ioData objectForKey:shipKey];
conditions = [shipEntry objectForKey:@"conditions"];
condition_script = [shipEntry oo_stringForKey:@"condition_script"];
if (condition_script != nil)
{
if (![conditionScripts containsObject:condition_script])
{
[conditionScripts addObject:condition_script];
}
}
hasShipyard = [shipEntry objectForKey:@"has_shipyard"];
if (![hasShipyard isKindOfClass:[NSArray class]]) hasShipyard = nil; // May also be fuzzy boolean
if (hasShipyard == nil)
{
hasShipyard = [shipEntry objectForKey:@"hasShipyard"];
if (![hasShipyard isKindOfClass:[NSArray class]]) hasShipyard = nil; // May also be fuzzy boolean
}
shipyardConditions = [[shipEntry oo_dictionaryForKey:@"_oo_shipyard"] objectForKey:@"conditions"];
shipyard_condition_script = [[shipEntry oo_dictionaryForKey:@"_oo_shipyard"] oo_stringForKey:@"condition_script"];
if (shipyard_condition_script != nil)
{
if (![conditionScripts containsObject:shipyard_condition_script])
{
[conditionScripts addObject:shipyard_condition_script];
}
}
if (conditions == nil && hasShipyard && shipyardConditions == nil) continue;
if (conditions != nil)
{
OOStandardsDeprecated([NSString stringWithFormat:@"The 'conditions' key is deprecated in shipdata entry %@",shipKey]);
if (!OOEnforceStandards())
{
if ([conditions isKindOfClass:[NSArray class]])
{
conditions = OOSanitizeLegacyScriptConditions(conditions, [NSString stringWithFormat:@"<shipdata.plist entry \"%@\">", shipKey]);
}
else
{
OOLogWARN(@"shipdata.load.warning", @"conditions for shipdata.plist entry \"%@\" are not an array, ignoring.", shipKey);
conditions = nil;
}
if (conditions != nil)
{
[shipEntry setObject:conditions forKey:@"conditions"];
}
else
{
[shipEntry removeObjectForKey:@"conditions"];
}
}
}
if (hasShipyard != nil)
{
hasShipyard = OOSanitizeLegacyScriptConditions(hasShipyard, [NSString stringWithFormat:@"<shipdata.plist entry \"%@\" hasShipyard conditions>", shipKey]);
OOStandardsDeprecated([NSString stringWithFormat:@"Use of legacy script conditions in the 'has_shipyard' key is deprecated in shipyard entry %@",shipKey]);
if (!OOEnforceStandards())
{
if (hasShipyard != nil)
{
[shipEntry setObject:hasShipyard forKey:@"has_shipyard"];
}
else
{
[shipEntry removeObjectForKey:@"hasShipyard"];
[shipEntry removeObjectForKey:@"has_shipyard"];
}
}
}
if (shipyardConditions != nil)
{
OOStandardsDeprecated([NSString stringWithFormat:@"The 'conditions' key is deprecated in shipyard entry %@",shipKey]);
if (!OOEnforceStandards())
{
mutableShipyard = [[[shipEntry oo_dictionaryForKey:@"_oo_shipyard"] mutableCopy] autorelease];
if ([shipyardConditions isKindOfClass:[NSArray class]])
{
shipyardConditions = OOSanitizeLegacyScriptConditions(shipyardConditions, [NSString stringWithFormat:@"<shipyard.plist entry \"%@\">", shipKey]);
}
else
{
OOLogWARN(@"shipdata.load.warning", @"conditions for shipyard.plist entry \"%@\" are not an array, ignoring.", shipKey);
shipyardConditions = nil;
}
if (shipyardConditions != nil)
{
[mutableShipyard setObject:shipyardConditions forKey:@"conditions"];
}
else
{
[mutableShipyard removeObjectForKey:@"conditions"];
}
[shipEntry setObject:mutableShipyard forKey:@"_oo_shipyard"];
}
}
}
[[OOCacheManager sharedCache] setObject:conditionScripts forKey:@"ship conditions" inCache:@"condition scripts"];
[conditionScripts release];
return YES;
}
#if PRELOAD
- (BOOL) preloadShipMeshes:(NSMutableDictionary *)ioData
{
NSEnumerator *shipKeyEnum = nil;
NSString *shipKey = nil;
NSMutableDictionary *shipEntry = nil;
BOOL remove;
NSString *modelName = nil;
OOMesh *mesh = nil;
NSAutoreleasePool *pool = nil;
NSUInteger i = 0, count;
count = [ioData count];
// Preload ship meshes. (Iterates over a copy of keys since it mutates the dictionary.)
for (shipKeyEnum = [[ioData allKeys] objectEnumerator]; (shipKey = [shipKeyEnum nextObject]); )
{
pool = [[NSAutoreleasePool alloc] init];
[[GameController sharedController] setProgressBarValue:(float)i++ / (float)count];
shipEntry = [ioData objectForKey:shipKey];
remove = NO;
modelName = [shipEntry oo_stringForKey:@"model"];
mesh = [OOMesh meshWithName:modelName
materialDictionary:[shipEntry oo_dictionaryForKey:@"materials"]
shadersDictionary:[shipEntry oo_dictionaryForKey:@"shaders"]
smooth:[shipEntry oo_boolForKey:@"smooth"]
shaderMacros:nil
shaderBindingTarget:nil];
[pool release]; // NOTE: mesh is now invalid, but pointer nil check is OK.
if (mesh == nil)
{
// FIXME: what if it's a subentity? Need to rearrange things.
OOLogERR(@"shipData.load.error", @"model \"%@\" could not be loaded for ship \"%@\", removing.", modelName, shipKey);
[ioData removeObjectForKey:shipKey];
}
}
[[GameController sharedController] setProgressBarValue:-1.0f];
return YES;
}
#endif
- (void) mergeShipRoles:(NSString *)roles
forShipKey:(NSString *)shipKey
intoProbabilityMap:(NSMutableDictionary *)probabilitySets
{
NSDictionary *rolesAndWeights = nil;
NSEnumerator *roleEnum = nil;
NSString *role = nil;
OOMutableProbabilitySet *probSet = nil;
/* probabilitySets is a dictionary whose keys are roles and whose values
are mutable probability sets, whose values are ship keys.
When creating new ships Oolite looks up this probability map.
To upgrade all soliton 'thargon' roles to 'EQ_THARGON' we need
to swap these roles here.
*/
rolesAndWeights = OOParseRolesFromString(roles);
// add default [shipKey] role
NSMutableDictionary *mutable = [NSMutableDictionary dictionaryWithDictionary:rolesAndWeights];
[mutable setObject:[NSNumber numberWithFloat:1.0] forKey:[[[NSString alloc] initWithFormat:@"[%@]",shipKey] autorelease]];
rolesAndWeights = mutable;
id thargonValue = [rolesAndWeights objectForKey:@"thargon"];
if (thargonValue != nil && [rolesAndWeights objectForKey:@"EQ_THARGON"] == nil)
{
NSMutableDictionary *mutable = [NSMutableDictionary dictionaryWithDictionary:rolesAndWeights];
[mutable setObject:thargonValue forKey:@"EQ_THARGON"];
rolesAndWeights = mutable;
}
for (roleEnum = [rolesAndWeights keyEnumerator]; (role = [roleEnum nextObject]); )
{
probSet = [probabilitySets objectForKey:role];
if (probSet == nil)
{
probSet = [OOMutableProbabilitySet probabilitySet];
[probabilitySets setObject:probSet forKey:role];
}
[probSet setWeight:[rolesAndWeights oo_floatForKey:role] forObject:shipKey];
}
}
- (NSDictionary *) canonicalizeSubentityDeclaration:(id)declaration
forShip:(NSString *)shipKey
shipData:(NSDictionary *)shipData
fatalError:(BOOL *)outFatalError
{
NSDictionary *result = nil;
assert(outFatalError != NULL);
*outFatalError = NO;
if ([declaration isKindOfClass:[NSString class]])
{
// Update old-style string-based declaration.
OOStandardsDeprecated([NSString stringWithFormat:@"Old style sub-entity declarations are deprecated in %@",shipKey]);
if (!OOEnforceStandards())
{
result = [self translateOldStyleSubentityDeclaration:declaration
forShip:shipKey
shipData:shipData
fatalError:outFatalError];
}
if (result != nil)
{
// Ensure internal translation made sense, and clean up a bit.
result = [self validateNewStyleSubentityDeclaration:result
forShip:shipKey
fatalError:outFatalError];
}
}
else if ([declaration isKindOfClass:[NSDictionary class]])
{
// Validate dictionary-based declaration.
result = [self validateNewStyleSubentityDeclaration:declaration
forShip:shipKey
fatalError:outFatalError];
}
else
{
OOLogERR(@"shipData.load.error.badSubentity", @"subentity declaration for ship %@ should be string or dictionary, found %@.", shipKey, [declaration class]);
*outFatalError = YES;
}
// For frangible ships, bad subentities are non-fatal.
if (*outFatalError && [[shipData oo_dictionaryForKey:shipKey] oo_boolForKey:@"frangible"]) *outFatalError = NO;
return result;
}
- (NSDictionary *) translateOldStyleSubentityDeclaration:(NSString *)declaration
forShip:(NSString *)shipKey
shipData:(NSDictionary *)shipData
fatalError:(BOOL *)outFatalError
{
NSArray *tokens = nil;
NSString *subentityKey = nil;
BOOL isFlasher;
tokens = ScanTokensFromString(declaration);
subentityKey = [tokens objectAtIndex:0];
isFlasher = [subentityKey isEqualToString:@"*FLASHER*"];
// Sanity check: require eight tokens.
if ([tokens count] != 8)
{
if (!isFlasher)
{
OOLogERR(@"shipData.load.error.badSubentity", @"the shipdata.plist entry \"%@\" has a broken subentity definition \"%@\" (should have 8 tokens, has %lu).", shipKey, subentityKey, [tokens count]);
*outFatalError = YES;
}
else
{
OOLogWARN(@"shipData.load.warning.badFlasher", @"the shipdata.plist entry \"%@\" has a broken flasher definition (should have 8 tokens, has %lu). This flasher will be ignored.", shipKey, [tokens count]);
}
return nil;
}
if (isFlasher)
{
return [self translateOldStyleFlasherDeclaration:tokens
forShip:shipKey
fatalError:outFatalError];
}
else
{
return [self translateOldStandardBasicSubentityDeclaration:tokens
forShip:shipKey
shipData:shipData
fatalError:outFatalError];
}
}
- (NSDictionary *) translateOldStyleFlasherDeclaration:(NSArray *)tokens
forShip:(NSString *)shipKey
fatalError:(BOOL *)outFatalError
{
Vector position;
float size, frequency, phase, hue;
NSDictionary *colorDict = nil;
NSDictionary *result = nil;
position.x = [tokens oo_floatAtIndex:1];
position.y = [tokens oo_floatAtIndex:2];
position.z = [tokens oo_floatAtIndex:3];
hue = [tokens oo_floatAtIndex:4];
frequency = [tokens oo_floatAtIndex:5];
phase = [tokens oo_floatAtIndex:6];
size = [tokens oo_floatAtIndex:7];
colorDict = [NSDictionary dictionaryWithObject:[NSNumber numberWithFloat:hue] forKey:@"hue"];
result = [NSDictionary dictionaryWithObjectsAndKeys:
@"flasher", @"type",
OOPropertyListFromVector(position), @"position",
[NSArray arrayWithObject:colorDict], @"colors",
[NSNumber numberWithFloat:frequency], @"frequency",
[NSNumber numberWithFloat:phase], @"phase",
[NSNumber numberWithFloat:size], @"size",
nil];
OOLog(@"shipData.translateSubentity.flasher", @"Translated flasher declaration \"%@\" to %@", [tokens componentsJoinedByString:@" "], result);
return result;
}
- (NSDictionary *) translateOldStandardBasicSubentityDeclaration:(NSArray *)tokens
forShip:(NSString *)shipKey
shipData:(NSDictionary *)shipData
fatalError:(BOOL *)outFatalError
{
NSString *subentityKey = nil;
Vector position;
Quaternion orientation;
NSMutableDictionary *result = nil;
BOOL isTurret, isDock = NO;
subentityKey = [tokens oo_stringAtIndex:0];
isTurret = [self shipIsBallTurretForKey:subentityKey inShipData:shipData];
position.x = [tokens oo_floatAtIndex:1];
position.y = [tokens oo_floatAtIndex:2];
position.z = [tokens oo_floatAtIndex:3];
orientation.w = [tokens oo_floatAtIndex:4];
orientation.x = [tokens oo_floatAtIndex:5];
orientation.y = [tokens oo_floatAtIndex:6];
orientation.z = [tokens oo_floatAtIndex:7];
if(orientation.w == 0 && orientation.x == 0 && orientation.y == 0 && orientation.z == 0)
{
orientation.w = 1; // avoid dividing by zero.
OOLogWARN(@"shipData.load.error", @"The ship %@ has an undefined orientation for its %@ subentity. Setting it now at (1,0,0,0)", shipKey, subentityKey);
}
quaternion_normalize(&orientation);
if (!isTurret)
{
isDock = [subentityKey rangeOfString:@"dock"].location != NSNotFound;
}
result = [NSMutableDictionary dictionaryWithCapacity:5];
[result setObject:isTurret ? @"ball_turret" : @"standard" forKey:@"type"];
[result setObject:subentityKey forKey:@"subentity_key"];
[result oo_setVector:position forKey:@"position"];
[result oo_setQuaternion:orientation forKey:@"orientation"];
if (isDock) [result oo_setBool:YES forKey:@"is_dock"];
OOLog(@"shipData.translateSubentity.standard", @"Translated subentity declaration \"%@\" to %@", [tokens componentsJoinedByString:@" "], result);
return [[result copy] autorelease];
}
- (NSDictionary *) validateNewStyleSubentityDeclaration:(NSDictionary *)declaration
forShip:(NSString *)shipKey
fatalError:(BOOL *)outFatalError
{
NSString *type = nil;
type = [declaration oo_stringForKey:@"type"];
if (type == nil) type = @"standard";
if ([type isEqualToString:@"flasher"])
{
return [self validateNewStyleFlasherDeclaration:declaration forShip:shipKey fatalError:outFatalError];
}
else if ([type isEqualToString:@"standard"] || [type isEqualToString:@"ball_turret"])
{
return [self validateNewStyleStandardSubentityDeclaration:declaration forShip:shipKey fatalError:outFatalError];
}
else
{
OOLogERR(@"shipData.load.error.badSubentity", @"subentity declaration for ship %@ does not declare a valid type (must be standard, flasher or ball_turret).", shipKey);
*outFatalError = YES;
return nil;
}
}
- (NSDictionary *) validateNewStyleFlasherDeclaration:(NSDictionary *)declaration
forShip:(NSString *)shipKey
fatalError:(BOOL *)outFatalError
{
NSMutableDictionary *result = nil;
Vector position = kZeroVector;
NSArray *colors = nil;
id colorDesc = nil;
float size, frequency, phase, brightfraction;
BOOL initiallyOn;
#define kDefaultFlasherColor @"redColor"
// "Validate" is really "clean up", since all values have defaults.
colors = [declaration oo_arrayForKey:@"colors"];
if ([colors count] == 0)
{
colorDesc = [declaration objectForKey:@"color"];
if (colorDesc == nil) colorDesc = kDefaultFlasherColor;
if ([colorDesc isKindOfClass:[NSArray class]])
{
// an easy made error is adding an array to "color" instead of "colors"
OOLogWARN(@"shipData.load.warning.flasher.badColor", @"changing flasher for ship %@ from a color to a colors definition.", shipKey);
colors = colorDesc;
}
else
{
colors = [NSArray arrayWithObject:colorDesc];
}
}
// Validate colours.
NSMutableArray *validColors = [NSMutableArray arrayWithCapacity:[colors count]];
foreach (colorDesc, colors)
{
OOColor *color = [OOColor colorWithDescription:colorDesc];
if (color != nil)
{
[validColors addObject:[color normalizedArray]];
}
else
{
OOLogWARN(@"shipdata.load.warning.flasher.badColor", @"skipping invalid colour specifier for flasher for ship %@.", shipKey);
}
}
// Ensure there's at least one.
if ([validColors count] == 0)
{
[validColors addObject:kDefaultFlasherColor];
}
colors = validColors;
position = [declaration oo_vectorForKey:@"position"];
size = [declaration oo_floatForKey:@"size" defaultValue:8.0];
if (size <= 0)
{
OOLogWARN(@"shipData.load.warning.flasher.badSize", @"skipping flasher of invalid size %g for ship %@.", size, shipKey);
return nil;
}
brightfraction = [declaration oo_floatForKey:@"bright_fraction" defaultValue:0.5];
if (brightfraction < 0.0 || brightfraction > 1.0)
{
OOLogWARN(@"shipData.load.warning.flasher.badFraction", @"skipping flasher of invalid bright fraction %g for ship %@.", brightfraction, shipKey);
return nil;
}
frequency = [declaration oo_floatForKey:@"frequency" defaultValue:2.0];
phase = [declaration oo_floatForKey:@"phase" defaultValue:0.0];
initiallyOn = [declaration oo_boolForKey:@"initially_on" defaultValue:YES];
result = [NSMutableDictionary dictionaryWithCapacity:8];
[result setObject:@"flasher" forKey:@"type"];
[result setObject:colors forKey:@"colors"];
[result oo_setVector:position forKey:@"position"];
[result setObject:[NSNumber numberWithFloat:size] forKey:@"size"];
[result setObject:[NSNumber numberWithFloat:frequency] forKey:@"frequency"];
if (phase != 0) [result setObject:[NSNumber numberWithFloat:phase] forKey:@"phase"];
[result setObject:[NSNumber numberWithFloat:brightfraction] forKey:@"bright_fraction"];
[result setObject:[NSNumber numberWithBool:initiallyOn] forKey:@"initially_on"];
return [[result copy] autorelease];
}
- (NSDictionary *) validateNewStyleStandardSubentityDeclaration:(NSDictionary *)declaration
forShip:(NSString *)shipKey
fatalError:(BOOL *)outFatalError
{
NSMutableDictionary *result = nil;
NSString *subentityKey = nil;
Vector position = kZeroVector;
Quaternion orientation = kIdentityQuaternion;
BOOL isTurret;
BOOL isDock = NO;
float fireRate = -1.0f; // out of range constants
float weaponRange = -1.0f;
float weaponEnergy = -1.0f;
NSDictionary *scriptInfo = nil;
subentityKey = [declaration objectForKey:@"subentity_key"];
if (subentityKey == nil)
{
OOLogERR(@"shipData.load.error.badSubentity", @"subentity declaration for ship %@ specifies no subentity_key.", shipKey);
*outFatalError = YES;
return nil;
}
isTurret = [[declaration oo_stringForKey:@"type"] isEqualToString:@"ball_turret"];
if (isTurret)
{
fireRate = [declaration oo_floatForKey:@"fire_rate" defaultValue:-1.0f];
if (fireRate < 0.25f && fireRate >= 0.0f)
{
OOLogWARN(@"shipData.load.warning.turret.badFireRate", @"ball turret fire rate of %g for subentity of ship %@ is invalid, using 0.25.", fireRate, shipKey);
fireRate = 0.25f;
}
weaponRange = [declaration oo_floatForKey:@"weapon_range" defaultValue:-1.0f];
if (weaponRange > TURRET_SHOT_RANGE * COMBAT_WEAPON_RANGE_FACTOR)
{
OOLogWARN(@"shipData.load.warning.turret.badWeaponRange", @"ball turret weapon range of %g for subentity of ship %@ is too high, using %.1f.", weaponRange, shipKey, TURRET_SHOT_RANGE * COMBAT_WEAPON_RANGE_FACTOR);
weaponRange = TURRET_SHOT_RANGE * COMBAT_WEAPON_RANGE_FACTOR; // approx. range of primary plasma canon.
}
weaponEnergy = [declaration oo_floatForKey:@"weapon_energy" defaultValue:-1.0f];
if (weaponEnergy > 100.0f)
{
OOLogWARN(@"shipData.load.warning.turret.badWeaponEnergy", @"ball turret weapon energy of %g for subentity of ship %@ is too high, using 100.", weaponEnergy, shipKey);
weaponEnergy = 100.0f;
}
}
else
{
isDock = [declaration oo_boolForKey:@"is_dock"];
}
position = [declaration oo_vectorForKey:@"position"];
orientation = [declaration oo_quaternionForKey:@"orientation"];
quaternion_normalize(&orientation);
scriptInfo = [declaration oo_dictionaryForKey:@"script_info"];
result = [NSMutableDictionary dictionaryWithCapacity:10];
[result setObject:isTurret ? @"ball_turret" : @"standard" forKey:@"type"];
[result setObject:subentityKey forKey:@"subentity_key"];
[result oo_setVector:position forKey:@"position"];
[result oo_setQuaternion:orientation forKey:@"orientation"];
if (isDock)
{
[result oo_setBool:YES forKey:@"is_dock"];
NSString* docklabel = [declaration oo_stringForKey:@"dock_label" defaultValue:@"the docking bay"];
[result setObject:docklabel forKey:@"dock_label"];
BOOL dockable = [declaration oo_boolForKey:@"allow_docking" defaultValue:YES];
BOOL playerdockable = [declaration oo_boolForKey:@"disallowed_docking_collides" defaultValue:NO];
BOOL undockable = [declaration oo_boolForKey:@"allow_launching" defaultValue:YES];
[result oo_setBool:dockable forKey:@"allow_docking"];
[result oo_setBool:playerdockable forKey:@"disallowed_docking_collides"];
[result oo_setBool:undockable forKey:@"allow_launching"];
}
if (isTurret)
{
// default constants are defined and set in shipEntity
if (fireRate > 0) [result oo_setFloat:fireRate forKey:@"fire_rate"];
if (weaponRange >= 0) [result oo_setFloat:weaponRange forKey:@"weapon_range"];
if (weaponEnergy >= 0) [result oo_setFloat:weaponEnergy forKey:@"weapon_energy"];
}
if (scriptInfo != nil)
{
[result setObject:scriptInfo forKey:@"script_info"];
}
return [[result copy] autorelease];
}
- (BOOL) shipIsBallTurretForKey:(NSString *)shipKey inShipData:(NSDictionary *)shipData
{
// Test for presence of setup_actions containing initialiseTurret.
NSArray *setupActions = nil;
NSEnumerator *actionEnum = nil;
NSString *action = nil;
setupActions = [[shipData oo_dictionaryForKey:shipKey] oo_arrayForKey:@"setup_actions"];
for (actionEnum = [setupActions objectEnumerator]; (action = [actionEnum nextObject]); )
{
if ([[ScanTokensFromString(action) objectAtIndex:0] isEqualToString:@"initialiseTurret"]) return YES;
}
if ([shipKey isEqualToString:@"ballturret"])
{
// compatibility for OXPs using old subentity declarations and the
// core turret entity
return YES;
}
return NO;
}
@end
@implementation OOShipRegistry (Singleton)
/* Canonical singleton boilerplate.
See Cocoa Fundamentals Guide: Creating a Singleton Instance.
See also +sharedRegistry above.
NOTE: assumes single-threaded access.
*/
+ (id) allocWithZone:(NSZone *)inZone
{
if (sSingleton == nil)
{
OOLog(@"shipData.load.begin", @"%@", @"Loading ship data.");
sSingleton = [super allocWithZone:inZone];
return sSingleton;
}
return nil;
}
- (id) copyWithZone:(NSZone *)inZone
{
return self;
}
- (id) retain
{
return self;
}
- (NSUInteger) retainCount
{
return UINT_MAX;
}
- (void) release
{}
- (id) autorelease
{
return self;
}
@end
static void GatherStringAddrsDict(NSDictionary *dict, NSMutableSet *strings, NSString *context);
static void GatherStringAddrsArray(NSArray *array, NSMutableSet *strings, NSString *context);
static void GatherStringAddrs(id object, NSMutableSet *strings, NSString *context);
static void DumpStringAddrs(NSDictionary *dict, NSString *context)
{
return;
static FILE *dump = NULL;
if (dump == NULL) dump = fopen("strings.txt", "w");
if (dump == NULL) return;
NSAutoreleasePool *pool = [NSAutoreleasePool new];
NSMutableSet *strings = [NSMutableSet set];
GatherStringAddrs(dict, strings, context);
NSEnumerator *entryEnum = nil;
NSDictionary *entry = nil;
for (entryEnum = [strings objectEnumerator]; (entry = [entryEnum nextObject]); )
{
NSString *string = [entry objectForKey:@"string"];
NSString *context = [entry objectForKey:@"context"];
void *pointer = [[entry objectForKey:@"address"] pointerValue];
string = [NSString stringWithFormat:@"%p\t%@: \"%@\"", pointer, context, string];
fprintf(dump, "%s\n", [string UTF8String]);
}
fprintf(dump, "\n");
fflush(dump);
[pool release];
}
static void GatherStringAddrsDict(NSDictionary *dict, NSMutableSet *strings, NSString *context)
{
NSEnumerator *keyEnum = nil;
id key = nil;
NSString *keyContext = [context stringByAppendingString:@" key"];
for (keyEnum = [dict keyEnumerator]; (key = [keyEnum nextObject]); )
{
GatherStringAddrs(key, strings, keyContext);
GatherStringAddrs([dict objectForKey:key], strings, [context stringByAppendingFormat:@".%@", key]);
}
}
static void GatherStringAddrsArray(NSArray *array, NSMutableSet *strings, NSString *context)
{
NSEnumerator *vEnum = nil;
NSString *v = nil;
unsigned i = 0;
for (vEnum = [array objectEnumerator]; (v = [vEnum nextObject]); )
{
GatherStringAddrs(v, strings, [context stringByAppendingFormat:@"[%u]", i++]);
}
}
static void GatherStringAddrs(id object, NSMutableSet *strings, NSString *context)
{
if ([object isKindOfClass:[NSString class]])
{
NSDictionary *entry = [NSDictionary dictionaryWithObjectsAndKeys:object, @"string", [NSValue valueWithPointer:object], @"address", context, @"context", nil];
[strings addObject:entry];
}
else if ([object isKindOfClass:[NSArray class]])
{
GatherStringAddrsArray(object, strings, context);
}
else if ([object isKindOfClass:[NSDictionary class]])
{
GatherStringAddrsDict(object, strings, context);
}
}
static NSComparisonResult SortDemoShipsByName (id a, id b, void* context)
{
return [[a oo_stringForKey:@"name"] compare:[b oo_stringForKey:@"name"]];
}
static NSComparisonResult SortDemoCategoriesByName (id a, id b, void* context)
{
return [OOShipLibraryCategoryPlural([[a oo_dictionaryAtIndex:0] oo_stringForKey:@"class"]) compare:OOShipLibraryCategoryPlural([[b oo_dictionaryAtIndex:0] oo_stringForKey:@"class"])];
}