Oolite 1.91.0.7645-241119-222d325
Loading...
Searching...
No Matches
AI.m
Go to the documentation of this file.
1/*
2
3AI.m
4
5Oolite
6Copyright (C) 2004-2013 Giles C Williams and contributors
7
8This program is free software; you can redistribute it and/or
9modify it under the terms of the GNU General Public License
10as published by the Free Software Foundation; either version 2
11of the License, or (at your option) any later version.
12
13This program is distributed in the hope that it will be useful,
14but WITHOUT ANY WARRANTY; without even the implied warranty of
15MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16GNU General Public License for more details.
17
18You should have received a copy of the GNU General Public License
19along with this program; if not, write to the Free Software
20Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
21MA 02110-1301, USA.
22
23*/
24
25#import "AI.h"
26#import "ResourceManager.h"
27#import "OOStringParsing.h"
28#import "OOWeakReference.h"
29#import "OOCacheManager.h"
31#import "OOPListParsing.h"
32
33#import "ShipEntity.h"
34#import "ShipEntityAI.h"
35
36
37enum
38{
39 kRecursionLimiter = 32, // reactToMethod: recursion
40 kStackLimiter = 32 // setAITo: stack overflow
41};
42
43
50
51
53
54
55@interface AI (OOPrivate)
56
57// Wrapper for performSelector:withObject:afterDelay: to catch/fix bugs.
58- (void) performDeferredCall:(SEL)selector withObject:(id)object afterDelay:(NSTimeInterval)delay;
59+ (void) deferredCallTrampolineWithInfo:(NSValue *)info;
60
61- (void) refreshOwnerDesc;
62
63// Set state machine and state without side effects.
64- (void) directSetStateMachine:(NSDictionary *)newSM name:(NSString *)name;
65- (void) directSetState:(NSString *)state;
66
67// Loading/whitelisting
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;
71
72@end
73
74
75#if DEBUG_GRAPHVIZ
76extern void GenerateGraphVizForAIStateMachine(NSDictionary *stateMachine, NSString *name);
77#endif
78
79
80@interface OOPreservedAIStateMachine: NSObject
81{
82@private
83 NSDictionary *_stateMachine;
84 NSString *_name;
85 NSString *_state;
86 NSMutableSet *_pendingMessages;
87 NSString *_jsScript;
88}
89
90- (id) initWithStateMachine:(NSDictionary *)stateMachine
91 name:(NSString *)name
92 state:(NSString *)state
93 pendingMessages:(NSSet *)pendingMessages
94 jsScript:(NSString *)script;
95
96- (NSDictionary *) stateMachine;
97- (NSString *) name;
98- (NSString *) state;
99- (NSSet *) pendingMessages;
100- (NSString *) jsScript;
101
102@end
103
104
105@implementation AI
106
107+ (AI *) currentlyRunningAI
108{
109 return sCurrentlyRunningAI;
110}
111
112
113+ (NSString *) currentlyRunningAIDescription
114{
116 {
117 return [NSString stringWithFormat:@"%@ in state %@", [sCurrentlyRunningAI name], [sCurrentlyRunningAI state]];
118 }
119 else
120 {
121 return @"<no AI running>";
122 }
123}
124
125
126- (id) init
127{
128 if ((self = [super init]))
129 {
130 nextThinkTime = INFINITY; // don't think for a while
131 thinkTimeInterval = AI_THINK_INTERVAL;
132
133 stateMachineName = @"<no AI>"; // no initial brain
134 }
135
136 return self;
137}
138
139
140- (id) initWithStateMachine:(NSString *)smName andState:(NSString *)stateName
141{
142 if ((self = [self init]))
143 {
144 if (smName != nil) [self setStateMachine:smName withJSScript:@"oolite-nullAI.js"];
145 if (stateName != nil) currentState = [stateName retain];
146 }
147
148 return self;
149}
150
151
152- (void) dealloc
153{
154 if (sCurrentlyRunningAI == self)
155 {
157 }
158
159 DESTROY(_owner);
160 DESTROY(ownerDesc);
161 DESTROY(aiStack);
162 DESTROY(stateMachine);
163 DESTROY(stateMachineName);
164 DESTROY(currentState);
165 DESTROY(pendingMessages);
166 DESTROY(jsScript);
167
168 [super dealloc];
169}
170
171
172- (NSString *) descriptionComponents
173{
174 return [NSString stringWithFormat:@"\"%@\" in state: \"%@\" for %@", stateMachineName, currentState, ownerDesc];
175}
176
177
178- (NSString *) shortDescriptionComponents
179{
180 return [NSString stringWithFormat:@"%@:%@ / %@", stateMachineName, currentState, [stateMachine objectForKey:@"jsScript"]];
181}
182
183
184- (ShipEntity *)owner
185{
186 ShipEntity *owner = [_owner weakRefUnderlyingObject];
187 if (owner == nil)
188 {
189 [_owner release];
190 _owner = nil;
191 }
192
193 return owner;
194}
195
196
197- (void) setOwner:(ShipEntity *)ship
198{
199 [_owner release];
200 _owner = [ship weakRetain];
201 [self refreshOwnerDesc];
202}
203
204
205- (void) reportStackOverflow
206{
207 if (OOLogWillDisplayMessagesInClass(@"ai.error.stackOverflow"))
208 {
209 BOOL stackDump = OOLogWillDisplayMessagesInClass(@"ai.error.stackOverflow.dump");
210
211 NSString *trailer = stackDump ? @" -- stack:" : @".";
212 OOLogERR(@"ai.error.stackOverflow", @"AI stack overflow for %@ in %@: %@%@\n", [_owner shortDescription], stateMachineName, currentState, trailer);
213
214 if (stackDump)
215 {
216 OOLogIndent();
217
218 NSUInteger count = [aiStack count];
219 while (count--)
220 {
221 OOPreservedAIStateMachine *preservedMachine = [aiStack objectAtIndex:count];
222 OOLog(@"ai.error.stackOverflow.dump", @"%3lu: %@: %@", count, [preservedMachine name], [preservedMachine state]);
223 }
224
225 OOLogOutdent();
226 }
227 }
228}
229
230
231- (void) preserveCurrentStateMachine
232{
233 if (stateMachine == nil) return;
234
235 if (aiStack == nil)
236 {
237 aiStack = [[NSMutableArray alloc] init];
238 }
239
240 if ([aiStack count] >= kStackLimiter)
241 {
242 [self reportStackOverflow];
243
244 [NSException raise:@"OoliteException"
245 format:@"AI stack overflow for %@", _owner];
246 }
247
248 OOPreservedAIStateMachine *preservedMachine = [[OOPreservedAIStateMachine alloc]
249 initWithStateMachine:stateMachine
250 name:stateMachineName
251 state:currentState
252 pendingMessages:pendingMessages
253 jsScript:[stateMachine objectForKey:@"jsScript"]];
254
255#ifndef NDEBUG
256 if ([[self owner] reportAIMessages]) OOLog(@"ai.stack.push", @"Pushing state machine for %@", self);
257#endif
258
259 [aiStack addObject:preservedMachine]; // PUSH
260
261 [preservedMachine release];
262}
263
264
265- (void) restorePreviousStateMachine
266{
267 if ([aiStack count] == 0) return;
268
269 OOPreservedAIStateMachine *preservedMachine = [aiStack lastObject];
270
271#ifndef NDEBUG
272 if ([[self owner] reportAIMessages]) OOLog(@"ai.stack.pop", @"Popping previous state machine for %@", self);
273#endif
274
275 [self directSetStateMachine:[preservedMachine stateMachine]
276 name:[preservedMachine name]];
277
278 [self directSetState:[preservedMachine state]];
279
280 // restore JS script
281 [[self owner] setAIScript:[preservedMachine jsScript]];
282
283 [pendingMessages release];
284 pendingMessages = [[preservedMachine pendingMessages] mutableCopy]; // restore a MUTABLE set
285
286 [aiStack removeLastObject]; // POP
287}
288
289
290- (BOOL) hasSuspendedStateMachines
291{
292 return [aiStack count] != 0;
293}
294
295
296- (void) exitStateMachineWithMessage:(NSString *)message
297{
298 if ([aiStack count] != 0)
299 {
300 [self restorePreviousStateMachine];
301 if (message == nil) message = @"RESTARTED";
302 [self reactToMessage:message context:@"suspended AI restart"];
303 }
304}
305
306
307- (void) setStateMachine:(NSString *)smName withJSScript:(NSString *)script
308{
309 NSDictionary *newSM = [self loadStateMachine:smName jsName:script];
310
311 if (newSM)
312 {
313 [self preserveCurrentStateMachine];
314 [self directSetStateMachine:newSM name:smName];
315 [self directSetState:@"GLOBAL"];
316
317 nextThinkTime = 0.0; // think at next tick
318
319 /* CRASH in objc_msgSend, apparently on [self reactToMessage:@"ENTER"] (1.69, OS X/x86).
320 Analysis: self corrupted. We're being called by __NSFireDelayedPerform, which doesn't go
321 through -[NSObject performSelector:withObject:], suggesting it's using IMP caching. An
322 invalid self is therefore possible.
323 Attempted fix: new delayed dispatch with trampoline, see -[AI setStateMachine:afterDelay:].
324 -- Ahruman, 20070706
325 */
326 [self reactToMessage:@"ENTER" context:@"changing AI"];
327
328 // refresh name
329 [self refreshOwnerDesc];
330 }
331}
332
333
334- (void) setState:(NSString *) stateName
335{
336 if ([stateMachine objectForKey:stateName])
337 {
338 /* CRASH in objc_msgSend, apparently on [self reactToMessage:@"EXIT"] (1.69, OS X/x86).
339 Analysis: self corrupted. We're being called by __NSFireDelayedPerform, which doesn't go
340 through -[NSObject performSelector:withObject:], suggesting it's using IMP caching. An
341 invalid self is therefore possible.
342 Attempted fix: new delayed dispatch with trampoline, see -[AI setState:afterDelay:].
343 -- Ahruman, 20070706
344 */
345 [self reactToMessage:@"EXIT" context:@"changing state"];
346 [self directSetState:stateName];
347 [self reactToMessage:@"ENTER" context:@"changing state"];
348 }
349}
350
351
352- (void) setStateMachine:(NSString *)smName afterDelay:(NSTimeInterval)delay
353{
354 [self performDeferredCall:@selector(setStateMachine:) withObject:smName afterDelay:delay];
355}
356
357
358- (void) setState:(NSString *)stateName afterDelay:(NSTimeInterval)delay
359{
360 [self performDeferredCall:@selector(setState:) withObject:stateName afterDelay:delay];
361}
362
363
364- (NSString *) name
365{
366 return [[stateMachineName retain] autorelease];
367}
368
369
370- (NSString *) associatedJS
371{
372 return [stateMachine objectForKey:@"jsScript"];
373}
374
375
376- (NSString *) state
377{
378 return [[currentState retain] autorelease];
379}
380
381
382- (NSUInteger) stackDepth
383{
384 return [aiStack count];
385}
386
387
388#ifndef NDEBUG
391{
394 NSString *aiName;
395 NSString *state;
396 NSString *message;
397 NSString *context;
398};
399
400static AIStackElement *sStack = NULL;
401#endif
402
403
404- (void) reactToMessage:(NSString *) message context:(NSString *)debugContext
405{
406 unsigned i;
407 NSArray *actions = nil;
408 NSDictionary *messagesForState = nil;
409 ShipEntity *owner = [self owner];
410 static unsigned recursionLimiter = 0;
411 AI *previousRunning = sCurrentlyRunningAI;
412
413 /* CRASH in _freedHandler when called via -setState: __NSFireDelayedPerform (1.69, OS X/x86).
414 Analysis: owner invalid.
415 Fix: make owner an OOWeakReference.
416 -- Ahruman, 20070706
417 */
418 if (message == nil || owner == nil || [owner universalID] == NO_TARGET) return;
419
420#ifndef NDEBUG
421 // Push debug stack frame.
422 if (debugContext == nil) debugContext = @"unspecified";
423 AIStackElement stackElement =
424 {
425 .back = sStack,
426 .owner = owner,
427 .aiName = [[stateMachineName retain] autorelease],
428 .state = [[currentState retain] autorelease],
429 .message = message,
430 .context = debugContext
431 };
432 sStack = &stackElement;
433#endif
434
435 /* CRASH when calling reactToMessage: FOO in state FOO causes infinite
436 recursion. (NB: there are other ways of triggering this.)
437 FIX: recursion limiter. Alternative is to explicitly catch this case
438 in takeAction:, but that could potentially miss indirect recursion via
439 scripts.
440 */
441 if (recursionLimiter > kRecursionLimiter)
442 {
443 OOLogERR(@"ai.error.recursion", @"AI dispatch: hit stack depth limit in AI %@, state %@ handling message %@ in context \"%@\", aborting.", stateMachineName, currentState, message, debugContext);
444
445#ifndef NDEBUG
446 AIStackElement *stack = sStack;
447 unsigned depth = 0;
448 while (stack != NULL)
449 {
450 OOLog(@"ai.error.recursion.stackTrace", @"%4u %@ - %@:%@.%@ (%@)", depth++, [stack->owner shortDescription], stack->aiName, stack->state, stack->message, stack->context);
451 stack = stack->back;
452 }
453
454 // unwind.
455 if (sStack != NULL) sStack = sStack->back;
456#endif
457
458 return;
459 }
460
461 messagesForState = [stateMachine objectForKey:currentState];
462 if (messagesForState == nil) return;
463
464#ifndef NDEBUG
465 if (currentState != nil && ![message isEqual:@"UPDATE"] && [owner reportAIMessages])
466 {
467 OOLog(@"ai.message.receive", @"AI %@ for %@ in state '%@' receives message '%@'. Context: %@, stack depth: %u", stateMachineName, ownerDesc, currentState, message, debugContext, recursionLimiter);
468 }
469#endif
470
471 actions = [[[messagesForState objectForKey:message] copy] autorelease];
472
473 sCurrentlyRunningAI = self;
474 if ([actions count] > 0)
475 {
476 ++recursionLimiter;
477 @try
478 {
479 for (i = 0; i < [actions count]; i++)
480 {
481 [self takeAction:[actions objectAtIndex:i]];
482 }
483 }
484 @catch (NSException *exception)
485 {
486 OOLog(kOOLogException, @"Squashing exception %@:%@ in AI handler %@:%@.%@", [exception name], [exception reason], stateMachineName, currentState, message);
487 }
488
489 --recursionLimiter;
490 }
491 else
492 {
493 if (currentState != nil)
494 {
495 if ([owner respondsToSelector:@selector(interpretAIMessage:)])
496 {
497 [owner performSelector:@selector(interpretAIMessage:) withObject:message];
498 }
499 }
500 }
501
502 sCurrentlyRunningAI = previousRunning;
503#ifndef NDEBUG
504 // Unwind stack.
505 if (sStack != NULL) sStack = sStack->back;
506#endif
507}
508
509
510- (void) takeAction:(NSString *)action
511{
512 ShipEntity *owner = [self owner];
513
514#ifndef NDEBUG
515 BOOL report = [owner reportAIMessages];
516 if (report)
517 {
518 OOLog(@"ai.takeAction", @"%@ to take action %@", ownerDesc, action);
519 OOLogIndent();
520 }
521#endif
522
523 NSArray *tokens = ScanTokensFromString(action);
524 NSUInteger tokenCount = [tokens count];
525
526 if (tokenCount != 0)
527 {
528 NSString *selectorStr = [tokens objectAtIndex:0];
529
530 if (owner != nil)
531 {
532 NSString *dataString = nil;
533
534 if (tokenCount == 2)
535 {
536 dataString = [tokens objectAtIndex:1];
537 }
538 else if ([tokens count] > 1)
539 {
540 dataString = [[tokens subarrayWithRange:NSMakeRange(1, tokenCount - 1)] componentsJoinedByString:@" "];
541 }
542
543 SEL selector = NSSelectorFromString(selectorStr);
544 if ([owner respondsToSelector:selector])
545 {
546 if (dataString != nil) [owner performSelector:selector withObject:dataString];
547 else [owner performSelector:selector];
548 }
549 else
550 {
551 OOLogERR(@"ai.takeAction.badSelector", @"in AI %@ in state %@: %@ does not respond to %@", stateMachineName, currentState, ownerDesc, selectorStr);
552 }
553 }
554 else
555 {
556 OOLog(@"ai.takeAction.orphaned", @"***** AI %@, trying to perform %@, is orphaned (no owner)", stateMachineName, selectorStr);
557 }
558 }
559 else
560 {
561#ifndef NDEBUG
562 if (report) OOLog(@"ai.takeAction.noAction", @"DEBUG: - no action '%@'", action);
563#endif
564 }
565
566#ifndef NDEBUG
567 if (report)
568 {
569 OOLogOutdent();
570 }
571#endif
572}
573
574
575- (void) think
576{
577 NSArray *ms_list = nil;
578 unsigned i;
579
580 if ([[self owner] universalID] == NO_TARGET || stateMachine == nil) return; // don't think until launched
581
582 [self reactToMessage:@"UPDATE" context:@"periodic update"];
583
584 if ([pendingMessages count] > 0)
585 {
586 ms_list = [pendingMessages allObjects];
587 [pendingMessages removeAllObjects];
588 }
589
590 if (ms_list != nil)
591 {
592 for (i = 0; i < [ms_list count]; i++)
593 {
594 [self reactToMessage:[ms_list objectAtIndex:i] context:@"handling deferred message"];
595 }
596 }
597}
598
599
600- (void) message:(NSString *)ms
601{
602 if ([[self owner] universalID] == NO_TARGET) return; // don't think until launched
603
604 if (EXPECT_NOT([pendingMessages count] > 32))
605 {
606 // Generate the error, but don't crash Oolite! Fixes bug #18055 - Pending message overflow for thargoids, -> crash !
607 OOLogERR(@"ai.message.failed.overflow", @"AI message \"%@\" received by '%@' AI while pending messages stack full; message discarded. Pending messages:\n%@", ms, ownerDesc, pendingMessages);
608 }
609 else
610 {
611 if (pendingMessages == nil)
612 {
613 pendingMessages = [[NSMutableSet alloc] init];
614 }
615 [pendingMessages addObject:ms];
616 }
617}
618
619
620- (void) dropMessage:(NSString *)ms
621{
622 [pendingMessages removeObject:ms];
623}
624
625
626- (NSSet *) pendingMessages
627{
628 if (pendingMessages != nil)
629 {
630 return [[pendingMessages copy] autorelease];
631 }
632 else
633 {
634 return [NSSet set];
635 }
636}
637
638
639- (void) debugDumpPendingMessages
640{
641 NSArray *sortedMessages = nil;
642 NSString *displayMessages = nil;
643
644 if ([pendingMessages count] > 0)
645 {
646 sortedMessages = [[pendingMessages allObjects] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
647 displayMessages = [sortedMessages componentsJoinedByString:@", "];
648 }
649 else
650 {
651 displayMessages = @"none";
652 }
653
654 OOLog(@"ai.debug.pendingMessages", @"Pending messages for AI %@: %@", [self descriptionComponents], displayMessages);
655}
656
657
658- (void) setNextThinkTime:(OOTimeAbsolute) ntt
659{
660 nextThinkTime = ntt;
661}
662
663
664- (OOTimeAbsolute) nextThinkTime
665{
666 if (!stateMachine)
667 return INFINITY;
668
669 return nextThinkTime;
670}
671
672
673- (void) setThinkTimeInterval:(OOTimeDelta) tti
674{
675 thinkTimeInterval = tti;
676}
677
678
679- (OOTimeDelta) thinkTimeInterval
680{
681 return thinkTimeInterval;
682}
683
684
685- (void) clearStack
686{
687 [aiStack removeAllObjects];
688}
689
690
691- (void) clearAllData
692{
693 [aiStack removeAllObjects];
694 [pendingMessages removeAllObjects];
695
696 nextThinkTime += 36000.0; // should dealloc in under ten hours!
697}
698
699
700- (void)dumpState
701{
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);
706}
707
708@end
709
710
711/* This is an attempt to fix the bugs referred to above regarding calls from
712 __NSFireDelayedPerform with a corrupt self. I'm not certain whether this
713 will fix the issue or merely cause a less weird crash in
714 +deferredCallTrampolineWithInfo:.
715 -- Ahruman 20070706
716*/
717@implementation AI (OOPrivate)
718
719- (void)performDeferredCall:(SEL)selector withObject:(id)object afterDelay:(NSTimeInterval)delay
720{
722 NSValue *info = nil;
723
724 if (selector != NULL)
725 {
726 infoStruct.ai = [self retain];
727 infoStruct.selector = selector;
728 infoStruct.parameter = object;
729
730 info = [[NSValue alloc] initWithBytes:&infoStruct objCType:@encode(OOAIDeferredCallTrampolineInfo)];
731
732 [[AI class] performSelector:@selector(deferredCallTrampolineWithInfo:)
733 withObject:info
734 afterDelay:delay];
735 [info release];
736 }
737}
738
739
740+ (void)deferredCallTrampolineWithInfo:(NSValue *)info
741{
743
744 if (info != nil)
745 {
746 assert(strcmp([info objCType], @encode(OOAIDeferredCallTrampolineInfo)) == 0);
747 [info getValue:&infoStruct];
748
749 [infoStruct.ai performSelector:infoStruct.selector withObject:infoStruct.parameter];
750
751 [infoStruct.ai release];
752 [infoStruct.parameter release];
753 }
754}
755
756
757- (void)refreshOwnerDesc
758{
759 ShipEntity *owner = [self owner];
760 [ownerDesc release];
761 if ([owner isPlayer])
762 {
763 ownerDesc = @"player autopilot";
764 }
765 else if (owner != nil)
766 {
767 ownerDesc = [[NSString alloc] initWithFormat:@"%@ %d", [owner name], [owner universalID]];
768 }
769 else
770 {
771 ownerDesc = @"no owner";
772 }
773}
774
775
776- (void) directSetStateMachine:(NSDictionary *)newSM name:(NSString *)name
777{
778 if (stateMachine != newSM)
779 {
780 [stateMachine release];
781 stateMachine = [newSM copy];
782 }
783 if (stateMachineName != name)
784 {
785 [stateMachineName release];
786 stateMachineName = [name copy];
787 }
788}
789
790
791- (void) directSetState:(NSString *)state
792{
793 if (currentState != state)
794 {
795 [currentState release];
796 currentState = [state copy];
797 }
798}
799
800
801- (NSDictionary *) loadStateMachine:(NSString *)smName jsName:(NSString *)script
802{
803 NSDictionary *newSM = nil;
804 NSMutableDictionary *cleanSM = nil;
806 NSEnumerator *stateEnum = nil;
807 NSString *stateKey = nil;
808 NSDictionary *stateHandlers = nil;
809 NSAutoreleasePool *pool = nil;
810
811 if (![smName isEqualToString:@"nullAI.plist"])
812 {
813 // don't cache nullAI since they're different depending on associated JS AI
814 newSM = [cacheMgr objectForKey:smName inCache:@"AIs"];
815 if (newSM != nil && ![newSM isKindOfClass:[NSDictionary class]]) return nil; // catches use of @"nil" to indicate no AI found.
816 }
817
818 if (newSM == nil)
819 {
820 pool = [[NSAutoreleasePool alloc] init];
821 OOLog(@"ai.load", @"Loading and sanitizing AI \"%@\"", smName);
823 OOLogIndentIf(@"ai.load");
824
825 @try
826 {
827 // Load state machine and validate against whitelist.
828 NSString *aiPath = [ResourceManager pathForFileNamed:smName inFolder:@"AIs"];
829 if (aiPath != nil)
830 {
831 newSM = OODictionaryFromFile(aiPath);
832 }
833 if (newSM == nil)
834 {
835 [cacheMgr setObject:@"nil" forKey:smName inCache:@"AIs"];
836 NSString *fromString = @"";
837 if ([self state] != nil)
838 {
839 fromString = [NSString stringWithFormat:@" from %@:%@", [self name], [self state]];
840 }
841 OOLog(@"ai.load.failed.unknownAI", @"Can't switch AI for %@%@ to \"%@\" - could not load file.", [[self owner] shortDescription], fromString, smName);
842 return nil;
843 }
844
845 cleanSM = [NSMutableDictionary dictionaryWithCapacity:[newSM count]];
846
847 for (stateEnum = [newSM keyEnumerator]; (stateKey = [stateEnum nextObject]); )
848 {
849 stateHandlers = [newSM objectForKey:stateKey];
850 if (![stateHandlers isKindOfClass:[NSDictionary class]])
851 {
852 OOLogWARN(@"ai.invalidFormat.state", @"State \"%@\" in AI \"%@\" is not a dictionary, ignoring.", stateKey, smName);
853 continue;
854 }
855
856 stateHandlers = [self cleanHandlers:stateHandlers forState:stateKey stateMachine:smName];
857 [cleanSM setObject:stateHandlers forKey:stateKey];
858 }
859 [cleanSM setObject:script forKey:@"jsScript"];
860
861 // Make immutable.
862 newSM = [[cleanSM copy] autorelease];
863
864#if DEBUG_GRAPHVIZ
865 if ([[NSUserDefaults standardUserDefaults] boolForKey:@"generate-ai-graphviz"])
866 {
867 GenerateGraphVizForAIStateMachine(newSM, smName);
868 }
869#endif
870
871 // Cache.
872 [cacheMgr setObject:newSM forKey:smName inCache:@"AIs"];
873 }
874 @finally
875 {
877 }
878
879 [newSM retain];
880 [pool release];
881 [newSM autorelease];
882 }
883
884 return newSM;
885}
886
887
888- (NSDictionary *) cleanHandlers:(NSDictionary *)handlers forState:(NSString *)stateKey stateMachine:(NSString *)smName
889{
890 NSEnumerator *handlerEnum = nil;
891 NSString *handlerKey = nil;
892 NSArray *handlerActions = nil;
893 NSMutableDictionary *result = nil;
894
895 result = [NSMutableDictionary dictionaryWithCapacity:[handlers count]];
896 for (handlerEnum = [handlers keyEnumerator]; (handlerKey = [handlerEnum nextObject]); )
897 {
898 handlerActions = [handlers objectForKey:handlerKey];
899 if (![handlerActions isKindOfClass:[NSArray class]])
900 {
901 OOLogWARN(@"ai.invalidFormat.handler", @"Handler \"%@\" for state \"%@\" in AI \"%@\" is not an array, ignoring.", handlerKey, stateKey, smName);
902 continue;
903 }
904
905 handlerActions = [self cleanActions:handlerActions forHandler:handlerKey state:stateKey stateMachine:smName];
906 [result setObject:handlerActions forKey:handlerKey];
907 }
908
909 // Return immutable copy.
910 return [[result copy] autorelease];
911}
912
913
914- (NSArray *) cleanActions:(NSArray *)actions forHandler:(NSString *)handlerKey state:(NSString *)stateKey stateMachine:(NSString *)smName
915{
916 NSEnumerator *actionEnum = nil;
917 NSString *action = nil;
918 NSRange spaceRange;
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;
926
927 if (whitelist == nil)
928 {
929 whitelistArray1 = [[ResourceManager whitelistDictionary] oo_arrayForKey:@"ai_methods"];
930 if (whitelistArray1 == nil) whitelistArray1 = [NSArray array];
931 whitelistArray2 = [[ResourceManager whitelistDictionary] oo_arrayForKey:@"ai_and_action_methods"];
932 if (whitelistArray2 != nil) whitelistArray1 = [whitelistArray1 arrayByAddingObjectsFromArray:whitelistArray2];
933
934 whitelist = [[NSSet alloc] initWithArray:whitelistArray1];
935 aliases = [[[ResourceManager whitelistDictionary] oo_dictionaryForKey:@"ai_method_aliases"] retain];
936 }
937
938 result = [NSMutableArray arrayWithCapacity:[actions count]];
939 for (actionEnum = [actions objectEnumerator]; (action = [actionEnum nextObject]); )
940 {
941 if (![action isKindOfClass:[NSString class]])
942 {
943 OOLogWARN(@"ai.invalidFormat.action", @"An action in handler \"%@\" for state \"%@\" in AI \"%@\" is not a string, ignoring.", handlerKey, stateKey, smName);
944 continue;
945 }
946
947 // Trim spaces from beginning and end.
948 action = [action stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
949
950 // Cut off parameters.
951 spaceRange = [action rangeOfString:@" "];
952 if (spaceRange.location == NSNotFound) selector = action;
953 else selector = [action substringToIndex:spaceRange.location];
954
955 // Look in alias table.
956 aliasedSelector = [aliases objectForKey:selector];
957 if (aliasedSelector != nil)
958 {
959 if ([aliasedSelector isKindOfClass:[NSString class]])
960 {
961 // Change selector and action to use real method name.
962 selector = aliasedSelector;
963 if (spaceRange.location == NSNotFound) action = aliasedSelector;
964 else action = [aliasedSelector stringByAppendingString:[action substringFromIndex:spaceRange.location]];
965 }
966 else if ([aliasedSelector isKindOfClass:[NSArray class]] && [aliasedSelector count] != 0)
967 {
968 // Alias is complete expression, pretokenized in anticipation of a tokenized future.
969 action = [aliasedSelector componentsJoinedByString:@" "];
970 selector = [[aliasedSelector objectAtIndex:0] description];
971 }
972 }
973
974 // Check for selector in whitelist.
975 if (![whitelist containsObject:selector])
976 {
977 OOLog(@"ai.unpermittedMethod", @"Handler \"%@\" for state \"%@\" in AI \"%@\" uses \"%@\", which is not a permitted AI method.", handlerKey, stateKey, smName, selector);
978 continue;
979 }
980
981 [result addObject:action];
982 }
983
984 // Return immutable copy.
985 return [[result copy] autorelease];
986}
987
988@end
989
990
991@implementation OOPreservedAIStateMachine
992
993- (id) initWithStateMachine:(NSDictionary *)stateMachine
994 name:(NSString *)name
995 state:(NSString *)state
996 pendingMessages:(NSSet *)pendingMessages
997 jsScript:(NSString *)script
998{
999 if ((self = [super init]))
1000 {
1001 _stateMachine = [stateMachine copy];
1002 _name = [name copy];
1003 _state = [state copy];
1004 _pendingMessages = [pendingMessages copy];
1005 _jsScript = [script copy];
1006 }
1007
1008 return self;
1009}
1010
1011
1012- (void) dealloc
1013{
1014 [_stateMachine autorelease];
1015 [_name autorelease];
1016 [_state autorelease];
1017 [_pendingMessages autorelease];
1018 [_jsScript autorelease];
1019
1020 [super dealloc];
1021}
1022
1023
1024- (NSDictionary *) stateMachine
1025{
1026 return _stateMachine;
1027}
1028
1029
1030- (NSString *) name
1031{
1032 return _name;
1033}
1034
1035
1036- (NSString *) state
1037{
1038 return _state;
1039}
1040
1041
1042- (NSSet *) pendingMessages
1043{
1044 return _pendingMessages;
1045}
1046
1047- (NSString *) jsScript
1048{
1049 return _jsScript;
1050}
1051
1052@end
#define AI_THINK_INTERVAL
Definition AI.h:31
static AI * sCurrentlyRunningAI
Definition AI.m:52
static AIStackElement * sStack
Definition AI.m:400
@ kStackLimiter
Definition AI.m:40
@ kRecursionLimiter
Definition AI.m:39
#define DESTROY(x)
Definition OOCocoa.h:77
#define EXPECT_NOT(x)
void OOLogPushIndent(void)
Definition OOLogging.m:316
#define OOLogWARN(class, format,...)
Definition OOLogging.h:113
#define OOLogERR(class, format,...)
Definition OOLogging.h:112
NSString *const kOOLogException
Definition OOLogging.m:651
void OOLogPopIndent(void)
Definition OOLogging.m:340
BOOL OOLogWillDisplayMessagesInClass(NSString *inMessageClass)
Definition OOLogging.m:144
void OOLogOutdent(void)
Definition OOLogging.m:376
#define OOLog(class, format,...)
Definition OOLogging.h:88
#define OOLogIndentIf(class)
Definition OOLogging.h:101
void OOLogIndent(void)
Definition OOLogging.m:366
NSDictionary * OODictionaryFromFile(NSString *path)
unsigned count
return nil
NSMutableArray * ScanTokensFromString(NSString *values)
double OOTimeDelta
Definition OOTypes.h:224
double OOTimeAbsolute
Definition OOTypes.h:223
@ NO_TARGET
Definition OOTypes.h:194
void refreshOwnerDesc()
Definition AI.m:757
Definition AI.h:38
NSString * currentState
Definition AI.h:45
NSDictionary * stateMachine
Definition AI.h:43
NSString * name()
Definition AI.m:364
ShipEntity * owner()
Definition AI.m:184
void takeAction:(NSString *action)
Definition AI.m:510
NSString * stateMachineName
Definition AI.h:44
NSString * state()
Definition AI.m:376
OOUniversalID universalID
Definition Entity.h:89
void setObject:forKey:inCache:(id inElement,[forKey] NSString *inKey,[inCache] NSString *inCacheKey)
id objectForKey:inCache:(NSString *inKey,[inCache] NSString *inCacheKey)
OOCacheManager * sharedCache()
NSString * name()
Definition AI.m:1030
NSString * _state
Definition AI.m:85
NSString * jsScript()
Definition AI.m:1047
NSSet * pendingMessages()
Definition AI.m:1042
NSString * state()
Definition AI.m:1036
NSDictionary * stateMachine()
Definition AI.m:1024
NSMutableSet * _pendingMessages
Definition AI.m:86
NSString * _name
Definition AI.m:84
NSString * _jsScript
Definition AI.m:87
NSDictionary * _stateMachine
Definition AI.m:83
NSString * pathForFileNamed:inFolder:(NSString *fileName,[inFolder] NSString *folderName)
NSDictionary * whitelistDictionary()
unsigned reportAIMessages
Definition ShipEntity.h:255
NSString * name
Definition ShipEntity.h:327
ShipEntity * owner
Definition AI.m:393
AIStackElement * back
Definition AI.m:392
NSString * aiName
Definition AI.m:394
NSString * context
Definition AI.m:397
NSString * message
Definition AI.m:396
NSString * state
Definition AI.m:395