55@interface AI (OOPrivate)
58- (void) performDeferredCall:(
SEL)selector withObject:(
id)object afterDelay:(NSTimeInterval)delay;
59+ (void) deferredCallTrampolineWithInfo:(NSValue *)info;
64- (void) directSetStateMachine:(NSDictionary *)newSM name:(NSString *)name;
65- (void) directSetState:(NSString *)state;
68- (NSDictionary *) loadStateMachine:(NSString *)smName jsName:(NSString *)script;
69- (NSDictionary *) cleanHandlers:(NSDictionary *)handlers forState:(NSString *)stateKey stateMachine:(NSString *)smName;
70- (NSArray *) cleanActions:(NSArray *)actions forHandler:(NSString *)handlerKey state:(NSString *)stateKey stateMachine:(NSString *)smName;
76extern void GenerateGraphVizForAIStateMachine(NSDictionary *stateMachine, NSString *name);
90- (id) initWithStateMachine:(NSDictionary *)stateMachine
92 state:(NSString *)state
93 pendingMessages:(NSSet *)pendingMessages
94 jsScript:(NSString *)script;
96- (NSDictionary *) stateMachine;
99- (NSSet *) pendingMessages;
100- (NSString *) jsScript;
107+ (
AI *) currentlyRunningAI
113+ (NSString *) currentlyRunningAIDescription
117 return [NSString stringWithFormat:@"%@ in state %@", [sCurrentlyRunningAI
name], [sCurrentlyRunningAI
state]];
121 return @"<no AI running>";
128 if ((
self = [super init]))
130 nextThinkTime = INFINITY;
133 stateMachineName =
@"<no AI>";
140- (id) initWithStateMachine:(NSString *)smName andState:(NSString *)stateName
142 if ((
self = [
self init]))
144 if (smName !=
nil) [
self setStateMachine:smName withJSScript:@"oolite-nullAI.js"];
145 if (stateName !=
nil) currentState = [stateName retain];
172- (NSString *) descriptionComponents
174 return [NSString stringWithFormat:@"\"%@\" in state: \"%@\" for %@", stateMachineName, currentState, ownerDesc];
178- (NSString *) shortDescriptionComponents
180 return [NSString stringWithFormat:@"%@:%@ / %@", stateMachineName, currentState, [stateMachine objectForKey:@"jsScript"]];
186 ShipEntity *owner = [_owner weakRefUnderlyingObject];
201 [
self refreshOwnerDesc];
205- (void) reportStackOverflow
211 NSString *trailer = stackDump ?
@" -- stack:" :
@".";
212 OOLogERR(
@"ai.error.stackOverflow",
@"AI stack overflow for %@ in %@: %@%@\n", [_owner shortDescription], stateMachineName, currentState, trailer);
218 NSUInteger
count = [aiStack count];
222 OOLog(
@"ai.error.stackOverflow.dump",
@"%3lu: %@: %@",
count, [preservedMachine name], [preservedMachine state]);
231- (void) preserveCurrentStateMachine
233 if (stateMachine ==
nil)
return;
237 aiStack = [[NSMutableArray alloc] init];
242 [
self reportStackOverflow];
244 [NSException raise:@"OoliteException"
245 format:@"AI stack overflow for %@", _owner];
249 initWithStateMachine:stateMachine
250 name:stateMachineName
252 pendingMessages:pendingMessages
253 jsScript:[stateMachine objectForKey:@"jsScript"]];
256 if ([[
self owner] reportAIMessages])
OOLog(
@"ai.stack.push",
@"Pushing state machine for %@",
self);
259 [aiStack addObject:preservedMachine];
261 [preservedMachine release];
265- (void) restorePreviousStateMachine
267 if ([aiStack
count] == 0)
return;
272 if ([[
self owner] reportAIMessages])
OOLog(
@"ai.stack.pop",
@"Popping previous state machine for %@",
self);
275 [
self directSetStateMachine:[preservedMachine
stateMachine]
276 name:[preservedMachine
name]];
278 [
self directSetState:[preservedMachine
state]];
281 [[
self owner] setAIScript:[preservedMachine
jsScript]];
283 [pendingMessages release];
286 [aiStack removeLastObject];
290- (BOOL) hasSuspendedStateMachines
292 return [aiStack count] != 0;
296- (void) exitStateMachineWithMessage:(NSString *)message
298 if ([aiStack
count] != 0)
300 [
self restorePreviousStateMachine];
301 if (message ==
nil) message =
@"RESTARTED";
302 [
self reactToMessage:message context:@"suspended AI restart"];
307- (void) setStateMachine:(NSString *)smName withJSScript:(NSString *)script
309 NSDictionary *newSM = [
self loadStateMachine:smName jsName:script];
313 [
self preserveCurrentStateMachine];
314 [
self directSetStateMachine:newSM name:smName];
315 [
self directSetState:@"GLOBAL"];
326 [
self reactToMessage:@"ENTER" context:@"changing AI"];
329 [
self refreshOwnerDesc];
334- (void) setState:(NSString *) stateName
336 if ([stateMachine objectForKey:stateName])
345 [
self reactToMessage:@"EXIT" context:@"changing state"];
346 [
self directSetState:stateName];
347 [
self reactToMessage:@"ENTER" context:@"changing state"];
352- (void) setStateMachine:(NSString *)smName afterDelay:(NSTimeInterval)delay
354 [
self performDeferredCall:@selector(setStateMachine:) withObject:smName afterDelay:delay];
358- (void) setState:(NSString *)stateName afterDelay:(NSTimeInterval)delay
360 [
self performDeferredCall:@selector(setState:) withObject:stateName afterDelay:delay];
366 return [[stateMachineName retain] autorelease];
370- (NSString *) associatedJS
372 return [stateMachine objectForKey:@"jsScript"];
378 return [[currentState retain] autorelease];
382- (NSUInteger) stackDepth
384 return [aiStack count];
404- (void) reactToMessage:(NSString *) message context:(NSString *)debugContext
407 NSArray *actions =
nil;
408 NSDictionary *messagesForState =
nil;
410 static unsigned recursionLimiter = 0;
418 if (message ==
nil || owner ==
nil || [owner universalID] ==
NO_TARGET)
return;
422 if (debugContext ==
nil) debugContext =
@"unspecified";
430 .context = debugContext
443 OOLogERR(
@"ai.error.recursion",
@"AI dispatch: hit stack depth limit in AI %@, state %@ handling message %@ in context \"%@\
", aborting.", stateMachineName, currentState, message, debugContext);
448 while (stack != NULL)
461 messagesForState = [
stateMachine objectForKey:currentState];
462 if (messagesForState ==
nil)
return;
465 if (currentState !=
nil && ![message isEqual:
@"UPDATE"] && [owner reportAIMessages])
467 OOLog(
@"ai.message.receive",
@"AI %@ for %@ in state '%@' receives message '%@'. Context: %@, stack depth: %u", stateMachineName, ownerDesc, currentState, message, debugContext, recursionLimiter);
471 actions = [[[messagesForState objectForKey:message] copy] autorelease];
474 if ([actions
count] > 0)
479 for (i = 0; i < [actions count]; i++)
484 @catch (NSException *exception)
486 OOLog(
kOOLogException,
@"Squashing exception %@:%@ in AI handler %@:%@.%@", [exception name], [exception reason], stateMachineName, currentState, message);
493 if (currentState !=
nil)
495 if ([owner respondsToSelector:
@selector(interpretAIMessage:)])
497 [owner performSelector:@selector(interpretAIMessage:) withObject:message];
510- (void) takeAction:(NSString *)action
518 OOLog(
@"ai.takeAction",
@"%@ to take action %@", ownerDesc, action);
524 NSUInteger tokenCount = [tokens count];
528 NSString *selectorStr = [tokens objectAtIndex:0];
532 NSString *dataString =
nil;
536 dataString = [tokens objectAtIndex:1];
538 else if ([tokens
count] > 1)
540 dataString = [[tokens subarrayWithRange:NSMakeRange(1, tokenCount - 1)] componentsJoinedByString:@" "];
543 SEL selector = NSSelectorFromString(selectorStr);
544 if ([owner respondsToSelector:selector])
546 if (dataString !=
nil) [owner performSelector:selector withObject:dataString];
547 else [owner performSelector:selector];
551 OOLogERR(
@"ai.takeAction.badSelector",
@"in AI %@ in state %@: %@ does not respond to %@", stateMachineName, currentState, ownerDesc, selectorStr);
556 OOLog(
@"ai.takeAction.orphaned",
@"***** AI %@, trying to perform %@, is orphaned (no owner)", stateMachineName, selectorStr);
562 if (report)
OOLog(
@"ai.takeAction.noAction",
@"DEBUG: - no action '%@'", action);
577 NSArray *ms_list =
nil;
580 if ([[
self owner] universalID] ==
NO_TARGET || stateMachine ==
nil)
return;
582 [
self reactToMessage:@"UPDATE" context:@"periodic update"];
584 if ([pendingMessages
count] > 0)
586 ms_list = [pendingMessages allObjects];
587 [pendingMessages removeAllObjects];
592 for (i = 0; i < [ms_list count]; i++)
594 [
self reactToMessage:[ms_list objectAtIndex:i] context:@"handling deferred message"];
600- (void) message:(NSString *)ms
602 if ([[
self owner] universalID] ==
NO_TARGET)
return;
607 OOLogERR(
@"ai.message.failed.overflow",
@"AI message \"%@\
" received by '%@' AI while pending messages stack full; message discarded. Pending messages:\n%@", ms, ownerDesc, pendingMessages);
611 if (pendingMessages ==
nil)
613 pendingMessages = [[NSMutableSet alloc] init];
615 [pendingMessages addObject:ms];
620- (void) dropMessage:(NSString *)ms
622 [pendingMessages removeObject:ms];
626- (NSSet *) pendingMessages
628 if (pendingMessages !=
nil)
630 return [[pendingMessages copy] autorelease];
639- (void) debugDumpPendingMessages
641 NSArray *sortedMessages =
nil;
642 NSString *displayMessages =
nil;
644 if ([pendingMessages
count] > 0)
646 sortedMessages = [[pendingMessages allObjects] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
647 displayMessages = [sortedMessages componentsJoinedByString:@", "];
651 displayMessages =
@"none";
654 OOLog(
@"ai.debug.pendingMessages",
@"Pending messages for AI %@: %@", [
self descriptionComponents], displayMessages);
669 return nextThinkTime;
675 thinkTimeInterval = tti;
681 return thinkTimeInterval;
687 [aiStack removeAllObjects];
693 [aiStack removeAllObjects];
694 [pendingMessages removeAllObjects];
696 nextThinkTime += 36000.0;
702 OOLog(
@"dumpState.ai",
@"State machine name: %@", stateMachineName);
703 OOLog(
@"dumpState.ai",
@"Current state: %@", currentState);
704 OOLog(
@"dumpState.ai",
@"Next think time: %g", nextThinkTime);
705 OOLog(
@"dumpState.ai",
@"Next think interval: %g", thinkTimeInterval);
717@implementation AI (OOPrivate)
719- (void)performDeferredCall:(
SEL)selector withObject:(
id)object afterDelay:(NSTimeInterval)delay
724 if (selector != NULL)
726 infoStruct.
ai = [
self retain];
730 info = [[NSValue alloc] initWithBytes:&infoStruct objCType:@encode(OOAIDeferredCallTrampolineInfo)];
732 [[
AI class] performSelector:@selector(deferredCallTrampolineWithInfo:)
740+ (void)deferredCallTrampolineWithInfo:(NSValue *)info
747 [info getValue:&infoStruct];
749 [infoStruct.ai performSelector:infoStruct.selector withObject:infoStruct.parameter];
751 [infoStruct.ai release];
752 [infoStruct.parameter release];
761 if ([owner isPlayer])
763 ownerDesc =
@"player autopilot";
765 else if (owner !=
nil)
767 ownerDesc = [[NSString alloc] initWithFormat:@"%@ %d", [owner
name], [owner
universalID]];
771 ownerDesc =
@"no owner";
776- (void) directSetStateMachine:(NSDictionary *)newSM name:(NSString *)name
778 if (stateMachine != newSM)
780 [stateMachine release];
781 stateMachine = [newSM copy];
783 if (stateMachineName != name)
785 [stateMachineName release];
786 stateMachineName = [name copy];
791- (void) directSetState:(NSString *)state
793 if (currentState != state)
795 [currentState release];
796 currentState = [state copy];
801- (NSDictionary *) loadStateMachine:(NSString *)smName jsName:(NSString *)script
803 NSDictionary *newSM =
nil;
804 NSMutableDictionary *cleanSM =
nil;
806 NSEnumerator *stateEnum =
nil;
807 NSString *stateKey =
nil;
808 NSDictionary *stateHandlers =
nil;
809 NSAutoreleasePool *pool =
nil;
811 if (![smName isEqualToString:
@"nullAI.plist"])
815 if (newSM !=
nil && ![newSM isKindOfClass:[NSDictionary
class]])
return nil;
820 pool = [[NSAutoreleasePool alloc] init];
821 OOLog(
@"ai.load",
@"Loading and sanitizing AI \"%@\
"", smName);
836 NSString *fromString =
@"";
837 if ([
self state] !=
nil)
839 fromString = [NSString stringWithFormat:@" from %@:%@", [
self name], [
self state]];
841 OOLog(
@"ai.load.failed.unknownAI",
@"Can't switch AI for %@%@ to \"%@\
" - could not load file.", [[
self owner] shortDescription], fromString, smName);
845 cleanSM = [NSMutableDictionary dictionaryWithCapacity:[newSM count]];
847 for (stateEnum = [newSM keyEnumerator]; (stateKey = [stateEnum nextObject]); )
849 stateHandlers = [newSM objectForKey:stateKey];
850 if (![stateHandlers isKindOfClass:[NSDictionary
class]])
852 OOLogWARN(
@"ai.invalidFormat.state",
@"State \"%@\
" in AI \"%@\" is not a dictionary, ignoring.", stateKey, smName);
856 stateHandlers = [
self cleanHandlers:stateHandlers forState:stateKey stateMachine:smName];
857 [cleanSM setObject:stateHandlers forKey:stateKey];
859 [cleanSM setObject:script forKey:@"jsScript"];
862 newSM = [[cleanSM copy] autorelease];
865 if ([[NSUserDefaults standardUserDefaults] boolForKey:
@"generate-ai-graphviz"])
867 GenerateGraphVizForAIStateMachine(newSM, smName);
888- (NSDictionary *) cleanHandlers:(NSDictionary *)handlers forState:(NSString *)stateKey stateMachine:(NSString *)smName
890 NSEnumerator *handlerEnum =
nil;
891 NSString *handlerKey =
nil;
892 NSArray *handlerActions =
nil;
893 NSMutableDictionary *result =
nil;
895 result = [NSMutableDictionary dictionaryWithCapacity:[handlers count]];
896 for (handlerEnum = [handlers keyEnumerator]; (handlerKey = [handlerEnum nextObject]); )
898 handlerActions = [handlers objectForKey:handlerKey];
899 if (![handlerActions isKindOfClass:[NSArray
class]])
901 OOLogWARN(
@"ai.invalidFormat.handler",
@"Handler \"%@\
" for state \"%@\" in AI \"%@\" is not an array, ignoring.", handlerKey, stateKey, smName);
905 handlerActions = [
self cleanActions:handlerActions forHandler:handlerKey state:stateKey stateMachine:smName];
906 [result setObject:handlerActions forKey:handlerKey];
910 return [[result copy] autorelease];
914- (NSArray *) cleanActions:(NSArray *)actions forHandler:(NSString *)handlerKey state:(NSString *)stateKey stateMachine:(NSString *)smName
916 NSEnumerator *actionEnum =
nil;
917 NSString *action =
nil;
919 NSString *selector =
nil;
920 id aliasedSelector =
nil;
921 NSMutableArray *result =
nil;
922 static NSSet *whitelist =
nil;
923 static NSDictionary *aliases =
nil;
924 NSArray *whitelistArray1 =
nil;
925 NSArray *whitelistArray2 =
nil;
927 if (whitelist ==
nil)
930 if (whitelistArray1 ==
nil) whitelistArray1 = [NSArray array];
932 if (whitelistArray2 !=
nil) whitelistArray1 = [whitelistArray1 arrayByAddingObjectsFromArray:whitelistArray2];
934 whitelist = [[NSSet alloc] initWithArray:whitelistArray1];
938 result = [NSMutableArray arrayWithCapacity:[actions count]];
939 for (actionEnum = [actions objectEnumerator]; (action = [actionEnum nextObject]); )
941 if (![action isKindOfClass:[NSString
class]])
943 OOLogWARN(
@"ai.invalidFormat.action",
@"An action in handler \"%@\
" for state \"%@\" in AI \"%@\" is not a string, ignoring.", handlerKey, stateKey, smName);
948 action = [action stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
951 spaceRange = [action rangeOfString:@" "];
952 if (spaceRange.location == NSNotFound) selector = action;
953 else selector = [action substringToIndex:spaceRange.location];
956 aliasedSelector = [aliases objectForKey:selector];
957 if (aliasedSelector !=
nil)
959 if ([aliasedSelector isKindOfClass:[NSString
class]])
962 selector = aliasedSelector;
963 if (spaceRange.location == NSNotFound) action = aliasedSelector;
964 else action = [aliasedSelector stringByAppendingString:[action substringFromIndex:spaceRange.location]];
966 else if ([aliasedSelector isKindOfClass:[NSArray
class]] && [aliasedSelector
count] != 0)
969 action = [aliasedSelector componentsJoinedByString:@" "];
970 selector = [[aliasedSelector objectAtIndex:0] description];
975 if (![whitelist containsObject:selector])
977 OOLog(
@"ai.unpermittedMethod",
@"Handler \"%@\
" for state \"%@\" in AI \"%@\" uses \"%@\", which is not a permitted AI method.", handlerKey, stateKey, smName, selector);
981 [result addObject:action];
985 return [[result copy] autorelease];
993- (id) initWithStateMachine:(NSDictionary *)stateMachine
994 name:(NSString *)name
995 state:(NSString *)state
996 pendingMessages:(NSSet *)pendingMessages
997 jsScript:(NSString *)script
999 if ((
self = [super init]))
1001 _stateMachine = [stateMachine copy];
1002 _name = [name copy];
1003 _state = [state copy];
1004 _pendingMessages = [pendingMessages copy];
1005 _jsScript = [script copy];
1014 [_stateMachine autorelease];
1015 [_name autorelease];
1016 [_state autorelease];
1017 [_pendingMessages autorelease];
1018 [_jsScript autorelease];
1024- (NSDictionary *) stateMachine
1026 return _stateMachine;
1042- (NSSet *) pendingMessages
1044 return _pendingMessages;
1047- (NSString *) jsScript
#define AI_THINK_INTERVAL
static AI * sCurrentlyRunningAI
static AIStackElement * sStack
void OOLogPushIndent(void)
#define OOLogWARN(class, format,...)
#define OOLogERR(class, format,...)
NSString *const kOOLogException
void OOLogPopIndent(void)
BOOL OOLogWillDisplayMessagesInClass(NSString *inMessageClass)
#define OOLog(class, format,...)
#define OOLogIndentIf(class)
NSDictionary * OODictionaryFromFile(NSString *path)
NSMutableArray * ScanTokensFromString(NSString *values)
NSDictionary * stateMachine
void takeAction:(NSString *action)
NSString * stateMachineName
OOUniversalID universalID
void setObject:forKey:inCache:(id inElement,[forKey] NSString *inKey,[inCache] NSString *inCacheKey)
id objectForKey:inCache:(NSString *inKey,[inCache] NSString *inCacheKey)
OOCacheManager * sharedCache()
NSSet * pendingMessages()
NSDictionary * stateMachine()
NSMutableSet * _pendingMessages
NSDictionary * _stateMachine
NSString * pathForFileNamed:inFolder:(NSString *fileName,[inFolder] NSString *folderName)
NSDictionary * whitelistDictionary()
unsigned reportAIMessages