SameBoy | Accurate GB/GBC emulator |
| download: https://git.y1.nz/archives/sameboy.tar.gz | |
| README | Files | Log | Refs | LICENSE |
commit 4c10ba4cd383f2f3cc46fb590ab95ec96f425a35 parent 526b7130b1564b777641199407593e94b4c25ea1 Author: Lior Halphon <LIJI32@gmail.com> Date: Mon, 2 Mar 2026 21:48:13 +0200 Handle state/battery conflicts Diffstat:
| M | iOS/GBViewController.m | 235 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- |
| A | iOS/UILabel+LockFonts.h | 5 | +++++ |
| A | iOS/UILabel+LockFonts.m | 29 | +++++++++++++++++++++++++++++ |
3 files changed, 267 insertions(+), 2 deletions(-)
diff --git a/iOS/GBViewController.m b/iOS/GBViewController.m @@ -16,6 +16,9 @@ #import "GBCheckableAlertController.h" #import "GBPrinterFeedController.h" #import "GBCheatsController.h" +#import "UILabel+LockFonts.h" +#import <CommonCrypto/CommonCrypto.h> +#include <sys/xattr.h> #import "GCControllerGetElements.h" #import "GBZipReader.h" #import <sys/stat.h> @@ -1355,7 +1358,6 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) return (_verticalLayout.theme.isDark || _backgroundView.fullScreenMode)? UIStatusBarStyleLightContent : UIStatusBarStyleDefault; } - - (void)preRun { dispatch_async(dispatch_get_main_queue(), ^{ @@ -1450,8 +1452,226 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) } } +- (NSString *)formatDate:(NSDate *)date +{ + + NSCalendar *calendar = [NSCalendar currentCalendar]; + NSDate *now = [NSDate date]; + + NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; + formatter.locale = [NSLocale currentLocale]; + formatter.timeZone = [NSTimeZone localTimeZone]; + formatter.dateStyle = NSDateFormatterNoStyle; + formatter.timeStyle = NSDateFormatterMediumStyle; + NSString *time = [formatter stringFromDate:date]; + + if ([calendar isDate:date inSameDayAsDate:now]) { + return time; + } + + if ([calendar component:NSCalendarUnitYear fromDate:date] == [calendar component:NSCalendarUnitYear fromDate:now]) { + formatter.dateFormat = [NSDateFormatter dateFormatFromTemplate:@"MMM d" + options:0 + locale:formatter.locale]; + return [[formatter stringFromDate:date] stringByAppendingFormat:@", %@", time]; + } + + formatter.dateFormat = [NSDateFormatter dateFormatFromTemplate:@"MMM d yyyy" + options:0 + locale:formatter.locale]; + return [[formatter stringFromDate:date] stringByAppendingFormat:@", %@", time]; +} + +- (NSData *)batteryHash +{ + NSFileHandle *file = [NSFileHandle fileHandleForReadingAtPath:[GBROMManager sharedManager].batterySaveFile]; + if (!file) return nil; + + CC_SHA256_CTX ctx; + CC_SHA256_Init(&ctx); + + while (true) { + @autoreleasepool { + NSData *chunk = [file readDataOfLength:0x4000]; + if (chunk.length == 0) break; + CC_SHA256_Update(&ctx, chunk.bytes, (CC_LONG)chunk.length); + } + } + [file closeFile]; + + unsigned char digest[CC_SHA256_DIGEST_LENGTH]; + CC_SHA256_Final(digest, &ctx); + + return [NSData dataWithBytes:digest length:CC_SHA256_DIGEST_LENGTH]; +} + +- (bool)areSavesInSync +{ + if (!GBROMManager.sharedManager.currentROM) return true; // No ROM + NSString *saveState = [[GBROMManager sharedManager] autosaveStateFile]; + NSString *batterySave = [[GBROMManager sharedManager] batterySaveFile]; + + // There can't be a mismatch if one of the files is missing + if (access(saveState.UTF8String, F_OK) || access(batterySave.UTF8String, F_OK)) return true; + + uint8_t savedHash[CC_SHA256_DIGEST_LENGTH]; + if (getxattr(saveState.UTF8String, "battery-hash", &savedHash, sizeof(savedHash), 0, 0) == sizeof(savedHash)) { + NSData *computedHash = [self batteryHash]; + if (computedHash && memcmp(savedHash, computedHash.bytes, sizeof(savedHash)) == 0) { + return true; // Battery didn't change + } + } + + GB_gameboy_t *gb = NULL; + GB_model_t model; + if (GB_get_state_model(saveState.UTF8String, &model)) { + return true; // Not a valid state, ignore + } + gb = GB_init(GB_alloc(), model); + NSString *romFile = [[GBROMManager sharedManager] romFile]; + if ([romFile.pathExtension.lowercaseString isEqualToString:@"isx"]) { + if (GB_load_isx(gb, romFile.fileSystemRepresentation)) { + // Can't load ROM + GB_dealloc(gb); + return true; + } + } + else { + if (GB_load_rom(gb, romFile.fileSystemRepresentation)) { + // Can't load ROM + GB_dealloc(gb); + return true; + } + } + if (GB_load_state(gb, saveState.UTF8String)) { + // Not a valid state again, ignore + GB_dealloc(gb); + return true; + } + + size_t ramSize = 0; + const uint8_t *ram = GB_get_direct_access(gb, GB_DIRECT_ACCESS_CART_RAM, &ramSize, NULL); + uint8_t *ramCopy = malloc(ramSize); + memcpy(ramCopy, ram, ramSize); + + GB_load_battery(gb, batterySave.UTF8String); + ram = GB_get_direct_access(gb, GB_DIRECT_ACCESS_CART_RAM, &ramSize, NULL); + bool matching = memcmp(ram, ramCopy, ramSize) == 0; + free(ramCopy); + GB_dealloc(gb); + return matching; +} + +- (bool)verifySavesInSync +{ + if ([self areSavesInSync]) return true; // Saves are in sync + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + __block bool ret = false; + dispatch_async(dispatch_get_main_queue(), ^{ + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Save Conflict Detected" + message:[NSString stringWithFormat: @"“%@” has both an automatic save state and a conflicting battery save file. The battery save may have newer progress that isn’t in the save state. Which one would you like to keep?", [GBROMManager sharedManager].currentROM.lastPathComponent] + preferredStyle:UIAlertControllerStyleAlert]; + + NSString *saveState = [[GBROMManager sharedManager] autosaveStateFile]; + NSString *batterySave = [[GBROMManager sharedManager] batterySaveFile]; + + NSDate *date; + [[NSURL fileURLWithPath:saveState] getResourceValue:&date + forKey:NSURLContentModificationDateKey + error:nil]; + + NSString *stateDateString = [self formatDate:date]; + + UIAlertAction *stateAction = [UIAlertAction actionWithTitle:@"Save State\n" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + unlink(batterySave.UTF8String); + ret = true; + dispatch_semaphore_signal(sem); + }]; + + [[NSURL fileURLWithPath:batterySave] getResourceValue:&date + forKey:NSURLContentModificationDateKey + error:nil]; + + NSString *batteryDateString = [self formatDate:date]; + + UIAlertAction *batteryAction = [UIAlertAction actionWithTitle:@"Battery Save\n" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + unlink(saveState.UTF8String); + ret = true; + dispatch_semaphore_signal(sem); + }]; + [alert addAction:stateAction]; + [alert addAction:batteryAction]; + [alert addAction:[UIAlertAction actionWithTitle:@"Close ROM" + style:UIAlertActionStyleCancel + handler:^(UIAlertAction *action) { + ret = false; + dispatch_semaphore_signal(sem); + }]]; + + // hack + _running = false; + [self presentViewController:alert animated:true completion:nil]; + dispatch_async(dispatch_get_main_queue(), ^{ + NSMutableSet<UIView *> *views = [NSMutableSet setWithObject:alert.view]; + + UIColor *secondaryLabelColor; + if (@available(iOS 13.0, *)) { + secondaryLabelColor = [UIColor secondaryLabelColor]; + } + else { + secondaryLabelColor = [UIColor systemGrayColor]; + } + + UILabel *view; + while ((view = (UILabel *)views.anyObject)) { + [views removeObject:view]; + [views addObjectsFromArray:view.subviews]; + if ([view isKindOfClass:[UILabel class]]) { + if ([view.text isEqualToString:stateAction.title]) { + NSMutableAttributedString *text = [view.attributedText mutableCopy]; + [text appendAttributedString:[[NSAttributedString alloc] initWithString:stateDateString + attributes:@{ + NSFontAttributeName: [UIFont systemFontOfSize:UIFont.smallSystemFontSize], + NSForegroundColorAttributeName: secondaryLabelColor, + }]]; + view.attributedText = text; + view.numberOfLines = 2; + view.locksFonts = true; + } + if ([view.text isEqualToString:batteryAction.title]) { + NSMutableAttributedString *text = [view.attributedText mutableCopy]; + [text appendAttributedString:[[NSAttributedString alloc] initWithString:batteryDateString + attributes:@{ + NSFontAttributeName: [UIFont systemFontOfSize:UIFont.smallSystemFontSize], + NSForegroundColorAttributeName: secondaryLabelColor, + }]]; + view.attributedText = text; + view.numberOfLines = 2; + view.locksFonts = true; + } + } + } + }); + }); + dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); + _running = true; + return ret; +} + - (void)run { + if (![self verifySavesInSync]) { + _romLoaded = false; + _running = false; + _stopping = false; + GBROMManager.sharedManager.currentROM = nil; + return; + } + [self loadROM]; if (!_romLoaded) { _running = false; @@ -1556,7 +1776,18 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response _audioClient = nil; if (!_swappingROM) { - [self preformAutosave]; + GB_save_battery(&_gb, [GBROMManager sharedManager].batterySaveFile.fileSystemRepresentation); + [self saveStateToFile:[GBROMManager sharedManager].autosaveStateFile]; + + // Assoicate the battery save with the save state via a hash xattr + NSData *batteryHash = [self batteryHash]; + if (batteryHash) { + setxattr([GBROMManager sharedManager].autosaveStateFile.UTF8String, "battery-hash", + batteryHash.bytes, batteryHash.length, 0, 0); + } + else { + removexattr([GBROMManager sharedManager].autosaveStateFile.UTF8String, "battery-hash", 0); + } NSDate *date; [[NSURL fileURLWithPath:[GBROMManager sharedManager].autosaveStateFile] getResourceValue:&date diff --git a/iOS/UILabel+LockFonts.h b/iOS/UILabel+LockFonts.h @@ -0,0 +1,5 @@ +#import <UIKit/UIKit.h> + +@interface UILabel (LockFonts) +@property bool locksFonts; +@end diff --git a/iOS/UILabel+LockFonts.m b/iOS/UILabel+LockFonts.m @@ -0,0 +1,29 @@ +#import "UILabel+LockFonts.h" +#import <objc/runtime.h> + +@implementation UILabel (LockFonts) + +- (void)setLocksFonts:(bool)locksFont +{ + objc_setAssociatedObject(self, @selector(locksFonts), @(locksFont), OBJC_ASSOCIATION_RETAIN); +} + +- (bool)locksFonts +{ + return [objc_getAssociatedObject(self, _cmd) boolValue]; +} + +- (void)setFontHook:(UIFont *)font +{ + if (self.locksFonts) return; + [self setFontHook:font]; +} + ++ (void)load +{ + method_exchangeImplementations(class_getInstanceMethod(self, @selector(setFontHook:)), + class_getInstanceMethod(self, @selector(setFont:))); + +} + +@end
This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.