Skip to content

Commit 7dded8e

Browse files
committed
Reset transformation progress on new run identity
1 parent 305559a commit 7dded8e

1 file changed

Lines changed: 137 additions & 0 deletions

File tree

src/EIDRunResetFix.m

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#import "EIDLogger.h"
2+
#import <Foundation/Foundation.h>
3+
#import <mach-o/dyld.h>
4+
#import <mach-o/loader.h>
5+
#import <mach/mach.h>
6+
#import <objc/message.h>
7+
#import <objc/runtime.h>
8+
9+
static const char *kEIDRunResetSupportedUUID = "F4357753-A25F-30EE-BACF-63709F902895";
10+
static const uintptr_t kEIDRunResetGameGlobalOffset = 0xac3b90;
11+
12+
struct EIDRemotePointerVector {
13+
uintptr_t begin;
14+
uintptr_t end;
15+
uintptr_t capacity;
16+
};
17+
18+
static BOOL EIDRunResetRead(vm_address_t address, void *destination, vm_size_t size) {
19+
if (!address || !destination || !size) return NO;
20+
vm_size_t copied = 0;
21+
return vm_read_overwrite(mach_task_self(), address, size,
22+
(vm_address_t)destination, &copied) == KERN_SUCCESS && copied == size;
23+
}
24+
25+
static NSString *EIDRunResetUUIDForHeader(const struct mach_header_64 *header) {
26+
if (!header || header->magic != MH_MAGIC_64) return nil;
27+
const uint8_t *cursor = (const uint8_t *)(header + 1);
28+
const uint8_t *end = cursor + header->sizeofcmds;
29+
for (uint32_t index = 0; index < header->ncmds; ++index) {
30+
if (cursor + sizeof(struct load_command) > end) break;
31+
const struct load_command *command = (const struct load_command *)cursor;
32+
if (!command->cmdsize || cursor + command->cmdsize > end) break;
33+
if (command->cmd == LC_UUID && command->cmdsize >= sizeof(struct uuid_command)) {
34+
const struct uuid_command *uuidCommand = (const struct uuid_command *)cursor;
35+
const unsigned char *u = uuidCommand->uuid;
36+
return [NSString stringWithFormat:
37+
@"%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X",
38+
u[0],u[1],u[2],u[3],u[4],u[5],u[6],u[7],u[8],u[9],u[10],u[11],u[12],u[13],u[14],u[15]];
39+
}
40+
cursor += command->cmdsize;
41+
}
42+
return nil;
43+
}
44+
45+
static const struct mach_header_64 *EIDRunResetIsaacHeader(void) {
46+
NSString *wanted = [NSString stringWithUTF8String:kEIDRunResetSupportedUUID];
47+
for (uint32_t index = 0; index < _dyld_image_count(); ++index) {
48+
const struct mach_header_64 *header = (const struct mach_header_64 *)_dyld_get_image_header(index);
49+
NSString *uuid = EIDRunResetUUIDForHeader(header);
50+
if (uuid && [uuid caseInsensitiveCompare:wanted] == NSOrderedSame) return header;
51+
}
52+
return NULL;
53+
}
54+
55+
static uintptr_t EIDCurrentRunIdentity(id probe) {
56+
if (!probe) return 0;
57+
NSNumber *offsetNumber = nil;
58+
@try { offsetNumber = [probe valueForKey:@"playerVectorOffset"]; }
59+
@catch (__unused NSException *exception) { return 0; }
60+
if (![offsetNumber isKindOfClass:NSNumber.class]) return 0;
61+
NSUInteger playerVectorOffset = offsetNumber.unsignedIntegerValue;
62+
if (playerVectorOffset == NSUIntegerMax) return 0;
63+
64+
const struct mach_header_64 *header = EIDRunResetIsaacHeader();
65+
if (!header) return 0;
66+
uintptr_t game = 0;
67+
if (!EIDRunResetRead((vm_address_t)header + kEIDRunResetGameGlobalOffset,
68+
&game, sizeof(game)) || !game) return 0;
69+
70+
EIDRemotePointerVector vector = {};
71+
if (!EIDRunResetRead(game + playerVectorOffset, &vector, sizeof(vector))) return 0;
72+
if (!vector.begin || vector.end <= vector.begin || vector.capacity < vector.end) return 0;
73+
if ((vector.end - vector.begin) % sizeof(uintptr_t) != 0) return 0;
74+
75+
uintptr_t firstPlayer = 0;
76+
if (!EIDRunResetRead(vector.begin, &firstPlayer, sizeof(firstPlayer))) return 0;
77+
return firstPlayer;
78+
}
79+
80+
static void EIDResetTransformationTracker(NSString *reason) {
81+
Class trackerClass = NSClassFromString(@"EIDTransformationProgress");
82+
SEL sharedSEL = NSSelectorFromString(@"shared");
83+
SEL resetSEL = NSSelectorFromString(@"resetForNewRun");
84+
if (!trackerClass || ![trackerClass respondsToSelector:sharedSEL]) return;
85+
id (*sendShared)(id, SEL) = (id (*)(id, SEL))objc_msgSend;
86+
id tracker = sendShared(trackerClass, sharedSEL);
87+
if (!tracker || ![tracker respondsToSelector:resetSEL]) return;
88+
void (*sendReset)(id, SEL) = (void (*)(id, SEL))objc_msgSend;
89+
sendReset(tracker, resetSEL);
90+
EIDLog(@"transformation progress reset: %@", reason);
91+
}
92+
93+
@interface NSObject (EIDRunResetFix)
94+
- (void)eid_runreset_updateMenuModeForGameplay:(BOOL)gameplayActive;
95+
@end
96+
97+
@implementation NSObject (EIDRunResetFix)
98+
- (void)eid_runreset_updateMenuModeForGameplay:(BOOL)gameplayActive {
99+
static uintptr_t lastRunIdentity = 0;
100+
static BOOL sawInactiveSinceRun = YES;
101+
102+
if (!gameplayActive) {
103+
sawInactiveSinceRun = YES;
104+
} else {
105+
id probe = nil;
106+
@try { probe = [self valueForKey:@"probe"]; }
107+
@catch (__unused NSException *exception) {}
108+
uintptr_t identity = EIDCurrentRunIdentity(probe);
109+
if (identity) {
110+
BOOL changedIdentity = lastRunIdentity && identity != lastRunIdentity;
111+
BOOL restartedAfterInactive = sawInactiveSinceRun && lastRunIdentity != 0;
112+
if (changedIdentity || restartedAfterInactive) {
113+
EIDResetTransformationTracker(changedIdentity ? @"native run identity changed"
114+
: @"gameplay restarted after menu/death");
115+
}
116+
lastRunIdentity = identity;
117+
sawInactiveSinceRun = NO;
118+
}
119+
}
120+
121+
[self eid_runreset_updateMenuModeForGameplay:gameplayActive];
122+
}
123+
@end
124+
125+
__attribute__((constructor)) static void EIDInstallRunResetFix(void) {
126+
dispatch_async(dispatch_get_main_queue(), ^{
127+
Class cls = NSClassFromString(@"EIDOverlayController");
128+
if (!cls) return;
129+
SEL replacementSEL = @selector(eid_runreset_updateMenuModeForGameplay:);
130+
Method source = class_getInstanceMethod(NSObject.class, replacementSEL);
131+
if (!source) return;
132+
class_addMethod(cls, replacementSEL, method_getImplementation(source), method_getTypeEncoding(source));
133+
Method original = class_getInstanceMethod(cls, NSSelectorFromString(@"updateMenuModeForGameplay:"));
134+
Method replacement = class_getInstanceMethod(cls, replacementSEL);
135+
if (original && replacement) method_exchangeImplementations(original, replacement);
136+
});
137+
}

0 commit comments

Comments
 (0)