Merge branch 'master' of https://git.do.srb2.org/KartKrew/Kart into round-queue

# Conflicts:
#	src/d_netcmd.h
This commit is contained in:
toaster 2023-04-17 20:08:37 +01:00
commit 2d46112c58
58 changed files with 2402 additions and 664 deletions

View file

@ -141,6 +141,7 @@ add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32
k_podium.c
k_rank.c
k_vote.c
k_serverstats.c
)
if(SRB2_CONFIG_ENABLE_WEBM_MOVIES)

View file

@ -61,6 +61,7 @@
#include "m_cond.h" // netUnlocked
#include "g_party.h"
#include "k_vote.h"
#include "k_serverstats.h"
// cl loading screen
#include "v_video.h"
@ -2775,6 +2776,7 @@ void CL_ClearPlayer(INT32 playernum)
P_SetTarget(&players[playernum].hoverhyudoro, NULL);
P_SetTarget(&players[playernum].stumbleIndicator, NULL);
P_SetTarget(&players[playernum].sliptideZipIndicator, NULL);
P_SetTarget(&players[playernum].ringShooter, NULL);
}
// Handle parties.
@ -2972,12 +2974,18 @@ static void Command_Nodes(void)
if (playernode[i] != UINT8_MAX)
{
CONS_Printf(" - node %.2d", playernode[i]);
CONS_Printf(" [node %.2d]", playernode[i]);
if (I_GetNodeAddress && (address = I_GetNodeAddress(playernode[i])) != NULL)
CONS_Printf(" - %s", address);
}
CONS_Printf(" [RRID-%s] ", GetPrettyRRID(players[i].public_key, true));
if (K_UsingPowerLevels() != PWRLV_DISABLED) // No power type?!
{
CONS_Printf(" [%.4d PWR]", clientpowerlevels[i][K_UsingPowerLevels()]);
}
CONS_Printf(" [RRID-%s]", GetPrettyRRID(players[i].public_key, true));
if (IsPlayerAdmin(i))
CONS_Printf(M_GetText(" (verified admin)"));
@ -3846,6 +3854,7 @@ static void Got_AddPlayer(UINT8 **p, INT32 playernum)
READSTRINGN(*p, player_names[newplayernum], MAXPLAYERNAME);
READMEM(*p, players[newplayernum].public_key, PUBKEYLENGTH);
READMEM(*p, clientpowerlevels[newplayernum], sizeof(((serverplayer_t *)0)->powerlevels));
console = READUINT8(*p);
splitscreenplayer = READUINT8(*p);
@ -4002,8 +4011,10 @@ static void Got_AddBot(UINT8 **p, INT32 playernum)
}
static boolean SV_AddWaitingPlayers(SINT8 node, UINT8 *availabilities,
const char *name, uint8_t *key, const char *name2, uint8_t *key2,
const char *name3, uint8_t *key3, const char *name4, uint8_t *key4)
const char *name, uint8_t *key, UINT16 *pwr,
const char *name2, uint8_t *key2, UINT16 *pwr2,
const char *name3, uint8_t *key3, UINT16 *pwr3,
const char *name4, uint8_t *key4, UINT16 *pwr4)
{
INT32 n, newplayernum, i;
UINT8 buf[4 + MAXPLAYERNAME + PUBKEYLENGTH + MAXAVAILABILITY];
@ -4070,24 +4081,28 @@ const char *name3, uint8_t *key3, const char *name4, uint8_t *key4)
nodetoplayer[node] = newplayernum;
WRITESTRINGN(buf_p, name, MAXPLAYERNAME);
WRITEMEM(buf_p, key, PUBKEYLENGTH);
WRITEMEM(buf_p, pwr, sizeof(((serverplayer_t *)0)->powerlevels));
}
else if (playerpernode[node] < 2)
{
nodetoplayer2[node] = newplayernum;
WRITESTRINGN(buf_p, name2, MAXPLAYERNAME);
WRITEMEM(buf_p, key2, PUBKEYLENGTH);
WRITEMEM(buf_p, pwr2, sizeof(((serverplayer_t *)0)->powerlevels));
}
else if (playerpernode[node] < 3)
{
nodetoplayer3[node] = newplayernum;
WRITESTRINGN(buf_p, name3, MAXPLAYERNAME);
WRITEMEM(buf_p, key3, PUBKEYLENGTH);
WRITEMEM(buf_p, pwr3, sizeof(((serverplayer_t *)0)->powerlevels));
}
else if (playerpernode[node] < 4)
{
nodetoplayer4[node] = newplayernum;
WRITESTRINGN(buf_p, name4, MAXPLAYERNAME);
WRITEMEM(buf_p, key4, PUBKEYLENGTH);
WRITEMEM(buf_p, pwr4, sizeof(((serverplayer_t *)0)->powerlevels));
}
WRITEUINT8(buf_p, nodetoplayer[node]); // consoleplayer
@ -4177,8 +4192,11 @@ boolean SV_SpawnServer(void)
UINT8 *availabilitiesbuffer = R_GetSkinAvailabilities(false, false);
SINT8 node = 0;
for (; node < MAXNETNODES; node++)
result |= SV_AddWaitingPlayers(node, availabilitiesbuffer, cv_playername[0].zstring, PR_GetLocalPlayerProfile(0)->public_key, cv_playername[1].zstring, PR_GetLocalPlayerProfile(1)->public_key,
cv_playername[2].zstring, PR_GetLocalPlayerProfile(2)->public_key, cv_playername[3].zstring, PR_GetLocalPlayerProfile(3)->public_key);
result |= SV_AddWaitingPlayers(node, availabilitiesbuffer,
cv_playername[0].zstring, PR_GetLocalPlayerProfile(0)->public_key, SV_RetrieveStats(PR_GetLocalPlayerProfile(0)->public_key)->powerlevels,
cv_playername[1].zstring, PR_GetLocalPlayerProfile(1)->public_key, SV_RetrieveStats(PR_GetLocalPlayerProfile(1)->public_key)->powerlevels,
cv_playername[2].zstring, PR_GetLocalPlayerProfile(2)->public_key, SV_RetrieveStats(PR_GetLocalPlayerProfile(2)->public_key)->powerlevels,
cv_playername[3].zstring, PR_GetLocalPlayerProfile(3)->public_key, SV_RetrieveStats(PR_GetLocalPlayerProfile(3)->public_key)->powerlevels);
}
return result;
#endif
@ -4259,13 +4277,13 @@ static size_t TotalTextCmdPerTic(tic_t tic)
memset(allZero, 0, PUBKEYLENGTH);
if (split == 0)
return (memcmp(players[nodetoplayer[node]].public_key, allZero, PUBKEYLENGTH) == 0);
return PR_IsKeyGuest(players[nodetoplayer[node]].public_key);
else if (split == 1)
return (memcmp(players[nodetoplayer2[node]].public_key, allZero, PUBKEYLENGTH) == 0);
return PR_IsKeyGuest(players[nodetoplayer2[node]].public_key);
else if (split == 2)
return (memcmp(players[nodetoplayer3[node]].public_key, allZero, PUBKEYLENGTH) == 0);
return PR_IsKeyGuest(players[nodetoplayer3[node]].public_key);
else if (split == 3)
return (memcmp(players[nodetoplayer4[node]].public_key, allZero, PUBKEYLENGTH) == 0);
return PR_IsKeyGuest(players[nodetoplayer4[node]].public_key);
else
I_Error("IsSplitPlayerOnNodeGuest: Out of bounds");
return false; // unreachable
@ -4274,10 +4292,7 @@ static size_t TotalTextCmdPerTic(tic_t tic)
static boolean IsPlayerGuest(int player)
{
char allZero[PUBKEYLENGTH];
memset(allZero, 0, PUBKEYLENGTH);
return (memcmp(players[player].public_key, allZero, PUBKEYLENGTH) == 0);
return PR_IsKeyGuest(players[player].public_key);
}
/** Called when a PT_CLIENTJOIN packet is received
@ -4288,7 +4303,7 @@ static boolean IsPlayerGuest(int player)
static void HandleConnect(SINT8 node)
{
char names[MAXSPLITSCREENPLAYERS][MAXPLAYERNAME + 1];
INT32 i;
INT32 i, j;
UINT8 availabilitiesbuffer[MAXAVAILABILITY];
// Sal: Dedicated mode is INCREDIBLY hacked together.
@ -4402,11 +4417,8 @@ static void HandleConnect(SINT8 node)
memcpy(lastReceivedKey[node][i], PR_GetLocalPlayerProfile(i)->public_key, sizeof(lastReceivedKey[node][i]));
}
else // Remote player, gotta check their signature.
{
char allZero[PUBKEYLENGTH];
memset(allZero, 0, sizeof(allZero));
if (memcmp(lastReceivedKey[node][i], allZero, PUBKEYLENGTH) == 0) // IsSplitPlayerOnNodeGuest isn't appropriate here, they're not in-game yet!
{
if (PR_IsKeyGuest(lastReceivedKey[node][i])) // IsSplitPlayerOnNodeGuest isn't appropriate here, they're not in-game yet!
{
if (!cv_allowguests.value)
{
@ -4427,6 +4439,42 @@ static void HandleConnect(SINT8 node)
return;
}
}
// Check non-GUESTS for duplicate pubkeys, they'll create nonsense stats
if (!PR_IsKeyGuest(lastReceivedKey[node][i]))
{
// Players already here
for (j = 0; j < MAXPLAYERS; j++)
{
if (memcmp(lastReceivedKey[node][i], players[j].public_key, PUBKEYLENGTH) == 0)
{
#ifdef DEVELOP
CONS_Alert(CONS_WARNING, "Joining player's pubkey matches existing player, stat updates will be nonsense!\n");
#else
SV_SendRefuse(node, M_GetText("Duplicate pubkey already on server.\n(Did you share your profile?)"));
return;
#endif
}
}
// Players we're trying to add
for (j = 0; j < netbuffer->u.clientcfg.localplayers - playerpernode[node]; j++)
{
if (PR_IsKeyGuest(lastReceivedKey[node][j]))
continue;
if (i == j)
continue;
if (memcmp(lastReceivedKey[node][i], lastReceivedKey[node][j], PUBKEYLENGTH) == 0)
{
#ifdef DEVELOP
CONS_Alert(CONS_WARNING, "Players with same pubkey in the joning party, stat updates will be nonsense!\n");
#else
SV_SendRefuse(node, M_GetText("Duplicate pubkey in local party.\n(How did you even do this?)"));
return;
#endif
}
}
}
}
memcpy(availabilitiesbuffer, netbuffer->u.clientcfg.availabilities, sizeof(availabilitiesbuffer));
@ -4466,8 +4514,11 @@ static void HandleConnect(SINT8 node)
DEBFILE("send savegame\n");
}
SV_AddWaitingPlayers(node, availabilitiesbuffer, names[0], lastReceivedKey[node][0], names[1], lastReceivedKey[node][1],
names[2], lastReceivedKey[node][2], names[3], lastReceivedKey[node][3]);
SV_AddWaitingPlayers(node, availabilitiesbuffer,
names[0], lastReceivedKey[node][0], SV_RetrieveStats(lastReceivedKey[node][0])->powerlevels,
names[1], lastReceivedKey[node][1], SV_RetrieveStats(lastReceivedKey[node][1])->powerlevels,
names[2], lastReceivedKey[node][2], SV_RetrieveStats(lastReceivedKey[node][2])->powerlevels,
names[3], lastReceivedKey[node][3], SV_RetrieveStats(lastReceivedKey[node][3])->powerlevels);
joindelay += cv_joindelay.value * TICRATE;
player_joining = true;
}

View file

@ -38,7 +38,7 @@ struct event_t
{
evtype_t type;
INT32 data1; // keys / mouse/joystick buttons
INT32 data2; // mouse/joystick x move
INT32 data2; // mouse/joystick x move; key repeat
INT32 data3; // mouse/joystick y move
INT32 device; // which device ID it belongs to (controller ID)
};

View file

@ -82,6 +82,7 @@
#include "acs/interface.h"
#include "k_podium.h"
#include "k_vote.h"
#include "k_serverstats.h"
#ifdef HWRENDER
#include "hardware/hw_main.h" // 3D View Rendering
@ -1620,6 +1621,8 @@ void D_SRB2Main(void)
// Load Profiles now that default controls have been defined
PR_LoadProfiles(); // load control profiles
SV_LoadStats();
#if (defined (__unix__) && !defined (MSDOS)) || defined (UNIXCOMMON) || defined (HAVE_SDL)
VID_PrepareModeList(); // Regenerate Modelist according to cv_fullscreen
#endif
@ -1888,6 +1891,8 @@ void D_SRB2Main(void)
}
}
SV_SaveStats();
if (autostart || netgame)
{
gameaction = ga_nothing;

View file

@ -80,7 +80,6 @@
static void Got_NameAndColor(UINT8 **cp, INT32 playernum);
static void Got_WeaponPref(UINT8 **cp, INT32 playernum);
static void Got_PowerLevel(UINT8 **cp, INT32 playernum);
static void Got_PartyInvite(UINT8 **cp, INT32 playernum);
static void Got_AcceptPartyInvite(UINT8 **cp, INT32 playernum);
static void Got_CancelPartyInvite(UINT8 **cp, INT32 playernum);
@ -93,7 +92,6 @@ static void Got_PickVotecmd(UINT8 **cp, INT32 playernum);
static void Got_RequestAddfilecmd(UINT8 **cp, INT32 playernum);
static void Got_Addfilecmd(UINT8 **cp, INT32 playernum);
static void Got_Pause(UINT8 **cp, INT32 playernum);
static void Got_Respawn(UINT8 **cp, INT32 playernum);
static void Got_RandomSeed(UINT8 **cp, INT32 playernum);
static void Got_RunSOCcmd(UINT8 **cp, INT32 playernum);
static void Got_Teamchange(UINT8 **cp, INT32 playernum);
@ -181,7 +179,6 @@ static void Command_ListWADS_f(void);
static void Command_ListDoomednums_f(void);
static void Command_RunSOC(void);
static void Command_Pause(void);
static void Command_Respawn(void);
static void Command_Version_f(void);
#ifdef UPDATE_ALERT
@ -612,7 +609,6 @@ const char *netxcmdnames[MAXNETXCMD - 1] =
"RUNSOC", // XD_RUNSOC
"REQADDFILE", // XD_REQADDFILE
"SETMOTD", // XD_SETMOTD
"RESPAWN", // XD_RESPAWN
"DEMOTED", // XD_DEMOTED
"LUACMD", // XD_LUACMD
"LUAVAR", // XD_LUAVAR
@ -623,7 +619,6 @@ const char *netxcmdnames[MAXNETXCMD - 1] =
"MODIFYVOTE", // XD_MODIFYVOTE
"PICKVOTE", // XD_PICKVOTE
"REMOVEPLAYER", // XD_REMOVEPLAYER
"POWERLEVEL", // XD_POWERLEVEL
"PARTYINVITE", // XD_PARTYINVITE
"ACCEPTPARTYINVITE", // XD_ACCEPTPARTYINVITE
"LEAVEPARTY", // XD_LEAVEPARTY
@ -664,7 +659,6 @@ void D_RegisterServerCommands(void)
RegisterNetXCmd(XD_NAMEANDCOLOR, Got_NameAndColor);
RegisterNetXCmd(XD_WEAPONPREF, Got_WeaponPref);
RegisterNetXCmd(XD_POWERLEVEL, Got_PowerLevel);
RegisterNetXCmd(XD_PARTYINVITE, Got_PartyInvite);
RegisterNetXCmd(XD_ACCEPTPARTYINVITE, Got_AcceptPartyInvite);
RegisterNetXCmd(XD_CANCELPARTYINVITE, Got_CancelPartyInvite);
@ -674,7 +668,6 @@ void D_RegisterServerCommands(void)
RegisterNetXCmd(XD_ADDFILE, Got_Addfilecmd);
RegisterNetXCmd(XD_REQADDFILE, Got_RequestAddfilecmd);
RegisterNetXCmd(XD_PAUSE, Got_Pause);
RegisterNetXCmd(XD_RESPAWN, Got_Respawn);
RegisterNetXCmd(XD_RUNSOC, Got_RunSOCcmd);
RegisterNetXCmd(XD_LUACMD, Got_Luacmd);
RegisterNetXCmd(XD_LUAFILE, Got_LuaFile);
@ -724,7 +717,6 @@ void D_RegisterServerCommands(void)
COM_AddCommand("runsoc", Command_RunSOC);
COM_AddCommand("pause", Command_Pause);
COM_AddCommand("respawn", Command_Respawn);
COM_AddCommand("gametype", Command_ShowGametype_f);
COM_AddCommand("version", Command_Version_f);
@ -1817,17 +1809,6 @@ static void Got_WeaponPref(UINT8 **cp,INT32 playernum)
demo_extradata[playernum] |= DXD_WEAPONPREF;
}
static void Got_PowerLevel(UINT8 **cp,INT32 playernum)
{
UINT16 race = (UINT16)READUINT16(*cp);
UINT16 battle = (UINT16)READUINT16(*cp);
clientpowerlevels[playernum][PWRLV_RACE] = min(PWRLVRECORD_MAX, race);
clientpowerlevels[playernum][PWRLV_BATTLE] = min(PWRLVRECORD_MAX, battle);
CONS_Debug(DBG_GAMELOGIC, "set player %d to power %d\n", playernum, race);
}
static void Got_PartyInvite(UINT8 **cp,INT32 playernum)
{
UINT8 invitee;
@ -1979,28 +1960,8 @@ static void Got_LeaveParty(UINT8 **cp,INT32 playernum)
void D_SendPlayerConfig(UINT8 n)
{
const profile_t *pr = PR_GetProfile(cv_lastprofile[n].value);
UINT8 buf[4];
UINT8 *p = buf;
SendNameAndColor(n);
WeaponPref_Send(n);
if (pr != NULL)
{
// Send it over
WRITEUINT16(p, pr->powerlevels[PWRLV_RACE]);
WRITEUINT16(p, pr->powerlevels[PWRLV_BATTLE]);
}
else
{
// Guest players have no power level
WRITEUINT16(p, 0);
WRITEUINT16(p, 0);
}
SendNetXCmdForPlayer(n, XD_POWERLEVEL, buf, p-buf);
}
void D_Cheat(INT32 playernum, INT32 cheat, ...)
@ -3596,61 +3557,6 @@ static void Got_Pause(UINT8 **cp, INT32 playernum)
G_ResetAllDeviceRumbles();
}
// Command for stuck characters in netgames, griefing, etc.
static void Command_Respawn(void)
{
UINT8 buf[4];
UINT8 *cp = buf;
if (!(gamestate == GS_LEVEL || gamestate == GS_INTERMISSION || gamestate == GS_VOTING))
{
CONS_Printf(M_GetText("You must be in a level to use this.\n"));
return;
}
if (players[consoleplayer].mo && !P_IsObjectOnGround(players[consoleplayer].mo)) // KART: Nice try, but no, you won't be cheesing spb anymore.
{
CONS_Printf(M_GetText("You must be on the floor to use this.\n"));
return;
}
// todo: this probably isnt necessary anymore with v2
if (players[consoleplayer].mo && (P_PlayerInPain(&players[consoleplayer]) || spbplace == players[consoleplayer].position)) // KART: Nice try, but no, you won't be cheesing spb anymore (x2)
{
CONS_Printf(M_GetText("Nice try.\n"));
return;
}
WRITEINT32(cp, consoleplayer);
SendNetXCmd(XD_RESPAWN, &buf, 4);
}
static void Got_Respawn(UINT8 **cp, INT32 playernum)
{
INT32 respawnplayer = READINT32(*cp);
// You can't respawn someone else. Nice try, there.
if (respawnplayer != playernum || P_PlayerInPain(&players[respawnplayer]) || spbplace == players[respawnplayer].position) // srb2kart: "|| (!(gametyperules & GTR_CIRCUIT))"
{
CONS_Alert(CONS_WARNING, M_GetText("Illegal respawn command received from %s\n"), player_names[playernum]);
if (server)
SendKick(playernum, KICK_MSG_CON_FAIL);
return;
}
if (players[respawnplayer].mo)
{
// incase the above checks were modified to allow sending a respawn on these occasions:
if (!P_IsObjectOnGround(players[respawnplayer].mo))
return;
P_DamageMobj(players[respawnplayer].mo, NULL, NULL, 1, DMG_DEATHPIT);
demo_extradata[playernum] |= DXD_RESPAWN;
}
}
/** Deals with an ::XD_RANDOMSEED message in a netgame.
* These messages set the position of the random number LUT and are crucial to
* correct synchronization.
@ -4165,10 +4071,7 @@ static void Command_Login_f(void)
boolean IsPlayerAdmin(INT32 playernum)
{
#if defined (TESTERS) || defined (HOSTTESTERS)
(void)playernum;
return false;
#elif defined (DEVELOP)
#if defined(DEVELOP) && !(defined(HOSTTESTERS) || defined(TESTERS))
return playernum != serverplayer;
#else
INT32 i;

View file

@ -158,31 +158,29 @@ typedef enum
XD_RUNSOC, // 15
XD_REQADDFILE, // 16
XD_SETMOTD, // 17
XD_RESPAWN, // 18
XD_DEMOTED, // 19
XD_LUACMD, // 20
XD_LUAVAR, // 21
XD_LUAFILE, // 22
XD_DEMOTED, // 18
XD_LUACMD, // 19
XD_LUAVAR, // 20
XD_LUAFILE, // 21
// SRB2Kart
XD_SETUPVOTE, // 23
XD_MODIFYVOTE, // 24
XD_PICKVOTE, // 25
XD_REMOVEPLAYER,// 26
XD_POWERLEVEL, // 27
XD_PARTYINVITE, // 28
XD_ACCEPTPARTYINVITE, // 29
XD_LEAVEPARTY, // 30
XD_CANCELPARTYINVITE, // 31
XD_CHEAT, // 32
XD_ADDBOT, // 33
XD_DISCORD, // 34
XD_PLAYSOUND, // 35
XD_SCHEDULETASK, // 36
XD_SCHEDULECLEAR, // 37
XD_AUTOMATE, // 38
XD_REQMAPQUEUE, // 39
XD_MAPQUEUE, // 40
XD_SETUPVOTE, // 22
XD_MODIFYVOTE, // 23
XD_PICKVOTE, // 24
XD_REMOVEPLAYER,// 25
XD_PARTYINVITE, // 26
XD_ACCEPTPARTYINVITE, // 27
XD_LEAVEPARTY, // 28
XD_CANCELPARTYINVITE, // 29
XD_CHEAT, // 30
XD_ADDBOT, // 31
XD_DISCORD, // 32
XD_PLAYSOUND, // 33
XD_SCHEDULETASK, // 34
XD_SCHEDULECLEAR, // 35
XD_AUTOMATE, // 36
XD_REQMAPQUEUE, // 37
XD_MAPQUEUE, // 38
MAXNETXCMD
} netxcmd_t;

View file

@ -310,6 +310,7 @@ struct respawnvars_t
tic_t dropdash; // Drop Dash charge timer
boolean truedeath; // Your soul has left your body
boolean manual; // Respawn coords were manually set, please respawn exactly there
boolean fromRingShooter; // Respawn was from Ring Shooter, don't allow E-Brake drop
boolean init;
};
@ -505,8 +506,9 @@ struct player_t
UINT32 distancetofinish;
waypoint_t *currentwaypoint;
waypoint_t *nextwaypoint;
respawnvars_t respawn; // Respawn info
tic_t airtime; // Keep track of how long you've been in the air
respawnvars_t respawn; // Respawn info
mobj_t *ringShooter; // DEZ respawner object
tic_t airtime; // Used to track just air time, but has evolved over time into a general "karted" timer. Rename this variable?
UINT8 startboost; // (0 to 125) - Boost you get from start of race or respawn drop dash
UINT16 flashing;

View file

@ -35,13 +35,15 @@ typedef enum
BT_BRAKE = 1<<3, // Brake
BT_ATTACK = 1<<4, // Use Item
BT_LOOKBACK = 1<<5, // Look Backward
BT_RESPAWN = 1<<6, // Respawn
BT_VOTE = 1<<7, // Vote
BT_EBRAKEMASK = (BT_ACCELERATE|BT_BRAKE),
BT_SPINDASHMASK = (BT_ACCELERATE|BT_BRAKE|BT_DRIFT),
// free: 1<<6 to 1<<12
// free: 1<<8 to 1<<12
// Lua garbage
// Lua garbage, replace with freeslottable buttons some day
BT_LUAA = 1<<13,
BT_LUAB = 1<<14,
BT_LUAC = 1<<15,

View file

@ -2458,25 +2458,6 @@ static void readcondition(UINT8 set, UINT32 id, char *word2)
return;
}
}
else if (fastcmp(params[0], "POWERLEVEL"))
{
PARAMCHECK(2);
ty = UC_POWERLEVEL;
re = atoi(params[1]);
x1 = atoi(params[2]);
if (re < PWRLVRECORD_MIN || re > PWRLVRECORD_MAX)
{
deh_warning("Power level requirement %d out of range (%d - %d) for condition ID %d", re, PWRLVRECORD_MIN, PWRLVRECORD_MAX, id+1);
return;
}
if (x1 < 0 || x1 >= PWRLV_NUMTYPES)
{
deh_warning("Power level type %d out of range (0 - %d) for condition ID %d", x1, PWRLV_NUMTYPES-1, id+1);
return;
}
}
else if (fastcmp(params[0], "GAMECLEAR"))
{
ty = UC_GAMECLEAR;

View file

@ -326,6 +326,7 @@ actionpointer_t actionpointers[] =
{{A_FlameShieldPaper}, "A_FLAMESHIELDPAPER"},
{{A_InvincSparkleRotate}, "A_INVINCSPARKLEROTATE"},
{{A_SpawnItemDebrisCloud}, "A_SPAWNITEMDEBRISCLOUD"},
{{A_RingShooterFace}, "A_RINGSHOOTERFACE"},
{{NULL}, "NONE"},
@ -3867,6 +3868,15 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi
"S_SMOOTHLANDING",
// DEZ Ring Shooter
"S_TIREGRABBER",
"S_RINGSHOOTER_SIDE",
"S_RINGSHOOTER_NIPPLES",
"S_RINGSHOOTER_SCREEN",
"S_RINGSHOOTER_NUMBERBACK",
"S_RINGSHOOTER_NUMBERFRONT",
"S_RINGSHOOTER_FACE",
// DEZ respawn laser
"S_DEZLASER",
"S_DEZLASER_TRAIL1",
@ -5433,6 +5443,11 @@ const char *const MOBJTYPE_LIST[] = { // array length left dynamic for sanity t
"MT_SMOOTHLANDING",
"MT_TIREGRABBER",
"MT_RINGSHOOTER",
"MT_RINGSHOOTER_PART",
"MT_RINGSHOOTER_SCREEN",
"MT_DEZLASER",
"MT_WAYPOINT",

View file

@ -353,14 +353,6 @@ void G_ReadDemoExtraData(void)
}
}
}
if (extradata & DXD_RESPAWN)
{
if (players[p].mo)
{
// Is this how this should work..?
P_DamageMobj(players[p].mo, NULL, NULL, 1, DMG_DEATHPIT);
}
}
if (extradata & DXD_WEAPONPREF)
{
WeaponPref_Parse(&demobuf.p, p);

View file

@ -127,7 +127,7 @@ extern UINT8 demo_writerng;
#define DXD_NAME 0x08 // name changed
#define DXD_COLOR 0x10 // color changed
#define DXD_FOLLOWER 0x20 // follower was changed
#define DXD_RESPAWN 0x40 // "respawn" command in console
#define DXD_WEAPONPREF 0x80 // netsynced playsim settings were changed
#define DXD_PST_PLAYING 0x01

View file

@ -67,6 +67,7 @@
#include "acs/interface.h"
#include "g_party.h"
#include "k_vote.h"
#include "k_serverstats.h"
#ifdef HAVE_DISCORDRPC
#include "discord.h"
@ -1323,7 +1324,6 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
// C
if (G_PlayerInputDown(forplayer, gc_spindash, 0))
{
forward = 0;
cmd->buttons |= BT_SPINDASHMASK;
}
@ -1339,6 +1339,18 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
cmd->buttons |= BT_LOOKBACK;
}
// respawn
if (G_PlayerInputDown(forplayer, gc_respawn, 0))
{
cmd->buttons |= (BT_RESPAWN | BT_EBRAKEMASK);
}
// mp general function button
if (G_PlayerInputDown(forplayer, gc_vote, 0))
{
cmd->buttons |= BT_VOTE;
}
// lua buttons a thru c
if (G_PlayerInputDown(forplayer, gc_luaa, 0)) { cmd->buttons |= BT_LUAA; }
if (G_PlayerInputDown(forplayer, gc_luab, 0)) { cmd->buttons |= BT_LUAB; }
@ -1580,6 +1592,8 @@ void G_DoLoadLevelEx(boolean resetplayer, gamestate_t newstate)
// clear hud messages remains (usually from game startup)
CON_ClearHUD();
SV_UpdateStats();
server_lagless = cv_lagless.value;
if (doAutomate == true)
@ -2630,6 +2644,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps)
P_SetTarget(&players[player].follower, NULL);
P_SetTarget(&players[player].awayview.mobj, NULL);
P_SetTarget(&players[player].stumbleIndicator, NULL);
P_SetTarget(&players[player].ringShooter, NULL);
P_SetTarget(&players[player].followmobj, NULL);
hoverhyudoro = players[player].hoverhyudoro;

View file

@ -456,6 +456,11 @@ void G_MapEventsToControls(event_t *ev)
case ev_keydown:
if (ev->data1 < NUMINPUTS)
{
if (ev->data2) // OS repeat? We handle that ourselves
{
break;
}
DeviceGameKeyDownArray[ev->data1] = JOYAXISRANGE;
if (AutomaticControllerReassignmentIsAllowed(ev->device))

View file

@ -91,13 +91,13 @@ typedef enum
// alias gameplay controls
gc_accel = gc_a,
gc_brake = gc_x,
gc_drift = gc_r,
gc_item = gc_l,
gc_spindash = gc_c,
gc_lookback = gc_b,
gc_spindash = gc_c,
gc_brake = gc_x,
gc_respawn = gc_y,
gc_vote = gc_z,
gc_item = gc_l,
gc_drift = gc_r,
} gamecontrols_e;
// mouse values are used once

View file

@ -19,6 +19,8 @@ target_sources(SRB2SDL2 PRIVATE
pass_twodee.hpp
pass.cpp
pass.hpp
patch_atlas.cpp
patch_atlas.hpp
twodee.cpp
twodee.hpp
)

View file

@ -22,47 +22,10 @@ using namespace srb2;
using namespace srb2::hwr2;
using namespace srb2::rhi;
namespace
{
struct AtlasEntry
{
uint32_t x;
uint32_t y;
uint32_t w;
uint32_t h;
uint32_t trim_x;
uint32_t trim_y;
uint32_t orig_w;
uint32_t orig_h;
};
struct Atlas
{
Atlas() = default;
Atlas(Atlas&&) = default;
Handle<Texture> tex;
uint32_t tex_width;
uint32_t tex_height;
std::unordered_map<const patch_t*, AtlasEntry> entries;
std::unique_ptr<stbrp_context> rp_ctx {nullptr};
std::unique_ptr<stbrp_node[]> rp_nodes {nullptr};
Atlas& operator=(Atlas&&) = default;
};
} // namespace
struct srb2::hwr2::TwodeePassData
{
Handle<Texture> default_tex;
Handle<Texture> default_colormap_tex;
std::vector<Atlas> patch_atlases;
std::unordered_map<const patch_t*, size_t> patch_lookup;
std::vector<const patch_t*> patches_to_upload;
std::unordered_map<const uint8_t*, Handle<Texture>> colormaps;
std::vector<const uint8_t*> colormaps_to_upload;
std::unordered_map<TwodeePipelineKey, Handle<Pipeline>> pipelines;
@ -83,202 +46,6 @@ TwodeePass::~TwodeePass() = default;
static constexpr const uint32_t kVboInitSize = 32768;
static constexpr const uint32_t kIboInitSize = 4096;
static Rect trimmed_patch_dim(const patch_t* patch);
static void create_atlas(Rhi& rhi, TwodeePassData& pass_data)
{
Atlas new_atlas;
new_atlas.tex = rhi.create_texture({
TextureFormat::kLuminanceAlpha,
2048,
2048,
TextureWrapMode::kClamp,
TextureWrapMode::kClamp
});
new_atlas.tex_width = 2048;
new_atlas.tex_height = 2048;
new_atlas.rp_ctx = std::make_unique<stbrp_context>();
new_atlas.rp_nodes = std::make_unique<stbrp_node[]>(4096);
for (size_t i = 0; i < 4096; i++)
{
new_atlas.rp_nodes[i] = {};
}
stbrp_init_target(new_atlas.rp_ctx.get(), 2048, 2048, new_atlas.rp_nodes.get(), 4096);
// it is CRITICALLY important that the atlas is MOVED, not COPIED, otherwise the node ptrs will be broken
pass_data.patch_atlases.push_back(std::move(new_atlas));
}
static void pack_patches(Rhi& rhi, TwodeePassData& pass_data, tcb::span<const patch_t*> patches)
{
// Prepare stbrp rects for patches to be loaded.
std::vector<stbrp_rect> rects;
for (size_t i = 0; i < patches.size(); i++)
{
const patch_t* patch = patches[i];
Rect trimmed_rect = trimmed_patch_dim(patch);
stbrp_rect rect {};
rect.id = i;
rect.w = trimmed_rect.w;
rect.h = trimmed_rect.h;
rects.push_back(std::move(rect));
}
while (rects.size() > 0)
{
if (pass_data.patch_atlases.size() == 0)
{
create_atlas(rhi, pass_data);
}
for (size_t atlas_index = 0; atlas_index < pass_data.patch_atlases.size(); atlas_index++)
{
auto& atlas = pass_data.patch_atlases[atlas_index];
stbrp_pack_rects(atlas.rp_ctx.get(), rects.data(), rects.size());
for (auto itr = rects.begin(); itr != rects.end();)
{
auto& rect = *itr;
if (rect.was_packed)
{
AtlasEntry entry;
const patch_t* patch = patches[rect.id];
// TODO prevent unnecessary recalculation of trim?
Rect trimmed_rect = trimmed_patch_dim(patch);
entry.x = static_cast<uint32_t>(rect.x);
entry.y = static_cast<uint32_t>(rect.y);
entry.w = static_cast<uint32_t>(rect.w);
entry.h = static_cast<uint32_t>(rect.h);
entry.trim_x = static_cast<uint32_t>(trimmed_rect.x);
entry.trim_y = static_cast<uint32_t>(trimmed_rect.y);
entry.orig_w = static_cast<uint32_t>(patch->width);
entry.orig_h = static_cast<uint32_t>(patch->height);
atlas.entries.insert_or_assign(patch, std::move(entry));
pass_data.patch_lookup.insert_or_assign(patch, atlas_index);
pass_data.patches_to_upload.push_back(patch);
rects.erase(itr);
continue;
}
++itr;
}
// If we still have rects to pack, and we're at the last atlas, create another atlas.
// TODO This could end up in an infinite loop if the patches are bigger than an atlas. Such patches need to
// be loaded as individual RHI textures instead.
if (atlas_index == pass_data.patch_atlases.size() - 1 && rects.size() > 0)
{
create_atlas(rhi, pass_data);
}
}
}
}
/// @brief Derive the subrect of the given patch with empty columns and rows excluded.
static Rect trimmed_patch_dim(const patch_t* patch)
{
bool minx_found = false;
int32_t minx = 0;
int32_t maxx = 0;
int32_t miny = patch->height;
int32_t maxy = 0;
for (int32_t x = 0; x < patch->width; x++)
{
const int32_t columnofs = patch->columnofs[x];
const column_t* column = reinterpret_cast<const column_t*>(patch->columns + columnofs);
// If the first pole is empty (topdelta = 255), there are no pixels in this column
if (!minx_found && column->topdelta == 0xFF)
{
// Thus, the minx is at least one higher than the current column.
minx = x + 1;
continue;
}
minx_found = true;
if (minx_found && column->topdelta != 0xFF)
{
maxx = x;
}
miny = std::min(static_cast<int32_t>(column->topdelta), miny);
int32_t prevdelta = 0;
int32_t topdelta = 0;
while (column->topdelta != 0xFF)
{
topdelta = column->topdelta;
// Tall patches hack
if (topdelta <= prevdelta)
{
topdelta += prevdelta;
}
prevdelta = topdelta;
maxy = std::max(topdelta + column->length, maxy);
column = reinterpret_cast<const column_t*>(reinterpret_cast<const uint8_t*>(column) + column->length + 4);
}
}
maxx += 1;
maxx = std::max(minx, maxx);
maxy = std::max(miny, maxy);
return {minx, miny, static_cast<uint32_t>(maxx - minx), static_cast<uint32_t>(maxy - miny)};
}
static void convert_patch_to_trimmed_rg8_pixels(const patch_t* patch, std::vector<uint8_t>& out)
{
Rect trimmed_rect = trimmed_patch_dim(patch);
if (trimmed_rect.w % 2 > 0)
{
// In order to force 4-byte row alignment, an extra column is added to the image data.
// Look up GL_UNPACK_ALIGNMENT (which defaults to 4 bytes)
trimmed_rect.w += 1;
}
out.clear();
// 2 bytes per pixel; 1 for the color index, 1 for the alpha. (RG8)
out.resize(trimmed_rect.w * trimmed_rect.h * 2, 0);
for (int32_t x = 0; x < static_cast<int32_t>(trimmed_rect.w) && x < (patch->width - trimmed_rect.x); x++)
{
const int32_t columnofs = patch->columnofs[x + trimmed_rect.x];
const column_t* column = reinterpret_cast<const column_t*>(patch->columns + columnofs);
int32_t prevdelta = 0;
int32_t topdelta = 0;
while (column->topdelta != 0xFF)
{
topdelta = column->topdelta;
// prevdelta is used to implement tall patches hack
if (topdelta <= prevdelta)
{
topdelta += prevdelta;
}
prevdelta = topdelta;
const uint8_t* source = reinterpret_cast<const uint8_t*>(column) + 3;
int32_t count = column->length; // is this byte order safe...?
for (int32_t i = 0; i < count; i++)
{
int32_t output_y = topdelta + i - trimmed_rect.y;
if (output_y < 0)
{
continue;
}
if (output_y >= static_cast<int32_t>(trimmed_rect.h))
{
break;
}
size_t pixel_index = (output_y * trimmed_rect.w + x) * 2;
out[pixel_index + 0] = source[i]; // index in luminance/red channel
out[pixel_index + 1] = 0xFF; // alpha/green value of 1
}
column = reinterpret_cast<const column_t*>(reinterpret_cast<const uint8_t*>(column) + column->length + 4);
}
}
}
static TwodeePipelineKey pipeline_key_for_cmd(const Draw2dCmd& cmd)
{
return {hwr2::get_blend_mode(cmd), hwr2::is_draw_lines(cmd)};
@ -358,24 +125,26 @@ static PipelineDesc make_pipeline_desc(TwodeePipelineKey key)
{0.f, 0.f, 0.f, 1.f}};
}
static void rewrite_patch_quad_vertices(Draw2dList& list, const Draw2dPatchQuad& cmd, TwodeePassData* data)
void TwodeePass::rewrite_patch_quad_vertices(Draw2dList& list, const Draw2dPatchQuad& cmd) const
{
// Patch quads are clipped according to the patch's atlas entry
if (cmd.patch == nullptr)
const patch_t* patch = cmd.patch;
if (patch == nullptr)
{
return;
}
std::size_t atlas_index = data->patch_lookup[cmd.patch];
auto& atlas = data->patch_atlases[atlas_index];
auto& entry = atlas.entries[cmd.patch];
srb2::NotNull<const PatchAtlas*> atlas = patch_atlas_cache_->find_patch(patch);
std::optional<PatchAtlas::Entry> entry_optional = atlas->find_patch(patch);
SRB2_ASSERT(entry_optional.has_value());
PatchAtlas::Entry entry = *entry_optional;
// Rewrite the vertex data completely.
// The UVs of the trimmed patch in atlas UV space.
const float atlas_umin = static_cast<float>(entry.x) / atlas.tex_width;
const float atlas_umax = static_cast<float>(entry.x + entry.w) / atlas.tex_width;
const float atlas_vmin = static_cast<float>(entry.y) / atlas.tex_height;
const float atlas_vmax = static_cast<float>(entry.y + entry.h) / atlas.tex_height;
const float atlas_umin = static_cast<float>(entry.x) / atlas->texture_size();
const float atlas_umax = static_cast<float>(entry.x + entry.w) / atlas->texture_size();
const float atlas_vmin = static_cast<float>(entry.y) / atlas->texture_size();
const float atlas_vmax = static_cast<float>(entry.y + entry.h) / atlas->texture_size();
// The UVs of the trimmed patch in untrimmed UV space.
// The command's UVs are in untrimmed UV space.
@ -542,27 +311,6 @@ void TwodeePass::prepass(Rhi& rhi)
);
}
// Check for patches that are being freed after this frame. Those patches must be present in the atlases for this
// frame, but all atlases need to be cleared and rebuilt on next call to prepass.
// This is based on the assumption that patches are very rarely freed during runtime; occasionally repacking the
// atlases to free up space from patches that will never be referenced again is acceptable.
if (rebuild_atlases_)
{
for (auto& atlas : data_->patch_atlases)
{
rhi.destroy_texture(atlas.tex);
}
data_->patch_atlases.clear();
data_->patch_lookup.clear();
rebuild_atlases_ = false;
}
if (data_->patch_atlases.size() > 2)
{
// Rebuild the atlases next frame because we have too many patches in the atlas cache.
rebuild_atlases_ = true;
}
// Stage 1 - command list patch detection
std::unordered_set<const patch_t*> found_patches;
std::unordered_set<const uint8_t*> found_colormaps;
@ -587,19 +335,11 @@ void TwodeePass::prepass(Rhi& rhi)
}
}
std::unordered_set<const patch_t*> patch_cache_hits;
std::unordered_set<const patch_t*> patch_cache_misses;
for (auto patch : found_patches)
{
if (data_->patch_lookup.find(patch) != data_->patch_lookup.end())
{
patch_cache_hits.insert(patch);
}
else
{
patch_cache_misses.insert(patch);
}
patch_atlas_cache_->queue_patch(patch);
}
patch_atlas_cache_->pack(rhi);
for (auto colormap : found_colormaps)
{
@ -612,11 +352,6 @@ void TwodeePass::prepass(Rhi& rhi)
data_->colormaps_to_upload.push_back(colormap);
}
// Stage 2 - pack rects into atlases
std::vector<const patch_t*> patches_to_pack(patch_cache_misses.begin(), patch_cache_misses.end());
pack_patches(rhi, *data_, patches_to_pack);
// We now know what patches need to be uploaded.
size_t list_index = 0;
for (auto& list : *ctx_)
{
@ -695,7 +430,6 @@ void TwodeePass::prepass(Rhi& rhi)
// We need to split the merged commands based on the kind of texture
// Patches are converted to atlas texture indexes, which we've just packed the patch rects for
// Flats are uploaded as individual textures.
// TODO actually implement flat drawing
auto tex_visitor = srb2::Overload {
[&](const Draw2dPatchQuad& cmd)
{
@ -705,8 +439,8 @@ void TwodeePass::prepass(Rhi& rhi)
}
else
{
size_t atlas_index = data_->patch_lookup[cmd.patch];
typeof(merged_cmd.texture) atlas_index_texture = atlas_index;
srb2::NotNull<const PatchAtlas*> atlas = patch_atlas_cache_->find_patch(cmd.patch);
typeof(merged_cmd.texture) atlas_index_texture = atlas->texture();
new_cmd_needed = new_cmd_needed || (merged_cmd.texture != atlas_index_texture);
}
@ -739,7 +473,8 @@ void TwodeePass::prepass(Rhi& rhi)
{
if (cmd.patch != nullptr)
{
the_new_one.texture = data_->patch_lookup[cmd.patch];
srb2::NotNull<const PatchAtlas*> atlas = patch_atlas_cache_->find_patch(cmd.patch);
the_new_one.texture = atlas->texture();
}
else
{
@ -776,7 +511,7 @@ void TwodeePass::prepass(Rhi& rhi)
// Perform coordinate transformations
{
auto vtx_transform_visitor = srb2::Overload {
[&](const Draw2dPatchQuad& cmd) { rewrite_patch_quad_vertices(list, cmd, data_.get()); },
[&](const Draw2dPatchQuad& cmd) { rewrite_patch_quad_vertices(list, cmd); },
[&](const Draw2dVertices& cmd) {}};
std::visit(vtx_transform_visitor, cmd);
}
@ -828,25 +563,6 @@ void TwodeePass::transfer(Rhi& rhi, Handle<TransferContext> ctx)
}
data_->colormaps_to_upload.clear();
// Convert patches to RG8 textures and upload to atlas pages
std::vector<uint8_t> patch_data;
for (const patch_t* patch_to_upload : data_->patches_to_upload)
{
Atlas& atlas = data_->patch_atlases[data_->patch_lookup[patch_to_upload]];
AtlasEntry& entry = atlas.entries[patch_to_upload];
convert_patch_to_trimmed_rg8_pixels(patch_to_upload, patch_data);
rhi.update_texture(
ctx,
atlas.tex,
{static_cast<int32_t>(entry.x), static_cast<int32_t>(entry.y), entry.w, entry.h},
PixelFormat::kRG8,
tcb::as_bytes(tcb::span(patch_data))
);
}
data_->patches_to_upload.clear();
Handle<Texture> palette_tex = palette_manager_->palette();
// Update the buffers for each list
@ -867,10 +583,9 @@ void TwodeePass::transfer(Rhi& rhi, Handle<TransferContext> ctx)
{
TextureBinding tx[3];
auto tex_visitor = srb2::Overload {
[&](size_t atlas_index)
[&](Handle<Texture> texture)
{
Atlas& atlas = data_->patch_atlases[atlas_index];
tx[0] = {SamplerName::kSampler0, atlas.tex};
tx[0] = {SamplerName::kSampler0, texture};
tx[1] = {SamplerName::kSampler1, palette_tex};
},
[&](const MergedTwodeeCommandFlatTexture& tex)

View file

@ -18,6 +18,7 @@
#include <vector>
#include "../cxxutil.hpp"
#include "patch_atlas.hpp"
#include "pass.hpp"
#include "pass_resource_managers.hpp"
#include "twodee.hpp"
@ -52,7 +53,7 @@ struct MergedTwodeeCommand
{
TwodeePipelineKey pipeline_key = {};
rhi::Handle<rhi::BindingSet> binding_set = {};
std::optional<std::variant<size_t, MergedTwodeeCommandFlatTexture>> texture;
std::optional<std::variant<rhi::Handle<rhi::Texture>, MergedTwodeeCommandFlatTexture>> texture;
const uint8_t* colormap;
uint32_t index_offset = 0;
uint32_t elements = 0;
@ -78,17 +79,19 @@ struct TwodeePass final : public Pass
std::shared_ptr<TwodeePassData> data_;
std::shared_ptr<MainPaletteManager> palette_manager_;
std::shared_ptr<FlatTextureManager> flat_manager_;
std::shared_ptr<PatchAtlasCache> patch_atlas_cache_;
rhi::Handle<rhi::UniformSet> us_1;
rhi::Handle<rhi::UniformSet> us_2;
std::vector<MergedTwodeeCommandList> cmd_lists_;
std::vector<std::tuple<rhi::Handle<rhi::Buffer>, std::size_t>> vbos_;
std::vector<std::tuple<rhi::Handle<rhi::Buffer>, std::size_t>> ibos_;
bool rebuild_atlases_ = false;
rhi::Handle<rhi::RenderPass> render_pass_;
rhi::Handle<rhi::Texture> output_;
uint32_t output_width_ = 0;
uint32_t output_height_ = 0;
void rewrite_patch_quad_vertices(Draw2dList& list, const Draw2dPatchQuad& cmd) const;
TwodeePass();
virtual ~TwodeePass();

385
src/hwr2/patch_atlas.cpp Normal file
View file

@ -0,0 +1,385 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#include "patch_atlas.hpp"
#include <stb_rect_pack.h>
#include "../r_patch.h"
using namespace srb2;
using namespace srb2::hwr2;
using namespace srb2::rhi;
rhi::Rect srb2::hwr2::trimmed_patch_dimensions(const patch_t* patch)
{
bool minx_found = false;
int32_t minx = 0;
int32_t maxx = 0;
int32_t miny = patch->height;
int32_t maxy = 0;
for (int32_t x = 0; x < patch->width; x++)
{
const int32_t columnofs = patch->columnofs[x];
const column_t* column = reinterpret_cast<const column_t*>(patch->columns + columnofs);
// If the first pole is empty (topdelta = 255), there are no pixels in this column
if (!minx_found && column->topdelta == 0xFF)
{
// Thus, the minx is at least one higher than the current column.
minx = x + 1;
continue;
}
minx_found = true;
if (minx_found && column->topdelta != 0xFF)
{
maxx = x;
}
miny = std::min(static_cast<int32_t>(column->topdelta), miny);
int32_t prevdelta = 0;
int32_t topdelta = 0;
while (column->topdelta != 0xFF)
{
topdelta = column->topdelta;
// Tall patches hack
if (topdelta <= prevdelta)
{
topdelta += prevdelta;
}
prevdelta = topdelta;
maxy = std::max(topdelta + column->length, maxy);
column = reinterpret_cast<const column_t*>(reinterpret_cast<const uint8_t*>(column) + column->length + 4);
}
}
maxx += 1;
maxx = std::max(minx, maxx);
maxy = std::max(miny, maxy);
return {minx, miny, static_cast<uint32_t>(maxx - minx), static_cast<uint32_t>(maxy - miny)};
}
void srb2::hwr2::convert_patch_to_trimmed_rg8_pixels(const patch_t* patch, std::vector<uint8_t>& out)
{
Rect trimmed_rect = srb2::hwr2::trimmed_patch_dimensions(patch);
if (trimmed_rect.w % 2 > 0)
{
// In order to force 4-byte row alignment, an extra column is added to the image data.
// Look up GL_UNPACK_ALIGNMENT (which defaults to 4 bytes)
trimmed_rect.w += 1;
}
out.clear();
// 2 bytes per pixel; 1 for the color index, 1 for the alpha. (RG8)
out.resize(trimmed_rect.w * trimmed_rect.h * 2, 0);
for (int32_t x = 0; x < static_cast<int32_t>(trimmed_rect.w) && x < (patch->width - trimmed_rect.x); x++)
{
const int32_t columnofs = patch->columnofs[x + trimmed_rect.x];
const column_t* column = reinterpret_cast<const column_t*>(patch->columns + columnofs);
int32_t prevdelta = 0;
int32_t topdelta = 0;
while (column->topdelta != 0xFF)
{
topdelta = column->topdelta;
// prevdelta is used to implement tall patches hack
if (topdelta <= prevdelta)
{
topdelta += prevdelta;
}
prevdelta = topdelta;
const uint8_t* source = reinterpret_cast<const uint8_t*>(column) + 3;
int32_t count = column->length; // is this byte order safe...?
for (int32_t i = 0; i < count; i++)
{
int32_t output_y = topdelta + i - trimmed_rect.y;
if (output_y < 0)
{
continue;
}
if (output_y >= static_cast<int32_t>(trimmed_rect.h))
{
break;
}
size_t pixel_index = (output_y * trimmed_rect.w + x) * 2;
out[pixel_index + 0] = source[i]; // index in luminance/red channel
out[pixel_index + 1] = 0xFF; // alpha/green value of 1
}
column = reinterpret_cast<const column_t*>(reinterpret_cast<const uint8_t*>(column) + column->length + 4);
}
}
}
PatchAtlas::PatchAtlas(Handle<Texture> texture, uint32_t size) : tex_(texture), size_(size)
{
rp_ctx = std::make_unique<stbrp_context>();
rp_nodes = std::make_unique<stbrp_node[]>(size * 2);
const size_t double_size = size * 2;
for (size_t i = 0; i < double_size; i++)
{
rp_nodes[i] = {};
}
stbrp_init_target(rp_ctx.get(), size, size, rp_nodes.get(), double_size);
}
PatchAtlas::PatchAtlas(PatchAtlas&&) = default;
PatchAtlas& PatchAtlas::operator=(PatchAtlas&&) = default;
void PatchAtlas::pack_rects(tcb::span<stbrp_rect> rects)
{
stbrp_pack_rects(rp_ctx.get(), rects.data(), rects.size());
}
std::optional<PatchAtlas::Entry> PatchAtlas::find_patch(srb2::NotNull<const patch_t*> patch) const
{
auto itr = entries_.find(patch);
if (itr == entries_.end())
{
return std::nullopt;
}
return itr->second;
}
PatchAtlasCache::PatchAtlasCache(uint32_t tex_size, size_t max_textures)
: tex_size_(tex_size)
, max_textures_(max_textures)
{
}
PatchAtlasCache::PatchAtlasCache(PatchAtlasCache&&) = default;
PatchAtlasCache& PatchAtlasCache::operator=(PatchAtlasCache&&) = default;
PatchAtlasCache::~PatchAtlasCache() = default;
bool PatchAtlasCache::need_to_reset() const
{
if (atlases_.size() > max_textures_)
{
return true;
}
if (Patch_WasFreedThisFrame())
{
return true;
}
return false;
}
void PatchAtlasCache::reset(Rhi& rhi)
{
for (auto& atlas : atlases_)
{
rhi.destroy_texture(atlas.texture());
}
atlases_.clear();
patch_lookup_.clear();
}
bool PatchAtlasCache::ready_for_lookup() const
{
if (!patches_to_pack_.empty())
{
return false;
}
return true;
}
static PatchAtlas create_atlas(Rhi& rhi, uint32_t size)
{
Handle<Texture> texture = rhi.create_texture(
{
TextureFormat::kLuminanceAlpha,
size,
size,
TextureWrapMode::kClamp,
TextureWrapMode::kClamp
}
);
PatchAtlas new_atlas(texture, size);
return new_atlas;
}
void PatchAtlasCache::pack(Rhi& rhi)
{
// Prepare stbrp rects for patches to be loaded.
std::vector<stbrp_rect> rects;
std::vector<const patch_t*> large_patches;
std::vector<const patch_t*> patches;
for (auto patch : patches_to_pack_)
{
patches.push_back(patch);
}
for (size_t i = 0; i < patches.size(); i++)
{
const patch_t* patch = patches[i];
Rect trimmed_rect = trimmed_patch_dimensions(patch);
if (rect_is_large(trimmed_rect.w, trimmed_rect.h))
{
large_patches.push_back(patch);
continue;
}
stbrp_rect rect {};
rect.id = i;
rect.w = trimmed_rect.w;
rect.h = trimmed_rect.h;
rects.push_back(std::move(rect));
}
while (rects.size() > 0)
{
if (atlases_.size() == 0)
{
atlases_.push_back(create_atlas(rhi, tex_size_));
}
for (size_t atlas_index = 0; atlas_index < atlases_.size(); atlas_index++)
{
auto& atlas = atlases_[atlas_index];
atlas.pack_rects(rects);
for (auto itr = rects.begin(); itr != rects.end();)
{
auto& rect = *itr;
if (rect.was_packed)
{
PatchAtlas::Entry entry;
const patch_t* patch = patches[rect.id];
Rect trimmed_rect = trimmed_patch_dimensions(patch);
entry.x = static_cast<uint32_t>(rect.x);
entry.y = static_cast<uint32_t>(rect.y);
entry.w = static_cast<uint32_t>(rect.w);
entry.h = static_cast<uint32_t>(rect.h);
entry.trim_x = static_cast<uint32_t>(trimmed_rect.x);
entry.trim_y = static_cast<uint32_t>(trimmed_rect.y);
entry.orig_w = static_cast<uint32_t>(patch->width);
entry.orig_h = static_cast<uint32_t>(patch->height);
atlas.entries_.insert_or_assign(patch, std::move(entry));
patch_lookup_.insert_or_assign(patch, atlas_index);
patches_to_upload_.insert(patch);
rects.erase(itr);
continue;
}
++itr;
}
// If we still have rects to pack, and we're at the last atlas, create another atlas.
if (atlas_index == atlases_.size() - 1 && rects.size() > 0)
{
atlases_.push_back(create_atlas(rhi, tex_size_));
}
}
}
// TODO Create large patch "atlases"
patches_to_pack_.clear();
}
PatchAtlas* PatchAtlasCache::find_patch(srb2::NotNull<const patch_t*> patch)
{
SRB2_ASSERT(ready_for_lookup());
auto itr = patch_lookup_.find(patch);
if (itr == patch_lookup_.end())
{
return nullptr;
}
size_t atlas_index = itr->second;
SRB2_ASSERT(atlas_index < atlases_.size());
return &atlases_[atlas_index];
}
const PatchAtlas* PatchAtlasCache::find_patch(srb2::NotNull<const patch_t*> patch) const
{
SRB2_ASSERT(ready_for_lookup());
auto itr = patch_lookup_.find(patch);
if (itr == patch_lookup_.end())
{
return nullptr;
}
size_t atlas_index = itr->second;
SRB2_ASSERT(atlas_index < atlases_.size());
return &atlases_[atlas_index];
}
void PatchAtlasCache::queue_patch(srb2::NotNull<const patch_t*> patch)
{
if (patch_lookup_.find(patch) != patch_lookup_.end())
{
return;
}
patches_to_pack_.insert(patch);
}
void PatchAtlasCache::prepass(Rhi& rhi)
{
if (need_to_reset())
{
reset(rhi);
}
}
void PatchAtlasCache::transfer(Rhi& rhi, Handle<TransferContext> ctx)
{
SRB2_ASSERT(ready_for_lookup());
// Upload atlased patches
std::vector<uint8_t> patch_data;
for (const patch_t* patch_to_upload : patches_to_upload_)
{
srb2::NotNull<PatchAtlas*> atlas = find_patch(patch_to_upload);
std::optional<PatchAtlas::Entry> entry = atlas->find_patch(patch_to_upload);
SRB2_ASSERT(entry.has_value());
convert_patch_to_trimmed_rg8_pixels(patch_to_upload, patch_data);
rhi.update_texture(
ctx,
atlas->tex_,
{static_cast<int32_t>(entry->x), static_cast<int32_t>(entry->y), entry->w, entry->h},
PixelFormat::kRG8,
tcb::as_bytes(tcb::span(patch_data))
);
patch_data.clear();
}
patches_to_upload_.clear();
}
void PatchAtlasCache::graphics(Rhi& rhi, Handle<GraphicsContext> ctx)
{
}
void PatchAtlasCache::postpass(Rhi& rhi)
{
}

145
src/hwr2/patch_atlas.hpp Normal file
View file

@ -0,0 +1,145 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#ifndef __SRB2_HWR2_PATCH_ATLAS_HPP__
#define __SRB2_HWR2_PATCH_ATLAS_HPP__
#include <cstdint>
#include <memory>
#include <optional>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <tcb/span.hpp>
#include "pass.hpp"
#include "../r_defs.h"
extern "C"
{
// Forward declare the stb_rect_pack types since they are only pointed to
struct stbrp_context;
struct stbrp_node;
struct stbrp_rect;
};
namespace srb2::hwr2
{
class PatchAtlas
{
public:
struct Entry
{
uint32_t x;
uint32_t y;
uint32_t w;
uint32_t h;
uint32_t trim_x;
uint32_t trim_y;
uint32_t orig_w;
uint32_t orig_h;
};
private:
rhi::Handle<rhi::Texture> tex_;
uint32_t size_;
std::unordered_map<const patch_t*, Entry> entries_;
std::unique_ptr<stbrp_context> rp_ctx {nullptr};
std::unique_ptr<stbrp_node[]> rp_nodes {nullptr};
friend class PatchAtlasCache;
public:
PatchAtlas(rhi::Handle<rhi::Texture> tex, uint32_t size);
PatchAtlas(const PatchAtlas&) = delete;
PatchAtlas& operator=(const PatchAtlas&) = delete;
PatchAtlas(PatchAtlas&&);
PatchAtlas& operator=(PatchAtlas&&);
/// @brief Get the Luminance-Alpha RHI texture handle for this atlas texture
rhi::Handle<rhi::Texture> texture() const noexcept { return tex_; }
uint32_t texture_size() const noexcept { return size_; }
std::optional<Entry> find_patch(srb2::NotNull<const patch_t*> patch) const;
void pack_rects(tcb::span<stbrp_rect> rects);
};
/// @brief A resource-managing pass which creates and manages a set of Atlas Textures with
/// optimally packed Patches, allowing drawing passes to reuse the same texture binds for
/// drawing things like sprites and 2D elements.
class PatchAtlasCache : public Pass
{
std::vector<PatchAtlas> atlases_;
std::unordered_map<const patch_t*, size_t> patch_lookup_;
std::unordered_set<const patch_t*> patches_to_pack_;
std::unordered_set<const patch_t*> patches_to_upload_;
uint32_t tex_size_ = 2048;
size_t max_textures_ = 2;
bool need_to_reset() const;
/// @brief Clear the atlases and reset for lookup.
void reset(rhi::Rhi& rhi);
bool ready_for_lookup() const;
/// @brief Decide if a rect's dimensions are Large, that is, the rect should not be packed and instead its patch
/// should be uploaded in isolation.
bool rect_is_large(uint32_t w, uint32_t h) const noexcept { return false; }
public:
PatchAtlasCache(uint32_t tex_size, size_t max_textures);
PatchAtlasCache(const PatchAtlasCache&) = delete;
PatchAtlasCache(PatchAtlasCache&&);
PatchAtlasCache& operator=(const PatchAtlasCache&) = delete;
PatchAtlasCache& operator=(PatchAtlasCache&&);
virtual ~PatchAtlasCache();
/// @brief Queue a patch to be packed. All patches will be packed after the prepass phase,
/// or the owner can explicitly request a pack.
void queue_patch(srb2::NotNull<const patch_t*> patch);
/// @brief Pack queued patches, allowing them to be looked up with find_patch.
void pack(rhi::Rhi& rhi);
/// @brief Find the atlas a patch belongs to, or nullopt if it is not cached.
/// This may not be called if there are still patches that need to be packed.
/// The return value of this function may change between invocations of prepass for any given input.
const PatchAtlas* find_patch(srb2::NotNull<const patch_t*> patch) const;
PatchAtlas* find_patch(srb2::NotNull<const patch_t*> patch);
virtual void prepass(rhi::Rhi& rhi) override;
virtual void transfer(rhi::Rhi& rhi, rhi::Handle<rhi::TransferContext> ctx) override;
virtual void graphics(rhi::Rhi& rhi, rhi::Handle<rhi::GraphicsContext> ctx) override;
virtual void postpass(rhi::Rhi& rhi) override;
};
/// @brief Calculate the subregion of the patch which excludes empty space on the borders.
rhi::Rect trimmed_patch_dimensions(const patch_t* patch);
/// @brief Convert a patch to RG8 pixel data. If the patch's trimmed width is not a multiple of 2,
/// an additional blank column will be emitted to the output; this pixel data is ignored by RHI
/// during upload, but required for the RHI device's Unpack Alignment of 4 bytes.
/// @param patch the patch to convert
/// @param out the output vector, cleared before writing.
void convert_patch_to_trimmed_rg8_pixels(const patch_t* patch, std::vector<uint8_t>& out);
} // namespace srb2::hwr2
#endif // __SRB2_HWR2_PATCH_ATLAS_HPP__

View file

@ -17,6 +17,7 @@
#include "cxxutil.hpp"
#include "f_finale.h"
#include "hwr2/patch_atlas.hpp"
#include "hwr2/pass_blit_postimg_screens.hpp"
#include "hwr2/pass_blit_rect.hpp"
#include "hwr2/pass_imgui.hpp"
@ -192,12 +193,14 @@ static InternalPassData build_pass_manager()
auto palette_manager = std::make_shared<MainPaletteManager>();
auto common_resources_manager = std::make_shared<CommonResourcesManager>();
auto flat_texture_manager = std::make_shared<FlatTextureManager>();
auto patch_atlas_cache = std::make_shared<PatchAtlasCache>(2048, 2);
auto resource_manager = std::make_shared<PassManager>();
resource_manager->insert("framebuffer_manager", framebuffer_manager);
resource_manager->insert("palette_manager", palette_manager);
resource_manager->insert("common_resources_manager", common_resources_manager);
resource_manager->insert("flat_texture_manager", flat_texture_manager);
resource_manager->insert("patch_atlas_cache", patch_atlas_cache);
// Basic Rendering is responsible for drawing 3d, 2d, and postprocessing the image.
// This is drawn to an alternating internal color buffer.
@ -209,6 +212,7 @@ static InternalPassData build_pass_manager()
auto blit_postimg_screens = std::make_shared<BlitPostimgScreens>(palette_manager);
auto twodee = std::make_shared<TwodeePass>();
twodee->flat_manager_ = flat_texture_manager;
twodee->patch_atlas_cache_ = patch_atlas_cache;
twodee->data_ = make_twodee_pass_data();
twodee->ctx_ = &g_2d;
auto pp_simple_blit_pass = std::make_shared<BlitRectPass>(false);
@ -451,6 +455,7 @@ static InternalPassData build_pass_manager()
void I_NewTwodeeFrame(void)
{
g_2d = Twodee();
Patch_ResetFreedThisFrame();
}
void I_NewImguiFrame(void)

View file

@ -606,6 +606,10 @@ char sprnames[NUMSPRITES + 1][5] =
"TWBS", // Tripwire Boost
"TWBT", // Tripwire BLASTER
"SMLD", // Smooth landing
"TIRG", // Tire grabbers
"RSHT", // DEZ Ring Shooter
"DEZL", // DEZ Laser respawn
// Additional Kart Objects
@ -4464,6 +4468,14 @@ state_t states[NUMSTATES] =
{SPR_SMLD, FF_FULLBRIGHT|FF_ADD|FF_ANIMATE, -1, {NULL}, 7, 2, S_NULL}, // S_SMOOTHLANDING
{SPR_TIRG, FF_ANIMATE, -1, {NULL}, 1, 1, S_NULL}, // S_TIREGRABBER
{SPR_RSHT, FF_PAPERSPRITE|0, -1, {NULL}, 0, 0, S_NULL}, // S_RINGSHOOTER_SIDE
{SPR_RSHT, FF_SEMIBRIGHT|FF_PAPERSPRITE|2, -1, {NULL}, 0, 0, S_NULL}, // S_RINGSHOOTER_NIPPLES
{SPR_RSHT, FF_PAPERSPRITE|4, -1, {NULL}, 0, 0, S_NULL}, // S_RINGSHOOTER_SCREEN
{SPR_RSHT, FF_FULLBRIGHT|FF_PAPERSPRITE|5, -1, {NULL}, 0, 0, S_NULL}, // S_RINGSHOOTER_NUMBERBACK
{SPR_RSHT, FF_FULLBRIGHT|FF_PAPERSPRITE|9, -1, {NULL}, 0, 0, S_NULL}, // S_RINGSHOOTER_NUMBERFRONT
{SPR_PLAY, FF_FULLBRIGHT|FF_PAPERSPRITE|SPR2_XTRA, -1, {A_RingShooterFace}, 0, 0, S_NULL}, // S_RINGSHOOTER_FACE
{SPR_DEZL, FF_FULLBRIGHT|FF_PAPERSPRITE, 8, {NULL}, 0, 0, S_NULL}, // S_DEZLASER
{SPR_DEZL, FF_FULLBRIGHT|1, 2, {NULL}, 0, 0, S_DEZLASER_TRAIL2}, // S_DEZLASER_TRAIL1
{SPR_DEZL, FF_FULLBRIGHT|2, 2, {NULL}, 0, 0, S_DEZLASER_TRAIL3}, // S_DEZLASER_TRAIL2
@ -24623,6 +24635,114 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
S_NULL // raisestate
},
{ // MT_TIREGRABBER
-1, // doomednum
S_TIREGRABBER, // spawnstate
1000, // spawnhealth
S_NULL, // seestate
sfx_None, // seesound
0, // reactiontime
sfx_None, // attacksound
S_NULL, // painstate
0, // painchance
sfx_None, // painsound
S_NULL, // meleestate
S_NULL, // missilestate
S_NULL, // deathstate
S_NULL, // xdeathstate
sfx_None, // deathsound
0, // speed
20*FRACUNIT, // radius
36*FRACUNIT, // height
0, // display offset
0, // mass
0, // damage
sfx_None, // activesound
MF_NOBLOCKMAP|MF_NOCLIPHEIGHT|MF_NOCLIPTHING|MF_NOGRAVITY|MF_SCENERY|MF_DONTENCOREMAP, // flags
S_NULL // raisestate
},
{ // MT_RINGSHOOTER
-1, // doomednum
S_INVISIBLE, // spawnstate
1000, // spawnhealth
S_NULL, // seestate
sfx_None, // seesound
0, // reactiontime
sfx_None, // attacksound
S_NULL, // painstate
0, // painchance
sfx_s3ka7, // painsound
S_NULL, // meleestate
S_NULL, // missilestate
S_NULL, // deathstate
S_NULL, // xdeathstate
sfx_s3kad, // deathsound
0, // speed
24*FRACUNIT, // radius
8*FRACUNIT, // height
0, // display offset
0, // mass
0, // damage
sfx_None, // activesound
MF_NOGRAVITY|MF_SCENERY|MF_DONTENCOREMAP, // flags
S_NULL // raisestate
},
{ // MT_RINGSHOOTER_PART
-1, // doomednum
S_RINGSHOOTER_SIDE, // spawnstate
1000, // spawnhealth
S_NULL, // seestate
sfx_None, // seesound
0, // reactiontime
sfx_None, // attacksound
S_NULL, // painstate
0, // painchance
sfx_None, // painsound
S_NULL, // meleestate
S_NULL, // missilestate
S_NULL, // deathstate
S_NULL, // xdeathstate
sfx_None, // deathsound
0, // speed
6*FRACUNIT, // radius
70*FRACUNIT, // height
0, // display offset
0, // mass
0, // damage
sfx_None, // activesound
MF_NOBLOCKMAP|MF_NOCLIPHEIGHT|MF_NOCLIPTHING|MF_NOGRAVITY|MF_SCENERY|MF_DONTENCOREMAP, // flags
S_NULL // raisestate
},
{ // MT_RINGSHOOTER_SCREEN
-1, // doomednum
S_RINGSHOOTER_SCREEN, // spawnstate
1000, // spawnhealth
S_NULL, // seestate
sfx_None, // seesound
0, // reactiontime
sfx_None, // attacksound
S_NULL, // painstate
0, // painchance
sfx_None, // painsound
S_NULL, // meleestate
S_NULL, // missilestate
S_NULL, // deathstate
S_NULL, // xdeathstate
sfx_None, // deathsound
0, // speed
23*FRACUNIT, // radius
39*FRACUNIT, // height
0, // display offset
0, // mass
0, // damage
sfx_None, // activesound
MF_NOBLOCKMAP|MF_NOCLIPHEIGHT|MF_NOCLIPTHING|MF_NOGRAVITY|MF_SCENERY|MF_DONTENCOREMAP, // flags
S_NULL // raisestate
},
{ // MT_DEZLASER
-1, // doomednum
S_DEZLASER, // spawnstate

View file

@ -294,6 +294,7 @@ enum actionnum
A_FLAMESHIELDPAPER,
A_INVINCSPARKLEROTATE,
A_SPAWNITEMDEBRISCLOUD,
A_RINGSHOOTERFACE,
NUMACTIONS
};
@ -568,6 +569,7 @@ void A_MementosTPParticles();
void A_FlameShieldPaper();
void A_InvincSparkleRotate();
void A_SpawnItemDebrisCloud();
void A_RingShooterFace();
extern boolean actionsoverridden[NUMACTIONS];
@ -1157,6 +1159,10 @@ typedef enum sprite
SPR_TWBS, // Tripwire Boost
SPR_TWBT, // Tripwire BLASTER
SPR_SMLD, // Smooth landing
SPR_TIRG, // Tire grabbers
SPR_RSHT, // DEZ Ring Shooter
SPR_DEZL, // DEZ Laser respawn
// Additional Kart Objects
@ -4906,6 +4912,15 @@ typedef enum state
S_SMOOTHLANDING,
// DEZ Ring Shooter
S_TIREGRABBER,
S_RINGSHOOTER_SIDE,
S_RINGSHOOTER_NIPPLES,
S_RINGSHOOTER_SCREEN,
S_RINGSHOOTER_NUMBERBACK,
S_RINGSHOOTER_NUMBERFRONT,
S_RINGSHOOTER_FACE,
// DEZ Laser respawn
S_DEZLASER,
S_DEZLASER_TRAIL1,
@ -6508,6 +6523,11 @@ typedef enum mobj_type
MT_SMOOTHLANDING,
MT_TIREGRABBER,
MT_RINGSHOOTER,
MT_RINGSHOOTER_PART,
MT_RINGSHOOTER_SCREEN,
MT_DEZLASER,
MT_WAYPOINT,

View file

@ -444,15 +444,22 @@ boolean K_LandMineCollide(mobj_t *t1, mobj_t *t2)
else
t2->z += t2->height;
P_SpawnMobj(t2->x/2 + t1->x/2, t2->y/2 + t1->y/2, t2->z/2 + t1->z/2, MT_ITEMCLASH);
S_StartSound(t2, t2->info->deathsound);
P_KillMobj(t2, t1, t1, DMG_NORMAL);
P_SetObjectMomZ(t2, 24*FRACUNIT, false);
P_InstaThrust(t2, bounceangle, 16*FRACUNIT);
if (P_MobjWasRemoved(t2))
{
t2 = NULL; // handles the arguments to P_KillMobj
}
else
{
P_SetObjectMomZ(t2, 24*FRACUNIT, false);
P_InstaThrust(t2, bounceangle, 16*FRACUNIT);
P_SpawnMobj(t2->x/2 + t1->x/2, t2->y/2 + t1->y/2, t2->z/2 + t1->z/2, MT_ITEMCLASH);
t1->reactiontime = t2->hitlag;
t1->reactiontime = t2->hitlag;
}
P_KillMobj(t1, t2, t2, DMG_NORMAL);
}
else if (t2->type == MT_SSMINE_SHIELD || t2->type == MT_SSMINE || t2->type == MT_LANDMINE)
@ -466,7 +473,15 @@ boolean K_LandMineCollide(mobj_t *t1, mobj_t *t2)
// Shootable damage
P_DamageMobj(t2, t1, t1->target, 1, DMG_NORMAL);
t1->reactiontime = t2->hitlag;
if (P_MobjWasRemoved(t2))
{
t2 = NULL; // handles the arguments to P_KillMobj
}
else
{
t1->reactiontime = t2->hitlag;
}
P_KillMobj(t1, t2, t2, DMG_NORMAL);
}
@ -796,6 +811,10 @@ boolean K_KitchenSinkCollide(mobj_t *t1, mobj_t *t2)
{
// Shootable damage
P_KillMobj(t2, t2, t1->target, DMG_NORMAL);
if (P_MobjWasRemoved(t2))
{
t2 = NULL; // handles the arguments to P_KillMobj
}
// This item damage
P_KillMobj(t1, t2, t2, DMG_NORMAL);
}

View file

@ -3407,7 +3407,9 @@ SINT8 K_GetForwardMove(player_t *player)
{
SINT8 forwardmove = player->cmd.forwardmove;
if ((player->pflags & PF_STASIS) || (player->carry == CR_SLIDING))
if ((player->pflags & PF_STASIS)
|| (player->carry == CR_SLIDING)
|| Obj_PlayerRingShooterFreeze(player) == true)
{
return 0;
}
@ -3418,7 +3420,9 @@ SINT8 K_GetForwardMove(player_t *player)
return MAXPLMOVE;
}
if (player->spinouttimer || K_PlayerEBrake(player))
if (player->spinouttimer != 0
|| K_PressingEBrake(player) == true
|| K_PlayerEBrake(player) == true)
{
return 0;
}
@ -6164,7 +6168,8 @@ void K_PopPlayerShield(player_t *player)
return; // everything is handled by Obj_GardenTopDestroy
case KSHIELD_LIGHTNING:
K_DoLightningShield(player);
S_StartSound(player->mo, sfx_s3k7c);
// K_DoLightningShield(player);
break;
}
@ -7546,6 +7551,11 @@ static void K_UpdateTripwire(player_t *player)
}
}
boolean K_PressingEBrake(player_t *player)
{
return ((K_GetKartButtons(player) & BT_EBRAKEMASK) == BT_EBRAKEMASK);
}
/** \brief Decreases various kart timers and powers per frame. Called in P_PlayerThink in p_user.c
\param player player object passed from P_PlayerThink
@ -7725,7 +7735,7 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd)
}
// Make ABSOLUTELY SURE that your flashing tics don't get set WHILE you're still in hit animations.
if (player->spinouttimer != 0 || player->wipeoutslow != 0)
if (player->spinouttimer != 0)
{
if (( player->spinouttype & KSPIN_IFRAMES ) == 0)
player->flashing = 0;
@ -8073,7 +8083,7 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd)
K_SpawnGardenTopSpeedLines(player);
}
// Only allow drifting while NOT trying to do an spindash input.
else if ((K_GetKartButtons(player) & BT_EBRAKEMASK) != BT_EBRAKEMASK)
else if (K_PressingEBrake(player) == false)
{
player->pflags |= PF_DRIFTINPUT;
}
@ -8855,10 +8865,21 @@ INT16 K_UpdateSteeringValue(INT16 inputSteering, INT16 destSteering)
// player->steering is the turning value, but with easing applied.
// Keeps micro-turning from old easing, but isn't controller dependent.
const INT16 amount = KART_FULLTURN/3;
INT16 amount = KART_FULLTURN/3;
INT16 diff = destSteering - inputSteering;
INT16 outputSteering = inputSteering;
// We switched steering directions, lighten up on easing for a more responsive countersteer.
// (Don't do this for steering 0, let digital inputs tap-adjust!)
if ((inputSteering > 0 && destSteering < 0) || (inputSteering < 0 && destSteering > 0))
{
// Don't let small turns in direction X allow instant turns in direction Y.
INT16 countersteer = min(KART_FULLTURN, abs(inputSteering)); // The farthest we should go is to 0 -- neutral.
amount = max(countersteer, amount); // But don't reduce turning strength from baseline either.
}
if (abs(diff) <= amount)
{
// Reached the intended value, set instantly.
@ -8929,10 +8950,16 @@ INT16 K_GetKartTurnValue(player_t *player, INT16 turnvalue)
return 0;
}
if (Obj_PlayerRingShooterFreeze(player) == true)
{
// No turning while using Ring Shooter
return 0;
}
currentSpeed = FixedHypot(player->mo->momx, player->mo->momy);
if ((currentSpeed <= 0) // Not moving
&& ((K_GetKartButtons(player) & BT_EBRAKEMASK) != BT_EBRAKEMASK) // Not e-braking
&& (K_PressingEBrake(player) == false) // Not e-braking
&& (player->respawn.state == RESPAWNST_NONE) // Not respawning
&& (player->curshield != KSHIELD_TOP) // Not riding a Top
&& (P_IsObjectOnGround(player->mo) == true)) // On the ground
@ -9718,7 +9745,12 @@ static INT32 K_FlameShieldMax(player_t *player)
boolean K_PlayerEBrake(player_t *player)
{
if (player->respawn.state != RESPAWNST_NONE
&& player->respawn.init == true)
&& (player->respawn.init == true || player->respawn.fromRingShooter == true))
{
return false;
}
if (Obj_PlayerRingShooterFreeze(player) == true)
{
return false;
}
@ -9728,7 +9760,7 @@ boolean K_PlayerEBrake(player_t *player)
return true;
}
if ((K_GetKartButtons(player) & BT_EBRAKEMASK) == BT_EBRAKEMASK
if (K_PressingEBrake(player) == true
&& player->drift == 0
&& P_PlayerInPain(player) == false
&& player->justbumped == 0
@ -11375,6 +11407,8 @@ void K_MoveKartPlayer(player_t *player, boolean onground)
{
player->pflags &= ~PF_AIRFAILSAFE;
}
Obj_RingShooterInput(player);
}
void K_CheckSpectateStatus(void)
@ -11644,6 +11678,7 @@ void K_EggmanTransfer(player_t *source, player_t *victim)
return;
K_AddHitLag(victim->mo, 2, true);
K_DropItems(victim);
victim->eggmanexplode = 6*TICRATE;
victim->itemRoulette.eggman = false;
victim->itemRoulette.active = false;

View file

@ -81,6 +81,7 @@ void K_SpawnBumpEffect(mobj_t *mo);
void K_KartMoveAnimation(player_t *player);
void K_KartPlayerHUDUpdate(player_t *player);
void K_KartResetPlayerColor(player_t *player);
boolean K_PressingEBrake(player_t *player);
void K_KartPlayerThink(player_t *player, ticcmd_t *cmd);
void K_KartPlayerAfterThink(player_t *player);
fixed_t K_MomentumThreshold(const mobj_t *mo);

View file

@ -1673,7 +1673,6 @@ static void M_DrawProfileCard(INT32 x, INT32 y, boolean greyedout, profile_t *p)
UINT16 truecol = SKINCOLOR_BLACK;
UINT8 *colormap = R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_BLACK, GTC_CACHE);
INT32 skinnum = -1;
INT32 powerlevel = -1;
char pname[PROFILENAMELEN+1] = "NEW";
@ -1683,7 +1682,6 @@ static void M_DrawProfileCard(INT32 x, INT32 y, boolean greyedout, profile_t *p)
colormap = R_GetTranslationColormap(TC_DEFAULT, truecol, GTC_CACHE);
strcpy(pname, p->profilename);
skinnum = R_SkinAvailable(p->skinname);
powerlevel = p->powerlevels[0]; // Only display race power level.
}
// check setup_player for colormap for the card.
@ -1700,13 +1698,13 @@ static void M_DrawProfileCard(INT32 x, INT32 y, boolean greyedout, profile_t *p)
if (greyedout)
return; // only used for profiles we can't select.
// Draw pwlv if we can
if (powerlevel > -1)
if (p != NULL)
{
V_DrawFixedPatch((x+30)*FRACUNIT, (y+84)*FRACUNIT, FRACUNIT, 0, pwrlv, colormap);
V_DrawCenteredKartString(x+30, y+87, 0, va("%d\n", powerlevel));
V_DrawCenteredKartString(x+30, y+87, 0, va("%d", p->wins));
}
// check what setup_player is doing in priority.
if (sp->mdepth >= CSSTEP_CHARS)
{

View file

@ -106,6 +106,14 @@ void Obj_LoopEndpointCollide(mobj_t *special, mobj_t *toucher);
void Obj_BeginDropTargetMorph(mobj_t *target, skincolornum_t color);
boolean Obj_DropTargetMorphThink(mobj_t *morph);
/* Ring Shooter */
boolean Obj_RingShooterThinker(mobj_t *mo);
boolean Obj_PlayerRingShooterFreeze(player_t *const player);
void Obj_RingShooterInput(player_t *player);
void Obj_PlayerUsedRingShooter(mobj_t *base, player_t *player);
void Obj_RingShooterDelete(mobj_t *mo);
void Obj_UpdateRingShooterFace(mobj_t *part);
#ifdef __cplusplus
} // extern "C"
#endif

