From 7ff3556bc3af05bdc5ca3e469fc479da2ff21bba Mon Sep 17 00:00:00 2001 From: Lior Halphon Date: Sat, 19 Sep 2020 19:31:24 +0300 Subject: [PATCH] Workboy emulation (Cocoa only) --- Cocoa/Document.m | 23 +++++- Cocoa/GBView.m | 115 ++++++++++++++++++++++++++++++ Cocoa/MainMenu.xib | 6 ++ Core/gb.c | 3 +- Core/gb.h | 5 ++ Core/printer.c | 14 ++-- Core/printer.h | 4 +- Core/workboy.c | 169 +++++++++++++++++++++++++++++++++++++++++++++ Core/workboy.h | 118 +++++++++++++++++++++++++++++++ 9 files changed, 446 insertions(+), 11 deletions(-) create mode 100644 Core/workboy.c create mode 100644 Core/workboy.h diff --git a/Cocoa/Document.m b/Cocoa/Document.m index 2c11833..653179d 100644 --- a/Cocoa/Document.m +++ b/Cocoa/Document.m @@ -47,7 +47,7 @@ enum model { bool oamUpdating; NSMutableData *currentPrinterImageData; - enum {GBAccessoryNone, GBAccessoryPrinter} accessory; + enum {GBAccessoryNone, GBAccessoryPrinter, GBAccessoryWorkboy} accessory; bool rom_warning_issued; @@ -138,6 +138,16 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, [self printImage:image height:height topMargin:top_margin bottomMargin:bottom_margin exposure:exposure]; } +static void setWorkboyTime(GB_gameboy_t *gb, time_t t) +{ + [[NSUserDefaults standardUserDefaults] setInteger:time(NULL) - t forKey:@"GBWorkboyTimeOffset"]; +} + +static time_t getWorkboyTime(GB_gameboy_t *gb) +{ + return time(NULL) - [[NSUserDefaults standardUserDefaults] integerForKey:@"GBWorkboyTimeOffset"]; +} + static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) { Document *self = (__bridge Document *)GB_get_user_data(gb); @@ -791,6 +801,9 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) else if ([anItem action] == @selector(connectPrinter:)) { [(NSMenuItem*)anItem setState:accessory == GBAccessoryPrinter]; } + else if ([anItem action] == @selector(connectWorkboy:)) { + [(NSMenuItem*)anItem setState:accessory == GBAccessoryWorkboy]; + } else if ([anItem action] == @selector(toggleCheats:)) { [(NSMenuItem*)anItem setState:GB_cheats_enabled(&gb)]; } @@ -1701,6 +1714,14 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) }]; } +- (IBAction)connectWorkboy:(id)sender +{ + [self performAtomicBlock:^{ + accessory = GBAccessoryWorkboy; + GB_connect_workboy(&gb, setWorkboyTime, getWorkboyTime); + }]; +} + - (void) updateHighpassFilter { if (GB_is_inited(&gb)) { diff --git a/Cocoa/GBView.m b/Cocoa/GBView.m index e5cb7c8..6854ba7 100644 --- a/Cocoa/GBView.m +++ b/Cocoa/GBView.m @@ -1,4 +1,5 @@ #import +#import #import "GBView.h" #import "GBViewGL.h" #import "GBViewMetal.h" @@ -8,6 +9,98 @@ #define JOYSTICK_HIGH 0x4000 #define JOYSTICK_LOW 0x3800 +static const uint8_t workboy_ascii_to_key[] = { + ['0'] = GB_WORKBOY_0, + ['`'] = GB_WORKBOY_UMLAUT, + ['1'] = GB_WORKBOY_1, + ['2'] = GB_WORKBOY_2, + ['3'] = GB_WORKBOY_3, + ['4'] = GB_WORKBOY_4, + ['5'] = GB_WORKBOY_5, + ['6'] = GB_WORKBOY_6, + ['7'] = GB_WORKBOY_7, + ['8'] = GB_WORKBOY_8, + ['9'] = GB_WORKBOY_9, + + ['\r'] = GB_WORKBOY_ENTER, + [3] = GB_WORKBOY_ENTER, + + ['!'] = GB_WORKBOY_EXCLAMATION_MARK, + ['$'] = GB_WORKBOY_DOLLAR, + ['#'] = GB_WORKBOY_HASH, + ['~'] = GB_WORKBOY_TILDE, + ['*'] = GB_WORKBOY_ASTERISK, + ['+'] = GB_WORKBOY_PLUS, + ['-'] = GB_WORKBOY_MINUS, + ['('] = GB_WORKBOY_LEFT_PARENTHESIS, + [')'] = GB_WORKBOY_RIGHT_PARENTHESIS, + [';'] = GB_WORKBOY_SEMICOLON, + [':'] = GB_WORKBOY_COLON, + ['%'] = GB_WORKBOY_PERCENT, + ['='] = GB_WORKBOY_EQUAL, + [','] = GB_WORKBOY_COMMA, + ['<'] = GB_WORKBOY_LT, + ['.'] = GB_WORKBOY_DOT, + ['>'] = GB_WORKBOY_GT, + ['/'] = GB_WORKBOY_SLASH, + ['?'] = GB_WORKBOY_QUESTION_MARK, + [' '] = GB_WORKBOY_SPACE, + ['\''] = GB_WORKBOY_QUOTE, + ['@'] = GB_WORKBOY_AT, + + ['q'] = GB_WORKBOY_Q, + ['w'] = GB_WORKBOY_W, + ['e'] = GB_WORKBOY_E, + ['r'] = GB_WORKBOY_R, + ['t'] = GB_WORKBOY_T, + ['y'] = GB_WORKBOY_Y, + ['u'] = GB_WORKBOY_U, + ['i'] = GB_WORKBOY_I, + ['o'] = GB_WORKBOY_O, + ['p'] = GB_WORKBOY_P, + ['a'] = GB_WORKBOY_A, + ['s'] = GB_WORKBOY_S, + ['d'] = GB_WORKBOY_D, + ['f'] = GB_WORKBOY_F, + ['g'] = GB_WORKBOY_G, + ['h'] = GB_WORKBOY_H, + ['j'] = GB_WORKBOY_J, + ['k'] = GB_WORKBOY_K, + ['l'] = GB_WORKBOY_L, + ['z'] = GB_WORKBOY_Z, + ['x'] = GB_WORKBOY_X, + ['c'] = GB_WORKBOY_C, + ['v'] = GB_WORKBOY_V, + ['b'] = GB_WORKBOY_B, + ['n'] = GB_WORKBOY_N, + ['m'] = GB_WORKBOY_M, +}; + +static const uint8_t workboy_vk_to_key[] = { + [kVK_F1] = GB_WORKBOY_CLOCK, + [kVK_F2] = GB_WORKBOY_TEMPERATURE, + [kVK_F3] = GB_WORKBOY_MONEY, + [kVK_F4] = GB_WORKBOY_CALCULATOR, + [kVK_F5] = GB_WORKBOY_DATE, + [kVK_F6] = GB_WORKBOY_CONVERSION, + [kVK_F7] = GB_WORKBOY_RECORD, + [kVK_F8] = GB_WORKBOY_WORLD, + [kVK_F9] = GB_WORKBOY_PHONE, + [kVK_F10] = GB_WORKBOY_UNKNOWN, + [kVK_Delete] = GB_WORKBOY_BACKSPACE, + [kVK_Shift] = GB_WORKBOY_SHIFT_DOWN, + [kVK_RightShift] = GB_WORKBOY_SHIFT_DOWN, + [kVK_UpArrow] = GB_WORKBOY_UP, + [kVK_DownArrow] = GB_WORKBOY_DOWN, + [kVK_LeftArrow] = GB_WORKBOY_LEFT, + [kVK_RightArrow] = GB_WORKBOY_RIGHT, + [kVK_Escape] = GB_WORKBOY_ESCAPE, + [kVK_ANSI_KeypadDecimal] = GB_WORKBOY_DECIMAL_POINT, + [kVK_ANSI_KeypadClear] = GB_WORKBOY_M, + [kVK_ANSI_KeypadMultiply] = GB_WORKBOY_H, + [kVK_ANSI_KeypadDivide] = GB_WORKBOY_J, +}; + @implementation GBView { uint32_t *image_buffers[3]; @@ -188,7 +281,20 @@ -(void)keyDown:(NSEvent *)theEvent { + if ([theEvent type] != NSEventTypeFlagsChanged && theEvent.isARepeat) return; unsigned short keyCode = theEvent.keyCode; + if (GB_workboy_is_enabled(_gb)) { + if (theEvent.keyCode < sizeof(workboy_vk_to_key) && workboy_vk_to_key[theEvent.keyCode]) { + GB_workboy_set_key(_gb, workboy_vk_to_key[theEvent.keyCode]); + return; + } + unichar c = [theEvent type] != NSEventTypeFlagsChanged? [theEvent.charactersIgnoringModifiers.lowercaseString characterAtIndex:0] : 0; + if (c < sizeof(workboy_ascii_to_key) && workboy_ascii_to_key[c]) { + GB_workboy_set_key(_gb, workboy_ascii_to_key[c]); + return; + } + } + bool handled = false; NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; @@ -232,6 +338,15 @@ -(void)keyUp:(NSEvent *)theEvent { unsigned short keyCode = theEvent.keyCode; + if (GB_workboy_is_enabled(_gb)) { + if (keyCode == kVK_Shift || keyCode == kVK_RightShift) { + GB_workboy_set_key(_gb, GB_WORKBOY_SHIFT_UP); + } + else { + GB_workboy_set_key(_gb, GB_WORKBOY_NONE); + } + + } bool handled = false; NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; diff --git a/Cocoa/MainMenu.xib b/Cocoa/MainMenu.xib index 71add1c..586d872 100644 --- a/Cocoa/MainMenu.xib +++ b/Cocoa/MainMenu.xib @@ -379,6 +379,12 @@ + + + + + + diff --git a/Core/gb.c b/Core/gb.c index 1bc2235..7325d79 100644 --- a/Core/gb.c +++ b/Core/gb.c @@ -1131,8 +1131,9 @@ void GB_disconnect_serial(GB_gameboy_t *gb) gb->serial_transfer_bit_start_callback = NULL; gb->serial_transfer_bit_end_callback = NULL; - /* Reset any internally-emulated device. Currently, only the printer. */ + /* Reset any internally-emulated device. */ memset(&gb->printer, 0, sizeof(gb->printer)); + memset(&gb->workboy, 0, sizeof(gb->workboy)); } bool GB_is_inited(GB_gameboy_t *gb) diff --git a/Core/gb.h b/Core/gb.h index f085eac..9043936 100644 --- a/Core/gb.h +++ b/Core/gb.h @@ -23,6 +23,7 @@ #include "sgb.h" #include "cheats.h" #include "rumble.h" +#include "workboy.h" #define GB_STRUCT_VERSION 13 @@ -372,6 +373,7 @@ struct GB_gameboy_internal_s { GB_printer_t printer; uint8_t extra_oam[0xff00 - 0xfea0]; uint32_t ram_size; // Different between CGB and DMG + GB_workboy_t workboy; ); /* DMA and HDMA */ @@ -608,6 +610,9 @@ struct GB_gameboy_internal_s { GB_read_memory_callback_t read_memory_callback; GB_boot_rom_load_callback_t boot_rom_load_callback; GB_print_image_callback_t printer_callback; + GB_workboy_set_time_callback workboy_set_time_callback; + GB_workboy_get_time_callback workboy_get_time_callback; + /* IR */ uint64_t cycles_since_ir_change; // In 8MHz units uint64_t cycles_since_input_ir_change; // In 8MHz units diff --git a/Core/printer.c b/Core/printer.c index 7b47ace..f04e54d 100644 --- a/Core/printer.c +++ b/Core/printer.c @@ -189,13 +189,13 @@ static void byte_reieve_completed(GB_gameboy_t *gb, uint8_t byte_received) static void serial_start(GB_gameboy_t *gb, bool bit_received) { - gb->printer.byte_being_recieved <<= 1; - gb->printer.byte_being_recieved |= bit_received; - gb->printer.bits_recieved++; - if (gb->printer.bits_recieved == 8) { - byte_reieve_completed(gb, gb->printer.byte_being_recieved); - gb->printer.bits_recieved = 0; - gb->printer.byte_being_recieved = 0; + gb->printer.byte_being_received <<= 1; + gb->printer.byte_being_received |= bit_received; + gb->printer.bits_received++; + if (gb->printer.bits_received == 8) { + byte_reieve_completed(gb, gb->printer.byte_being_received); + gb->printer.bits_received = 0; + gb->printer.byte_being_received = 0; } } diff --git a/Core/printer.h b/Core/printer.h index 71f919a..b29650f 100644 --- a/Core/printer.h +++ b/Core/printer.h @@ -54,8 +54,8 @@ typedef struct uint8_t compression_run_lenth; bool compression_run_is_compressed; - uint8_t bits_recieved; - uint8_t byte_being_recieved; + uint8_t bits_received; + uint8_t byte_being_received; bool bit_to_send; } GB_printer_t; diff --git a/Core/workboy.c b/Core/workboy.c new file mode 100644 index 0000000..3b10379 --- /dev/null +++ b/Core/workboy.c @@ -0,0 +1,169 @@ +#include "gb.h" +#include + +static inline uint8_t int_to_bcd(uint8_t i) +{ + return (i % 10) + ((i / 10) << 4); +} + +static inline uint8_t bcd_to_int(uint8_t i) +{ + return (i & 0xF) + (i >> 4) * 10; +} + +/* + Note: This peripheral was never released. This is a hacky software reimplementation of it that allows + reaccessing all of the features present in Workboy's ROM. Some of the implementation details are + obviously wrong, but without access to the actual hardware, this is the best I can do. +*/ + +static void serial_start(GB_gameboy_t *gb, bool bit_received) +{ + gb->workboy.byte_being_received <<= 1; + gb->workboy.byte_being_received |= bit_received; + gb->workboy.bits_received++; + if (gb->workboy.bits_received == 8) { + gb->workboy.byte_to_send = 0; + if (gb->workboy.mode != 'W' && gb->workboy.byte_being_received == 'R') { + gb->workboy.byte_to_send = 'D'; + gb->workboy.key = GB_WORKBOY_NONE; + gb->workboy.mode = gb->workboy.byte_being_received; + gb->workboy.buffer_index = 1; + + time_t time = gb->workboy_get_time_callback(gb); + struct tm tm; + tm = *localtime(&time); + memset(gb->workboy.buffer, 0, sizeof(gb->workboy.buffer)); + + gb->workboy.buffer[0] = 4; // Unknown, unused, but appears to be expected to be 4 + gb->workboy.buffer[2] = int_to_bcd(tm.tm_sec); // Seconds, BCD + gb->workboy.buffer[3] = int_to_bcd(tm.tm_min); // Minutes, BCD + gb->workboy.buffer[4] = int_to_bcd(tm.tm_hour); // Hours, BCD + gb->workboy.buffer[5] = int_to_bcd(tm.tm_mday); // Days, BCD. Upper most 2 bits are added to Year for some reason + gb->workboy.buffer[6] = int_to_bcd(tm.tm_mon + 1); // Months, BCD + gb->workboy.buffer[0xF] = tm.tm_year; // Years, plain number, since 1900 + + } + else if (gb->workboy.mode != 'W' && gb->workboy.byte_being_received == 'W') { + gb->workboy.byte_to_send = 'D'; // It is actually unknown what this value should be + gb->workboy.key = GB_WORKBOY_NONE; + gb->workboy.mode = gb->workboy.byte_being_received; + gb->workboy.buffer_index = 0; + } + else if (gb->workboy.mode != 'W' && (gb->workboy.byte_being_received == 'O' || gb->workboy.mode == 'O')) { + gb->workboy.mode = 'O'; + gb->workboy.byte_to_send = gb->workboy.key; + if (gb->workboy.key != GB_WORKBOY_NONE) { + if (gb->workboy.key & GB_WORKBOY_REQUIRE_SHIFT) { + gb->workboy.key &= ~GB_WORKBOY_REQUIRE_SHIFT; + if (gb->workboy.shift_down) { + gb->workboy.byte_to_send = gb->workboy.key; + gb->workboy.key = GB_WORKBOY_NONE; + } + else { + gb->workboy.byte_to_send = GB_WORKBOY_SHIFT_DOWN; + gb->workboy.shift_down = true; + } + } + else if (gb->workboy.key & GB_WORKBOY_FORBID_SHIFT) { + gb->workboy.key &= ~GB_WORKBOY_FORBID_SHIFT; + if (!gb->workboy.shift_down) { + gb->workboy.byte_to_send = gb->workboy.key; + gb->workboy.key = GB_WORKBOY_NONE; + } + else { + gb->workboy.byte_to_send = GB_WORKBOY_SHIFT_UP; + gb->workboy.shift_down = false; + } + } + else { + if (gb->workboy.key == GB_WORKBOY_SHIFT_DOWN) { + gb->workboy.shift_down = true; + gb->workboy.user_shift_down = true; + } + else if (gb->workboy.key == GB_WORKBOY_SHIFT_UP) { + gb->workboy.shift_down = false; + gb->workboy.user_shift_down = false; + } + gb->workboy.byte_to_send = gb->workboy.key; + gb->workboy.key = GB_WORKBOY_NONE; + } + } + } + else if (gb->workboy.mode == 'R') { + if (gb->workboy.buffer_index / 2 >= sizeof(gb->workboy.buffer)) { + gb->workboy.byte_to_send = 0; + } + else { + if (gb->workboy.buffer_index & 1) { + gb->workboy.byte_to_send = "0123456789ABCDEF"[gb->workboy.buffer[gb->workboy.buffer_index / 2] & 0xF]; + } + else { + gb->workboy.byte_to_send = "0123456789ABCDEF"[gb->workboy.buffer[gb->workboy.buffer_index / 2] >> 4]; + } + gb->workboy.buffer_index++; + } + } + else if (gb->workboy.mode == 'W') { + gb->workboy.byte_to_send = 'D'; + if (gb->workboy.buffer_index < 2) { + gb->workboy.buffer_index++; + } + else if ((gb->workboy.buffer_index - 2) < sizeof(gb->workboy.buffer)) { + gb->workboy.buffer[gb->workboy.buffer_index - 2] = gb->workboy.byte_being_received; + gb->workboy.buffer_index++; + if (gb->workboy.buffer_index - 2 == sizeof(gb->workboy.buffer)) { + struct tm tm = {0,}; + tm.tm_sec = bcd_to_int(gb->workboy.buffer[7]); + tm.tm_min = bcd_to_int(gb->workboy.buffer[8]); + tm.tm_hour = bcd_to_int(gb->workboy.buffer[9]); + tm.tm_mday = bcd_to_int(gb->workboy.buffer[0xA]); + tm.tm_mon = bcd_to_int(gb->workboy.buffer[0xB] & 0x3F) - 1; + tm.tm_year = (uint8_t)(gb->workboy.buffer[0x14] + (gb->workboy.buffer[0xA] >> 6)); // What were they thinking? + gb->workboy_set_time_callback(gb, mktime(&tm)); + gb->workboy.mode = 'O'; + } + } + } + gb->workboy.bits_received = 0; + gb->workboy.byte_being_received = 0; + } +} + +static bool serial_end(GB_gameboy_t *gb) +{ + bool ret = gb->workboy.bit_to_send; + gb->workboy.bit_to_send = gb->workboy.byte_to_send & 0x80; + gb->workboy.byte_to_send <<= 1; + return ret; +} + +void GB_connect_workboy(GB_gameboy_t *gb, + GB_workboy_set_time_callback set_time_callback, + GB_workboy_get_time_callback get_time_callback) +{ + memset(&gb->workboy, 0, sizeof(gb->workboy)); + GB_set_serial_transfer_bit_start_callback(gb, serial_start); + GB_set_serial_transfer_bit_end_callback(gb, serial_end); + gb->workboy_set_time_callback = set_time_callback; + gb->workboy_get_time_callback = get_time_callback; +} + +bool GB_workboy_is_enabled(GB_gameboy_t *gb) +{ + return gb->workboy.mode; +} + +void GB_workboy_set_key(GB_gameboy_t *gb, uint8_t key) +{ + if (gb->workboy.user_shift_down != gb->workboy.shift_down && + (key & (GB_WORKBOY_REQUIRE_SHIFT | GB_WORKBOY_FORBID_SHIFT)) == 0) { + if (gb->workboy.user_shift_down) { + key |= GB_WORKBOY_REQUIRE_SHIFT; + } + else { + key |= GB_WORKBOY_FORBID_SHIFT; + } + } + gb->workboy.key = key; +} diff --git a/Core/workboy.h b/Core/workboy.h new file mode 100644 index 0000000..d21f273 --- /dev/null +++ b/Core/workboy.h @@ -0,0 +1,118 @@ +#ifndef workboy_h +#define workboy_h +#include +#include +#include +#include "gb_struct_def.h" + + +typedef struct { + uint8_t byte_to_send; + bool bit_to_send; + uint8_t byte_being_received; + uint8_t bits_received; + uint8_t mode; + uint8_t key; + bool shift_down; + bool user_shift_down; + uint8_t buffer[0x15]; + uint8_t buffer_index; // In nibbles during read, in bytes during write +} GB_workboy_t; + +typedef void (*GB_workboy_set_time_callback)(GB_gameboy_t *gb, time_t time); +typedef time_t (*GB_workboy_get_time_callback)(GB_gameboy_t *gb); + +enum { + GB_WORKBOY_NONE = 0xFF, + GB_WORKBOY_REQUIRE_SHIFT = 0x40, + GB_WORKBOY_FORBID_SHIFT = 0x80, + + GB_WORKBOY_CLOCK = 1, + GB_WORKBOY_TEMPERATURE = 2, + GB_WORKBOY_MONEY = 3, + GB_WORKBOY_CALCULATOR = 4, + GB_WORKBOY_DATE = 5, + GB_WORKBOY_CONVERSION = 6, + GB_WORKBOY_RECORD = 7, + GB_WORKBOY_WORLD = 8, + GB_WORKBOY_PHONE = 9, + GB_WORKBOY_ESCAPE = 10, + GB_WORKBOY_BACKSPACE = 11, + GB_WORKBOY_UNKNOWN = 12, + GB_WORKBOY_LEFT = 13, + GB_WORKBOY_Q = 17, + GB_WORKBOY_1 = 17 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_W = 18, + GB_WORKBOY_2 = 18 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_E = 19, + GB_WORKBOY_3 = 19 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_R = 20, + GB_WORKBOY_T = 21, + GB_WORKBOY_Y = 22 , + GB_WORKBOY_U = 23 , + GB_WORKBOY_I = 24, + GB_WORKBOY_EXCLAMATION_MARK = 24 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_O = 25, + GB_WORKBOY_TILDE = 25 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_P = 26, + GB_WORKBOY_ASTERISK = 26 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_DOLLAR = 27 | GB_WORKBOY_FORBID_SHIFT, + GB_WORKBOY_HASH = 27 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_A = 28, + GB_WORKBOY_4 = 28 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_S = 29, + GB_WORKBOY_5 = 29 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_D = 30, + GB_WORKBOY_6 = 30 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_F = 31, + GB_WORKBOY_PLUS = 31 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_G = 32, + GB_WORKBOY_MINUS = 32 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_H = 33, + GB_WORKBOY_J = 34, + GB_WORKBOY_K = 35, + GB_WORKBOY_LEFT_PARENTHESIS = 35 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_L = 36, + GB_WORKBOY_RIGHT_PARENTHESIS = 36 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_SEMICOLON = 37 | GB_WORKBOY_FORBID_SHIFT, + GB_WORKBOY_COLON = 37, + GB_WORKBOY_ENTER = 38, + GB_WORKBOY_SHIFT_DOWN = 39, + GB_WORKBOY_Z = 40, + GB_WORKBOY_7 = 40 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_X = 41, + GB_WORKBOY_8 = 41 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_C = 42, + GB_WORKBOY_9 = 42 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_V = 43, + GB_WORKBOY_DECIMAL_POINT = 43 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_B = 44, + GB_WORKBOY_PERCENT = 44 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_N = 45, + GB_WORKBOY_EQUAL = 45 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_M = 46, + GB_WORKBOY_COMMA = 47 | GB_WORKBOY_FORBID_SHIFT, + GB_WORKBOY_LT = 47 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_DOT = 48 | GB_WORKBOY_FORBID_SHIFT, + GB_WORKBOY_GT = 48 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_SLASH = 49 | GB_WORKBOY_FORBID_SHIFT, + GB_WORKBOY_QUESTION_MARK = 49 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_SHIFT_UP = 50, + GB_WORKBOY_0 = 51 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_UMLAUT = 51, + GB_WORKBOY_SPACE = 52, + GB_WORKBOY_QUOTE = 53 | GB_WORKBOY_FORBID_SHIFT, + GB_WORKBOY_AT = 53 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_UP = 54, + GB_WORKBOY_DOWN = 55, + GB_WORKBOY_RIGHT = 56, +}; + + +void GB_connect_workboy(GB_gameboy_t *gb, + GB_workboy_set_time_callback set_time_callback, + GB_workboy_get_time_callback get_time_callback); +bool GB_workboy_is_enabled(GB_gameboy_t *gb); +void GB_workboy_set_key(GB_gameboy_t *gb, uint8_t key); + +#endif