|
| 1 | +#import "EIDNativeProbe.h" |
| 2 | +#import "EIDLogger.h" |
| 3 | +#import <UIKit/UIKit.h> |
| 4 | +#import <mach-o/dyld.h> |
| 5 | +#import <mach-o/loader.h> |
| 6 | +#import <mach/mach.h> |
| 7 | +#import <objc/runtime.h> |
| 8 | + |
| 9 | +static const char *kEIDPillSupportedUUID = "F4357753-A25F-30EE-BACF-63709F902895"; |
| 10 | +static const uintptr_t kEIDPillGameGlobalOffset = 0xac3b90; |
| 11 | +static const size_t kEIDPillGameItemPoolOffset = 0x242c0; |
| 12 | +static const size_t kEIDPillEffectsOffset = 0xa2c; |
| 13 | +static const NSInteger kEIDGoldenPillColor = 14; |
| 14 | + |
| 15 | +static BOOL EIDPillReadMemory(vm_address_t address, void *destination, vm_size_t size) { |
| 16 | + if (!address || !destination || !size) return NO; |
| 17 | + vm_size_t copied = 0; |
| 18 | + return vm_read_overwrite(mach_task_self(), address, size, |
| 19 | + (vm_address_t)destination, &copied) == KERN_SUCCESS && copied == size; |
| 20 | +} |
| 21 | + |
| 22 | +static NSString *EIDPillUUIDForHeader(const struct mach_header_64 *header) { |
| 23 | + if (!header || header->magic != MH_MAGIC_64) return nil; |
| 24 | + const uint8_t *cursor = (const uint8_t *)(header + 1); |
| 25 | + const uint8_t *end = cursor + header->sizeofcmds; |
| 26 | + for (uint32_t index = 0; index < header->ncmds; ++index) { |
| 27 | + if (cursor + sizeof(struct load_command) > end) break; |
| 28 | + const struct load_command *command = (const struct load_command *)cursor; |
| 29 | + if (!command->cmdsize || cursor + command->cmdsize > end) break; |
| 30 | + if (command->cmd == LC_UUID && command->cmdsize >= sizeof(struct uuid_command)) { |
| 31 | + const struct uuid_command *uuidCommand = (const struct uuid_command *)cursor; |
| 32 | + const unsigned char *u = uuidCommand->uuid; |
| 33 | + return [NSString stringWithFormat: |
| 34 | + @"%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X", |
| 35 | + 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]]; |
| 36 | + } |
| 37 | + cursor += command->cmdsize; |
| 38 | + } |
| 39 | + return nil; |
| 40 | +} |
| 41 | + |
| 42 | +static const struct mach_header_64 *EIDPillIsaacHeader(void) { |
| 43 | + NSString *wanted = [NSString stringWithUTF8String:kEIDPillSupportedUUID]; |
| 44 | + for (uint32_t index = 0; index < _dyld_image_count(); ++index) { |
| 45 | + const struct mach_header_64 *header = (const struct mach_header_64 *)_dyld_get_image_header(index); |
| 46 | + NSString *uuid = EIDPillUUIDForHeader(header); |
| 47 | + if (uuid && [uuid caseInsensitiveCompare:wanted] == NSOrderedSame) return header; |
| 48 | + } |
| 49 | + return NULL; |
| 50 | +} |
| 51 | + |
| 52 | +static NSArray<NSString *> *EIDPillBundleRoots(void) { |
| 53 | + NSMutableArray<NSString *> *roots = [NSMutableArray array]; |
| 54 | + NSString *main = NSBundle.mainBundle.bundlePath; |
| 55 | + [roots addObjectsFromArray:@[ |
| 56 | + [main stringByAppendingPathComponent:@"Frameworks/IsaacEID.bundle"], |
| 57 | + [main stringByAppendingPathComponent:@"IsaacEID.bundle"], |
| 58 | + [main stringByAppendingPathComponent:@"Frameworks/IsaacExternalItemDescriptions.framework/Resources/IsaacEID.bundle"], |
| 59 | + [main stringByAppendingPathComponent:@"Frameworks/IsaacExternalItemDescriptions.framework/IsaacEID.bundle"], |
| 60 | + [NSHomeDirectory() stringByAppendingPathComponent:@"Library/Application Support/IsaacExternalItemDescriptions"], |
| 61 | + @"/var/jb/Library/Application Support/IsaacExternalItemDescriptions", |
| 62 | + @"/Library/Application Support/IsaacExternalItemDescriptions" |
| 63 | + ]]; |
| 64 | + NSBundle *framework = [NSBundle bundleForClass:NSClassFromString(@"EIDOverlayController") ?: NSObject.class]; |
| 65 | + if (framework.bundlePath.length) { |
| 66 | + [roots addObject:[framework.bundlePath stringByAppendingPathComponent:@"Resources/IsaacEID.bundle"]]; |
| 67 | + [roots addObject:[framework.bundlePath stringByAppendingPathComponent:@"IsaacEID.bundle"]]; |
| 68 | + [roots addObject:framework.bundlePath]; |
| 69 | + } |
| 70 | + return roots; |
| 71 | +} |
| 72 | + |
| 73 | +static NSString *EIDPillBundledResource(NSString *name) { |
| 74 | + for (NSString *root in EIDPillBundleRoots()) { |
| 75 | + NSString *path = [root stringByAppendingPathComponent:name]; |
| 76 | + if ([[NSFileManager defaultManager] isReadableFileAtPath:path]) return path; |
| 77 | + } |
| 78 | + return nil; |
| 79 | +} |
| 80 | + |
| 81 | +@interface EIDPillAnimationParser : NSObject <NSXMLParserDelegate> |
| 82 | +@property(nonatomic, strong) NSMutableArray *pillFrames; |
| 83 | +@property(nonatomic) BOOL readingPills; |
| 84 | +@property(nonatomic) BOOL readingLayer; |
| 85 | +@end |
| 86 | + |
| 87 | +@implementation EIDPillAnimationParser |
| 88 | +- (instancetype)init { |
| 89 | + if ((self = [super init])) _pillFrames = [NSMutableArray array]; |
| 90 | + return self; |
| 91 | +} |
| 92 | +- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName |
| 93 | + namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qualifiedName |
| 94 | + attributes:(NSDictionary<NSString *,NSString *> *)attributes { |
| 95 | + (void)parser; (void)namespaceURI; (void)qualifiedName; |
| 96 | + if ([elementName isEqualToString:@"Animation"]) { |
| 97 | + self.readingPills = [attributes[@"Name"] isEqualToString:@"Pills"]; |
| 98 | + self.readingLayer = NO; |
| 99 | + } else if ([elementName isEqualToString:@"LayerAnimation"] && self.readingPills) { |
| 100 | + self.readingLayer = [attributes[@"LayerId"] integerValue] == 0; |
| 101 | + } else if ([elementName isEqualToString:@"Frame"] && self.readingPills && self.readingLayer) { |
| 102 | + CGFloat x = [attributes[@"XCrop"] doubleValue]; |
| 103 | + CGFloat y = [attributes[@"YCrop"] doubleValue]; |
| 104 | + CGFloat w = [attributes[@"Width"] doubleValue]; |
| 105 | + CGFloat h = [attributes[@"Height"] doubleValue]; |
| 106 | + BOOL visible = ![attributes[@"Visible"] isEqualToString:@"false"]; |
| 107 | + [self.pillFrames addObject:(visible && w > 0 && h > 0) |
| 108 | + ? [NSValue valueWithCGRect:CGRectMake(x, y, w, h)] : NSNull.null]; |
| 109 | + } |
| 110 | +} |
| 111 | +- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName |
| 112 | + namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qualifiedName { |
| 113 | + (void)parser; (void)namespaceURI; (void)qualifiedName; |
| 114 | + if ([elementName isEqualToString:@"LayerAnimation"]) self.readingLayer = NO; |
| 115 | + if ([elementName isEqualToString:@"Animation"]) self.readingPills = NO; |
| 116 | +} |
| 117 | +@end |
| 118 | + |
| 119 | +static UIImage *EIDPillCropImage(UIImage *atlas, id frameValue) { |
| 120 | + if (!atlas.CGImage || ![frameValue isKindOfClass:NSValue.class]) return nil; |
| 121 | + CGRect frame = [frameValue CGRectValue]; |
| 122 | + CGRect bounds = CGRectMake(0, 0, CGImageGetWidth(atlas.CGImage), CGImageGetHeight(atlas.CGImage)); |
| 123 | + if (!CGRectContainsRect(bounds, frame)) return nil; |
| 124 | + CGImageRef cropped = CGImageCreateWithImageInRect(atlas.CGImage, frame); |
| 125 | + if (!cropped) return nil; |
| 126 | + UIImage *image = [UIImage imageWithCGImage:cropped scale:1 orientation:UIImageOrientationUp]; |
| 127 | + CGImageRelease(cropped); |
| 128 | + return image; |
| 129 | +} |
| 130 | + |
| 131 | +@interface EIDOriginalPillArtwork : NSObject |
| 132 | +@property(nonatomic, strong) UIImage *atlas; |
| 133 | +@property(nonatomic, copy) NSArray *frames; |
| 134 | +@property(nonatomic, strong) NSMutableDictionary<NSString *, id> *cache; |
| 135 | ++ (instancetype)shared; |
| 136 | +- (UIImage *)imageForEffectSubtype:(NSInteger)subtype horse:(BOOL)horse; |
| 137 | +@end |
| 138 | + |
| 139 | +@implementation EIDOriginalPillArtwork |
| 140 | ++ (instancetype)shared { |
| 141 | + static EIDOriginalPillArtwork *resolver; |
| 142 | + static dispatch_once_t once; |
| 143 | + dispatch_once(&once, ^{ resolver = [EIDOriginalPillArtwork new]; }); |
| 144 | + return resolver; |
| 145 | +} |
| 146 | +- (instancetype)init { |
| 147 | + if (!(self = [super init])) return nil; |
| 148 | + _cache = [NSMutableDictionary dictionary]; |
| 149 | + NSString *png = EIDPillBundledResource(@"eid_cardspills.png"); |
| 150 | + NSString *anm2 = EIDPillBundledResource(@"eid_cardspills.anm2"); |
| 151 | + _atlas = png.length ? [UIImage imageWithContentsOfFile:png] : nil; |
| 152 | + NSData *data = anm2.length ? [NSData dataWithContentsOfFile:anm2] : nil; |
| 153 | + if (data.length) { |
| 154 | + EIDPillAnimationParser *delegate = [EIDPillAnimationParser new]; |
| 155 | + NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data]; |
| 156 | + parser.delegate = delegate; |
| 157 | + if ([parser parse]) _frames = delegate.pillFrames.copy; |
| 158 | + } |
| 159 | + if (!_frames) _frames = @[]; |
| 160 | + EIDLog(@"original EID pill artwork: atlas %@, frames %lu", |
| 161 | + _atlas ? @"yes" : @"no", (unsigned long)_frames.count); |
| 162 | + return self; |
| 163 | +} |
| 164 | +- (NSInteger)colorForEffectSubtype:(NSInteger)subtype { |
| 165 | + if (subtype == 9999) return kEIDGoldenPillColor; |
| 166 | + NSInteger effect = subtype - 1; |
| 167 | + if (effect < 0 || effect > 63) return 0; |
| 168 | + const struct mach_header_64 *header = EIDPillIsaacHeader(); |
| 169 | + if (!header) return 0; |
| 170 | + uintptr_t game = 0; |
| 171 | + if (!EIDPillReadMemory((vm_address_t)header + kEIDPillGameGlobalOffset, |
| 172 | + &game, sizeof(game)) || !game) return 0; |
| 173 | + uintptr_t itemPool = game + kEIDPillGameItemPoolOffset; |
| 174 | + for (NSInteger color = 1; color < kEIDGoldenPillColor; ++color) { |
| 175 | + int32_t mappedEffect = -1; |
| 176 | + if (!EIDPillReadMemory(itemPool + kEIDPillEffectsOffset + color * sizeof(mappedEffect), |
| 177 | + &mappedEffect, sizeof(mappedEffect))) continue; |
| 178 | + if (mappedEffect == effect) return color; |
| 179 | + } |
| 180 | + return 0; |
| 181 | +} |
| 182 | +- (UIImage *)imageForEffectSubtype:(NSInteger)subtype horse:(BOOL)horse { |
| 183 | + NSInteger color = [self colorForEffectSubtype:subtype]; |
| 184 | + if (color <= 0) return nil; |
| 185 | + NSString *key = [NSString stringWithFormat:@"%ld:%d", (long)color, horse]; |
| 186 | + id cached = self.cache[key]; |
| 187 | + if (cached) return cached == NSNull.null ? nil : cached; |
| 188 | + |
| 189 | + // Original EID's Pills animation has exactly 14 visual frames corresponding |
| 190 | + // to Isaac PillColor values 1...14. This is animation-frame mapping, not a |
| 191 | + // guessed PNG grid index: color 1 -> animation frame 0, ... color 14 -> 13. |
| 192 | + NSInteger frameIndex = color - 1; |
| 193 | + UIImage *image = nil; |
| 194 | + if (frameIndex >= 0 && frameIndex < (NSInteger)self.frames.count) { |
| 195 | + image = EIDPillCropImage(self.atlas, self.frames[(NSUInteger)frameIndex]); |
| 196 | + } |
| 197 | + if (!image) EIDLog(@"no original EID pill frame for color %ld", (long)color); |
| 198 | + self.cache[key] = image ?: NSNull.null; |
| 199 | + return image; |
| 200 | +} |
| 201 | +@end |
| 202 | + |
| 203 | +@interface NSObject (EIDPillFrameFix) |
| 204 | +- (UIImage *)eid_original_pill_pocketIconForVariant:(NSInteger)variant subtype:(NSInteger)subtype; |
| 205 | +@end |
| 206 | + |
| 207 | +@implementation NSObject (EIDPillFrameFix) |
| 208 | +- (UIImage *)eid_original_pill_pocketIconForVariant:(NSInteger)variant subtype:(NSInteger)subtype { |
| 209 | + if (variant == EIDPickupVariantPill || variant == EIDPickupVariantHorsePill) { |
| 210 | + UIImage *image = [[EIDOriginalPillArtwork shared] imageForEffectSubtype:subtype |
| 211 | + horse:(variant == EIDPickupVariantHorsePill)]; |
| 212 | + if (image) return image; |
| 213 | + } |
| 214 | + return [self eid_original_pill_pocketIconForVariant:variant subtype:subtype]; |
| 215 | +} |
| 216 | +@end |
| 217 | + |
| 218 | +__attribute__((constructor)) static void EIDInstallOriginalPillArtwork(void) { |
| 219 | + dispatch_async(dispatch_get_main_queue(), ^{ |
| 220 | + Class cls = NSClassFromString(@"EIDOverlayController"); |
| 221 | + if (!cls) return; |
| 222 | + SEL replacementSEL = @selector(eid_original_pill_pocketIconForVariant:subtype:); |
| 223 | + Method source = class_getInstanceMethod(NSObject.class, replacementSEL); |
| 224 | + if (!source) return; |
| 225 | + class_addMethod(cls, replacementSEL, method_getImplementation(source), method_getTypeEncoding(source)); |
| 226 | + Method original = class_getInstanceMethod(cls, NSSelectorFromString(@"pocketIconForVariant:subtype:")); |
| 227 | + Method replacement = class_getInstanceMethod(cls, replacementSEL); |
| 228 | + if (original && replacement) method_exchangeImplementations(original, replacement); |
| 229 | + }); |
| 230 | +} |
0 commit comments