diff options
author | WlodekM <[email protected]> | 2024-06-16 10:35:45 +0300 |
---|---|---|
committer | WlodekM <[email protected]> | 2024-06-16 10:35:45 +0300 |
commit | abef6da56913f1c55528103e60a50451a39628b1 (patch) | |
tree | b3c8092471ecbb73e568cd0d336efa0e7871ee8d /src/Server.c |
initial commit
Diffstat (limited to 'src/Server.c')
-rw-r--r-- | src/Server.c | 555 |
1 files changed, 555 insertions, 0 deletions
diff --git a/src/Server.c b/src/Server.c new file mode 100644 index 0000000..63d4d27 --- /dev/null +++ b/src/Server.c @@ -0,0 +1,555 @@ +#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; +} + +void Server_RetrieveTexturePack(const cc_string* url) { + if (!Game_AllowServerTextures || TextureCache_HasDenied(url)) return; + + if (!url->length || TextureCache_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 + 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 + 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 double net_connectTimeout; +#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(void) { + cc_bool writable; + double now = Game.Time; + cc_result res = Socket_CheckWritable(net_socket, &writable); + + if (res) { + MPConnection_FailConnect(res); + } else if (writable) { + MPConnection_FinishConnect(); + } else if (now > net_connectTimeout) { + MPConnection_FailConnect(0); + } else { + double left = net_connectTimeout - now; + Event_RaiseFloat(&WorldEvents.Loading, (float)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_Connect(&net_socket, &addrs[0], true); + if (res == ERR_INVALID_ARGUMENT) { + MPConnection_Fail(&invalid_reason); + } else if (res && res != ReturnCode_SocketInProgess && res != ReturnCode_SocketWouldBlock) { + MPConnection_FailConnect(res); + } else { + Server.Disconnected = false; + net_connecting = true; + net_connectTimeout = Game.Time + NET_TIMEOUT_SECS; + + 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(); 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((EntityID)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 */ +}; |