1696 lines
68 KiB
C
1696 lines
68 KiB
C
/*
|
|
* Code conventions:
|
|
* MyStructType
|
|
* myFunction()
|
|
* MyMacro()
|
|
* my_variable
|
|
* MY_CONSTANT
|
|
* */
|
|
#include <stdlib.h>
|
|
#include <sys/random.h>
|
|
#include <math.h>
|
|
#include "shared.h"
|
|
#include "base/impl.c"
|
|
#define NET_OUTGOING_MESSAGE_QUEUE_LEN (64)
|
|
#define NET_SERVER_MAX_CLIENTS (16)
|
|
#include "lib/network.c"
|
|
#include "lib/tui.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_MAX_HEAP_MEMORY MB(256)
|
|
#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
|
|
#define SYSTEM_MESSAGES_LEN 40
|
|
#define MAX_SYSTEM_MESSAGE_LEN 512
|
|
#define SBUFLEN (512)
|
|
|
|
///// TypeDefs
|
|
typedef enum Tab {
|
|
TabDebug,
|
|
TabMap,
|
|
Tab_Count,
|
|
} Tab;
|
|
|
|
typedef struct ParsedClientCommand {
|
|
CommandType type;
|
|
u8 byte;
|
|
u8 people;
|
|
u8 time_limit;
|
|
u16 sender_port;
|
|
u16 alt_port;
|
|
u32 sender_ip;
|
|
u32 alt_ip;
|
|
i32 socket_fd;
|
|
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 {
|
|
bool active;
|
|
bool needs_systems_init;
|
|
i32 socket_fd;
|
|
u64 account_id;
|
|
SocketAddress address;
|
|
CommandType commands[CLIENT_COMMAND_LIST_LEN];
|
|
} Client;
|
|
|
|
typedef struct ClientList {
|
|
u64 length;
|
|
u64 capacity;
|
|
Client* items;
|
|
} ClientList;
|
|
|
|
typedef struct State {
|
|
bool all_accounts_ready;
|
|
Tab tab;
|
|
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;
|
|
String server_ip_address;
|
|
ParsedClientCommandThreadQueue* network_recv_queue;
|
|
OutgoingMessageQueue* network_send_queue;
|
|
StarSystem map[STAR_SYSTEM_COUNT];
|
|
} State;
|
|
|
|
///// Global Variables
|
|
global State state = { 0 };
|
|
global str TAB_STRS[Tab_Count] = {"Debug", "Map"};
|
|
global Arena permanent_arena = { 0 };
|
|
global bool debug_mode = false;
|
|
global bool should_quit = false;
|
|
global u8List system_messages[SYSTEM_MESSAGES_LEN] = {0};
|
|
global u8 system_message_index = 0;
|
|
|
|
///// functionImplementations()
|
|
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 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, i32 socket_fd) {
|
|
Client new_client = {
|
|
.active = true,
|
|
.socket_fd = socket_fd,
|
|
.address = addr,
|
|
};
|
|
|
|
// first, try to overwrite an old dc'ed client
|
|
for (u32 i = 1; i < clients->length; i++) {
|
|
if (clients->items[i].active == false) {
|
|
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 findClientHandle(ClientList* clients, SocketAddress address, i32 socket_fd) {
|
|
for (u32 i = 0; i < clients->length; i++) {
|
|
Client c = clients->items[i];
|
|
if (socketAddressEqual(address, c.address) && socket_fd == c.socket_fd) {
|
|
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 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 NetworkMessage makeMessageSystemPassengers(StarSystem* sys) {
|
|
NetworkMessage outgoing_message = { .use_socket = true };
|
|
u32 msg_i = 0;
|
|
outgoing_message.bytes[msg_i++] = (u8)MessageSystemPassengers;
|
|
outgoing_message.bytes[msg_i++] = (u8)sys->idx;
|
|
for (u32 i = 0; i < MAX_PASSENGER_JOB_OFFERS; i++) {
|
|
if (sys->offers[i].people) {
|
|
outgoing_message.bytes[msg_i++] = sys->offers[i].goal_system_idx;
|
|
outgoing_message.bytes[msg_i++] = sys->offers[i].people;
|
|
outgoing_message.bytes[msg_i++] = sys->offers[i].time_limit;
|
|
msg_i += writeU32ToBufferLE(outgoing_message.bytes + msg_i, sys->offers[i].offer);
|
|
}
|
|
}
|
|
outgoing_message.bytes_len = msg_i;
|
|
return outgoing_message;
|
|
}
|
|
|
|
fn NetworkMessage makeMessageSystemCommodities(StarSystem* sys) {
|
|
NetworkMessage outgoing_message = { .use_socket = true };
|
|
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++) {
|
|
msg_i += writeU32ToBufferLE(outgoing_message.bytes + msg_i, sys->planets[i].commodities[ii]);
|
|
}
|
|
}
|
|
outgoing_message.bytes_len = msg_i;
|
|
return outgoing_message;
|
|
}
|
|
|
|
fn void sendMessageTransactionResult(i32 socket_fd, SocketAddress addr, u32 qty, bool buying, u32 credits) {
|
|
NetworkMessage outgoing_message = { .address = addr, .use_socket = true, .socket_fd = socket_fd };
|
|
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);
|
|
addSystemMessage((u8*)"Message TransactionResult sent");
|
|
}
|
|
|
|
fn NetworkMessage makeMessagePlayerDetails(PlayerShip ship) {
|
|
NetworkMessage 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]);
|
|
}
|
|
for (u32 i = 0; i < MAX_PASSENGER_BERTHS; i++) {
|
|
outgoing_message.bytes[msg_i++] = ship.passengers[i].goal_system_idx;
|
|
outgoing_message.bytes[msg_i++] = ship.passengers[i].people;
|
|
outgoing_message.bytes[msg_i++] = ship.passengers[i].turns_remaining;
|
|
msg_i += writeU32ToBufferLE(outgoing_message.bytes + msg_i, ship.passengers[i].reward);
|
|
}
|
|
outgoing_message.bytes_len = msg_i;
|
|
return outgoing_message;
|
|
}
|
|
|
|
fn void sendMessageJobAcceptResult(i32 socket_fd, SocketAddress addr, bool result) {
|
|
NetworkMessage outgoing_message = { .address = addr, .use_socket = true, .socket_fd = socket_fd };
|
|
u32 msg_i = 0;
|
|
outgoing_message.bytes[msg_i++] = (u8)MessageJobAcceptResult;
|
|
outgoing_message.bytes[msg_i++] = result;
|
|
outgoing_message.bytes_len = msg_i;
|
|
outgoingMessageQueuePush(state.network_send_queue, &outgoing_message);
|
|
addSystemMessage((u8*)"MessageJobAcceptResult sent");
|
|
}
|
|
|
|
fn void sendMessagePayoffResult(i32 socket_fd, SocketAddress addr) {
|
|
NetworkMessage outgoing_message = { .address = addr, .use_socket = true, .socket_fd = socket_fd };
|
|
u32 msg_i = 0;
|
|
outgoing_message.bytes[msg_i++] = (u8)MessagePayoffResult;
|
|
outgoing_message.bytes_len = msg_i;
|
|
outgoingMessageQueuePush(state.network_send_queue, &outgoing_message);
|
|
addSystemMessage((u8*)"MessagePayoffResult sent");
|
|
}
|
|
|
|
fn void sendMessageStarPositions(i32 socket_fd, SocketAddress addr) {
|
|
NetworkMessage outgoing_message = { .use_socket = true, .address = addr, .socket_fd = socket_fd };
|
|
// tell the client about the map
|
|
u32 star_msg_size = 2+MAX_PLANETS;
|
|
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);
|
|
char sbuf[SBUFLEN] = {0};
|
|
sprintf(sbuf, "MessageStarPositions sent socket=%d, message_len=%d", socket_fd, outgoing_message.bytes_len);
|
|
addSystemMessage((u8*)sbuf);
|
|
}
|
|
|
|
fn void sendMessagePlayerDetails(i32 socket_fd, PlayerShip ship, SocketAddress addr) {
|
|
NetworkMessage outgoing_message = makeMessagePlayerDetails(ship);
|
|
outgoing_message.address = addr;
|
|
outgoing_message.use_socket = true;
|
|
outgoing_message.socket_fd = socket_fd;
|
|
outgoingMessageQueuePush(state.network_send_queue, &outgoing_message);
|
|
char sbuf[SBUFLEN] = {0};
|
|
sprintf(sbuf, "MessagePlayerDetails sent socket=%d, message_len=%d", socket_fd, outgoing_message.bytes_len);
|
|
addSystemMessage((u8*)sbuf);
|
|
}
|
|
|
|
fn void sendMessageAuctionBidResult(i32 socket_fd, SocketAddress addr, AuctionBidResult result) {
|
|
NetworkMessage outgoing_message = { .address = addr, .use_socket = true, .socket_fd = socket_fd };
|
|
u32 msg_i = 0;
|
|
outgoing_message.bytes[msg_i++] = (u8)MessageAuctionBidResult;
|
|
outgoing_message.bytes[msg_i++] = result;
|
|
outgoing_message.bytes_len = msg_i;
|
|
outgoingMessageQueuePush(state.network_send_queue, &outgoing_message);
|
|
addSystemMessage((u8*)"Message AuctionBidResult sent");
|
|
}
|
|
|
|
fn void handleIncomingMessage(u8* message, u32 len, SocketAddress sender, i32 socket) {
|
|
char sbuf[SBUFLEN] = {0};
|
|
sprintf(sbuf, "%d: %s from %s:%d\n", len, COMMAND_TYPE_STRINGS[message[0]], inet_ntoa(sender.sin_addr), sender.sin_port);
|
|
addSystemMessage((u8*)sbuf);
|
|
u32 msg_idx = 0;
|
|
ParsedClientCommand parsed = {
|
|
.type = (CommandType)message[msg_idx++],
|
|
.socket_fd = socket,
|
|
.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 CommandBuyAuction: break; // nothing to parse but the 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;
|
|
|
|
MemoryZero(sbuf, SBUFLEN);
|
|
sprintf(sbuf, "Logging in player: %s %d %s\n", COMMAND_TYPE_STRINGS[parsed.type], name_len, message + 7);
|
|
addSystemMessage((u8*)sbuf);
|
|
} break;
|
|
case CommandPayMortgage: {
|
|
parsed.id = readU64FromBufferLE(message + msg_idx);
|
|
MemoryZero(sbuf, SBUFLEN);
|
|
sprintf(sbuf, "paying off %lld", parsed.id);
|
|
addSystemMessage((u8*)sbuf);
|
|
} 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: {
|
|
addSystemMessage((u8*)"command transact received");
|
|
parsed.byte = message[1]; // buy?
|
|
parsed.qty = readU32FromBufferLE(message + 2);
|
|
parsed.commodity = message[6];
|
|
} break;
|
|
case CommandCreateCharacter: {
|
|
addSystemMessage((u8*)"command create character received");
|
|
parsed.byte = message[1];
|
|
} break;
|
|
case CommandAcceptPassengerJob: {
|
|
addSystemMessage((u8*)"command accept passenger job received\n");
|
|
parsed.byte = message[1];
|
|
parsed.people = message[2];
|
|
parsed.time_limit = message[3];
|
|
parsed.qty = readU32FromBufferLE(message + 4);
|
|
} break;
|
|
case CommandInvalid:
|
|
case CommandType_Count: {
|
|
dbg("invalid command type");
|
|
} break;
|
|
}
|
|
|
|
pccThreadSafeQueuePush(state.network_recv_queue, &parsed);
|
|
}
|
|
|
|
fn void removeClientBySocketFd(i32 socket_fd) {
|
|
Client blank_client = {0};
|
|
// i=1 because first client is null-client
|
|
for (u32 i = 1; i < state.clients.length; i++) {
|
|
if (state.clients.items[i].socket_fd == socket_fd) {
|
|
state.clients.items[i] = blank_client;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn void* receiveNetworkUpdates(void* tcp_server) {
|
|
TCPServer server = *(TCPServer*)tcp_server;
|
|
char sbuf[SBUFLEN] = {0};
|
|
sprintf(sbuf, "receiveNetworkUpdates() sock=%d\n", server.socket_fd);
|
|
addSystemMessage((u8*)sbuf);
|
|
infiniteReadTCPServer(&server, &should_quit, handleIncomingMessage, removeClientBySocketFd, addSystemMessage);
|
|
return NULL;
|
|
}
|
|
|
|
fn void* sendNetworkUpdates(void* tcp_server) {
|
|
TCPServer server = *(TCPServer*)tcp_server;
|
|
ThreadContext tctx = {0};
|
|
tctxInit(&tctx);
|
|
i32 socket = server.socket_fd;
|
|
char sbuf[SBUFLEN] = {0};
|
|
u64 send_loop = 0;
|
|
while (!should_quit) {
|
|
send_loop += 1;
|
|
u64 loop_start = osTimeMicrosecondsNow();
|
|
|
|
// 1. clear out our "outgoingMessage" queue
|
|
{
|
|
NetworkMessage to_send = { 0 };
|
|
NetworkMessage* next_to_send = outgoingMessageNonblockingQueuePop(state.network_send_queue, &to_send);
|
|
u8List bytes_list = { 0 };
|
|
while (next_to_send != NULL) {
|
|
if (to_send.use_socket) {
|
|
char sbuf[SBUFLEN] = {0};
|
|
sprintf(sbuf, "sendTCPMessage() sock=%d msg=%s len=%d\n", to_send.socket_fd, MESSAGE_STRINGS[to_send.bytes[0]], to_send.bytes_len);
|
|
addSystemMessage((u8*)sbuf);
|
|
sendTCPMessage(to_send);
|
|
} else { // UDP
|
|
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.active == false) {
|
|
continue;
|
|
}
|
|
|
|
if (send_loop % 2 == 0) {
|
|
// send the client the current auction details for their current system
|
|
Account* account = &state.accounts[client.account_id];
|
|
if (!accountIsEmpty(account)) {
|
|
StarSystem curr = state.map[account->ship.system_idx];
|
|
NetworkMessage auction_msg = {
|
|
.use_socket = true,
|
|
.socket_fd = client.socket_fd,
|
|
.address = client.address,
|
|
};
|
|
u32 msgidx = 0;
|
|
auction_msg.bytes[msgidx++] = MessageAuctionDetails;
|
|
auction_msg.bytes[msgidx++] = curr.auction.type;
|
|
auction_msg.bytes[msgidx++] = curr.auction.qty;
|
|
msgidx += writeU32ToBufferLE((u8*)auction_msg.bytes + msgidx, curr.auction.price);
|
|
msgidx += writeU32ToBufferLE((u8*)auction_msg.bytes + msgidx, curr.auction.started_at);
|
|
msgidx += writeU32ToBufferLE((u8*)auction_msg.bytes + msgidx, curr.auction.finished_at);
|
|
auction_msg.bytes_len = msgidx;
|
|
sendTCPMessage(auction_msg);
|
|
}
|
|
}
|
|
|
|
// update all the changed systems
|
|
NetworkMessage msg_data;
|
|
for (u32 ii = 0; ii < STAR_SYSTEM_COUNT; ii++) {
|
|
if (state.map[ii].changed == true || client.needs_systems_init) {
|
|
// send commodities
|
|
msg_data = makeMessageSystemCommodities(&state.map[ii]);
|
|
msg_data.address = client.address;
|
|
msg_data.socket_fd = client.socket_fd;
|
|
msg_data.use_socket = true;
|
|
sendTCPMessage(msg_data);
|
|
|
|
// send passenger jobs
|
|
MemoryZero(sbuf, SBUFLEN);
|
|
sprintf(sbuf, "sending passenger jobs for %s\n", STAR_NAMES[ii]);
|
|
addSystemMessage((u8*)sbuf);
|
|
msg_data = makeMessageSystemPassengers(&state.map[ii]);
|
|
msg_data.address = client.address;
|
|
msg_data.socket_fd = client.socket_fd;
|
|
msg_data.use_socket = true;
|
|
sendTCPMessage(msg_data);
|
|
}
|
|
}
|
|
// 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);
|
|
msg_data.address = client.address;
|
|
msg_data.socket_fd = client.socket_fd;
|
|
msg_data.use_socket = true;
|
|
sendTCPMessage(msg_data);
|
|
}
|
|
}
|
|
|
|
state.clients.items[i].needs_systems_init = false;
|
|
}
|
|
} 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 void* gameLoop(void* params) {
|
|
LaneCtx* lane_ctx = (LaneCtx*)params;
|
|
ThreadContext tctx = {
|
|
.lane_ctx = *lane_ctx,
|
|
};
|
|
tctxInit(&tctx);
|
|
char sbuf[SBUFLEN] = {0};
|
|
sprintf(sbuf, "Lane %lld (%lld) of %lld starting.", lane_ctx->lane_idx, LaneIdx(), lane_ctx->lane_count);
|
|
addSystemMessage((u8*)sbuf);
|
|
NetworkMessage outgoing_message = { .use_socket = true };
|
|
u64 loop_start;
|
|
u64 last_burn = 0;
|
|
u64 last_hp_regen = 0;
|
|
Arena scratch_arena = {0};
|
|
arenaInit(&scratch_arena);
|
|
while (!should_quit) {
|
|
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 = findClientHandle(&state.clients, sender, msg.socket_fd);
|
|
Client* client = &state.clients.items[client_handle];
|
|
switch (msg.type) {
|
|
case CommandBuyAuction: {
|
|
if (client_handle == 0) break;
|
|
Account* account = &state.accounts[client->account_id];
|
|
StarSystem* player_sys = &state.map[account->ship.system_idx];
|
|
bool is_auction_still_running = player_sys->auction.finished_at == 0;
|
|
if (is_auction_still_running) {
|
|
bool player_has_money_for_purchase = account->ship.credits >= player_sys->auction.price;
|
|
bool player_has_cargo_space_for_purchase = (account->ship.vacuum_cargo_slots - usedVacuumCargoSlots(account->ship)) >= player_sys->auction.qty;
|
|
if (player_has_cargo_space_for_purchase && player_has_money_for_purchase) {
|
|
player_sys->auction.finished_at = state.frame;
|
|
account->ship.credits -= player_sys->auction.price;
|
|
account->ship.commodities[player_sys->auction.type] += player_sys->auction.qty;
|
|
sendMessageAuctionBidResult(msg.socket_fd, sender, AuctionBidResultPurchased);
|
|
sendMessagePlayerDetails(msg.socket_fd, account->ship, sender);
|
|
} else if (!player_has_money_for_purchase) {
|
|
sendMessageAuctionBidResult(msg.socket_fd, sender, AuctionBidResultNotEnoughMoney);
|
|
} else {
|
|
sendMessageAuctionBidResult(msg.socket_fd, sender, AuctionBidResultNotEnoughCargoSpace);
|
|
}
|
|
} else {
|
|
sendMessageAuctionBidResult(msg.socket_fd, sender, AuctionBidResultAuctionAlreadyFinished);
|
|
}
|
|
} break;
|
|
case CommandAcceptPassengerJob: {
|
|
if (client_handle == 0) break;
|
|
// move the offer out of the system and INTO the PlayerShip
|
|
Account* account = &state.accounts[client->account_id];
|
|
StarSystem* player_sys = &state.map[account->ship.system_idx];
|
|
PassengerJobOffer pjo = {
|
|
.goal_system_idx = msg.byte,
|
|
.people = msg.people,
|
|
.time_limit = msg.time_limit,
|
|
.offer = msg.qty,
|
|
};
|
|
// find the matching job, and IF the ship has room,
|
|
// "accept" it by clearing it from the system list and adding it to the ship
|
|
bool succeeded = false;
|
|
for (u32 i = 0; i < MAX_PASSENGER_JOB_OFFERS; i++) {
|
|
if (passengerJobEq(player_sys->offers[i], pjo)) {
|
|
if (shipAvailablePassengerBerths(account->ship) > 0) {
|
|
for (u32 ii = 0; ii < MAX_PASSENGER_BERTHS; ii++) {
|
|
if (account->ship.passengers[ii].people == 0) {
|
|
account->ship.passengers[ii].people = pjo.people;
|
|
account->ship.passengers[ii].goal_system_idx = pjo.goal_system_idx;
|
|
account->ship.passengers[ii].turns_remaining = pjo.time_limit;
|
|
account->ship.passengers[ii].reward = pjo.offer;
|
|
account->changed = true;
|
|
MemoryZero(&player_sys->offers[i], sizeof(PassengerJobOffer));
|
|
player_sys->changed = true;
|
|
succeeded = true;
|
|
sendMessageJobAcceptResult(client->socket_fd, sender, succeeded);
|
|
// end both for loops "break break;"
|
|
ii = MAX_PASSENGER_BERTHS;
|
|
i = MAX_PASSENGER_JOB_OFFERS;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!succeeded) {
|
|
sendMessageJobAcceptResult(client->socket_fd, sender, succeeded);
|
|
}
|
|
} break;
|
|
case CommandPayMortgage: {
|
|
if (client_handle == 0) break;
|
|
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(client->socket_fd, sender);
|
|
} break;
|
|
case CommandSetDestination: {
|
|
if (client_handle == 0) break;
|
|
Account* account = &state.accounts[client->account_id];
|
|
account->destination_sys_idx = msg.byte;
|
|
account->changed = true;
|
|
} break;
|
|
case CommandReadyStatus: {
|
|
if (client_handle == 0) break;
|
|
Account* account = &state.accounts[client->account_id];
|
|
account->ship.ready_to_depart = msg.byte;
|
|
account->changed = true;
|
|
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: {
|
|
if (client_handle == 0) break;
|
|
MemoryZero(sbuf, SBUFLEN);
|
|
sprintf(sbuf, "Transact for client_handle=%d, on #%lld", client_handle, state.frame);
|
|
addSystemMessage((u8*)sbuf);
|
|
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;
|
|
account->changed = true;
|
|
sendMessagePlayerDetails(client->socket_fd, account->ship, sender);
|
|
sendMessageTransactionResult(client->socket_fd, sender, qty_traded, is_buying_from_system, credit_value);
|
|
} break;
|
|
case CommandLogin: {
|
|
if (client_handle == 0) {
|
|
client_handle = pushClient(&state.clients, sender, msg.socket_fd);
|
|
client = &state.clients.items[client_handle];
|
|
MemoryZero(sbuf, SBUFLEN);
|
|
sprintf(sbuf, "pushed new client handle = %d\n", client_handle);
|
|
addSystemMessage((u8*)sbuf);
|
|
}
|
|
|
|
/*
|
|
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);
|
|
MemoryZero(sbuf, SBUFLEN);
|
|
sprintf(sbuf, "name(%d): %s pw(%d): %s acct?: %d\n", name.length, name.bytes, pw.length, pw.bytes, existing_account != NULL);
|
|
addSystemMessage((u8*)sbuf);
|
|
if (existing_account) {
|
|
addSystemMessage((u8*)" existing account\n");
|
|
bool pw_matches = stringsEq(&pw, &existing_account->pw);
|
|
arenaDealloc(&permanent_arena, pw.capacity);
|
|
arenaDealloc(&permanent_arena, name.capacity);
|
|
if (pw_matches) {
|
|
addSystemMessage((u8*)" pw matched\n");
|
|
existing_account->changed = true;
|
|
sendMessageStarPositions(client->socket_fd, sender);
|
|
} else {
|
|
// tell the client they did a bad pw
|
|
outgoing_message.bytes[0] = (u8)MessageBadPw;
|
|
outgoing_message.bytes_len = 1;
|
|
outgoing_message.address = sender;
|
|
outgoing_message.socket_fd = client->socket_fd;
|
|
outgoingMessageQueuePush(state.network_send_queue, &outgoing_message);
|
|
addSystemMessage((u8*)"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;
|
|
}
|
|
}
|
|
MemoryZero(sbuf, SBUFLEN);
|
|
sprintf(sbuf, "new account id=%d\n", existing_account->id);
|
|
addSystemMessage((u8*)sbuf);
|
|
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;
|
|
outgoing_message.socket_fd = client->socket_fd;
|
|
outgoingMessageQueuePush(state.network_send_queue, &outgoing_message);
|
|
addSystemMessage((u8*)"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;
|
|
outgoing_message.socket_fd = client->socket_fd;
|
|
outgoingMessageQueuePush(state.network_send_queue, &outgoing_message);
|
|
addSystemMessage((u8*)"MessageNewAccountCreated sent\n");
|
|
}
|
|
client->needs_systems_init = true;
|
|
MemoryZero(sbuf, SBUFLEN);
|
|
sprintf(sbuf, "client_handle=%d, acct_id=%d\n", client_handle, existing_account->id);
|
|
addSystemMessage((u8*)sbuf);
|
|
} break;
|
|
case CommandCreateCharacter: {
|
|
if (client_handle == 0) break;
|
|
Account* account = &state.accounts[client->account_id];
|
|
MemoryZero(sbuf, SBUFLEN);
|
|
sprintf(sbuf, "creating character for account id=%d, %s\n", account->id, SHIP_TYPE_STRINGS[msg.byte]);
|
|
addSystemMessage((u8*)sbuf);
|
|
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 = 2000.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;
|
|
|
|
MemoryZero(sbuf, SBUFLEN);
|
|
sprintf(sbuf, "ship_type=%s, client_handle=%d, acct_id=%d\n", SHIP_TYPE_STRINGS[msg.byte], client_handle, account->id);
|
|
addSystemMessage((u8*)sbuf);
|
|
|
|
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.socket_fd = client->socket_fd;
|
|
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);
|
|
addSystemMessage((u8*)"MessageCharacterId sent\n");
|
|
|
|
sendMessagePlayerDetails(client->socket_fd, account->ship, sender);
|
|
|
|
sendMessageStarPositions(client->socket_fd, sender);
|
|
|
|
outgoing_message = makeMessageSystemCommodities(&starting_system);
|
|
outgoing_message.address = sender;
|
|
outgoing_message.socket_fd = client->socket_fd;
|
|
outgoingMessageQueuePush(state.network_send_queue, &outgoing_message);
|
|
MemoryZero(sbuf, SBUFLEN);
|
|
sprintf(sbuf, "%s sent\n", MESSAGE_STRINGS[outgoing_message.bytes[0]]);
|
|
addSystemMessage((u8*)sbuf);
|
|
} else {
|
|
addSystemMessage((u8*)"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 < NET_SERVER_MAX_CLIENTS; i++) {
|
|
if (state.clients.items[i].active) {
|
|
outgoing_message.address = state.clients.items[i].address;
|
|
outgoing_message.socket_fd = state.clients.items[i].socket_fd;
|
|
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 < NET_SERVER_MAX_CLIENTS; i++) {
|
|
if (state.clients.items[i].active) {
|
|
outgoing_message.address = state.clients.items[i].address;
|
|
outgoing_message.socket_fd = state.clients.items[i].socket_fd;
|
|
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;
|
|
}
|
|
}
|
|
|
|
// tick all the star systems (auctions)
|
|
if (player_count > 0) { // don't bother until there's actually a player
|
|
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];
|
|
|
|
// tick the auction price
|
|
u32 grace_period_ends_at = sys->auction.started_at + (GOAL_GAME_LOOPS_PER_S * 3);
|
|
bool is_auction_still_running = sys->auction.finished_at == 0;
|
|
bool is_auction_grace_period_finished = state.frame > grace_period_ends_at;
|
|
if (is_auction_still_running && is_auction_grace_period_finished) {
|
|
f32 initial_price = COMMODITIES[sys->auction.type].price * AUCTION_PRICE_START_MULTIPLE;
|
|
f32 t_sec = ((f32)state.frame - (f32)grace_period_ends_at) / (f32)GOAL_GAME_LOOPS_PER_S;
|
|
// t is in minutes
|
|
f32 t = t_sec / 60;
|
|
f32 decay = -0.65 * t;
|
|
f32 floor_price = COMMODITIES[sys->auction.type].price * 0.9;
|
|
sys->auction.price = Max((initial_price * pow(EULERS_E, decay)), floor_price);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. tick non-user entities
|
|
if (state.all_accounts_ready) {
|
|
addSystemMessage((u8*)"NEW TURN: ticking 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;
|
|
// 1. consume and produce commodities
|
|
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];
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. potentially create a new passenger job
|
|
u32 planet_divisor = ((MAX_PLANETS - starSystemPlanetCount(sys))+1);
|
|
u32 max_passenger_job_count = 1 + ((MAX_PASSENGER_JOB_OFFERS-1) / planet_divisor);
|
|
for (u32 ii = 0; ii < max_passenger_job_count; ii++) {
|
|
if (sys->offers[ii].people == 0) {
|
|
// TODO choose a system further away
|
|
sys->offers[ii].goal_system_idx = (i+1+(rand() % 6)) % STAR_SYSTEM_COUNT;
|
|
sys->offers[ii].people = 1 + (rand() % (MAX_PASSENGER_JOB_PEOPLE - 1));
|
|
sys->offers[ii].time_limit = 3 + (rand() % 5);
|
|
// TODO compute based on distance between people and time_limit
|
|
sys->offers[ii].offer = 1000 + (rand() % MAX_PASSENGER_JOB_PRICE);
|
|
break;
|
|
} else { //increase the offer amount each turn
|
|
sys->offers[ii].offer = (u32)((f32)sys->offers[ii].offer * 1.05);
|
|
}
|
|
}
|
|
|
|
// 3. reset the auction
|
|
MemoryZero(&sys->auction, sizeof(Auction));
|
|
sys->auction.started_at = state.frame;
|
|
sys->auction.type = AUCTION_COMMODITY_CUTOFF + (rand() % (Commodity_Count - AUCTION_COMMODITY_CUTOFF));
|
|
sys->auction.price = COMMODITIES[sys->auction.type].price * AUCTION_PRICE_START_MULTIPLE;
|
|
sys->auction.qty = COMMODITIES[sys->auction.type].consumption;
|
|
}
|
|
|
|
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];
|
|
u32 projected_oxygen_cost = oxyCostForTravel(&acct->ship, curr_pos, dest_pos);
|
|
bool have_enough_oxy = projected_oxygen_cost <= acct->ship.commodities[CommodityOxygen];
|
|
if (have_enough_fuel || have_enough_oxy) {
|
|
// remove the fuel they used on the journey
|
|
acct->ship.commodities[CommodityHydrogenFuel] -= projected_fuel_cost;
|
|
// remove the oxygen they used on the journey
|
|
acct->ship.commodities[CommodityOxygen] -= projected_oxygen_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;
|
|
}
|
|
}
|
|
|
|
// check for the completion of a passenger job (MUST COME AFTER acct->ship.system_idx is updated)
|
|
for (u32 ii = 0; ii < MAX_PASSENGER_BERTHS; ii++) {
|
|
Passenger* job = &acct->ship.passengers[ii];
|
|
bool is_valid_job = job->people > 0;
|
|
bool is_at_job_destination = job->goal_system_idx == acct->ship.system_idx;
|
|
if (is_valid_job) {
|
|
if (is_at_job_destination) {
|
|
acct->ship.credits += job->reward;
|
|
MemoryZero(sbuf, SBUFLEN);
|
|
sprintf(sbuf, "%d credits awarded to %s for finishing passenger job\n", job->reward, acct->name.bytes);
|
|
addSystemMessage((u8*)sbuf);
|
|
MemoryZero(job, sizeof(Passenger));
|
|
MemoryZero(sbuf, SBUFLEN);
|
|
sprintf(sbuf, "%s FINISHED a passenger job!\n", acct->name.bytes);
|
|
addSystemMessage((u8*)sbuf);
|
|
// send a job completion message?
|
|
for (u32 i = 1; i < state.clients.length; i++) {
|
|
if (state.clients.items[i].active && state.clients.items[i].account_id == acct->id) {
|
|
outgoing_message.address = state.clients.items[i].address;
|
|
outgoing_message.socket_fd = state.clients.items[i].socket_fd;
|
|
outgoing_message.bytes_len = 2;
|
|
outgoing_message.bytes[0] = (u8)MessageJobComplete;
|
|
outgoing_message.bytes[1] = true;
|
|
outgoingMessageQueuePush(state.network_send_queue, &outgoing_message);
|
|
MemoryZero(sbuf, SBUFLEN);
|
|
sprintf(sbuf, "sent MessageJobComplete to %s\n", acct->name.bytes);
|
|
addSystemMessage((u8*)sbuf);
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
job->turns_remaining -= 1;
|
|
if (job->turns_remaining == 0) {
|
|
// they failed the job
|
|
MemoryZero(job, sizeof(Passenger));
|
|
MemoryZero(sbuf, SBUFLEN);
|
|
sprintf(sbuf, "%s failed a passenger job\n", acct->name.bytes);
|
|
addSystemMessage((u8*)sbuf);
|
|
// send a job failed message?
|
|
for (u32 i = 1; i < state.clients.length; i++) {
|
|
if (state.clients.items[i].active && state.clients.items[i].account_id == acct->id) {
|
|
outgoing_message.address = state.clients.items[i].address;
|
|
outgoing_message.socket_fd = state.clients.items[i].socket_fd;
|
|
outgoing_message.bytes_len = 2;
|
|
outgoing_message.bytes[0] = (u8)MessageJobComplete;
|
|
outgoing_message.bytes[1] = false;
|
|
outgoingMessageQueuePush(state.network_send_queue, &outgoing_message);
|
|
MemoryZero(sbuf, SBUFLEN);
|
|
sprintf(sbuf, "sent MessageJobComplete to %s\n", acct->name.bytes);
|
|
addSystemMessage((u8*)sbuf);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
MemoryZero(sbuf, SBUFLEN);
|
|
sprintf(sbuf, "%s WON!!! %d\n", acct->name.bytes, i);
|
|
addSystemMessage((u8*)sbuf);
|
|
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);
|
|
if (t == CommodityHydrogenFuel || t == CommodityOxygen) {
|
|
p->commodities[t] = COMMODITIES[t].qty;
|
|
}
|
|
}
|
|
|
|
fn void setLowProductionCommodity(Planet* p, CommodityType t) {
|
|
p->commodities[t] = rand() % (COMMODITIES[t].qty / 2);
|
|
p->production[t] = rand() % (COMMODITIES[t].consumption / 2);
|
|
if (t == CommodityHydrogenFuel || t == CommodityOxygen) {
|
|
p->commodities[t] = COMMODITIES[t].qty / 2;
|
|
}
|
|
}
|
|
|
|
fn bool updateAndRender(TuiState* tui, void* s, u8* input_buffer, u64 loop_count) {
|
|
State* state = (State*) s;
|
|
Dim2 screen_dimensions = tui->screen_dimensions;
|
|
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};
|
|
|
|
// 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_space = input_buffer[0] == ' ' && input_buffer[1] == 0;
|
|
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;
|
|
|
|
if (user_pressed_tab || user_pressed_shift_tab) {
|
|
state->tab = state->tab == TabDebug ? TabMap : TabDebug;
|
|
}
|
|
|
|
// show what ip to connect to
|
|
renderStrToBuffer(tui->frame_buffer, screen_dimensions.width - 18, 1, state->server_ip_address.bytes, screen_dimensions);
|
|
|
|
if (input_buffer[0] == 'q' || user_pressed_esc) {
|
|
should_quit = true;
|
|
}
|
|
if (state->someone_won) {
|
|
for (u32 i = 0; i < ACCOUNT_LEN; i++) {
|
|
Account* acct = &state->accounts[i];
|
|
if (!shipIsNull(&acct->ship)) {
|
|
if (acct->ship.remaining_mortgage == 0) {
|
|
MemoryZero(sbuf, SBUFLEN);
|
|
sprintf(sbuf, "%s WON!!!", acct->name.bytes);
|
|
renderStrToBuffer(tui->frame_buffer, (screen_dimensions.width-strlen(sbuf)) /2, 10, sbuf, screen_dimensions);
|
|
return should_quit;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// draw the tabs box
|
|
u32 tabs_y = 0;
|
|
u32 box_y = tabs_y + 2;
|
|
Box box = {
|
|
.x = 1,
|
|
.y = box_y,
|
|
.width = screen_dimensions.width - 3,
|
|
.height = screen_dimensions.height - box_y - 2,
|
|
};
|
|
drawAnsiBox(tui->frame_buffer, box, screen_dimensions, true);
|
|
u32 tx = 2;
|
|
// draw the actual tabs
|
|
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->tab == i);
|
|
renderStrToBuffer(tui->frame_buffer, tx+1, tabs_y+ 1, TAB_STRS[i], screen_dimensions);
|
|
tx += (tab_len + 3);
|
|
}
|
|
|
|
switch (state->tab) {
|
|
case TabDebug: {
|
|
renderSystemMessages(tui->frame_buffer, tui->screen_dimensions, box);
|
|
} break;
|
|
case TabMap: {
|
|
u8 COLORS[] = {ANSI_HIGHLIGHT_RED, ANSI_HIGHLIGHT_BLUE, ANSI_HIGHLIGHT_YELLOW, ANSI_HIGHLIGHT_GREEN, ANSI_WHITE};
|
|
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;
|
|
}
|
|
|
|
// render the damn map
|
|
u32 x_off = box.x+((box.width - MAP_WIDTH*xw)/2);
|
|
u32 y_off = box.y+1;
|
|
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] = ' ';
|
|
tui->frame_buffer[bufpos].foreground = ANSI_WHITE;
|
|
tui->frame_buffer[bufpos].background = ANSI_BLACK;
|
|
// try to colorize the map cell if a player is on it
|
|
for (u32 iii = 0; iii < ACCOUNT_LEN; iii++) {
|
|
Account* acct = &state->accounts[iii];
|
|
if (acct->name.length > 0) {
|
|
StarSystem sys = state->map[acct->ship.system_idx];
|
|
if (sys.x == sysx && sys.y == sysy) {
|
|
Box current_key = { .height = yh, .width = xw, .x = x_off+i, .y = y_off+ii };
|
|
colorizeBox(tui, current_key, COLORS[iii], 0, ' ');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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)
|
|
);
|
|
renderStrToBuffer(tui->frame_buffer, sysx-((strlen(STAR_NAMES[i])-4)/2), sysy-1, STAR_NAMES[i], screen_dimensions);
|
|
}
|
|
// TODO fix the background bleed from previous line
|
|
|
|
// map key
|
|
y_off += 1+(MAP_HEIGHT*yh);
|
|
x_off = box.x + 3;
|
|
for (u32 i = 0; i < ACCOUNT_LEN; i++, x_off += 18) {
|
|
Account* acct = &state->accounts[i];
|
|
if (acct->name.length > 0) {
|
|
Box current_key = { .height = yh, .width = xw, .x = x_off, .y = y_off };
|
|
colorizeBox(tui, current_key, COLORS[i], 0, ' ');
|
|
renderStrToBuffer(tui->frame_buffer, x_off+5, y_off, acct->name.bytes, screen_dimensions);
|
|
MemoryZero(sbuf, SBUFLEN);
|
|
sprintf(sbuf, "$%d", acct->ship.remaining_mortgage);
|
|
renderStrToBuffer(tui->frame_buffer, x_off+5, y_off+1, sbuf, screen_dimensions);
|
|
}
|
|
}
|
|
|
|
} break;
|
|
case Tab_Count: {} break;
|
|
}
|
|
|
|
return should_quit;
|
|
}
|
|
|
|
|
|
// 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
|
|
// - this is 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 = NET_SERVER_MAX_CLIENTS;
|
|
state.clients.length = 1; // making entry 0 to be a "null" client
|
|
state.clients.items = (Client*)arenaAllocArray(&permanent_arena, Client, NET_SERVER_MAX_CLIENTS);
|
|
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);
|
|
}
|
|
state.server_ip_address.bytes = arenaAllocArraySized(&permanent_arena, sizeof(u8), 16);
|
|
state.server_ip_address.capacity = 16;
|
|
{
|
|
struct ifaddrs* ifaddr, *ifa;
|
|
getifaddrs(&ifaddr);
|
|
for (ifa = ifaddr; ifa; ifa = ifa->ifa_next) {
|
|
if (!ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_INET) continue;
|
|
if (strcmp(ifa->ifa_name, "en0") != 0) continue;
|
|
inet_ntop(AF_INET, &((struct sockaddr_in*)ifa->ifa_addr)->sin_addr, state.server_ip_address.bytes, 16);
|
|
}
|
|
freeifaddrs(ifaddr);
|
|
}
|
|
|
|
// GAME LOGIC SETUP
|
|
|
|
// set position of all star systems
|
|
state.map[0].x = 3;
|
|
state.map[0].y = 4;
|
|
state.map[1].x = 13;
|
|
state.map[1].y = 2;
|
|
state.map[2].x = 22;
|
|
state.map[2].y = 3;
|
|
state.map[3].x = 25;
|
|
state.map[3].y = 8;
|
|
state.map[4].x = 17;
|
|
state.map[4].y = 9;
|
|
state.map[5].x = 6;
|
|
state.map[5].y = 7;
|
|
for (u32 i = 0; i < STAR_SYSTEM_COUNT; i++) {
|
|
state.map[i].idx = i;
|
|
state.map[i].name = STAR_NAMES[i];
|
|
if (state.map[i].x == 0 && state.map[i].y == 0) {
|
|
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 and passenger jobs for the systems
|
|
char sbuf[SBUFLEN] = {0};
|
|
for (u32 i = 0; i < STAR_SYSTEM_COUNT; i++) {
|
|
// setup the auction
|
|
state.map[i].auction.started_at = state.frame;
|
|
state.map[i].auction.type = AUCTION_COMMODITY_CUTOFF + (rand() % (Commodity_Count - AUCTION_COMMODITY_CUTOFF));
|
|
state.map[i].auction.price = COMMODITIES[state.map[i].auction.type].price * AUCTION_PRICE_START_MULTIPLE;
|
|
state.map[i].auction.qty = COMMODITIES[state.map[i].auction.type].consumption;
|
|
// setup the planets
|
|
u32 planet_count = rand() % MAX_PLANETS + 1;
|
|
switch (i) {
|
|
case 0:
|
|
addSystemMessage((u8*)"Vega Earth Gas");
|
|
planet_count = 2;
|
|
state.map[i].planets[0].type = PlanetTypeEarth;
|
|
state.map[i].planets[1].type = PlanetTypeGas;
|
|
break;
|
|
case 1:
|
|
addSystemMessage((u8*)"Aldebara Earth Earth Asteroid");
|
|
planet_count = 3;
|
|
state.map[i].planets[0].type = PlanetTypeEarth;
|
|
state.map[i].planets[1].type = PlanetTypeEarth;
|
|
state.map[i].planets[2].type = PlanetTypeAsteroid;
|
|
break;
|
|
case 2:
|
|
addSystemMessage((u8*)"Mining Colony 17 Asteroid");
|
|
planet_count = 1;
|
|
state.map[i].planets[0].type = PlanetTypeAsteroid;
|
|
break;
|
|
case 3:
|
|
planet_count = 2;
|
|
state.map[i].planets[0].type = PlanetTypeGas;
|
|
state.map[i].planets[1].type = PlanetTypeMoon;
|
|
break;
|
|
case 4:
|
|
planet_count = 3;
|
|
state.map[i].planets[0].type = PlanetTypeStation;
|
|
state.map[i].planets[1].type = PlanetTypeAsteroid;
|
|
state.map[i].planets[2].type = PlanetTypeAsteroid;
|
|
break;
|
|
case 5:
|
|
planet_count = 2;
|
|
state.map[i].planets[0].type = PlanetTypeStation;
|
|
state.map[i].planets[1].type = PlanetTypeMoon;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
for (u32 ii = 0; ii < planet_count; ii++) {
|
|
Planet* p = &state.map[i].planets[ii];
|
|
if (p->type == PlanetTypeNull) {
|
|
addSystemMessage((u8*)"random planet type");
|
|
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);
|
|
}
|
|
// everyone starts with "a lot" of fuel and o2
|
|
p->commodities[CommodityHydrogenFuel] = COMMODITIES[CommodityHydrogenFuel].qty;
|
|
p->commodities[CommodityOxygen] = COMMODITIES[CommodityOxygen].qty;
|
|
// now, override for the "specialty" of the planet
|
|
switch (p->type) {
|
|
case PlanetTypeEarth: {
|
|
setHighProductionCommodity(p, CommodityWater);
|
|
setHighProductionCommodity(p, CommodityFood);
|
|
|
|
setLowProductionCommodity(p, CommodityHydrogenFuel);
|
|
setLowProductionCommodity(p, CommodityOre);
|
|
setLowProductionCommodity(p, CommodityMetals);
|
|
} break;
|
|
case PlanetTypeGas: {
|
|
// a lot of fuel and chemicals
|
|
setHighProductionCommodity(p, CommodityHydrogenFuel);
|
|
setHighProductionCommodity(p, CommodityOxygen);
|
|
setHighProductionCommodity(p, CommodityWater);
|
|
setHighProductionCommodity(p, CommodityPlastics);
|
|
|
|
setLowProductionCommodity(p, CommodityOre);
|
|
setLowProductionCommodity(p, CommodityMetals);
|
|
setLowProductionCommodity(p, CommodityRawTextiles);
|
|
setLowProductionCommodity(p, CommodityGlass);
|
|
setLowProductionCommodity(p, CommodityHandTools);
|
|
} break;
|
|
case PlanetTypeMoon: {
|
|
// a lot of manufactured goods
|
|
setHighProductionCommodity(p, CommodityElectronics);
|
|
setHighProductionCommodity(p, CommodityGlass);
|
|
|
|
setLowProductionCommodity(p, CommodityOre);
|
|
setLowProductionCommodity(p, CommodityHydrogenFuel);
|
|
setLowProductionCommodity(p, CommodityOxygen);
|
|
setLowProductionCommodity(p, CommodityWater);
|
|
setLowProductionCommodity(p, CommodityFood);
|
|
} break;
|
|
case PlanetTypeAsteroid: {
|
|
// a lot of raw metals
|
|
setHighProductionCommodity(p, CommodityOre);
|
|
setHighProductionCommodity(p, CommodityMetals);
|
|
setHighProductionCommodity(p, CommoditySemiConductors);
|
|
|
|
setLowProductionCommodity(p, CommodityHydrogenFuel);
|
|
setLowProductionCommodity(p, CommodityOxygen);
|
|
setLowProductionCommodity(p, CommodityWater);
|
|
setLowProductionCommodity(p, CommodityFood);
|
|
setLowProductionCommodity(p, CommodityElectronics);
|
|
setLowProductionCommodity(p, CommodityPlastics);
|
|
setLowProductionCommodity(p, CommodityHandTools);
|
|
} break;
|
|
case PlanetTypeStation: {
|
|
// a lot of ??? drugs?
|
|
setHighProductionCommodity(p, CommodityElectronics);
|
|
setHighProductionCommodity(p, CommodityPlastics);
|
|
setHighProductionCommodity(p, CommodityHandTools);
|
|
|
|
setLowProductionCommodity(p, CommodityHydrogenFuel);
|
|
setLowProductionCommodity(p, CommodityOxygen);
|
|
setLowProductionCommodity(p, CommodityWater);
|
|
setLowProductionCommodity(p, CommodityFood);
|
|
} break;
|
|
case PlanetTypeNull:
|
|
case PlanetType_Count:
|
|
break;
|
|
}
|
|
}
|
|
|
|
MemoryZero(sbuf, SBUFLEN);
|
|
sprintf(
|
|
sbuf,
|
|
"%s: fuel: %d o2: %d",
|
|
STAR_NAMES[i],
|
|
state.map[i].planets[0].commodities[CommodityHydrogenFuel] +
|
|
state.map[i].planets[1].commodities[CommodityHydrogenFuel] +
|
|
state.map[i].planets[2].commodities[CommodityHydrogenFuel],
|
|
state.map[i].planets[0].commodities[CommodityOxygen] +
|
|
state.map[i].planets[1].commodities[CommodityOxygen] +
|
|
state.map[i].planets[2].commodities[CommodityOxygen]
|
|
);
|
|
addSystemMessage((u8*)sbuf);
|
|
|
|
u32 planet_divisor = ((MAX_PLANETS - planet_count)+1);
|
|
u32 offer_count = 1 + (rand() % (MAX_PASSENGER_JOB_OFFERS / planet_divisor));
|
|
for (u32 ii = 0; ii < offer_count; ii++) {
|
|
// TODO choose a system further away
|
|
state.map[i].offers[ii].goal_system_idx = (i+1+(rand() % 6)) % STAR_SYSTEM_COUNT;
|
|
state.map[i].offers[ii].people = 1 + (rand() % (MAX_PASSENGER_JOB_PEOPLE - 1));
|
|
state.map[i].offers[ii].time_limit = 1 + (rand() % 3);
|
|
// TODO compute based on distance between people and time_limit
|
|
state.map[i].offers[ii].offer = 1000 + (rand() % MAX_PASSENGER_JOB_PRICE);
|
|
}
|
|
}
|
|
|
|
// 2. spin off sendNetworkUpdates() infinite loop thread
|
|
TCPServer server = createTCPServer(SERVER_PORT);
|
|
if (!server.ready) {
|
|
exitWithErrorMessage("Couldn't start the server");
|
|
}
|
|
Thread send_thread = spawnThread(&sendNetworkUpdates, &server);
|
|
// 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, &server);
|
|
|
|
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]);
|
|
}
|
|
infiniteUILoop(
|
|
MAX_SCREEN_WIDTH,
|
|
MAX_SCREEN_HEIGHT,
|
|
GOAL_GAME_LOOP_US,
|
|
&state,
|
|
updateAndRender
|
|
);
|
|
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;
|
|
}
|
|
|