Unlockable skins (in a way friendly to #define MAXSKINS 255)

Mammoth commit, sorry. I only realised halfway through writing it that SECRET_SKIN was only partially merged.

Ports from 2.2:
- Merge SECRET_SKIN (STJr/SRB2!1474)
    - Default skin is now handled by checking all skins for unlock status, and I_Erroring if none are available
    - Don't show skin names on game startup, to keep our secrets hidden
    - Unlockables now have string variables zallocated.
         - For skin names rather than numbers.
    - Correctly clean up memory when freeing unlockables and emblems.

Bespoke code:
- For temporary testing. `unlocks.pk3`
    - Using this for rapid testing gameboot SOC instead of patch.pk3 because of the intent to turn that into scripts.pk3
- Don't not save gamedata in DEVELOP builds, even if you've used cheats!
- `player->availabilities` is now an array of UINT8
    - (MAXSKINS + 7)/8 entries, or 32 bytes.
    - Included with XD_ADDPLAYER instead of XD_NAMEANDCOLOR.
         - Simplifies a lot of logic with respect to demos, skin changes mid-game, etc.
             - Seriously, there's a lot of random places in the code that just iterate over MAXSPLITSCREENPLAYERS and g_localplayers to update availabilities in real time in a way that's not particularly netsafe...
         - Lines up with the plan for handling unlocks when returning to menus.
         - Was included with XD_ADDBOT, but that actually overruns the netxcmd buffer at first mapload with 7 bots. We might need to consider expanding the size of the netxcmd buffer...
    - In demos, can be interpreted as both relative to the original replay and the current skin list depending on boolean context provided to R_SkinUsable.
    - Used for SF_IRONMAN (and will crash if all other skins are inaccessible).
- Grand Prix bot randomisation uses the host's unlocks.
- Don't show locked characters on the fancy new character select.
-  DXD_JOINDATA for demos
    - Replaces the dual-purpose behaviour of DXD_PLAYSTATE
    - Contains availabilities
    - Handles bot material in a different way
- Forceskin restrictions
    - Won't run in demos, because it's assumed recorded DXD_SKIN will handle all the conversions the original match had
    - Won't run if K_CanChangeRules says no
- Correctly set `mapvisited` on level visit, even in [fake gasp] MULTIPLAYER/NETGAMES!! 🥹
- Added updating unlockables and extra emblems on `mapvisited` update.
    - Currently fails to produce the cecho, but that'll be stripped out entirely in a future commit so I'm not bothered.
This commit is contained in:
toaster 2022-11-27 22:53:29 +00:00
parent 8179b39773
commit 6d0637d39d
24 changed files with 377 additions and 198 deletions

View file

@ -1481,7 +1481,7 @@ boolean CV_CompleteValue(consvar_t *var, const char **valstrp, INT32 *intval)
{
v = R_SkinAvailable(valstr);
if (!R_SkinUsable(-1, v))
if (!R_SkinUsable(-1, v, false))
v = -1;
goto finish;
@ -1984,7 +1984,7 @@ static void CV_SetCVar(consvar_t *var, const char *value, boolean stealth)
if (var == &cv_forceskin)
{
INT32 skin = R_SkinAvailable(value);
if ((stricmp(value, "None")) && ((skin == -1) || !R_SkinUsable(-1, skin)))
if ((stricmp(value, "None")) && ((skin == -1) || !R_SkinUsable(-1, skin, false)))
{
CONS_Printf("Please provide a valid skin name (\"None\" disables).\n");
return;
@ -2109,7 +2109,7 @@ void CV_AddValue(consvar_t *var, INT32 increment)
else if (newvalue >= numskins)
newvalue = -1;
} while ((oldvalue != newvalue)
&& !(R_SkinUsable(-1, newvalue)));
&& !(R_SkinUsable(-1, newvalue, false)));
}
else
newvalue = var->value + increment;

View file

@ -824,6 +824,8 @@ static boolean CL_SendJoin(void)
for (; i < MAXSPLITSCREENPLAYERS; i++)
strncpy(netbuffer->u.clientcfg.names[i], va("Player %c", 'A' + i), MAXPLAYERNAME);
memcpy(&netbuffer->u.clientcfg.availabilities, R_GetSkinAvailabilities(false), MAXAVAILABILITY*sizeof(UINT8));
return HSendPacket(servernode, false, 0, sizeof (clientconfig_pak));
}
@ -3565,8 +3567,8 @@ static void Got_AddPlayer(UINT8 **p, INT32 playernum)
return;
}
node = (UINT8)READUINT8(*p);
newplayernum = (UINT8)READUINT8(*p);
node = READUINT8(*p);
newplayernum = READUINT8(*p);
CONS_Debug(DBG_NETPLAY, "addplayer: %d %d\n", node, newplayernum);
@ -3587,8 +3589,13 @@ static void Got_AddPlayer(UINT8 **p, INT32 playernum)
READSTRINGN(*p, player_names[newplayernum], MAXPLAYERNAME);
console = (UINT8)READUINT8(*p);
splitscreenplayer = (UINT8)READUINT8(*p);
console = READUINT8(*p);
splitscreenplayer = READUINT8(*p);
for (i = 0; i < MAXAVAILABILITY; i++)
{
newplayer->availabilities[i] = READUINT8(*p);
}
// the server is creating my player
if (node == mynode)
@ -3690,8 +3697,9 @@ static void Got_RemovePlayer(UINT8 **p, INT32 playernum)
static void Got_AddBot(UINT8 **p, INT32 playernum)
{
INT16 newplayernum;
UINT8 skinnum = 0;
UINT8 i, skinnum = 0;
UINT8 difficulty = DIFFICULTBOT;
UINT8 availabilitiesbuffer[MAXAVAILABILITY];
if (playernum != serverplayer && !IsPlayerAdmin(playernum))
{
@ -3704,9 +3712,9 @@ static void Got_AddBot(UINT8 **p, INT32 playernum)
return;
}
newplayernum = (UINT8)READUINT8(*p);
skinnum = (UINT8)READUINT8(*p);
difficulty = (UINT8)READUINT8(*p);
newplayernum = READUINT8(*p);
skinnum = READUINT8(*p);
difficulty = READUINT8(*p);
CONS_Debug(DBG_NETPLAY, "addbot: %d\n", newplayernum);
@ -3720,6 +3728,15 @@ static void Got_AddBot(UINT8 **p, INT32 playernum)
playernode[newplayernum] = servernode;
// todo find a way to have all auto unlocked for dedicated
if (playeringame[0])
{
for (i = 0; i < MAXAVAILABILITY; i++)
{
players[newplayernum].availabilities[i] = players[0].availabilities[i];
}
}
players[newplayernum].splitscreenindex = 0;
players[newplayernum].bot = true;
players[newplayernum].botvars.difficulty = difficulty;
@ -3737,10 +3754,10 @@ static void Got_AddBot(UINT8 **p, INT32 playernum)
LUA_HookInt(newplayernum, HOOK(PlayerJoin));
}
static boolean SV_AddWaitingPlayers(SINT8 node, const char *name, const char *name2, const char *name3, const char *name4)
static boolean SV_AddWaitingPlayers(SINT8 node, UINT8 *availabilities, const char *name, const char *name2, const char *name3, const char *name4)
{
INT32 n, newplayernum;
UINT8 buf[4 + MAXPLAYERNAME];
INT32 n, newplayernum, i;
UINT8 buf[4 + MAXPLAYERNAME + MAXAVAILABILITY];
UINT8 *buf_p = buf;
boolean newplayer = false;
@ -3823,6 +3840,11 @@ static boolean SV_AddWaitingPlayers(SINT8 node, const char *name, const char *na
WRITEUINT8(buf_p, nodetoplayer[node]); // consoleplayer
WRITEUINT8(buf_p, playerpernode[node]); // splitscreen num
for (i = 0; i < MAXAVAILABILITY; i++)
{
WRITEUINT8(buf_p, availabilities[i]);
}
playerpernode[node]++;
SendNetXCmd(XD_ADDPLAYER, buf, buf_p - buf);
@ -3886,9 +3908,10 @@ boolean SV_SpawnServer(void)
// strictly speaking, i'm not convinced the following is necessary
// but I'm not confident enough to remove it entirely in case it breaks something
{
UINT8 *availabilitiesbuffer = R_GetSkinAvailabilities(false);
SINT8 node = 0;
for (; node < MAXNETNODES; node++)
result |= SV_AddWaitingPlayers(node, cv_playername[0].zstring, cv_playername[1].zstring, cv_playername[2].zstring, cv_playername[3].zstring);
result |= SV_AddWaitingPlayers(node, availabilitiesbuffer, cv_playername[0].zstring, cv_playername[1].zstring, cv_playername[2].zstring, cv_playername[3].zstring);
}
return result;
#endif
@ -3971,6 +3994,7 @@ static void HandleConnect(SINT8 node)
{
char names[MAXSPLITSCREENPLAYERS][MAXPLAYERNAME + 1];
INT32 i;
UINT8 availabilitiesbuffer[MAXAVAILABILITY];
// Sal: Dedicated mode is INCREDIBLY hacked together.
// If a server filled out, then it'd overwrite the host and turn everyone into weird husks.....
@ -4077,6 +4101,8 @@ static void HandleConnect(SINT8 node)
}
}
memcpy(availabilitiesbuffer, netbuffer->u.clientcfg.availabilities, sizeof(availabilitiesbuffer));
// client authorised to join
nodewaiting[node] = (UINT8)(netbuffer->u.clientcfg.localplayers - playerpernode[node]);
if (!nodeingame[node])
@ -4111,7 +4137,8 @@ static void HandleConnect(SINT8 node)
SV_SendSaveGame(node, false); // send a complete game state
DEBFILE("send savegame\n");
}
SV_AddWaitingPlayers(node, names[0], names[1], names[2], names[3]);
SV_AddWaitingPlayers(node, availabilitiesbuffer, names[0], names[1], names[2], names[3]);
joindelay += cv_joindelay.value * TICRATE;
player_joining = true;
}

