mirror of
https://github.com/ClassiCube/ClassiCube.git
synced 2025-09-13 01:26:50 -04:00
558 lines
17 KiB
C
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 */
|
|
};
|