1455 lines
58 KiB
C
1455 lines
58 KiB
C
/*
|
|
* Code conventions:
|
|
* MyStructType
|
|
* myFunction()
|
|
* MyMacro()
|
|
* my_variable
|
|
* MY_CONSTANT
|
|
* */
|
|
#include <string.h>
|
|
#include <math.h>
|
|
#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;
|
|
}
|