]> Dogcows Code - chaz/thecheat/blob - LocalCheater.m
update contact information and project URL
[chaz/thecheat] / LocalCheater.m
1
2 /*
3 * The Cheat - The legendary universal game trainer for Mac OS X.
4 * http://www.brokenzipper.com/trac/wiki/TheCheat
5 *
6 * Copyright (c) 2003-2011, Charles McGarvey et al.
7 *
8 * Distributable under the terms and conditions of the 2-clause BSD
9 * license; see the file COPYING for the legal text of the license.
10 */
11
12 #import "LocalCheater.h"
13
14 #include "cheat_global.h"
15
16
17 // memory dump function
18 int _MemoryDumpTask( ThreadedTask *task, unsigned iteration );
19
20
21 @interface LocalCheater ( Private )
22
23 // internal methods so they can be used throughout the class
24 - (void)_clearSearch;
25 - (BOOL)_pauseTarget; // returns TRUE for success
26 - (BOOL)_resumeTarget; // returns TRUE for success
27 // handling app launching/quitting
28 - (void)_applicationLaunched:(NSNotification *)note;
29 - (void)_applicationTerminated:(NSNotification *)note;
30 // cheating
31 - (unsigned)_doChange:(NSArray *)variables;
32 - (void)_changeTimer:(NSTimer *)timer;
33 // watch variables
34 - (void)_doWatch;
35 - (void)_watchTimer:(NSTimer *)timer;
36 // tasks
37 - (int)_memoryDumpTask:(ThreadedTask *)task iteration:(unsigned)iteration;
38
39 @end
40
41
42 @implementation LocalCheater
43
44
45 - (id)init
46 {
47 if ( self = [super init] ) {
48 NSNotificationCenter *nc= [[NSWorkspace sharedWorkspace] notificationCenter];
49
50 // register for app launch/quit notifications
51 [nc addObserver:self selector:@selector(_applicationLaunched:) name:NSWorkspaceDidLaunchApplicationNotification object:nil];
52 [nc addObserver:self selector:@selector(_applicationTerminated:) name:NSWorkspaceDidTerminateApplicationNotification object:nil];
53
54 _searchResults = [[NSMutableArray alloc] init];
55 _savedResults = [[NSMutableArray alloc] init];
56 _returnLimit = -1;
57 }
58 return self;
59 }
60
61 - (void)dealloc
62 {
63 [(NSNotificationCenter *)[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:self];
64
65 // make sure the process isn't left paused
66 [self _resumeTarget];
67
68 // release local objects
69 [_target release];
70 [self _clearSearch];
71
72 // make sure everything is cancelled.
73 [_searchTask cancelAndRemoveDelegate];
74 [_dumpTask cancelAndRemoveDelegate];
75 [self stopChangingVariables];
76 [self stopWatchingVariables];
77
78 [_searchTask release];
79 [_dumpTask release];
80
81 [_processes release];
82
83 [super dealloc];
84 }
85
86
87 - (BOOL)shouldCopy
88 {
89 return _shouldCopy;
90 }
91
92 - (void)setShouldCopy:(BOOL)flag
93 {
94 _shouldCopy = flag;
95 }
96
97
98 - (NSString *)hostAddress
99 {
100 //return @"127.0.0.1";
101 return @"this computer";
102 }
103
104
105 // #############################################################################
106 #pragma mark Cheater Override
107 // #############################################################################
108
109 - (void)connect
110 {
111 if ( !_isConnected ) {
112 _isConnected = YES;
113 // return as connected
114 [_delegate cheaterDidConnect:self];
115 }
116 }
117
118 - (void)disconnect
119 {
120 if ( _isConnected ) {
121 [_searchTask cancelAndRemoveDelegate];
122 [_dumpTask cancelAndRemoveDelegate];
123 [self stopChangingVariables];
124 [self stopWatchingVariables];
125
126 _isConnected = NO;
127 // return as disconnected
128 //[_delegate cheaterDidDisconnect:self];
129 }
130 }
131
132 - (void)authenticateWithPassword:(NSString *)password
133 {
134 // being a local cheater, authentication really isn't required.
135 // return succes every time.
136 _isAuthenticated = YES;
137 [_delegate cheaterAcceptedPassword:self];
138 }
139
140
141 - (void)getProcessList
142 {
143 NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
144 // NSArray *launchedApps = [workspace launchedApplications];
145 // unsigned i, len = [launchedApps count];
146 ProcessSerialNumber psn = {0, kNoProcess};
147
148 if ( !_processes ) {
149 //_processes = [[NSMutableArray alloc] initWithCapacity:len];
150 _processes = [[NSMutableArray alloc] initWithCapacity:1];
151 }
152
153 // compile process array
154 // for ( i = 0; i < len; i++ ) {
155 while(/*procNotFound != */!GetNextProcess(&psn)) {
156 // NSDictionary *application = [launchedApps objectAtIndex:i];
157 NSDictionary *application = (NSDictionary *)ProcessInformationCopyDictionary(&psn, kProcessDictionaryIncludeAllInformationMask);
158 void *bundlePath = [application objectForKey:@"BundlePath"];
159
160 // don't allow The Cheat to be cheated
161 // if ( [[application objectForKey:@"NSApplicationBundleIdentifier"] isEqualToString:[[NSBundle mainBundle] bundleIdentifier]] ) {
162 if ( [[application objectForKey:(NSString *)kCFBundleIdentifierKey] isEqualToString:[[NSBundle mainBundle] bundleIdentifier]] ) {
163 continue;
164 }
165
166 /*Process *process = [[Process alloc] initWithName:[application objectForKey:@"NSApplicationName"]]
167 version:ApplicationVersion( [application objectForKey:@"NSApplicationPath"] )
168 icon:[workspace iconForFile:[application objectForKey:@"NSApplicationPath"]]
169 pid:[[application objectForKey:@"NSApplicationProcessIdentifier"] intValue]];*/
170 Process *process = [[Process alloc] initWithName:[application objectForKey:(NSString *)kCFBundleNameKey]
171 version:ApplicationVersion( bundlePath ? bundlePath: [application objectForKey:(NSString *)kCFBundleExecutableKey] )
172 icon:[workspace iconForFile:bundlePath ? bundlePath: [application objectForKey:(NSString *)kCFBundleExecutableKey]]
173 pid:[[application objectForKey:@"pid"] intValue]];
174 [_processes addObject:process];
175 [process release];
176 }
177
178 // return process list
179 [_delegate cheater:self didFindProcesses:[NSArray arrayWithArray:_processes]];
180 }
181
182
183 - (void)setTarget:(Process *)target
184 {
185 // unpause the current target if needed
186 [self resumeTarget];
187 // clear the search
188 [self clearSearch];
189 // set the new target
190 [_target release];
191
192 if ( _shouldCopy ) {
193 _target = [target copy];
194 [_delegate cheater:self didSetTarget:[_target copy]];
195 }
196 else {
197 _target = [target retain];
198 [_delegate cheater:self didSetTarget:_target];
199 }
200 }
201
202 - (void)pauseTarget
203 {
204 // attempt to pause the current target
205 if ( !_isTargetPaused && [self _pauseTarget] ) {
206 [_delegate cheaterPausedTarget:self];
207 }
208 else {
209 [_delegate cheater:self echo:@"This process cannot be paused."];
210 }
211 }
212
213 - (void)resumeTarget
214 {
215 // attempt to resume the current target
216 if ( _isTargetPaused && [self _resumeTarget] ) {
217 [_delegate cheaterResumedTarget:self];
218 }
219 else {
220 [_delegate cheater:self echo:@"This process cannot be resumed."];
221 }
222 }
223
224
225 - (void)limitReturnedResults:(unsigned)limit
226 {
227 _returnLimit = limit;
228 }
229
230 - (void)searchForVariable:(Variable *)var comparison:(TCSearchOperator)op
231 {
232 SearchContext *context;
233 void *function;
234
235 if ( _searchTask ) {
236 ChazLog( @"there is already a search task" );
237 return;
238 }
239
240 if ( [_searchResults count] > 0 ) {
241 SearchContext *lastContext = [_searchResults lastObject];
242 if ( ([var type] == TCString) && ([lastContext->value valueSize] < [var valueSize]) ) {
243 // string size not good
244 [_delegate cheater:self didFailLastRequest:@"String is too long."];
245 return;
246 }
247 context = [[SearchContext alloc] initWithLastContext:[_searchResults lastObject] searchOperator:op value:var];
248 }
249 else {
250 context = [[SearchContext alloc] initWithPID:[_target pid] searchOperator:op value:var];
251 }
252
253 function = [context iterationFunction];
254 if ( function ) {
255 SearchContext *searchContext = context;
256 if (searchContext->value->_type != TCFloat && searchContext->value->_type != TCDouble)
257 {
258 bigEndianValue(searchContext->value->_value, searchContext->value);
259 }
260
261 _searchTask = [[ThreadedTask alloc] initWithFunction:function
262 context:context
263 delegate:self];
264
265 if ( ![_searchTask run] ) {
266 // task didn't run
267 [_searchTask release];
268 _searchTask = nil;
269 [_delegate cheater:self didFailLastRequest:@"Internal error."];
270 }
271 }
272 else {
273 [_delegate cheater:self didFailLastRequest:@"Invalid search parameters."];
274 }
275
276 [context release];
277 }
278
279
280 - (void)searchLastValuesComparison:(TCSearchOperator)op
281 {
282 SearchContext *context;
283 void *function;
284
285 if ( _searchTask ) {
286 ChazLog( @"there is already a search task" );
287 return;
288 }
289
290 if ( [_searchResults count] > 0 ) {
291 context = [[SearchContext alloc] initWithLastContext:[_searchResults lastObject] searchOperator:op];
292 }
293 else {
294 ChazLog( @"doing a searchLastValues search without previous results..." );
295 [_delegate cheater:self didFailLastRequest:@"Invalid search."];
296 return;
297 }
298
299 function = [context iterationFunction];
300 if ( function ) {
301 _searchTask = [[ThreadedTask alloc] initWithFunction:function
302 context:context
303 delegate:self];
304
305 if ( ![_searchTask run] ) {
306 // task didn't run
307 [_searchTask release];
308 _searchTask = nil;
309 [_delegate cheater:self didFailLastRequest:@"Internal error."];
310 }
311 }
312 else {
313 [_delegate cheater:self didFailLastRequest:@"Invalid search parameters."];
314 }
315
316 [context release];
317 }
318
319 - (void)cancelSearch
320 {
321 [_searchTask cancelWithoutWaiting];
322 }
323
324 - (void)clearSearch
325 {
326 [self _clearSearch];
327 [_delegate cheaterDidClearSearch:self];
328 }
329
330
331 - (void)getMemoryDump
332 {
333 if ( _dumpTask ) {
334 ChazLog( @"there is already a dump task" );
335 return;
336 }
337
338 _dumpTask = [[ThreadedTask alloc] initWithFunction:_MemoryDumpTask
339 context:[[[DumpContext alloc] initWithPID:[_target pid]] autorelease]
340 delegate:self];
341
342 if ( ![_dumpTask run] ) {
343 [_dumpTask release];
344 _dumpTask = nil;
345 [_delegate cheater:self didFailLastRequest:@"Internal error."];
346 }
347 }
348
349 int _MemoryDumpTask( ThreadedTask *task, unsigned iteration )
350 {
351 DumpContext *context = [task context];
352 VMRegion region = VMNextRegionWithAttributes( context->process, context->lastRegion, VMREGION_READABLE );
353
354 if ( VMRegionIsNotNull( region ) ) {
355 [context->dump appendData:VMRegionData( region )];
356
357 context->lastRegion = region;
358
359 // continue looping
360 return 1;
361 }
362 else {
363 // no more regions, exit
364 return 0;
365 }
366 }
367
368 - (void)cancelMemoryDump
369 {
370 [_dumpTask cancelWithoutWaiting];
371 }
372
373
374 - (void)makeVariableChanges:(NSArray *)variables repeat:(BOOL)doRepeat interval:(NSTimeInterval)repeatInterval
375 {
376 unsigned changes;
377
378 [self stopChangingVariables];
379
380 // repeat the changes if necessary
381 if ( doRepeat ) {
382 if ( _shouldCopy ) {
383 _cheatVariables = [variables copy];
384 }
385 else {
386 _cheatVariables = [variables retain];
387 }
388 _cheatTimer = [[NSTimer scheduledTimerWithTimeInterval:repeatInterval
389 target:self
390 selector:@selector(_changeTimer:)
391 userInfo:nil
392 repeats:YES] retain];
393 [[NSRunLoop currentRunLoop] addTimer:_cheatTimer forMode:NSEventTrackingRunLoopMode];
394 [[NSRunLoop currentRunLoop] addTimer:_cheatTimer forMode:NSModalPanelRunLoopMode];
395 }
396
397 // change variables
398 changes = [self _doChange:variables];
399 [_delegate cheater:self didChangeVariables:changes];
400 }
401
402 - (void)stopChangingVariables
403 {
404 if ( _cheatVariables ) {
405 [_cheatTimer invalidate];
406 [_cheatTimer release];
407 _cheatTimer = nil;
408 [_cheatVariables release];
409 _cheatVariables = nil;
410
411 // report back to delegate
412 [_delegate cheaterDidStopChangingVariables:self];
413 }
414 }
415
416
417 - (void)undo
418 {
419 SearchContext *searchContext;
420 TCArray variables;
421 TCArray values;
422
423 if ( searchContext = [_searchResults lastObject] ) {
424 [_savedResults addObject:searchContext];
425 [_searchResults removeLastObject];
426
427 [self stopWatchingVariables];
428
429 if ( searchContext = [_searchResults lastObject] ) {
430 if ( _shouldCopy ) {
431 variables = TCArrayCopyElements( searchContext->addresses, _returnLimit );
432 values = TCArrayCopyElements( searchContext->values, _returnLimit );
433 }
434 else {
435 variables = TCArrayCopyContainer( searchContext->addresses, _returnLimit );
436 values = TCArrayCopyContainer( searchContext->values, _returnLimit );
437 }
438
439 [_delegate cheater:self didRevertToVariables:variables actualAmount:TCArrayElementCount( searchContext->addresses )];
440 [_delegate cheater:self didFindValues:values];
441 }
442
443 [_delegate cheaterDidUndo:self];
444 }
445 }
446
447 - (void)redo
448 {
449 SearchContext *searchContext;
450 TCArray variables;
451 TCArray values;
452
453 [self stopWatchingVariables];
454
455 if ( searchContext = [_savedResults lastObject] ) {
456 [_searchResults addObject:searchContext];
457 [_savedResults removeLastObject];
458
459 if ( _shouldCopy ) {
460 variables = TCArrayCopyElements( searchContext->addresses, _returnLimit );
461 values = TCArrayCopyElements( searchContext->values, _returnLimit );
462 }
463 else {
464 variables = TCArrayCopyContainer( searchContext->addresses, _returnLimit );
465 values = TCArrayCopyContainer( searchContext->values, _returnLimit );
466 }
467
468 [_delegate cheater:self didRevertToVariables:variables actualAmount:TCArrayElementCount( searchContext->addresses )];
469 [_delegate cheater:self didFindValues:values];
470 [_delegate cheaterDidRedo:self];
471 }
472 }
473
474
475 - (void)watchVariablesAtIndex:(unsigned)index count:(unsigned)count interval:(NSTimeInterval)checkInterval
476 {
477 SearchContext *context;
478 unsigned i, top;
479
480 ChazLog( @"watchVariablesAtIndex:.." );
481
482 [self stopWatchingVariables];
483
484 if ( count == 0 ) {
485 ChazLog( @"invalid watch parameters: 0 count" );
486 return;
487 }
488
489 if ( context = [_searchResults lastObject] ) {
490 TCArray addresses = context->addresses;
491 TCArray values = context->values;
492 // check the index & count
493 if ( index + count <= TCArrayElementCount( addresses ) ) {
494 // save current values
495 NSMutableArray *vars = [[NSMutableArray alloc] initWithCapacity:count];
496 top = index + count;
497 for ( i = index; i < top; i++ ) {
498 Variable *var = [[Variable alloc] initWithType:[context variableType] integerSign:[context integerSign]];
499 [var setProcess:_target];
500 [var setAddress:*(TCAddress *)TCArrayElementAtIndex( addresses, i )];
501 [var setValue:TCArrayElementAtIndex(values, i) size:TCArrayElementSize(values)];
502 [vars addObject:var];
503 [var release];
504 }
505 _watchVariables = [[NSArray arrayWithArray:vars] retain];
506 [vars release];
507
508 _watchRange = NSMakeRange( index, count );
509 _watchTimer = [[NSTimer scheduledTimerWithTimeInterval:checkInterval
510 target:self
511 selector:@selector(_watchTimer:)
512 userInfo:nil
513 repeats:YES] retain];
514 [[NSRunLoop currentRunLoop] addTimer:_watchTimer forMode:NSEventTrackingRunLoopMode];
515 [[NSRunLoop currentRunLoop] addTimer:_watchTimer forMode:NSModalPanelRunLoopMode];
516
517 // do a watch check right now
518 [self _doWatch];
519 }
520 else {
521 ChazLog( @"invalid watch parameters" );
522 }
523 }
524 }
525
526 - (void)stopWatchingVariables
527 {
528 if ( _watchVariables ) {
529 [_watchTimer invalidate];
530 [_watchTimer release];
531 _watchTimer = nil;
532 [_watchVariables release];
533 _watchVariables = nil;
534 }
535 }
536
537
538 // #############################################################################
539 #pragma mark ThreadedTaskDelegate
540 // #############################################################################
541
542 - (void)threadedTaskFinished:(ThreadedTask *)theTask
543 {
544 id context = [theTask context];
545
546 [self stopWatchingVariables];
547
548 ChazLog( @"threaded task finished" );
549
550 if ( [context isKindOfClass:[SearchContext class]] ) {
551 SearchContext *searchContext = context;
552 TCArray variables;
553 TCArray values;
554
555 if ( _shouldCopy ) {
556 variables = TCArrayCopyElements( searchContext->addresses, _returnLimit );
557 values = TCArrayCopyElements( searchContext->values, _returnLimit );
558 }
559 else {
560 variables = TCArrayCopyContainer( searchContext->addresses, _returnLimit );
561 values = TCArrayCopyContainer( searchContext->values, _returnLimit );
562 }
563
564 [_searchResults addObject:searchContext];
565 [_searchTask release];
566 _searchTask = nil;
567
568 [_delegate cheater:self didFindVariables:variables actualAmount:TCArrayElementCount( searchContext->addresses )];
569 [_delegate cheater:self didFindValues:values];
570 }
571 else if ( [context isKindOfClass:[DumpContext class]] ) {
572 DumpContext *dumpContext = context;
573 [_delegate cheater:self didDumpMemory:dumpContext->dump];
574 [_dumpTask release];
575 _dumpTask = nil;
576 }
577 }
578
579 - (void)threadedTaskCancelled:(ThreadedTask *)theTask
580 {
581 id context = [theTask context];
582 ChazLog( @"threaded task cancelled" );
583
584 if ( [context isKindOfClass:[SearchContext class]] ) {
585 [_delegate cheaterDidCancelSearch:self];
586 [_searchTask release];
587 _searchTask = nil;
588 }
589 else if ( [context isKindOfClass:[DumpContext class]] ) {
590 [_delegate cheaterDidCancelMemoryDump:self];
591 [_dumpTask release];
592 _dumpTask = nil;
593 }
594 }
595
596 - (void)threadedTask:(ThreadedTask *)theTask failedWithErrorCode:(int)errorCode
597 {
598 id context = [theTask context];
599 ChazLog( @"threaded task failed with code: %i", errorCode );
600
601 if ( [context isKindOfClass:[SearchContext class]] ) {
602 [_delegate cheater:self didFailLastRequest:[NSString stringWithFormat:@"Search failed with error: %i", errorCode]];
603 [_searchTask release];
604 _searchTask = nil;
605 }
606 else if ( [context isKindOfClass:[DumpContext class]] ) {
607 [_delegate cheater:self didFailLastRequest:[NSString stringWithFormat:@"Dump failed with error: %i", errorCode]];
608 [_dumpTask release];
609 _dumpTask = nil;
610 }
611 }
612
613 - (void)threadedTask:(ThreadedTask *)theTask reportedProgress:(int)theProgress
614 {
615 [_delegate cheater:self didReportProgress:theProgress];
616 }
617
618
619 // #############################################################################
620 #pragma mark Private Methods
621 // #############################################################################
622
623 - (void)_clearSearch
624 {
625 [self stopWatchingVariables];
626 [self cancelSearch];
627
628 // empty the results array
629 [_searchResults removeAllObjects];
630 [_savedResults removeAllObjects];
631 }
632
633 - (BOOL)_pauseTarget
634 {
635 // attempt to pause
636 if ( VMStopProcess( [_target pid] ) ) {
637 _isTargetPaused = YES;
638 return YES;
639 }
640 _isTargetPaused = NO;
641 return NO;
642 }
643
644 - (BOOL)_resumeTarget
645 {
646 if ( VMContinueProcess( [_target pid] ) ) {
647 _isTargetPaused = NO;
648 return YES;
649 }
650 _isTargetPaused = YES;
651 return NO;
652 }
653
654
655 - (void)_applicationLaunched:(NSNotification *)note
656 {
657 NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
658 NSDictionary *application = [note userInfo];
659 NSString *bundleID;
660
661 // don't allow The Cheat to be cheated
662 bundleID = [application objectForKey:@"NSApplicationBundleIdentifier"];
663 if ( bundleID && [bundleID isEqualToString:[[NSBundle mainBundle] bundleIdentifier]] ) {
664 return;
665 }
666
667 Process *process = [[Process alloc] initWithName:[application objectForKey:@"NSApplicationName"]
668 version:ApplicationVersion( [application objectForKey:@"NSApplicationPath"] )
669 icon:[workspace iconForFile:[application objectForKey:@"NSApplicationPath"]]
670 pid:[[application objectForKey:@"NSApplicationProcessIdentifier"] intValue]];
671 if ( ![_processes containsObject:process] ) {
672 [_processes addObject:process];
673 // inform the delegate of the new process
674 [_delegate cheater:self didAddProcess:process];
675 }
676 // cleanup
677 [process release];
678 }
679
680 - (void)_applicationTerminated:(NSNotification *)note
681 {
682 NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
683 NSDictionary *application = [note userInfo];
684
685 Process *process = [[Process alloc] initWithName:[application objectForKey:@"NSApplicationName"]
686 version:ApplicationVersion( [application objectForKey:@"NSApplicationPath"] )
687 icon:[workspace iconForFile:[application objectForKey:@"NSApplicationPath"]]
688 pid:[[application objectForKey:@"NSApplicationProcessIdentifier"] intValue]];
689
690 // if this is the current process, take appropriate actions
691 if ( [_target isEqual:process] ) {
692 [self cancelSearch];
693 [self clearSearch];
694 [self cancelMemoryDump];
695 [self stopChangingVariables];
696 [_delegate cheater:self didRemoveProcess:process];
697 [_delegate cheater:self didFailLastRequest:@"The target quit."];
698 }
699 else {
700 // inform the delegate of the removed process
701 [_delegate cheater:self didRemoveProcess:process];
702 }
703 [_processes removeObject:process];
704
705 // cleanup
706 [process release];
707 }
708
709
710 - (unsigned)_doChange:(NSArray *)variables
711 {
712 unsigned i, top;
713 unsigned successes = 0;
714
715 top = [variables count];
716 for ( i = 0; i < top; i++ ) {
717 Variable *variable = [variables objectAtIndex:i];
718
719 if ([[variable process] pid] != [_target pid])
720 {
721 [variable setProcess:_target];
722 }
723
724 char buffer[variable->_size];
725 memcpy(buffer, variable->_value, variable->_size);
726 bigEndianValue(buffer, variable);
727
728 if ( VMWriteBytes( [_target pid], [variable address], buffer, [variable valueSize] ) )
729 {
730 successes++;
731 }
732 }
733 return successes;
734 }
735
736 - (void)_changeTimer:(NSTimer *)timer
737 {
738 unsigned changes = [self _doChange:_cheatVariables];
739 [_delegate cheater:self didChangeVariables:changes];
740 }
741
742
743 - (void)_doWatch
744 {
745 unsigned i, top;
746 char value[TC_MAX_VAR_SIZE];
747 mach_vm_size_t size;
748
749 top = [_watchVariables count];
750 for ( i = 0; i < top; i++ ) {
751 Variable *variable = [_watchVariables objectAtIndex:i];
752
753 size = [variable valueSize];
754 if ( VMReadBytes( [_target pid], [variable address], value, &size ) ) {
755 bigEndianValue(value, variable);
756
757 // check if memory changed
758 if (memcmp(value, variable->_value, size) != 0)
759 {
760 [variable setValue:value];
761 // inform delegate of the change
762 [_delegate cheater:self variableAtIndex:_watchRange.location+i didChangeTo:variable];
763 }
764 }
765 }
766 }
767
768 - (void)_watchTimer:(NSTimer *)timer
769 {
770 [self _doWatch];
771 }
772
773
774 @end
This page took 0.060449 seconds and 4 git commands to generate.