/* * Code conventions: * MyStructType * myFunction() * MyMacro() * my_variable * MY_CONSTANT * */ #include #include #include "base/impl.c" #include "lib/network.c" #include "render.c" #include "string_chunk.c" //#include "assets/asset1.h" //#include "assets/asset2.h" //#include "assets/asset3.h" ///// #define a bunch of client-only tunable game constants #define SYSTEM_MESSAGES_LEN 32 #define MAX_SYSTEM_MESSAGE_LEN 512 #define GOAL_LOOPS_PER_S 50 #define GOAL_LOOP_US 1000000/GOAL_LOOPS_PER_S #define TURN_TICK_S (3) #define TURN_TICK_LEN (GOAL_LOOPS_PER_S * TURN_TICK_S) #define LOGIN_NAME_BUFFER_LEN 16 #define PARSED_SERVER_MESSAGE_THREAD_QUEUE_LEN 16 #define SBUFLEN (512) ///// TYPES typedef enum Tab { TabDebug, TabChat, TabMap, TabShip, TabStation, Tab_Count, } Tab; typedef enum ShipTabStates { ShipTabStateMain, ShipTabStatePayoffModal, ShipTabStateLoading, ShipTabStates_Count, } ShipTabStates; typedef enum StationTabStates { StationTabStateTable, StationTabStateTransact, StationTabStateResult, StationTabStateLoading, StationTabStates_Count, } StationTabStates; typedef enum Screen { ScreenLogin, ScreenCreateCharacter, ScreenMainGame, ScreenDefeat, ScreenVictory, ScreenTurnTick, Screen_Count } Screen; typedef enum LoginScreenState { LoginScreenStateInit, LoginScreenStateLoading, LoginScreenStateBadPw, LoginScreenState_Count } LoginScreenState; typedef enum SpeechTailDirection { SpeechTailDirectionBottomRight, SpeechTailDirectionBottomLeft, SpeechTailDirectionRight, SpeechTailDirectionLeft, SpeechTailDirection_Count } SpeechTailDirection; typedef struct Entity { EntityType type; u8 x; u8 y; u8 color; u64 features; u64 id; StringChunkList name; } Entity; typedef struct EntityList { u64 length; // the currently used length u64 capacity; Entity* items; } EntityList; typedef struct TransactionResult { bool buying; u32 qty; u32 cr; } TransactionResult; typedef struct ParsedServerMessage { Message type; u16 port; u16 port2; u32 ip; u32 ip2; u64 id; u64 server_frame; XYZ xyz; Pos2u8 positions[STAR_SYSTEM_COUNT]; PlanetType planet_types[MAX_PLANETS*STAR_SYSTEM_COUNT]; StarSystem sys; PlayerShip ship; TransactionResult tx_result; //Entity entities[PARSED_CLIENT_ENTITY_LEN]; //u64 ids[PARSED_IDS_LEN]; } ParsedServerMessage; typedef struct ParsedServerMessageThreadQueue { ParsedServerMessage items[PARSED_SERVER_MESSAGE_THREAD_QUEUE_LEN]; u32 head; u32 tail; u32 count; Mutex mutex; Cond not_empty; Cond not_full; } ParsedServerMessageThreadQueue; typedef struct LoginState { LoginScreenState state; u8 selected_field; u8 field_index; String name; String password; } LoginState; typedef struct MenuState { u8 selected_index; u8 len; u64 id; } MenuState; typedef struct GameState { Screen screen; Screen old_screen; EntityList entities; PlayerShip me; u64 server_frame; u64 loop_count; Arena entity_arena; StringArena string_arena; LoginState login_state; MenuState menu; MenuState row; // for tracking which row of a table the user has selected MenuState modal_choice; UDPMessage keep_alive_msg; UDPClient client; StringChunkList message_input; StarSystem map[STAR_SYSTEM_COUNT]; Pos2 pos; u8 destination_sys_idx; StationTabStates station_tab_state; ShipTabStates ship_tab_states; TransactionResult tx_result; u64 turn_tick_started_on; } GameState; ///// GLOBALS global bool should_quit; global bool debug_mode = false; global Arena permanent_arena = {0}; global u8List system_messages[SYSTEM_MESSAGES_LEN] = {0}; global u8 system_message_index = 0; global GameState state = {0}; global OutgoingMessageQueue* network_send_queue = {0}; global ParsedServerMessageThreadQueue* network_recv_queue = {0}; global str TAB_STRS[Tab_Count] = {"Debug", "Chat", "Map", "Ship", "Station"}; ///// FUNCTIONS fn ParsedServerMessageThreadQueue* newPSMThreadQueue(Arena* a) { ParsedServerMessageThreadQueue* result = arenaAlloc(a, sizeof(ParsedServerMessageThreadQueue)); MemoryZero(result, (sizeof *result)); result->mutex = newMutex(); result->not_full = newCond(); result->not_empty = newCond(); return result; } fn void psmThreadSafeQueuePush(ParsedServerMessageThreadQueue* queue, ParsedServerMessage* msg) { lockMutex(&queue->mutex); { while (queue->count == PARSED_SERVER_MESSAGE_THREAD_QUEUE_LEN) { waitForCondSignal(&queue->not_full, &queue->mutex); } MemoryCopy(&queue->items[queue->tail], msg, (sizeof *msg)); queue->tail = (queue->tail + 1) % PARSED_SERVER_MESSAGE_THREAD_QUEUE_LEN; queue->count++; signalCond(&queue->not_empty); } unlockMutex(&queue->mutex); } fn ParsedServerMessage* psmThreadSafeNonblockingQueuePop(ParsedServerMessageThreadQueue* q, ParsedServerMessage* copy_target) { // immediately returns NULL if there's nothing in the ThreadQueue // copies the ParsedServerMessage into `copy_target` if there is something in the queue // and marks it as popped from the queue ParsedServerMessage* result = NULL; lockMutex(&q->mutex); { if (q->count > 0) { result = &q->items[q->head]; MemoryCopy(copy_target, result, (sizeof *copy_target)); q->head = (q->head + 1) % PARSED_SERVER_MESSAGE_THREAD_QUEUE_LEN; q->count--; signalCond(&q->not_full); } } unlockMutex(&q->mutex); return result; } fn void entityPush(EntityList* list, Entity e) { if (list->length >= list->capacity) { arenaAllocArray(&state.entity_arena, Entity, list->capacity); list->capacity = list->capacity * 2; } list->items[list->length] = e; list->length += 1; } fn bool entityDelete(EntityList* list, u64 id) { assert(list->length > 0); if (list->items[list->length - 1].id == id) { list->length -= 1; return true; } else { for (u32 i = 0; i < list->length; i++) { if (list->items[i].id == id) { // copy the last one over the one we are deleting list->items[i] = list->items[list->length - 1]; list->length -= 1; return true; } } } return false; } fn void addSystemMessage(u8* msg) { // save the message to our system_messages ring buffer memset(system_messages[system_message_index].items, 0, SYSTEM_MESSAGES_LEN); sprintf((char*)system_messages[system_message_index].items, "%s", msg); system_messages[system_message_index].length = strlen((char*)system_messages[system_message_index].items); system_message_index += 1; if (system_message_index == SYSTEM_MESSAGES_LEN) { system_message_index = 0; } } fn void renderSystemMessages(Pixel* buf, Dim2 screen_dimensions, Box sys_msg_box) { i32 printable_lines = sys_msg_box.height - 2; if (printable_lines > SYSTEM_MESSAGES_LEN) { printable_lines = SYSTEM_MESSAGES_LEN; } for (i32 i = 0; i < printable_lines; i++) { i32 index = (system_message_index - 1 - i); if (index < 0) { index = SYSTEM_MESSAGES_LEN + index; } u32 y = sys_msg_box.y + (sys_msg_box.height - i) - 1; u8List sys_msg = system_messages[index]; for (i32 j = 0; j < MAX_SYSTEM_MESSAGE_LEN && j < sys_msg_box.width-4; j++) { u32 pos = (sys_msg_box.x + 2+j) + (screen_dimensions.width * y); if (j < sys_msg.length) { if (sys_msg.items[j] != '\n') { buf[pos].bytes[0] = sys_msg.items[j]; } } } } } fn void renderPercentBar(TuiState* tui, u16 x, u16 y, u16 width, u8 ansi_color, u64 value, u64 max) { Pixel* buf = tui->frame_buffer; Dim2 screen_dimensions = tui->screen_dimensions; u16 pos = x + (screen_dimensions.width * y); buf[pos].bytes[0] = '['; pos = x+width + (screen_dimensions.width * y); buf[pos].bytes[0] = ']'; pos = x+1 + (screen_dimensions.width * y); f32 base_ratio = 0; if (max != 0) { base_ratio = ((f32)value / (f32)max); } f32 raw_ratio = base_ratio * (width-2); u32 full_spaces_count = (u32) raw_ratio; f32 remainder = raw_ratio - full_spaces_count; for (u32 i = 0; i < full_spaces_count; i++) { buf[pos+i].foreground = ansi_color; renderUtf8CharToBuffer(buf, x+1+i, y, "█", screen_dimensions); } buf[pos+full_spaces_count].foreground = ansi_color; if (value == max) { renderUtf8CharToBuffer(buf, x+1+full_spaces_count, y, "█", screen_dimensions); } else if (remainder > 0.875) { renderUtf8CharToBuffer(buf, x+1+full_spaces_count, y, "▉", screen_dimensions); } else if (remainder > 0.75) { renderUtf8CharToBuffer(buf, x+1+full_spaces_count, y, "▊", screen_dimensions); } else if (remainder > 0.625) { renderUtf8CharToBuffer(buf, x+1+full_spaces_count, y, "▋", screen_dimensions); } else if (remainder > 0.5) { renderUtf8CharToBuffer(buf, x+1+full_spaces_count, y, "▌", screen_dimensions); } else if (remainder > 0.375) { renderUtf8CharToBuffer(buf, x+1+full_spaces_count, y, "▍", screen_dimensions); } else if (remainder > 0.25) { renderUtf8CharToBuffer(buf, x+1+full_spaces_count, y, "▎", screen_dimensions); } else if (remainder > 0.125) { renderUtf8CharToBuffer(buf, x+1+full_spaces_count, y, "▏", screen_dimensions); } else { renderUtf8CharToBuffer(buf, x+1+full_spaces_count, y, " ", screen_dimensions); } } fn void renderSpeechBubble(TuiState* tui, u16 x, u16 y, u16 max_width, String message, SpeechTailDirection dir) { Pixel* buf = tui->frame_buffer; Dim2 screen_dimensions = tui->screen_dimensions; // calculate dimenions u32 height = (message.length / max_width) + 1; u32 width = message.length; if (message.length > max_width) { width = max_width; } if (width < 12) { width = 12; // minimum width } // print the upper border renderUtf8CharToBuffer(buf, x, y, "╭", screen_dimensions); for (i32 i = 0; i < width+1; i++) { renderUtf8CharToBuffer(buf, x+1+i, y, "─", screen_dimensions); } renderUtf8CharToBuffer(buf, x+width+1, y, "╮", screen_dimensions); // start printing the rows for (u32 i = 0; i < height; i++) { renderUtf8CharToBuffer(buf, x, y+1+i, "│", screen_dimensions); // print the line of text for (u32 j = 0; j < width; j++) { u32 character_index = (i*(width-1))+j; u16 pos = (x+1+j) + (screen_dimensions.width * (y+1+i)); if (message.length > character_index) { buf[pos].bytes[0] = message.bytes[character_index]; } else { buf[pos].bytes[0] = ' '; } } renderUtf8CharToBuffer(buf, x+1+width, y+1+i, "│", screen_dimensions); } // print the bottom box border renderUtf8CharToBuffer(buf, x, y+1+height, "╰", screen_dimensions); for (i32 i = 0; i < width+1; i++) { renderUtf8CharToBuffer(buf, x+1+i, y+1+height, "─", screen_dimensions); } renderUtf8CharToBuffer(buf, x+width+1, y+1+height, "╯", screen_dimensions); // TODO actually print the speech tail } fn void renderStaticAssetToPixelBuffer(TuiState* tui, u8* asset, u32 len, u16 x, u16 y) { u16 line = 0; u16 x_in_line = 0; u16 pos = x + (tui->screen_dimensions.width * y); for (u32 i = 0; i < len; i++, x_in_line++) { if (asset[i] == '\n') { line += 1; x_in_line = 0; pos = x + (tui->screen_dimensions.width * (y+line)); } else if (asset[i] == ' ') { // do nothing, we skip spaces in our assets } else { tui->frame_buffer[pos+x_in_line].bytes[0] = asset[i]; } } } fn void clearServerSentState() { arenaClear(&state.entity_arena); state.entities.capacity = 64; state.entities.length = 0; state.entities.items = arenaAllocArray(&state.entity_arena, Entity, state.entities.capacity); } fn void resetTabRow(Tab tab) { if (state.menu.selected_index == TabStation) { state.row.len = Commodity_Count; state.row.selected_index = 0; } else if (state.menu.selected_index == TabShip) { state.ship_tab_states = ShipTabStateMain; state.row.len = 2; state.row.selected_index = 0; } } fn void moveRowUpDown(bool user_pressed_up, bool user_pressed_down) { if (user_pressed_up) { if (state.row.selected_index == 0) { state.row.selected_index = state.row.len - 1; } else { state.row.selected_index -= 1; } } else if (user_pressed_down) { if (state.row.selected_index == state.row.len - 1) { state.row.selected_index = 0; } else { state.row.selected_index += 1; } } } fn void handleIncomingMessage(u8* message, u32 len, SocketAddress sender, i32 socket) { u64 msg_pos = 0; Message msg_type = message[msg_pos++]; dbg("handleIncomingMessage() of len=%d, message=%s\n", len, MESSAGE_STRINGS[msg_type]); u8List bytes = {len, len, message}; ParsedServerMessage parsed = {0}; parsed.type = msg_type; switch (msg_type) { case MessagePayoffResult: case MessageTurnTick: case MessageNewAccountCreated: case MessageBadPw: {/*nothing to parse but the type*/} break; case MessageTransactionResult: { parsed.tx_result.buying = message[msg_pos++]; parsed.tx_result.qty = readU32FromBufferLE(message + msg_pos); msg_pos += 4; parsed.tx_result.cr = readU32FromBufferLE(message + msg_pos); msg_pos += 4; } break; case MessageStarPositions: { u32 star_msg_size = 2+MAX_PLANETS; for (u32 i = 0; i < STAR_SYSTEM_COUNT; i++) { parsed.positions[i].x = message[1+(i*star_msg_size)]; parsed.positions[i].y = message[2+(i*star_msg_size)]; for (u32 ii = 0; ii < MAX_PLANETS; ii++) { parsed.planet_types[i*MAX_PLANETS + ii] = message[(i*star_msg_size)+3+ii]; } } } break; case MessageSystemCommodities: { parsed.id = (u64)message[msg_pos++]; u32 planet_count = message[msg_pos++]; for (u32 i = 0; i < planet_count; i++) { for (u32 ii = 0; ii < Commodity_Count; ii++) { parsed.sys.planets[i].commodities[ii] = message[msg_pos++]; } } } break; case MessagePlayerDetails: { parsed.ship.type = message[msg_pos++]; parsed.ship.system_idx = message[msg_pos++]; parsed.ship.ready_to_depart = message[msg_pos++]; parsed.ship.drive_efficiency = message[msg_pos++]; parsed.ship.life_support_efficiency = message[msg_pos++]; parsed.ship.vacuum_cargo_slots = readU16FromBufferLE(message + msg_pos); msg_pos += 2; parsed.ship.climate_cargo_slots = readU16FromBufferLE(message + msg_pos); msg_pos += 2; parsed.ship.passenger_berths = readU16FromBufferLE(message + msg_pos); msg_pos += 2; parsed.ship.passenger_amenities_flags = readU16FromBufferLE(message + msg_pos); msg_pos += 2; parsed.ship.smugglers_hold_cu_m = readU16FromBufferLE(message + msg_pos); msg_pos += 2; parsed.ship.remaining_mortgage = readU32FromBufferLE(message + msg_pos); msg_pos += 4; parsed.ship.interest_rate = readF32FromBufferLE(message + msg_pos); msg_pos += 4; parsed.ship.cu_m_fuel = readU32FromBufferLE(message + msg_pos); msg_pos += 4; parsed.ship.cu_m_o2 = readU32FromBufferLE(message + msg_pos); msg_pos += 4; parsed.ship.credits = readF32FromBufferLE(message + msg_pos); msg_pos += 4; parsed.ship.id = readU64FromBufferLE(message + msg_pos); msg_pos += 8; for (u32 i = 0; i < Commodity_Count; i++, msg_pos += 4) { parsed.ship.commodities[i] = readU32FromBufferLE(message + msg_pos); } } break; case MessageCharacterId: { parsed.id = readU64FromBufferLE(message + 1); } break; case Message_Count: assert(false && "invalid msg_type detected"); break; case MessageInvalid: addSystemMessage((u8*)"NOT IMPLEMENTED"); break; } psmThreadSafeQueuePush(network_recv_queue, &parsed); } fn void* receiveNetworkUpdates(void* udp) { UDPClient client = *(UDPClient*)udp; dbg("receiveNetworkUpdates() sock=%d\n", client.socket); UDPServer server = { .ready = true, .server_address = client.server_address, .server_socket = client.socket }; infiniteReadUDPServer(&server, handleIncomingMessage); return NULL; } fn void* sendNetworkUpdates(void* udp) { i32 socket_fd = ((UDPClient*)udp)->socket; dbg("sendNetworkUpdates() sock=%d\n", socket_fd); u8List bytes_list = {0}; while (!should_quit) { UDPMessage msg = {0}; outgoingMessageQueuePop(network_send_queue, &msg); bytes_list.items = msg.bytes; bytes_list.length = msg.bytes_len; bytes_list.capacity = msg.bytes_len; sendUDPu8List(socket_fd, &msg.address, &bytes_list); } return NULL; } fn bool updateAndRender(TuiState* tui, void* s, u8* input_buffer, u64 loop_count) { GameState* state = (GameState*) s; state->loop_count = loop_count; UDPClient* udp = &state->client; state->old_screen = state->screen; Dim2 screen_dimensions = tui->screen_dimensions; bool game_screen_changed = state->old_screen != state->screen; // detect new screens tui->redraw = tui->redraw || game_screen_changed; if (screen_dimensions.height > MAX_SCREEN_HEIGHT || screen_dimensions.width > MAX_SCREEN_WIDTH) { printf("\033[2J\033[%d;%df Your screen is too damn big. Shrink it to %dx%d max, bro... geez", 1,1, MAX_SCREEN_WIDTH, MAX_SCREEN_HEIGHT); fflush(stdout); return should_quit; } char sbuf[SBUFLEN] = {0}; // process server messages u32 msg_iters = 0; ParsedServerMessage msg = {0}; ParsedServerMessage* next_net_msg = psmThreadSafeNonblockingQueuePop(network_recv_queue, &msg); while (next_net_msg != NULL) { msg_iters += 0; switch (msg.type) { case MessagePayoffResult: { state->ship_tab_states = ShipTabStateMain; } break; case MessageTurnTick: { state->screen = ScreenTurnTick; state->turn_tick_started_on = loop_count; } break; case MessageTransactionResult: { state->station_tab_state = StationTabStateResult; MemoryCopyStruct(&state->tx_result, &msg.tx_result); } break; case MessagePlayerDetails: { if (msg.ship.id == state->me.id) { state->me.type = msg.ship.type; state->me.system_idx = msg.ship.system_idx; state->me.ready_to_depart = msg.ship.ready_to_depart; state->me.drive_efficiency = msg.ship.drive_efficiency; state->me.life_support_efficiency = msg.ship.life_support_efficiency; state->me.vacuum_cargo_slots = msg.ship.vacuum_cargo_slots; state->me.climate_cargo_slots = msg.ship.climate_cargo_slots; state->me.passenger_berths = msg.ship.passenger_berths; state->me.passenger_amenities_flags = msg.ship.passenger_amenities_flags; state->me.smugglers_hold_cu_m = msg.ship.smugglers_hold_cu_m; state->me.remaining_mortgage = msg.ship.remaining_mortgage; state->me.interest_rate = msg.ship.interest_rate; state->me.cu_m_fuel = msg.ship.cu_m_fuel; state->me.cu_m_o2 = msg.ship.cu_m_o2; state->me.credits = msg.ship.credits; for (u32 i = 0; i < Commodity_Count; i++) { state->me.commodities[i] = msg.ship.commodities[i]; } } } break; case MessageStarPositions: { for (u32 i = 0; i < STAR_SYSTEM_COUNT; i++) { state->map[i].name = STAR_NAMES[i]; state->map[i].x = msg.positions[i].x; state->map[i].y = msg.positions[i].y; for (u32 ii = 0; ii < MAX_PLANETS; ii++) { state->map[i].planets[ii].type = msg.planet_types[i*MAX_PLANETS + ii]; } } } break; case MessageSystemCommodities: { u32 sys_idx = msg.id; StarSystem* sys = &state->map[sys_idx]; for (u32 i = 0; i < MAX_PLANETS; i++) { for (u32 ii = 0; ii < Commodity_Count; ii++) { sys->planets[i].commodities[ii] = msg.sys.planets[i].commodities[ii]; } } } break; case MessageNewAccountCreated: { state->screen = ScreenCreateCharacter; } break; case MessageBadPw: { state->login_state.state = LoginScreenStateBadPw; } break; case MessageCharacterId: { state->me.id = msg.id; char bytes[256] = {0}; sprintf(bytes,"got character id: %lld", msg.id); addSystemMessage((u8*)bytes); state->screen = ScreenMainGame; state->menu.selected_index = 0; state->row.selected_index = 0; } break; case Message_Count: case MessageInvalid: assert(false && "invalid message from queue"); break; } next_net_msg = psmThreadSafeNonblockingQueuePop(network_recv_queue, &msg); msg_iters++; } // operate on screen-based input+state bool user_pressed_tab = input_buffer[0] == ASCII_TAB && input_buffer[1] == 0; bool user_pressed_shift_tab = input_buffer[0] == '\x1b' && input_buffer[1] == '[' && input_buffer[2] == 'Z'; bool user_pressed_esc = input_buffer[0] == ASCII_ESCAPE && input_buffer[1] == 0; bool user_pressed_a_number = input_buffer[0] >= '0' && input_buffer[0] <= '9' && input_buffer[1] == 0; bool user_pressed_up = input_buffer[0] == 27 && input_buffer[1] == 91 && input_buffer[2] == 65; bool user_pressed_down = input_buffer[0] == 27 && input_buffer[1] == 91 && input_buffer[2] == 66; bool user_pressed_left = input_buffer[0] == 27 && input_buffer[1] == 91 && input_buffer[2] == 68; bool user_pressed_right = input_buffer[0] == 27 && input_buffer[1] == 91 && input_buffer[2] == 67; bool user_pressed_backspace = input_buffer[0] == ASCII_BACKSPACE || input_buffer[0] == ASCII_DEL; bool user_pressed_enter = input_buffer[0] == ASCII_RETURN || input_buffer[0] == ASCII_LINE_FEED; switch (state->screen) { case ScreenTurnTick: { u32 line = 2; StarSystem dest = state->map[state->destination_sys_idx]; MemoryZero(sbuf, SBUFLEN); sprintf(sbuf, "Warping to %s...", dest.name); renderStrToBuffer(tui->frame_buffer, (screen_dimensions.width - (strlen(sbuf)+4))/2, line++, sbuf, screen_dimensions); MemoryZero(sbuf, SBUFLEN); u32 frames_left = (TURN_TICK_LEN - (loop_count - state->turn_tick_started_on)); u32 seconds_left = (frames_left / GOAL_LOOPS_PER_S + 1); sprintf(sbuf, "in %d", seconds_left); renderStrToBuffer(tui->frame_buffer, (screen_dimensions.width - (strlen(sbuf)+4))/2, ++line, sbuf, screen_dimensions); if (state->turn_tick_started_on + TURN_TICK_LEN < loop_count) { state->screen = ScreenMainGame; state->menu.len = Tab_Count; state->menu.selected_index = TabMap; } } break; case ScreenCreateCharacter: { //// SIMULATION if (state->me.id != 0) { state->screen = ScreenMainGame; state->menu.selected_index = 0; state->menu.len = Tab_Count; break; } state->menu.len = ShipType_Count; if (input_buffer[0] == 'q' || user_pressed_esc) { should_quit = true; } else if (user_pressed_down || user_pressed_right) { state->menu.selected_index++; } else if (user_pressed_up || user_pressed_left) { state->menu.selected_index--; } else if (user_pressed_a_number) { if (input_buffer[0] != '0') { state->menu.selected_index = input_buffer[0] - '1'; } } else if (user_pressed_enter) { // save ship choice to our gamestate ShipTemplate template = SHIPS[state->menu.selected_index]; state->me.type = template.type; state->me.drive_efficiency = template.drive_efficiency; state->me.life_support_efficiency = template.life_support_efficiency; state->me.vacuum_cargo_slots = template.vacuum_cargo_slots; state->me.climate_cargo_slots = template.climate_cargo_slots; state->me.passenger_berths = template.passenger_berths; state->me.passenger_amenities_flags = template.passenger_amenities_flags; state->me.smugglers_hold_cu_m = template.smugglers_hold_cu_m; state->me.base_cost = template.base_cost; state->me.remaining_mortgage = template.base_cost - STARTING_DOWN_PAYMENT; state->me.interest_rate = calcInterestRate(template.base_cost, STARTING_DOWN_PAYMENT); // send character ship details to server UDPMessage msg = {0}; msg.address = udp->server_address; msg.bytes_len = 2; // 1. msg type/CommandType msg.bytes[0] = CommandCreateCharacter; // 2. chosen ShipType msg.bytes[1] = SHIPS[state->menu.selected_index].type; outgoingMessageQueuePush(network_send_queue, &msg); state->screen = ScreenMainGame; state->menu.len = Tab_Count; state->menu.selected_index = 0; } if (state->menu.selected_index >= state->menu.len) { state->menu.selected_index = 0; } //// RENDERING u16 line = 2; // 1. draw the outline Box b = { .x = 2, .y = line, .width = screen_dimensions.width - 5, .height = screen_dimensions.height - 5 }; drawAnsiBox(tui->frame_buffer, b, screen_dimensions, true); // 2. draw the header label str label = "Create Character"; renderStrToBuffer(tui->frame_buffer, (screen_dimensions.width - (strlen(label)+4))/2, ++line, label, screen_dimensions); line++; // 3. draw the ship choices str ship_label = "Choose your starting ship:"; renderStrToBuffer(tui->frame_buffer, 8, ++line, ship_label, screen_dimensions); line++; str ship_desc = "You have 10,000 credits to use as a down-payment on a ship."; renderStrToBuffer(tui->frame_buffer, 6, ++line, ship_desc, screen_dimensions); u32 selected_ship_cost = SHIPS[state->menu.selected_index].base_cost; u32 principal_borrowed = selected_ship_cost - STARTING_DOWN_PAYMENT; f32 mortgage_rate = calcInterestRate(selected_ship_cost, STARTING_DOWN_PAYMENT); f32 r = mortgage_rate / 100.0; f32 rm = r / 12.0; u32 n = 12 * 4; u32 payment = principal_borrowed * (rm*pow((1.0+rm),n))/(pow((1+rm),n) - 1); sprintf(sbuf, "Mortgage: $%d @ %f%% = $%d minimum payment per turn", selected_ship_cost - STARTING_DOWN_PAYMENT, mortgage_rate, payment); renderStrToBuffer(tui->frame_buffer, 5, ++line, sbuf, screen_dimensions); line++; // the table TableDrawInfo info = { .x_offset = 5, .y_offset = line, .rows = ShipType_Count, .cols = SHIP_DETAIL_COUNT, }; renderTable(tui, info, state->menu.selected_index, SHIP_FIELDS, 1, SHIPS, sizeof(SHIPS[0])); } break; case ScreenMainGame: { if (user_pressed_tab) { state->menu.selected_index += 1; if (state->menu.selected_index >= state->menu.len) { state->menu.selected_index = 0; } resetTabRow(state->menu.selected_index); } else if (user_pressed_shift_tab) { if (state->menu.selected_index > 0) { state->menu.selected_index -= 1; } else { state->menu.selected_index = state->menu.len - 1; } resetTabRow(state->menu.selected_index); } Tab tab = state->menu.selected_index; // per-tab simulation+rendering u32 tabs_y = 1; u32 tx = 2; for (u32 i = 0; i < Tab_Count; i++) { u32 tab_len = strlen(TAB_STRS[i]); Box b = { .x = tx, .y = tabs_y, .width = tab_len+1, .height = 1 }; drawAnsiBox(tui->frame_buffer, b, screen_dimensions, state->menu.selected_index == i); renderStrToBuffer(tui->frame_buffer, tx+1, tabs_y+ 1, TAB_STRS[i], screen_dimensions); tx += (tab_len + 3); } // draw the tabs box u32 box_y = tabs_y + 3; Box box = { .x = 1, .y = box_y, .width = screen_dimensions.width - 4, .height = screen_dimensions.height - box_y - 2, }; drawAnsiBox(tui->frame_buffer, box, screen_dimensions, true); StarSystem curr = state->map[state->me.system_idx]; switch (tab) { case TabDebug: { if (input_buffer[0] == 'q' || user_pressed_esc) { should_quit = true; } renderSystemMessages(tui->frame_buffer, tui->screen_dimensions, box); } break; case TabChat: { renderStrToBuffer(tui->frame_buffer, box.x+2, box.y+1, "You haven't heard anything interesting lately...", screen_dimensions); } break; case TabMap: { if (input_buffer[0] == 'q' || user_pressed_esc) { should_quit = true; } if (user_pressed_up) { state->pos.y -= 1; } else if (user_pressed_down) { state->pos.y += 1; } else if (user_pressed_left) { state->pos.x -= 1; } else if (user_pressed_right) { state->pos.x += 1; } u32 xw = 4; u32 yh = 2; if (box.width < MAP_WIDTH*xw) { renderStrToBuffer(tui->frame_buffer, box.x+2, box.y+2, "Your screen needs to be wider...", screen_dimensions); break; } if (box.height < MAP_HEIGHT*yh) { renderStrToBuffer(tui->frame_buffer, box.x+2, box.y+2, "Your screen needs to be taller...", screen_dimensions); break; } u32 x_off = box.x+((box.width - MAP_WIDTH*xw)/2); u32 y_off = box.y+1; StarSystem dest = state->map[state->destination_sys_idx]; tui->cursor.x = x_off+state->pos.x*xw; tui->cursor.y = y_off+state->pos.y*yh; for (u32 i = 0; i < MAP_WIDTH*xw; i++) { for (u32 ii = 0; ii < MAP_HEIGHT*yh; ii++) { u32 sysx = i / xw; u32 sysy = ii / yh; u32 bufpos = XYToPos(x_off+i, y_off+ii, screen_dimensions.width); tui->frame_buffer[bufpos].bytes[0] = ' '; if (state->pos.x == sysx && state->pos.y == sysy) { tui->frame_buffer[bufpos].background = ANSI_WHITE; tui->frame_buffer[bufpos].foreground = ANSI_BLACK; } else if (curr.x == sysx && curr.y == sysy) { // highlight the system we are currently at tui->frame_buffer[bufpos].background = ANSI_HIGHLIGHT_BLUE; tui->frame_buffer[bufpos].foreground = ANSI_BLACK; } else if (dest.x == sysx && dest.y == sysy) { tui->frame_buffer[bufpos].background = ANSI_HIGHLIGHT_RED; tui->frame_buffer[bufpos].foreground = ANSI_BLACK; } else { tui->frame_buffer[bufpos].foreground = ANSI_WHITE; tui->frame_buffer[bufpos].background = ANSI_BLACK; } } } for (u32 i = 0; i < STAR_SYSTEM_COUNT; i++) { StarSystem sys = state->map[i]; u32 sysx = x_off + xw*sys.x; u32 sysy = y_off + yh*sys.y; // clear the render-grid for this "tile" position for (u32 ii = 0; ii < xw; ii++) { for (u32 iii = 0; iii < yh; iii++) { u32 bufpos = XYToPos(sysx+ii, sysy+iii, screen_dimensions.width); tui->frame_buffer[bufpos].bytes[0] = 0; } } renderUtf8CodePoint(tui, sysx, sysy, "⭐"); renderUtf8CodePoint( tui, sysx +2, sysy, strForPlanet(sys.planets[0].type) ); renderUtf8CodePoint( tui, sysx, sysy +1, strForPlanet(sys.planets[1].type) ); renderUtf8CodePoint( tui, sysx +2, sysy +1, strForPlanet(sys.planets[2].type) ); } // map key y_off += 1+(MAP_HEIGHT*yh); Box current_key = { .height = yh, .width = xw, .x = x_off, .y = y_off }; colorizeBox(tui, current_key, ANSI_HIGHLIGHT_BLUE, 0, ' '); renderStrToBuffer(tui->frame_buffer, x_off+5, y_off++, "Your current star system:", screen_dimensions); if (curr.name) { renderStrToBuffer(tui->frame_buffer, x_off+5, y_off++, curr.name, screen_dimensions); } y_off++; Box flight_dest_key = { .height = yh, .width = xw, .x = x_off, .y = y_off }; colorizeBox(tui, flight_dest_key, ANSI_HIGHLIGHT_RED, 0, ' '); renderStrToBuffer(tui->frame_buffer, x_off+5, y_off++, "Flight Destination:", screen_dimensions); if (dest.name) { renderStrToBuffer(tui->frame_buffer, x_off+5, y_off++, dest.name, screen_dimensions); } y_off++; Pos2 dest_pos = {dest.x, dest.y}; Pos2 curr_pos = {curr.x, curr.y}; u32 projected_fuel_cost = fuelCostForTravel(state->me.drive_efficiency, curr_pos, dest_pos); bool have_enough_fuel = projected_fuel_cost < state->me.commodities[CommodityHydrogenFuel]; MemoryZero(sbuf, SBUFLEN); if (have_enough_fuel) { sprintf( sbuf, "Fuel Cost: %dkg / %dkg", projected_fuel_cost, state->me.commodities[CommodityHydrogenFuel] ); } else { sprintf( sbuf, "Not enough Fuel! Need %dkg more", projected_fuel_cost - state->me.commodities[CommodityHydrogenFuel] ); Range1u32 range = { .min = XYToPos(x_off, y_off, screen_dimensions.width), .max = XYToPos(x_off+strlen(sbuf), y_off, screen_dimensions.width), }; colorizeRange(tui, range, ANSI_HIGHLIGHT_RED, 0); } renderStrToBuffer(tui->frame_buffer, x_off, y_off++, sbuf, screen_dimensions); if (user_pressed_enter) { for (u32 i = 0; i < STAR_SYSTEM_COUNT; i++) { if (state->pos.x == state->map[i].x && state->pos.y == state->map[i].y) { state->destination_sys_idx = i; // send the destination to the server u32 msg_idx = 0; UDPMessage msg = {0}; msg.address = udp->server_address; // 1. msg type/CommandType msg.bytes[msg_idx++] = CommandSetDestination; // 2. buy? msg.bytes[msg_idx++] = state->destination_sys_idx; msg.bytes_len = msg_idx; outgoingMessageQueuePush(network_send_queue, &msg); } } } } break; case TabShip: { if (input_buffer[0] == 'q' || user_pressed_esc) { should_quit = true; } moveRowUpDown(user_pressed_up, user_pressed_down); u32 yoff = box.y+1; MemoryZero(sbuf, SBUFLEN); sprintf(sbuf, "Player: %s", state->login_state.name.bytes); renderStrToBuffer(tui->frame_buffer, box.x+2, yoff++, sbuf, screen_dimensions); MemoryZero(sbuf, SBUFLEN); sprintf(sbuf, "Ship Type: %s", SHIP_TYPE_STRINGS[state->me.type]); renderStrToBuffer(tui->frame_buffer, box.x+2, yoff++, sbuf, screen_dimensions); MemoryZero(sbuf, SBUFLEN); sprintf(sbuf, "Remaining Mortgage: %d @ %.2f%%", state->me.remaining_mortgage, state->me.interest_rate); if (state->row.selected_index == 0) { Range1u32 range = { .min = XYToPos(box.x+2, yoff, screen_dimensions.width), .max = XYToPos(box.x+2+strlen(sbuf), yoff, screen_dimensions.width), }; tui->cursor.x = box.x+2; tui->cursor.y = yoff; colorizeRange(tui, range, ANSI_WHITE, ANSI_BLACK); renderStrToBuffer(tui->frame_buffer, box.x+2+strlen(sbuf), yoff, " [ENTER] to pay off", screen_dimensions); if (user_pressed_enter && state->ship_tab_states == ShipTabStateMain) { // open the payoff modal state->ship_tab_states = ShipTabStatePayoffModal; user_pressed_enter = false; // to prevent immediately submitting the modal } } renderStrToBuffer(tui->frame_buffer, box.x+2, yoff++, sbuf, screen_dimensions); MemoryZero(sbuf, SBUFLEN); sprintf(sbuf, "Credits: %.2f", state->me.credits); renderStrToBuffer(tui->frame_buffer, box.x+2, yoff++, sbuf, screen_dimensions); u32 used_cargo = usedVacuumCargoSlots(state->me); MemoryZero(sbuf, SBUFLEN); sprintf(sbuf, "Cargo: %d / %d", used_cargo, state->me.vacuum_cargo_slots); renderStrToBuffer(tui->frame_buffer, box.x+2, yoff++, sbuf, screen_dimensions); MemoryZero(sbuf, SBUFLEN); sprintf(sbuf, "Ready To Depart: %s", state->me.ready_to_depart ? "yes" : "no"); if (state->row.selected_index == 1) { Range1u32 range = { .min = XYToPos(box.x+2, yoff, screen_dimensions.width), .max = XYToPos(box.x+2+strlen(sbuf), yoff, screen_dimensions.width), }; tui->cursor.x = box.x+2; tui->cursor.y = yoff; colorizeRange(tui, range, ANSI_WHITE, ANSI_BLACK); renderStrToBuffer(tui->frame_buffer, box.x+2+strlen(sbuf), yoff, " [ENTER] to toggle", screen_dimensions); if (user_pressed_enter && state->ship_tab_states == ShipTabStateMain) { // send the "ready" or "not ready" message to the server u32 msg_idx = 0; UDPMessage msg = {0}; msg.address = udp->server_address; // 1. msg type/CommandType msg.bytes[msg_idx++] = CommandReadyStatus; // 2. buy? msg.bytes[msg_idx++] = !state->me.ready_to_depart; msg.bytes_len = msg_idx; outgoingMessageQueuePush(network_send_queue, &msg); } } renderStrToBuffer(tui->frame_buffer, box.x+2, yoff++, sbuf, screen_dimensions); for (u32 i = 0; i < Commodity_Count; i++) { MemoryZero(sbuf, SBUFLEN); sprintf(sbuf, "%-42s %d", COMMODITIES[i].name, state->me.commodities[i]); renderStrToBuffer(tui->frame_buffer, box.x+4, yoff+4+i, sbuf, screen_dimensions); } Box modal_outline = { .x = (tui->screen_dimensions.width - 50) / 2, .y = (tui->screen_dimensions.height - 15) / 2, .height = 15, .width = 50, }; u32 y_off = modal_outline.y+2; if (state->ship_tab_states == ShipTabStatePayoffModal) { if (user_pressed_a_number) { String next_char = { 1, 2, (char*)input_buffer }; stringChunkListAppend(&state->string_arena, &state->message_input, next_char); } else if (user_pressed_backspace) { stringChunkListDeleteLast(&state->string_arena, &state->message_input); } // draw the modal clearBox(tui, modal_outline); drawAnsiBox(tui->frame_buffer, modal_outline, tui->screen_dimensions, false); MemoryZero(sbuf, SBUFLEN); sprintf(sbuf, "How much would you like to pay?"); renderStrToBuffer(tui->frame_buffer, modal_outline.x+((modal_outline.width-strlen(sbuf))/2), y_off++, sbuf, screen_dimensions); renderStringChunkList(tui, &state->message_input, modal_outline.x+8, y_off); tui->cursor.x = modal_outline.x + 8 + state->message_input.total_size; tui->cursor.y = y_off++; String input_q_str = stringChunkToString(&permanent_arena, state->message_input); u32 input_quantity = atoi(input_q_str.bytes); u32 remaining = input_quantity > state->me.remaining_mortgage ? 0 : (state->me.remaining_mortgage - input_quantity); arenaDealloc(&permanent_arena, input_q_str.capacity); MemoryZero(sbuf, SBUFLEN); sprintf(sbuf, "Leaving $%d remaining to pay off", remaining); renderStrToBuffer(tui->frame_buffer, modal_outline.x+8, y_off++, sbuf, screen_dimensions); u32 exit_x = modal_outline.x+((modal_outline.width-3)/2); u32 pos = XYToPos(exit_x, y_off, tui->screen_dimensions.width); for (u32 i = 0; i < 3; i++) { tui->frame_buffer[pos+i].background = ANSI_WHITE; tui->frame_buffer[pos+i].foreground = ANSI_BLACK; } renderStrToBuffer(tui->frame_buffer, exit_x, y_off++, "PAY", screen_dimensions); if (user_pressed_enter) { // send the "payoff" message to server u32 msg_idx = 0; UDPMessage msg = {0}; msg.address = udp->server_address; // 1. msg type/CommandType msg.bytes[msg_idx++] = CommandPayMortgage; // 2. buy? msg_idx += writeU64ToBufferLE(msg.bytes + msg_idx, input_quantity); msg.bytes_len = msg_idx; outgoingMessageQueuePush(network_send_queue, &msg); state->ship_tab_states = ShipTabStateLoading; } } else if (state->ship_tab_states == ShipTabStateLoading) { // draw the modal clearBox(tui, modal_outline); drawAnsiBox(tui->frame_buffer, modal_outline, tui->screen_dimensions, false); renderStrToBuffer(tui->frame_buffer, modal_outline.x+2, y_off, "Loading...", screen_dimensions); } } break; case TabStation: { if (input_buffer[0] == 'q' || user_pressed_esc) { switch (state->station_tab_state) { case StationTabStateTable: should_quit = true; break; case StationTabStateTransact: state->station_tab_state = StationTabStateTable; break; case StationTabStateResult: state->station_tab_state = StationTabStateTransact; break; case StationTabStateLoading: state->station_tab_state = StationTabStateTable; break; case StationTabStates_Count: assert(false && "something has gone horribly wrong"); } } moveRowUpDown(user_pressed_up, user_pressed_down); renderStrToBuffer(tui->frame_buffer, box.x+2, box.y+1, "Welcome to ", screen_dimensions); renderStrToBuffer(tui->frame_buffer, box.x+2+11, box.y+1, curr.name, screen_dimensions); // the table TableDrawInfo info = { .x_offset = box.x+2, .y_offset = box.y+3, .rows = Commodity_Count, .cols = 6, }; MarketCommodity rows[Commodity_Count] = { 0 }; for (u32 i = 0; i < Commodity_Count; i++) { Commodity c = COMMODITIES[i]; u32 qty = curr.planets[0].commodities[i] + curr.planets[1].commodities[i] + curr.planets[2].commodities[i]; rows[i].type = c.type; rows[i].unit = c.unit; rows[i].bid = priceForCommodity(i, qty, true); rows[i].ask = priceForCommodity(i, qty, false); rows[i].qty = qty; rows[i].owned = state->me.commodities[i]; } renderTable(tui, info, state->row.selected_index, MARKET_COMMODITY_FIELDS, 1, rows, sizeof(MarketCommodity)); if (state->station_tab_state != StationTabStateTable) { if (user_pressed_right) { state->modal_choice.selected_index = 1; } else if (user_pressed_left) { state->modal_choice.selected_index = 0; } else if (user_pressed_a_number) { String next_char = { 1, 2, (char*)input_buffer }; stringChunkListAppend(&state->string_arena, &state->message_input, next_char); } else if (user_pressed_backspace) { stringChunkListDeleteLast(&state->string_arena, &state->message_input); } // draw the modal bool buy_selected = state->modal_choice.selected_index == 0; Box modal_outline = { .x = (tui->screen_dimensions.width - 50) / 2, .y = (tui->screen_dimensions.height - 15) / 2, .height = 15, .width = 50, }; clearBox(tui, modal_outline); drawAnsiBox(tui->frame_buffer, modal_outline, tui->screen_dimensions, false); u32 y_off = modal_outline.y+2; str cname = COMMODITIES[state->row.selected_index].name; if (state->station_tab_state == StationTabStateResult) { if (state->tx_result.buying) { renderStrToBuffer(tui->frame_buffer, modal_outline.x+((modal_outline.width-10)/2), y_off++, "Purchased!", screen_dimensions); } else { renderStrToBuffer(tui->frame_buffer, modal_outline.x+((modal_outline.width-5)/2), y_off++, "Sold!", screen_dimensions); } MemoryZero(sbuf, SBUFLEN); sprintf(sbuf, "%d %s", state->tx_result.qty, cname); renderStrToBuffer(tui->frame_buffer, modal_outline.x+((modal_outline.width-strlen(sbuf))/2), y_off++, sbuf, screen_dimensions); renderStrToBuffer(tui->frame_buffer, modal_outline.x+((modal_outline.width-3)/2), y_off++, "for", screen_dimensions); MemoryZero(sbuf, SBUFLEN); sprintf(sbuf, "%d credits", state->tx_result.cr); renderStrToBuffer(tui->frame_buffer, modal_outline.x+((modal_outline.width-strlen(sbuf))/2), y_off++, sbuf, screen_dimensions); y_off++; u32 exit_x = modal_outline.x+((modal_outline.width-4)/2); u32 pos = XYToPos(exit_x, y_off, tui->screen_dimensions.width); for (u32 i = 0; i < 4; i++) { tui->frame_buffer[pos+i].background = ANSI_WHITE; tui->frame_buffer[pos+i].foreground = ANSI_BLACK; } tui->cursor.x = exit_x; tui->cursor.y = y_off; renderStrToBuffer(tui->frame_buffer, exit_x, y_off++, "EXIT", screen_dimensions); if (user_pressed_enter) { state->station_tab_state = StationTabStateTable; } } else if (state->station_tab_state == StationTabStateLoading) { renderStrToBuffer(tui->frame_buffer, modal_outline.x+2, y_off, "Loading...", screen_dimensions); } else { // StationTabStateTransact // draw the "purchase order" modal renderStrToBuffer(tui->frame_buffer, modal_outline.x+(modal_outline.width/2)-strlen(cname), modal_outline.y+1, cname, screen_dimensions); u32 buy_x = modal_outline.x+2; u32 sell_x = modal_outline.x+modal_outline.width-6; renderStrToBuffer(tui->frame_buffer, buy_x, y_off, "Buy", screen_dimensions); renderStrToBuffer(tui->frame_buffer, sell_x, y_off, "Sell", screen_dimensions); if (buy_selected) { u32 pos = XYToPos(buy_x, y_off, tui->screen_dimensions.width); tui->frame_buffer[pos].background = ANSI_WHITE; tui->frame_buffer[pos].foreground = ANSI_BLACK; tui->frame_buffer[pos+1].background = ANSI_WHITE; tui->frame_buffer[pos+1].foreground = ANSI_BLACK; tui->frame_buffer[pos+2].background = ANSI_WHITE; tui->frame_buffer[pos+2].foreground = ANSI_BLACK; } else { u32 pos = XYToPos(sell_x, y_off, tui->screen_dimensions.width); tui->frame_buffer[pos].background = ANSI_WHITE; tui->frame_buffer[pos].foreground = ANSI_BLACK; tui->frame_buffer[pos+1].background = ANSI_WHITE; tui->frame_buffer[pos+1].foreground = ANSI_BLACK; tui->frame_buffer[pos+2].background = ANSI_WHITE; tui->frame_buffer[pos+2].foreground = ANSI_BLACK; tui->frame_buffer[pos+3].background = ANSI_WHITE; tui->frame_buffer[pos+3].foreground = ANSI_BLACK; } y_off++; renderStrToBuffer(tui->frame_buffer, modal_outline.x+((modal_outline.width-8)/2), y_off++, "Quantity", screen_dimensions); renderStringChunkList(tui, &state->message_input, modal_outline.x+8, y_off); tui->cursor.x = modal_outline.x + 8 + state->message_input.total_size; tui->cursor.y = y_off++; if (buy_selected) { renderStrToBuffer(tui->frame_buffer, modal_outline.x+((modal_outline.width-4)/2), y_off++, "Cost", screen_dimensions); } else { renderStrToBuffer(tui->frame_buffer, modal_outline.x+((modal_outline.width-6)/2), y_off++, "Profit", screen_dimensions); } String input_q_str = stringChunkToString(&permanent_arena, state->message_input); u32 input_quantity = atoi(input_q_str.bytes); u32 credits = input_quantity * (buy_selected ? rows[state->row.selected_index].ask : rows[state->row.selected_index].bid); arenaDealloc(&permanent_arena, input_q_str.capacity); MemoryZero(sbuf, SBUFLEN); sprintf(sbuf, "%d", credits); renderStrToBuffer(tui->frame_buffer, modal_outline.x+8, y_off++, sbuf, screen_dimensions); if (user_pressed_enter) { // send the "purchase" or "sell" message to server // get back new "status" of things u32 msg_idx = 0; UDPMessage msg = {0}; msg.address = udp->server_address; // 1. msg type/CommandType msg.bytes[msg_idx++] = CommandTransact; // 2. buy? msg.bytes[msg_idx++] = buy_selected; // 3. quantity msg_idx += writeU32ToBufferLE(msg.bytes + msg_idx, input_quantity); // 4. commodity msg.bytes[msg_idx++] = rows[state->row.selected_index].type; msg.bytes_len = msg_idx; outgoingMessageQueuePush(network_send_queue, &msg); state->station_tab_state = StationTabStateLoading; } } } else { // we are on the table if (user_pressed_enter) { state->station_tab_state = StationTabStateTransact; state->modal_choice.len = 2; state->modal_choice.selected_index = 0; } } } break; case Tab_Count: {} break; } } break; case ScreenLogin: { // SIMULATION if (user_pressed_esc) { should_quit = true; } if (state->login_state.state == LoginScreenStateLoading) { break; } String* field; if (state->login_state.selected_field == 0) { field = &state->login_state.name; } else { field = &state->login_state.password; } if (isAlphaUnderscoreSpace(input_buffer[0])) { field->bytes[state->login_state.field_index] = input_buffer[0]; if (state->login_state.field_index+1 < field->capacity) { state->login_state.field_index += 1; field->length += 1; } } else if (user_pressed_backspace) { field->bytes[state->login_state.field_index] = 0; if (state->login_state.field_index > 0) { state->login_state.field_index -= 1; field->length -= 1; } } else if (input_buffer[0] == ASCII_RETURN || input_buffer[0] == ASCII_LINE_FEED) { if (state->login_state.selected_field == 0) { state->login_state.selected_field = 1; state->login_state.field_index = 0; } else { u32 msg_idx = 0; // drop the login message into the network_send_queue UDPMessage msg = {0}; msg.address = udp->server_address; // 1. msg type/CommandType msg.bytes[msg_idx++] = CommandLogin; // 2. our LAN-IP to handle the case where we are on the same LAN as the guy we are trying to fight // and our "listened" UDP port msg_idx += writeU16ToBufferLE(msg.bytes + msg_idx, ~(udp->client_port)); msg_idx += writeI32ToBufferLE(msg.bytes + msg_idx, ~osLanIPAddress()); // 2. how long is the name msg.bytes[msg_idx++] = state->login_state.name.length; // 3. the name memcpy(msg.bytes + msg_idx, state->login_state.name.bytes, state->login_state.name.length); // 4. the password memcpy(msg.bytes + msg_idx + state->login_state.name.length, state->login_state.password.bytes, state->login_state.password.length); msg.bytes_len = msg_idx + state->login_state.name.length + state->login_state.password.length; addSystemMessage((u8*)state->login_state.name.bytes); addSystemMessage((u8*)state->login_state.password.bytes); outgoingMessageQueuePush(network_send_queue, &msg); // show loading signal, stop accepting input state->login_state.state = LoginScreenStateLoading; } } // RENDERING // TODO: 1. DiffieHelman exchange // 2. encrypt password // 3. send [CommandLogin uname-len username encrypted_pass] if (screen_dimensions.height < 30 || screen_dimensions.width < 80) { str warning_label = "Screen too smol. Plz resize."; renderStrToBuffer(tui->frame_buffer, 6, 2, warning_label, screen_dimensions); } else { u32 width = screen_dimensions.width / 3; u32 height = screen_dimensions.height / 4; u32 x = (screen_dimensions.width - width) / 2; // center u32 y = (screen_dimensions.height - height) / 2; // center // outline Box b = { .x = x, .y = y, .width = width, .height = height }; drawAnsiBox(tui->frame_buffer, b, screen_dimensions, true); // labels str login_label = " Please login:"; renderStrToBuffer(tui->frame_buffer, x+1, y+1, login_label, screen_dimensions); str name_label = " Name: "; renderStrToBuffer(tui->frame_buffer, x+1, y+2, name_label, screen_dimensions); str pw_label = " Password: "; renderStrToBuffer(tui->frame_buffer, x+1, y+3, pw_label, screen_dimensions); // render name u16 pos = ((y+2)*screen_dimensions.width) + (x+11); for (u32 i = 0; i < state->login_state.name.length; i++) { tui->frame_buffer[pos+i].bytes[0] = state->login_state.name.bytes[i]; } // render password pos = ((y+3)*screen_dimensions.width) + (x+15); for (u32 i = 0; i < state->login_state.password.length; i++) { tui->frame_buffer[pos+i].bytes[0] = '*'; } if (state->login_state.state == LoginScreenStateLoading) { renderStrToBuffer(tui->frame_buffer, x+5, y+5, "Loading...", screen_dimensions); } else if (state->login_state.state == LoginScreenStateBadPw) { renderStrToBuffer(tui->frame_buffer, x+5, y+5, "Bad password, dumbass", screen_dimensions); } // calculate where to put the cursor if (state->login_state.selected_field == 0) { tui->cursor.y = y+2; tui->cursor.x = x+11+state->login_state.name.length; } else { tui->cursor.y = y+3; tui->cursor.x = x+15+state->login_state.password.length; } } } break; case ScreenVictory: { // SIMULATION if (user_pressed_esc || input_buffer[0] == ASCII_RETURN || input_buffer[0] == ASCII_LINE_FEED) { state->screen = ScreenMainGame; } // RENDERING renderStrToBuffer(tui->frame_buffer, 10, 10, "You WIN!!!", screen_dimensions); } break; case ScreenDefeat: { // SIMULATION if (user_pressed_esc || input_buffer[0] == ASCII_RETURN || input_buffer[0] == ASCII_LINE_FEED) { state->me.id = 0; state->menu.selected_index = 0; clearServerSentState(); state->screen = ScreenCreateCharacter; } // RENDERING renderStrToBuffer(tui->frame_buffer, 10, 10, "You died...", screen_dimensions); renderStrToBuffer(tui->frame_buffer, 10, 11, "[press ESC to continue]", screen_dimensions); } break; case Screen_Count: assert(false && "unhandled screen"); break; } if (loop_count % 10 == 0) { outgoingMessageQueuePush(network_send_queue, &state->keep_alive_msg); //outgoingMessageQueuePush(network_send_queue, &testm); } return should_quit; } i32 main(i32 argc, ptr argv[]) { assert(Message_Count < 256); assert(CommandType_Count < 256); osInit(); // 3 threads, each their own loop: // 1. recvNetwork() // 2. sendNetwork() // 3. input/update/draw "normal" gameloop // - which is actually "wide" doing each room in parallel across N threads // clear and init the state arenaInit(&permanent_arena); arenaInit(&state.entity_arena); state.entities.capacity = 64; state.entities.length = 0; state.entities.items = arenaAllocArray(&state.entity_arena, Entity, state.entities.capacity); arenaInit(&state.string_arena.a); state.string_arena.mutex = newMutex(); state.message_input = stringChunkListInit(&state.string_arena); for (i32 i = 0; i < SYSTEM_MESSAGES_LEN; i++) { system_messages[i].capacity = MAX_SYSTEM_MESSAGE_LEN; system_messages[i].length = 0; system_messages[i].items = arenaAllocArraySized(&permanent_arena, sizeof(u8), MAX_SYSTEM_MESSAGE_LEN); } // network queues network_send_queue = newOutgoingMessageQueue(&permanent_arena); network_recv_queue = newPSMThreadQueue(&permanent_arena); // draw login screen, get input username + password state.screen = ScreenLogin; state.login_state.name.capacity = LOGIN_NAME_BUFFER_LEN; state.login_state.name.bytes = arenaAllocArray(&permanent_arena, char, state.login_state.name.capacity); state.login_state.password.capacity = LOGIN_NAME_BUFFER_LEN; state.login_state.password.bytes = arenaAllocArray(&permanent_arena, char, state.login_state.password.capacity); // network incantation for connecting to the server if (osInitNetwork() != true) { printf("bad network startup"); exit(1); } state.client = createUDPClient(7777, argc > 1 ? argv[1] : NULL); // "hardcoded" keep alive message to periodically send to server state.keep_alive_msg.address = state.client.server_address; state.keep_alive_msg.bytes[0] = CommandKeepAlive; state.keep_alive_msg.bytes_len = 1; Thread recv_thread = spawnThread(&receiveNetworkUpdates, &state.client); Thread send_thread = spawnThread(&sendNetworkUpdates, &state.client); // game loop (read input, simulate next frame, render) infiniteUILoop( MAX_SCREEN_WIDTH, MAX_SCREEN_HEIGHT, GOAL_LOOP_US, &state, updateAndRender ); return 0; }