ClassiCube/src/Server.c
UnknownShadow200 c89e224050 Whoops
2025-06-01 12:00:24 +10:00

558 lines
17 KiB
C

#include "Server.h"
#include "String.h"
#include "BlockPhysics.h"
#include "Game.h"
#include "Drawer2D.h"
#include "Chat.h"
#include "Block.h"
#include "Event.h"
#include "Http.h"
#include "Funcs.h"
#include "Entity.h"
#include "Graphics.h"
#include "Gui.h"
#include "Screens.h"
#include "Formats.h"
#include "Generator.h"
#include "World.h"
#include "Camera.h"
#include "TexturePack.h"
#include "Menus.h"
#include "Logger.h"
#include "Protocol.h"
#include "Inventory.h"
#include "Platform.h"
#include "Input.h"
#include "Errors.h"
#include "Options.h"
static char nameBuffer[STRING_SIZE];
static char motdBuffer[STRING_SIZE];
static char appBuffer[STRING_SIZE];
static int ticks;
struct _ServerConnectionData Server;
/*########################################################################################################################*
*-----------------------------------------------------Common handlers-----------------------------------------------------*
*#########################################################################################################################*/
static void Server_ResetState(void) {
Server.Disconnected = false;
Server.SupportsExtPlayerList = false;
Server.SupportsPlayerClick = false;
Server.SupportsPartialMessages = false;
Server.SupportsFullCP437 = false;
Server.SupportsNotifyAction = false;
}
void Server_RetrieveTexturePack(const cc_string* url) {
if (!Game_AllowServerTextures || TextureUrls_HasDenied(url)) return;
if (!url->length || TextureUrls_HasAccepted(url)) {
TexturePack_Extract(url);
} else {
TexPackOverlay_Show(url);
}
}
/*########################################################################################################################*
*--------------------------------------------------------PingList---------------------------------------------------------*
*#########################################################################################################################*/
struct PingEntry { cc_int64 sent, recv; cc_uint16 id; };
static struct PingEntry ping_entries[10];
static int ping_head;
int Ping_NextPingId(void) {
int head = ping_head;
int next = ping_entries[head].id + 1;
head = (head + 1) % Array_Elems(ping_entries);
ping_entries[head].id = next;
ping_entries[head].sent = Stopwatch_Measure();
ping_entries[head].recv = 0;
ping_head = head;
return next;
}
void Ping_Update(int id) {
int i;
for (i = 0; i < Array_Elems(ping_entries); i++) {
if (ping_entries[i].id != id) continue;
ping_entries[i].recv = Stopwatch_Measure();
return;
}
}
int Ping_AveragePingMS(void) {
int i, measures = 0, totalMs;
cc_int64 total = 0;
for (i = 0; i < Array_Elems(ping_entries); i++) {
struct PingEntry entry = ping_entries[i];
if (!entry.sent || !entry.recv) continue;
total += entry.recv - entry.sent;
measures++;
}
if (!measures) return 0;
totalMs = Stopwatch_ElapsedMS(0, total);
/* (recv - send) is average time for packet to be sent to server and then sent back. */
/* However for ping, only want average time to send data to server, so half the total. */
totalMs /= 2;
return totalMs / measures;
}
static void Ping_Reset(void) {
Mem_Set(ping_entries, 0, sizeof(ping_entries));
ping_head = 0;
}
/*########################################################################################################################*
*-------------------------------------------------Singleplayer connection-------------------------------------------------*
*#########################################################################################################################*/
static char autoloadBuffer[FILENAME_SIZE];
cc_string SP_AutoloadMap = String_FromArray(autoloadBuffer);
static void SPConnection_BeginConnect(void) {
static const cc_string logName = String_FromConst("Singleplayer");
RNGState rnd;
int horSize, verSize;
Chat_SetLogName(&logName);
Game_UseCPEBlocks = Game_Version.HasCPE;
/* For when user drops a map file onto ClassiCube.exe */
if (SP_AutoloadMap.length) {
Map_LoadFrom(&SP_AutoloadMap); return;
}
Random_SeedFromCurrentTime(&rnd);
World_NewMap();
#if defined CC_BUILD_NDS || defined CC_BUILD_PS1 || defined CC_BUILD_SATURN || defined CC_BUILD_MACCLASSIC || defined CC_BUILD_32X || defined CC_BUILD_GBA
horSize = 16;
verSize = 16;
#elif defined CC_BUILD_LOWMEM
horSize = 64;
verSize = 64;
#else
horSize = Game_ClassicMode ? 256 : 128;
verSize = 64;
#endif
World_SetDimensions(horSize, verSize, horSize);
#if defined CC_BUILD_N64 || defined CC_BUILD_NDS || defined CC_BUILD_PS1 || defined CC_BUILD_SATURN || defined CC_BUILD_32X || defined CC_BUILD_GBA
Gen_Active = &FlatgrassGen;
#else
Gen_Active = &NotchyGen;
#endif
Gen_Seed = Random_Next(&rnd, Int32_MaxValue);
Gen_Start();
GeneratingScreen_Show();
}
static char sp_lastCol;
static void SPConnection_AddPart(const cc_string* text) {
cc_string tmp; char tmpBuffer[STRING_SIZE * 2];
char col;
int i;
String_InitArray(tmp, tmpBuffer);
/* Prepend color codes for subsequent lines of multi-line chat */
if (!Drawer2D_IsWhiteColor(sp_lastCol)) {
String_Append(&tmp, '&');
String_Append(&tmp, sp_lastCol);
}
String_AppendString(&tmp, text);
/* Replace all % with & */
for (i = 0; i < tmp.length; i++) {
if (tmp.buffer[i] == '%') tmp.buffer[i] = '&';
}
String_UNSAFE_TrimEnd(&tmp);
col = Drawer2D_LastColor(&tmp, tmp.length);
if (col) sp_lastCol = col;
Chat_Add(&tmp);
}
static void SPConnection_SendChat(const cc_string* text) {
cc_string left, part;
if (!text->length) return;
sp_lastCol = '\0';
left = *text;
while (left.length > STRING_SIZE) {
part = String_UNSAFE_Substring(&left, 0, STRING_SIZE);
SPConnection_AddPart(&part);
left = String_UNSAFE_SubstringAt(&left, STRING_SIZE);
}
SPConnection_AddPart(&left);
}
static void SPConnection_SendBlock(int x, int y, int z, BlockID old, BlockID now) {
Physics_OnBlockChanged(x, y, z, old, now);
}
static void SPConnection_SendData(const cc_uint8* data, cc_uint32 len) { }
static void SPConnection_Tick(struct ScheduledTask* task) {
if (Server.Disconnected) return;
/* 60 -> 20 ticks a second */
if ((ticks++ % 3) != 0) return;
Physics_Tick();
TexturePack_CheckPending();
}
static void SPConnection_Init(void) {
Server_ResetState();
Physics_Init();
Server.BeginConnect = SPConnection_BeginConnect;
Server.Tick = SPConnection_Tick;
Server.SendBlock = SPConnection_SendBlock;
Server.SendChat = SPConnection_SendChat;
Server.SendData = SPConnection_SendData;
Server.SupportsFullCP437 = !Game_ClassicMode;
Server.SupportsPartialMessages = true;
Server.IsSinglePlayer = true;
}
/*########################################################################################################################*
*--------------------------------------------------Multiplayer connection-------------------------------------------------*
*#########################################################################################################################*/
static cc_socket net_socket = -1;
static cc_result net_writeFailure;
static void OnClose(void);
#ifdef CC_BUILD_NETWORKING
static cc_uint8 net_readBuffer[4096 * 5];
static cc_uint8* net_readCurrent;
static double net_lastPacket;
static cc_uint8 lastOpcode;
static cc_bool net_connecting;
static float net_connectElapsed;
#define NET_TIMEOUT_SECS 15
static void MPConnection_FinishConnect(void) {
net_connecting = false;
Event_RaiseVoid(&NetEvents.Connected);
Event_RaiseFloat(&WorldEvents.Loading, 0.0f);
net_readCurrent = net_readBuffer;
net_lastPacket = Game.Time;
Classic_SendLogin();
}
static void MPConnection_Fail(const cc_string* reason) {
cc_string msg; char msgBuffer[STRING_SIZE * 2];
String_InitArray(msg, msgBuffer);
net_connecting = false;
String_Format2(&msg, "Failed to connect to %s:%i", &Server.Address, &Server.Port);
Game_Disconnect(&msg, reason);
OnClose();
}
static void MPConnection_FailConnect(cc_result result) {
static const cc_string reason = String_FromConst("You failed to connect to the server. It's probably down!");
cc_string msg; char msgBuffer[STRING_SIZE * 2];
String_InitArray(msg, msgBuffer);
if (result) {
String_Format3(&msg, "Error connecting to %s:%i: %e" _NL, &Server.Address, &Server.Port, &result);
Logger_Log(&msg);
}
MPConnection_Fail(&reason);
}
static void MPConnection_TickConnect(struct ScheduledTask* task) {
cc_bool writable;
cc_result res = Socket_CheckWritable(net_socket, &writable);
net_connectElapsed += task->interval;
if (res) {
MPConnection_FailConnect(res);
} else if (writable) {
MPConnection_FinishConnect();
} else if (net_connectElapsed > NET_TIMEOUT_SECS) {
MPConnection_FailConnect(0);
} else {
float left = NET_TIMEOUT_SECS - net_connectElapsed;
Event_RaiseFloat(&WorldEvents.Loading, left / NET_TIMEOUT_SECS);
}
}
static void MPConnection_BeginConnect(void) {
static const cc_string invalid_reason = String_FromConst("Invalid IP address");
cc_string title; char titleBuffer[STRING_SIZE];
cc_sockaddr addrs[SOCKET_MAX_ADDRS];
int numValidAddrs;
cc_result res;
String_InitArray(title, titleBuffer);
/* Default block permissions (in case server supports SetBlockPermissions but doesn't send) */
Blocks.CanPlace[BLOCK_AIR] = false;
Blocks.CanPlace[BLOCK_LAVA] = false; Blocks.CanDelete[BLOCK_LAVA] = false;
Blocks.CanPlace[BLOCK_WATER] = false; Blocks.CanDelete[BLOCK_WATER] = false;
Blocks.CanPlace[BLOCK_STILL_LAVA] = false; Blocks.CanDelete[BLOCK_STILL_LAVA] = false;
Blocks.CanPlace[BLOCK_STILL_WATER] = false; Blocks.CanDelete[BLOCK_STILL_WATER] = false;
Blocks.CanPlace[BLOCK_BEDROCK] = false; Blocks.CanDelete[BLOCK_BEDROCK] = false;
res = Socket_ParseAddress(&Server.Address, Server.Port, addrs, &numValidAddrs);
if (res == ERR_INVALID_ARGUMENT) {
MPConnection_Fail(&invalid_reason); return;
} else if (res) {
MPConnection_FailConnect(res); return;
}
res = Socket_Create(&net_socket, &addrs[0], true);
if (res) { MPConnection_FailConnect(res); return; }
res = Socket_Connect(net_socket, &addrs[0]);
if (res && res != ReturnCode_SocketInProgess && res != ReturnCode_SocketWouldBlock) {
MPConnection_FailConnect(res);
} else {
Server.Disconnected = false;
net_connecting = true;
net_connectElapsed = 0;
String_Format2(&title, "Connecting to %s:%i..", &Server.Address, &Server.Port);
LoadingScreen_Show(&title, &String_Empty);
}
}
static void MPConnection_SendBlock(int x, int y, int z, BlockID old, BlockID now) {
if (now == BLOCK_AIR) {
now = Inventory_SelectedBlock;
Classic_SendSetBlock(x, y, z, false, now);
} else {
Classic_SendSetBlock(x, y, z, true, now);
}
}
static void MPConnection_SendChat(const cc_string* text) {
cc_string left;
if (!text->length || net_connecting) return;
left = *text;
while (left.length > STRING_SIZE) {
Classic_SendChat(&left, true);
left = String_UNSAFE_SubstringAt(&left, STRING_SIZE);
}
Classic_SendChat(&left, false);
}
static void MPConnection_Disconnect(void) {
static const cc_string title = String_FromConst("Disconnected!");
static const cc_string reason = String_FromConst("You've lost connection to the server");
Game_Disconnect(&title, &reason);
}
static void DisconnectReadFailed(cc_result res) {
cc_string msg; char msgBuffer[STRING_SIZE * 2];
String_InitArray(msg, msgBuffer);
String_Format3(&msg, "Error reading from %s:%i: %e" _NL, &Server.Address, &Server.Port, &res);
Logger_Log(&msg);
MPConnection_Disconnect();
}
static void DisconnectInvalidOpcode(cc_uint8 opcode) {
static const cc_string title = String_FromConst("Disconnected");
cc_string tmp; char tmpBuffer[STRING_SIZE];
String_InitArray(tmp, tmpBuffer);
String_Format2(&tmp, "Server sent invalid packet %b! (prev %b)", &opcode, &lastOpcode);
Game_Disconnect(&title, &tmp); return;
}
static void MPConnection_Tick(struct ScheduledTask* task) {
Net_Handler handler;
cc_uint8* readEnd;
cc_uint8* readCur;
cc_uint32 read;
int i, remaining;
cc_result res;
if (Server.Disconnected) return;
if (net_connecting) { MPConnection_TickConnect(task); return; }
/* NOTE: using a read call that is a multiple of 4096 (appears to?) improve read performance */
res = Socket_Read(net_socket, net_readCurrent, 4096 * 4, &read);
if (res) {
/* 'no data available for non-blocking read' is an expected error */
if (res == ReturnCode_SocketInProgess) res = 0;
if (res == ReturnCode_SocketWouldBlock) res = 0;
if (res) { DisconnectReadFailed(res); return; }
} else if (read == 0) {
/* recv only returns 0 read when socket is closed.. probably? */
/* Over 30 seconds since last packet, connection probably dropped */
/* TODO: Should this be checked unconditonally instead of just when read = 0 ? */
if (net_lastPacket + 30 < Game.Time) { MPConnection_Disconnect(); return; }
} else {
readCur = net_readBuffer;
readEnd = net_readCurrent + read;
net_lastPacket = Game.Time;
while (readCur < readEnd) {
cc_uint8 opcode = readCur[0];
/* Workaround for older D3 servers which wrote one byte too many for HackControl packets */
if (cpe_needD3Fix && lastOpcode == OPCODE_HACK_CONTROL && (opcode == 0x00 || opcode == 0xFF)) {
Platform_LogConst("Skipping invalid HackControl byte from D3 server");
readCur++;
LocalPlayer_ResetJumpVelocity(Entities.CurPlayer);
continue;
}
if (readCur + Protocol.Sizes[opcode] > readEnd) break;
handler = Protocol.Handlers[opcode];
if (!handler) { DisconnectInvalidOpcode(opcode); return; }
lastOpcode = opcode;
handler(readCur + 1); /* skip opcode */
readCur += Protocol.Sizes[opcode];
}
/* Protocol packets might be split up across TCP packets */
/* If so, copy last few unprocessed bytes back to beginning of buffer */
/* These bytes are then later combined with subsequently read TCP packet data */
remaining = (int)(readEnd - readCur);
for (i = 0; i < remaining; i++)
{
net_readBuffer[i] = readCur[i];
}
net_readCurrent = net_readBuffer + remaining;
}
if (net_writeFailure) {
Platform_Log1("Error from send: %e", &net_writeFailure);
MPConnection_Disconnect(); return;
}
/* Network is ticked 60 times a second. We only send position updates 20 times a second */
if ((ticks++ % 3) != 0) return;
TexturePack_CheckPending();
Protocol_Tick();
}
static void MPConnection_SendData(const cc_uint8* data, cc_uint32 len) {
cc_uint32 wrote;
cc_result res;
int tries = 0;
if (Server.Disconnected) return;
while (len) {
res = Socket_Write(net_socket, data, len, &wrote);
/* If sending would block (send buffer full), retry for a bit up to 10 seconds */
/* TODO: Avoid doing this and manually buffer data when this happens */
if (res && tries < 1000 && (res == ReturnCode_SocketInProgess || res == ReturnCode_SocketWouldBlock)) {
Thread_Sleep(10);
tries++;
continue;
}
/* NOTE: Not immediately disconnecting here, as otherwise we sometimes miss out on kick messages */
if (res) { net_writeFailure = res; return; }
if (!wrote) { net_writeFailure = ERR_INVALID_ARGUMENT; return; }
data += wrote; len -= wrote;
}
}
static void MPConnection_Init(void) {
Server_ResetState();
Server.IsSinglePlayer = false;
Server.BeginConnect = MPConnection_BeginConnect;
Server.Tick = MPConnection_Tick;
Server.SendBlock = MPConnection_SendBlock;
Server.SendChat = MPConnection_SendChat;
Server.SendData = MPConnection_SendData;
net_readCurrent = net_readBuffer;
}
#else
static void MPConnection_Init(void) { SPConnection_Init(); }
#endif
/*########################################################################################################################*
*---------------------------------------------------Component interface---------------------------------------------------*
*#########################################################################################################################*/
static void OnNewMap(void) {
int i;
if (Server.IsSinglePlayer) return;
/* wipe all existing entities */
for (i = 0; i < MAX_NET_PLAYERS; i++)
{
Entities_Remove(i);
}
}
static void OnInit(void) {
String_InitArray(Server.Name, nameBuffer);
String_InitArray(Server.MOTD, motdBuffer);
String_InitArray(Server.AppName, appBuffer);
if (!Server.Address.length) {
SPConnection_Init();
} else {
MPConnection_Init();
}
ScheduledTask_Add(GAME_NET_TICKS, Server.Tick);
String_AppendConst(&Server.AppName, GAME_APP_NAME);
String_AppendConst(&Server.AppName, Platform_AppNameSuffix);
#ifdef CC_BUILD_WEB
if (!Input_TouchMode) return;
Server.AppName.length = 0;
String_AppendConst(&Server.AppName, GAME_APP_ALT);
#endif
}
static void OnReset(void) {
if (Server.IsSinglePlayer) return;
net_writeFailure = 0;
OnClose();
}
static void OnFree(void) {
Server.Address.length = 0;
OnClose();
}
static void OnClose(void) {
if (Server.IsSinglePlayer) {
Physics_Free();
} else {
Ping_Reset();
if (Server.Disconnected) return;
Socket_Close(net_socket);
Server.Disconnected = true;
}
}
struct IGameComponent Server_Component = {
OnInit, /* Init */
OnFree, /* Free */
OnReset, /* Reset */
OnNewMap /* OnNewMap */
};