Huge Netcode/Resynchcode changes #134
No files matched your search
@@ -207,6 +207,18 @@ USE_FBO_OGL=1
|
||||
endif
|
||||
endif
|
||||
|
||||
ifndef NONET
|
||||
ifndef NOSATURNSYNCH
|
||||
DOSATURNSYNCH=1
|
||||
ifndef NOSATURNJOIN
|
||||
DOSATURNJOIN=1
|
||||
endif
|
||||
#ifndef NOSATURNPAK
|
||||
#DOSATURNPAK=1
|
||||
#endif
|
||||
endif
|
||||
endif
|
||||
|
||||
ifdef MINGW
|
||||
include win32/Makefile.cfg
|
||||
endif #ifdef MINGW
|
||||
@@ -386,6 +398,18 @@ OPTS+=-DUSE_FBO_OGL
|
||||
endif
|
||||
endif
|
||||
|
||||
ifndef NONET
|
||||
ifndef NOSATURNSYNCH
|
||||
OPTS+=-DDOSATURNSYNCH
|
||||
ifndef NOSATURNJOIN
|
||||
OPTS+=-DDOSATURNJOIN
|
||||
endif
|
||||
#ifndef NOSATURNPAK
|
||||
#OPTS+=-DDOSATURNPAK
|
||||
#endif
|
||||
endif
|
||||
endif
|
||||
|
||||
ifdef HAVE_MINIUPNPC
|
||||
ifdef NONET
|
||||
HAVE_MINIUPNPC=''
|
||||
|
||||
+339
-56
@@ -10,6 +10,7 @@
|
||||
/// \file d_clisrv.c
|
||||
/// \brief SRB2 Network game communication and protocol, all OS independent parts.
|
||||
|
||||
#include "doomdef.h"
|
||||
#include <time.h>
|
||||
#ifdef __GNUC__
|
||||
#include <unistd.h> //for unlink
|
||||
@@ -92,6 +93,10 @@ UINT8 playernode[MAXPLAYERS];
|
||||
// The actual timeout will be longer depending on the savegame length
|
||||
tic_t jointimeout = (3*TICRATE);
|
||||
static boolean sendingsavegame[MAXNETNODES]; // Are we sending the savegame?
|
||||
#ifdef SATURNSYNCH
|
||||
static boolean resendingsavegame[MAXNETNODES]; // Are we resending the savegame?
|
||||
static tic_t savegameresendcooldown[MAXNETNODES]; // How long before we can resend again?
|
||||
#endif
|
||||
static tic_t freezetimeout[MAXNETNODES]; // Until when can this node freeze the server before getting a timeout?
|
||||
|
||||
UINT16 pingmeasurecount = 1;
|
||||
@@ -124,6 +129,12 @@ static UINT8 resynch_local_inprogress = false; // WE are desynched and getting p
|
||||
static UINT8 player_joining = false;
|
||||
UINT8 hu_resynching = 0;
|
||||
|
||||
#ifdef SATURNSYNCH
|
||||
UINT8 hu_redownloadinggamestate = 0;
|
||||
static boolean can_receive_gamestate[MAXNETNODES];
|
||||
static UINT8 gamestate_resend_counter[MAXNETNODES];
|
||||
#endif
|
||||
|
||||
// kart, true when a player is connecting or disconnecting so that the gameplay has stopped in its tracks
|
||||
UINT8 hu_stopped = 0;
|
||||
|
||||
@@ -136,6 +147,18 @@ static boolean cl_packetmissed;
|
||||
// here it is for the secondary local player (splitscreen)
|
||||
static UINT8 mynode; // my address pointofview server
|
||||
|
||||
#ifdef SATURNSYNCH
|
||||
static boolean cl_redownloadinggamestate = false;
|
||||
#endif
|
||||
|
||||
#ifdef SATURNPAK
|
||||
boolean is_client_saturn[MAXNETNODES];
|
||||
#endif
|
||||
|
||||
#ifdef SATURNJOIN
|
||||
#define ISSATURN 69
|
||||
#endif
|
||||
|
||||
static UINT8 localtextcmd[MAXTEXTCMD];
|
||||
static UINT8 localtextcmd2[MAXTEXTCMD]; // splitscreen
|
||||
static UINT8 localtextcmd3[MAXTEXTCMD]; // splitscreen == 2
|
||||
@@ -172,7 +195,6 @@ typedef struct textcmdtic_s
|
||||
ticcmd_t netcmds[TICQUEUE][MAXPLAYERS];
|
||||
static textcmdtic_t *textcmds[TEXTCMD_HASH_SIZE] = {NULL};
|
||||
|
||||
|
||||
consvar_t cv_showjoinaddress = {"showjoinaddress", "On", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
|
||||
|
||||
consvar_t cv_shownodeip = {"showipinnodelist", "On", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
|
||||
@@ -1430,6 +1452,9 @@ static boolean CL_SendJoin(void)
|
||||
netbuffer->u.clientcfg.subversion = SUBVERSION;
|
||||
strncpy(netbuffer->u.clientcfg.application, SRB2APPLICATION,
|
||||
sizeof netbuffer->u.clientcfg.application);
|
||||
#ifdef SATURNJOIN
|
||||
netbuffer->u.clientcfg.issaturn = ISSATURN;
|
||||
#endif
|
||||
|
||||
return HSendPacket(servernode, false, 0, sizeof (clientconfig_pak));
|
||||
}
|
||||
@@ -1727,23 +1752,23 @@ static boolean SV_SendServerConfig(INT32 node)
|
||||
netbuffer->u.servercfg.discordinvites = (boolean)cv_discordinvites.value;
|
||||
|
||||
memcpy(netbuffer->u.servercfg.server_context, server_context, 8);
|
||||
|
||||
op = p = netbuffer->u.servercfg.varlengthinputs;
|
||||
|
||||
CV_SavePlayerNames(&p);
|
||||
CV_SaveNetVars(&p, false);
|
||||
{
|
||||
const size_t len = sizeof (serverconfig_pak) + (size_t)(p - op);
|
||||
|
||||
const size_t len = sizeof (serverconfig_pak) + (size_t)(p - op);
|
||||
|
||||
#ifdef DEBUGFILE
|
||||
if (debugfile)
|
||||
{
|
||||
fprintf(debugfile, "ServerConfig Packet about to be sent, size of packet:%s to node:%d\n",
|
||||
sizeu1(len), node);
|
||||
}
|
||||
if (debugfile)
|
||||
{
|
||||
fprintf(debugfile, "ServerConfig Packet about to be sent, size of packet:%s to node:%d\n",
|
||||
sizeu1(len), node);
|
||||
}
|
||||
#endif
|
||||
|
||||
waspacketsent = HSendPacket(node, true, 0, len);
|
||||
}
|
||||
waspacketsent = HSendPacket(node, true, 0, len);
|
||||
|
||||
#ifdef DEBUGFILE
|
||||
if (debugfile)
|
||||
@@ -1765,7 +1790,7 @@ static boolean SV_SendServerConfig(INT32 node)
|
||||
#ifdef JOININGAME
|
||||
#define SAVEGAMESIZE (768*1024)
|
||||
|
||||
static void SV_SendSaveGame(INT32 node)
|
||||
static void SV_SendSaveGame(INT32 node, boolean resending)
|
||||
{
|
||||
size_t length, compressedlen;
|
||||
UINT8 *savebuffer;
|
||||
@@ -1783,7 +1808,7 @@ static void SV_SendSaveGame(INT32 node)
|
||||
// Leave room for the uncompressed length.
|
||||
save_p = savebuffer + sizeof(UINT32);
|
||||
|
||||
P_SaveNetGame();
|
||||
P_SaveNetGame(resending);
|
||||
|
||||
length = save_p - savebuffer;
|
||||
if (length > SAVEGAMESIZE)
|
||||
@@ -1856,7 +1881,7 @@ static void SV_SavedGame(void)
|
||||
return;
|
||||
}
|
||||
|
||||
P_SaveNetGame();
|
||||
P_SaveNetGame(false);
|
||||
|
||||
length = save_p - savebuffer;
|
||||
if (length > SAVEGAMESIZE)
|
||||
@@ -1878,8 +1903,7 @@ static void SV_SavedGame(void)
|
||||
#endif
|
||||
#define TMPSAVENAME "$$$.sav"
|
||||
|
||||
|
||||
static void CL_LoadReceivedSavegame(void)
|
||||
static void CL_LoadReceivedSavegame(boolean reloading)
|
||||
{
|
||||
UINT8 *savebuffer = NULL;
|
||||
size_t length, decompressedlen;
|
||||
@@ -1914,29 +1938,23 @@ static void CL_LoadReceivedSavegame(void)
|
||||
automapactive = false;
|
||||
|
||||
// load a base level
|
||||
if (P_LoadNetGame())
|
||||
if (P_LoadNetGame(reloading))
|
||||
{
|
||||
CON_LogMessage(va(M_GetText("Map is now \"%s"), G_BuildMapName(gamemap)));
|
||||
if (strlen(mapheaderinfo[gamemap-1]->lvlttl) > 0)
|
||||
if (!reloading)
|
||||
{
|
||||
CON_LogMessage(va(": %s", mapheaderinfo[gamemap-1]->lvlttl));
|
||||
if (strlen(mapheaderinfo[gamemap-1]->zonttl) > 0)
|
||||
CON_LogMessage(va(" %s", mapheaderinfo[gamemap-1]->zonttl));
|
||||
else if (!(mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE))
|
||||
CON_LogMessage(M_GetText(" Zone"));
|
||||
if (strlen(mapheaderinfo[gamemap-1]->actnum) > 0)
|
||||
CON_LogMessage(va(" %s", mapheaderinfo[gamemap-1]->actnum));
|
||||
CON_LogMessage(va(M_GetText("Map is now \"%s"), G_BuildMapName(gamemap)));
|
||||
if (strlen(mapheaderinfo[gamemap-1]->lvlttl) > 0)
|
||||
{
|
||||
CON_LogMessage(va(": %s", mapheaderinfo[gamemap-1]->lvlttl));
|
||||
if (strlen(mapheaderinfo[gamemap-1]->zonttl) > 0)
|
||||
CON_LogMessage(va(" %s", mapheaderinfo[gamemap-1]->zonttl));
|
||||
else if (!(mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE))
|
||||
CON_LogMessage(M_GetText(" Zone"));
|
||||
if (strlen(mapheaderinfo[gamemap-1]->actnum) > 0)
|
||||
CON_LogMessage(va(" %s", mapheaderinfo[gamemap-1]->actnum));
|
||||
}
|
||||
CON_LogMessage("\"\n");
|
||||
}
|
||||
CON_LogMessage("\"\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
CONS_Alert(CONS_ERROR, M_GetText("Can't load the level!\n"));
|
||||
Z_Free(savebuffer);
|
||||
save_p = NULL;
|
||||
if (unlink(tmpsave) == -1)
|
||||
CONS_Alert(CONS_ERROR, M_GetText("Can't delete %s\n"), tmpsave);
|
||||
return;
|
||||
}
|
||||
|
||||
// done
|
||||
@@ -1946,20 +1964,61 @@ static void CL_LoadReceivedSavegame(void)
|
||||
CONS_Alert(CONS_ERROR, M_GetText("Can't delete %s\n"), tmpsave);
|
||||
consistancy[gametic%TICQUEUE] = Consistancy();
|
||||
CON_ToggleOff();
|
||||
|
||||
#ifdef SATURNSYNCH
|
||||
// Tell the server we have received and reloaded the gamestate
|
||||
// so they know they can resume the game
|
||||
if (reloading)
|
||||
{
|
||||
netbuffer->packettype = PT_RECEIVEDGAMESTATE;
|
||||
HSendPacket(servernode, true, 0, 0);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef SATURNSYNCH
|
||||
static void CL_ReloadReceivedSavegame(void)
|
||||
{
|
||||
INT32 i;
|
||||
|
||||
for (i = 0; i < MAXPLAYERS; i++)
|
||||
{
|
||||
LUA_InvalidatePlayer(&players[i]);
|
||||
sprintf(player_names[i], "Player %d", i + 1);
|
||||
}
|
||||
|
||||
CL_LoadReceivedSavegame(true);
|
||||
|
||||
if (neededtic < gametic)
|
||||
neededtic = gametic;
|
||||
maketic = neededtic;
|
||||
|
||||
// set camera angle to somewhat match where your player is turning
|
||||
for (i = 0; i <= splitscreen; i++)
|
||||
{
|
||||
localangle[i] = players[displayplayers[i]].cmd.angleturn<<16;
|
||||
}
|
||||
|
||||
camera->subsector = R_PointInSubsector(camera->x, camera->y);
|
||||
|
||||
cl_redownloadinggamestate = false;
|
||||
|
||||
CONS_Printf(M_GetText("Game state reloaded\n"));
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef NONET
|
||||
static void SendAskInfo(INT32 node)
|
||||
{
|
||||
tic_t asktime;
|
||||
#ifdef HOLEPUNCH
|
||||
#ifdef HOLEPUNCH
|
||||
if (node != 0 && node != BROADCASTADDR &&
|
||||
cv_rendezvousserver.string[0])
|
||||
{
|
||||
I_NetRequestHolePunch(node);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
asktime = I_GetTime();
|
||||
|
||||
@@ -2559,7 +2618,7 @@ static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic
|
||||
if (fileneeded[0].status == FS_FOUND)
|
||||
{
|
||||
// Gamestate is now handled within CL_LoadReceivedSavegame()
|
||||
CL_LoadReceivedSavegame();
|
||||
CL_LoadReceivedSavegame(false);
|
||||
cl_mode = CL_CONNECTED;
|
||||
break;
|
||||
} // don't break case continue to CL_CONNECTED
|
||||
@@ -2584,7 +2643,6 @@ static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic
|
||||
// Call it only once by tic
|
||||
if (*oldtic != I_GetTime())
|
||||
{
|
||||
|
||||
INT32 key;
|
||||
|
||||
I_OsPolling();
|
||||
@@ -3162,10 +3220,15 @@ void CL_RemovePlayer(INT32 playernum, INT32 reason)
|
||||
playerpernode[node]--;
|
||||
if (playerpernode[node] <= 0)
|
||||
{
|
||||
// If a resynch was in progress, well, it no longer needs to be.
|
||||
SV_InitResynchVars(node);
|
||||
SV_InitResynchVars(node); // If a resynch was in progress, well, it no longer needs to be.
|
||||
|
||||
nodeingame[node] = false;
|
||||
#ifdef SATURNPAK
|
||||
is_client_saturn[node] = false;
|
||||
#endif
|
||||
#ifdef SATURNSYNCH
|
||||
can_receive_gamestate[node] = false;
|
||||
#endif
|
||||
Net_CloseConnection(node);
|
||||
ResetNode(node);
|
||||
}
|
||||
@@ -3271,6 +3334,9 @@ void CL_Reset(void)
|
||||
if (servernode > 0 && servernode < MAXNETNODES)
|
||||
{
|
||||
nodeingame[(UINT8)servernode] = false;
|
||||
#ifdef SATURNPAK
|
||||
is_client_saturn[(UINT8)servernode] = false;
|
||||
#endif
|
||||
Net_CloseConnection(servernode);
|
||||
}
|
||||
D_CloseConnection(); // netgame = false
|
||||
@@ -3966,6 +4032,36 @@ static void Command_list_http_logins (void)
|
||||
}
|
||||
#endif/*HAVE_CURL*/
|
||||
|
||||
#ifdef SATURNSYNCH
|
||||
static void Command_ResendGamestate(void)
|
||||
{
|
||||
SINT8 playernum;
|
||||
|
||||
if (COM_Argc() == 1)
|
||||
{
|
||||
CONS_Printf(M_GetText("resendgamestate <playername/playernum>: resend the game state to a player\n"));
|
||||
return;
|
||||
}
|
||||
else if (client)
|
||||
{
|
||||
CONS_Printf(M_GetText("Only the server can use this.\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
playernum = nametonum(COM_Argv(1));
|
||||
if (playernum == -1 || playernum == 0)
|
||||
return;
|
||||
|
||||
// Send a PT_WILLRESENDGAMESTATE packet to the client so they know what's going on
|
||||
netbuffer->packettype = PT_WILLRESENDGAMESTATE;
|
||||
if (!HSendPacket(playernode[playernum], true, 0, 0))
|
||||
{
|
||||
CONS_Alert(CONS_ERROR, M_GetText("A problem occured, please try again.\n"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static CV_PossibleValue_t netticbuffer_cons_t[] = {{0, "MIN"}, {3, "MAX"}, {0, NULL}};
|
||||
consvar_t cv_netticbuffer = {"netticbuffer", "1", CV_SAVE, netticbuffer_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
|
||||
|
||||
@@ -3975,6 +4071,10 @@ consvar_t cv_joinrefusemessage = {"joinrefusemessage", "The server is not accept
|
||||
|
||||
consvar_t cv_allownewplayer = {"allowjoin", "On", CV_SAVE|CV_CALL, CV_OnOff, Joinable_OnChange, 0, NULL, NULL, 0, 0, NULL};
|
||||
|
||||
#ifdef SATURNJOIN
|
||||
consvar_t cv_allownewsaturnplayer = {"allowsaturnjoin", "On", CV_HIDEN, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
|
||||
#endif
|
||||
|
||||
#ifdef VANILLAJOINNEXTROUND
|
||||
consvar_t cv_joinnextround = {"joinnextround", "Off", CV_SAVE|CV_NETVAR, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL}; /// \todo not done
|
||||
#endif
|
||||
@@ -3987,7 +4087,16 @@ static CV_PossibleValue_t discordinvites_cons_t[] = {{0, "Admins Only"}, {1, "Ev
|
||||
consvar_t cv_discordinvites = {"discordinvites", "Everyone", CV_SAVE|CV_CALL, discordinvites_cons_t, Joinable_OnChange, 0, NULL, NULL, 0, 0, NULL};
|
||||
|
||||
static CV_PossibleValue_t resynchattempts_cons_t[] = {{0, "MIN"}, {20, "MAX"}, {0, NULL}};
|
||||
consvar_t cv_resynchattempts = {"resynchattempts", "2", CV_SAVE, resynchattempts_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL };
|
||||
consvar_t cv_resynchattempts = {"resynchattempts", "2", CV_SAVE, resynchattempts_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
|
||||
|
||||
#ifdef SATURNSYNCH
|
||||
static CV_PossibleValue_t gamestateattempts_cons_t[] = {{0, "MIN"}, {30, "MAX"}, {0, NULL}};
|
||||
consvar_t cv_gamestateattempts = {"gamestateresendattempts", "10", CV_SAVE, gamestateattempts_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
|
||||
|
||||
static CV_PossibleValue_t resynchcooldown_cons_t[] = {{0, "MIN"}, {20, "MAX"}, {0, NULL}};
|
||||
consvar_t cv_resynchcooldown = {"gamestatecooldown", "5", CV_SAVE, resynchcooldown_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL };
|
||||
#endif
|
||||
|
||||
consvar_t cv_blamecfail = {"blamecfail", "Off", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL };
|
||||
|
||||
// max file size to send to a player (in kilobytes)
|
||||
@@ -4014,6 +4123,11 @@ static void Joinable_OnChange(void)
|
||||
if (!server)
|
||||
return;
|
||||
|
||||
#ifdef SATURNJOIN
|
||||
// disabling joins should also disable saturn joins unless its called with CV_StealthSet and vice versa to make life a bit easier
|
||||
CV_SetValue(&cv_allownewsaturnplayer, cv_allownewplayer.value);
|
||||
#endif
|
||||
|
||||
maxplayer = (UINT8)(min((dedicated ? MAXPLAYERS-1 : MAXPLAYERS), cv_maxplayers.value));
|
||||
|
||||
WRITEUINT8(p, maxplayer);
|
||||
@@ -4039,6 +4153,9 @@ void D_ClientServerInit(void)
|
||||
COM_AddCommand("reloadbans", Command_ReloadBan);
|
||||
COM_AddCommand("connect", Command_connect);
|
||||
COM_AddCommand("nodes", Command_Nodes);
|
||||
#ifdef SATURNSYNCH
|
||||
COM_AddCommand("resendgamestate", Command_ResendGamestate);
|
||||
#endif
|
||||
COM_AddCommand("listplayers", Command_Listplayers);
|
||||
COM_AddCommand("packetstat", Command_Packetstat);
|
||||
#ifdef HAVE_CURL
|
||||
@@ -4077,17 +4194,33 @@ void D_ClientServerInit(void)
|
||||
static void ResetNode(INT32 node)
|
||||
{
|
||||
nodeingame[node] = false;
|
||||
nodewaiting[node] = 0;
|
||||
|
||||
nodetoplayer[node] = -1;
|
||||
nodetoplayer2[node] = -1;
|
||||
nodetoplayer3[node] = -1;
|
||||
nodetoplayer4[node] = -1;
|
||||
playerpernode[node] = 0;
|
||||
|
||||
nettics[node] = gametic;
|
||||
supposedtics[node] = gametic;
|
||||
nodewaiting[node] = 0;
|
||||
playerpernode[node] = 0;
|
||||
|
||||
sendingsavegame[node] = false;
|
||||
|
||||
bannednode[node].banid = SIZE_MAX;
|
||||
bannednode[node].timeleft = NO_BAN_TIME;
|
||||
|
||||
// SATURN
|
||||
#ifdef SATURNPAK
|
||||
is_client_saturn[node] = false;
|
||||
#endif
|
||||
|
||||
#ifdef SATURNSYNCH
|
||||
resendingsavegame[node] = false;
|
||||
can_receive_gamestate[node] = false;
|
||||
savegameresendcooldown[node] = 0;
|
||||
#endif
|
||||
//
|
||||
}
|
||||
|
||||
void SV_ResetServer(void)
|
||||
@@ -4123,6 +4256,9 @@ void SV_ResetServer(void)
|
||||
|
||||
mynode = 0;
|
||||
cl_packetmissed = false;
|
||||
#ifdef SATURNSYNCH
|
||||
cl_redownloadinggamestate = false;
|
||||
#endif
|
||||
|
||||
if (dedicated)
|
||||
{
|
||||
@@ -4337,7 +4473,6 @@ static void Got_RemovePlayer(UINT8 **p, INT32 playernum)
|
||||
if (pnum == serverplayer)
|
||||
{
|
||||
CONS_Alert(CONS_WARNING, "Attempt to remove server player\n");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4416,6 +4551,14 @@ static boolean SV_AddWaitingPlayers(void)
|
||||
return newplayer;
|
||||
}
|
||||
|
||||
#ifdef SATURNPAK
|
||||
static inline void SendSaturnInfo(INT32 node)
|
||||
{
|
||||
netbuffer->packettype = PT_ISSATURN;
|
||||
HSendPacket(node, true, 0, 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
void CL_AddSplitscreenPlayer(void)
|
||||
{
|
||||
if (cl_mode == CL_CONNECTED)
|
||||
@@ -4601,7 +4744,11 @@ static void HandleConnect(SINT8 node)
|
||||
{
|
||||
SV_SendRefuse(node, va(M_GetText("Different SRB2Kart versions cannot\nplay a netgame!\n(server version %d.%d)"), VERSION, SUBVERSION));
|
||||
}
|
||||
#ifdef SATURNJOIN
|
||||
else if ((!cv_allownewplayer.value && node && netbuffer->u.clientcfg.issaturn != ISSATURN) || (!cv_allownewsaturnplayer.value && node && netbuffer->u.clientcfg.issaturn == ISSATURN))
|
||||
#else
|
||||
else if (!cv_allownewplayer.value && node)
|
||||
#endif
|
||||
{
|
||||
SV_SendRefuse(node, M_GetText(cv_joinrefusemessage.string));
|
||||
}
|
||||
@@ -4668,7 +4815,7 @@ static void HandleConnect(SINT8 node)
|
||||
{
|
||||
if (node && newnode)
|
||||
{
|
||||
SV_SendSaveGame(node); // send a complete game state
|
||||
SV_SendSaveGame(node, false); // send a complete game state
|
||||
DEBFILE("send savegame\n");
|
||||
}
|
||||
SV_AddWaitingPlayers();
|
||||
@@ -4740,6 +4887,44 @@ static void HandleServerInfo(SINT8 node)
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef SATURNSYNCH
|
||||
static void PT_WillResendGamestate(void)
|
||||
{
|
||||
char tmpsave[264];
|
||||
|
||||
if (server || cl_redownloadinggamestate)
|
||||
return;
|
||||
|
||||
// Send back a PT_CANRECEIVEGAMESTATE packet to the server
|
||||
// so they know they can start sending the game state
|
||||
netbuffer->packettype = PT_CANRECEIVEGAMESTATE;
|
||||
if (!HSendPacket(servernode, true, 0, 0))
|
||||
return;
|
||||
|
||||
CONS_Printf(M_GetText("Reloading game state...\n"));
|
||||
|
||||
sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home);
|
||||
|
||||
// Don't get a corrupt savegame error because tmpsave already exists
|
||||
if (FIL_FileExists(tmpsave) && unlink(tmpsave) == -1)
|
||||
I_Error("Can't delete %s\n", tmpsave);
|
||||
|
||||
CL_PrepareDownloadSaveGame(tmpsave);
|
||||
cl_redownloadinggamestate = true;
|
||||
}
|
||||
|
||||
static void PT_CanReceiveGamestate(SINT8 node)
|
||||
{
|
||||
if (client || sendingsavegame[node])
|
||||
return;
|
||||
|
||||
CONS_Printf(M_GetText("Resending game state to %s...\n"), player_names[nodetoplayer[node]]);
|
||||
|
||||
SV_SendSaveGame(node, true); // Resend a complete game state
|
||||
resendingsavegame[node] = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
/** Handles a packet received from a node that isn't in game
|
||||
*
|
||||
* \param node The packet sender
|
||||
@@ -4928,6 +5113,9 @@ static void HandlePacketFromAwayNode(SINT8 node)
|
||||
#endif
|
||||
DEBFILE(va("Server accept join gametic=%u mynode=%d\n", gametic, mynode));
|
||||
|
||||
#ifdef SATURNPAK
|
||||
SendSaturnInfo(node);
|
||||
#endif
|
||||
memset(playeringame, 0, sizeof(playeringame));
|
||||
for (j = 0; j < MAXPLAYERS; j++)
|
||||
{
|
||||
@@ -5041,7 +5229,6 @@ static void HandlePacketFromPlayer(SINT8 node)
|
||||
tic_t realend, realstart;
|
||||
UINT8 *pak, *txtpak, numtxtpak;
|
||||
|
||||
|
||||
txtpak = NULL;
|
||||
|
||||
if (dedicated && node == 0)
|
||||
@@ -5165,11 +5352,69 @@ static void HandlePacketFromPlayer(SINT8 node)
|
||||
--resynch_delay[node];
|
||||
break;
|
||||
}
|
||||
|
||||
#ifdef SATURNSYNCH
|
||||
if ((gamestate_resend_counter[node] != 0) && (I_GetTime() % ((max(cv_resynchcooldown.value, 1) * TICRATE) *2) == 0))
|
||||
gamestate_resend_counter[node]--;
|
||||
|
||||
// Check player consistancy during the level
|
||||
if (((!can_receive_gamestate[node]) && realstart <= gametic && realstart > gametic - TICQUEUE+1 && gamestate == GS_LEVEL
|
||||
&& consistancy[realstart%TICQUEUE] != SHORT(netbuffer->u.clientpak.consistancy)) || (can_receive_gamestate[node] && realstart <= gametic && realstart + TICQUEUE - 1 > gametic && gamestate == GS_LEVEL
|
||||
&& consistancy[realstart%TICQUEUE] != SHORT(netbuffer->u.clientpak.consistancy)
|
||||
&& !resendingsavegame[node] && savegameresendcooldown[node] <= I_GetTime()))
|
||||
{
|
||||
// we need to send this so the client can tell us if it can receive the savegame
|
||||
netbuffer->packettype = PT_WILLRESENDGAMESTATE;
|
||||
HSendPacket(node, true, 0, 0);
|
||||
|
||||
if (can_receive_gamestate[node])
|
||||
resendingsavegame[node] = true;
|
||||
else
|
||||
{
|
||||
SV_RequireResynch(node);
|
||||
resendingsavegame[node] = false;
|
||||
}
|
||||
|
||||
if ((!can_receive_gamestate[node] && cv_resynchattempts.value && resynch_score[node] <= (unsigned)cv_resynchattempts.value*250) || (can_receive_gamestate[node] && (gamestate_resend_counter[node] < cv_gamestateattempts.value)))
|
||||
{
|
||||
if (can_receive_gamestate[node] && resendingsavegame[node])
|
||||
{
|
||||
gamestate_resend_counter[node]++;
|
||||
DEBFILE(va("gamestate counter %d for player %d\n", gamestate_resend_counter[node], netconsole));
|
||||
}
|
||||
|
||||
if (cv_blamecfail.value)
|
||||
CONS_Printf(M_GetText("Synch failure for player %d (%s); expected %hd, got %hd\n"),
|
||||
netconsole+1, player_names[netconsole],
|
||||
consistancy[realstart%TICQUEUE],
|
||||
SHORT(netbuffer->u.clientpak.consistancy));
|
||||
DEBFILE(va("Restoring player %d (synch failure) [%update] %d!=%d\n",
|
||||
netconsole, realstart, consistancy[realstart%TICQUEUE],
|
||||
SHORT(netbuffer->u.clientpak.consistancy)));
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
UINT8 buf[3];
|
||||
|
||||
buf[0] = (UINT8)netconsole;
|
||||
buf[1] = KICK_MSG_CON_FAIL;
|
||||
SendNetXCmd(XD_KICK, &buf, 2);
|
||||
DEBFILE(va("player %d kicked (synch failure) [%u] %d!=%d\n",
|
||||
netconsole, realstart, consistancy[realstart%TICQUEUE],
|
||||
SHORT(netbuffer->u.clientpak.consistancy)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (resynch_score[node])
|
||||
--resynch_score[node];
|
||||
break;
|
||||
#else
|
||||
// Check player consistancy during the level
|
||||
if (realstart <= gametic && realstart > gametic - TICQUEUE+1 && gamestate == GS_LEVEL
|
||||
&& consistancy[realstart%TICQUEUE] != SHORT(netbuffer->u.clientpak.consistancy))
|
||||
{
|
||||
SV_RequireResynch(node);
|
||||
SV_RequireResynch(node);
|
||||
|
||||
if (cv_resynchattempts.value && resynch_score[node] <= (unsigned)cv_resynchattempts.value*250)
|
||||
{
|
||||
@@ -5199,6 +5444,8 @@ static void HandlePacketFromPlayer(SINT8 node)
|
||||
else if (resynch_score[node])
|
||||
--resynch_score[node];
|
||||
break;
|
||||
#endif
|
||||
|
||||
case PT_BASICKEEPALIVE:
|
||||
if (client)
|
||||
break;
|
||||
@@ -5333,6 +5580,12 @@ static void HandlePacketFromPlayer(SINT8 node)
|
||||
}
|
||||
Net_CloseConnection(node);
|
||||
nodeingame[node] = false;
|
||||
#ifdef SATURNPAK
|
||||
is_client_saturn[node] = false;
|
||||
#endif
|
||||
#ifdef SATURNSYNCH
|
||||
can_receive_gamestate[node] = false;
|
||||
#endif
|
||||
break;
|
||||
// -------------------------------------------- CLIENT RECEIVE ----------
|
||||
case PT_RESYNCHEND:
|
||||
@@ -5496,6 +5749,26 @@ static void HandlePacketFromPlayer(SINT8 node)
|
||||
if (client)
|
||||
Got_Filetxpak();
|
||||
break;
|
||||
#ifdef SATURNSYNCH
|
||||
case PT_CANRECEIVEGAMESTATE:
|
||||
can_receive_gamestate[node] = true;
|
||||
PT_CanReceiveGamestate(node);
|
||||
break;
|
||||
case PT_RECEIVEDGAMESTATE:
|
||||
sendingsavegame[node] = false;
|
||||
resendingsavegame[node] = false;
|
||||
savegameresendcooldown[node] = I_GetTime() + cv_resynchcooldown.value * TICRATE; // I_GetTime() + 5 * TICRATE;
|
||||
break;
|
||||
case PT_WILLRESENDGAMESTATE:
|
||||
PT_WillResendGamestate();
|
||||
break;
|
||||
#endif
|
||||
#ifdef SATURNPAK
|
||||
case PT_ISSATURN:
|
||||
//CONS_Printf("hi im on saturn%d\n", node);
|
||||
is_client_saturn[node] = true;
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
DEBFILE(va("UNKNOWN PACKET TYPE RECEIVED %d from host %d\n",
|
||||
netbuffer->packettype, node));
|
||||
@@ -5511,7 +5784,6 @@ static void GetPackets(void)
|
||||
{
|
||||
SINT8 node; // The packet sender
|
||||
|
||||
|
||||
player_joining = false;
|
||||
|
||||
while (HGetPacket())
|
||||
@@ -6328,13 +6600,12 @@ void NetKeepAlive(void)
|
||||
MasterClient_Ticker();
|
||||
#endif
|
||||
|
||||
#ifdef HOLEPUNCH
|
||||
#ifdef HOLEPUNCH
|
||||
if (netgame && serverrunning)
|
||||
{
|
||||
RenewHolePunch();
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
if (client)
|
||||
{
|
||||
@@ -6447,18 +6718,27 @@ void NetUpdate(void)
|
||||
MasterClient_Ticker(); // Acking the Master Server
|
||||
#endif
|
||||
|
||||
#ifdef HOLEPUNCH
|
||||
#ifdef HOLEPUNCH
|
||||
if (netgame && serverrunning)
|
||||
{
|
||||
RenewHolePunch();
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
if (client)
|
||||
{
|
||||
#ifdef SATURNSYNCH
|
||||
// If the client just finished redownloading the game state, load it
|
||||
if (cl_redownloadinggamestate && fileneeded[0].status == FS_FOUND)
|
||||
CL_ReloadReceivedSavegame();
|
||||
#endif
|
||||
if (!resynch_local_inprogress)
|
||||
CL_SendClientCmd(); // Send tic cmd
|
||||
|
||||
hu_resynching = resynch_local_inprogress;
|
||||
#ifdef SATURNSYNCH
|
||||
hu_redownloadinggamestate = cl_redownloadinggamestate;
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -6467,6 +6747,9 @@ void NetUpdate(void)
|
||||
INT32 counts;
|
||||
|
||||
hu_resynching = false;
|
||||
#ifdef SATURNSYNCH
|
||||
hu_redownloadinggamestate = false;
|
||||
#endif
|
||||
|
||||
firstticstosend = gametic;
|
||||
for (i = 0; i < MAXNETNODES; i++)
|
||||
@@ -6568,7 +6851,7 @@ rewind_t *CL_SaveRewindPoint(size_t demopos)
|
||||
return NULL;
|
||||
|
||||
save_p = rewind->savebuffer;
|
||||
P_SaveNetGame();
|
||||
P_SaveNetGame(false);
|
||||
rewind->leveltime = leveltime;
|
||||
rewind->next = rewindhead;
|
||||
rewind->demopos = demopos;
|
||||
@@ -6592,7 +6875,7 @@ rewind_t *CL_RewindToTime(tic_t time)
|
||||
return NULL;
|
||||
|
||||
save_p = rewindhead->savebuffer;
|
||||
P_LoadNetGame();
|
||||
P_LoadNetGame(false);
|
||||
wipegamestate = gamestate; // No fading back in!
|
||||
timeinmap = leveltime;
|
||||
|
||||
|
||||
+25
-1
@@ -98,6 +98,16 @@ typedef enum
|
||||
PT_MOREFILESNEEDED, // Server, to client: "you need these (+ more on top of those)"
|
||||
|
||||
PT_PING, // Packet sent to tell clients the other client's latency to server.
|
||||
|
||||
#ifdef SATURNSYNCH
|
||||
PT_WILLRESENDGAMESTATE, // Hey Client, I am about to resend you the gamestate!
|
||||
PT_CANRECEIVEGAMESTATE, // Okay Server, I'm ready to receive it, you can go ahead.
|
||||
PT_RECEIVEDGAMESTATE, // Thank you Server, I am ready to play again!
|
||||
|
||||
// we will reserve this for now even if unused, so order wont get mangled
|
||||
PT_ISSATURN, // Saturn specific identifier packet
|
||||
#endif
|
||||
|
||||
NUMPACKETTYPE
|
||||
} packettype_t;
|
||||
|
||||
@@ -363,6 +373,9 @@ typedef struct
|
||||
UINT8 subversion; // Contains build version
|
||||
UINT8 localplayers; // number of splitscreen players
|
||||
UINT8 mode;
|
||||
#ifdef SATURNJOIN
|
||||
UINT8 issaturn;
|
||||
#endif
|
||||
} ATTRPACK clientconfig_pak;
|
||||
|
||||
#define SV_SPEEDMASK 0x03 // used to send kartspeed
|
||||
@@ -567,7 +580,15 @@ extern consvar_t
|
||||
#ifdef VANILLAJOINNEXTROUND
|
||||
cv_joinnextround,
|
||||
#endif
|
||||
cv_netticbuffer, cv_allownewplayer, cv_joinrefusemessage, cv_maxplayers, cv_resynchattempts, cv_blamecfail, cv_maxsend, cv_noticedownload, cv_downloadspeed;
|
||||
cv_netticbuffer, cv_allownewplayer,
|
||||
#ifdef SATURNJOIN
|
||||
cv_allownewsaturnplayer,
|
||||
#endif
|
||||
cv_joinrefusemessage, cv_maxplayers, cv_resynchattempts,
|
||||
#ifdef SATURNSYNCH
|
||||
cv_resynchcooldown, cv_gamestateattempts,
|
||||
#endif
|
||||
cv_blamecfail, cv_maxsend, cv_noticedownload, cv_downloadspeed;
|
||||
|
||||
extern consvar_t cv_connectawaittime;
|
||||
|
||||
@@ -632,6 +653,9 @@ tic_t GetLag(INT32 node);
|
||||
UINT8 GetFreeXCmdSize(void);
|
||||
|
||||
extern UINT8 hu_resynching;
|
||||
#ifdef SATURNSYNCH
|
||||
extern UINT8 hu_redownloadinggamestate;
|
||||
#endif
|
||||
extern UINT8 hu_stopped; // kart, true when the game is stopped for players due to a disconnecting or connecting player
|
||||
|
||||
typedef struct rewind_s {
|
||||
|
||||
+1
-1
@@ -2076,7 +2076,7 @@ void D_SRB2Main(void)
|
||||
pagename = "TITLESKY";
|
||||
levelstarttic = gametic;
|
||||
G_SetGamestate(GS_LEVEL);
|
||||
if (!P_SetupLevel(false))
|
||||
if (!P_SetupLevel(false, false))
|
||||
I_Quit(); // fail so reset game stuff
|
||||
}
|
||||
|
||||
|
||||
+10
@@ -833,6 +833,16 @@ const char *packettypename[NUMPACKETTYPE] =
|
||||
"MOREFILESNEEDED",
|
||||
|
||||
"PING"
|
||||
#ifdef SATURNSYNCH
|
||||
,
|
||||
|
||||
"WILLRESENDGAMESTATE",
|
||||
"CANRECEIVEGAMESTATE",
|
||||
"RECEIVEDGAMESTATE",
|
||||
|
||||
// we will reserve this for now even if unused, so order wont get mangled
|
||||
"ISSATURN"
|
||||
#endif
|
||||
};
|
||||
|
||||
const char *Net_GetPacketName(UINT8 packettype)
|
||||
|
||||
@@ -788,6 +788,10 @@ void D_RegisterServerCommands(void)
|
||||
// d_clisrv
|
||||
CV_RegisterVar(&cv_maxplayers);
|
||||
CV_RegisterVar(&cv_resynchattempts);
|
||||
#ifdef SATURNSYNCH
|
||||
CV_RegisterVar(&cv_gamestateattempts);
|
||||
CV_RegisterVar(&cv_resynchcooldown);
|
||||
#endif
|
||||
CV_RegisterVar(&cv_maxsend);
|
||||
CV_RegisterVar(&cv_noticedownload);
|
||||
CV_RegisterVar(&cv_downloadspeed);
|
||||
@@ -795,6 +799,9 @@ void D_RegisterServerCommands(void)
|
||||
CV_RegisterVar(&cv_httpsource);
|
||||
#ifndef NONET
|
||||
CV_RegisterVar(&cv_allownewplayer);
|
||||
#ifdef SATURNJOIN
|
||||
CV_RegisterVar(&cv_allownewsaturnplayer);
|
||||
#endif
|
||||
CV_RegisterVar(&cv_joinrefusemessage);
|
||||
CV_RegisterVar(&cv_chatlogsize);
|
||||
#ifdef VANILLAJOINNEXTROUND
|
||||
|
||||
+27
-4
@@ -128,11 +128,11 @@ extern char logfilename[1024];
|
||||
#define VERSION 1 // Game version
|
||||
#define SUBVERSION 6 // more precise version number
|
||||
#ifndef USE_FBO_OGL
|
||||
#define VERSIONSTRING "Saturn v7 - Test "
|
||||
#define VERSIONSTRINGW L"Saturn v7 - Test"
|
||||
#define VERSIONSTRING "Saturn v7"
|
||||
#define VERSIONSTRINGW L"Saturn v7"
|
||||
#else
|
||||
#define VERSIONSTRING "Saturn v7 - Test - FBO "
|
||||
#define VERSIONSTRINGW L"Saturn v7 - Test - FBO"
|
||||
#define VERSIONSTRING "Saturn v7 - FBO "
|
||||
#define VERSIONSTRINGW L"Saturn v7 - FBO"
|
||||
#endif
|
||||
// Hey! If you change this, add 1 to the MODVERSION below! Otherwise we can't force updates!
|
||||
// And change CMakeLists.txt (not src/, but in root), for CMake users!
|
||||
@@ -568,6 +568,29 @@ extern const char *compdate, *comptime, *comprevision, *compbranch;
|
||||
// None of these that are disabled in the normal build are guaranteed to work perfectly
|
||||
// Compile them at your own risk!
|
||||
|
||||
#ifndef NONET
|
||||
//-- SATURN __
|
||||
/// Enable gamestate resynching between Saturn servers and clients
|
||||
/// Like SRB2 and RR does
|
||||
/// Still highly experimental
|
||||
#ifdef DOSATURNSYNCH
|
||||
#define SATURNSYNCH
|
||||
|
||||
/// Detect if a client is on Saturn in the clientconfig.
|
||||
/// To seperately allow them to join or block joining from vanilla clients.
|
||||
#ifdef DOSATURNJOIN
|
||||
#define SATURNJOIN
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/// Server detection for if a connecting client is on Saturn.
|
||||
/// For stuff like extra synching, etc.
|
||||
//#ifdef DOSATURNPAK
|
||||
//#define SATURNPAK
|
||||
//#endif
|
||||
//-- <( ̄︶ ̄)> __
|
||||
#endif
|
||||
|
||||
/// Undefine to use the new method of Gamma correction see colour cube in v_video.c
|
||||
#define BACKWARDSCOMPATCORRECTION
|
||||
|
||||
|
||||
+6
-2
@@ -1197,7 +1197,11 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
|
||||
// why build a ticcmd if we're paused?
|
||||
// Or, for that matter, if we're being reborn.
|
||||
// Kart, don't build a ticcmd if someone is resynching or the server is stopped too so we don't fly off course in bad conditions
|
||||
if (paused || P_AutoPause() || (gamestate == GS_LEVEL && player->playerstate == PST_REBORN) || hu_resynching)
|
||||
if (paused || P_AutoPause() || (gamestate == GS_LEVEL && player->playerstate == PST_REBORN) || hu_resynching
|
||||
#ifdef SATURNSYNCH
|
||||
|| hu_redownloadinggamestate
|
||||
#endif
|
||||
)
|
||||
{
|
||||
cmd->angleturn = (INT16)(lang >> 16);
|
||||
cmd->aiming = G_ClipAimingPitch(&laim);
|
||||
@@ -1647,7 +1651,7 @@ void G_DoLoadLevel(boolean resetplayer)
|
||||
}
|
||||
|
||||
// Setup the level.
|
||||
if (!P_SetupLevel(false))
|
||||
if (!P_SetupLevel(false,false))
|
||||
{
|
||||
// fail so reset game stuff
|
||||
Command_ExitGame_f();
|
||||
|
||||
+10
-2
@@ -1256,7 +1256,11 @@ void HU_Ticker(void)
|
||||
if (cechotimer > 0) --cechotimer;
|
||||
|
||||
// Animate the desynch dots
|
||||
if (hu_resynching)
|
||||
if (hu_resynching
|
||||
#ifdef SATURNSYNCH
|
||||
|| hu_redownloadinggamestate
|
||||
#endif
|
||||
)
|
||||
resynch_ticker++; //tic tic tic tic tic
|
||||
|
||||
HU_TickSongCredits();
|
||||
@@ -2421,7 +2425,11 @@ void HU_Drawer(void)
|
||||
HU_DrawSongCredits();
|
||||
|
||||
// draw desynch text
|
||||
if (hu_resynching)
|
||||
if (hu_resynching
|
||||
#ifdef SATURNSYNCH
|
||||
|| hu_redownloadinggamestate
|
||||
#endif
|
||||
)
|
||||
{
|
||||
char resynch_text[14];
|
||||
UINT32 i;
|
||||
|
||||
@@ -670,8 +670,17 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
|
||||
lua_pop(gL, 1);
|
||||
}
|
||||
if (!found)
|
||||
{
|
||||
t++;
|
||||
|
||||
if (t == 0)
|
||||
{
|
||||
CONS_Alert(CONS_ERROR, "Too many tables to archive!\n");
|
||||
WRITEUINT8(save_p, ARCH_NULL);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
WRITEUINT8(save_p, ARCH_TABLE);
|
||||
WRITEUINT16(save_p, t);
|
||||
|
||||
|
||||
+21
-2
@@ -2027,6 +2027,22 @@ static menuitem_t OP_AdvServerOptionsMenu[] =
|
||||
NULL, "Server Browser Address", &cv_masterserver, 10},
|
||||
#endif
|
||||
{IT_STRING | IT_CVAR, NULL, "Attempts to resynchronise", &cv_resynchattempts, 40},
|
||||
#ifdef SATURNSYNCH
|
||||
{IT_STRING | IT_CVAR, NULL, "Resend Gamestate Cooldown", &cv_resynchcooldown, 50},
|
||||
{IT_STRING | IT_CVAR, NULL, "Resend Gamestate Attempts", &cv_gamestateattempts, 60},
|
||||
|
||||
{IT_STRING | IT_CVAR, NULL, "Delay limit (frames)", &cv_maxping, 70},
|
||||
{IT_STRING | IT_CVAR, NULL, "Delay timeout (s)", &cv_pingtimeout, 80},
|
||||
{IT_STRING | IT_CVAR, NULL, "Connection timeout (tics)", &cv_nettimeout, 90},
|
||||
{IT_STRING | IT_CVAR, NULL, "Join timeout (tics)", &cv_jointimeout, 100},
|
||||
|
||||
{IT_STRING | IT_CVAR, NULL, "Max. file transfer send (KB)", &cv_maxsend, 120},
|
||||
{IT_STRING | IT_CVAR, NULL, "File transfer packet rate", &cv_downloadspeed, 130},
|
||||
|
||||
{IT_STRING | IT_CVAR, NULL, "Log join addresses", &cv_showjoinaddress, 150},
|
||||
{IT_STRING | IT_CVAR, NULL, "Log resyncs", &cv_blamecfail, 160},
|
||||
{IT_STRING | IT_CVAR, NULL, "Log file transfers", &cv_noticedownload, 170},
|
||||
#else
|
||||
{IT_STRING | IT_CVAR, NULL, "Delay limit (frames)", &cv_maxping, 50},
|
||||
{IT_STRING | IT_CVAR, NULL, "Delay timeout (s)", &cv_pingtimeout, 60},
|
||||
{IT_STRING | IT_CVAR, NULL, "Connection timeout (tics)", &cv_nettimeout, 70},
|
||||
@@ -2038,14 +2054,17 @@ static menuitem_t OP_AdvServerOptionsMenu[] =
|
||||
{IT_STRING | IT_CVAR, NULL, "Log join addresses", &cv_showjoinaddress, 130},
|
||||
{IT_STRING | IT_CVAR, NULL, "Log resyncs", &cv_blamecfail, 140},
|
||||
{IT_STRING | IT_CVAR, NULL, "Log file transfers", &cv_noticedownload, 150},
|
||||
};
|
||||
#endif
|
||||
};
|
||||
|
||||
#ifndef NONET
|
||||
static const char* OP_AdvServerOptionsTooltips[] =
|
||||
{
|
||||
"Server used for master server.",
|
||||
"Attempts to resynchronise player to server.",
|
||||
#ifdef SATURNSYNCH
|
||||
"Cooldown in seconds before the Server attempts\nto resend the gamestate to a Saturn Client\nbetween resending attempts.",
|
||||
"Attempts to resend the Gamestate to player\nThis increments a Counter everytime a gamestate resend occurs\nThis Counter decrements at double the time of the Cooldown\nonce the Counter reaches the threshold, the client gets kicked.",
|
||||
#endif
|
||||
"Maximum allowed delay.",
|
||||
"Delay timeout in seconds.",
|
||||
"Connection timeout in tics.",
|
||||
|
||||
+58
-12
@@ -107,7 +107,7 @@ static inline void P_UnArchivePlayer(void)
|
||||
//
|
||||
// P_NetArchivePlayers
|
||||
//
|
||||
static void P_NetArchivePlayers(void)
|
||||
static void P_NetArchivePlayers(boolean resending)
|
||||
{
|
||||
INT32 i, j;
|
||||
UINT16 flags;
|
||||
@@ -117,6 +117,9 @@ static void P_NetArchivePlayers(void)
|
||||
|
||||
for (i = 0; i < MAXPLAYERS; i++)
|
||||
{
|
||||
if (resending)
|
||||
WRITESINT8(save_p, (SINT8)adminplayers[i]);
|
||||
|
||||
if (!playeringame[i])
|
||||
continue;
|
||||
|
||||
@@ -124,6 +127,9 @@ static void P_NetArchivePlayers(void)
|
||||
|
||||
// no longer send ticcmds, player name, skin, or color
|
||||
|
||||
if (resending)
|
||||
WRITESTRINGN(save_p, player_names[i], MAXPLAYERNAME);
|
||||
|
||||
WRITEANGLE(save_p, players[i].aiming);
|
||||
WRITEANGLE(save_p, players[i].awayviewaiming);
|
||||
WRITEINT32(save_p, players[i].awayviewtics);
|
||||
@@ -148,6 +154,13 @@ static void P_NetArchivePlayers(void)
|
||||
WRITEUINT16(save_p, players[i].flashpal);
|
||||
WRITEUINT16(save_p, players[i].flashcount);
|
||||
|
||||
if (resending)
|
||||
{
|
||||
WRITEUINT8(save_p, players[i].skincolor);
|
||||
WRITEINT32(save_p, players[i].skin);
|
||||
}
|
||||
|
||||
|
||||
WRITEUINT32(save_p, players[i].score);
|
||||
WRITEFIXED(save_p, players[i].dashspeed);
|
||||
WRITEINT32(save_p, players[i].dashtime);
|
||||
@@ -285,7 +298,7 @@ static void P_NetArchivePlayers(void)
|
||||
//
|
||||
// P_NetUnArchivePlayers
|
||||
//
|
||||
static void P_NetUnArchivePlayers(void)
|
||||
static void P_NetUnArchivePlayers(boolean resending)
|
||||
{
|
||||
INT32 i, j;
|
||||
UINT16 flags;
|
||||
@@ -295,6 +308,8 @@ static void P_NetUnArchivePlayers(void)
|
||||
|
||||
for (i = 0; i < MAXPLAYERS; i++)
|
||||
{
|
||||
if (resending)
|
||||
adminplayers[i] = (INT32)READSINT8(save_p);
|
||||
// Do NOT memset player struct to 0
|
||||
// other areas may initialize data elsewhere
|
||||
//memset(&players[i], 0, sizeof (player_t));
|
||||
@@ -305,6 +320,9 @@ static void P_NetUnArchivePlayers(void)
|
||||
// sending player names, skin and color should not be necessary at all!
|
||||
// (that data is handled in the server config now)
|
||||
|
||||
if (resending)
|
||||
READSTRINGN(save_p, player_names[i], MAXPLAYERNAME);
|
||||
|
||||
players[i].aiming = READANGLE(save_p);
|
||||
players[i].awayviewaiming = READANGLE(save_p);
|
||||
players[i].awayviewtics = READINT32(save_p);
|
||||
@@ -329,6 +347,12 @@ static void P_NetUnArchivePlayers(void)
|
||||
players[i].flashpal = READUINT16(save_p);
|
||||
players[i].flashcount = READUINT16(save_p);
|
||||
|
||||
if (resending)
|
||||
{
|
||||
players[i].skincolor = READUINT8(save_p);
|
||||
players[i].skin = READINT32(save_p);
|
||||
}
|
||||
|
||||
players[i].score = READUINT32(save_p);
|
||||
players[i].dashspeed = READFIXED(save_p); // dashing speed
|
||||
players[i].dashtime = READINT32(save_p); // dashing speed
|
||||
@@ -3228,19 +3252,25 @@ static inline void P_UnArchiveSPGame(INT16 mapoverride)
|
||||
playeringame[consoleplayer] = true;
|
||||
}
|
||||
|
||||
static void P_NetArchiveMisc(void)
|
||||
static void P_NetArchiveMisc(boolean resending)
|
||||
{
|
||||
UINT32 pig = 0;
|
||||
INT32 i;
|
||||
|
||||
WRITEUINT32(save_p, ARCHIVEBLOCK_MISC);
|
||||
|
||||
if (resending)
|
||||
WRITEUINT32(save_p, gametic);
|
||||
|
||||
WRITEINT16(save_p, gamemap);
|
||||
if (gamestate != GS_LEVEL)
|
||||
WRITEINT16(save_p, GS_WAITINGPLAYERS); // nice hack to put people back into waitingplayers
|
||||
else
|
||||
WRITEINT16(save_p, gamestate);
|
||||
|
||||
if (resending)
|
||||
WRITEINT16(save_p, gametype);
|
||||
|
||||
for (i = 0; i < MAXPLAYERS; i++)
|
||||
pig |= (playeringame[i] != 0)<<i;
|
||||
WRITEUINT32(save_p, pig);
|
||||
@@ -3323,14 +3353,18 @@ static void P_NetArchiveMisc(void)
|
||||
WRITEUINT8(save_p, 0x2e);
|
||||
}
|
||||
|
||||
FUNCINLINE static ATTRINLINE boolean P_NetUnArchiveMisc(void)
|
||||
FUNCINLINE static ATTRINLINE boolean P_NetUnArchiveMisc(boolean reloading)
|
||||
{
|
||||
UINT32 pig;
|
||||
INT32 i;
|
||||
UINT8 *old_save_p;
|
||||
|
||||
if (READUINT32(save_p) != ARCHIVEBLOCK_MISC)
|
||||
I_Error("Bad $$$.sav at archive block Misc");
|
||||
|
||||
if (reloading)
|
||||
gametic = READUINT32(save_p);
|
||||
|
||||
gamemap = READINT16(save_p);
|
||||
|
||||
// gamemap changed; we assume that its map header is always valid,
|
||||
@@ -3340,10 +3374,14 @@ FUNCINLINE static ATTRINLINE boolean P_NetUnArchiveMisc(void)
|
||||
|
||||
// tell the sound code to reset the music since we're skipping what
|
||||
// normally sets this flag
|
||||
mapmusflags |= MUSIC_RELOADRESET;
|
||||
if (!reloading)
|
||||
mapmusflags |= MUSIC_RELOADRESET;
|
||||
|
||||
G_SetGamestate(READINT16(save_p));
|
||||
|
||||
if (reloading)
|
||||
gametype = READINT16(save_p);
|
||||
|
||||
pig = READUINT32(save_p);
|
||||
for (i = 0; i < MAXPLAYERS; i++)
|
||||
{
|
||||
@@ -3357,8 +3395,16 @@ FUNCINLINE static ATTRINLINE boolean P_NetUnArchiveMisc(void)
|
||||
|
||||
encoremode = (boolean)READUINT8(save_p);
|
||||
|
||||
if (!P_SetupLevel(true))
|
||||
// FIXME: save_p should not be global!!!
|
||||
old_save_p = save_p;
|
||||
|
||||
if (!P_SetupLevel(true, reloading))
|
||||
{
|
||||
CONS_Alert(CONS_ERROR, M_GetText("Can't load the level!\n"));
|
||||
return false;
|
||||
}
|
||||
|
||||
save_p = old_save_p;
|
||||
|
||||
// get the time
|
||||
leveltime = READUINT32(save_p);
|
||||
@@ -3441,14 +3487,14 @@ void P_SaveGame(void)
|
||||
WRITEUINT8(save_p, 0x1d); // consistency marker
|
||||
}
|
||||
|
||||
void P_SaveNetGame(void)
|
||||
void P_SaveNetGame(boolean resending)
|
||||
{
|
||||
thinker_t *th;
|
||||
mobj_t *mobj;
|
||||
INT32 i = 1; // don't start from 0, it'd be confused with a blank pointer otherwise
|
||||
|
||||
CV_SaveNetVars(&save_p, false);
|
||||
P_NetArchiveMisc();
|
||||
P_NetArchiveMisc(resending);
|
||||
|
||||
// Assign the mobjnumber for pointer tracking
|
||||
if (gamestate == GS_LEVEL)
|
||||
@@ -3467,7 +3513,7 @@ void P_SaveNetGame(void)
|
||||
}
|
||||
}
|
||||
|
||||
P_NetArchivePlayers();
|
||||
P_NetArchivePlayers(resending);
|
||||
if (gamestate == GS_LEVEL)
|
||||
{
|
||||
P_NetArchiveWorld();
|
||||
@@ -3502,12 +3548,12 @@ boolean P_LoadGame(INT16 mapoverride)
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean P_LoadNetGame(void)
|
||||
boolean P_LoadNetGame(boolean resending)
|
||||
{
|
||||
CV_LoadNetVars(&save_p);
|
||||
if (!P_NetUnArchiveMisc())
|
||||
if (!P_NetUnArchiveMisc(resending))
|
||||
return false;
|
||||
P_NetUnArchivePlayers();
|
||||
P_NetUnArchivePlayers(resending);
|
||||
if (gamestate == GS_LEVEL)
|
||||
{
|
||||
P_NetUnArchiveWorld();
|
||||
|
||||
+2
-2
@@ -22,9 +22,9 @@
|
||||
// These are the load / save game routines.
|
||||
|
||||
void P_SaveGame(void);
|
||||
void P_SaveNetGame(void);
|
||||
void P_SaveNetGame(boolean resending);
|
||||
boolean P_LoadGame(INT16 mapoverride);
|
||||
boolean P_LoadNetGame(void);
|
||||
boolean P_LoadNetGame(boolean resending);
|
||||
|
||||
mobj_t *P_FindNewPosition(UINT32 oldposition);
|
||||
|
||||
|
||||
+52
-56
@@ -2025,14 +2025,12 @@ lumpnum_t lastloadedmaplumpnum; // for comparative savegame
|
||||
//
|
||||
// Some player initialization for map start.
|
||||
//
|
||||
static void P_LevelInitStuff(void)
|
||||
static void P_LevelInitStuff(boolean reloadinggamestate)
|
||||
{
|
||||
INT32 i;
|
||||
|
||||
leveltime = 0;
|
||||
|
||||
memset(localaiming, 0, sizeof(localaiming));
|
||||
|
||||
// map object scale
|
||||
mapobjectscale = mapheaderinfo[gamemap-1]->mobj_scale;
|
||||
|
||||
@@ -2060,7 +2058,10 @@ static void P_LevelInitStuff(void)
|
||||
// circuit, race and competition stuff
|
||||
circuitmap = false;
|
||||
numstarposts = 0;
|
||||
totalrings = timeinmap = 0;
|
||||
totalrings = 0;
|
||||
|
||||
if (!reloadinggamestate)
|
||||
timeinmap = 0;
|
||||
|
||||
// special stage
|
||||
stagefailed = false;
|
||||
@@ -2164,7 +2165,9 @@ void P_LoadThingsOnly(void)
|
||||
P_RemoveMobj(mo);
|
||||
}
|
||||
|
||||
P_LevelInitStuff();
|
||||
P_LevelInitStuff(false);
|
||||
|
||||
memset(localaiming, 0, sizeof(localaiming));
|
||||
|
||||
P_PrepareRawThings(vth->data);
|
||||
P_LoadThings();
|
||||
@@ -2452,30 +2455,36 @@ static void P_SetupCamera(UINT8 pnum, camera_t *cam)
|
||||
}
|
||||
}
|
||||
|
||||
static void P_InitCamera(void)
|
||||
{
|
||||
INT32 i;
|
||||
|
||||
if (!dedicated)
|
||||
{
|
||||
if (!demo.freecam)
|
||||
for (i = 0; i <= splitscreen; i++)
|
||||
P_SetupCamera(displayplayers[i], &camera[i]);
|
||||
|
||||
// Though, I don't think anyone would care about cam_rotate being reset back to the only value that makes sense :P
|
||||
if (!cv_cam_rotate.changed)
|
||||
CV_Set(&cv_cam_rotate, cv_cam_rotate.defaultvalue);
|
||||
|
||||
if (!cv_cam2_rotate.changed)
|
||||
CV_Set(&cv_cam2_rotate, cv_cam2_rotate.defaultvalue);
|
||||
|
||||
if (!cv_cam3_rotate.changed)
|
||||
CV_Set(&cv_cam3_rotate, cv_cam3_rotate.defaultvalue);
|
||||
|
||||
if (!cv_cam4_rotate.changed)
|
||||
CV_Set(&cv_cam4_rotate, cv_cam4_rotate.defaultvalue);
|
||||
|
||||
displayplayers[0] = consoleplayer; // Start with your OWN view, please!
|
||||
}
|
||||
}
|
||||
|
||||
static boolean P_CanSave(void)
|
||||
{
|
||||
#if 0
|
||||
// Saving is completely ignored under these conditions:
|
||||
if ((cursaveslot < 0) // Playing without saving
|
||||
|| (modifiedgame && !savemoddata) // Game is modified
|
||||
|| (netgame || multiplayer) // Not in single-player
|
||||
|| (demo.playback || demo.recording || metalrecording) // Currently in demo
|
||||
|| (players[consoleplayer].lives <= 0) // Completely dead
|
||||
|| (modeattacking || ultimatemode || G_IsSpecialStage(gamemap))) // Specialized instances
|
||||
return false;
|
||||
|
||||
if (mapheaderinfo[gamemap-1]->saveoverride == SAVE_ALWAYS)
|
||||
return true; // Saving should ALWAYS happen!
|
||||
else if (mapheaderinfo[gamemap-1]->saveoverride == SAVE_NEVER)
|
||||
return false; // Saving should NEVER happen!
|
||||
|
||||
// Default condition: In a non-hidden map, at the beginning of a zone or on a completed save-file, and not on save reload.
|
||||
return (!(mapheaderinfo[gamemap-1]->menuflags & LF2_HIDEINMENU)
|
||||
&& (mapheaderinfo[gamemap-1]->actnum < 2 || gamecomplete)
|
||||
&& (gamemap != lastmapsaved));
|
||||
#else
|
||||
return false; // SRB2Kart: no SP, no saving.
|
||||
#endif
|
||||
}
|
||||
|
||||
/** Loads a level from a lump or external wad.
|
||||
@@ -2483,7 +2492,7 @@ static boolean P_CanSave(void)
|
||||
* \param skipprecip If true, don't spawn precipitation.
|
||||
* \todo Clean up, refactor, split up; get rid of the bloat.
|
||||
*/
|
||||
boolean P_SetupLevel(boolean skipprecip)
|
||||
boolean P_SetupLevel(boolean skipprecip, boolean reloadinggamestate)
|
||||
{
|
||||
// use gamemap to get map number.
|
||||
// 99% of the things already did, so.
|
||||
@@ -2521,7 +2530,7 @@ boolean P_SetupLevel(boolean skipprecip)
|
||||
if (cv_runscripts.value && mapheaderinfo[gamemap-1]->scriptname[0] != '#')
|
||||
P_RunLevelScript(mapheaderinfo[gamemap-1]->scriptname);
|
||||
|
||||
P_LevelInitStuff();
|
||||
P_LevelInitStuff(reloadinggamestate);
|
||||
|
||||
for (i = 0; i <= splitscreen; i++)
|
||||
postimgtype[i] = postimg_none;
|
||||
@@ -2554,9 +2563,12 @@ boolean P_SetupLevel(boolean skipprecip)
|
||||
// will be set by player think.
|
||||
players[consoleplayer].viewz = 1;
|
||||
|
||||
// Cancel all d_main.c fadeouts (keep fade in though).
|
||||
if (reloadinggamestate)
|
||||
wipegamestate = gamestate; // Don't fade if reloading the gamestate
|
||||
// Encore mode fade to pink to white
|
||||
// This is handled BEFORE sounds are stopped.
|
||||
if (encoremode && !prevencoremode && !demo.rewinding)
|
||||
else if (encoremode && !prevencoremode && !demo.rewinding)
|
||||
{
|
||||
tic_t locstarttime, endtime, nowtime;
|
||||
|
||||
@@ -2615,15 +2627,16 @@ boolean P_SetupLevel(boolean skipprecip)
|
||||
|
||||
// As oddly named as this is, this handles music only.
|
||||
// We should be fine starting it here.
|
||||
S_Start();
|
||||
if (!reloadinggamestate)
|
||||
S_Start();
|
||||
|
||||
levelfadecol = (encoremode && !ranspecialwipe ? 122 : 120);
|
||||
|
||||
// Let's fade to white here
|
||||
// But only if we didn't do the encore startup wipe
|
||||
if (!ranspecialwipe && !demo.rewinding)
|
||||
if (!ranspecialwipe && !demo.rewinding && !reloadinggamestate)
|
||||
{
|
||||
if(rendermode != render_none)
|
||||
if (rendermode != render_none)
|
||||
{
|
||||
F_WipeStartScreen();
|
||||
V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, levelfadecol);
|
||||
@@ -2732,7 +2745,7 @@ boolean P_SetupLevel(boolean skipprecip)
|
||||
globalweather = mapheaderinfo[gamemap-1]->weather;
|
||||
|
||||
// set up world state
|
||||
P_SpawnSpecials(fromnetsave);
|
||||
P_SpawnSpecials(fromnetsave, reloadinggamestate);
|
||||
|
||||
if (loadprecip) // ugly hack for P_NetUnArchiveMisc (and P_LoadNetGame)
|
||||
P_SpawnPrecipitation();
|
||||
@@ -2843,8 +2856,6 @@ boolean P_SetupLevel(boolean skipprecip)
|
||||
G_BeginRecording(); //this has to move here, since dedicated servers dont run got_mapcmd
|
||||
}
|
||||
|
||||
K_InitDirector();
|
||||
|
||||
wantedcalcdelay = wantedfrequency*2;
|
||||
indirectitemcooldown = 0;
|
||||
hyubgone = 0;
|
||||
@@ -2880,26 +2891,11 @@ boolean P_SetupLevel(boolean skipprecip)
|
||||
// landing point for netgames.
|
||||
netgameskip:
|
||||
|
||||
if (!dedicated)
|
||||
if (!reloadinggamestate)
|
||||
{
|
||||
if (!demo.freecam)
|
||||
for (i = 0; i <= splitscreen; i++)
|
||||
P_SetupCamera(displayplayers[i], &camera[i]);
|
||||
|
||||
// Though, I don't think anyone would care about cam_rotate being reset back to the only value that makes sense :P
|
||||
if (!cv_cam_rotate.changed)
|
||||
CV_Set(&cv_cam_rotate, cv_cam_rotate.defaultvalue);
|
||||
|
||||
if (!cv_cam2_rotate.changed)
|
||||
CV_Set(&cv_cam2_rotate, cv_cam2_rotate.defaultvalue);
|
||||
|
||||
if (!cv_cam3_rotate.changed)
|
||||
CV_Set(&cv_cam3_rotate, cv_cam3_rotate.defaultvalue);
|
||||
|
||||
if (!cv_cam4_rotate.changed)
|
||||
CV_Set(&cv_cam4_rotate, cv_cam4_rotate.defaultvalue);
|
||||
|
||||
displayplayers[0] = consoleplayer; // Start with your OWN view, please!
|
||||
P_InitCamera();
|
||||
memset(localaiming, 0, sizeof(localaiming));
|
||||
K_InitDirector();
|
||||
}
|
||||
|
||||
// clear special respawning que
|
||||
@@ -2908,7 +2904,7 @@ boolean P_SetupLevel(boolean skipprecip)
|
||||
P_MapEnd();
|
||||
|
||||
// Remove the loading shit from the screen
|
||||
if (rendermode != render_none)
|
||||
if (rendermode != render_none && !reloadinggamestate)
|
||||
V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, levelfadecol);
|
||||
|
||||
if (precache || dedicated)
|
||||
@@ -2955,7 +2951,7 @@ boolean P_SetupLevel(boolean skipprecip)
|
||||
LUAh_MapLoad();
|
||||
}
|
||||
|
||||
if (rendermode != render_none)
|
||||
if (rendermode != render_none && !reloadinggamestate)
|
||||
{
|
||||
R_ResetViewInterpolation(0);
|
||||
R_ResetViewInterpolation(0);
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ extern mapthing_t *mapthings;
|
||||
|
||||
void P_SetupLevelSky(INT32 skynum, boolean global);
|
||||
void P_LoadThingsOnly(void);
|
||||
boolean P_SetupLevel(boolean skipprecip);
|
||||
boolean P_SetupLevel(boolean skipprecip, boolean reloadinggamestate);
|
||||
#ifdef HWRENDER
|
||||
void HWR_LoadLevel(void);
|
||||
#endif
|
||||
|
||||
+3
-2
@@ -5234,7 +5234,7 @@ static void P_RunLevelLoadExecutors(void)
|
||||
* as they'll just be erased by UnArchiveThinkers.
|
||||
* \sa P_SpawnPrecipitation, P_SpawnFriction, P_SpawnPushers, P_SpawnScrollers
|
||||
*/
|
||||
void P_SpawnSpecials(INT32 fromnetsave)
|
||||
void P_SpawnSpecials(INT32 fromnetsave, boolean reloadinggamestate)
|
||||
{
|
||||
sector_t *sector;
|
||||
size_t i;
|
||||
@@ -6261,7 +6261,8 @@ void P_SpawnSpecials(INT32 fromnetsave)
|
||||
}
|
||||
}
|
||||
|
||||
P_RunLevelLoadExecutors();
|
||||
if (!reloadinggamestate)
|
||||
P_RunLevelLoadExecutors();
|
||||
}
|
||||
|
||||
/** Adds 3Dfloors as appropriate based on a common control linedef.
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ void P_InitPicAnims(void);
|
||||
void P_SetupLevelFlatAnims(void);
|
||||
|
||||
// at map load
|
||||
void P_SpawnSpecials(INT32 fromnetsave);
|
||||
void P_SpawnSpecials(INT32 fromnetsave, boolean reloadinggamestate);
|
||||
|
||||
// every tic
|
||||
void P_UpdateSpecials(void);
|
||||
|
||||
Reference in new issue
Block a user