View file

@ -46,7 +46,6 @@ profile_t* PR_MakeProfile(
boolean guest)
{
profile_t *new = Z_Malloc(sizeof(profile_t), PU_STATIC, NULL);
UINT8 i;
new->version = PROFILEVER;
@ -72,11 +71,7 @@ profile_t* PR_MakeProfile(
// Copy from gamecontrol directly as we'll be setting controls up directly in the profile.
memcpy(new->controls, controlarray, sizeof(new->controls));
// Init both power levels
for (i = 0; i < PWRLV_NUMTYPES; i++)
{
new->powerlevels[i] = (guest ? 0 : PWRLVRECORD_START);
}
new->wins = 0;
return new;
}
@ -270,11 +265,7 @@ void PR_SaveProfiles(void)
WRITESTRINGN(save.p, profilesList[i]->follower, SKINNAMESIZE);
WRITEUINT16(save.p, profilesList[i]->followercolor);
// PWR.
for (j = 0; j < PWRLV_NUMTYPES; j++)
{
WRITEUINT16(save.p, profilesList[i]->powerlevels[j]);
}
WRITEUINT32(save.p, profilesList[i]->wins);
// Consvars.
WRITEUINT8(save.p, profilesList[i]->kickstartaccel);
@ -397,16 +388,15 @@ void PR_LoadProfiles(void)
profilesList[i]->followercolor = PROFILEDEFAULTFOLLOWERCOLOR;
}
// PWR.
for (j = 0; j < PWRLV_NUMTYPES; j++)
// Profile update 5-->6: PWR isn't in profile data anymore.
if (version < 6)
{
profilesList[i]->powerlevels[j] = READUINT16(save.p);
if (profilesList[i]->powerlevels[j] < PWRLVRECORD_MIN
|| profilesList[i]->powerlevels[j] > PWRLVRECORD_MAX)
{
// invalid, reset
profilesList[i]->powerlevels[j] = PWRLVRECORD_START;
}
save.p += PWRLV_NUMTYPES*2;
profilesList[i]->wins = 0;
}
else
{
profilesList[i]->wins = READUINT32(save.p);
}
// Consvars.
@ -425,7 +415,7 @@ void PR_LoadProfiles(void)
{
#ifdef DEVELOP
// Profile update 1-->2: Add gc_rankings.
// Profile update 3-->5: Add gc_startlossless.
// Profile update 4-->5: Add gc_startlossless.
if ((j == gc_rankings && version < 2) ||
(j == gc_startlossless && version < 5))
{
@ -630,3 +620,12 @@ char *GetPrettyRRID(const unsigned char *bin, boolean brief)
return rrid_buf;
}
static char allZero[PUBKEYLENGTH];
boolean PR_IsKeyGuest(uint8_t *key)
{
//memset(allZero, 0, PUBKEYLENGTH); -- not required, allZero is 0's
return (memcmp(key, allZero, PUBKEYLENGTH) == 0);
}

View file

@ -31,7 +31,7 @@ extern "C" {
#define SKINNAMESIZE 16
#define PROFILENAMELEN 6
#define PROFILEVER 5
#define PROFILEVER 6
#define MAXPROFILES 16
#define PROFILESFILE "ringprofiles.prf"
#define PROFILE_GUEST 0
@ -69,7 +69,7 @@ struct profile_t
char follower[SKINNAMESIZE+1]; // Follower
UINT16 followercolor; // Follower color
UINT16 powerlevels[PWRLV_NUMTYPES]; // PWRLV for each gametype.
UINT32 wins; // I win I win I win
// Player-specific consvars.
// @TODO: List all of those
@ -166,6 +166,8 @@ boolean PR_IsLocalPlayerGuest(INT32 player);
char *GetPrettyRRID(const unsigned char *bin, boolean brief);
boolean PR_IsKeyGuest(uint8_t *key);
#ifdef __cplusplus
} // extern "C"
#endif

View file

@ -17,6 +17,7 @@
#include "p_tick.h" // leveltime
#include "k_grandprix.h"
#include "k_profiles.h"
#include "k_serverstats.h"
// Client-sided calculations done for Power Levels.
// This is done so that clients will never be able to hack someone else's score over the server.
@ -414,7 +415,6 @@ void K_CashInPowerLevels(void)
{
SINT8 powerType = K_UsingPowerLevels();
UINT8 i;
boolean gamedataupdate;
//CONS_Printf("\n========\n");
//CONS_Printf("Cashing in power level changes...\n");
@ -424,29 +424,17 @@ void K_CashInPowerLevels(void)
{
if (playeringame[i] == true && powerType != PWRLV_DISABLED)
{
profile_t *pr = PR_GetPlayerProfile(&players[i]);
INT16 inc = K_FinalPowerIncrement(&players[i], clientpowerlevels[i][powerType], clientPowerAdd[i]);
clientpowerlevels[i][powerType] += inc;
//CONS_Printf("%s: %d -> %d (%d)\n", player_names[i], clientpowerlevels[i][powerType] - inc, clientpowerlevels[i][powerType], inc);
if (pr != NULL && inc != 0)
{
pr->powerlevels[powerType] = clientpowerlevels[i][powerType];
gamedataupdate = true;
}
}
clientPowerAdd[i] = 0;
}
if (gamedataupdate)
{
M_UpdateUnlockablesAndExtraEmblems(true, true);
G_SaveGameData();
}
SV_UpdateStats();
//CONS_Printf("========\n");
}
@ -580,7 +568,6 @@ void K_SetPowerLevelScrambles(SINT8 powertype)
void K_PlayerForfeit(UINT8 playerNum, boolean pointLoss)
{
profile_t *pr;
UINT8 p = 0;
SINT8 powerType = PWRLV_DISABLED;
@ -651,18 +638,10 @@ void K_PlayerForfeit(UINT8 playerNum, boolean pointLoss)
return;
}
if (inc < 0 && pointLoss == false)
if (pointLoss)
{
// Don't record point losses for sync-out / crashes.
return;
}
pr = PR_GetPlayerProfile(&players[playerNum]);
if (pr != NULL)
{
pr->powerlevels[powerType] = yourPower + inc;
M_UpdateUnlockablesAndExtraEmblems(true, true);
G_SaveGameData();
clientpowerlevels[playerNum][powerType] += clientPowerAdd[playerNum];
clientPowerAdd[playerNum] = 0;
SV_UpdateStats();
}
}

