Workboy emulation (Cocoa only)

This commit is contained in:
Lior Halphon 2020-09-19 19:31:24 +03:00
parent e35c22d405
commit 7ff3556bc3
9 changed files with 446 additions and 11 deletions

View File

@ -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)) {

View File

@ -1,4 +1,5 @@
#import <IOKit/pwr_mgt/IOPMLib.h>
#import <Carbon/Carbon.h>
#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];

View File

@ -379,6 +379,12 @@
<action selector="connectPrinter:" target="-1" id="tl1-CL-tAw"/>
</connections>
</menuItem>
<menuItem title="Workboy" id="lo9-CX-BJj">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="connectWorkboy:" target="-1" id="6vS-bq-wAX"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>

View File

@ -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)

View File

@ -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

View File

@ -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;
}
}

View File

@ -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;

169
Core/workboy.c Normal file
View File

@ -0,0 +1,169 @@
#include "gb.h"
#include <time.h>
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;
}

118
Core/workboy.h Normal file
View File

@ -0,0 +1,118 @@
#ifndef workboy_h
#define workboy_h
#include <stdint.h>
#include <stdbool.h>
#include <time.h>
#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