SameBoy | Accurate GB/GBC emulator |
| download: https://git.y1.nz/archives/sameboy.tar.gz | |
| README | Files | Log | Refs | LICENSE |
iOS/GBCheatsController.m
1 #import "GBCheatsController.h"
2 #import "GBROMManager.h"
3 #import "UIToolbar+disableCompact.h"
4 #import <CoreServices/CoreServices.h>
5 #import <objc/runtime.h>
6
7 @interface GBCheatsController()<UIDocumentPickerDelegate>
8 @end
9
10 @implementation GBCheatsController
11 {
12 GB_gameboy_t *_gb;
13 NSIndexPath *_renamingPath;
14 __weak UITextField *_editingField;
15 }
16
17 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
18 {
19 return 2;
20 }
21
22 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
23 {
24 if (section == 0) return 1;
25 size_t count;
26 GB_get_cheats(_gb, &count);
27 self.toolbarItems[0].enabled = count;
28 ((UIButton *)(self.toolbarItems[0].customView.subviews[0])).enabled = count;
29 return count;
30 }
31
32
33 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
34 {
35
36 UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:nil];
37
38 UISwitch *button = [[UISwitch alloc] init];
39 cell.accessoryView = button;
40 const GB_cheat_t *cheat = NULL;
41 if (indexPath.section == 0) {
42 button.on = GB_cheats_enabled(_gb);
43 cell.textLabel.text = @"Enable Cheats";
44 }
45 else {
46 cheat = GB_get_cheats(_gb, NULL)[indexPath.row];
47 button.on = cheat->enabled;
48 cell.textLabel.text = @(cheat->description) ?: @"Unnamed Cheat";
49 button.enabled = GB_cheats_enabled(_gb);
50 }
51
52 id block = ^(){
53 if (!cheat) {
54 GB_set_cheats_enabled(_gb, button.on);
55 [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationNone];
56 }
57 else {
58 GB_update_cheat(_gb, cheat, cheat->description,
59 cheat->address, cheat->bank,
60 cheat->value, cheat->old_value, cheat->use_old_value,
61 button.on);
62 }
63 };
64 objc_setAssociatedObject(cell, "RetainedBlock", block, OBJC_ASSOCIATION_RETAIN);
65
66 [button addTarget:block action:@selector(invoke) forControlEvents:UIControlEventValueChanged];
67 cell.selectionStyle = UITableViewCellSelectionStyleNone;
68
69 return cell;
70 }
71
72 - (void)addCheat
73 {
74 [self setEditing:false animated:true];
75 UIAlertController *alertController = [UIAlertController alertControllerWithTitle: @"Add Cheat"
76 message: @"Add a GameShark or Game Genie cheat code"
77 preferredStyle:UIAlertControllerStyleAlert];
78 [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
79 textField.placeholder = @"Description";
80 textField.clearButtonMode = UITextFieldViewModeWhileEditing;
81 }];
82 [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
83 textField.placeholder = @"Cheat Code";
84 textField.clearButtonMode = UITextFieldViewModeWhileEditing;
85 textField.keyboardType = UIKeyboardTypeASCIICapable;
86 }];
87 [alertController addAction:[UIAlertAction actionWithTitle:@"Add" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
88 size_t index = [self tableView:self.tableView numberOfRowsInSection:1];
89 NSString *name = alertController.textFields[0].text;
90 if (GB_import_cheat(_gb, alertController.textFields[1].text.UTF8String, name.length? name.UTF8String : "Unnamed Cheat", true)) {
91 [self.tableView insertRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:index inSection:1]] withRowAnimation:UITableViewRowAnimationAutomatic];
92 }
93 else {
94 alertController.title = @"Invalid cheat code entered";
95 [self presentViewController:alertController animated:true completion:nil];
96 }
97 }]];
98 [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
99 [self presentViewController:alertController animated:true completion:nil];
100 }
101
102 + (UIBarButtonItem *)buttonWithLabel:(NSString *)label
103 imageWithName:(NSString *)imageName
104 target:(id)target
105 action:(SEL)action
106 {
107 if (@available(iOS 13.0, *)) {
108 UIImage *image = [UIImage systemImageNamed:imageName
109 withConfiguration:[UIImageSymbolConfiguration configurationWithScale:UIImageSymbolScaleLarge]];
110 UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
111 [button setImage:image forState:UIControlStateNormal];
112 button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
113 [button addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
114 if (label) {
115 [button setTitle:label forState:UIControlStateNormal];
116 [button setTitleColor:button.tintColor forState:UIControlStateNormal];
117 button.titleEdgeInsets = UIEdgeInsetsMake(0, 4, 0, 0);
118 button.contentEdgeInsets = UIEdgeInsetsMake(0, 12, 0, 0);
119 }
120 [button sizeToFit];
121 CGRect frame = button.frame;
122 frame.size.width = ceil(frame.size.width + (label? 4 : 0));
123 if (@available(iOS 19.0, *)) {
124 if (label) {
125 frame.size.width += 12;
126 }
127 }
128 frame.size.height = 28;
129 button.frame = frame;
130 UIView *wrapper = [[UIView alloc] initWithFrame:button.bounds];
131 [wrapper addSubview:button];
132 UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithCustomView:wrapper];
133 return item;
134 }
135 return [[UIBarButtonItem alloc] initWithTitle:label style:UIBarButtonItemStylePlain target:target action:action];
136 }
137
138 - (void)importCheats
139 {
140 [self setEditing:false animated:true];
141 NSString *chtUTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)@"cht", NULL);
142
143
144 UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[chtUTI]
145 inMode:UIDocumentPickerModeImport];
146 if (@available(iOS 13.0, *)) {
147 picker.shouldShowFileExtensions = true;
148 }
149 picker.delegate = self;
150 [self presentViewController:picker animated:true completion:nil];
151
152 return;
153
154 }
155
156 - (void)exportCheats
157 {
158 [self setEditing:false animated:true];
159 NSString *cheatsFile = [[GBROMManager sharedManager] cheatsFile];
160 GB_save_cheats(_gb, cheatsFile.UTF8String);
161 NSURL *url = [NSURL fileURLWithPath:cheatsFile];
162 UIActivityViewController *controller = [[UIActivityViewController alloc] initWithActivityItems:@[url]
163 applicationActivities:nil];
164
165 controller.popoverPresentationController.barButtonItem = self.toolbarItems.firstObject;
166
167 [self presentViewController:controller
168 animated:true
169 completion:nil];
170
171 }
172
173 - (instancetype)initWithGameBoy:(GB_gameboy_t *)gb
174 {
175 UITableViewStyle style = UITableViewStyleGrouped;
176 if (@available(iOS 13.0, *)) {
177 style = UITableViewStyleInsetGrouped;
178 }
179 self = [super initWithStyle:style];
180 self.tableView.allowsSelectionDuringEditing = true;
181 self.navigationItem.rightBarButtonItem = self.editButtonItem;
182
183 bool hasSFSymbols = false;
184 if (@available(iOS 13.0, *)) {
185 hasSFSymbols = true;
186 }
187
188 UIBarButtonItem *export = hasSFSymbols?
189 [self.class buttonWithLabel:nil
190 imageWithName:@"square.and.arrow.up"
191 target:self
192 action:@selector(exportCheats)] :
193 [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction
194 target:self
195 action:@selector(exportCheats)];
196
197 UIBarButtonItem *flexItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
198 target:nil
199 action:NULL];
200 UIBarButtonItem *import = [self.class buttonWithLabel:@"Import"
201 imageWithName:@"square.and.arrow.down"
202 target:self
203 action:@selector(importCheats)];
204
205 UIBarButtonItem *add = [self.class buttonWithLabel:@"Add"
206 imageWithName:@"plus"
207 target:self
208 action:@selector(addCheat)];
209
210 if (@available(iOS 19.0, *)) {
211 self.toolbarItems = @[export,
212 flexItem,
213 import, [UIBarButtonItem fixedSpaceItemOfWidth:0], add];
214 }
215 else {
216 self.toolbarItems = @[export,
217 flexItem,
218 import, add];
219 }
220
221 _gb = gb;
222 return self;
223 }
224
225 - (NSString *)title
226 {
227 return @"Cheats";
228 }
229
230 - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
231 {
232 return indexPath.section == 1;
233 }
234
235 - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
236 {
237 if (indexPath.section != 1) return;
238 if (editingStyle != UITableViewCellEditingStyleDelete) return;
239
240 const GB_cheat_t *cheat = GB_get_cheats(_gb, NULL)[indexPath.row];
241 GB_remove_cheat(_gb, cheat);
242 [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
243 }
244
245 - (void)viewWillAppear:(BOOL)animated
246 {
247 [super viewWillAppear:animated];
248 [self.navigationController setToolbarHidden:false animated:false];
249 self.navigationController.toolbar.disableCompactLayout = true;
250 }
251
252
253 - (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url
254 {
255 [url startAccessingSecurityScopedResource];
256 NSString *tempDir = NSTemporaryDirectory();
257 NSString *newPath = [tempDir stringByAppendingPathComponent:@"import.cht"];
258 [[NSFileManager defaultManager] copyItemAtPath:url.path toPath:newPath error:nil];
259 [url stopAccessingSecurityScopedResource];
260 unsigned count = [self tableView:self.tableView numberOfRowsInSection:1];
261
262 void (^load)(bool) = ^(bool replace) {
263 if (GB_load_cheats(_gb, newPath.UTF8String, replace)) {
264 [[NSFileManager defaultManager] removeItemAtPath:newPath error:nil];
265 UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Import Failed"
266 message:@"The imported cheats file is invalid."
267 preferredStyle:UIAlertControllerStyleAlert];
268 [alertController addAction:[UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:nil]];
269 [self presentViewController:alertController animated:true completion:nil];
270 return;
271 }
272
273 [[NSFileManager defaultManager] removeItemAtPath:newPath error:nil];
274 unsigned newCount = [self tableView:self.tableView numberOfRowsInSection:1];
275 if (!replace) {
276 NSMutableArray *paths = [NSMutableArray arrayWithCapacity:newCount - count];
277 for (unsigned i = count; i < newCount; i++) {
278 [paths addObject:[NSIndexPath indexPathForRow:i inSection:1]];
279 }
280 if (paths.count) {
281 [self.tableView insertRowsAtIndexPaths:paths withRowAnimation:UITableViewRowAnimationAutomatic];
282 }
283 }
284 else {
285 NSMutableArray *paths = [NSMutableArray arrayWithCapacity:abs((signed)newCount - (signed)count)];
286 for (unsigned i = MIN(newCount, count); i < count || i < newCount; i++) {
287 [paths addObject:[NSIndexPath indexPathForRow:i inSection:1]];
288 }
289 if (newCount > count) {
290 [self.tableView insertRowsAtIndexPaths:paths withRowAnimation:UITableViewRowAnimationAutomatic];
291 }
292 else {
293 [self.tableView deleteRowsAtIndexPaths:paths withRowAnimation:UITableViewRowAnimationAutomatic];
294 }
295
296 paths = [NSMutableArray arrayWithCapacity:MIN(newCount, count)];
297 for (unsigned i = 0; i < count && i < newCount; i++) {
298 [paths addObject:[NSIndexPath indexPathForRow:i inSection:1]];
299 }
300 [self.tableView reloadRowsAtIndexPaths:paths withRowAnimation:UITableViewRowAnimationRight];
301 }
302 };
303
304 if (count) {
305 UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Replace Existing Cheats?"
306 message:@"Append the newly imported cheats or replace the existing ones?"
307 preferredStyle:UIAlertControllerStyleAlert];
308 [alertController addAction:[UIAlertAction actionWithTitle:@"Append" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
309 load(false);
310 }]];
311 [alertController addAction:[UIAlertAction actionWithTitle:@"Replace" style:UIAlertActionStyleDestructive handler:^(UIAlertAction *action) {
312 load(true);
313 }]];
314 [self presentViewController:alertController animated:true completion:nil];
315 }
316 else {
317 load(true);
318 }
319 }
320
321 - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
322 {
323 if (indexPath.section == 0) return;
324 if (!self.editing) return;
325
326 UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
327 CGRect frame = cell.textLabel.frame;
328 frame.size.width = cell.textLabel.superview.frame.size.width - 8 - frame.origin.x;
329 UITextField *field = [[UITextField alloc] initWithFrame:frame];
330 field.font = cell.textLabel.font;
331 field.text = cell.textLabel.text;
332 cell.textLabel.text = @"";
333 [[cell.textLabel superview] addSubview:field];
334 [field becomeFirstResponder];
335 [field selectAll:nil];
336 _renamingPath = indexPath;
337 [field addTarget:self action:@selector(doneRename:) forControlEvents:UIControlEventEditingDidEnd | UIControlEventEditingDidEndOnExit];
338 _editingField = field;
339 }
340
341 - (void)doneRename:(UITextField *)sender
342 {
343 if (!_renamingPath) return;
344 const GB_cheat_t *cheat = GB_get_cheats(_gb, NULL)[_renamingPath.row];
345 GB_update_cheat(_gb, cheat, sender.text.length? sender.text.UTF8String : "Unnamed Cheat",
346 cheat->address, cheat->bank,
347 cheat->value, cheat->old_value, cheat->use_old_value,
348 cheat->enabled);
349 [self.tableView reloadRowsAtIndexPaths:@[_renamingPath] withRowAnimation:UITableViewRowAnimationNone];
350 _renamingPath = nil;
351 }
352
353 - (void)setEditing:(BOOL)editing animated:(BOOL)animated
354 {
355 [super setEditing:editing animated:animated];
356 if (!editing && _editingField) {
357 [self doneRename:_editingField];
358 }
359 }
360
361 - (void)viewWillDisappear:(BOOL)animated
362 {
363 [super viewWillDisappear:animated];
364 NSString *cheatsFile = [[GBROMManager sharedManager] cheatsFile];
365 [[NSFileManager defaultManager] removeItemAtPath:cheatsFile error:nil];
366 GB_save_cheats(_gb, cheatsFile.UTF8String);
367 }
368
369 @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.