Line data Source code
1 0 : /*
2 :
3 : AI.m
4 :
5 : Oolite
6 : Copyright (C) 2004-2013 Giles C Williams and contributors
7 :
8 : This program is free software; you can redistribute it and/or
9 : modify it under the terms of the GNU General Public License
10 : as published by the Free Software Foundation; either version 2
11 : of the License, or (at your option) any later version.
12 :
13 : This program is distributed in the hope that it will be useful,
14 : but WITHOUT ANY WARRANTY; without even the implied warranty of
15 : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 : GNU General Public License for more details.
17 :
18 : You should have received a copy of the GNU General Public License
19 : along with this program; if not, write to the Free Software
20 : Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
21 : MA 02110-1301, USA.
22 :
23 : */
24 :
25 : #import "AI.h"
26 : #import "ResourceManager.h"
27 : #import "OOStringParsing.h"
28 : #import "OOWeakReference.h"
29 : #import "OOCacheManager.h"
30 : #import "OOCollectionExtractors.h"
31 : #import "OOPListParsing.h"
32 :
33 : #import "ShipEntity.h"
34 : #import "ShipEntityAI.h"
35 :
36 :
37 0 : enum
38 : {
39 : kRecursionLimiter = 32, // reactToMethod: recursion
40 : kStackLimiter = 32 // setAITo: stack overflow
41 : };
42 :
43 :
44 0 : typedef struct
45 : {
46 0 : AI *ai;
47 0 : SEL selector;
48 0 : id parameter;
49 : } OOAIDeferredCallTrampolineInfo;
50 :
51 :
52 0 : static AI *sCurrentlyRunningAI = nil;
53 :
54 :
55 : @interface AI (OOPrivate)
56 :
57 : // Wrapper for performSelector:withObject:afterDelay: to catch/fix bugs.
58 0 : - (void) performDeferredCall:(SEL)selector withObject:(id)object afterDelay:(NSTimeInterval)delay;
59 0 : + (void) deferredCallTrampolineWithInfo:(NSValue *)info;
60 :
61 0 : - (void) refreshOwnerDesc;
62 :
63 : // Set state machine and state without side effects.
64 0 : - (void) directSetStateMachine:(NSDictionary *)newSM name:(NSString *)name;
65 0 : - (void) directSetState:(NSString *)state;
66 :
67 : // Loading/whitelisting
68 0 : - (NSDictionary *) loadStateMachine:(NSString *)smName jsName:(NSString *)script;
69 0 : - (NSDictionary *) cleanHandlers:(NSDictionary *)handlers forState:(NSString *)stateKey stateMachine:(NSString *)smName;
70 0 : - (NSArray *) cleanActions:(NSArray *)actions forHandler:(NSString *)handlerKey state:(NSString *)stateKey stateMachine:(NSString *)smName;
71 :
72 : @end
73 :
74 :
75 : #if DEBUG_GRAPHVIZ
76 : extern void GenerateGraphVizForAIStateMachine(NSDictionary *stateMachine, NSString *name);
77 : #endif
78 :
79 :
80 0 : @interface OOPreservedAIStateMachine: NSObject
81 : {
82 : @private
83 0 : NSDictionary *_stateMachine;
84 0 : NSString *_name;
85 0 : NSString *_state;
86 0 : NSMutableSet *_pendingMessages;
87 0 : NSString *_jsScript;
88 : }
89 :
90 0 : - (id) initWithStateMachine:(NSDictionary *)stateMachine
91 : name:(NSString *)name
92 : state:(NSString *)state
93 : pendingMessages:(NSSet *)pendingMessages
94 : jsScript:(NSString *)script;
95 :
96 0 : - (NSDictionary *) stateMachine;
97 0 : - (NSString *) name;
98 0 : - (NSString *) state;
99 0 : - (NSSet *) pendingMessages;
100 0 : - (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 : {
115 : if (sCurrentlyRunningAI != nil)
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 0 : - (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 0 : - (void) dealloc
153 : {
154 : if (sCurrentlyRunningAI == self)
155 : {
156 : sCurrentlyRunningAI = nil;
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 0 : - (NSString *) descriptionComponents
173 : {
174 : return [NSString stringWithFormat:@"\"%@\" in state: \"%@\" for %@", stateMachineName, currentState, ownerDesc];
175 : }
176 :
177 :
178 0 : - (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 0 : - (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
389 0 : typedef struct AIStackElement AIStackElement;
390 0 : struct AIStackElement
391 : {
392 0 : AIStackElement *back;
393 0 : ShipEntity *owner;
394 0 : NSString *aiName;
395 0 : NSString *state;
396 0 : NSString *message;
397 0 : NSString *context;
398 : };
399 :
400 0 : static 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 : {
721 : OOAIDeferredCallTrampolineInfo infoStruct;
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 : {
742 : OOAIDeferredCallTrampolineInfo infoStruct;
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;
805 : OOCacheManager *cacheMgr = [OOCacheManager sharedCache];
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);
822 : OOLogPushIndent();
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 : {
876 : OOLogPopIndent();
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 0 : - (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
|