/* * Code conventions: * MyStructType * myFunction() * MyMacro() * my_variable * MY_CONSTANT * */ #include #include #include #include "shared.h" #include "base/impl.c" #define NET_OUTGOING_MESSAGE_QUEUE_LEN 64 #include "lib/network.c" #include "render.c" #include "string_chunk.c" ///// CONSTANTS #define LEFT_ROOM_ENTITES_LEN (KB(1)) #define ROOM_MAP_COLLISIONS_LEN MAX_ROOMS/8 #define CLIENT_COMMAND_LIST_LEN 8 #define SERVER_PORT 7777 #define SERVER_MAX_HEAP_MEMORY MB(256) #define SERVER_MAX_CLIENTS 16 #define GAME_THREAD_CONCURRENCY 2 #define GOAL_NETWORK_SEND_LOOPS_PER_S 8 #define GOAL_NETWORK_SEND_LOOP_US 1000000/GOAL_NETWORK_SEND_LOOPS_PER_S #define GOAL_GAME_LOOPS_PER_S 24 #define GOAL_GAME_LOOP_US 1000000/GOAL_GAME_LOOPS_PER_S #define CLIENT_TIMEOUT_FRAMES GOAL_GAME_LOOPS_PER_S*3 #define CHUNK_SIZE 64 #define ACCOUNT_LEN (16) #define PARSED_CLIENT_COMMAND_THREAD_QUEUE_LEN 64 ///// TypeDefs typedef struct ParsedClientCommand { CommandType type; u8 byte; u16 sender_port; u16 alt_port; u32 sender_ip; u32 alt_ip; u32 qty; CommodityType commodity; StringChunkList name; StringChunkList pass; u64 id; } ParsedClientCommand; typedef struct ParsedClientCommandThreadQueue { ParsedClientCommand items[PARSED_CLIENT_COMMAND_THREAD_QUEUE_LEN]; u32 head; u32 tail; u32 count; Mutex mutex; Cond not_empty; Cond not_full; } ParsedClientCommandThreadQueue; typedef struct Account { u8 destination_sys_idx; bool changed; u32 id; // the index in the array String name; String pw; PlayerShip ship; } Account; typedef struct Client { u16 lan_port; i32 lan_ip; u64 account_id; SocketAddress address; CommandType commands[CLIENT_COMMAND_LIST_LEN]; u64 last_ping; } Client; typedef struct ClientList { u64 length; u64 capacity; Client* items; } ClientList; typedef struct State { bool all_accounts_ready; u8 winner_id; bool someone_won; Mutex client_mutex; Mutex mutex; ClientList clients; u64 next_eid; Account accounts[ACCOUNT_LEN]; u64 frame; Arena game_scratch; StringArena string_arena; ParsedClientCommandThreadQueue* network_recv_queue; OutgoingMessageQueue* network_send_queue; StarSystem map[STAR_SYSTEM_COUNT]; } State; ///// Global Variables global State state = { 0 }; global Arena permanent_arena = { 0 }; global bool debug_mode = false; ///// functionImplementations() fn ParsedClientCommandThreadQueue* newPCCThreadQueue(Arena* a) { ParsedClientCommandThreadQueue* result = arenaAlloc(a, sizeof(ParsedClientCommandThreadQueue)); MemoryZero(result, (sizeof *result)); result->mutex = newMutex(); result->not_full = newCond(); result->not_empty = newCond(); return result; } fn void pccThreadSafeQueuePush(ParsedClientCommandThreadQueue* queue, ParsedClientCommand* msg) { lockMutex(&queue->mutex); { while (queue->count == PARSED_CLIENT_COMMAND_THREAD_QUEUE_LEN) { waitForCondSignal(&queue->not_full, &queue->mutex); } MemoryCopy(&queue->items[queue->tail], msg, (sizeof *msg)); queue->tail = (queue->tail + 1) % PARSED_CLIENT_COMMAND_THREAD_QUEUE_LEN; queue->count++; signalCond(&queue->not_empty); } unlockMutex(&queue->mutex); } fn ParsedClientCommand* pccThreadSafeNonblockingQueuePop(ParsedClientCommandThreadQueue* q, ParsedClientCommand* copy_target) { // immediately returns NULL if there's nothing in the ThreadQueue // copies the ParsedClientCommand into `copy_target` if there is something in the queue // and marks it as popped from the queue ParsedClientCommand* 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_CLIENT_COMMAND_THREAD_QUEUE_LEN; q->count--; signalCond(&q->not_full); } } unlockMutex(&q->mutex); return result; } fn Account* findAccountByName(String name) { for (u32 i = 0; i < ACCOUNT_LEN; i++) { if (stringsEq(&state.accounts[i].name, &name)) { return &state.accounts[i]; } } return NULL; } fn void exitWithErrorMessage(ptr msg) { printf("error: %s", msg); exit(1); } fn u32 pushClient(ClientList* clients, SocketAddress addr) { Client new_client = {0}; new_client.last_ping = state.frame; new_client.address = addr; // first, try to overwrite an old dc'ed client for (u32 i = 1; i < clients->length; i++) { if (clients->items[i].last_ping == 0) { clients->items[i] = new_client; return i; } } // if there are none, then add the client to the end of the list assert(clients->capacity > clients->length);//TODO grow the list if we need to clients->items[clients->length] = new_client; u32 result = clients->length; clients->length += 1; return result; } fn bool deleteClientByAccountId(ClientList* clients, u64 id) { bool succeeded = false; Client blank_client = {0}; // i=1 because first client is null-client for (u32 i = 1; i < clients->length && !succeeded; i++) { if (clients->items[i].account_id == id) { clients->items[i] = blank_client; succeeded = true; } } return succeeded; } fn u32 findClientHandleByAccountId(ClientList* clients, u64 id) { for (u32 i = 0; i < clients->length; i++) { Client c = clients->items[i]; if (c.account_id == id) { return i; } } return 0; } fn u32 findClientHandleBySocketAddress(ClientList* clients, SocketAddress address) { for (u32 i = 0; i < clients->length; i++) { Client c = clients->items[i]; if (socketAddressEqual(address, c.address)) { return i; } } return 0; } fn StarSystem* findAccountsSystem(Account* a) { for (u32 i = 0; i < STAR_SYSTEM_COUNT; i++) { if (i == a->ship.system_idx) { return &state.map[i]; } } return &state.map[0]; } fn u32 starSystemPlanetCount(StarSystem* sys) { u32 planet_count = 0; for (u32 i = 0; i < MAX_PLANETS; i++) { if (sys->planets[i].type != PlanetTypeNull) { planet_count++; } } return planet_count; } fn UDPMessage makeMessageSystemCommodities(StarSystem* sys) { UDPMessage outgoing_message = {0}; u32 planet_count = starSystemPlanetCount(sys); u32 msg_i = 0; outgoing_message.bytes[msg_i++] = (u8)MessageSystemCommodities; outgoing_message.bytes[msg_i++] = (u8)sys->idx; outgoing_message.bytes[msg_i++] = (u8)planet_count; for (u32 i = 0; i < planet_count; i++) { for (u32 ii = 0; ii < Commodity_Count; ii++) { outgoing_message.bytes[msg_i++] = sys->planets[i].commodities[ii]; } } outgoing_message.bytes_len = msg_i; return outgoing_message; } fn void sendMessageTransactionResult(SocketAddress addr, u32 qty, bool buying, u32 credits) { UDPMessage outgoing_message = {.address = addr}; u32 msg_i = 0; outgoing_message.bytes[msg_i++] = (u8)MessageTransactionResult; outgoing_message.bytes[msg_i++] = buying; msg_i += writeU32ToBufferLE(outgoing_message.bytes + msg_i, qty); msg_i += writeU32ToBufferLE(outgoing_message.bytes + msg_i, credits); outgoing_message.bytes_len = msg_i; outgoingMessageQueuePush(state.network_send_queue, &outgoing_message); printf("%s sent\n", MESSAGE_STRINGS[outgoing_message.bytes[0]]); } fn UDPMessage makeMessagePlayerDetails(PlayerShip ship) { UDPMessage outgoing_message = {0}; u32 msg_i = 0; outgoing_message.bytes[msg_i++] = (u8)MessagePlayerDetails; outgoing_message.bytes[msg_i++] = ship.type; outgoing_message.bytes[msg_i++] = ship.system_idx; outgoing_message.bytes[msg_i++] = ship.ready_to_depart; outgoing_message.bytes[msg_i++] = ship.drive_efficiency; outgoing_message.bytes[msg_i++] = ship.life_support_efficiency; msg_i += writeU16ToBufferLE(outgoing_message.bytes + msg_i, ship.vacuum_cargo_slots); msg_i += writeU16ToBufferLE(outgoing_message.bytes + msg_i, ship.climate_cargo_slots); msg_i += writeU16ToBufferLE(outgoing_message.bytes + msg_i, ship.passenger_berths); msg_i += writeU16ToBufferLE(outgoing_message.bytes + msg_i, ship.passenger_amenities_flags); msg_i += writeU16ToBufferLE(outgoing_message.bytes + msg_i, ship.smugglers_hold_cu_m); msg_i += writeU32ToBufferLE(outgoing_message.bytes + msg_i, ship.remaining_mortgage); msg_i += writeF32ToBufferLE(outgoing_message.bytes + msg_i, ship.interest_rate); msg_i += writeU32ToBufferLE(outgoing_message.bytes + msg_i, ship.cu_m_fuel); msg_i += writeU32ToBufferLE(outgoing_message.bytes + msg_i, ship.cu_m_o2); msg_i += writeU32ToBufferLE(outgoing_message.bytes + msg_i, ship.credits); msg_i += writeU64ToBufferLE(outgoing_message.bytes + msg_i, ship.id); for (u32 i = 0; i < Commodity_Count; i++) { msg_i += writeU32ToBufferLE(outgoing_message.bytes + msg_i, ship.commodities[i]); } outgoing_message.bytes_len = msg_i; 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; outgoingMessageQueuePush(state.network_send_queue, &outgoing_message); printf("%s sent\n", MESSAGE_STRINGS[outgoing_message.bytes[0]]); } fn void handleIncomingMessage(u8* message, u32 len, SocketAddress sender, i32 socket) { dbg("%d: %s from %s:%d\n", len, command_type_strings[message[0]], inet_ntoa(sender.sin_addr), sender.sin_port); u32 msg_idx = 0; ParsedClientCommand parsed = { .type = (CommandType)message[msg_idx++], .sender_ip = sender.sin_addr.s_addr, .sender_port = sender.sin_port, }; u8 temp_bytes[UDP_MAX_MESSAGE_LEN] = {0}; String temp_str = { .bytes = (char*)temp_bytes, .length = 0, .capacity = 0, }; switch (parsed.type) { case CommandLogin: { // parse the login command parsed.alt_port = ~readU16FromBufferLE(message + msg_idx); msg_idx += 2; parsed.alt_ip = ~readI32FromBufferLE(message + msg_idx); msg_idx += 4; // parse the name u8 name_len = message[msg_idx++]; MemoryZero(temp_bytes, UDP_MAX_MESSAGE_LEN); temp_str.length = name_len; temp_str.capacity = temp_str.length+1; for (u32 j = 0; j < temp_str.length; j++) { temp_str.bytes[j] = message[msg_idx+j]; } parsed.name = allocStringChunkList(&state.string_arena, temp_str); msg_idx += temp_str.length; // parse the password u32 pw_len = 0; while (message[msg_idx+pw_len]) { pw_len += 1; } MemoryZero(temp_bytes, UDP_MAX_MESSAGE_LEN); temp_str.length = pw_len; temp_str.capacity = temp_str.length+1; for (u32 j = 0; j < temp_str.length; j++) { temp_str.bytes[j] = message[msg_idx+j]; } parsed.pass = allocStringChunkList(&state.string_arena, temp_str); msg_idx += temp_str.length; printf("Logging in player: %s %d %s\n", MESSAGE_STRINGS[parsed.type], name_len, message + 7); } 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; case CommandReadyStatus: { parsed.byte = message[1]; // boolean ready_to_depart } break; case CommandTransact: { printf("command transact received\n"); parsed.byte = message[1]; // buy? parsed.qty = readU32FromBufferLE(message + 2); parsed.commodity = message[6]; } break; case CommandCreateCharacter: { printf("command create character received\n"); parsed.byte = message[1]; } break; case CommandInvalid: case CommandType_Count: { dbg("invalid command type"); } break; } pccThreadSafeQueuePush(state.network_recv_queue, &parsed); fflush(stdout); } fn void* receiveNetworkUpdates(void* udp) { UDPServer server = *(UDPServer*)udp; dbg("receiveNetworkUpdates() sock=%d\n", server.server_socket); infiniteReadUDPServer(&server, handleIncomingMessage); return NULL; } fn void* sendNetworkUpdates(void* sock) { ThreadContext tctx = {0}; tctxInit(&tctx); i32* socket_ptr = (i32*)sock; i32 socket = *socket_ptr; while (true) { u64 loop_start = osTimeMicrosecondsNow(); // 1. clear out our "outgoingMessage" queue { UDPMessage to_send = { 0 }; UDPMessage* next_to_send = outgoingMessageNonblockingQueuePop(state.network_send_queue, &to_send); u8List bytes_list = { 0 }; while (next_to_send != NULL) { bytes_list.items = to_send.bytes; bytes_list.length = to_send.bytes_len; bytes_list.capacity = to_send.bytes_len; sendUDPu8List(socket, &to_send.address, &bytes_list); next_to_send = outgoingMessageNonblockingQueuePop(state.network_send_queue, &to_send); } } lockMutex(&state.client_mutex); { // WARNING the `i` starts at 1 here because state.clients.items[0] is a "null" Client for (u32 i = 1; i < state.clients.length; i++) { Client client = state.clients.items[i]; if (client.last_ping+CLIENT_TIMEOUT_FRAMES < state.frame) { memset(&state.clients.items[i], 0, sizeof(Client)); continue; } //if (client.character_eid == 0) { // continue; // they are still creating their character //} // update all the changed systems UDPMessage msg_data; for (u32 ii = 0; ii < STAR_SYSTEM_COUNT; ii++) { if (state.map[ii].changed == true) { msg_data = makeMessageSystemCommodities(&state.map[ii]); u8List msg = { .capacity = UDP_MAX_MESSAGE_LEN, .items = msg_data.bytes, .length = msg_data.bytes_len, }; sendUDPu8List(socket, &client.address, &msg); } } // update all the changed accounts for (u32 ii = 0; ii < ACCOUNT_LEN; ii++) { if (state.accounts[ii].changed == true) { msg_data = makeMessagePlayerDetails(state.accounts[ii].ship); u8List msg = { .capacity = UDP_MAX_MESSAGE_LEN, .items = msg_data.bytes, .length = msg_data.bytes_len, }; sendUDPu8List(socket, &client.address, &msg); } } } } unlockMutex(&state.client_mutex); lockMutex(&state.mutex); { for (u32 i = 0; i < STAR_SYSTEM_COUNT; i++) { state.map[i].changed = false; } for (u32 i = 0; i < ACCOUNT_LEN; i++) { state.accounts[i].changed = false; } } unlockMutex(&state.mutex); u32 loop_duration = osTimeMicrosecondsNow() - loop_start; i32 remaining_time = GOAL_NETWORK_SEND_LOOP_US - loop_duration; if (remaining_time > 0) { osSleepMicroseconds(remaining_time); } } return NULL; } 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 = { .lane_ctx = *lane_ctx, }; tctxInit(&tctx); printf("Lane %lld (%lld) of %lld starting.\n", lane_ctx->lane_idx, LaneIdx(), lane_ctx->lane_count); fflush(stdout); UDPMessage outgoing_message = {0}; u64 loop_start; u64 last_burn = 0; u64 last_hp_regen = 0; Arena scratch_arena = {0}; arenaInit(&scratch_arena); while (true) { loop_start = osTimeMicrosecondsNow(); 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); { u32 msg_iters = 0; SocketAddress sender = {0}; ParsedClientCommand msg = {0}; ParsedClientCommand* next_net_msg = pccThreadSafeNonblockingQueuePop(state.network_recv_queue, &msg); while (next_net_msg != NULL) { msg_iters += 0; sender.sin_addr.s_addr = msg.sender_ip; sender.sin_port = msg.sender_port; // find which client it is 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]; u32 amount_to_pay = msg.id; if (amount_to_pay > account->ship.credits) { amount_to_pay = account->ship.credits; } if (amount_to_pay > account->ship.remaining_mortgage) { amount_to_pay = account->ship.remaining_mortgage; } account->ship.credits -= amount_to_pay; account->ship.remaining_mortgage -= amount_to_pay; account->changed = true; sendMessagePayoffResult(sender); } break; case CommandSetDestination: { Account* account = &state.accounts[client->account_id]; account->destination_sys_idx = msg.byte; } break; case CommandReadyStatus: { Account* account = &state.accounts[client->account_id]; account->ship.ready_to_depart = msg.byte; sendMessagePlayerDetails(account->ship, sender); state.all_accounts_ready = true; for (u32 i = 0; i < ACCOUNT_LEN; i++) { if (!accountIsEmpty(&state.accounts[i])) { if (state.accounts[i].ship.ready_to_depart == false) { state.all_accounts_ready = false; } } } } break; case CommandTransact: { printf("Transact for client_handle=%d, on #%lld", client_handle, state.frame); bool is_buying_from_system = msg.byte; Account* account = &state.accounts[client->account_id]; StarSystem* sys = findAccountsSystem(account); u32 total_available = sys->planets[0].commodities[msg.commodity] + sys->planets[1].commodities[msg.commodity] + sys->planets[2].commodities[msg.commodity]; u32 qty_traded = 0; u32 credit_value = 0; if (is_buying_from_system) { u32 ship_space = account->ship.cu_m_fuel - account->ship.commodities[CommodityHydrogenFuel]; if (msg.commodity == CommodityOxygen) { ship_space = account->ship.cu_m_o2 - account->ship.commodities[CommodityOxygen]; } else if (COMMODITIES[msg.commodity].unit == StorageUnitContainer) { ship_space = account->ship.vacuum_cargo_slots - usedVacuumCargoSlots(account->ship); } for (u32 amount_to_buy = Min(msg.qty, total_available); amount_to_buy > 0; amount_to_buy--, total_available--, ship_space--) { u32 price = priceForCommodity(msg.commodity, total_available, false); if (price > account->ship.credits || ship_space == 0) { amount_to_buy = 0; break; } credit_value += price; account->ship.credits -= price; account->ship.commodities[msg.commodity] += 1; qty_traded += 1; if (sys->planets[0].commodities[msg.commodity]) { sys->planets[0].commodities[msg.commodity] -= 1; } else if (sys->planets[1].commodities[msg.commodity]) { sys->planets[1].commodities[msg.commodity] -= 1; } else if (sys->planets[2].commodities[msg.commodity]) { sys->planets[2].commodities[msg.commodity] -= 1; } } } else { // selling to system logic qty_traded = Min(msg.qty, account->ship.commodities[msg.commodity]); u32 planet_count = starSystemPlanetCount(sys); for ( u32 amount_to_sell = qty_traded; amount_to_sell > 0; amount_to_sell--, total_available++ ) { u32 price = priceForCommodity(msg.commodity, total_available, true); credit_value += price; account->ship.credits += price; account->ship.commodities[msg.commodity] -= 1; u8 planet_idx = rand() % planet_count; sys->planets[planet_idx].commodities[msg.commodity] += 1; } } sys->changed = true; sendMessagePlayerDetails(account->ship, sender); sendMessageTransactionResult(sender, qty_traded, is_buying_from_system, credit_value); } break; case CommandKeepAlive: { dbg("KeepAlive for client_handle=%d, %ld", client_handle, state.frame); state.clients.items[client_handle].last_ping = state.frame; } break; case CommandLogin: { if (client_handle == 0) { client_handle = pushClient(&state.clients, sender); client = &state.clients.items[client_handle]; printf("pushed new client handle = %d\n", client_handle); } // update/set the lan_ip/port info for p2p connections client->lan_ip = htonl(msg.alt_ip); client->lan_port = htons(msg.alt_port); /* struct in_addr ipaddr; ipaddr.s_addr = htonl(msg.alt_ip); printf("client #%d: SENDER=%s:%d\n", client_handle, inet_ntoa(sender.sin_addr), ntohs(sender.sin_port)); printf(" LAN=%s:%d %d vs %d vs %d\n", inet_ntoa(ipaddr), msg.alt_port, msg.alt_ip, htonl(msg.alt_ip), sender.sin_addr.s_addr); */ String name = stringChunkToString(&permanent_arena, msg.name); releaseStringChunkList(&state.string_arena, &msg.name); String pw = stringChunkToString(&permanent_arena, msg.pass); releaseStringChunkList(&state.string_arena, &msg.pass); Account* existing_account = findAccountByName(name); printf("name(%d): %s pw(%d): %s acct?: %d\n", name.length, name.bytes, pw.length, pw.bytes, existing_account != NULL); fflush(stdout); if (existing_account) { printf(" existing account\n"); bool pw_matches = stringsEq(&pw, &existing_account->pw); arenaDealloc(&permanent_arena, pw.capacity); arenaDealloc(&permanent_arena, name.capacity); if (pw_matches) { printf(" pw matched\n"); } else { // tell the client they did a bad pw outgoing_message.bytes[0] = (u8)MessageBadPw; outgoing_message.bytes_len = 1; outgoing_message.address = sender; outgoingMessageQueuePush(state.network_send_queue, &outgoing_message); printf("MessageBadPw sent\n"); break; } } else { for (u32 i = 0; i < ACCOUNT_LEN; i++) { if (accountIsEmpty(&state.accounts[i])) { existing_account = &state.accounts[i]; existing_account->id = i; 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 (!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); outgoing_message.bytes_len = 9; outgoing_message.address = sender; outgoingMessageQueuePush(state.network_send_queue, &outgoing_message); printf("MessageCharacterId sent\n"); } else { // tell the client they made a new account outgoing_message.bytes[0] = (u8)MessageNewAccountCreated; outgoing_message.bytes_len = 1; outgoing_message.address = sender; outgoingMessageQueuePush(state.network_send_queue, &outgoing_message); printf("MessageNewAccountCreated sent\n"); } printf("client_handle=%d, acct_id=%d\n", client_handle, existing_account->id); } break; case CommandCreateCharacter: { Account* account = &state.accounts[client->account_id]; 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, .drive_efficiency = template.drive_efficiency, .life_support_efficiency = template.life_support_efficiency, .vacuum_cargo_slots = template.vacuum_cargo_slots, .climate_cargo_slots = template.climate_cargo_slots, .passenger_berths = template.passenger_berths, .passenger_amenities_flags = template.passenger_amenities_flags, .smugglers_hold_cu_m = template.smugglers_hold_cu_m, .base_cost = template.base_cost, .remaining_mortgage = template.base_cost - STARTING_DOWN_PAYMENT, .interest_rate = calcInterestRate(template.base_cost, STARTING_DOWN_PAYMENT), .credits = 10000.0, .cu_m_fuel = template.cu_m_fuel, .cu_m_o2 = template.cu_m_o2, .id = account->id, }; player_ship.commodities[CommodityHydrogenFuel] = player_ship.cu_m_fuel; player_ship.commodities[CommodityOxygen] = player_ship.cu_m_o2; account->ship = player_ship; printf("ship_type=%s, client_handle=%d, acct_id=%d\n", SHIP_TYPE_STRINGS[msg.byte], client_handle, account->id); u32 starting_system_idx = rand() % STAR_SYSTEM_COUNT; StarSystem starting_system = state.map[starting_system_idx]; account->ship.system_idx = starting_system_idx; // tell the client their account id outgoing_message.address = sender; outgoing_message.bytes_len = 9; outgoing_message.bytes[0] = (u8)MessageCharacterId; writeU64ToBufferLE(outgoing_message.bytes + 1, account->ship.id); outgoingMessageQueuePush(state.network_send_queue, &outgoing_message); printf("MessageCharacterId sent\n"); sendMessagePlayerDetails(account->ship, sender); // tell the client about the map u32 star_msg_size = 2+MAX_PLANETS; outgoing_message.address = sender; outgoing_message.bytes_len = 1+(star_msg_size*STAR_SYSTEM_COUNT); outgoing_message.bytes[0] = (u8)MessageStarPositions; for (u32 i = 0; i < STAR_SYSTEM_COUNT; i++) { outgoing_message.bytes[1+(i*star_msg_size)] = (u8)state.map[i].x; outgoing_message.bytes[2+(i*star_msg_size)] = (u8)state.map[i].y; for (u32 ii = 0; ii < MAX_PLANETS; ii++) { outgoing_message.bytes[3+ii+(i*star_msg_size)] = state.map[i].planets[ii].type; } } outgoingMessageQueuePush(state.network_send_queue, &outgoing_message); printf("%s sent\n", MESSAGE_STRINGS[outgoing_message.bytes[0]]); outgoing_message = makeMessageSystemCommodities(&starting_system); outgoing_message.address = sender; outgoingMessageQueuePush(state.network_send_queue, &outgoing_message); printf("%s sent\n", MESSAGE_STRINGS[outgoing_message.bytes[0]]); } else { printf("client tried to create a character when he already has one."); } } break; case CommandType_Count: case CommandInvalid: dbg("invalid message from queue"); break; } next_net_msg = pccThreadSafeNonblockingQueuePop(state.network_recv_queue, &msg); msg_iters++; } if (state.all_accounts_ready) { 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 = 1; outgoing_message.bytes[0] = (u8)MessageTurnTick; outgoingMessageQueuePush(state.network_send_queue, &outgoing_message); } } } 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); } } } } unlockMutex(&state.mutex); unlockMutex(&state.client_mutex); } if (state.someone_won) { return NULL; } 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 StarSystem* sys = NULL; Range1u64 sys_range = LaneRange(STAR_SYSTEM_COUNT); for (u32 i = sys_range.min; i < sys_range.max; i++) { sys = &state.map[i]; sys->changed = true; for (u32 ii = 0; ii < Commodity_Count; ii++) { for (u32 iii = 0; iii < MAX_PLANETS; iii++) { if (sys->planets[iii].type != PlanetTypeNull) { if (sys->planets[iii].commodities[ii] > COMMODITIES[ii].consumption) { sys->planets[iii].commodities[ii] -= COMMODITIES[ii].consumption; } else { sys->planets[iii].commodities[ii] = 0; } sys->planets[iii].commodities[ii] += sys->planets[iii].production[ii]; } } } } 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}; Pos2 curr_pos = {current.x, current.y}; u32 projected_fuel_cost = fuelCostForTravel(acct->ship.drive_efficiency, curr_pos, dest_pos); bool have_enough_fuel = projected_fuel_cost < acct->ship.commodities[CommodityHydrogenFuel]; if (have_enough_fuel) { // remove the fuel they used on the journey acct->ship.commodities[CommodityHydrogenFuel] -= projected_fuel_cost; // set the pos to be the system they had as destination acct->ship.system_idx = acct->destination_sys_idx; } acct->ship.ready_to_depart = false; acct->changed = true; // increase the mortgage from interest if (!shipIsNull(&acct->ship)) { if (acct->ship.remaining_mortgage > 0) { f64 compounding = ((f64)acct->ship.remaining_mortgage) * (f64)(acct->ship.interest_rate / 100.0); acct->ship.remaining_mortgage += (u32)compounding; } } } } // look for a winner Range1u64 ship_range = LaneRange(player_count); for (u32 i = ship_range.min; i < ship_range.max; i++) { Account* acct = &state.accounts[i]; if (!shipIsNull(&acct->ship)) { if (acct->ship.remaining_mortgage == 0) { state.winner_id = acct->id; state.someone_won = true; printf("%s WON!!! %d\n", acct->name.bytes, i); acct->changed = true; break; } } } // 3. scratch cleanup arenaClear(&scratch_arena); // 4. loop timing u32 loop_duration = osTimeMicrosecondsNow() - loop_start; i32 remaining_time = GOAL_GAME_LOOP_US - loop_duration; if (remaining_time > 0) { osSleepMicroseconds(remaining_time); } } return NULL; } fn void setHighProductionCommodity(Planet* p, CommodityType t) { p->commodities[t] = (COMMODITIES[t].qty / 2) + (rand() % COMMODITIES[t].qty); p->production[t] = COMMODITIES[t].consumption + (rand() % COMMODITIES[t].consumption); } fn void setLowProductionCommodity(Planet* p, CommodityType t) { p->commodities[t] = rand() % (COMMODITIES[t].qty / 2); p->production[t] = rand() % (COMMODITIES[t].consumption / 2); } // THE SERVER i32 main(i32 argc, ptr argv[]) { osInit(); // multi-thread architecture: // - the gameLoop() which just inifinite loops every "tick" and processes user input and updates gameworld state // - TODO: actually make this N "lanes" as described https://www.rfleury.com/p/multi-core-by-default // such that we can split room-work among all the various cores on our machine // - one is the sendNetworkUpdates() infinite loop, which sends a UDP snapshot-or-delta update to each connected client N/sec // - one is the receiveNetworkUpdates() infinte loop, which waits for new UDP messages from clients // 1. initialize gameworld, and spin off infinite game-loop thread // 2. spin off sendNetworkUpdates() infinite loop thread // 3. infinitely wait for incoming UDP messages and process them (usually by just dropping user-commands into the relevant block of shared memory) // 1. initialize gameworld, and spin off infinite game-loop thread arenaInit(&permanent_arena); arenaInit(&state.game_scratch); arenaInit(&state.string_arena.a); state.string_arena.mutex = newMutex(); state.client_mutex = newMutex(); state.mutex = newMutex(); state.network_recv_queue = newPCCThreadQueue(&permanent_arena); state.network_send_queue = newOutgoingMessageQueue(&permanent_arena); // alloc the global hashmap of rooms // init + alloc clients state.clients.capacity = SERVER_MAX_CLIENTS; state.clients.length = 1; // making entry 0 to be a "null" client state.clients.items = (Client*)arenaAllocArray(&permanent_arena, Client, SERVER_MAX_CLIENTS); // set position of all star systems for (u32 i = 0; i < STAR_SYSTEM_COUNT; i++) { state.map[i].idx = i; state.map[i].name = STAR_NAMES[i]; state.map[i].crime = rand() % 100; state.map[i].x = rand() % MAP_WIDTH; state.map[i].y = rand() % MAP_HEIGHT; // ensure all the star systems are "spaced out" a bit bool conflicting_pos = false; for (u32 ii = 0; ii < i; ii++) { bool same_pos = state.map[ii].x == state.map[i].x && state.map[ii].y == state.map[i].y; bool north_pos = state.map[ii].x == state.map[i].x && state.map[ii].y == state.map[i].y+1; bool south_pos = state.map[ii].x == state.map[i].x && state.map[ii].y+1 == state.map[i].y; bool west_pos = state.map[ii].x == state.map[i].x+1 && state.map[ii].y == state.map[i].y; bool east_pos = state.map[ii].x+1 == state.map[i].x && state.map[ii].y == state.map[i].y; if (same_pos || north_pos || south_pos || west_pos || east_pos) { conflicting_pos = true; } } if (conflicting_pos) { i--; // re-do the position calc } } // now, build out the commodity info for the systems for (u32 i = 0; i < STAR_SYSTEM_COUNT; i++) { u32 planet_count = rand() % MAX_PLANETS + 1; for (u32 ii = 0; ii < planet_count; ii++) { Planet* p = &state.map[i].planets[ii]; p->type = 1+(PlanetType)(rand() % (PlanetType_Count -1)); // default commodity + production roll for (u32 iii = 0; iii < Commodity_Count; iii++) { p->commodities[iii] = (COMMODITIES[iii].qty / 5) + (rand() % COMMODITIES[iii].qty); p->production[iii] = 1 + (rand() % COMMODITIES[iii].consumption); } // now, override for the "specialty" of the planet switch (p->type) { case PlanetTypeEarth: { setHighProductionCommodity(p, CommodityWater); setHighProductionCommodity(p, CommodityFertilizer); setHighProductionCommodity(p, CommodityGrain); setHighProductionCommodity(p, CommodityMeat); setHighProductionCommodity(p, CommoditySpices); setHighProductionCommodity(p, CommodityAlcohol); setLowProductionCommodity(p, CommodityHydrogenFuel); setLowProductionCommodity(p, CommodityLowGradeOre); setLowProductionCommodity(p, CommodityHighGradeOre); setLowProductionCommodity(p, CommodityCommonMetals); setLowProductionCommodity(p, CommodityRareMetals); //setLowProductionCommodity(p, CommodityPreciousMetals); } break; case PlanetTypeGas: { // a lot of fuel and chemicals setHighProductionCommodity(p, CommodityHydrogenFuel); setHighProductionCommodity(p, CommodityOxygen); setHighProductionCommodity(p, CommodityWater); setHighProductionCommodity(p, CommodityIndustrialChemicals); setHighProductionCommodity(p, CommodityPlastics); setLowProductionCommodity(p, CommodityLowGradeOre); setLowProductionCommodity(p, CommodityHighGradeOre); setLowProductionCommodity(p, CommodityCommonMetals); setLowProductionCommodity(p, CommodityRareMetals); //setLowProductionCommodity(p, CommodityPreciousMetals); setLowProductionCommodity(p, CommodityClothes); setLowProductionCommodity(p, CommodityRawTextiles); setLowProductionCommodity(p, CommodityGlass); setLowProductionCommodity(p, CommodityPersonalSundries); setLowProductionCommodity(p, CommodityHandTools); setLowProductionCommodity(p, CommodityMeat); } break; case PlanetTypeMoon: { // a lot of manufactured goods setHighProductionCommodity(p, CommodityPersonalSundries); setHighProductionCommodity(p, CommodityClothes); setHighProductionCommodity(p, CommodityAlcohol); setHighProductionCommodity(p, CommodityElectronics); setLowProductionCommodity(p, CommodityHighGradeOre); setLowProductionCommodity(p, CommodityHydrogenFuel); setLowProductionCommodity(p, CommodityOxygen); setLowProductionCommodity(p, CommodityWater); setLowProductionCommodity(p, CommodityFertilizer); } break; case PlanetTypeAsteroid: { // a lot of raw metals setHighProductionCommodity(p, CommodityLowGradeOre); setHighProductionCommodity(p, CommodityHighGradeOre); setHighProductionCommodity(p, CommodityCommonMetals); setHighProductionCommodity(p, CommodityRareMetals); //setHighProductionCommodity(p, CommodityPreciousMetals); setHighProductionCommodity(p, CommoditySemiConductors); setLowProductionCommodity(p, CommodityHydrogenFuel); setLowProductionCommodity(p, CommodityOxygen); setLowProductionCommodity(p, CommodityWater); setLowProductionCommodity(p, CommodityFertilizer); setLowProductionCommodity(p, CommodityPersonalSundries); setLowProductionCommodity(p, CommodityClothes); setLowProductionCommodity(p, CommodityElectronics); setLowProductionCommodity(p, CommodityPlastics); setLowProductionCommodity(p, CommodityHandTools); } break; case PlanetTypeStation: { // a lot of ??? drugs? setHighProductionCommodity(p, CommodityAlcohol); setHighProductionCommodity(p, CommodityPersonalSundries); setHighProductionCommodity(p, CommodityClothes); setHighProductionCommodity(p, CommodityElectronics); setHighProductionCommodity(p, CommodityPlastics); setHighProductionCommodity(p, CommodityHandTools); setLowProductionCommodity(p, CommodityHydrogenFuel); setLowProductionCommodity(p, CommodityOxygen); setLowProductionCommodity(p, CommodityWater); setLowProductionCommodity(p, CommodityFertilizer); setLowProductionCommodity(p, CommodityMeat); setLowProductionCommodity(p, CommodityGrain); setLowProductionCommodity(p, CommoditySpices); } break; case PlanetTypeNull: case PlanetType_Count: break; } } } // 2. spin off sendNetworkUpdates() infinite loop thread UDPServer listener = createUDPServer(SERVER_PORT); if (!listener.ready) { exitWithErrorMessage("Couldn't start the udp server"); } Thread send_thread = spawnThread(&sendNetworkUpdates, &listener.server_socket); // 3. infinitely wait for incoming UDP messages and process them (usually by just dropping user-commands into the relevant block of shared memory) Thread recv_thread = spawnThread(&receiveNetworkUpdates, &listener); u64 lane_broadcast_val = 0; Barrier barrier = osBarrierAlloc(GAME_THREAD_CONCURRENCY); LaneCtx lane_ctxs[GAME_THREAD_CONCURRENCY] = {0}; Thread game_threads[GAME_THREAD_CONCURRENCY] = {0}; for (u32 i = 0; i < GAME_THREAD_CONCURRENCY; i++) { lane_ctxs[i].lane_idx = i; lane_ctxs[i].lane_count = GAME_THREAD_CONCURRENCY; lane_ctxs[i].barrier = barrier; lane_ctxs[i].broadcast_memory = &lane_broadcast_val; game_threads[i] = spawnThread(&gameLoop, &lane_ctxs[i]); } for (u32 i = 0; i < GAME_THREAD_CONCURRENCY; i++) { osThreadJoin(game_threads[i], MAX_u64); } osThreadJoin(recv_thread, MAX_u64); osThreadJoin(send_thread, MAX_u64); return 0; }