View file

@ -246,6 +246,7 @@ typedef struct
UINT8 localplayers; // number of splitscreen players
UINT8 mode;
char names[MAXSPLITSCREENPLAYERS][MAXPLAYERNAME];
UINT8 availabilities[MAXAVAILABILITY];
} ATTRPACK clientconfig_pak;
#define SV_SPEEDMASK 0x03 // used to send kartspeed

View file

@ -950,11 +950,6 @@ void D_StartTitle(void)
for (i = 0; i < MAXPLAYERS; i++)
CL_ClearPlayer(i);
for (i = 0; i < MAXSPLITSCREENPLAYERS; i++)
{
players[g_localplayers[i]].availabilities = R_GetSkinAvailabilities();
}
splitscreen = 0;
SplitScreen_OnChange();
@ -1148,6 +1143,10 @@ static void IdentifyVersion(void)
#ifdef USE_PATCH_FILE
D_AddFile(startupiwads, va(pandf,srb2waddir,PATCHNAME));
#endif
#define UNLOCKTESTING
#if defined(DEVELOP) && defined(UNLOCKTESTING)
D_AddFile(startupiwads, va(pandf,srb2waddir,"unlocks.pk3"));
#endif
////
#undef TEXTURESNAME
#undef MAPSNAME
@ -1449,6 +1448,9 @@ void D_SRB2Main(void)
#ifdef USE_PATCH_FILE
mainwads++; // patch.pk3
#endif
#ifdef UNLOCKTESTING
mainwads++; // unlocks.pk3
#endif
#endif //ifndef DEVELOP

View file

