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",
@"%3llu: %@: %@",
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;
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 NSString *stateKey =
nil;
807 NSDictionary *stateHandlers =
nil;
808 NSAutoreleasePool *pool =
nil;
810 if (![smName isEqualToString:
@"nullAI.plist"])
814 if (newSM !=
nil && ![newSM isKindOfClass:[NSDictionary class]]) return
nil;
819 pool = [[NSAutoreleasePool alloc] init];
820 OOLog(
@"ai.load",
@"Loading and sanitizing AI \"%@\
"", smName);
835 NSString *fromString =
@"";
836 if ([
self state] !=
nil)
838 fromString = [NSString stringWithFormat:@" from %@:%@", [
self name], [
self state]];
840 OOLog(
@"ai.load.failed.unknownAI",
@"Can't switch AI for %@%@ to \"%@\
" - could not load file.", [[
self owner] shortDescription], fromString, smName);
844 cleanSM = [NSMutableDictionary dictionaryWithCapacity:[newSM count]];
848 stateHandlers = [newSM objectForKey:stateKey];
849 if (![stateHandlers isKindOfClass:[NSDictionary class]])
851 OOLogWARN(
@"ai.invalidFormat.state",
@"State \"%@\
" in AI \"%@\" is not a dictionary, ignoring.", stateKey, smName);
855 stateHandlers = [
self cleanHandlers:stateHandlers forState:stateKey stateMachine:smName];
856 [cleanSM setObject:stateHandlers forKey:stateKey];
858 [cleanSM setObject:script forKey:@"jsScript"];
861 newSM = [[cleanSM copy] autorelease];
864 if ([[NSUserDefaults standardUserDefaults] boolForKey:
@"generate-ai-graphviz"])
866 GenerateGraphVizForAIStateMachine(newSM, smName);
887- (NSDictionary *) cleanHandlers:(NSDictionary *)handlers forState:(NSString *)stateKey stateMachine:(NSString *)smName
889 NSString *handlerKey =
nil;
890 NSArray *handlerActions =
nil;
891 NSMutableDictionary *result =
nil;
893 result = [NSMutableDictionary dictionaryWithCapacity:[handlers count]];
896 handlerActions = [handlers objectForKey:handlerKey];
897 if (![handlerActions isKindOfClass:[NSArray class]])
899 OOLogWARN(
@"ai.invalidFormat.handler",
@"Handler \"%@\
" for state \"%@\" in AI \"%@\" is not an array, ignoring.", handlerKey, stateKey, smName);
903 handlerActions = [
self cleanActions:handlerActions forHandler:handlerKey state:stateKey stateMachine:smName];
904 [result setObject:handlerActions forKey:handlerKey];
908 return [[result copy] autorelease];
912- (NSArray *) cleanActions:(NSArray *)actions forHandler:(NSString *)handlerKey state:(NSString *)stateKey stateMachine:(NSString *)smName
914 NSString *action =
nil;
916 NSString *selector =
nil;
917 id aliasedSelector =
nil;
918 NSMutableArray *result =
nil;
919 static NSSet *whitelist =
nil;
920 static NSDictionary *aliases =
nil;
921 NSArray *whitelistArray1 =
nil;
922 NSArray *whitelistArray2 =
nil;
924 if (whitelist ==
nil)
927 if (whitelistArray1 ==
nil) whitelistArray1 = [NSArray array];
929 if (whitelistArray2 !=
nil) whitelistArray1 = [whitelistArray1 arrayByAddingObjectsFromArray:whitelistArray2];
931 whitelist = [[NSSet alloc] initWithArray:whitelistArray1];
935 result = [NSMutableArray arrayWithCapacity:[actions count]];
936 foreach (action, actions)
938 if (![action isKindOfClass:[NSString class]])
940 OOLogWARN(
@"ai.invalidFormat.action",
@"An action in handler \"%@\
" for state \"%@\" in AI \"%@\" is not a string, ignoring.", handlerKey, stateKey, smName);
945 action = [action stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
948 spaceRange = [action rangeOfString:@" "];
949 if (spaceRange.location == NSNotFound) selector = action;
950 else selector = [action substringToIndex:spaceRange.location];
953 aliasedSelector = [aliases objectForKey:selector];
954 if (aliasedSelector !=
nil)
956 if ([aliasedSelector isKindOfClass:[NSString class]])
959 selector = aliasedSelector;
960 if (spaceRange.location == NSNotFound) action = aliasedSelector;
961 else action = [aliasedSelector stringByAppendingString:[action substringFromIndex:spaceRange.location]];
963 else if ([aliasedSelector isKindOfClass:[NSArray class]] && [aliasedSelector
count] != 0)
966 action = [aliasedSelector componentsJoinedByString:@" "];
967 selector = [[aliasedSelector objectAtIndex:0] description];
972 if (![whitelist containsObject:selector])
974 OOLog(
@"ai.unpermittedMethod",
@"Handler \"%@\
" for state \"%@\" in AI \"%@\" uses \"%@\", which is not a permitted AI method.", handlerKey, stateKey, smName, selector);
978 [result addObject:action];
982 return [[result copy] autorelease];
990- (id) initWithStateMachine:(NSDictionary *)stateMachine
991 name:(NSString *)name
992 state:(NSString *)state
993 pendingMessages:(NSSet *)pendingMessages
994 jsScript:(NSString *)script
996 if ((
self = [super init]))
998 _stateMachine = [stateMachine copy];
1000 _state = [state copy];
1001 _pendingMessages = [pendingMessages copy];
1002 _jsScript = [script copy];
1011 [_stateMachine autorelease];
1012 [_name autorelease];
1013 [_state autorelease];
1014 [_pendingMessages autorelease];
1015 [_jsScript autorelease];
1021- (NSDictionary *) stateMachine
1023 return _stateMachine;
1039- (NSSet *) pendingMessages
1041 return _pendingMessages;
1044- (NSString *) jsScript
#define AI_THINK_INTERVAL
static AI * sCurrentlyRunningAI
static AIStackElement * sStack
#define foreachkey(VAR, DICT)
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