Skip to content

Commit d2c78d7

Browse files
committed
Use original EID pill animation frames
1 parent 3fc580f commit d2c78d7

1 file changed

Lines changed: 230 additions & 0 deletions

File tree

src/EIDPillFrameFix.m

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
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

Comments
 (0)