@ -1408,6 +1408,10 @@ boolean CanChangeSkinWhilePlaying(INT32 playernum)
static void ForceAllSkins(INT32 forcedskin)
{
INT32 i;
if (demo.playback)
return; // DXD_SKIN should handle all changes for us
for (i = 0; i < MAXPLAYERS; ++i)
{
if (!playeringame[i])
@ -1476,6 +1480,8 @@ static void SendNameAndColor(UINT8 n)
char buf[MAXPLAYERNAME+12];
char *p;
UINT16 i = 0;
if (splitscreen < n)
return; // can happen if skin4/color4/name4 changed
@ -1493,9 +1499,10 @@ static void SendNameAndColor(UINT8 n)
CV_StealthSetValue(&cv_playercolor[n], skins[player->skin].prefcolor);
else if (skincolors[atoi(cv_playercolor[n].defaultvalue)].accessible)
CV_StealthSet(&cv_playercolor[n], cv_playercolor[n].defaultvalue);
else {
UINT16 i = 0;
while (i<numskincolors && !skincolors[i].accessible) i++;
else
{
while (i < numskincolors && !skincolors[i].accessible)
i++;
CV_StealthSetValue(&cv_playercolor[n], (i != numskincolors) ? i : SKINCOLOR_BLUE);
}
}
@ -1512,8 +1519,6 @@ static void SendNameAndColor(UINT8 n)
&& cv_followercolor[n].value == player->followercolor)
return;
player->availabilities = R_GetSkinAvailabilities();
// We'll handle it later if we're not playing.
if (!Playing())
return;
@ -1530,7 +1535,7 @@ static void SendNameAndColor(UINT8 n)
K_KartResetPlayerColor(player);
if ((foundskin = R_SkinAvailable(cv_skin[n].string)) != -1 && R_SkinUsable(playernum, foundskin))
if ((foundskin = R_SkinAvailable(cv_skin[n].string)) != -1 && R_SkinUsable(playernum, foundskin, false))
{
SetPlayerSkin(playernum, cv_skin[n].string);
CV_StealthSet(&cv_skin[n], skins[foundskin].name);
@ -1575,7 +1580,7 @@ static void SendNameAndColor(UINT8 n)
// check if player has the skin loaded (cv_skin may have
// the name of a skin that was available in the previous game)
cv_skin[n].value = R_SkinAvailable(cv_skin[n].string);
if ((cv_skin[n].value < 0) || !R_SkinUsable(playernum, cv_skin[n].value))
if ((cv_skin[n].value < 0) || !R_SkinUsable(playernum, cv_skin[n].value, false))
{
CV_StealthSet(&cv_skin[n], DEFAULTSKIN);
cv_skin[n].value = 0;
@ -1590,7 +1595,6 @@ static void SendNameAndColor(UINT8 n)
// Finally write out the complete packet and send it off.
WRITESTRINGN(p, cv_playername[n].zstring, MAXPLAYERNAME);
WRITEUINT32(p, (UINT32)player->availabilities);
WRITEUINT16(p, (UINT16)cv_playercolor[n].value);
WRITEUINT8(p, (UINT8)cv_skin[n].value);
WRITEINT16(p, (INT16)cv_follower[n].value);
@ -1632,11 +1636,9 @@ static void Got_NameAndColor(UINT8 **cp, INT32 playernum)
}
READSTRINGN(*cp, name, MAXPLAYERNAME);
p->availabilities = READUINT32(*cp);
color = READUINT16(*cp);
skin = READUINT8(*cp);
follower = READINT16(*cp);
//CONS_Printf("Recieved follower id %d\n", follower);
followercolor = READUINT16(*cp);
// set name
@ -1657,56 +1659,20 @@ static void Got_NameAndColor(UINT8 **cp, INT32 playernum)
{
boolean kick = false;
// team colors
if (G_GametypeHasTeams())
{
if (p->ctfteam == 1 && p->skincolor != skincolor_redteam)
kick = true;
else if (p->ctfteam == 2 && p->skincolor != skincolor_blueteam)
kick = true;
}
// don't allow inaccessible colors
if (skincolors[p->skincolor].accessible == false)
kick = true;
// availabilities
for (i = 0; i < MAXSKINS; i++)
{
UINT32 playerhasunlocked = (p->availabilities & (1 << i));
boolean islocked = false;
UINT8 j;
for (j = 0; j < MAXUNLOCKABLES; j++)
{
if (unlockables[j].type != SECRET_SKIN)
continue;
if (unlockables[j].variable == i)
{
islocked = true;
break;
}
}
if (islocked == false && playerhasunlocked == true)
{
// hacked client that enabled every bit
kick = true;
break;
}
}
if (kick)
{
CONS_Alert(CONS_WARNING, M_GetText("Illegal color change received from %s (team: %d), color: %d)\n"), player_names[playernum], p->ctfteam, p->skincolor);
CONS_Alert(CONS_WARNING, M_GetText("Illegal color change received from %s, color: %d)\n"), player_names[playernum], p->skincolor);
SendKick(playernum, KICK_MSG_CON_FAIL);
return;
}
}
// set skin
if (cv_forceskin.value >= 0 && (netgame || multiplayer)) // Server wants everyone to use the same player
if (cv_forceskin.value >= 0 && K_CanChangeRules(true)) // Server wants everyone to use the same player
{
const INT32 forcedskin = cv_forceskin.value;
SetPlayerSkinByNum(playernum, forcedskin);
@ -5729,11 +5695,6 @@ void Command_ExitGame_f(void)
for (i = 0; i < MAXPLAYERS; i++)
CL_ClearPlayer(i);
for (i = 0; i < MAXSPLITSCREENPLAYERS; i++)
{
players[g_localplayers[i]].availabilities = R_GetSkinAvailabilities();
}
splitscreen = 0;
SplitScreen_OnChange();

View file

@ -385,7 +385,7 @@ typedef struct player_s
UINT16 skincolor;
INT32 skin;
UINT32 availabilities;
UINT8 availabilities[MAXAVAILABILITY];
UINT8 fakeskin; // ironman
UINT8 lastfakeskin;

View file

@ -131,6 +131,36 @@ static float searchfvalue(const char *s)
#endif
// These are for clearing all of various things
void clear_emblems(void)
{
INT32 i;
for (i = 0; i < MAXEMBLEMS; ++i)
{
Z_Free(emblemlocations[i].level);
emblemlocations[i].level = NULL;
Z_Free(emblemlocations[i].stringVar);
emblemlocations[i].stringVar = NULL;
}
memset(&emblemlocations, 0, sizeof(emblemlocations));
numemblems = 0;
}
void clear_unlockables(void)
{
INT32 i;
for (i = 0; i < MAXUNLOCKABLES; ++i)
{
Z_Free(unlockables[i].stringVar);
unlockables[i].stringVar = NULL;
}
memset(&unlockables, 0, sizeof(unlockables));
}
void clear_conditionsets(void)
{
UINT8 i;
@ -2140,7 +2170,12 @@ void reademblemdata(MYFILE *f, INT32 num)
else if (fastcmp(word, "COLOR"))
emblemlocations[num-1].color = get_number(word2);
else if (fastcmp(word, "VAR"))
{
Z_Free(emblemlocations[num-1].stringVar);
emblemlocations[num-1].stringVar = Z_StrDup(word2);
emblemlocations[num-1].var = get_number(word2);
}
else
deh_warning("Emblem %d: unknown word '%s'", num, word);
}
@ -2344,7 +2379,8 @@ void readunlockable(MYFILE *f, INT32 num)
}
else if (fastcmp(word, "VAR"))
{
// TODO: different field for level name string
Z_Free(unlockables[num].stringVar);
unlockables[num].stringVar = Z_StrDup(word2);
unlockables[num].variable = (INT16)i;
}
else

View file

@ -77,6 +77,8 @@ void readlight(MYFILE *f, INT32 num);
void readskincolor(MYFILE *f, INT32 num);
void readthing(MYFILE *f, INT32 num);
void readfreeslots(MYFILE *f);
void clear_emblems(void);
void clear_unlockables(void);
void clear_levels(void);
void clear_conditionsets(void);

View file

@ -584,12 +584,13 @@ static void DEH_LoadDehackedFile(MYFILE *f, boolean mainfile)
}
if (clearall || fastcmp(word2, "UNLOCKABLES"))
memset(&unlockables, 0, sizeof(unlockables));
{
clear_unlockables();
}
if (clearall || fastcmp(word2, "EMBLEMS"))
{
memset(&emblemlocations, 0, sizeof(emblemlocations));
numemblems = 0;
clear_emblems();
}
if (clearall || fastcmp(word2, "EXTRAEMBLEMS"))

View file

@ -201,7 +201,8 @@ extern char logfilename[1024];
#define MAXGAMEPADS (MAXSPLITSCREENPLAYERS * 2) // Number of gamepads we'll be allowing
#define MAXSKINS UINT8_MAX
#define SKINNAMESIZE 16 // Moved from r_skins.h as including that particular header causes issues later down the line.
#define SKINNAMESIZE 16
#define MAXAVAILABILITY ((MAXSKINS + 7)/8)
#define COLORRAMPSIZE 16
#define MAXCOLORNAME 32

View file

@ -276,10 +276,8 @@ void G_ReadDemoExtraData(void)
{
extradata = READUINT8(demo_p);
if (extradata & DXD_PLAYSTATE)
if (extradata & DXD_JOINDATA)
{
i = READUINT8(demo_p);
if (!playeringame[p])
{
CL_ClearPlayer(p);
@ -288,14 +286,22 @@ void G_ReadDemoExtraData(void)
players[p].spectator = true;
}
if ((players[p].bot = !!(i & DXD_PST_ISBOT)))
for (i = 0; i < MAXAVAILABILITY; i++)
{
players[p].availabilities[i] = READUINT8(demo_p);
}
players[p].bot = !!(READUINT8(demo_p));
if (players[p].bot)
{
players[p].botvars.difficulty = READUINT8(demo_p);
players[p].botvars.diffincrease = READUINT8(demo_p); // needed to avoid having to duplicate logic
players[p].botvars.rival = (boolean)READUINT8(demo_p);
i &= ~DXD_PST_ISBOT;
}
}
if (extradata & DXD_PLAYSTATE)
{
i = READUINT8(demo_p);
switch (i) {
case DXD_PST_PLAYING:
@ -334,14 +340,6 @@ void G_ReadDemoExtraData(void)
K_CheckBumpers();
P_CheckRacers();
}
if (extradata & DXD_RESPAWN)
{
if (players[p].mo)
{
// Is this how this should work..?
K_DoIngameRespawn(&players[p]);
}
}
if (extradata & DXD_SKIN)
{
UINT8 skinid;
@ -397,6 +395,14 @@ void G_ReadDemoExtraData(void)
}
}
}
if (extradata & DXD_RESPAWN)
{
if (players[p].mo)
{
// Is this how this should work..?
K_DoIngameRespawn(&players[p]);
}
}
if (extradata & DXD_WEAPONPREF)
{
WeaponPref_Parse(&demo_p, p);
@ -454,6 +460,21 @@ void G_WriteDemoExtraData(void)
WRITEUINT8(demo_p, i);
WRITEUINT8(demo_p, demo_extradata[i]);
if (demo_extradata[i] & DXD_JOINDATA)
{
for (j = 0; j < MAXAVAILABILITY; j++)
{
WRITEUINT8(demo_p, players[i].availabilities[i]);
}
WRITEUINT8(demo_p, (UINT8)players[i].bot);
if (players[i].bot)
{
WRITEUINT8(demo_p, players[i].botvars.difficulty);
WRITEUINT8(demo_p, players[i].botvars.diffincrease); // needed to avoid having to duplicate logic
WRITEUINT8(demo_p, (UINT8)players[i].botvars.rival);
}
}
if (demo_extradata[i] & DXD_PLAYSTATE)
{
UINT8 pst = DXD_PST_PLAYING;
@ -472,19 +493,7 @@ void G_WriteDemoExtraData(void)
pst = DXD_PST_SPECTATING;
}
if (players[i].bot)
{
pst |= DXD_PST_ISBOT;
}
WRITEUINT8(demo_p, pst);
if (pst & DXD_PST_ISBOT)
{
WRITEUINT8(demo_p, players[i].botvars.difficulty);
WRITEUINT8(demo_p, players[i].botvars.diffincrease); // needed to avoid having to duplicate logic
WRITEUINT8(demo_p, (UINT8)players[i].botvars.rival);
}
}
//if (demo_extradata[i] & DXD_RESPAWN) has no extra data
if (demo_extradata[i] & DXD_SKIN)
@ -1218,8 +1227,14 @@ void G_GhostTicker(void)
if (ziptic == 0) // Only support player 0 info for now
{
ziptic = READUINT8(g->p);
if (ziptic & DXD_JOINDATA)
{
g->p += MAXAVAILABILITY;
if (READUINT8(g->p) != 0)
I_Error("Ghost is not a record attack ghost (bot JOINDATA)");
}
if (ziptic & DXD_PLAYSTATE && READUINT8(g->p) != DXD_PST_PLAYING)
I_Error("Ghost is not a record attack ghost PLAYSTATE"); //@TODO lmao don't blow up like this
I_Error("Ghost is not a record attack ghost (has PLAYSTATE)");
if (ziptic & DXD_SKIN)
g->p++; // We _could_ read this info, but it shouldn't change anything in record attack...
if (ziptic & DXD_COLOR)
@ -2248,6 +2263,7 @@ static void G_SaveDemoSkins(UINT8 **pp)
{
char skin[16];
UINT8 i;
UINT8 *availabilitiesbuffer = R_GetSkinAvailabilities(true);
WRITEUINT8((*pp), numskins);
for (i = 0; i < numskins; i++)
@ -2262,12 +2278,17 @@ static void G_SaveDemoSkins(UINT8 **pp)
WRITEUINT8((*pp), skins[i].kartweight);
WRITEUINT32((*pp), skins[i].flags);
}
for (i = 0; i < MAXAVAILABILITY; i++)
{
WRITEUINT8((*pp), availabilitiesbuffer[i]);
}
}
static democharlist_t *G_LoadDemoSkins(UINT8 **pp, UINT8 *worknumskins, boolean getclosest)
{
char skin[17];
UINT8 i;
UINT8 i, byte, shif;
democharlist_t *skinlist = NULL;
(*worknumskins) = READUINT8((*pp));
@ -2310,6 +2331,24 @@ static democharlist_t *G_LoadDemoSkins(UINT8 **pp, UINT8 *worknumskins, boolean
}
}
for (byte = 0; byte < MAXAVAILABILITY; byte++)
{
UINT8 availabilitiesbuffer = READUINT8((*pp));
for (shif = 0; shif < 8; shif++)
{
i = (byte*8) + shif;
if (i >= (*worknumskins))
break;
if (availabilitiesbuffer & (1 << shif))
{
skinlist[i].unlockrequired = true;
}
}
}
return skinlist;
}
@ -2326,6 +2365,8 @@ static void G_SkipDemoSkins(UINT8 **pp)
(*pp)++; // kartweight
(*pp) += 4; // flags
}
(*pp) += MAXAVAILABILITY;
}
void G_BeginRecording(void)
@ -2443,6 +2484,11 @@ void G_BeginRecording(void)
M_Memcpy(demo_p,name,16);
demo_p += 16;
for (j = 0; j < MAXAVAILABILITY; j++)
{
WRITEUINT8(demo_p, player->availabilities[j]);
}
// Skin (now index into demo.skinlist)
WRITEUINT8(demo_p, player->skin);
WRITEUINT8(demo_p, player->lastfakeskin);
@ -2928,6 +2974,7 @@ void G_DoPlayDemo(char *defdemoname)
UINT8 i, p, numslots = 0;
lumpnum_t l;
char color[MAXCOLORNAME+1],follower[17],mapname[MAXMAPLUMPNAME],*n,*pdemoname;
UINT8 availabilities[MAXPLAYERS][MAXAVAILABILITY];
UINT8 version,subversion;
UINT32 randseed[PRNUMCLASS];
char msg[1024];
@ -3293,6 +3340,11 @@ void G_DoPlayDemo(char *defdemoname)
M_Memcpy(player_names[p],demo_p,16);
demo_p += 16;
for (i = 0; i < MAXAVAILABILITY; i++)
{
availabilities[p][i] = READUINT8(demo_p);
}
// Skin
i = READUINT8(demo_p);
@ -3376,6 +3428,8 @@ void G_DoPlayDemo(char *defdemoname)
for (i = 0; i < numslots; i++)
{
UINT8 j;
p = slots[i];
if (players[p].mo)
{
@ -3392,6 +3446,11 @@ void G_DoPlayDemo(char *defdemoname)
players[p].kartweight = ghostext[p].kartweight = demo.skinlist[demo.currentskinid[p]].kartweight;
players[p].charflags = ghostext[p].charflags = demo.skinlist[demo.currentskinid[p]].flags;
players[p].lastfakeskin = lastfakeskin[p];
for (j = 0; j < MAXAVAILABILITY; j++)
{
players[p].availabilities[j] = availabilities[p][j];
}
}
demo.deferstart = true;

View file

@ -33,6 +33,7 @@ typedef struct democharlist_s {
UINT8 kartspeed;
UINT8 kartweight;
UINT32 flags;
boolean unlockrequired;
} democharlist_t;
// Publicly-accessible demo vars
@ -119,20 +120,19 @@ typedef enum
extern UINT8 demo_extradata[MAXPLAYERS];
extern UINT8 demo_writerng;
#define DXD_PLAYSTATE 0x01 // state changed between playing, spectating, or not in-game
#define DXD_RESPAWN 0x02 // "respawn" command in console
#define DXD_JOINDATA 0x01 // join-specific data
#define DXD_PLAYSTATE 0x02 // state changed between playing, spectating, or not in-game
#define DXD_SKIN 0x04 // skin changed
#define DXD_NAME 0x08 // name changed
#define DXD_COLOR 0x10 // color changed
#define DXD_FOLLOWER 0x20 // follower was changed
#define DXD_WEAPONPREF 0x40 // netsynced playsim settings were changed
#define DXD_RESPAWN 0x40 // "respawn" command in console
#define DXD_WEAPONPREF 0x80 // netsynced playsim settings were changed
#define DXD_PST_PLAYING 0x01
#define DXD_PST_SPECTATING 0x02
#define DXD_PST_LEFT 0x03
#define DXD_PST_ISBOT 0x80 // extra flag
// Record/playback tics
void G_ReadDemoExtraData(void);
void G_WriteDemoExtraData(void);

View file

@ -2238,7 +2238,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps)
UINT8 latestlap;
UINT16 skincolor;
INT32 skin;
UINT32 availabilities;
UINT8 availabilities[MAXAVAILABILITY];
UINT8 fakeskin;
UINT8 lastfakeskin;
@ -2307,7 +2307,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps)
followercolor = players[player].followercolor;
followerskin = players[player].followerskin;
availabilities = players[player].availabilities;
memcpy(availabilities, players[player].availabilities, sizeof(availabilities));
followitem = players[player].followitem;
@ -2432,7 +2432,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps)
p->charflags = charflags;
p->lastfakeskin = lastfakeskin;
p->availabilities = availabilities;
memcpy(players[player].availabilities, availabilities, sizeof(availabilities));
p->followitem = followitem;
p->starpostnum = starpostnum;
@ -2918,7 +2918,7 @@ void G_AddPlayer(INT32 playernum)
{
player_t *p = &players[playernum];
p->playerstate = PST_REBORN;
demo_extradata[playernum] |= DXD_PLAYSTATE|DXD_COLOR|DXD_NAME|DXD_SKIN|DXD_FOLLOWER; // Set everything
demo_extradata[playernum] |= DXD_JOINDATA|DXD_PLAYSTATE|DXD_COLOR|DXD_NAME|DXD_SKIN|DXD_FOLLOWER; // Set everything
}
void G_ExitLevel(void)
@ -3690,6 +3690,10 @@ static void G_UpdateVisited(void)
if ((earnedEmblems = M_CompletionEmblems()))
CONS_Printf(M_GetText("\x82" "Earned %hu emblem%s for level completion.\n"), (UINT16)earnedEmblems, earnedEmblems > 1 ? "s" : "");
if (M_UpdateUnlockablesAndExtraEmblems())
S_StartSound(NULL, sfx_ncitem);
G_SaveGameData();
}
static boolean CanSaveLevel(INT32 mapnum)
@ -4507,12 +4511,14 @@ void G_SaveGameData(void)
return;
}
#ifndef DEVELOP
if (usedCheats)
{
free(savebuffer);
save_p = savebuffer = NULL;
return;
}
#endif
// Version test