View file

@ -175,9 +175,27 @@ void K_DoIngameRespawn(player_t *player)
}
else if (player->respawn.wp != NULL)
{
const UINT32 dist = RESPAWN_DIST + (player->airtime * 48);
player->respawn.distanceleft = (dist * mapobjectscale) / FRACUNIT;
K_RespawnAtWaypoint(player, player->respawn.wp);
if (player->respawn.fromRingShooter == true)
{
waypoint_t *prevWP = player->respawn.wp;
while (prevWP->numprevwaypoints > 0)
{
prevWP = prevWP->prevwaypoints[0];
if (K_GetWaypointIsSpawnpoint(prevWP) == true)
break;
}
const UINT32 dist = (player->airtime * 48);
player->respawn.distanceleft = (dist * mapobjectscale) / FRACUNIT;
K_RespawnAtWaypoint(player, prevWP);
}
else
{
const UINT32 dist = RESPAWN_DIST + (player->airtime * 48);
player->respawn.distanceleft = (dist * mapobjectscale) / FRACUNIT;
K_RespawnAtWaypoint(player, player->respawn.wp);
}
}
else
{
@ -465,7 +483,9 @@ static void K_MovePlayerToRespawnPoint(player_t *player)
player->mo->momz = step.z;
}
if (player->respawn.init == false && K_PlayerEBrake(player) == true)
if (player->respawn.init == false
&& player->respawn.fromRingShooter == false
&& K_PlayerEBrake(player) == true)
{
// Manual drop!
player->respawn.state = RESPAWNST_DROP;
@ -822,6 +842,7 @@ void K_RespawnChecker(player_t *player)
K_MovePlayerToRespawnPoint(player);
return;
case RESPAWNST_DROP:
player->respawn.fromRingShooter = false;
player->mo->momx = player->mo->momy = 0;
player->flashing = 3;
if (player->respawn.timer > 0)

236
src/k_serverstats.c Normal file
View file

@ -0,0 +1,236 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 1998-2000 by DooM Legacy Team.
// Copyright (C) 1999-2020 by Sonic Team Junior.
// Copyright (C) 2023 by AJ "Tyron" Martinez
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
/// \file k_serverstats.c
/// \brief implements methods for serverside stat tracking.
#include "doomtype.h"
#include "d_main.h" // pandf
#include "byteptr.h" // READ/WRITE macros
#include "p_saveg.h" // savebuffer_t
#include "m_misc.h" //FIL_WriteFile()
#include "k_serverstats.h"
#include "z_zone.h"
#include "time.h"
static serverplayer_t *trackedList;
static size_t numtracked = 0;
static size_t numallocated = 0;
static boolean initialized = false;
UINT16 guestpwr[PWRLV_NUMTYPES]; // All-zero power level to reference for guests
static void SV_InitializeStats(void)
{
if (!initialized)
{
numallocated = 8;
trackedList = Z_Calloc(
sizeof(serverplayer_t) * numallocated,
PU_STATIC,
&trackedList
);
if (trackedList == NULL)
{
I_Error("Not enough memory for server stats\n");
}
initialized = true;
}
}
static void SV_ExpandStats(size_t needed)
{
I_Assert(trackedList != NULL);
while (numallocated < needed)
{
numallocated *= 2;
trackedList = Z_Realloc(
trackedList,
sizeof(serverplayer_t) * numallocated,
PU_STATIC,
&trackedList
);
if (trackedList == NULL)
{
I_Error("Not enough memory for server stats\n");
}
}
}
// Read stats file to trackedList for ingame use
void SV_LoadStats(void)
{
const size_t headerlen = strlen(SERVERSTATSHEADER);
savebuffer_t save = {0};
unsigned int i, j;
if (!server)
return;
if (P_SaveBufferFromFile(&save, va(pandf, srb2home, SERVERSTATSFILE)) == false)
{
return;
}
SV_InitializeStats();
if (strncmp(SERVERSTATSHEADER, (const char *)save.buffer, headerlen))
{
const char *gdfolder = "the Ring Racers folder";
if (strcmp(srb2home,"."))
gdfolder = srb2home;
P_SaveBufferFree(&save);
I_Error("Not a valid server stats file.\nDelete %s (maybe in %s) and try again.", SERVERSTATSFILE, gdfolder);
}
save.p += headerlen;
UINT8 version = READUINT8(save.p);
(void)version; // for now
numtracked = READUINT32(save.p);
SV_ExpandStats(numtracked);
for(i = 0; i < numtracked; i++)
{
READMEM(save.p, trackedList[i].public_key, PUBKEYLENGTH);
READMEM(save.p, &trackedList[i].lastseen, sizeof(trackedList[i].lastseen));
for(j = 0; j < PWRLV_NUMTYPES; j++)
{
trackedList[i].powerlevels[j] = READUINT16(save.p);
}
trackedList[i].hash = quickncasehash((char*)trackedList[i].public_key, PUBKEYLENGTH);
}
}
// Save trackedList to disc
void SV_SaveStats(void)
{
size_t length = 0;
const size_t headerlen = strlen(SERVERSTATSHEADER);
savebuffer_t save = {0};
unsigned int i, j;
if (!server)
return;
// header + version + numtracked + payload
if (P_SaveBufferAlloc(&save, headerlen + sizeof(UINT32) + sizeof(UINT8) + (numtracked * sizeof(serverplayer_t))) == false)
{
I_Error("No more free memory for saving server stats\n");
return;
}
// Add header.
WRITESTRINGN(save.p, SERVERSTATSHEADER, headerlen);
WRITEUINT8(save.p, SERVERSTATSVER);
WRITEUINT32(save.p, numtracked);
for(i = 0; i < numtracked; i++)
{
WRITEMEM(save.p, trackedList[i].public_key, PUBKEYLENGTH);
WRITEMEM(save.p, &trackedList[i].lastseen, sizeof(trackedList[i].lastseen));
for(j = 0; j < PWRLV_NUMTYPES; j++)
{
WRITEUINT16(save.p, trackedList[i].powerlevels[j]);
}
}
length = save.p - save.buffer;
if (!FIL_WriteFile(va(pandf, srb2home, SERVERSTATSFILE), save.buffer, length))
{
P_SaveBufferFree(&save);
I_Error("Couldn't save server stats. Are you out of Disk space / playing in a protected folder?");
}
P_SaveBufferFree(&save);
}
// New player, grab their stats from trackedList or initialize new ones if they're new
serverplayer_t *SV_RetrieveStats(uint8_t *key)
{
UINT32 j, hash;
SV_InitializeStats();
hash = quickncasehash((char*)key, PUBKEYLENGTH);
// Existing record?
for(j = 0; j < numtracked; j++)
{
if (hash != trackedList[j].hash) // Not crypto magic, just an early out with a faster comparison
continue;
if (memcmp(trackedList[j].public_key, key, PUBKEYLENGTH) == 0)
return &trackedList[j];
}
// Untracked below this point, make a new record
SV_ExpandStats(numtracked+1);
// Default stats
trackedList[numtracked].lastseen = time(NULL);
memcpy(&trackedList[numtracked].public_key, key, PUBKEYLENGTH);
for(j = 0; j < PWRLV_NUMTYPES; j++)
{
trackedList[numtracked].powerlevels[j] = PR_IsKeyGuest(key) ? 0 : PWRLVRECORD_START;
}
trackedList[numtracked].hash = quickncasehash((char*)key, PUBKEYLENGTH);
numtracked++;
return &trackedList[numtracked - 1];
}
// Write player stats to trackedList, then save to disk
void SV_UpdateStats(void)
{
UINT32 i, j, hash;
if (!server)
return;
SV_InitializeStats();
for(i = 0; i < MAXPLAYERS; i++)
{
if (!playeringame[i])
continue;
if (PR_IsKeyGuest(players[i].public_key))
continue;
hash = quickncasehash((char*)players[i].public_key, PUBKEYLENGTH);
for(j = 0; j < numtracked; j++)
{
if (hash != trackedList[j].hash) // Not crypto magic, just an early out with a faster comparison
continue;
if (memcmp(&trackedList[j].public_key, players[i].public_key, PUBKEYLENGTH) == 0)
{
trackedList[j].lastseen = time(NULL);
memcpy(&trackedList[j].powerlevels, clientpowerlevels[i], sizeof(trackedList[j].powerlevels));
break;
}
}
// SV_RetrievePWR should always be called for a key before SV_UpdateStats runs,
// so this shouldn't be reachable.
}
SV_SaveStats();
}

53
src/k_serverstats.h Normal file
View file

@ -0,0 +1,53 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 1993-1996 by id Software, Inc.
// Copyright (C) 1998-2000 by DooM Legacy Team.
// Copyright (C) 2011-2016 by Matthew "Inuyasha" Walsh.
// Copyright (C) 1999-2018 by Sonic Team Junior.
// Copyright (C) 2023 by AJ "Tyron" Martinez
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
/// \file k_serverstats.h
/// \brief serverside stat tracking definitions
#ifndef __SERVERSTATS_H__
#define __SERVERSTATS_H__
#include "doomdef.h" // MAXPLAYERNAME
#include "g_input.h" // Input related stuff
#include "string.h" // strcpy etc
#include "g_game.h" // game CVs
#ifdef __cplusplus
extern "C" {
#endif
#define SERVERSTATSFILE "srvstats.dat"
#define SERVERSTATSHEADER "Doctor Robotnik's Ring Racers Server Stats"
#define SERVERSTATSVER 1
struct serverplayer_t
{
uint8_t public_key[PUBKEYLENGTH];
UINT32 lastseen;
UINT16 powerlevels[PWRLV_NUMTYPES];
UINT32 hash; // Not persisted! Used for early outs during key comparisons
};
void SV_SaveStats(void);
void SV_LoadStats(void);
serverplayer_t *SV_RetrieveStats(uint8_t *key);
void SV_UpdateStats(void);
#ifdef __cplusplus
} // extern "C"
#endif
#endif

View file

@ -584,7 +584,7 @@ void K_ProcessTerrainEffect(mobj_t *mo)
{
if (player->mo->floorrover != NULL)
{
slope = *player->mo->ceilingrover->t_slope;
slope = *player->mo->floorrover->t_slope;
}
else
{

View file

@ -683,25 +683,6 @@ boolean M_CheckCondition(condition_t *cn, player_t *player)
}
case UC_TOTALRINGS: // Requires grabbing >= x rings
return (gamedata->totalrings >= (unsigned)cn->requirement);
case UC_POWERLEVEL: // Requires power level >= x on a certain gametype
{
UINT8 i;
if (gamestate == GS_LEVEL)
return false; // this one could be laggy with many profiles available
for (i = PROFILE_GUEST; i < PR_GetNumProfiles(); i++)
{
profile_t *p = PR_GetProfile(i);
if (p->powerlevels[cn->extrainfo1] >= (unsigned)cn->requirement)
{
return true;
}
}
return false;
}
case UC_GAMECLEAR: // Requires game beaten >= x times
return (gamedata->timesBeaten >= (unsigned)cn->requirement);
case UC_OVERALLTIME: // Requires overall time <= x
@ -1039,12 +1020,6 @@ static const char *M_GetConditionString(condition_t *cn)
return va("collect %u,%03u Rings", (cn->requirement/1000), (cn->requirement%1000));
return va("collect %u Rings", cn->requirement);
case UC_POWERLEVEL: // Requires power level >= x on a certain gametype
return va("get a PWR of %d in %s", cn->requirement,
(cn->extrainfo1 == PWRLV_RACE)
? "Race"
: "Battle");
case UC_GAMECLEAR: // Requires game beaten >= x times
if (cn->requirement > 1)
return va("beat game %d times", cn->requirement);

View file

@ -32,8 +32,6 @@ typedef enum
UC_ROUNDSPLAYED, // ROUNDSPLAYED [x played]
UC_TOTALRINGS, // TOTALRINGS [x collected]
UC_POWERLEVEL, // SRB2Kart: POWERLEVEL [power level to reach] [gametype, "0" for race, "1" for battle]
UC_GAMECLEAR, // GAMECLEAR <x times>
UC_OVERALLTIME, // OVERALLTIME [time to beat, tics]

View file

@ -490,7 +490,7 @@ static void M_DrawTickStats(void)
void M_DrawPerfStats(void)
{
char s[100];
char s[363];
PS_SetFrameTime();

View file

@ -22,11 +22,10 @@ menuitem_t OPTIONS_ProfileControls[] = {
{IT_CONTROL, "X", "Brake / Back",
"PR_BTX", {.routine = M_ProfileSetControl}, gc_x, 0},
// @TODO What does this do???
{IT_CONTROL, "Y", "N/A ?",
{IT_CONTROL, "Y", "Respawn",
"PR_BTY", {.routine = M_ProfileSetControl}, gc_y, 0},
{IT_CONTROL, "Z", "N/A ?",
{IT_CONTROL, "Z", "Multiplayer quick-chat / quick-vote",
"PR_BTZ", {.routine = M_ProfileSetControl}, gc_z, 0},
{IT_CONTROL, "L", "Use item",
@ -62,13 +61,13 @@ menuitem_t OPTIONS_ProfileControls[] = {
{IT_CONTROL, "RECORD LOSSLESS", "Record a pixel perfect GIF.",
NULL, {.routine = M_ProfileSetControl}, gc_startlossless, 0},
{IT_CONTROL, "OPEN CHAT", "Opens chatbox in online games.",
{IT_CONTROL, "OPEN CHAT", "Opens full keyboard chatting for online games.",
NULL, {.routine = M_ProfileSetControl}, gc_talk, 0},
{IT_CONTROL, "OPEN TEAM CHAT", "Do we even have team gamemodes?",
{IT_CONTROL, "OPEN TEAM CHAT", "Opens team-only full chat for online games.",
NULL, {.routine = M_ProfileSetControl}, gc_teamtalk, 0},
{IT_CONTROL, "SHOW RANKINGS", "Show mid-game rankings.",
{IT_CONTROL, "SHOW RANKINGS", "Display the current rankings mid-game.",
NULL, {.routine = M_ProfileSetControl}, gc_rankings, 0},
{IT_CONTROL, "OPEN CONSOLE", "Opens the developer options console.",

View file

@ -14,4 +14,5 @@ target_sources(SRB2SDL2 PRIVATE
item-spot.c
loops.c
drop-target.c
ring-shooter.c
)

View file

@ -260,14 +260,25 @@ boolean Obj_OrbinautJawzCollide(mobj_t *t1, mobj_t *t2)
damageitem = false;
}
if (damageitem)
if (damageitem && P_MobjWasRemoved(t1) == false)
{
angle_t bounceangle;
if (P_MobjWasRemoved(t2) == false)
{
bounceangle = K_GetCollideAngle(t2, t1);
}
else
{
bounceangle = K_MomentumAngle(t1) + ANGLE_90;
t2 = NULL; // handles the arguments to P_KillMobj
}
// This Item Damage
angle_t bounceangle = K_GetCollideAngle(t2, t1);
S_StartSound(t1, t1->info->deathsound);
P_KillMobj(t1, t2, t2, DMG_NORMAL);
P_SetObjectMomZ(t1, 24*FRACUNIT, false);
P_InstaThrust(t1, bounceangle, 16*FRACUNIT);
}

801
src/objects/ring-shooter.c Normal file
View file

@ -0,0 +1,801 @@
// DR. ROBOTNIK'S RING RACERS
//-----------------------------------------------------------------------------
// Copyright (C) by Sally "TehRealSalt" Cochenour
// Copyright (C) by "Lach"
// Copyright (C) by Kart Krew
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
/// \file ring-shooter.c
/// \brief DEZ "Ring Shooter" respawner object
#include "../doomdef.h"
#include "../doomstat.h"
#include "../info.h"
#include "../k_kart.h"
#include "../k_objects.h"
#include "../m_random.h"
#include "../p_local.h"
#include "../r_main.h"
#include "../s_sound.h"
#include "../g_game.h"
#include "../z_zone.h"
#include "../k_waypoint.h"
#include "../r_skins.h"
#include "../k_respawn.h"
#include "../lua_hook.h"
#define RS_FUSE_TIME (4*TICRATE)
#define RS_FUSE_BLINK (TICRATE >> 1)
#define RS_GRABBER_START (16 << FRACBITS)
#define RS_GRABBER_SLIDE (RS_GRABBER_START >> 4)
#define RS_GRABBER_EXTRA (18 << FRACBITS)
#define RS_KARTED_INC (3)
#define rs_base_scalespeed(o) ((o)->scalespeed)
#define rs_base_initstate(o) ((o)->threshold)
#define rs_base_xscale(o) ((o)->extravalue1)
#define rs_base_yscale(o) ((o)->extravalue2)
#define rs_base_playerid(o) ((o)->lastlook)
#define rs_base_playerface(o) ((o)->cusval)
#define rs_base_playerlast(o) ((o)->watertop)
#define rs_base_karted(o) ((o)->movecount)
#define rs_base_grabberdist(o) ((o)->movefactor)
#define rs_base_canceled(o) ((o)->cvmem)
#define rs_part_xoffset(o) ((o)->extravalue1)
#define rs_part_yoffset(o) ((o)->extravalue2)
static void RemoveRingShooterPointer(mobj_t *base)
{
player_t *player = NULL;
if (rs_base_playerid(base) < 0 || rs_base_playerid(base) >= MAXPLAYERS)
{
// No pointer set
return;
}
// NULL the player's pointer.
player = &players[ rs_base_playerid(base) ];
P_SetTarget(&player->ringShooter, NULL);
// Remove our player ID
rs_base_playerid(base) = -1;
}
static void ChangeRingShooterPointer(mobj_t *base, player_t *player)
{
// Remove existing pointer first.
RemoveRingShooterPointer(base);
if (player == NULL)
{
// Just remove it.
return;
}
// Set new player pointer.
P_SetTarget(&player->ringShooter, base);
// Set new player ID.
rs_base_playerid(base) = (player - players);
}
static void ScalePart(mobj_t *part, mobj_t *base)
{
part->spritexscale = rs_base_xscale(base);
part->spriteyscale = rs_base_yscale(base);
if (part->type == MT_TIREGRABBER)
{
part->spritexscale /= 2;
part->spriteyscale /= 2;
}
}
static void MovePart(mobj_t *part, mobj_t *base, mobj_t *refNipple)
{
P_MoveOrigin(
part,
refNipple->x + FixedMul(rs_part_xoffset(part), rs_base_xscale(base)),
refNipple->y + FixedMul(rs_part_yoffset(part), rs_base_xscale(base)),
part->z
);
}
static void ShowHidePart(mobj_t *part, mobj_t *base)
{
part->renderflags = (part->renderflags & ~RF_DONTDRAW) | (base->renderflags & RF_DONTDRAW);
}
static fixed_t GetTireDist(mobj_t *base)
{
return -(RS_GRABBER_EXTRA + rs_base_grabberdist(base));
}
static void MoveTire(mobj_t *part, mobj_t *base)
{
const fixed_t dis = FixedMul(GetTireDist(base), base->scale);
const fixed_t c = FINECOSINE(part->angle >> ANGLETOFINESHIFT);
const fixed_t s = FINESINE(part->angle >> ANGLETOFINESHIFT);
P_MoveOrigin(
part,
base->x + FixedMul(dis, c),
base->y + FixedMul(dis, s),
part->z
);
}
// I've tried to reduce redundancy as much as I can,
// but check K_SpawnRingShooter if you edit this
static void UpdateRingShooterParts(mobj_t *mo)
{
mobj_t *part, *refNipple;
part = mo;
while (!P_MobjWasRemoved(part->target))
{
part = part->target;
ScalePart(part, mo);
MoveTire(part, mo);
}
part = mo;
while (!P_MobjWasRemoved(part->hprev))
{
part = part->hprev;
ScalePart(part, mo);
}
refNipple = part;
part = mo;
while (!P_MobjWasRemoved(part->hnext))
{
part = part->hnext;
MovePart(part, mo, refNipple);
ScalePart(part, mo);
}
part = mo->tracer;
part->z = mo->z + FixedMul(refNipple->height, rs_base_yscale(mo));
MovePart(part, mo, refNipple);
ScalePart(part, mo);
}
static void UpdateRingShooterPartsVisibility(mobj_t *mo)
{
mobj_t *part;
part = mo;
while (!P_MobjWasRemoved(part->target))
{
part = part->target;
ShowHidePart(part, mo);
}
part = mo;
while (!P_MobjWasRemoved(part->hprev))
{
part = part->hprev;
ShowHidePart(part, mo);
}
part = mo;
while (!P_MobjWasRemoved(part->hnext))
{
part = part->hnext;
ShowHidePart(part, mo);
}
part = mo->tracer;
ShowHidePart(part, mo);
}
static void RingShooterCountdown(mobj_t *mo)
{
mobj_t *part = mo->tracer;
if (mo->reactiontime < 0)
{
return;
}
if (--mo->reactiontime > 0)
{
return;
}
while (!P_MobjWasRemoved(part->tracer))
{
part = part->tracer;
part->frame--;
}
switch ((part->frame & FF_FRAMEMASK) - (part->state->frame & FF_FRAMEMASK))
{
case -1:
{
mo->reactiontime = -1;
if (rs_base_playerface(mo) >= 0 && rs_base_playerface(mo) < MAXPLAYERS)
{
if (playeringame[rs_base_playerface(mo)] == true)
{
player_t *player = &players[ rs_base_playerid(mo) ];
part->skin = &skins[player->skin];
}
}
P_SetMobjState(part, S_RINGSHOOTER_FACE);
break;
}
case 0:
{
mo->reactiontime = TICRATE;
S_StartSound(mo, mo->info->deathsound);
if (rs_base_playerid(mo) >= 0 && rs_base_playerid(mo) < MAXPLAYERS)
{
if (playeringame[rs_base_playerid(mo)] == true)
{
player_t *player = &players[ rs_base_playerid(mo) ];
Obj_PlayerUsedRingShooter(mo, player);
}
}
break;
}
default:
{
mo->reactiontime = TICRATE;
S_StartSound(mo, mo->info->painsound);
break;
}
}
}
static void RingShooterFlicker(mobj_t *mo)
{
UINT32 trans;
mobj_t *part = mo->tracer;
while (!P_MobjWasRemoved(part->tracer))
{
part = part->tracer;
}
part->renderflags ^= RF_DONTDRAW;
if (part->renderflags & RF_DONTDRAW)
{
trans = FF_TRANS50;
}
else
{
trans = 0;
}
part->target->frame = (part->target->frame & ~FF_TRANSMASK) | trans;
}
static void ActivateRingShooter(mobj_t *mo)
{
mobj_t *part = mo->tracer;
while (!P_MobjWasRemoved(part->tracer))
{
part = part->tracer;
part->renderflags &= ~RF_DONTDRAW;
part->frame += 4;
}
RingShooterCountdown(mo);
}
static boolean RingShooterInit(mobj_t *mo)
{
if (rs_base_initstate(mo) == -1)
{
return false;
}
switch (rs_base_initstate(mo))
{
case 0:
{
rs_base_yscale(mo) += rs_base_scalespeed(mo);
if (rs_base_yscale(mo) >= FRACUNIT)
{
//rs_base_xscale(mo) -= rs_base_scalespeed(mo);
rs_base_initstate(mo)++;
}
break;
}
case 1:
{
rs_base_scalespeed(mo) -= FRACUNIT/5;
rs_base_yscale(mo) += rs_base_scalespeed(mo);
rs_base_xscale(mo) -= rs_base_scalespeed(mo);
if (rs_base_yscale(mo) < 3*FRACUNIT/4)
{
rs_base_initstate(mo)++;
rs_base_scalespeed(mo) = FRACUNIT >> 2;
}
break;
}
case 2:
{
rs_base_yscale(mo) += rs_base_scalespeed(mo);
rs_base_xscale(mo) -= rs_base_scalespeed(mo);
if (rs_base_yscale(mo) >= FRACUNIT)
{
rs_base_initstate(mo)++;
rs_base_xscale(mo) = rs_base_yscale(mo) = FRACUNIT;
}
break;
}
case 3:
{
if (rs_base_canceled(mo) != 0)
{
rs_base_initstate(mo) = -1;
ActivateRingShooter(mo);
}
else
{
rs_base_grabberdist(mo) -= RS_GRABBER_SLIDE;
if (rs_base_grabberdist(mo) <= 0)
{
rs_base_initstate(mo) = -1;
rs_base_grabberdist(mo) = 0;
ActivateRingShooter(mo);
}
}
break;
}
default:
{
rs_base_initstate(mo) = 0; // fix invalid states
break;
}
}
UpdateRingShooterParts(mo);
return (rs_base_initstate(mo) != -1);
}
boolean Obj_RingShooterThinker(mobj_t *mo)
{
if (RingShooterInit(mo) == true)
{
return true;
}
if (mo->fuse > 0)
{
mo->fuse--;
if (mo->fuse == 0)
{
P_RemoveMobj(mo);
return false;
}
}
if (rs_base_canceled(mo) == 0)
{
rs_base_karted(mo) += RS_KARTED_INC;
if (P_MobjWasRemoved(mo->tracer) == false)
{
RingShooterCountdown(mo);
}
}
if (P_MobjWasRemoved(mo->tracer) == false)
{
RingShooterFlicker(mo);
}
if (mo->fuse < RS_FUSE_BLINK)
{
if (leveltime & 1)
{
mo->renderflags |= RF_DONTDRAW;
}
else
{
mo->renderflags &= ~RF_DONTDRAW;
}
UpdateRingShooterPartsVisibility(mo);
}
return true;
}
void Obj_PlayerUsedRingShooter(mobj_t *base, player_t *player)
{
const UINT8 playerID = player - players;
if (playerID == rs_base_playerlast(base))
{
return;
}
// The original player should no longer have control over it,
// if they are using it via releasing.
RemoveRingShooterPointer(base);
// Respawn using the respawner's karted value.
if (rs_base_karted(base) > 0)
{
player->airtime += rs_base_karted(base);
}
player->respawn.fromRingShooter = true;
K_DoIngameRespawn(player);
// Now other players can run into it!
base->flags |= MF_SPECIAL;
// Reset the fuse so everyone can conga line :B
if (base->fuse < RS_FUSE_TIME)
{
if (base->fuse < RS_FUSE_BLINK)
{
base->renderflags &= ~RF_DONTDRAW;
UpdateRingShooterPartsVisibility(base);
}
base->fuse = RS_FUSE_TIME;
}
// Record the last person to use the ring shooter.
rs_base_playerlast(base) = playerID;
}
void Obj_RingShooterDelete(mobj_t *mo)
{
mobj_t *part;
RemoveRingShooterPointer(mo);
part = mo->target;
while (P_MobjWasRemoved(part) == false)
{
mobj_t *delete = part;
part = part->target;
P_RemoveMobj(delete);
}
part = mo->hprev;
while (P_MobjWasRemoved(part) == false)
{
mobj_t *delete = part;
part = part->hprev;
P_RemoveMobj(delete);
}
part = mo->hnext;
while (P_MobjWasRemoved(part) == false)
{
mobj_t *delete = part;
part = part->hnext;
P_RemoveMobj(delete);
}
part = mo->tracer;
if (P_MobjWasRemoved(part) == false)
{
P_RemoveMobj(part);
}
}
// I've tried to reduce redundancy as much as I can,
// but check P_UpdateRingShooterParts if you edit this
static void SpawnRingShooter(player_t *player)
{
const fixed_t scale = 2*FRACUNIT;
mobjinfo_t *info = &mobjinfo[MT_RINGSHOOTER_PART];
mobj_t *mo = player->mo;
mobj_t *base = P_SpawnMobj(mo->x, mo->y, mo->z, MT_RINGSHOOTER);
mobj_t *part, *refNipple;
UINT32 frameNum;
angle_t angle;
vector2_t offset;
SINT8 i;
rs_base_playerid(base) = rs_base_playerlast(base) = -1;
rs_base_karted(base) = -(RS_KARTED_INC * TICRATE); // wait for "3"
rs_base_grabberdist(base) = RS_GRABBER_START;
K_FlipFromObject(base, mo);
P_SetScale(base, base->destscale = FixedMul(base->destscale, scale));
base->angle = mo->angle;
base->scalespeed = FRACUNIT/2;
base->extravalue1 = FRACUNIT; // horizontal scale
base->extravalue2 = 0; // vertical scale
base->fuse = RS_FUSE_TIME;
// the ring shooter object itself is invisible and acts as the thinker
// each ring shooter uses four linked lists to keep track of its parts
// the hprev chain stores the two NIPPLE BARS
// the hnext chain stores the four sides of the box
// the tracer chain stores the screen and the screen layers
// the target chain stores the tire grabbers
// spawn the RING NIPPLES
part = base;
frameNum = 0;
FV2_Load(&offset, -96*FRACUNIT, 160*FRACUNIT);
FV2_Divide(&offset, scale);
for (i = -1; i < 2; i += 2)
{
P_SetTarget(&part->hprev, P_SpawnMobjFromMobj(base,
P_ReturnThrustX(NULL, base->angle - ANGLE_90, i*offset.x) + P_ReturnThrustX(NULL, base->angle, offset.y),
P_ReturnThrustY(NULL, base->angle - ANGLE_90, i*offset.x) + P_ReturnThrustY(NULL, base->angle, offset.y),
0, MT_RINGSHOOTER_PART));
P_SetTarget(&part->hprev->hnext, part);
part = part->hprev;
P_SetTarget(&part->target, base);
part->angle = base->angle - i * ANGLE_45;
P_SetMobjState(part, S_RINGSHOOTER_NIPPLES);
part->frame += frameNum;
part->flags |= MF_NOTHINK;
part->old_spriteyscale = part->spriteyscale = 0;
frameNum++;
}
refNipple = part; // keep the second ring nipple; its position will be referenced by the box
// spawn the box
part = base;
frameNum = 0;
angle = base->angle + ANGLE_90;
FV2_Load(&offset, offset.x - info->radius, offset.y - info->radius); // set the new origin to the centerpoint of the box
FV2_Load(&offset,
P_ReturnThrustX(NULL, base->angle - ANGLE_90, offset.x) + P_ReturnThrustX(NULL, base->angle, offset.y),
P_ReturnThrustY(NULL, base->angle - ANGLE_90, offset.x) + P_ReturnThrustY(NULL, base->angle, offset.y)); // transform it relative to the base
for (i = 0; i < 4; i++)
{
P_SetTarget(&part->hnext, P_SpawnMobjFromMobj(base,
offset.x + P_ReturnThrustX(NULL, angle, info->radius),
offset.y + P_ReturnThrustY(NULL, angle, info->radius),
0, MT_RINGSHOOTER_PART));
P_SetTarget(&part->hnext->hprev, part);
part = part->hnext;
P_SetTarget(&part->target, base);
if (i == 2)
frameNum++;
frameNum ^= FF_HORIZONTALFLIP;
angle -= ANGLE_90;
part->angle = angle;
part->frame += frameNum;
part->extravalue1 = part->x - refNipple->x;
part->extravalue2 = part->y - refNipple->y;
part->flags |= MF_NOTHINK;
part->old_spriteyscale = part->spriteyscale = 0;
}
// spawn the screen
part = P_SpawnMobjFromMobj(base, offset.x, offset.y, 0, MT_RINGSHOOTER_SCREEN);
P_SetTarget(&base->tracer, part);
P_SetTarget(&part->target, base);
part->angle = base->angle - ANGLE_45;
part->extravalue1 = part->x - refNipple->x;
part->extravalue2 = part->y - refNipple->y;
part->flags |= MF_NOTHINK;
part->old_spriteyscale = part->spriteyscale = 0;
// spawn the screen numbers
for (i = 0; i < 2; i++)
{
P_SetTarget(&part->tracer, P_SpawnMobjFromMobj(part, 0, 0, 0, MT_OVERLAY));
P_SetTarget(&part->tracer->target, part);
part = part->tracer;
part->angle = part->target->angle;
P_SetMobjState(part, S_RINGSHOOTER_NUMBERBACK + i);
part->renderflags |= RF_DONTDRAW;
}
P_SetTarget(&part->hprev, base);
// spawn the grabbers
part = base;
angle = base->angle + ANGLE_45;
for (i = 0; i < 4; i++)
{
const fixed_t dis = GetTireDist(base);
P_SetTarget(
&part->target,
P_SpawnMobjFromMobj(
base,
P_ReturnThrustX(NULL, angle, dis),
P_ReturnThrustY(NULL, angle, dis),
0,
MT_TIREGRABBER
)
);
part = part->target;
P_SetTarget(&part->tracer, base);
angle -= ANGLE_90;
part->angle = angle;
part->extravalue1 = part->extravalue2 = 0;
part->old_spriteyscale = part->spriteyscale = 0;
}
ChangeRingShooterPointer(base, player);
rs_base_playerface(base) = (player - players);
}
static boolean AllowRingShooter(player_t *player)
{
const fixed_t minSpeed = 6 * player->mo->scale;
if (/*(gametyperules & GTR_CIRCUIT) &&*/ leveltime < starttime)
{
return false;
}
if (player->respawn.state != RESPAWNST_NONE)
{
return false;
}
if (player->drift == 0
&& player->justbumped == 0
&& player->spindashboost == 0
&& player->nocontrol == 0
&& player->fastfall == 0
&& player->speed < minSpeed
&& P_PlayerInPain(player) == false
&& P_IsObjectOnGround(player->mo) == true)
{
return true;
}
return false;
}
boolean Obj_PlayerRingShooterFreeze(player_t *const player)
{
mobj_t *const base = player->ringShooter;
if (AllowRingShooter(player) == true
&& (player->cmd.buttons & BT_RESPAWN) == BT_RESPAWN
&& P_MobjWasRemoved(base) == false)
{
return (rs_base_canceled(base) == 0);
}
return false;
}
void Obj_RingShooterInput(player_t *player)
{
mobj_t *const base = player->ringShooter;
if (AllowRingShooter(player) == true
&& (player->cmd.buttons & BT_RESPAWN) == BT_RESPAWN)
{
if (P_MobjWasRemoved(base) == true)
{
SpawnRingShooter(player);
return;
}
if (rs_base_canceled(base) == 0)
{
player->mo->momx = player->mo->momy = 0;
P_SetPlayerAngle(player, base->angle);
fixed_t setz;
if (base->eflags & MFE_VERTICALFLIP)
{
setz = base->z + base->height - player->mo->height;
setz = max(setz, player->mo->z);
}
else
{
setz = min(player->mo->z, base->z);
}
P_MoveOrigin(
player->mo,
base->x, base->y,
setz
);
player->fastfall = 0;
if (base->fuse < RS_FUSE_TIME)
{
if (base->fuse < RS_FUSE_BLINK)
{
base->renderflags &= ~RF_DONTDRAW;
UpdateRingShooterPartsVisibility(base);
}
base->fuse = RS_FUSE_TIME;
}
}
}
else if (P_MobjWasRemoved(base) == false)
{
if (rs_base_initstate(base) != -1)
{
// We released during the intro animation.
// Cancel it entirely, prevent another one being created for a bit.
rs_base_canceled(base) = 1;
if (base->fuse > RS_FUSE_BLINK)
{
base->fuse = RS_FUSE_BLINK;
}
}
else if (rs_base_canceled(base) == 0)
{
// We released during the countdown.
// We activate with the current karted timer on the ring shooter.
Obj_PlayerUsedRingShooter(base, player);
}
}
}
void Obj_UpdateRingShooterFace(mobj_t *part)
{
mobj_t *const base = part->hprev;
player_t *player = NULL;
if (P_MobjWasRemoved(base) == true)
{
return;
}
if (rs_base_playerface(base) < 0 || rs_base_playerface(base) >= MAXPLAYERS)
{
return;
}
if (playeringame[ rs_base_playerface(base) ] == false)
{
return;
}
player = &players[ rs_base_playerface(base) ];
// it's a good idea to set the actor's skin *before* it uses this action,
// but just in case, if it doesn't have the player's skin, set its skin then call the state again to get the correct sprite
if (part->skin != &skins[player->skin])
{
part->skin = &skins[player->skin];
P_SetMobjState(part, (statenum_t)(part->state - states));
return;
}
// okay, now steal the player's color nyehehehe
part->color = player->skincolor;
// set the frame to the WANTED pic
part->frame = (part->frame & ~FF_FRAMEMASK) | FACE_WANTED;
// set the threshold overlay flags
part->threshold = (OV_DONTXYSCALE|OV_DONTSCREENOFFSET);
// we're going to assume the character's WANTED icon is 32 x 32
// let's squish the sprite a bit so that it matches the dimensions of the screen's sprite, which is 26 x 22
// (TODO: maybe get the dimensions/offsets from the patches themselves?)
part->spritexscale = FixedDiv(26*FRACUNIT, 32*FRACUNIT);
part->spriteyscale = FixedDiv(22*FRACUNIT, 32*FRACUNIT);
// a normal WANTED icon should have (0, 0) offsets
// so let's offset it such that it will match the position of the screen's sprite
part->spritexoffset = 16*FRACUNIT; // 32 / 2
part->spriteyoffset = 28*FRACUNIT + FixedDiv(11*FRACUNIT, part->spriteyscale); // 32 - 4 (generic monster bottom) + 11 (vertical offset of screen sprite from the bottom)
}

View file

@ -330,6 +330,7 @@ void A_MementosTPParticles(mobj_t *actor);
void A_FlameShieldPaper(mobj_t *actor);
void A_InvincSparkleRotate(mobj_t *actor);
void A_SpawnItemDebrisCloud(mobj_t *actor);
void A_RingShooterFace(mobj_t *actor);
//for p_enemy.c
@ -13810,3 +13811,15 @@ A_SpawnItemDebrisCloud (mobj_t *actor)
puff->momz += FixedMul(target->momz, fade);
}
}
// sets the actor's
// vars do nothing
void A_RingShooterFace(mobj_t *actor)
{
if (LUA_CallAction(A_RINGSHOOTERFACE, actor))
{
return;
}
Obj_UpdateRingShooterFace(actor);
}

View file

@ -580,6 +580,10 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
Obj_LoopEndpointCollide(special, toucher);
return;
case MT_RINGSHOOTER:
Obj_PlayerUsedRingShooter(special, player);
return;
default: // SOC or script pickup
P_SetTarget(&special->target, toucher);
break;

View file

@ -2232,7 +2232,8 @@ boolean P_CheckPosition(mobj_t *thing, fixed_t x, fixed_t y, TryMoveResult_t *re
// with MF_NOCLIP enabled, but they won't be blocked
// regardless of the result. This allows for SPBs and
// the UFO to collide.
return true;
// ...but be careful about removed obj! ~toast 140423
return !P_MobjWasRemoved(thing);
}
validcount++;

View file

@ -1878,7 +1878,7 @@ void P_XYMovement(mobj_t *mo)
FIXED_TO_FLOAT(AngleFixed(oldangle-newangle))
);
*/
}
else if (predictedz - mo->z > abs(slopemom.z / 2))
{
@ -6641,6 +6641,12 @@ static void P_MobjSceneryThink(mobj_t *mobj)
mobj->momz = newz - mobj->z;
}
break;
case MT_RINGSHOOTER:
if (Obj_RingShooterThinker(mobj) == false)
{
return;
}
break;
case MT_SPINDASHWIND:
case MT_DRIFTELECTRICSPARK:
mobj->renderflags ^= RF_DONTDRAW;
@ -10399,7 +10405,7 @@ mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type)
if (type == MT_NULL)
{
#if 0
#if 0
#ifdef PARANOIA
I_Error("Tried to spawn MT_NULL\n");
#endif
@ -11140,26 +11146,10 @@ void P_RemoveMobj(mobj_t *mobj)
iquetail = (iquetail+1)&(ITEMQUESIZE-1);
}
if (mobj->type == MT_KARMAHITBOX) // Remove linked list objects for certain types
{
mobj_t *cur = mobj->hnext;
while (cur && !P_MobjWasRemoved(cur))
{
mobj_t *prev = cur; // Kind of a dumb var, but we need to set cur before we remove the mobj
cur = cur->hnext;
P_RemoveMobj(prev);
}
}
if (mobj->type == MT_OVERLAY)
P_RemoveOverlay(mobj);
if (mobj->type == MT_SPB)
spbplace = -1;
if (P_IsTrackerType(mobj->type))
{
P_RemoveTracker(mobj);
}
if (mobj->player && mobj->player->followmobj)
{
@ -11167,19 +11157,56 @@ void P_RemoveMobj(mobj_t *mobj)
P_SetTarget(&mobj->player->followmobj, NULL);
}
if (mobj->type == MT_SHRINK_POHBEE)
// Remove linked list objects for certain types
switch (mobj->type)
{
Obj_PohbeeRemoved(mobj);
}
case MT_KARMAHITBOX:
{
mobj_t *cur = mobj->hnext;
if (mobj->type == MT_SHRINK_GUN)
{
Obj_ShrinkGunRemoved(mobj);
}
while (cur && !P_MobjWasRemoved(cur))
{
mobj_t *prev = cur; // Kind of a dumb var, but we need to set cur before we remove the mobj
cur = cur->hnext;
P_RemoveMobj(prev);
}
if (mobj->type == MT_SPECIAL_UFO_PIECE)
{
Obj_UFOPieceRemoved(mobj);
break;
}
case MT_OVERLAY:
{
P_RemoveOverlay(mobj);
break;
}
case MT_SPB:
{
spbplace = -1;
break;
}
case MT_SHRINK_POHBEE:
{
Obj_PohbeeRemoved(mobj);
break;
}
case MT_SHRINK_GUN:
{
Obj_ShrinkGunRemoved(mobj);
break;
}
case MT_SPECIAL_UFO_PIECE:
{
Obj_UFOPieceRemoved(mobj);
break;
}
case MT_RINGSHOOTER:
{
Obj_RingShooterDelete(mobj);
break;
}
default:
{
break;
}
}
mobj->health = 0; // Just because
@ -13143,8 +13170,8 @@ static boolean P_SetupSpawnedMapThing(mapthing_t *mthing, mobj_t *mobj, boolean
}
case MT_RANDOMITEM:
{
boolean delayed = !(gametyperules & GTR_CIRCUIT);
if (leveltime < (delayed ? starttime : 3))
const boolean delayed = !(gametyperules & GTR_CIRCUIT);
if (leveltime == 0)
{
mobj->flags2 |= MF2_BOSSNOTRAP; // mark as here on map start
if (delayed)

View file

@ -64,14 +64,15 @@ savedata_t savedata;
// than an UINT16
typedef enum
{
AWAYVIEW = 0x01,
FOLLOWITEM = 0x02,
FOLLOWER = 0x04,
SKYBOXVIEW = 0x08,
SKYBOXCENTER = 0x10,
HOVERHYUDORO = 0x20,
STUMBLE = 0x40,
SLIPTIDEZIP = 0x80
AWAYVIEW = 0x0001,
FOLLOWITEM = 0x0002,
FOLLOWER = 0x0004,
SKYBOXVIEW = 0x0008,
SKYBOXCENTER = 0x0010,
HOVERHYUDORO = 0x0020,
STUMBLE = 0x0040,
SLIPTIDEZIP = 0x0080,
RINGSHOOTER = 0x0100
} player_saveflags;
static inline void P_ArchivePlayer(savebuffer_t *save)
@ -218,6 +219,9 @@ static void P_NetArchivePlayers(savebuffer_t *save)
if (players[i].sliptideZipIndicator)
flags |= SLIPTIDEZIP;
if (players[i].ringShooter)
flags |= RINGSHOOTER;
WRITEUINT16(save->p, flags);
if (flags & SKYBOXVIEW)
@ -241,6 +245,9 @@ static void P_NetArchivePlayers(savebuffer_t *save)
if (flags & SLIPTIDEZIP)
WRITEUINT32(save->p, players[i].sliptideZipIndicator->mobjnum);
if (flags & RINGSHOOTER)
WRITEUINT32(save->p, players[i].ringShooter->mobjnum);
WRITEUINT32(save->p, (UINT32)players[i].followitem);
WRITEUINT32(save->p, players[i].charflags);
@ -614,6 +621,9 @@ static void P_NetUnArchivePlayers(savebuffer_t *save)
if (flags & SLIPTIDEZIP)
players[i].sliptideZipIndicator = (mobj_t *)(size_t)READUINT32(save->p);
if (flags & RINGSHOOTER)
players[i].ringShooter = (mobj_t *)(size_t)READUINT32(save->p);
players[i].followitem = (mobjtype_t)READUINT32(save->p);
//SetPlayerSkinByNum(i, players[i].skin);
@ -4380,6 +4390,8 @@ static void P_NetUnArchiveThinkers(savebuffer_t *save)
{
next = currentthinker->next;
currentthinker->references = 0; // Heinous but this is the only place the assertion in P_UnlinkThinkers is wrong
if (currentthinker->function.acp1 == (actionf_p1)P_MobjThinker || currentthinker->function.acp1 == (actionf_p1)P_NullPrecipThinker)
P_RemoveSavegameMobj((mobj_t *)currentthinker); // item isn't saved, don't remove it
else
@ -4864,6 +4876,13 @@ static void P_RelinkPointers(void)
if (!P_SetTarget(&players[i].sliptideZipIndicator, P_FindNewPosition(temp)))
CONS_Debug(DBG_GAMELOGIC, "sliptideZipIndicator not found on player %d\n", i);
}
if (players[i].ringShooter)
{
temp = (UINT32)(size_t)players[i].ringShooter;
players[i].ringShooter = NULL;
if (!P_SetTarget(&players[i].ringShooter, P_FindNewPosition(temp)))
CONS_Debug(DBG_GAMELOGIC, "ringShooter not found on player %d\n", i);
}
}
}

View file

@ -782,7 +782,8 @@ void P_Ticker(boolean run)
low = 65536 / (3+player->numsneakers);
high = 65536 / (3+player->numsneakers);
}
else if (player->boostpower < FRACUNIT && P_IsObjectOnGround(player->mo))
else if (((player->boostpower < FRACUNIT) || (player->stairjank > 8))
&& P_IsObjectOnGround(player->mo))
{
low = 65536 / 32;
high = 65536 / 32;

View file

@ -62,6 +62,7 @@
#include "k_rank.h"
#include "k_director.h"
#include "g_party.h"
#include "k_profiles.h"
#ifdef HW3SOUND
#include "hardware/hw3sound.h"
@ -1378,6 +1379,13 @@ void P_DoPlayerExit(player_t *player)
if (modeattacking)
G_UpdateRecords();
profile_t *pr = PR_GetPlayerProfile(player);
if (pr != NULL && !losing)
{
pr->wins++;
PR_SaveProfiles();
}
player->karthud[khud_cardanimation] = 0; // srb2kart: reset battle animation
if (player == &players[consoleplayer])
@ -2235,16 +2243,30 @@ static void P_UpdatePlayerAngle(player_t *player)
angle_t leniency = (4*ANG1/3) * min(player->cmd.latency, 6);
// Don't force another turning tic, just give them the desired angle!
if (targetDelta == angleChange || K_Sliptiding(player) || (maxTurnRight == 0 && maxTurnLeft == 0))
if (targetDelta == angleChange || (maxTurnRight == 0 && maxTurnLeft == 0))
{
// Either we're dead on, we can't steer, or we're in a special handling state.
// Stuff like sliptiding requires some blind-faith steering:
// if a camera correction stops our turn input, the sliptide randomly fails!
// Either we're dead on or we can't steer at all.
player->steering = targetsteering;
}
else
{
// We're off. Try to legally steer the player towards their camera.
if (K_Sliptiding(player) && P_IsObjectOnGround(player->mo) && (player->cmd.turning != 0) && ((player->cmd.turning > 0) == (player->aizdriftstrat > 0)))
{
// Don't change handling direction if someone's inputs are sliptiding, you'll break the sliptide!
if (player->cmd.turning > 0)
{
steeringLeft = max(steeringLeft, 1);
steeringRight = max(steeringRight, steeringLeft);
}
else
{
steeringRight = min(steeringRight, -1);
steeringLeft = min(steeringLeft, steeringRight);
}
}
player->steering = P_FindClosestTurningForAngle(player, targetDelta, steeringLeft, steeringRight);
angleChange = K_GetKartTurnValue(player, player->steering) << TICCMD_REDUCE;

View file

@ -61,6 +61,8 @@ patch_t *Patch_Create(softwarepatch_t *source, size_t srcsize, void *dest)
return patch;
}
static boolean g_patch_was_freed_this_frame = false;
//
// Frees a patch from memory.
//
@ -97,6 +99,8 @@ static void Patch_FreeData(patch_t *patch)
Z_Free(patch->columnofs);
Z_Free(patch->columns);
g_patch_was_freed_this_frame = true;
}
void Patch_Free(patch_t *patch)
@ -108,6 +112,16 @@ void Patch_Free(patch_t *patch)
Z_Free(patch);
}
boolean Patch_WasFreedThisFrame(void)
{
return g_patch_was_freed_this_frame;
}
void Patch_ResetFreedThisFrame(void)
{
g_patch_was_freed_this_frame = false;
}
//
// Frees patches with a tag range.
//

View file

@ -24,6 +24,8 @@ extern "C" {
// Patch functions
patch_t *Patch_Create(softwarepatch_t *source, size_t srcsize, void *dest);
void Patch_Free(patch_t *patch);
boolean Patch_WasFreedThisFrame(void);
void Patch_ResetFreedThisFrame(void);
#define Patch_FreeTag(tagnum) Patch_FreeTags(tagnum, tagnum)
void Patch_FreeTags(INT32 lowtag, INT32 hightag);

View file

@ -1759,6 +1759,14 @@ Rect GlCoreRhi::get_renderbuffer_size(Handle<Renderbuffer> renderbuffer)
return ret;
}
uint32_t GlCoreRhi::get_buffer_size(Handle<Buffer> buffer)
{
SRB2_ASSERT(buffer_slab_.is_valid(buffer));
auto& buf = buffer_slab_[buffer];
return buf.desc.size;
}
void GlCoreRhi::finish()
{
SRB2_ASSERT(graphics_context_active_ == false);

View file

@ -184,6 +184,7 @@ public:
virtual TextureDetails get_texture_details(Handle<Texture> texture) override;
virtual Rect get_renderbuffer_size(Handle<Renderbuffer> renderbuffer) override;
virtual uint32_t get_buffer_size(Handle<Buffer> buffer) override;
virtual Handle<TransferContext> begin_transfer() override;
virtual void end_transfer(Handle<TransferContext> handle) override;

View file

@ -83,3 +83,28 @@ const ProgramRequirements& rhi::program_requirements_for_program(PipelineProgram
std::terminate();
}
}
bool rhi::recreate_buffer_to_size(Rhi& rhi, Handle<Buffer>& buffer, const BufferDesc& desc)
{
bool recreate = false;
if (buffer == kNullHandle)
{
recreate = true;
}
else
{
std::size_t existing_size = rhi.get_buffer_size(buffer);
if (existing_size < desc.size)
{
rhi.destroy_buffer(buffer);
recreate = true;
}
}
if (recreate)
{
buffer = rhi.create_buffer(desc);
}
return recreate;
}

View file

@ -608,6 +608,7 @@ struct Rhi
virtual TextureDetails get_texture_details(Handle<Texture> texture) = 0;
virtual Rect get_renderbuffer_size(Handle<Renderbuffer> renderbuffer) = 0;
virtual uint32_t get_buffer_size(Handle<Buffer> buffer) = 0;
virtual Handle<TransferContext> begin_transfer() = 0;
virtual void end_transfer(Handle<TransferContext> handle) = 0;
@ -653,6 +654,17 @@ struct Rhi
virtual void finish() = 0;
};
// Utility functions
/// @brief If the buffer for the given handle is too small or does not exist, creates a new buffer with the given
/// parameters.
/// @param buffer the existing valid buffer handle or kNullHandle, replaced if recreated
/// @param type
/// @param usage
/// @param size the target size of the new buffer
/// @return true if the buffer was recreated, false otherwise
bool recreate_buffer_to_size(Rhi& rhi, Handle<Buffer>& buffer, const BufferDesc& desc);
} // namespace srb2::rhi
#endif // __SRB2_RHI_RHI_HPP__

View file

@ -268,6 +268,96 @@ static void write_backtrace(INT32 signal)
#undef CRASHLOG_STDERR_WRITE
#endif // UNIXBACKTRACE
static void I_ShowErrorMessageBox(const char *messagefordevelopers, boolean dumpmade)
{
static char finalmessage[2048];
size_t firstimpressionsline = 3; // "Dr Robotnik's Ring Racers" has encountered...
if (M_CheckParm("-dedicated"))
return;
snprintf(
finalmessage,
sizeof(finalmessage),
"Hee Ho!\n"
"\n"
"\"Dr. Robotnik's Ring Racers\" has encountered an unrecoverable error and needs to close.\n"
"This is (usually) not your fault, but we encourage you to report it in the community. This should be done alongside your "
"%s"
"log file (%s).\n"
"\n"
"The following information is for a programmer (please be nice to them!) but\n"
"may also be useful for server hosts and add-on creators.\n"
"\n"
"%s",
dumpmade ?
#if defined (UNIXBACKTRACE)
"crash-log.txt"
#elif defined (_WIN32)
".rpt crash dump"
#endif
" (very important!) and " : "",
#ifdef LOGMESSAGES
logfilename[0] ? logfilename :
#endif
"uh oh, one wasn't made!?",
messagefordevelopers);
// Rudementary word wrapping.
// Simple and effective. Does not handle nonuniform letter sizes, etc. but who cares.
{
size_t max = 0, maxatstart = 0, start = 0, width = 0, i;
for (i = 0; finalmessage[i]; i++)
{
if (finalmessage[i] == ' ')
{
start = i;
max += 4;
maxatstart = max;
}
else if (finalmessage[i] == '\n')
{
if (firstimpressionsline > 0)
{
firstimpressionsline--;
if (firstimpressionsline == 0)
{
width = max;
}
}
start = 0;
max = 0;
maxatstart = 0;
continue;
}
else
max += 8;
// Start trying to wrap if presumed length exceeds the space we want.
if (width > 0 && max >= width && start > 0)
{
finalmessage[start] = '\n';
max -= maxatstart;
start = 0;
}
}
}
// Implement message box with SDL_ShowSimpleMessageBox,
// which should fail gracefully if it can't put a message box up
// on the target system
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR,
"Dr. Robotnik's Ring Racers "VERSIONSTRING" Error",
finalmessage, NULL);
// Note that SDL_ShowSimpleMessageBox does *not* require SDL to be
// initialized at the time, so calling it after SDL_Quit() is
// perfectly okay! In addition, we do this on purpose so the
// fullscreen window is closed before displaying the error message
// in case the fullscreen window blocks it for some absurd reason.
}
static void I_ReportSignal(int num, int coredumped)
{
//static char msg[] = "oh no! back to reality!\r\n";
@ -317,10 +407,15 @@ static void I_ReportSignal(int num, int coredumped)
I_OutputMsg("\nProcess killed by signal: %s\n\n", sigmsg);
if (!M_CheckParm("-dedicated"))
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR,
"Process killed by signal",
sigmsg, NULL);
I_ShowErrorMessageBox(sigmsg,
#if defined (UNIXBACKTRACE)
true
#elif defined (_WIN32)
!M_CheckParm("-noexchndl")
#else
false
#endif
);
}
#ifndef NEWSIGNALHANDLER
@ -1712,13 +1807,7 @@ void I_Error(const char *error, ...)
I_ShutdownGraphics();
I_ShutdownInput();
// Implement message box with SDL_ShowSimpleMessageBox,
// which should fail gracefully if it can't put a message box up
// on the target system
if (!M_CheckParm("-dedicated"))
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR,
"Dr. Robotnik's Ring Racers "VERSIONSTRING" Error",
buffer, NULL);
I_ShowErrorMessageBox(buffer, false);
// We wait until now to do this so the funny sound can be heard
I_ShutdownSound();
@ -1726,12 +1815,6 @@ void I_Error(const char *error, ...)
I_ShutdownSystem();
SDL_Quit();
// Note that SDL_ShowSimpleMessageBox does *not* require SDL to be
// initialized at the time, so calling it after SDL_Quit() is
// perfectly okay! In addition, we do this on purpose so the
// fullscreen window is closed before displaying the error message
// in case the fullscreen window blocks it for some absurd reason.
W_Shutdown();
#if defined (PARANOIA) || defined (DEVELOP)

View file

@ -571,6 +571,7 @@ static void Impl_HandleKeyboardEvent(SDL_KeyboardEvent evt, Uint32 type)
return;
}
event.data1 = Impl_SDL_Scancode_To_Keycode(evt.keysym.scancode);
event.data2 = evt.repeat;
if (event.data1) D_PostEvent(&event);
}
@ -746,6 +747,7 @@ static void Impl_HandleControllerButtonEvent(SDL_ControllerButtonEvent evt, Uint
}
event.data1 = KEY_JOY1;
event.data2 = 0;
if (type == SDL_CONTROLLERBUTTONUP)
{

View file

@ -196,6 +196,9 @@ TYPEDEF (pathfindsetup_t);
// k_profiles.h
TYPEDEF (profile_t);
// h_serverstats.h
TYPEDEF (serverplayer_t);
// k_terrain.h
TYPEDEF (t_splash_t);
TYPEDEF (t_footstep_t);