diff --git a/src/client.c b/src/client.c index f025dba..dbd1850 100644 --- a/src/client.c +++ b/src/client.c @@ -37,6 +37,13 @@ typedef enum Tab { Tab_Count, } Tab; +typedef enum ShipTabStates { + ShipTabStateMain, + ShipTabStatePayoffModal, + ShipTabStateLoading, + ShipTabStates_Count, +} ShipTabStates; + typedef enum StationTabStates { StationTabStateTable, StationTabStateTransact, @@ -154,6 +161,7 @@ typedef struct GameState { Pos2 pos; u8 destination_sys_idx; StationTabStates station_tab_state; + ShipTabStates ship_tab_states; TransactionResult tx_result; u64 turn_tick_started_on; } GameState; @@ -384,6 +392,33 @@ fn void clearServerSentState() { 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++]; @@ -392,6 +427,7 @@ fn void handleIncomingMessage(u8* message, u32 len, SocketAddress sender, i32 so 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; @@ -515,6 +551,9 @@ fn bool updateAndRender(TuiState* tui, void* s, u8* input_buffer, u64 loop_count 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; @@ -712,20 +751,14 @@ fn bool updateAndRender(TuiState* tui, void* s, u8* input_buffer, u64 loop_count if (state->menu.selected_index >= state->menu.len) { state->menu.selected_index = 0; } - if (state->menu.selected_index == TabStation) { - state->row.len = Commodity_Count; - state->row.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; } - if (state->menu.selected_index == TabStation) { - state->row.len = Commodity_Count; - state->row.selected_index = 0; - } + resetTabRow(state->menu.selected_index); } Tab tab = state->menu.selected_index; @@ -785,6 +818,8 @@ fn bool updateAndRender(TuiState* tui, void* s, u8* input_buffer, u64 loop_count 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; @@ -906,6 +941,9 @@ fn bool updateAndRender(TuiState* tui, void* s, u8* input_buffer, u64 loop_count 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); @@ -915,6 +953,26 @@ fn bool updateAndRender(TuiState* tui, void* s, u8* input_buffer, u64 loop_count 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); @@ -926,34 +984,103 @@ fn bool updateAndRender(TuiState* tui, void* s, u8* input_buffer, u64 loop_count MemoryZero(sbuf, SBUFLEN); sprintf(sbuf, "Ready To Depart: %s", state->me.ready_to_depart ? "yes" : "no"); - 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, yoff++, sbuf, screen_dimensions); + 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); - if (user_pressed_enter) { - // 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; + renderStrToBuffer(tui->frame_buffer, box.x+2+strlen(sbuf), yoff, " [ENTER] to toggle", screen_dimensions); - outgoingMessageQueuePush(network_send_queue, &msg); + 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) { @@ -973,19 +1100,8 @@ fn bool updateAndRender(TuiState* tui, void* s, u8* input_buffer, u64 loop_count case StationTabStates_Count: assert(false && "something has gone horribly wrong"); } } - 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; - } - } + 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); diff --git a/src/lib/tui.c b/src/lib/tui.c index c87d458..c515492 100644 --- a/src/lib/tui.c +++ b/src/lib/tui.c @@ -569,6 +569,8 @@ fn void renderTable(TuiState* tui, TableDrawInfo t, u32 selected_index, FieldDes // render the columns for (u32 ii = 0; ii < t.cols; col_x_pos += fields[ii].width+table_col_pad, ii++) { if (i == selected_index) { + tui->cursor.x = t.x_offset; + tui->cursor.y = t.y_offset; for (u32 iii = 0; iii < fields[ii].width+table_col_pad; iii++) { u32 pos = XYToPos(t.x_offset+col_x_pos+iii, t.y_offset, tui->screen_dimensions.width); tui->frame_buffer[pos].background = ANSI_WHITE; diff --git a/src/server.c b/src/server.c index baf4883..0d9549f 100644 --- a/src/server.c +++ b/src/server.c @@ -116,6 +116,8 @@ typedef struct ClientList { typedef struct State { bool all_accounts_ready; + u8 winner_id; + bool someone_won; Mutex client_mutex; Mutex mutex; ClientList clients; @@ -447,6 +449,16 @@ fn UDPMessage makeMessagePlayerDetails(PlayerShip ship) { return outgoing_message; } +fn void sendMessagePayoffResult(SocketAddress addr) { + UDPMessage outgoing_message = {0}; + outgoing_message.address = addr; + u32 msg_i = 0; + outgoing_message.bytes[msg_i++] = (u8)MessagePayoffResult; + outgoing_message.bytes_len = msg_i; + outgoingMessageQueuePush(state.network_send_queue, &outgoing_message); + printf("%s sent\n", MESSAGE_STRINGS[outgoing_message.bytes[0]]); +} + fn void sendMessagePlayerDetails(PlayerShip ship, SocketAddress addr) { UDPMessage outgoing_message = makeMessagePlayerDetails(ship); outgoing_message.address = addr; @@ -505,6 +517,10 @@ fn void handleIncomingMessage(u8* message, u32 len, SocketAddress sender, i32 so } break; case CommandKeepAlive: break; + case CommandPayMortgage: { + parsed.id = readU64FromBufferLE(message + msg_idx); + printf("paying off %lld\n", parsed.id); + } break; case CommandSetDestination: { parsed.byte = message[1]; // index of system in array } break; @@ -622,6 +638,10 @@ fn bool accountIsEmpty(Account* a) { return a->id == 0 && a->name.length == 0; } +fn bool shipIsNull(PlayerShip* ship) { + return ship->id == 0 && ship->base_cost == 0; +} + fn void* gameLoop(void* params) { LaneCtx* lane_ctx = (LaneCtx*)params; ThreadContext tctx = { @@ -642,6 +662,10 @@ fn void* gameLoop(void* params) { if (LaneIdx() == 0) { // narrow state.frame += 1; + if (state.all_accounts_ready) { + state.all_accounts_ready = false; + } + // 1. process client messages lockMutex(&state.client_mutex); lockMutex(&state.mutex); { @@ -657,6 +681,19 @@ fn void* gameLoop(void* params) { u32 client_handle = findClientHandleBySocketAddress(&state.clients, sender); Client* client = &state.clients.items[client_handle]; switch (msg.type) { + case CommandPayMortgage: { + Account* account = &state.accounts[client->account_id]; + f32 amount_to_pay = (f32)msg.id; + if (account->ship.credits >= amount_to_pay) { + account->ship.credits -= amount_to_pay; + account->ship.remaining_mortgage -= msg.id; + } else { + account->ship.remaining_mortgage -= (u32)account->ship.credits; + account->ship.credits = 0.0; + } + account->changed = true; + sendMessagePayoffResult(sender); + } break; case CommandSetDestination: { Account* account = &state.accounts[client->account_id]; account->destination_sys_idx = msg.byte; @@ -780,11 +817,12 @@ fn void* gameLoop(void* params) { break; } } + printf("new account id=%d\n", existing_account->id); existing_account->name = name; existing_account->pw = pw; } client->account_id = existing_account->id; - if (existing_account->ship.id != 0) { + if (!shipIsNull(&existing_account->ship)) { // tell the client their account id outgoing_message.bytes[0] = (u8)MessageCharacterId; writeU64ToBufferLE(outgoing_message.bytes + 1, existing_account->ship.id); @@ -804,7 +842,8 @@ fn void* gameLoop(void* params) { } break; case CommandCreateCharacter: { Account* account = &state.accounts[client->account_id]; - if (account->ship.id == 0) { + printf("creating character for account id=%d, %s\n", account->id, SHIP_TYPE_STRINGS[msg.byte]); + if (shipIsNull(&account->ship)) { ShipTemplate template = SHIPS[msg.byte]; PlayerShip player_ship = { .type = msg.byte, @@ -821,7 +860,7 @@ fn void* gameLoop(void* params) { .credits = 10000.0, //.cu_m_fuel; // we are just saying you can buy as much fuel as you want //.cu_m_o2; // we are just saying you can buy as much o2 as you want - .id = ++state.next_eid, // start from 1 + .id = account->id, }; player_ship.commodities[CommodityHydrogenFuel] = 2000; player_ship.commodities[CommodityOxygen] = 1000; @@ -884,11 +923,31 @@ fn void* gameLoop(void* params) { } } + if (state.someone_won) { + for (u32 i = 1; i < SERVER_MAX_CLIENTS; i++) { + if (state.clients.items[i].last_ping != 0) { + outgoing_message.address = state.clients.items[i].address; + outgoing_message.bytes_len = 2; + outgoing_message.bytes[0] = (u8)MessageGameOver; + outgoing_message.bytes[1] = state.winner_id; + outgoingMessageQueuePush(state.network_send_queue, &outgoing_message); + } + } + return NULL; + } + } unlockMutex(&state.mutex); unlockMutex(&state.client_mutex); } LaneSync(); + u32 player_count = 0; + for (u32 i = 0; i < ACCOUNT_LEN; i++) { + if (!accountIsEmpty(&state.accounts[i])) { + player_count += 1; + } + } + // 2. tick non-user entities if (state.all_accounts_ready) { // tick all the star systems @@ -911,16 +970,11 @@ fn void* gameLoop(void* params) { } } - // move all the players - u32 player_count = 0; - for (u32 i = 0; i < ACCOUNT_LEN; i++) { - if (!accountIsEmpty(&state.accounts[i])) { - player_count += 1; - } - } Range1u64 ship_range = LaneRange(player_count); for (u32 i = ship_range.min; i < ship_range.max; i++) { Account* acct = &state.accounts[i]; + + // move all the players StarSystem dest = state.map[acct->destination_sys_idx]; StarSystem current = state.map[acct->ship.system_idx]; Pos2 dest_pos = {dest.x, dest.y}; @@ -935,11 +989,22 @@ fn void* gameLoop(void* params) { } acct->ship.ready_to_depart = false; acct->changed = true; + + // look for a winner + increase the mortgage from interest + if (!shipIsNull(&acct->ship)) { + if (acct->ship.remaining_mortgage == 0) { + state.winner_id = acct->id; + state.someone_won = true; + printf("someone WON!!! %d\n", i); + break; + } else { + acct->ship.remaining_mortgage += acct->ship.remaining_mortgage * (acct->ship.interest_rate / 100.0); + } + } } // TODO pay the mortgage payment } - state.all_accounts_ready = false; // 3. scratch cleanup arenaClear(&scratch_arena); diff --git a/src/shared.h b/src/shared.h index e8c0056..80954a6 100644 --- a/src/shared.h +++ b/src/shared.h @@ -8,8 +8,8 @@ #define STARTING_DOWN_PAYMENT (10000) #define SHIP_DETAIL_COUNT (8) #define STAR_SYSTEM_COUNT (40) -#define MAP_WIDTH (40) -#define MAP_HEIGHT (10) +#define MAP_WIDTH (36) +#define MAP_HEIGHT (12) #define MAX_PLANETS (3) typedef enum EntityFeature { @@ -437,6 +437,7 @@ typedef enum CommandType { CommandTransact, CommandReadyStatus, CommandSetDestination, + CommandPayMortgage, CommandType_Count, } CommandType; @@ -447,6 +448,7 @@ static const char* command_type_strings[CommandType_Count] = { "CreateCharacter", "ReadyStatus", "SetDestination", + "PayMortgage", }; #define ENTITY_HEADER_MESSAGE_SIZE (8+8+1+1+1) @@ -461,6 +463,8 @@ typedef enum Message { MessagePlayerDetails, MessageTransactionResult, MessageTurnTick, + MessageGameOver, + MessagePayoffResult, Message_Count, } Message; @@ -474,6 +478,8 @@ static const char* MESSAGE_STRINGS[] = { "PlayerDetails", "TransactionResult", "TurnTick", + "GameOver", + "PayoffResult", }; fn u32 fuelCostForTravel(u32 drive_efficiency, Pos2 current, Pos2 dest) {