View file

@ -231,9 +231,9 @@ void K_InitGrandPrixBots(void)
// Rearrange usable bot skins list to prevent gaps for randomised selection
for (i = 0; i < usableskins; i++)
{
if (!(grabskins[i] == MAXSKINS /*|| K_SkinLocked(grabskins[i])*/))
if (!(grabskins[i] == MAXSKINS || !R_SkinUsable(-1, grabskins[i], true)))
continue;
while (usableskins > i && (grabskins[usableskins] == MAXSKINS /*|| K_SkinLocked(grabskins[i])*/))
while (usableskins > i && (grabskins[usableskins] == MAXSKINS || !R_SkinUsable(-1, grabskins[usableskins], true)))
{
usableskins--;
}
@ -557,9 +557,9 @@ void K_RetireBots(void)
// Rearrange usable bot skins list to prevent gaps for randomised selection
for (i = 0; i < usableskins; i++)
{
if (!(grabskins[i] == MAXSKINS /*|| K_SkinLocked(grabskins[i])*/))
if (!(grabskins[i] == MAXSKINS || !R_SkinUsable(-1, grabskins[i], true)))
continue;
while (usableskins > i && (grabskins[usableskins] == MAXSKINS /*|| K_SkinLocked(grabskins[i])*/))
while (usableskins > i && (grabskins[usableskins] == MAXSKINS || !R_SkinUsable(-1, grabskins[usableskins], true)))
usableskins--;
grabskins[i] = grabskins[usableskins];
grabskins[usableskins] = MAXSKINS;

View file

@ -2175,6 +2175,9 @@ void M_CharacterSelectInit(void)
UINT8 x = skins[i].kartspeed-1;
UINT8 y = skins[i].kartweight-1;
if (!R_SkinUsable(g_localplayers[0], i, false))
continue;
if (setup_chargrid[x][y].numskins >= MAXCLONES)
CONS_Alert(CONS_ERROR, "Max character alts reached for %d,%d\n", x+1, y+1);
else

View file

@ -2351,7 +2351,7 @@ static int lib_rSetPlayerSkin(lua_State *L)
return luaL_error(L, "skin %s (argument 2) is not loaded", skinname);
}
if (!R_SkinUsable(j, i))
if (!R_SkinUsable(j, i, false))
return luaL_error(L, "skin %d (argument 2) not usable - check with R_SkinUsable(player_t, skin) first.", i);
SetPlayerSkinByNum(j, i);
return 0;
@ -2381,7 +2381,7 @@ static int lib_rSkinUsable(lua_State *L)
return luaL_error(L, "skin %s (argument 2) is not loaded", skinname);
}
lua_pushboolean(L, R_SkinUsable(j, i));
lua_pushboolean(L, R_SkinUsable(j, i, false));
return 1;
}

View file

@ -669,7 +669,7 @@ static int mobj_set(lua_State *L)
for (i = 0; i < numskins; i++)
if (fastcmp(skins[i].name, skin))
{
if (!mo->player || R_SkinUsable(mo->player-players, i))
if (!mo->player || R_SkinUsable(mo->player-players, i, false))
mo->skin = &skins[i];
return 0;
}

View file

@ -408,8 +408,6 @@ static int player_get(lua_State *L)
lua_pushinteger(L, plr->skincolor);
else if (fastcmp(field,"skin"))
lua_pushinteger(L, plr->skin);
else if (fastcmp(field,"availabilities"))
lua_pushinteger(L, plr->availabilities);
else if (fastcmp(field,"fakeskin"))
lua_pushinteger(L, plr->fakeskin);
else if (fastcmp(field,"lastfakeskin"))
@ -577,8 +575,6 @@ static int player_set(lua_State *L)
}
else if (fastcmp(field,"skin"))
return NOSET;
else if (fastcmp(field,"availabilities"))
return NOSET;
else if (fastcmp(field,"fakeskin"))
return NOSET;
else if (fastcmp(field,"lastfakeskin"))

View file

@ -290,11 +290,6 @@ void M_SilentUpdateUnlockablesAndEmblems(void)
continue;
unlockables[i].unlocked = M_Achieved(unlockables[i].conditionset - 1);
}
for (i = 0; i < MAXSPLITSCREENPLAYERS; i++)
{
players[g_localplayers[i]].availabilities = R_GetSkinAvailabilities();
}
}
// Emblem unlocking shit
@ -372,6 +367,7 @@ UINT8 M_CompletionEmblems(void) // Bah! Duplication sucks, but it's for a separa
if (res)
++somethingUnlocked;
}
return somethingUnlocked;
}
@ -502,6 +498,35 @@ UINT8 M_GotLowEnoughTime(INT32 tictime)
return true;
}
// Gets the skin number for a SECRET_SKIN unlockable.
INT32 M_UnlockableSkinNum(unlockable_t *unlock)
{
if (unlock->type != SECRET_SKIN)
{
// This isn't a skin unlockable...
return -1;
}
if (unlock->stringVar && strcmp(unlock->stringVar, ""))
{
// Get the skin from the string.
INT32 skinnum = R_SkinAvailable(unlock->stringVar);
if (skinnum != -1)
{
return skinnum;
}
}
if (unlock->variable >= 0 && unlock->variable < numskins)
{
// Use the number directly.
return unlock->variable;
}
// Invalid skin unlockable.
return -1;
}
// ----------------
// Misc Emblem shit
// ----------------

View file

@ -77,6 +77,7 @@ typedef struct
UINT8 sprite; ///< emblem sprite to use, 0 - 25
UINT16 color; ///< skincolor to use
INT32 var; ///< If needed, specifies information on the target amount to achieve (or target skin)
char *stringVar; ///< String version
char hint[110]; ///< Hint for emblem hints menu
UINT8 collected; ///< Do you have this emblem?
} emblem_t;
@ -100,6 +101,7 @@ typedef struct
UINT8 showconditionset;
INT16 type;
INT16 variable;
char *stringVar;
UINT8 nocecho;
UINT8 nochecklist;
UINT8 unlocked;
@ -177,4 +179,7 @@ const char *M_GetExtraEmblemPatch(extraemblem_t *em, boolean big);
UINT8 M_GotEnoughEmblems(INT32 number);
UINT8 M_GotLowEnoughTime(INT32 tictime);
INT32 M_UnlockableSkinNum(unlockable_t *unlock);
INT32 M_EmblemSkinNum(emblem_t *emblem);
#define M_Achieved(a) ((a) >= MAXCONDITIONSETS || conditionSets[a].achieved)

View file

@ -150,7 +150,12 @@ static void P_NetArchivePlayers(void)
WRITEUINT8(save_p, players[i].skincolor);
WRITEINT32(save_p, players[i].skin);
WRITEUINT32(save_p, players[i].availabilities);
for (j = 0; j < MAXAVAILABILITY; j++)
{
WRITEUINT8(save_p, players[i].availabilities[j]);
}
WRITEUINT8(save_p, players[i].fakeskin);
WRITEUINT8(save_p, players[i].lastfakeskin);
WRITEUINT32(save_p, players[i].score);
@ -472,7 +477,12 @@ static void P_NetUnArchivePlayers(void)
players[i].skincolor = READUINT8(save_p);
players[i].skin = READINT32(save_p);
players[i].availabilities = READUINT32(save_p);
for (j = 0; i < MAXAVAILABILITY; j++)
{
players[i].availabilities[j] = READUINT8(save_p);
}
players[i].fakeskin = READUINT8(save_p);
players[i].lastfakeskin = READUINT8(save_p);
players[i].score = READUINT32(save_p);

View file

@ -7559,10 +7559,14 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
nextmapoverride = 0;
skipstats = 0;
if (!(netgame || multiplayer || demo.playback) && !majormods)
if (!demo.playback)
{
mapheaderinfo[gamemap-1]->mapvisited |= MV_VISITED;
else if (!demo.playback)
mapheaderinfo[gamemap-1]->mapvisited |= MV_MP; // you want to record that you've been there this session, but not permanently
if (M_UpdateUnlockablesAndExtraEmblems())
S_StartSound(NULL, sfx_ncitem);
G_SaveGameData();
}
G_AddMapToBuffer(gamemap-1);
@ -8036,8 +8040,8 @@ UINT16 P_PartialAddWadFile(const char *wadfilename)
//
// look for skins
//
R_AddSkins(wadnum); // faB: wadfile index in wadfiles[]
R_PatchSkins(wadnum); // toast: PATCH PATCH
R_AddSkins(wadnum, false); // faB: wadfile index in wadfiles[]
R_PatchSkins(wadnum, false); // toast: PATCH PATCH
//
// edit music defs

View file

@ -32,6 +32,7 @@
#if 0
#include "k_kart.h" // K_KartResetPlayerColor
#endif
#include "k_grandprix.h" // K_CanChangeRules
#ifdef HWRENDER
#include "hardware/hw_md2.h"
#endif
@ -150,36 +151,51 @@ void R_InitSkins(void)
for (i = 0; i < numwadfiles; i++)
{
R_AddSkins((UINT16)i);
R_PatchSkins((UINT16)i);
R_AddSkins((UINT16)i, true);
R_PatchSkins((UINT16)i, true);
R_LoadSpriteInfoLumps(i, wadfiles[i]->numlumps);
}
ST_ReloadSkinFaceGraphics();
}
UINT32 R_GetSkinAvailabilities(void)
UINT8 *R_GetSkinAvailabilities(boolean demolock)
{
UINT8 i;
UINT32 response = 0;
UINT8 i, shif, byte;
INT32 skinid;
static UINT8 responsebuffer[MAXAVAILABILITY];
memset(&responsebuffer, 0, sizeof(responsebuffer));
for (i = 0; i < MAXUNLOCKABLES; i++)
{
if (unlockables[i].type == SECRET_SKIN && unlockables[i].unlocked)
{
UINT8 s = min(unlockables[i].variable, MAXSKINS);
response |= (1 << s);
}
if (unlockables[i].type != SECRET_SKIN)
continue;
if (unlockables[i].unlocked != true && !demolock)
continue;
skinid = M_UnlockableSkinNum(&unlockables[i]);
if (skinid < 0 || skinid >= MAXSKINS)
continue;
shif = (skinid % 8);
byte = (skinid / 8);
responsebuffer[byte] |= (1 << shif);
}
return response;
return responsebuffer;
}
// returns true if available in circumstances, otherwise nope
// warning don't use with an invalid skinnum other than -1 which always returns true
boolean R_SkinUsable(INT32 playernum, INT32 skinnum)
boolean R_SkinUsable(INT32 playernum, INT32 skinnum, boolean demoskins)
{
boolean needsunlocked = false;
boolean useplayerstruct = (Playing() && playernum != -1);
UINT8 i;
INT32 skinid;
if (skinnum == -1)
{
@ -187,55 +203,54 @@ boolean R_SkinUsable(INT32 playernum, INT32 skinnum)
return true;
}
if (modeattacking)
{
// If you have someone else's run, you should be able to take a look
return true;
}
if (netgame && (cv_forceskin.value == skinnum))
if (K_CanChangeRules(true) && (cv_forceskin.value == skinnum))
{
// Being forced to play as this character by the server
return true;
}
if (metalrecording)
{
// Recording a Metal Sonic race
const INT32 metalskin = R_SkinAvailable("metalsonic");
return (skinnum == metalskin);
}
// Determine if this character is supposed to be unlockable or not
for (i = 0; i < MAXUNLOCKABLES; i++)
if (useplayerstruct && demo.playback)
{
if (unlockables[i].type == SECRET_SKIN && unlockables[i].variable == skinnum)
if (!demoskins)
skinnum = demo.skinlist[skinnum].mapping;
needsunlocked = demo.skinlist[skinnum].unlockrequired;
}
else
{
for (i = 0; i < MAXUNLOCKABLES; i++)
{
if (unlockables[i].type != SECRET_SKIN)
continue;
skinid = M_UnlockableSkinNum(&unlockables[i]);
if (skinid != skinnum)
continue;
// i is now the unlockable index, we can use this later
needsunlocked = true;
break;
}
}
if (needsunlocked == true)
{
// You can use this character IF you have it unlocked.
if ((netgame || multiplayer) && playernum != -1)
{
// Use the netgame synchronized unlocks.
return (boolean)(!(players[playernum].availabilities & (1 << skinnum)));
}
else
{
// Use the unlockables table directly
return (boolean)(unlockables[i].unlocked);
}
}
else
if (needsunlocked == false)
{
// Didn't trip anything, so we can use this character.
return true;
}
// Ok, you can use this character IF you have it unlocked.
if (useplayerstruct)
{
// Use the netgame synchronized unlocks.
UINT8 shif = (skinnum % 8);
UINT8 byte = (skinnum / 8);
return !!(players[playernum].availabilities[byte] & (1 << shif));
}
// Use the unlockables table directly
return (boolean)(unlockables[i].unlocked);
}
// returns true if the skin name is found (loaded from pwad)
@ -296,7 +311,25 @@ static void SetSkin(player_t *player, INT32 skinnum)
}
// for replays: We have changed our skin mid-game; let the game know so it can do the same in the replay!
demo_extradata[playernum] |= DXD_SKIN;
demo_extradata[(player-players)] |= DXD_SKIN;
}
// Gets the player to the first usuable skin in the game.
// (If your mod locked them all, then you kinda stupid)
static INT32 GetPlayerDefaultSkin(INT32 playernum)
{
INT32 i;
for (i = 0; i < numskins; i++)
{
if (R_SkinUsable(playernum, i, false))
{
return i;
}
}
I_Error("All characters are locked!");
return 0;
}
// network code calls this when a 'skin change' is received
@ -305,7 +338,7 @@ void SetPlayerSkin(INT32 playernum, const char *skinname)
INT32 i = R_SkinAvailable(skinname);
player_t *player = &players[playernum];
if ((i != -1) && R_SkinUsable(playernum, i))
if ((i != -1) && R_SkinUsable(playernum, i, false))
{
SetSkin(player, i);
return;
@ -316,7 +349,7 @@ void SetPlayerSkin(INT32 playernum, const char *skinname)
else if(server || IsPlayerAdmin(consoleplayer))
CONS_Alert(CONS_WARNING, M_GetText("Player %d (%s) skin '%s' not found\n"), playernum, player_names[playernum], skinname);
SetSkin(player, 0);
SetSkin(player, GetPlayerDefaultSkin(playernum));
}
// Same as SetPlayerSkin, but uses the skin #.
@ -325,7 +358,7 @@ void SetPlayerSkinByNum(INT32 playernum, INT32 skinnum)
{
player_t *player = &players[playernum];
if (skinnum >= 0 && skinnum < numskins && R_SkinUsable(playernum, skinnum)) // Make sure it exists!
if (skinnum >= 0 && skinnum < numskins && R_SkinUsable(playernum, skinnum, false)) // Make sure it exists!
{
SetSkin(player, skinnum);
return;
@ -336,7 +369,7 @@ void SetPlayerSkinByNum(INT32 playernum, INT32 skinnum)
else if(server || IsPlayerAdmin(consoleplayer))
CONS_Alert(CONS_WARNING, "Player %d (%s) skin %d not found\n", playernum, player_names[playernum], skinnum);
SetSkin(player, 0); // not found put the eggman skin
SetSkin(player, GetPlayerDefaultSkin(playernum)); // not found put the eggman skin
}
// Set mo skin but not player_t skin, for ironman
@ -386,11 +419,14 @@ void SetRandomFakePlayerSkin(player_t* player, boolean fast)
}
else if (skins[i].flags & SF_IRONMAN)
continue;
/*if (K_SkinLocked(i))
continue;*/
if (!R_SkinUsable(player-players, i, true))
continue;
grabskins[usableskins++] = i;
}
if (!usableskins)
I_Error("SetRandomFakePlayerSkin: No valid skins to pick from!?");
i = grabskins[P_RandomKey(PR_RANDOMSKIN, usableskins)];
SetFakePlayerSkin(player, i);
@ -662,7 +698,7 @@ static boolean R_ProcessPatchableFields(skin_t *skin, char *stoken, char *value)
//
// Find skin sprites, sounds & optional status bar face, & add them
//
void R_AddSkins(UINT16 wadnum)
void R_AddSkins(UINT16 wadnum, boolean mainfile)
{
UINT16 lump, lastlump = 0;
char *buf;
@ -810,7 +846,8 @@ next_token:
R_FlushTranslationColormapCache();
CONS_Printf(M_GetText("Added skin '%s'\n"), skin->name);
if (mainfile == false)
CONS_Printf(M_GetText("Added skin '%s'\n"), skin->name);
#ifdef SKINVALUES
skin_cons_t[numskins].value = numskins;
@ -834,7 +871,7 @@ next_token:
//
// Patch skin sprites
//
void R_PatchSkins(UINT16 wadnum)
void R_PatchSkins(UINT16 wadnum, boolean mainfile)
{
UINT16 lump, lastlump = 0;
char *buf;
@ -976,7 +1013,8 @@ next_token:
R_FlushTranslationColormapCache();
CONS_Printf(M_GetText("Patched skin '%s'\n"), skin->name);
if (mainfile == false)
CONS_Printf(M_GetText("Patched skin '%s'\n"), skin->name);
}
return;
}

View file

@ -84,11 +84,13 @@ void SetPlayerSkinByNum(INT32 playernum,INT32 skinnum); // Tails 03-16-2002
void SetFakePlayerSkin(player_t* player, INT32 skinnum);
void SetRandomFakePlayerSkin(player_t* player, boolean fast);
void ClearFakePlayerSkin(player_t* player);
boolean R_SkinUsable(INT32 playernum, INT32 skinnum);
UINT32 R_GetSkinAvailabilities(void);
boolean R_SkinUsable(INT32 playernum, INT32 skinnum, boolean demoskins);
UINT8 *R_GetSkinAvailabilities(boolean demolock);
INT32 R_SkinAvailable(const char *name);
void R_PatchSkins(UINT16 wadnum);
void R_AddSkins(UINT16 wadnum);
void R_PatchSkins(UINT16 wadnum, boolean mainfile);
void R_AddSkins(UINT16 wadnum, boolean mainfile);
UINT8 P_GetSkinSprite2(skin_t *skin, UINT8 spr2, player_t *player);