Kart-Public/src/hu_stuff.c
Eidolon 8f354ad9c1 Implement Uncapped (squashed)
Co-Authored-By: Sally Coolatta <tehrealsalt@gmail.com>
Co-Authored-By: James R <justsomejames2@gmail.com>
Co-Authored-By: Monster Iestyn <iestynjealous@ntlworld.com>
Co-Authored-By: katsy <katmint@live.com>

Place Frame Interpolation in "Experimental" video options header

This seems like an appropriate way to describe the feature for now.

Add smooth level platter under interpolation, `renderdeltatics`

`renderdeltatics` can be used as a standard delta time in any place,
allowing for smooth menus. It will always be equal to `realtics`
when frame interpolation is turned off, producing consistent
framerate behavior everywhere it is used.

Add smooth rendering to save select screen

Add smooth rendering to Record/NiGHTS Attack, F_SkyScroll

Ensure viewsector is accurate to viewx/viewy

This fixes a potential crash in OpenGL when changing between levels.

Ensure + commands get executed before map start

Always have precise_t defined

Fix misc dropshadow issues

Reset view interpolation on level load

Remove unnecessary precipmobj thinker hack

Add reset interpolation state functions

Reset precip interpolation on snap to ceil

Reset mobj interp state on TeleportMove

Only swap view interp state if a tick is run

Run anti-lag chasecam at tic frequency

Fixes jittery and unstable chasecam in high latency netgames

Homogenize mobj interpolations

Add sector plane level interpolations

Add SectorScroll interpolator

Add SideScroll interpolator

Add Polyobj interpolator

Intialize interpolator list at a better time

Delete interpolators associated with thinkers

Interpolate mobj angles and player drawangle

Interpolate HWR_DrawModel

Add functions to handle interpolation

Much less code duplication

P_InitAngle, to fix angle interpolation on spawning objects

Fully fix drop shadows

It used the thing's floorz / ceilingz directly -- that wouldn't account for interpolated coordinates.

Do not speed up underwater/heatwave effect in OpenGL

Closer OpenGL underwater/heatwave effect to Software

Interpolate from time of previous tic

Previously interpolated from last 35th of a second, which
may be offset from game time due to connection lag.

Consider this the proper fix to 54148a0dd0 too.

Calculate FPS stuff even if frame is skipped

I decided ultimately to actually keep the frame skip optimization disabled, because I think it is actually a little bit helpful that you can still get accurate rendering perfstats while paused, however if we decide otherwise then we can have this optimization back without making the game act like it's lagging.

Keep rect in memory

Feel better about this than creating one all da time

Lots of FPS stuff

- Disabled VSync, due to the numerous problems it has.
- Instead, added an FPS cap.
- Frame interpolation is now tied to fpscap != 35.
- By default, the FPS cap is set to the monitor's refresh rate.
- Rewrote the FPS counter.

(This also consolidates several more commits ahead of this
fixing various issues. -eid)

Misc changes after Kart cherry-picks

Fix renderdeltatics with new timing data

Update mobj oldstates before all thinkers

Allow FPS cap values

Adjust how FPS cap is checked to improve FPS stability

Fix precip crash from missing vars

Improve the framerate limiter's timing for extreme stable FPS

Handle the sleep at the end of D_SRB2Loop instead of the start

Simplifies logic in the other parts of the loop, and fixes problems with it frequently waiting too long.

Reset mobj interp state on add

Add mobj interpolator on load netgame

Move mobj interpolators to r_fps

Dynamic slope interpolators

I_GetFrameTime to try and improve frame pace

(It doesn't feel that much better though.)

Move I_FinishUpdate to D_SRB2Loop to sync screen updates with FPS cap, use timestamps in I_FrameCapSleep to simplify the code

Fix plane interpolation light level flickering

Fix flickering plane interpolation for OpenGL in the exact same way

Funny OpenGL renderer being at least 50% copy-pasted Software code :)

P_SetOrigin & P_MoveOrigin to replace P_TeleportMove

Convert P_TeleportMove use to origin funcs

Revert "P_InitAngle, to fix angle interpolation on spawning objects"

This reverts commit a80c98bd164a2748cbbfad9027b34601185d93f5.

Waypoint polyobjects interpolate z & children

Add interpolation to more moving plane types

Adds interpolation to the following:
- Crumbling platforms
- Mario blocks
- Floatbob platforms (this one works really strangely due to two thinkers, maybe double-check this one?)

Reset overlays interp states each TryRunTics

Interpolate model interpolation (lol)

Use interp tracer pos for GL linkdraw

Papersprite angle interpolation

Makes the ending signpost smooth

Move intermission emerald bounce to ticker

Bring back shadows on polyobjects

Also optimizes the method used so rings can show their shadows too. Using just the subsector is a tad bit imprecise admittedly but any more precise methods get really laggy.

Fix a bunch of ticking in hu_ drawing functions

Revert "Reset overlays interp states each TryRunTics"

This reverts commit a71a216faa20e8751b3bd0157354e8d748940c92.

Move intro ticking out of the drawer

Adjust 1up monitor icon z offsets

Fixes interpolation issues with 1up monitors.

Delta time choose player menu animations

Add drawerlib deltaTime function

Interpolate afterimages further back

Use old sleep in dedicated mode

Clamp cechotimer to 0

Fixes issues with cechos staying on-screen and glitching out
(NiGHTS items for example).

Revert "Remove unnecessary precipmobj thinker hack"

This reverts commit 0e38208620d19ec2ab690740438ac2fc7862a49e.

Fix frame pacing when game lags behind

The frame timestamp should've been made at the start of the frame, not the end.

Fix I_FrameCapSleep not respecting cpusleep

Jonathan Joestar bruh

Allow dedicated to use precise sleep timing again

Instead of only using one old sleep, just enforce framerate cap to match TICRATE.

Make Lua TeleportMove call MoveOrigin

Reset Metal fume interp state on appear

Add interpdebug

Put interpdebug stuff in perfstats instead

Add timescale cvar

Slow the game down to debug animations / interpolation problems! Speed it up if you need to get somewhere quickly while mapping!

Enable timescale outside of DEVELOP builds

It has NETVAR, so it should be fine -- put an end to useful debugging features excluded in multiplayer!

Force interpolation when timescale != 1.0

Reset old_z in MT_LOCKON think

Fixes interpolation artifacting due to spawn pos.

Fix cutscenes in interp

Fix boss1 laser in interp

Interpolate mobj scale

Precalculate refresh rate

Slower PCs can have issue querying mode over and over. This might kinda suck for windowed mode if you have different refresh rate displays but oh well

Fix interp scaling crashing software

Reset interp scale when Lua sets .scale

Disable angle interp on fresh mobjs

Fix interp scale crash for hires sprites

Interp shadow scales

Copy interp state in P_SpawnMobjFromMobj

Fix multiplayer character select

Don't interpolate mobj state if frac = 1.0

Fix Mario block item placement

Interpolate spritescale/offset x/y

Fix offset copies for SpawnMobjFromMobj

THANKS SAL

Add Lua HUD drawlists

Buffers draw calls between tics to ensure hooks
run at the originally intended rate.

Rename drawerlib deltaTime to getDeltaTime

Make renderisnewtic is false between tics

I know what I'm doing! I swear

Completely refactor timing system

Time is now tracked internally in the game using I_GetPreciseTime
and I_UpdateTime. I_Time now pulls from this internal timer. The
system code no longer needs to keep track of time itself.

This significantly improves frame and tic timing in interp mode,
resulting in a much smoother image with essentially no judder at
any framerate.

Ensure mobj interpolators reset on level load

Ensure view is not interpolated on first frame

Disable sprite offset interpolation (for now)

Refactor timing code even more

System layer is greatly simplified and framecap
logic has been moved internally. I_Sleep now
takes a sleep duration and I_SleepDuration
generically implements a precise sleep with spin
loop.
2022-05-01 17:35:30 -05:00

3120 lines
81 KiB
C

// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 1993-1996 by id Software, Inc.
// Copyright (C) 1998-2000 by DooM Legacy Team.
// Copyright (C) 1999-2018 by Sonic Team Junior.
//
// 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 hu_stuff.c
/// \brief Heads up display
#include "doomdef.h"
#include "byteptr.h"
#include "hu_stuff.h"
#include "m_menu.h" // gametype_cons_t
#include "m_cond.h" // emblems
#include "d_clisrv.h"
#include "g_game.h"
#include "g_input.h"
#include "i_video.h"
#include "i_system.h"
#include "st_stuff.h" // ST_HEIGHT
#include "r_local.h"
#include "keys.h"
#include "v_video.h"
#include "w_wad.h"
#include "z_zone.h"
#include "console.h"
#include "am_map.h"
#include "d_main.h"
#include "p_local.h" // camera[]
#include "p_tick.h"
#ifdef HWRENDER
#include "hardware/hw_main.h"
#endif
#ifdef HAVE_BLUA
#include "lua_hud.h"
#include "lua_hudlib_drawlist.h"
#include "lua_hook.h"
#endif
#include "s_sound.h" // song credits
#include "k_kart.h"
// coords are scaled
#define HU_INPUTX 0
#define HU_INPUTY 0
#define HU_SERVER_SAY 1 // Server message (dedicated).
#define HU_CSAY 2 // Server CECHOes to everyone.
//-------------------------------------------
// heads up font
//-------------------------------------------
patch_t *hu_font[HU_FONTSIZE];
patch_t *kart_font[KART_FONTSIZE]; // SRB2kart
patch_t *tny_font[HU_FONTSIZE];
patch_t *tallnum[10]; // 0-9
patch_t *nightsnum[10]; // 0-9
// Level title and credits fonts
patch_t *lt_font[LT_FONTSIZE];
patch_t *cred_font[CRED_FONTSIZE];
// ping font
// Note: I'd like to adress that at this point we might *REALLY* want to work towards a common drawString function that can take any font we want because this is really turning into a MESS. :V -Lat'
patch_t *pingnum[10];
patch_t *pinggfx[5]; // small ping graphic
patch_t *framecounter;
patch_t *frameslash; // framerate stuff. Used in screen.c
static player_t *plr;
boolean chat_on; // entering a chat message?
static char w_chat[HU_MAXMSGLEN];
static size_t c_input = 0; // let's try to make the chat input less shitty.
static boolean headsupactive = false;
boolean hu_showscores; // draw rankings
static char hu_tick;
#ifdef HAVE_BLUA
static huddrawlist_h luahuddrawlist_scores;
#endif
patch_t *rflagico;
patch_t *bflagico;
patch_t *rmatcico;
patch_t *bmatcico;
patch_t *tagico;
patch_t *tallminus;
//-------------------------------------------
// coop hud
//-------------------------------------------
patch_t *emeraldpics[7];
patch_t *tinyemeraldpics[7];
static patch_t *emblemicon;
static patch_t *tokenicon;
//-------------------------------------------
// misc vars
//-------------------------------------------
// crosshair 0 = off, 1 = cross, 2 = angle, 3 = point, see m_menu.c
static patch_t *crosshair[HU_CROSSHAIRS]; // 3 precached crosshair graphics
// song credits
static patch_t *songcreditbg;
// -------
// protos.
// -------
static void HU_DrawRankings(void);
//======================================================================
// KEYBOARD LAYOUTS FOR ENTERING TEXT
//======================================================================
char *shiftxform;
char english_shiftxform[] =
{
0,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
31,
' ', '!', '"', '#', '$', '%', '&',
'"', // shift-'
'(', ')', '*', '+',
'<', // shift-,
'_', // shift--
'>', // shift-.
'?', // shift-/
')', // shift-0
'!', // shift-1
'@', // shift-2
'#', // shift-3
'$', // shift-4
'%', // shift-5
'^', // shift-6
'&', // shift-7
'*', // shift-8
'(', // shift-9
':',
':', // shift-;
'<',
'+', // shift-=
'>', '?', '@',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'{', // shift-[
'|', // shift-backslash - OH MY GOD DOES WATCOM SUCK
'}', // shift-]
'"', '_',
'~', // shift-`
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'{', '|', '}', '~', 127
};
static char cechotext[1024];
static tic_t cechotimer = 0;
static tic_t cechoduration = 5*TICRATE;
static INT32 cechoflags = 0;
//======================================================================
// HEADS UP INIT
//======================================================================
static tic_t resynch_ticker = 0;
#ifndef NONET
// just after
static void Command_Say_f(void);
static void Command_Sayto_f(void);
static void Command_Sayteam_f(void);
static void Command_CSay_f(void);
static void Got_Saycmd(UINT8 **p, INT32 playernum);
#endif
void HU_LoadGraphics(void)
{
char buffer[9];
INT32 i, j;
if (dedicated)
return;
j = HU_FONTSTART;
for (i = 0; i < HU_FONTSIZE; i++, j++)
{
// cache the heads-up font for entire game execution
sprintf(buffer, "STCFN%.3d", j);
if (W_CheckNumForName(buffer) == LUMPERROR)
hu_font[i] = NULL;
else
hu_font[i] = (patch_t *)W_CachePatchName(buffer, PU_HUDGFX);
// tiny version of the heads-up font
sprintf(buffer, "TNYFN%.3d", j);
if (W_CheckNumForName(buffer) == LUMPERROR)
tny_font[i] = NULL;
else
tny_font[i] = (patch_t *)W_CachePatchName(buffer, PU_HUDGFX);
}
// cache the level title font for entire game execution
lt_font[0] = (patch_t *)W_CachePatchName("LTFNT039", PU_HUDGFX); /// \note fake start hack
// Number support
lt_font[9] = (patch_t *)W_CachePatchName("LTFNT048", PU_HUDGFX);
lt_font[10] = (patch_t *)W_CachePatchName("LTFNT049", PU_HUDGFX);
lt_font[11] = (patch_t *)W_CachePatchName("LTFNT050", PU_HUDGFX);
lt_font[12] = (patch_t *)W_CachePatchName("LTFNT051", PU_HUDGFX);
lt_font[13] = (patch_t *)W_CachePatchName("LTFNT052", PU_HUDGFX);
lt_font[14] = (patch_t *)W_CachePatchName("LTFNT053", PU_HUDGFX);
lt_font[15] = (patch_t *)W_CachePatchName("LTFNT054", PU_HUDGFX);
lt_font[16] = (patch_t *)W_CachePatchName("LTFNT055", PU_HUDGFX);
lt_font[17] = (patch_t *)W_CachePatchName("LTFNT056", PU_HUDGFX);
lt_font[18] = (patch_t *)W_CachePatchName("LTFNT057", PU_HUDGFX);
// SRB2kart
j = KART_FONTSTART;
for (i = 0; i < KART_FONTSIZE; i++, j++)
{
// cache the heads-up font for entire game execution
sprintf(buffer, "MKFNT%.3d", j);
if (W_CheckNumForName(buffer) == LUMPERROR)
kart_font[i] = NULL;
else
kart_font[i] = (patch_t *)W_CachePatchName(buffer, PU_HUDGFX);
}
//
j = LT_FONTSTART;
for (i = 0; i < LT_FONTSIZE; i++)
{
sprintf(buffer, "LTFNT%.3d", j);
j++;
if (W_CheckNumForName(buffer) == LUMPERROR)
lt_font[i] = NULL;
else
lt_font[i] = (patch_t *)W_CachePatchName(buffer, PU_HUDGFX);
}
// cache the credits font for entire game execution (why not?)
j = CRED_FONTSTART;
for (i = 0; i < CRED_FONTSIZE; i++)
{
sprintf(buffer, "CRFNT%.3d", j);
j++;
if (W_CheckNumForName(buffer) == LUMPERROR)
cred_font[i] = NULL;
else
cred_font[i] = (patch_t *)W_CachePatchName(buffer, PU_HUDGFX);
}
//cache numbers too!
for (i = 0; i < 10; i++)
{
sprintf(buffer, "STTNUM%d", i);
tallnum[i] = (patch_t *)W_CachePatchName(buffer, PU_HUDGFX);
sprintf(buffer, "NGTNUM%d", i);
nightsnum[i] = (patch_t *) W_CachePatchName(buffer, PU_HUDGFX);
sprintf(buffer, "PINGN%d", i);
pingnum[i] = (patch_t *) W_CachePatchName(buffer, PU_HUDGFX);
}
// minus for negative tallnums
tallminus = (patch_t *)W_CachePatchName("STTMINUS", PU_HUDGFX);
// cache the crosshairs, don't bother to know which one is being used,
// just cache all 3, they're so small anyway.
for (i = 0; i < HU_CROSSHAIRS; i++)
{
sprintf(buffer, "CROSHAI%c", '1'+i);
crosshair[i] = (patch_t *)W_CachePatchName(buffer, PU_HUDGFX);
}
emblemicon = W_CachePatchName("EMBLICON", PU_HUDGFX);
tokenicon = W_CachePatchName("TOKNICON", PU_HUDGFX);
emeraldpics[0] = W_CachePatchName("CHAOS1", PU_HUDGFX);
emeraldpics[1] = W_CachePatchName("CHAOS2", PU_HUDGFX);
emeraldpics[2] = W_CachePatchName("CHAOS3", PU_HUDGFX);
emeraldpics[3] = W_CachePatchName("CHAOS4", PU_HUDGFX);
emeraldpics[4] = W_CachePatchName("CHAOS5", PU_HUDGFX);
emeraldpics[5] = W_CachePatchName("CHAOS6", PU_HUDGFX);
emeraldpics[6] = W_CachePatchName("CHAOS7", PU_HUDGFX);
tinyemeraldpics[0] = W_CachePatchName("TEMER1", PU_HUDGFX);
tinyemeraldpics[1] = W_CachePatchName("TEMER2", PU_HUDGFX);
tinyemeraldpics[2] = W_CachePatchName("TEMER3", PU_HUDGFX);
tinyemeraldpics[3] = W_CachePatchName("TEMER4", PU_HUDGFX);
tinyemeraldpics[4] = W_CachePatchName("TEMER5", PU_HUDGFX);
tinyemeraldpics[5] = W_CachePatchName("TEMER6", PU_HUDGFX);
tinyemeraldpics[6] = W_CachePatchName("TEMER7", PU_HUDGFX);
songcreditbg = W_CachePatchName("K_SONGCR", PU_HUDGFX);
// cache ping gfx:
for (i = 0; i < 5; i++)
{
sprintf(buffer, "PINGGFX%d", i+1);
pinggfx[i] = (patch_t *)W_CachePatchName(buffer, PU_HUDGFX);
}
// fps stuff
framecounter = W_CachePatchName("FRAMER", PU_HUDGFX);
frameslash = W_CachePatchName("FRAMESL", PU_HUDGFX);;
}
// Initialise Heads up
// once at game startup.
//
void HU_Init(void)
{
#ifndef NONET
COM_AddCommand("say", Command_Say_f);
COM_AddCommand("sayto", Command_Sayto_f);
COM_AddCommand("sayteam", Command_Sayteam_f);
COM_AddCommand("csay", Command_CSay_f);
RegisterNetXCmd(XD_SAY, Got_Saycmd);
#endif
// set shift translation table
shiftxform = english_shiftxform;
HU_LoadGraphics();
}
static inline void HU_Stop(void)
{
headsupactive = false;
}
//
// Reset Heads up when consoleplayer spawns
//
void HU_Start(void)
{
if (headsupactive)
HU_Stop();
plr = &players[consoleplayer];
headsupactive = true;
}
//======================================================================
// EXECUTION
//======================================================================
#ifndef NONET
// EVERY CHANGE IN THIS SCRIPT IS LOL XD! BY VINCYTM
static UINT32 chat_nummsg_log = 0;
static UINT32 chat_nummsg_min = 0;
static UINT32 chat_scroll = 0;
static tic_t chat_scrolltime = 0;
static UINT32 chat_maxscroll = 0; // how far can we scroll?
//static chatmsg_t chat_mini[CHAT_BUFSIZE]; // Display the last few messages sent.
//static chatmsg_t chat_log[CHAT_BUFSIZE]; // Keep every message sent to us in memory so we can scroll n shit, it's cool.
static char chat_log[CHAT_BUFSIZE][255]; // hold the last 48 or so messages in that log.
static char chat_mini[8][255]; // display up to 8 messages that will fade away / get overwritten
static tic_t chat_timers[8];
static boolean chat_scrollmedown = false; // force instant scroll down on the chat log. Happens when you open it / send a message.
// remove text from minichat table
static INT16 addy = 0; // use this to make the messages scroll smoothly when one fades away
static void HU_removeChatText_Mini(void)
{
// MPC: Don't create new arrays, just iterate through an existing one
size_t i;
for(i=0;i<chat_nummsg_min-1;i++) {
strcpy(chat_mini[i], chat_mini[i+1]);
chat_timers[i] = chat_timers[i+1];
}
chat_nummsg_min--; // lost 1 msg.
// use addy and make shit slide smoothly af.
addy += (vid.width < 640) ? 8 : 6;
}
// same but w the log. TODO: optimize this and maybe merge in a single func? im bad at C.
static void HU_removeChatText_Log(void)
{
// MPC: Don't create new arrays, just iterate through an existing one
size_t i;
for(i=0;i<chat_nummsg_log-1;i++) {
strcpy(chat_log[i], chat_log[i+1]);
}
chat_nummsg_log--; // lost 1 msg.
}
#endif
void HU_AddChatText(const char *text, boolean playsound)
{
#ifndef NONET
if (playsound && cv_consolechat.value != 2) // Don't play the sound if we're using hidden chat.
S_StartSound(NULL, sfx_radio);
// reguardless of our preferences, put all of this in the chat buffer in case we decide to change from oldchat mid-game.
if (chat_nummsg_log >= CHAT_BUFSIZE) // too many messages!
HU_removeChatText_Log();
strcpy(chat_log[chat_nummsg_log], text);
chat_nummsg_log++;
if (chat_nummsg_min >= 8)
HU_removeChatText_Mini();
strcpy(chat_mini[chat_nummsg_min], text);
chat_timers[chat_nummsg_min] = TICRATE*cv_chattime.value;
chat_nummsg_min++;
if (OLDCHAT) // if we're using oldchat, print directly in console
CONS_Printf("%s\n", text);
else // if we aren't, still save the message to log.txt
CON_LogMessage(va("%s\n", text));
#else
(void)playsound;
CONS_Printf("%s\n", text);
#endif
}
#ifndef NONET
/** Runs a say command, sending an ::XD_SAY message.
* A say command consists of a signed 8-bit integer for the target, an
* unsigned 8-bit flag variable, and then the message itself.
*
* The target is 0 to say to everyone, 1 to 32 to say to that player, or -1
* to -32 to say to everyone on that player's team. Note: This means you
* have to add 1 to the player number, since they are 0 to 31 internally.
*
* The flag HU_SERVER_SAY will be set if it is the dedicated server speaking.
*
* This function obtains the message using COM_Argc() and COM_Argv().
*
* \param target Target to send message to.
* \param usedargs Number of arguments to ignore.
* \param flags Set HU_CSAY for server/admin to CECHO everyone.
* \sa Command_Say_f, Command_Sayteam_f, Command_Sayto_f, Got_Saycmd
* \author Graue <graue@oceanbase.org>
*/
static void DoSayCommand(SINT8 target, size_t usedargs, UINT8 flags)
{
XBOXSTATIC char buf[254];
size_t numwords, ix;
char *msg = &buf[2];
const size_t msgspace = sizeof buf - 2;
numwords = COM_Argc() - usedargs;
I_Assert(numwords > 0);
if (CHAT_MUTE) // TODO: Per Player mute.
{
HU_AddChatText(va("%s>ERROR: The chat is muted. You can't say anything.", "\x85"), false);
return;
}
// Only servers/admins can CSAY.
if(!server && !(IsPlayerAdmin(consoleplayer)))
flags &= ~HU_CSAY;
// We handle HU_SERVER_SAY, not the caller.
flags &= ~HU_SERVER_SAY;
if(dedicated && !(flags & HU_CSAY))
flags |= HU_SERVER_SAY;
buf[0] = target;
buf[1] = flags;
msg[0] = '\0';
for (ix = 0; ix < numwords; ix++)
{
if (ix > 0)
strlcat(msg, " ", msgspace);
strlcat(msg, COM_Argv(ix + usedargs), msgspace);
}
if (strlen(msg) > 4 && strnicmp(msg, "/pm", 3) == 0) // used /pm
{
// what we're gonna do now is check if the node exists
// with that logic, characters 4 and 5 are our numbers:
const char *newmsg;
INT32 spc = 1; // used if nodenum[1] is a space.
char *nodenum = (char*) malloc(3);
strncpy(nodenum, msg+3, 3);
// check for undesirable characters in our "number"
if (((nodenum[0] < '0') || (nodenum[0] > '9')) || ((nodenum[1] < '0') || (nodenum[1] > '9')))
{
// check if nodenum[1] is a space
if (nodenum[1] == ' ')
spc = 0;
// let it slide
else
{
HU_AddChatText("\x82NOTICE: \x80Invalid command format. Correct format is \'/pm<node> \'.", false);
free(nodenum);
return;
}
}
// I'm very bad at C, I swear I am, additional checks eww!
if (spc != 0)
{
if (msg[5] != ' ')
{
HU_AddChatText("\x82NOTICE: \x80Invalid command format. Correct format is \'/pm<node> \'.", false);
free(nodenum);
return;
}
}
target = atoi((const char*) nodenum); // turn that into a number
free(nodenum);
//CONS_Printf("%d\n", target);
// check for target player, if it doesn't exist then we can't send the message!
if (target < MAXPLAYERS && playeringame[target]) // player exists
target++; // even though playernums are from 0 to 31, target is 1 to 32, so up that by 1 to have it work!
else
{
HU_AddChatText(va("\x82NOTICE: \x80Player %d does not exist.", target), false); // same
return;
}
buf[0] = target;
newmsg = msg+5+spc;
strlcpy(msg, newmsg, 252);
}
SendNetXCmd(XD_SAY, buf, strlen(msg) + 1 + msg-buf);
}
/** Send a message to everyone.
* \sa DoSayCommand, Command_Sayteam_f, Command_Sayto_f
* \author Graue <graue@oceanbase.org>
*/
static void Command_Say_f(void)
{
if (COM_Argc() < 2)
{
CONS_Printf(M_GetText("say <message>: send a message\n"));
return;
}
DoSayCommand(0, 1, 0);
}
/** Send a message to a particular person.
* \sa DoSayCommand, Command_Sayteam_f, Command_Say_f
* \author Graue <graue@oceanbase.org>
*/
static void Command_Sayto_f(void)
{
INT32 target;
if (COM_Argc() < 3)
{
CONS_Printf(M_GetText("sayto <playername|playernum> <message>: send a message to a player\n"));
return;
}
target = nametonum(COM_Argv(1));
if (target == -1)
{
CONS_Alert(CONS_NOTICE, M_GetText("No player with that name!\n"));
return;
}
target++; // Internally we use 0 to 31, but say command uses 1 to 32.
DoSayCommand((SINT8)target, 2, 0);
}
/** Send a message to members of the player's team.
* \sa DoSayCommand, Command_Say_f, Command_Sayto_f
* \author Graue <graue@oceanbase.org>
*/
static void Command_Sayteam_f(void)
{
if (COM_Argc() < 2)
{
CONS_Printf(M_GetText("sayteam <message>: send a message to your team\n"));
return;
}
if (dedicated)
{
CONS_Alert(CONS_NOTICE, M_GetText("Dedicated servers can't send team messages. Use \"say\".\n"));
return;
}
if (G_GametypeHasTeams()) // revert to normal say if we don't have teams in this gametype.
DoSayCommand(-1, 1, 0);
else
DoSayCommand(0, 1, 0);
}
/** Send a message to everyone, to be displayed by CECHO. Only
* permitted to servers and admins.
*/
static void Command_CSay_f(void)
{
if (COM_Argc() < 2)
{
CONS_Printf(M_GetText("csay <message>: send a message to be shown in the middle of the screen\n"));
return;
}
if(!server && !IsPlayerAdmin(consoleplayer))
{
CONS_Alert(CONS_NOTICE, M_GetText("Only servers and admins can use csay.\n"));
return;
}
DoSayCommand(0, 1, HU_CSAY);
}
static tic_t stop_spamming[MAXPLAYERS];
/** Receives a message, processing an ::XD_SAY command.
* \sa DoSayCommand
* \author Graue <graue@oceanbase.org>
*/
static void Got_Saycmd(UINT8 **p, INT32 playernum)
{
SINT8 target;
UINT8 flags;
const char *dispname;
char *msg;
boolean action = false;
char *ptr;
INT32 spam_eatmsg = 0;
CONS_Debug(DBG_NETPLAY,"Received SAY cmd from Player %d (%s)\n", playernum+1, player_names[playernum]);
target = READSINT8(*p);
flags = READUINT8(*p);
msg = (char *)*p;
SKIPSTRING(*p);
if ((cv_mute.value || flags & (HU_CSAY|HU_SERVER_SAY)) && playernum != serverplayer && !(IsPlayerAdmin(playernum)))
{
CONS_Alert(CONS_WARNING, cv_mute.value ?
M_GetText("Illegal say command received from %s while muted\n") : M_GetText("Illegal csay command received from non-admin %s\n"),
player_names[playernum]);
if (server)
{
XBOXSTATIC UINT8 buf[2];
buf[0] = (UINT8)playernum;
buf[1] = KICK_MSG_CON_FAIL;
SendNetXCmd(XD_KICK, &buf, 2);
}
return;
}
//check for invalid characters (0x80 or above)
{
size_t i;
const size_t j = strlen(msg);
for (i = 0; i < j; i++)
{
if (msg[i] & 0x80)
{
CONS_Alert(CONS_WARNING, M_GetText("Illegal say command received from %s containing invalid characters\n"), player_names[playernum]);
if (server)
{
XBOXSTATIC char buf[2];
buf[0] = (char)playernum;
buf[1] = KICK_MSG_CON_FAIL;
SendNetXCmd(XD_KICK, &buf, 2);
}
return;
}
}
}
// before we do anything, let's verify the guy isn't spamming, get this easier on us.
//if (stop_spamming[playernum] != 0 && cv_chatspamprotection.value && !(flags & HU_CSAY))
if (stop_spamming[playernum] != 0 && consoleplayer != playernum && cv_chatspamprotection.value && !(flags & HU_CSAY))
{
CONS_Debug(DBG_NETPLAY,"Received SAY cmd too quickly from Player %d (%s), assuming as spam and blocking message.\n", playernum+1, player_names[playernum]);
stop_spamming[playernum] = 4;
spam_eatmsg = 1;
}
else
stop_spamming[playernum] = 4; // you can hold off for 4 tics, can you?
// run the lua hook even if we were supposed to eat the msg, netgame consistency goes first.
#ifdef HAVE_BLUA
if (LUAh_PlayerMsg(playernum, target, flags, msg, spam_eatmsg))
return;
#endif
if (spam_eatmsg)
return; // don't proceed if we were supposed to eat the message.
// If it's a CSAY, just CECHO and be done with it.
if (flags & HU_CSAY)
{
HU_SetCEchoDuration(5);
I_OutputMsg("Server message: ");
HU_DoCEcho(msg);
return;
}
// Handle "/me" actions, but only in messages to everyone.
if (target == 0 && strlen(msg) > 4 && strnicmp(msg, "/me ", 4) == 0)
{
msg += 4;
action = true;
}
if (flags & HU_SERVER_SAY)
dispname = "SERVER";
else
dispname = player_names[playernum];
// Clean up message a bit
// If you use a \r character, you can remove your name
// from before the text and then pretend to be someone else!
ptr = msg;
while (*ptr != '\0')
{
if (*ptr == '\r')
*ptr = ' ';
ptr++;
}
// Show messages sent by you, to you, to your team, or to everyone:
if (playernum == consoleplayer // By you
|| (target == -1 && ST_SameTeam(&players[consoleplayer], &players[playernum])) // To your team
|| target == 0 // To everyone
|| consoleplayer == target-1) // To you
{
const char *prefix = "", *cstart = "", *cend = "", *adminchar = "\x82~\x83", *remotechar = "\x82@\x83", *fmt2, *textcolor = "\x80";
char *tempchar = NULL;
// player is a spectator?
if (players[playernum].spectator)
{
cstart = "\x86"; // grey name
textcolor = "\x86";
}
else if (target == -1) // say team
{
if (players[playernum].ctfteam == 1) // red
{
cstart = "\x85";
textcolor = "\x85";
}
else // blue
{
cstart = "\x84";
textcolor = "\x84";
}
}
else
{
const UINT8 color = players[playernum].skincolor;
cstart = "\x83";
switch (color)
{
case SKINCOLOR_WHITE:
case SKINCOLOR_SILVER:
case SKINCOLOR_SLATE:
cstart = "\x80"; // White
break;
case SKINCOLOR_GREY:
case SKINCOLOR_NICKEL:
case SKINCOLOR_BLACK:
case SKINCOLOR_SKUNK:
case SKINCOLOR_JET:
cstart = "\x86"; // V_GRAYMAP
break;
case SKINCOLOR_SEPIA:
case SKINCOLOR_BEIGE:
case SKINCOLOR_WALNUT:
case SKINCOLOR_BROWN:
case SKINCOLOR_LEATHER:
case SKINCOLOR_RUST:
case SKINCOLOR_WRISTWATCH:
cstart = "\x8e"; // V_BROWNMAP
break;
case SKINCOLOR_FAIRY:
case SKINCOLOR_SALMON:
case SKINCOLOR_PINK:
case SKINCOLOR_ROSE:
case SKINCOLOR_BRICK:
case SKINCOLOR_LEMONADE:
case SKINCOLOR_BUBBLEGUM:
case SKINCOLOR_LILAC:
cstart = "\x8d"; // V_PINKMAP
break;
case SKINCOLOR_CINNAMON:
case SKINCOLOR_RUBY:
case SKINCOLOR_RASPBERRY:
case SKINCOLOR_CHERRY:
case SKINCOLOR_RED:
case SKINCOLOR_CRIMSON:
case SKINCOLOR_MAROON:
case SKINCOLOR_FLAME:
case SKINCOLOR_SCARLET:
case SKINCOLOR_KETCHUP:
cstart = "\x85"; // V_REDMAP
break;
case SKINCOLOR_DAWN:
case SKINCOLOR_SUNSET:
case SKINCOLOR_CREAMSICLE:
case SKINCOLOR_ORANGE:
case SKINCOLOR_PUMPKIN:
case SKINCOLOR_ROSEWOOD:
case SKINCOLOR_BURGUNDY:
case SKINCOLOR_TANGERINE:
cstart = "\x87"; // V_ORANGEMAP
break;
case SKINCOLOR_PEACH:
case SKINCOLOR_CARAMEL:
case SKINCOLOR_CREAM:
cstart = "\x8f"; // V_PEACHMAP
break;
case SKINCOLOR_GOLD:
case SKINCOLOR_ROYAL:
case SKINCOLOR_BRONZE:
case SKINCOLOR_COPPER:
case SKINCOLOR_THUNDER:
cstart = "\x8A"; // V_GOLDMAP
break;
case SKINCOLOR_POPCORN:
case SKINCOLOR_QUARRY:
case SKINCOLOR_YELLOW:
case SKINCOLOR_MUSTARD:
case SKINCOLOR_CROCODILE:
case SKINCOLOR_OLIVE:
cstart = "\x82"; // V_YELLOWMAP
break;
case SKINCOLOR_ARTICHOKE:
case SKINCOLOR_VOMIT:
case SKINCOLOR_GARDEN:
case SKINCOLOR_TEA:
case SKINCOLOR_PISTACHIO:
cstart = "\x8b"; // V_TEAMAP
break;
case SKINCOLOR_LIME:
case SKINCOLOR_HANDHELD:
case SKINCOLOR_MOSS:
case SKINCOLOR_CAMOUFLAGE:
case SKINCOLOR_ROBOHOOD:
case SKINCOLOR_MINT:
case SKINCOLOR_GREEN:
case SKINCOLOR_PINETREE:
case SKINCOLOR_EMERALD:
case SKINCOLOR_SWAMP:
case SKINCOLOR_DREAM:
case SKINCOLOR_PLAGUE:
case SKINCOLOR_ALGAE:
cstart = "\x83"; // V_GREENMAP
break;
case SKINCOLOR_CARIBBEAN:
case SKINCOLOR_AZURE:
case SKINCOLOR_AQUA:
case SKINCOLOR_TEAL:
case SKINCOLOR_CYAN:
case SKINCOLOR_JAWZ:
case SKINCOLOR_CERULEAN:
case SKINCOLOR_NAVY:
case SKINCOLOR_SAPPHIRE:
cstart = "\x88"; // V_SKYMAP
break;
case SKINCOLOR_PIGEON:
case SKINCOLOR_PLATINUM:
case SKINCOLOR_STEEL:
cstart = "\x8c"; // V_STEELMAP
break;
case SKINCOLOR_PERIWINKLE:
case SKINCOLOR_BLUE:
case SKINCOLOR_BLUEBERRY:
case SKINCOLOR_NOVA:
cstart = "\x84"; // V_BLUEMAP
break;
case SKINCOLOR_ULTRAVIOLET:
case SKINCOLOR_PURPLE:
case SKINCOLOR_FUCHSIA:
cstart = "\x81"; // V_PURPLEMAP
break;
case SKINCOLOR_PASTEL:
case SKINCOLOR_MOONSLAM:
case SKINCOLOR_DUSK:
case SKINCOLOR_TOXIC:
case SKINCOLOR_MAUVE:
case SKINCOLOR_LAVENDER:
case SKINCOLOR_BYZANTIUM:
case SKINCOLOR_POMEGRANATE:
cstart = "\x89"; // V_LAVENDERMAP
break;
default:
break;
}
}
prefix = cstart;
// Give admins and remote admins their symbols.
if (playernum == serverplayer)
tempchar = (char *)Z_Calloc(strlen(cstart) + strlen(adminchar) + 1, PU_STATIC, NULL);
else if (IsPlayerAdmin(playernum))
tempchar = (char *)Z_Calloc(strlen(cstart) + strlen(remotechar) + 1, PU_STATIC, NULL);
if (tempchar)
{
if (playernum == serverplayer)
strcat(tempchar, adminchar);
else
strcat(tempchar, remotechar);
strcat(tempchar, cstart);
cstart = tempchar;
}
// Choose the proper format string for display.
// Each format includes four strings: color start, display
// name, color end, and the message itself.
// '\4' makes the message yellow and beeps; '\3' just beeps.
if (action)
fmt2 = "* %s%s%s%s \x82%s%s";
else if (target-1 == consoleplayer) // To you
{
prefix = "\x82[PM]";
cstart = "\x82";
textcolor = "\x82";
fmt2 = "%s<%s%s>%s\x80 %s%s";
}
else if (target > 0) // By you, to another player
{
// Use target's name.
dispname = player_names[target-1];
prefix = "\x82[TO]";
cstart = "\x82";
fmt2 = "%s<%s%s>%s\x80 %s%s";
}
else // To everyone or sayteam, it doesn't change anything.
fmt2 = "%s<%s%s%s>\x80 %s%s";
/*else // To your team
{
if (players[playernum].ctfteam == 1) // red
prefix = "\x85[TEAM]";
else if (players[playernum].ctfteam == 2) // blue
prefix = "\x84[TEAM]";
else
prefix = "\x83"; // makes sure this doesn't implode if you sayteam on non-team gamemodes
fmt2 = "%s<%s%s>\x80%s %s%s";
}*/
HU_AddChatText(va(fmt2, prefix, cstart, dispname, cend, textcolor, msg), cv_chatnotifications.value); // add to chat
if (tempchar)
Z_Free(tempchar);
}
#ifdef _DEBUG
// I just want to point out while I'm here that because the data is still
// sent to all players, techincally anyone can see your chat if they really
// wanted to, even if you used sayto or sayteam.
// You should never send any sensitive info through sayto for that reason.
else
CONS_Printf("Dropped chat: %d %d %s\n", playernum, target, msg);
#endif
}
// Handles key input and string input
//
static inline boolean HU_keyInChatString(char *s, char ch)
{
size_t l;
if ((ch >= HU_FONTSTART && ch <= HU_FONTEND && hu_font[ch-HU_FONTSTART])
|| ch == ' ') // Allow spaces, of course
{
l = strlen(s);
if (l < HU_MAXMSGLEN - 1)
{
if (c_input >= strlen(s)) // don't do anything complicated
{
s[l++] = ch;
s[l]=0;
}
else
{
// move everything past c_input for new characters:
size_t m = HU_MAXMSGLEN-1;
while (m>=c_input)
{
if (s[m])
s[m+1] = (s[m]);
if (m == 0) // prevent overflow
break;
m--;
}
s[c_input] = ch; // and replace this.
}
c_input++;
return true;
}
return false;
}
else if (ch == KEY_BACKSPACE)
{
size_t i = c_input;
if (c_input <= 0)
return false;
if (!s[i-1])
return false;
if (i >= strlen(s)-1)
{
s[strlen(s)-1] = 0;
c_input--;
return false;
}
for (; (i < HU_MAXMSGLEN); i++)
{
s[i-1] = s[i];
}
c_input--;
}
else if (ch != KEY_ENTER)
return false; // did not eat key
return true; // ate the key
}
#endif
//
//
void HU_Ticker(void)
{
if (dedicated)
return;
hu_tick++;
hu_tick &= 7; // currently only to blink chat input cursor
if (PLAYER1INPUTDOWN(gc_scores))
hu_showscores = !chat_on;
else
hu_showscores = false;
if (chat_on)
{
// count down the scroll timer.
if (chat_scrolltime > 0)
chat_scrolltime--;
}
if (cechotimer > 0) --cechotimer;
}
#ifndef NONET
static boolean teamtalk = false;
/*static char chatchars[QUEUESIZE];
static INT32 head = 0, tail = 0;*/
// WHY DO YOU OVERCOMPLICATE EVERYTHING?????????
// Clear spaces so we don't end up with messages only made out of emptiness
static boolean HU_clearChatSpaces(void)
{
size_t i = 0; // Used to just check our message
char c; // current character we're iterating.
boolean nothingbutspaces = true;
for (; i < strlen(w_chat); i++) // iterate through message and eradicate all spaces that don't belong.
{
c = w_chat[i];
if (!c)
break; // if there's nothing, it's safe to assume our message has ended, so let's not waste any more time here.
if (c != ' ') // Isn't a space
{
nothingbutspaces = false;
}
}
return nothingbutspaces;
}
//
//
static void HU_queueChatChar(INT32 c)
{
// send automaticly the message (no more chat char)
if (c == KEY_ENTER)
{
char buf[2+256];
char *msg = &buf[2];
size_t i;
size_t ci = 2;
INT32 target = 0;
if (HU_clearChatSpaces()) // Avoids being able to send empty messages, or something.
return; // If this returns true, that means our message was NOTHING but spaces, so don't send it period.
do {
c = w_chat[-2+ci++];
if (!c || (c >= ' ' && !(c & 0x80))) // copy printable characters and terminating '\0' only.
buf[ci-1]=c;
} while (c);
i = 0;
for (;(i<HU_MAXMSGLEN);i++)
w_chat[i] = 0; // reset this.
c_input = 0;
for (;(i<HU_MAXMSGLEN);i++)
w_chat[i] = 0; // reset this.
c_input = 0;
// last minute mute check
if (CHAT_MUTE)
{
HU_AddChatText(va("%s>ERROR: The chat is muted. You can't say anything.", "\x85"), false);
return;
}
if (strlen(msg) > 4 && strnicmp(msg, "/pm", 3) == 0) // used /pm
{
INT32 spc = 1; // used if nodenum[1] is a space.
char *nodenum = (char*) malloc(3);
const char *newmsg;
// what we're gonna do now is check if the node exists
// with that logic, characters 4 and 5 are our numbers:
// teamtalk can't send PMs, just don't send it, else everyone would be able to see it, and no one wants to see your sex RP sicko.
if (teamtalk)
{
HU_AddChatText(va("%sCannot send sayto in Say-Team.", "\x85"), false);
return;
}
strncpy(nodenum, msg+3, 3);
// check for undesirable characters in our "number"
if (((nodenum[0] < '0') || (nodenum[0] > '9')) || ((nodenum[1] < '0') || (nodenum[1] > '9')))
{
// check if nodenum[1] is a space
if (nodenum[1] == ' ')
spc = 0;
// let it slide
else
{
HU_AddChatText("\x82NOTICE: \x80Invalid command format. Correct format is \'/pm<node> \'.", false);
free(nodenum);
return;
}
}
// I'm very bad at C, I swear I am, additional checks eww!
if (spc != 0)
{
if (msg[5] != ' ')
{
HU_AddChatText("\x82NOTICE: \x80Invalid command format. Correct format is \'/pm<node> \'.", false);
free(nodenum);
return;
}
}
target = atoi((const char*) nodenum); // turn that into a number
free(nodenum);
//CONS_Printf("%d\n", target);
// check for target player, if it doesn't exist then we can't send the message!
if (target < MAXPLAYERS && playeringame[target]) // player exists
target++; // even though playernums are from 0 to 31, target is 1 to 32, so up that by 1 to have it work!
else
{
HU_AddChatText(va("\x82NOTICE: \x80Player %d does not exist.", target), false); // same
return;
}
// we need to get rid of the /pm<node>
newmsg = msg+5+spc;
strlcpy(msg, newmsg, 255);
}
if (ci > 3) // don't send target+flags+empty message.
{
if (teamtalk)
buf[0] = -1; // target
else
buf[0] = target;
buf[1] = 0; // flags
SendNetXCmd(XD_SAY, buf, 2 + strlen(&buf[2]) + 1);
}
return;
}
}
#endif
void HU_clearChatChars(void)
{
size_t i = 0;
for (;i<HU_MAXMSGLEN;i++)
w_chat[i] = 0; // reset this.
chat_on = false;
c_input = 0;
}
#ifndef NONET
static boolean justscrolleddown;
static boolean justscrolledup;
static INT16 typelines = 1; // number of drawfill lines we need when drawing the chat. it's some weird hack and might be one frame off but I'm lazy to make another loop.
// It's up here since it has to be reset when we open the chat.
#endif
//
// Returns true if key eaten
//
boolean HU_Responder(event_t *ev)
{
if (ev->type != ev_keydown)
return false;
// only KeyDown events now...
// Shoot, to prevent P1 chatting from ruining the game for everyone else, it's either:
// A. completely disallow opening chat entirely in online splitscreen
// or B. iterate through all controls to make sure it's bound to player 1 before eating
// You can see which one I chose.
// (Unless if you're sharing a keyboard, since you probably establish when you start chatting that you have dibs on it...)
// (Ahhh, the good ol days when I was a kid who couldn't afford an extra USB controller...)
if (ev->data1 >= KEY_MOUSE1)
{
INT32 i;
for (i = 0; i < num_gamecontrols; i++)
{
if (gamecontrol[i][0] == ev->data1 || gamecontrol[i][1] == ev->data1)
break;
}
if (i == num_gamecontrols)
return false;
}
#ifndef NONET
if (!chat_on)
{
// enter chat mode
if ((ev->data1 == gamecontrol[gc_talkkey][0] || ev->data1 == gamecontrol[gc_talkkey][1])
&& netgame && !OLD_MUTE) // check for old chat mute, still let the players open the chat incase they want to scroll otherwise.
{
chat_on = true;
w_chat[0] = 0;
teamtalk = false;
chat_scrollmedown = true;
typelines = 1;
return true;
}
if ((ev->data1 == gamecontrol[gc_teamkey][0] || ev->data1 == gamecontrol[gc_teamkey][1])
&& netgame && !OLD_MUTE)
{
chat_on = true;
w_chat[0] = 0;
teamtalk = G_GametypeHasTeams(); // Don't teamtalk if we don't have teams.
chat_scrollmedown = true;
typelines = 1;
return true;
}
}
else // if chat_on
{
INT32 c = (INT32)ev->data1;
// Ignore modifier keys
// Note that we do this here so users can still set
// their chat keys to one of these, if they so desire.
if (ev->data1 == KEY_LSHIFT || ev->data1 == KEY_RSHIFT
|| ev->data1 == KEY_LCTRL || ev->data1 == KEY_RCTRL
|| ev->data1 == KEY_LALT || ev->data1 == KEY_RALT)
return true;
// Ignore non-keyboard keys, except when the talk key is bound
if (ev->data1 >= KEY_MOUSE1
&& (ev->data1 != gamecontrol[gc_talkkey][0]
&& ev->data1 != gamecontrol[gc_talkkey][1]))
return false;
c = CON_ShiftChar(c);
// pasting. pasting is cool. chat is a bit limited, though :(
if (((c == 'v' || c == 'V') && ctrldown) && !CHAT_MUTE)
{
const char *paste = I_ClipboardPaste();
size_t chatlen;
size_t pastelen;
// create a dummy string real quickly
if (paste == NULL)
return true;
chatlen = strlen(w_chat);
pastelen = strlen(paste);
if (chatlen+pastelen > HU_MAXMSGLEN)
return true; // we can't paste this!!
if (c_input >= strlen(w_chat)) // add it at the end of the string.
{
memcpy(&w_chat[chatlen], paste, pastelen); // copy all of that.
c_input += pastelen;
/*size_t i = 0;
for (;i<pastelen;i++)
{
HU_queueChatChar(paste[i]); // queue it so that it's actually sent. (this chat write thing is REALLY messy.)
}*/
return true;
}
else // otherwise, we need to shift everything and make space, etc etc
{
size_t i = HU_MAXMSGLEN-1;
while (i >= c_input)
{
if (w_chat[i])
w_chat[i+pastelen] = w_chat[i];
if (i == 0) // prevent overflow
break;
i--;
}
memcpy(&w_chat[c_input], paste, pastelen); // copy all of that.
c_input += pastelen;
return true;
}
}
if (!CHAT_MUTE && HU_keyInChatString(w_chat,c))
{
HU_queueChatChar(c);
}
if (c == KEY_ENTER)
{
chat_on = false;
c_input = 0; // reset input cursor
chat_scrollmedown = true; // you hit enter, so you might wanna autoscroll to see what you just sent. :)
}
else if (c == KEY_ESCAPE
|| ((c == gamecontrol[gc_talkkey][0] || c == gamecontrol[gc_talkkey][1]
|| c == gamecontrol[gc_teamkey][0] || c == gamecontrol[gc_teamkey][1])
&& c >= KEY_MOUSE1)) // If it's not a keyboard key, then the chat button is used as a toggle.
{
chat_on = false;
c_input = 0; // reset input cursor
}
else if ((c == KEY_UPARROW || c == KEY_MOUSEWHEELUP) && chat_scroll > 0 && !OLDCHAT) // CHAT SCROLLING YAYS!
{
chat_scroll--;
justscrolledup = true;
chat_scrolltime = 4;
}
else if ((c == KEY_DOWNARROW || c == KEY_MOUSEWHEELDOWN) && chat_scroll < chat_maxscroll && chat_maxscroll > 0 && !OLDCHAT)
{
chat_scroll++;
justscrolleddown = true;
chat_scrolltime = 4;
}
else if (c == KEY_LEFTARROW && c_input != 0 && !OLDCHAT) // i said go back
c_input--;
else if (c == KEY_RIGHTARROW && c_input < strlen(w_chat) && !OLDCHAT) // don't need to check for admin or w/e here since the chat won't ever contain anything if it's muted.
c_input++;
return true;
}
#endif
return false;
}
//======================================================================
// HEADS UP DRAWING
//======================================================================
#ifndef NONET
// Precompile a wordwrapped string to any given width.
// This is a muuuch better method than V_WORDWRAP.
// again stolen and modified a bit from video.c, don't mind me, will need to rearrange this one day.
// this one is simplified for the chat drawer.
static char *CHAT_WordWrap(INT32 x, INT32 w, INT32 option, const char *string)
{
INT32 c;
size_t chw, i, lastusablespace = 0;
size_t slen;
char *newstring = Z_StrDup(string);
INT32 spacewidth = (vid.width < 640) ? 8 : 4, charwidth = (vid.width < 640) ? 8 : 4;
slen = strlen(string);
x = 0;
for (i = 0; i < slen; ++i)
{
c = newstring[i];
if ((UINT8)c >= 0x80 && (UINT8)c <= 0x8F) //color parsing! -Inuyasha 2.16.09
continue;
if (c == '\n')
{
x = 0;
lastusablespace = 0;
continue;
}
if (!(option & V_ALLOWLOWERCASE))
c = toupper(c);
c -= HU_FONTSTART;
if (c < 0 || c >= HU_FONTSIZE || !hu_font[c])
{
chw = spacewidth;
lastusablespace = i;
}
else
chw = charwidth;
x += chw;
if (lastusablespace != 0 && x > w)
{
//CONS_Printf("Wrap at index %d\n", i);
newstring[lastusablespace] = '\n';
i = lastusablespace+1;
lastusablespace = 0;
x = 0;
}
}
return newstring;
}
// 30/7/18: chaty is now the distance at which the lowest point of the chat will be drawn if that makes any sense.
INT16 chatx = 13, chaty = 169; // let's use this as our coordinates, shh
// chat stuff by VincyTM LOL XD!
// HU_DrawMiniChat
static void HU_drawMiniChat(void)
{
INT32 x = chatx+2;
INT32 charwidth = 4, charheight = 6;
INT32 boxw = cv_chatwidth.value;
INT32 dx = 0, dy = 0;
size_t i = chat_nummsg_min;
boolean prev_linereturn = false; // a hack to prevent double \n while I have no idea why they happen in the first place.
INT32 msglines = 0;
// process all messages once without rendering anything or doing anything fancy so that we know how many lines each message has...
INT32 y;
if (!chat_nummsg_min)
return; // needless to say it's useless to do anything if we don't have anything to draw.
if (splitscreen > 1)
boxw = max(64, boxw/2);
for (; i>0; i--)
{
char *msg = CHAT_WordWrap(x+2, boxw-(charwidth*2), V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_ALLOWLOWERCASE, chat_mini[i-1]);
size_t j = 0;
INT32 linescount = 0;
while(msg[j]) // iterate through msg
{
if (msg[j] < HU_FONTSTART) // don't draw
{
if (msg[j] == '\n') // get back down.
{
++j;
if (!prev_linereturn)
{
linescount += 1;
dx = 0;
}
prev_linereturn = true;
continue;
}
else if (msg[j] & 0x80) // stolen from video.c, nice.
{
++j;
continue;
}
++j;
}
else
{
j++;
}
prev_linereturn = false;
dx += charwidth;
if (dx >= boxw)
{
dx = 0;
linescount += 1;
}
}
dy = 0;
dx = 0;
msglines += linescount+1;
if (msg)
Z_Free(msg);
}
y = chaty - charheight*(msglines+1);
#ifdef NETSPLITSCREEN
if (splitscreen)
{
y -= BASEVIDHEIGHT/2;
if (splitscreen > 1)
y += 16;
}
else
#endif
y -= (cv_kartspeedometer.value ? 16 : 0);
dx = 0;
dy = 0;
i = 0;
prev_linereturn = false;
for (; i<=(chat_nummsg_min-1); i++) // iterate through our hot messages
{
INT32 clrflag = 0;
INT32 timer = ((cv_chattime.value*TICRATE)-chat_timers[i]) - cv_chattime.value*TICRATE+9; // see below...
INT32 transflag = (timer >= 0 && timer <= 9) ? (timer*V_10TRANS) : 0; // you can make bad jokes out of this one.
size_t j = 0;
char *msg = CHAT_WordWrap(x+2, boxw-(charwidth*2), V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_ALLOWLOWERCASE, chat_mini[i]); // get the current message, and word wrap it.
UINT8 *colormap = NULL;
while(msg[j]) // iterate through msg
{
if (msg[j] < HU_FONTSTART) // don't draw
{
if (msg[j] == '\n') // get back down.
{
++j;
if (!prev_linereturn)
{
dy += charheight;
dx = 0;
}
prev_linereturn = true;
continue;
}
else if (msg[j] & 0x80) // stolen from video.c, nice.
{
clrflag = ((msg[j] & 0x7f) << V_CHARCOLORSHIFT) & V_CHARCOLORMASK;
colormap = V_GetStringColormap(clrflag);
++j;
continue;
}
++j;
}
else
{
if (cv_chatbacktint.value) // on request of wolfy
V_DrawFillConsoleMap(x + dx + 2, y+dy, charwidth, charheight, 239|V_SNAPTOBOTTOM|V_SNAPTOLEFT);
V_DrawChatCharacter(x + dx + 2, y+dy, msg[j++] |V_SNAPTOBOTTOM|V_SNAPTOLEFT|transflag, !cv_allcaps.value, colormap);
}
dx += charwidth;
prev_linereturn = false;
if (dx >= boxw)
{
dx = 0;
dy += charheight;
}
}
dy += charheight;
dx = 0;
if (msg)
Z_Free(msg);
}
// decrement addy and make that shit smooth:
addy /= 2;
}
// HU_DrawChatLog
static void HU_drawChatLog(INT32 offset)
{
INT32 charwidth = 4, charheight = 6;
INT32 boxw = cv_chatwidth.value, boxh = cv_chatheight.value;
INT32 x = chatx+2, y, dx = 0, dy = 0;
UINT32 i = 0;
INT32 chat_topy, chat_bottomy;
INT32 highlight = HU_GetHighlightColor();
boolean atbottom = false;
// make sure that our scroll position isn't "illegal";
if (chat_scroll > chat_maxscroll)
chat_scroll = chat_maxscroll;
#ifdef NETSPLITSCREEN
if (splitscreen)
{
boxh = max(6, boxh/2);
if (splitscreen > 1)
boxw = max(64, boxw/2);
}
#endif
y = chaty - offset*charheight - (chat_scroll*charheight) - boxh*charheight - 12;
#ifdef NETSPLITSCREEN
if (splitscreen)
{
y -= BASEVIDHEIGHT/2;
if (splitscreen > 1)
y += 16;
}
else
#endif
y -= (cv_kartspeedometer.value ? 16 : 0);
chat_topy = y + chat_scroll*charheight;
chat_bottomy = chat_topy + boxh*charheight;
V_DrawFillConsoleMap(chatx, chat_topy, boxw, boxh*charheight +2, 239|V_SNAPTOBOTTOM|V_SNAPTOLEFT); // log box
for (i=0; i<chat_nummsg_log; i++) // iterate through our chatlog
{
INT32 clrflag = 0;
INT32 j = 0;
char *msg = CHAT_WordWrap(x+2, boxw-(charwidth*2), V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_ALLOWLOWERCASE, chat_log[i]); // get the current message, and word wrap it.
UINT8 *colormap = NULL;
while(msg[j]) // iterate through msg
{
if (msg[j] < HU_FONTSTART) // don't draw
{
if (msg[j] == '\n') // get back down.
{
++j;
dy += charheight;
dx = 0;
continue;
}
else if (msg[j] & 0x80) // stolen from video.c, nice.
{
clrflag = ((msg[j] & 0x7f) << V_CHARCOLORSHIFT) & V_CHARCOLORMASK;
colormap = V_GetStringColormap(clrflag);
++j;
continue;
}
++j;
}
else
{
if ((y+dy+2 >= chat_topy) && (y+dy < (chat_bottomy)))
V_DrawChatCharacter(x + dx + 2, y+dy+2, msg[j++] |V_SNAPTOBOTTOM|V_SNAPTOLEFT, !cv_allcaps.value, colormap);
else
j++; // don't forget to increment this or we'll get stuck in the limbo.
}
dx += charwidth;
if (dx >= boxw-charwidth-2 && i<chat_nummsg_log && msg[j] >= HU_FONTSTART) // end of message shouldn't count, nor should invisible characters!!!!
{
dx = 0;
dy += charheight;
}
}
dy += charheight;
dx = 0;
if (msg)
Z_Free(msg);
}
if (((chat_scroll >= chat_maxscroll) || (chat_scrollmedown)) && !(justscrolleddown || justscrolledup || chat_scrolltime)) // was already at the bottom of the page before new maxscroll calculation and was NOT scrolling.
{
atbottom = true; // we should scroll
}
chat_scrollmedown = false;
// getmaxscroll through a lazy hack. We do all these loops, so let's not do more loops that are gonna lag the game more. :P
chat_maxscroll = (dy/charheight); // welcome to C, we don't know what min() and max() are.
if (chat_maxscroll <= (UINT32)boxh)
chat_maxscroll = 0;
else
chat_maxscroll -= boxh;
// if we're not bound by the time, autoscroll for next frame:
if (atbottom)
chat_scroll = chat_maxscroll;
// draw arrows to indicate that we can (or not) scroll.
if (chat_scroll > 0)
V_DrawCharacter(chatx-9, ((justscrolledup) ? (chat_topy-1) : (chat_topy)), V_SNAPTOBOTTOM | V_SNAPTOLEFT | highlight | '\x1A', false); // up arrow
if (chat_scroll < chat_maxscroll)
V_DrawCharacter(chatx-9, chat_bottomy-((justscrolleddown) ? 5 : 6), V_SNAPTOBOTTOM | V_SNAPTOLEFT | highlight | '\x1B', false); // down arrow
justscrolleddown = false;
justscrolledup = false;
}
//
// HU_DrawChat
//
// Draw chat input
//
static void HU_DrawChat(void)
{
INT32 charwidth = 4, charheight = 6;
INT32 boxw = cv_chatwidth.value;
INT32 t = 0, c = 0, y = chaty - (typelines*charheight);
UINT32 i = 0, saylen = strlen(w_chat); // You learn new things everyday!
INT32 cflag = 0;
const char *ntalk = "Say: ", *ttalk = "Team: ";
const char *talk = ntalk;
const char *mute = "Chat has been muted.";
#ifdef NETSPLITSCREEN
if (splitscreen)
{
y -= BASEVIDHEIGHT/2;
if (splitscreen > 1)
{
y += 16;
boxw = max(64, boxw/2);
}
}
else
#endif
y -= (cv_kartspeedometer.value ? 16 : 0);
if (teamtalk)
{
talk = ttalk;
#if 0
if (players[consoleplayer].ctfteam == 1)
t = 0x500; // Red
else if (players[consoleplayer].ctfteam == 2)
t = 0x400; // Blue
#endif
}
if (CHAT_MUTE)
{
talk = mute;
typelines = 1;
cflag = V_GRAYMAP; // set text in gray if chat is muted.
}
V_DrawFillConsoleMap(chatx, y-1, boxw, (typelines*charheight), 239 | V_SNAPTOBOTTOM | V_SNAPTOLEFT);
while (talk[i])
{
if (talk[i] < HU_FONTSTART)
++i;
else
{
V_DrawChatCharacter(chatx + c + 2, y, talk[i] |V_SNAPTOBOTTOM|V_SNAPTOLEFT|cflag, !cv_allcaps.value, V_GetStringColormap(talk[i]|cflag));
i++;
}
c += charwidth;
}
// if chat is muted, just draw the log and get it over with, no need to draw anything else.
if (CHAT_MUTE)
{
HU_drawChatLog(0);
return;
}
i = 0;
typelines = 1;
if ((strlen(w_chat) == 0 || c_input == 0) && hu_tick < 4)
V_DrawChatCharacter(chatx + 2 + c, y+1, '_' |V_SNAPTOBOTTOM|V_SNAPTOLEFT|t, !cv_allcaps.value, NULL);
while (w_chat[i])
{
boolean skippedline = false;
if (c_input == (i+1))
{
INT32 cursorx = (c+charwidth < boxw-charwidth) ? (chatx + 2 + c+charwidth) : (chatx+1); // we may have to go down.
INT32 cursory = (cursorx != chatx+1) ? (y) : (y+charheight);
if (hu_tick < 4)
V_DrawChatCharacter(cursorx, cursory+1, '_' |V_SNAPTOBOTTOM|V_SNAPTOLEFT|t, !cv_allcaps.value, NULL);
if (cursorx == chatx+1 && saylen == i) // a weirdo hack
{
typelines += 1;
skippedline = true;
}
}
//Hurdler: isn't it better like that?
if (w_chat[i] < HU_FONTSTART)
++i;
else
V_DrawChatCharacter(chatx + c + 2, y, w_chat[i++] | V_SNAPTOBOTTOM|V_SNAPTOLEFT | t, !cv_allcaps.value, NULL);
c += charwidth;
if (c > boxw-(charwidth*2) && !skippedline)
{
c = 0;
y += charheight;
typelines += 1;
}
}
// handle /pm list. It's messy, horrible and I don't care.
if (strnicmp(w_chat, "/pm", 3) == 0 && vid.width >= 400 && !teamtalk) // 320x200 unsupported kthxbai
{
INT32 count = 0;
INT32 p_dispy = chaty - charheight -1;
#ifdef NETSPLITSCREEN
if (splitscreen)
{
p_dispy -= BASEVIDHEIGHT/2;
if (splitscreen > 1)
p_dispy += 16;
}
else
#endif
p_dispy -= (cv_kartspeedometer.value ? 16 : 0);
i = 0;
for(i=0; (i<MAXPLAYERS); i++)
{
// filter: (code needs optimization pls help I'm bad with C)
if (w_chat[3])
{
char *nodenum;
UINT32 n;
// right, that's half important: (w_chat[4] may be a space since /pm0 msg is perfectly acceptable!)
if ( ( ((w_chat[3] != 0) && ((w_chat[3] < '0') || (w_chat[3] > '9'))) || ((w_chat[4] != 0) && (((w_chat[4] < '0') || (w_chat[4] > '9'))))) && (w_chat[4] != ' '))
break;
nodenum = (char*) malloc(3);
strncpy(nodenum, w_chat+3, 3);
n = atoi((const char*) nodenum); // turn that into a number
free(nodenum);
// special cases:
if ((n == 0) && !(w_chat[4] == '0'))
{
if (!(i<10))
continue;
}
else if ((n == 1) && !(w_chat[3] == '0'))
{
if (!((i == 1) || ((i >= 10) && (i <= 19))))
continue;
}
else if ((n == 2) && !(w_chat[3] == '0'))
{
if (!((i == 2) || ((i >= 20) && (i <= 29))))
continue;
}
else if ((n == 3) && !(w_chat[3] == '0'))
{
if (!((i == 3) || ((i >= 30) && (i <= 31))))
continue;
}
else // general case.
{
if (i != n)
continue;
}
}
if (playeringame[i])
{
char name[MAXPLAYERNAME+1];
strlcpy(name, player_names[i], 7); // shorten name to 7 characters.
V_DrawFillConsoleMap(chatx+ boxw + 2, p_dispy- (6*count), 48, 6, 239 | V_SNAPTOBOTTOM | V_SNAPTOLEFT); // fill it like the chat so the text doesn't become hard to read because of the hud.
V_DrawSmallString(chatx+ boxw + 4, p_dispy- (6*count), V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_ALLOWLOWERCASE, va("\x82%d\x80 - %s", i, name));
count++;
}
}
if (count == 0) // no results.
{
V_DrawFillConsoleMap(chatx+boxw+2, p_dispy- (6*count), 48, 6, 239 | V_SNAPTOBOTTOM | V_SNAPTOLEFT); // fill it like the chat so the text doesn't become hard to read because of the hud.
V_DrawSmallString(chatx+boxw+4, p_dispy- (6*count), V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_ALLOWLOWERCASE, "NO RESULT.");
}
}
HU_drawChatLog(typelines-1); // typelines is the # of lines we're typing. If there's more than 1 then the log should scroll up to give us more space.
}
// For anyone who, for some godforsaken reason, likes oldchat.
static void HU_DrawChat_Old(void)
{
INT32 t = 0, c = 0, y = HU_INPUTY;
size_t i = 0;
const char *ntalk = "Say: ", *ttalk = "Say-Team: ";
const char *talk = ntalk;
INT32 charwidth = 8 * con_scalefactor; //SHORT(hu_font['A'-HU_FONTSTART]->width) * con_scalefactor;
INT32 charheight = 8 * con_scalefactor; //SHORT(hu_font['A'-HU_FONTSTART]->height) * con_scalefactor;
if (teamtalk)
{
talk = ttalk;
#if 0
if (players[consoleplayer].ctfteam == 1)
t = 0x500; // Red
else if (players[consoleplayer].ctfteam == 2)
t = 0x400; // Blue
#endif
}
while (talk[i])
{
if (talk[i] < HU_FONTSTART)
{
++i;
//charwidth = 4 * con_scalefactor;
}
else
{
//charwidth = SHORT(hu_font[talk[i]-HU_FONTSTART]->width) * con_scalefactor;
V_DrawCharacter(HU_INPUTX + c, y, talk[i++] | cv_constextsize.value | V_NOSCALESTART, !cv_allcaps.value);
}
c += charwidth;
}
if ((strlen(w_chat) == 0 || c_input == 0) && hu_tick < 4)
V_DrawCharacter(HU_INPUTX+c, y+2*con_scalefactor, '_' |cv_constextsize.value | V_NOSCALESTART|t, !cv_allcaps.value);
i = 0;
while (w_chat[i])
{
if (c_input == (i+1) && hu_tick < 4)
{
INT32 cursorx = (HU_INPUTX+c+charwidth < vid.width) ? (HU_INPUTX + c + charwidth) : (HU_INPUTX); // we may have to go down.
INT32 cursory = (cursorx != HU_INPUTX) ? (y) : (y+charheight);
V_DrawCharacter(cursorx, cursory+2*con_scalefactor, '_' |cv_constextsize.value | V_NOSCALESTART|t, !cv_allcaps.value);
}
//Hurdler: isn't it better like that?
if (w_chat[i] < HU_FONTSTART)
{
++i;
//charwidth = 4 * con_scalefactor;
}
else
{
//charwidth = SHORT(hu_font[w_chat[i]-HU_FONTSTART]->width) * con_scalefactor;
V_DrawCharacter(HU_INPUTX + c, y, w_chat[i++] | cv_constextsize.value | V_NOSCALESTART | t, !cv_allcaps.value);
}
c += charwidth;
if (c >= vid.width)
{
c = 0;
y += charheight;
}
}
}
#endif
// draw the Crosshair, at the exact center of the view.
//
// Crosshairs are pre-cached at HU_Init
/*static inline void HU_DrawCrosshair(void)
{
INT32 i, x, y;
i = cv_crosshair.value & 3;
if (!i)
return;
if ((netgame || multiplayer) && players[displayplayers[0]].spectator)
return;
#ifdef HWRENDER
if (rendermode != render_soft)
{
x = (INT32)gr_basewindowcenterx;
y = (INT32)gr_basewindowcentery;
}
else
#endif
{
x = viewwindowx + (viewwidth>>1);
y = viewwindowy + (viewheight>>1);
}
V_DrawScaledPatch(x, y, V_NOSCALESTART|V_OFFSET|V_TRANSLUCENT, crosshair[i - 1]);
}
static inline void HU_DrawCrosshair2(void)
{
INT32 i, x, y;
i = cv_crosshair2.value & 3;
if (!i)
return;
if ((netgame || multiplayer) && players[displayplayers[1]].spectator)
return;
#ifdef HWRENDER
if (rendermode != render_soft)
{
x = (INT32)gr_basewindowcenterx;
y = (INT32)gr_basewindowcentery;
}
else
#endif
{
x = viewwindowx + (viewwidth>>1);
y = viewwindowy + (viewheight>>1);
}
if (splitscreen)
{
if (splitscreen > 1)
#ifdef HWRENDER
if (rendermode != render_soft)
x += (INT32)gr_viewwidth;
else
#endif
x += viewwidth;
else
{
#ifdef HWRENDER
if (rendermode != render_soft)
y += (INT32)gr_viewheight;
else
#endif
y += viewheight;
}
V_DrawScaledPatch(x, y, V_NOSCALESTART|V_OFFSET|V_TRANSLUCENT, crosshair[i - 1]);
}
}
static inline void HU_DrawCrosshair3(void)
{
INT32 i, x, y;
i = cv_crosshair3.value & 3;
if (!i)
return;
if ((netgame || multiplayer) && players[displayplayers[2]].spectator)
return;
#ifdef HWRENDER
if (rendermode != render_soft)
{
x = (INT32)gr_basewindowcenterx;
y = (INT32)gr_basewindowcentery;
}
else
#endif
{
x = viewwindowx + (viewwidth>>1);
y = viewwindowy + (viewheight>>1);
}
if (splitscreen > 1)
{
#ifdef HWRENDER
if (rendermode != render_soft)
y += (INT32)gr_viewheight;
else
#endif
y += viewheight;
V_DrawScaledPatch(x, y, V_NOSCALESTART|V_OFFSET|V_TRANSLUCENT, crosshair[i - 1]);
}
}
static inline void HU_DrawCrosshair4(void)
{
INT32 i, x, y;
i = cv_crosshair4.value & 3;
if (!i)
return;
if ((netgame || multiplayer) && players[displayplayers[3]].spectator)
return;
#ifdef HWRENDER
if (rendermode != render_soft)
{
x = (INT32)gr_basewindowcenterx;
y = (INT32)gr_basewindowcentery;
}
else
#endif
{
x = viewwindowx + (viewwidth>>1);
y = viewwindowy + (viewheight>>1);
}
if (splitscreen > 2)
{
#ifdef HWRENDER
if (rendermode != render_soft)
{
x += (INT32)gr_viewwidth;
y += (INT32)gr_viewheight;
}
else
#endif
{
x += viewwidth;
y += viewheight;
}
V_DrawScaledPatch(x, y, V_NOSCALESTART|V_OFFSET|V_TRANSLUCENT, crosshair[i - 1]);
}
}*/
static void HU_DrawCEcho(void)
{
INT32 i = 0;
INT32 y = (BASEVIDHEIGHT/2)-4;
INT32 pnumlines = 0;
UINT32 realflags = cechoflags;
INT32 realalpha = (INT32)((cechoflags & V_ALPHAMASK) >> V_ALPHASHIFT);
char *line;
char *echoptr;
char temp[1024];
for (i = 0; cechotext[i] != '\0'; ++i)
if (cechotext[i] == '\\')
pnumlines++;
y -= (pnumlines-1)*((realflags & V_RETURN8) ? 4 : 6);
// Prevent crashing because I'm sick of this
if (y < 0)
{
CONS_Alert(CONS_WARNING, "CEcho contained too many lines, not displaying\n");
cechotimer = 0;
return;
}
// Automatic fadeout
if (realflags & V_AUTOFADEOUT)
{
UINT32 tempalpha = (UINT32)max((INT32)(10 - cechotimer), realalpha);
realflags &= ~V_ALPHASHIFT;
realflags |= (tempalpha << V_ALPHASHIFT);
}
strcpy(temp, cechotext);
echoptr = &temp[0];
while (*echoptr != '\0')
{
line = strchr(echoptr, '\\');
if (line == NULL)
break;
*line = '\0';
V_DrawCenteredString(BASEVIDWIDTH/2, y, realflags, echoptr);
y += ((realflags & V_RETURN8) ? 8 : 12);
echoptr = line;
echoptr++;
}
}
//
// demo info stuff
//
UINT32 hu_demotime;
UINT32 hu_demolap;
static void HU_DrawDemoInfo(void)
{
if (!multiplayer)/* netreplay */
{
V_DrawCenteredString((BASEVIDWIDTH/2), BASEVIDHEIGHT-40, 0, M_GetText("Replay:"));
V_DrawCenteredString((BASEVIDWIDTH/2), BASEVIDHEIGHT-32, V_ALLOWLOWERCASE, player_names[0]);
}
else
{
V_DrawRightAlignedThinString(BASEVIDWIDTH-2, BASEVIDHEIGHT-10, V_ALLOWLOWERCASE, demo.titlename);
}
if (modeattacking)
{
V_DrawRightAlignedString((BASEVIDWIDTH/2)-4, BASEVIDHEIGHT-24, V_YELLOWMAP|V_MONOSPACE, "BEST TIME:");
if (hu_demotime != UINT32_MAX)
V_DrawString((BASEVIDWIDTH/2)+4, BASEVIDHEIGHT-24, V_MONOSPACE, va("%i'%02i\"%02i",
G_TicsToMinutes(hu_demotime,true),
G_TicsToSeconds(hu_demotime),
G_TicsToCentiseconds(hu_demotime)));
else
V_DrawString((BASEVIDWIDTH/2)+4, BASEVIDHEIGHT-24, V_MONOSPACE, "--'--\"--");
V_DrawRightAlignedString((BASEVIDWIDTH/2)-4, BASEVIDHEIGHT-16, V_YELLOWMAP|V_MONOSPACE, "BEST LAP:");
if (hu_demolap != UINT32_MAX)
V_DrawString((BASEVIDWIDTH/2)+4, BASEVIDHEIGHT-16, V_MONOSPACE, va("%i'%02i\"%02i",
G_TicsToMinutes(hu_demolap,true),
G_TicsToSeconds(hu_demolap),
G_TicsToCentiseconds(hu_demolap)));
else
V_DrawString((BASEVIDWIDTH/2)+4, BASEVIDHEIGHT-16, V_MONOSPACE, "--'--\"--");
}
}
//
// Song credits
//
void HU_DrawSongCredits(void)
{
char *str;
INT32 len, destx;
INT32 y = (splitscreen ? (BASEVIDHEIGHT/2)-4 : 32);
INT32 bgt;
if (!cursongcredit.def) // No def
return;
str = va("\x1F"" %s", cursongcredit.def->source);
len = V_ThinStringWidth(str, V_ALLOWLOWERCASE|V_6WIDTHSPACE);
destx = (len+7);
if (cursongcredit.anim)
{
if (cursongcredit.trans > 0)
cursongcredit.trans--;
if (cursongcredit.x < destx)
cursongcredit.x += (destx - cursongcredit.x) / 2;
if (cursongcredit.x > destx)
cursongcredit.x = destx;
cursongcredit.anim--;
}
else
{
if (cursongcredit.trans < NUMTRANSMAPS)
cursongcredit.trans++;
if (cursongcredit.x > 0)
cursongcredit.x /= 2;
if (cursongcredit.x < 0)
cursongcredit.x = 0;
}
bgt = (NUMTRANSMAPS/2)+(cursongcredit.trans/2);
if (bgt < NUMTRANSMAPS)
V_DrawScaledPatch(cursongcredit.x, y-2, V_SNAPTOLEFT|(bgt<<V_ALPHASHIFT), songcreditbg);
if (cursongcredit.trans < NUMTRANSMAPS)
V_DrawRightAlignedThinString(cursongcredit.x, y, V_ALLOWLOWERCASE|V_6WIDTHSPACE|V_SNAPTOLEFT|(cursongcredit.trans<<V_ALPHASHIFT), str);
}
// Heads up displays drawer, call each frame
//
void HU_Drawer(void)
{
if (cv_vhseffect.value && (paused || (demo.playback && cv_playbackspeed.value > 1)))
V_DrawVhsEffect(demo.rewinding);
#ifndef NONET
// draw chat string plus cursor
if (chat_on)
{
if (!OLDCHAT)
HU_DrawChat();
else
HU_DrawChat_Old(); // why the fuck.........................
}
else
{
typelines = 1;
chat_scrolltime = 0;
if (!OLDCHAT && cv_consolechat.value < 2 && netgame) // Don't display minimized chat if you set the mode to Window (Hidden)
HU_drawMiniChat(); // draw messages in a cool fashion.
}
#endif
if (cechotimer)
HU_DrawCEcho();
if (!( Playing() || demo.playback )
|| gamestate == GS_INTERMISSION || gamestate == GS_CUTSCENE
|| gamestate == GS_CREDITS || gamestate == GS_EVALUATION
|| gamestate == GS_GAMEEND
|| gamestate == GS_VOTING || gamestate == GS_WAITINGPLAYERS) // SRB2kart
return;
// draw multiplayer rankings
if (hu_showscores)
{
if (netgame || multiplayer)
{
#ifdef HAVE_BLUA
if (LUA_HudEnabled(hud_rankings))
#endif
HU_DrawRankings();
#ifdef HAVE_BLUA
if (renderisnewtic)
{
LUA_HUD_ClearDrawList(luahuddrawlist_scores);
LUAh_ScoresHUD(luahuddrawlist_scores);
}
LUA_HUD_DrawList(luahuddrawlist_scores);
#endif
}
if (demo.playback)
{
HU_DrawDemoInfo();
}
}
if (gamestate != GS_LEVEL)
return;
// draw the crosshair, not when viewing demos nor with chasecam
/*if (!automapactive && !demo.playback)
{
if (cv_crosshair.value && !camera[0].chase && !players[displayplayers[0]].spectator)
HU_DrawCrosshair();
if (cv_crosshair2.value && !camera[1].chase && !players[displayplayers[1]].spectator)
HU_DrawCrosshair2();
if (cv_crosshair3.value && !camera[2].chase && !players[displayplayers[2]].spectator)
HU_DrawCrosshair3();
if (cv_crosshair4.value && !camera[3].chase && !players[displayplayers[3]].spectator)
HU_DrawCrosshair4();
}*/
// draw song credits
if (cv_songcredits.value)
HU_DrawSongCredits();
// draw desynch text
if (hu_resynching)
{
char resynch_text[14];
UINT32 i;
strcpy(resynch_text, "Resynching");
for (i = 0; i < (resynch_ticker / 16) % 4; i++)
strcat(resynch_text, ".");
V_DrawCenteredString(BASEVIDWIDTH/2, 180, V_YELLOWMAP | V_ALLOWLOWERCASE, resynch_text);
}
}
//======================================================================
// HUD MESSAGES CLEARING FROM SCREEN
//======================================================================
// Clear old messages from the borders around the view window
// (only for reduced view, refresh the borders when needed)
//
// startline: y coord to start clear,
// clearlines: how many lines to clear.
//
static INT32 oldclearlines;
void HU_Erase(void)
{
INT32 topline, bottomline;
INT32 y, yoffset;
#ifdef HWRENDER
// clear hud msgs on double buffer (OpenGL mode)
boolean secondframe;
static INT32 secondframelines;
#endif
if (con_clearlines == oldclearlines && !con_hudupdate && !chat_on)
return;
#ifdef HWRENDER
// clear the other frame in double-buffer modes
secondframe = (con_clearlines != oldclearlines);
if (secondframe)
secondframelines = oldclearlines;
#endif
// clear the message lines that go away, so use _oldclearlines_
bottomline = oldclearlines;
oldclearlines = con_clearlines;
if (chat_on && OLDCHAT)
if (bottomline < 8)
bottomline = 8; // only do it for consolechat. consolechat is gay.
if (automapactive || viewwindowx == 0) // hud msgs don't need to be cleared
return;
// software mode copies view border pattern & beveled edges from the backbuffer
if (rendermode == render_soft)
{
topline = 0;
for (y = topline, yoffset = y*vid.width; y < bottomline; y++, yoffset += vid.width)
{
if (y < viewwindowy || y >= viewwindowy + viewheight)
R_VideoErase(yoffset, vid.width); // erase entire line
else
{
R_VideoErase(yoffset, viewwindowx); // erase left border
// erase right border
R_VideoErase(yoffset + viewwindowx + viewwidth, viewwindowx);
}
}
con_hudupdate = false; // if it was set..
}
#ifdef HWRENDER
else if (rendermode != render_none)
{
// refresh just what is needed from the view borders
HWR_DrawViewBorder(secondframelines);
con_hudupdate = secondframe;
}
#endif
}
//======================================================================
// IN-LEVEL MULTIPLAYER RANKINGS
//======================================================================
//
// HU_drawPing
//
void HU_drawPing(INT32 x, INT32 y, UINT32 ping, INT32 flags)
{
INT32 gfxnum = 4; // gfx to draw
UINT8 const *colormap = R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_SALMON, GTC_CACHE);
if (ping < 76)
gfxnum = 0;
else if (ping < 137)
gfxnum = 1;
else if (ping < 256)
gfxnum = 2;
else if (ping < 500)
gfxnum = 3;
V_DrawScaledPatch(x, y, flags, pinggfx[gfxnum]);
if (servermaxping && ping > servermaxping && hu_tick < 4) // flash ping red if too high
V_DrawPingNum(x, y+9, flags, ping, colormap);
else
V_DrawPingNum(x, y+9, flags, ping, NULL);
}
//
// HU_DrawTabRankings -- moved to k_kart.c
//
//
// HU_DrawTeamTabRankings
//
/*void HU_DrawTeamTabRankings(playersort_t *tab, INT32 whiteplayer)
{
INT32 i,x,y;
INT32 redplayers = 0, blueplayers = 0;
boolean smol = false;
const UINT8 *colormap;
char name[MAXPLAYERNAME+1];
// before we draw, we must count how many players are in each team. It makes an additional loop, but we need to know if we have to draw a big or a small ranking.
for (i = 0; i < MAXPLAYERS; i++)
{
if (players[tab[i].num].spectator)
continue; //ignore them.
if (tab[i].color == skincolor_redteam) //red
{
if (redplayers++ > 8)
{
smol = true;
break; // don't make more loops than we need to.
}
}
else if (tab[i].color == skincolor_blueteam) //blue
{
if (blueplayers++ > 8)
{
smol = true;
break;
}
}
else //er? not on red or blue, so ignore them
continue;
}
// I'll be blunt with you, this may add more lines, but I'm not adding weird cases for this, so we're executing a separate function.
if (smol == true || cv_compactscoreboard.value)
{
HU_Draw32TeamTabRankings(tab, whiteplayer);
return;
}
V_DrawFill(160, 26, 1, 154, 0); //Draw a vertical line to separate the two teams.
V_DrawFill(1, 26, 318, 1, 0); //And a horizontal line to make a T.
V_DrawFill(1, 180, 318, 1, 0); //And a horizontal line near the bottom.
i=0, redplayers=0, blueplayers=0;
for (i = 0; i < MAXPLAYERS; i++)
{
if (players[tab[i].num].spectator)
continue; //ignore them.
if (tab[i].color == skincolor_redteam) //red
{
if (redplayers++ > 8)
continue;
x = 32 + (BASEVIDWIDTH/2);
y = (redplayers * 16) + 16;
}
else if (tab[i].color == skincolor_blueteam) //blue
{
if (blueplayers++ > 8)
continue;
x = 32;
y = (blueplayers * 16) + 16;
}
else //er? not on red or blue, so ignore them
continue;
strlcpy(name, tab[i].name, 7);
V_DrawString(x + 20, y,
((tab[i].num == whiteplayer) ? V_YELLOWMAP : 0)
| ((players[tab[i].num].health > 0) ? 0 : V_TRANSLUCENT)
| V_ALLOWLOWERCASE, name);
if (gametype == GT_CTF)
{
if (players[tab[i].num].gotflag & GF_REDFLAG) // Red
V_DrawSmallScaledPatch(x-28, y-4, 0, rflagico);
else if (players[tab[i].num].gotflag & GF_BLUEFLAG) // Blue
V_DrawSmallScaledPatch(x-28, y-4, 0, bflagico);
}
// Draw emeralds
if (!players[tab[i].num].powers[pw_super]
|| ((leveltime/7) & 1))
{
HU_DrawEmeralds(x-12,y+2,tab[i].emeralds);
}
if (players[tab[i].num].powers[pw_super])
{
colormap = R_GetTranslationColormap(players[tab[i].num].skin, players[tab[i].num].mo ? players[tab[i].num].mo->color : tab[i].color, GTC_CACHE);
V_DrawSmallMappedPatch (x, y-4, 0, facewantprefix[players[tab[i].num].skin], colormap);
}
else
{
colormap = R_GetTranslationColormap(players[tab[i].num].skin, players[tab[i].num].mo ? players[tab[i].num].mo->color : tab[i].color, GTC_CACHE);
if (players[tab[i].num].health <= 0)
V_DrawSmallTranslucentMappedPatch (x, y-4, 0, facerankprefix[players[tab[i].num].skin], colormap);
else
V_DrawSmallMappedPatch (x, y-4, 0, facerankprefix[players[tab[i].num].skin], colormap);
}
V_DrawRightAlignedThinString(x+120, y-1, ((players[tab[i].num].health > 0) ? 0 : V_TRANSLUCENT), va("%u", tab[i].count));
if (!splitscreen)
{
if (!(tab[i].num == serverplayer))
HU_drawPing(x+ 113, y+2, playerpingtable[tab[i].num], false);
}
V_DrawRightAlignedThinString(x+100, y, ((players[tab[i].num].health > 0) ? 0 : V_TRANSLUCENT), va("%u", tab[i].count));
if (!splitscreen)
{
if (!(tab[i].num == serverplayer))
HU_drawPing(x+ 113, y+2, playerpingtable[tab[i].num], false);
//else
// V_DrawSmallString(x+ 94, y+4, V_YELLOWMAP, "SERVER");
}
}
}
//
// HU_DrawDualTabRankings
//
void HU_DrawDualTabRankings(INT32 x, INT32 y, playersort_t *tab, INT32 scorelines, INT32 whiteplayer)
{
INT32 i;
const UINT8 *colormap;
char name[MAXPLAYERNAME+1];
V_DrawFill(160, 26, 1, 154, 0); //Draw a vertical line to separate the two sides.
V_DrawFill(1, 26, 318, 1, 0); //And a horizontal line to make a T.
V_DrawFill(1, 180, 318, 1, 0); //And a horizontal line near the bottom.
for (i = 0; i < scorelines; i++)
{
if (players[tab[i].num].spectator)
continue; //ignore them.
strlcpy(name, tab[i].name, 7);
if (!(tab[i].num == serverplayer))
HU_drawPing(x+ 113, y+2, playerpingtable[tab[i].num], false);
//else
// V_DrawSmallString(x+ 94, y+4, V_YELLOWMAP, "SERVER");
V_DrawString(x + 20, y,
((tab[i].num == whiteplayer) ? V_YELLOWMAP : 0)
| ((players[tab[i].num].health > 0) ? 0 : V_TRANSLUCENT)
| V_ALLOWLOWERCASE, name);
if (G_GametypeUsesLives()) //show lives
V_DrawRightAlignedString(x, y+4, V_ALLOWLOWERCASE, va("%dx", players[tab[i].num].lives));
else if (G_TagGametype() && players[tab[i].num].pflags & PF_TAGIT)
V_DrawSmallScaledPatch(x-28, y-4, 0, tagico);
// Draw emeralds
if (!players[tab[i].num].powers[pw_super]
|| ((leveltime/7) & 1))
{
HU_DrawEmeralds(x-12,y+2,tab[i].emeralds);
}
//V_DrawSmallScaledPatch (x, y-4, 0, livesback);
if (tab[i].color == 0)
{
colormap = colormaps;
if (players[tab[i].num].powers[pw_super])
V_DrawSmallScaledPatch (x, y-4, 0, facewantprefix[players[tab[i].num].skin]);
else
{
if (players[tab[i].num].health <= 0)
V_DrawSmallTranslucentPatch (x, y-4, 0, facerankprefix[players[tab[i].num].skin]);
else
V_DrawSmallScaledPatch (x, y-4, 0, facerankprefix[players[tab[i].num].skin]);
}
}
else
{
if (players[tab[i].num].powers[pw_super])
{
colormap = R_GetTranslationColormap(players[tab[i].num].skin, players[tab[i].num].mo ? players[tab[i].num].mo->color : tab[i].color, GTC_CACHE);
V_DrawSmallMappedPatch (x, y-4, 0, facewantprefix[players[tab[i].num].skin], colormap);
}
else
{
colormap = R_GetTranslationColormap(players[tab[i].num].skin, players[tab[i].num].mo ? players[tab[i].num].mo->color : tab[i].color, GTC_CACHE);
if (players[tab[i].num].health <= 0)
V_DrawSmallTranslucentMappedPatch (x, y-4, 0, facerankprefix[players[tab[i].num].skin], colormap);
else
V_DrawSmallMappedPatch (x, y-4, 0, facerankprefix[players[tab[i].num].skin], colormap);
}
}
// All data drawn with thin string for space.
if (G_RaceGametype())
{
if (circuitmap)
{
if (players[tab[i].num].exiting)
V_DrawRightAlignedThinString(x+146, y, 0, va("%i:%02i.%02i", G_TicsToMinutes(players[tab[i].num].realtime,true), G_TicsToSeconds(players[tab[i].num].realtime), G_TicsToCentiseconds(players[tab[i].num].realtime)));
else
V_DrawRightAlignedThinString(x+146, y, ((players[tab[i].num].health > 0) ? 0 : V_TRANSLUCENT), va("%u", tab[i].count));
}
else
V_DrawRightAlignedThinString(x+146, y, ((players[tab[i].num].health > 0) ? 0 : V_TRANSLUCENT), va("%i:%02i.%02i", G_TicsToMinutes(tab[i].count,true), G_TicsToSeconds(tab[i].count), G_TicsToCentiseconds(tab[i].count)));
}
else
V_DrawRightAlignedThinString(x+100, y, ((players[tab[i].num].health > 0) ? 0 : V_TRANSLUCENT), va("%u", tab[i].count));
y += 16;
if (y > 160)
{
y = 32;
x += BASEVIDWIDTH/2;
}
}
}
//
// HU_Draw32TabRankings
//
static void HU_Draw32TabRankings(INT32 x, INT32 y, playersort_t *tab, INT32 scorelines, INT32 whiteplayer)
{
INT32 i;
const UINT8 *colormap;
char name[MAXPLAYERNAME+1];
V_DrawFill(160, 26, 1, 154, 0); //Draw a vertical line to separate the two sides.
V_DrawFill(1, 26, 318, 1, 0); //And a horizontal line to make a T.
V_DrawFill(1, 180, 318, 1, 0); //And a horizontal line near the bottom.
for (i = 0; i < scorelines; i++)
{
if (players[tab[i].num].spectator)
continue; //ignore them.
strlcpy(name, tab[i].name, 7);
if (!splitscreen) // don't draw it on splitscreen,
{
if (!(tab[i].num == serverplayer))
HU_drawPing(x+ 135, y+3, playerpingtable[tab[i].num], true);
//else
// V_DrawSmallString(x+ 129, y+4, V_YELLOWMAP, "HOST");
}
V_DrawString(x + 10, y,
((tab[i].num == whiteplayer) ? V_YELLOWMAP : 0)
| ((players[tab[i].num].health > 0) ? 0 : V_TRANSLUCENT)
| V_ALLOWLOWERCASE, name);
if (G_GametypeUsesLives()) //show lives
V_DrawRightAlignedThinString(x-1, y, V_ALLOWLOWERCASE, va("%d", players[tab[i].num].lives));
else if (G_TagGametype() && players[tab[i].num].pflags & PF_TAGIT)
V_DrawFixedPatch((x-10)*FRACUNIT, (y)*FRACUNIT, FRACUNIT/4, 0, tagico, 0);
// Draw emeralds
if (!players[tab[i].num].powers[pw_super]
|| ((leveltime/7) & 1))
{
HU_Draw32Emeralds(x+60, y+2, tab[i].emeralds);
//HU_DrawEmeralds(x-12,y+2,tab[i].emeralds);
}
//V_DrawSmallScaledPatch (x, y-4, 0, livesback);
if (tab[i].color == 0)
{
colormap = colormaps;
if (players[tab[i].num].powers[pw_super])
V_DrawFixedPatch(x*FRACUNIT, y*FRACUNIT, FRACUNIT/4, 0, superprefix[players[tab[i].num].skin], 0);
else
{
if (players[tab[i].num].health <= 0)
V_DrawFixedPatch(x*FRACUNIT, (y)*FRACUNIT, FRACUNIT/4, V_HUDTRANSHALF, faceprefix[players[tab[i].num].skin], 0);
else
V_DrawFixedPatch(x*FRACUNIT, (y)*FRACUNIT, FRACUNIT/4, 0, faceprefix[players[tab[i].num].skin], 0);
}
}
else
{
if (players[tab[i].num].powers[pw_super])
{
colormap = R_GetTranslationColormap(players[tab[i].num].skin, players[tab[i].num].mo ? players[tab[i].num].mo->color : tab[i].color, GTC_CACHE);
V_DrawFixedPatch(x*FRACUNIT, y*FRACUNIT, FRACUNIT/4, 0, superprefix[players[tab[i].num].skin], colormap);
}
else
{
colormap = R_GetTranslationColormap(players[tab[i].num].skin, players[tab[i].num].mo ? players[tab[i].num].mo->color : tab[i].color, GTC_CACHE);
if (players[tab[i].num].health <= 0)
V_DrawFixedPatch(x*FRACUNIT, (y)*FRACUNIT, FRACUNIT/4, V_HUDTRANSHALF, faceprefix[players[tab[i].num].skin], colormap);
else
V_DrawFixedPatch(x*FRACUNIT, (y)*FRACUNIT, FRACUNIT/4, 0, faceprefix[players[tab[i].num].skin], colormap);
}
}
// All data drawn with thin string for space.
if (gametype == GT_RACE)
{
if (circuitmap)
{
if (players[tab[i].num].exiting)
V_DrawRightAlignedThinString(x+128, y, 0, va("%i:%02i.%02i", G_TicsToMinutes(players[tab[i].num].realtime,true), G_TicsToSeconds(players[tab[i].num].realtime), G_TicsToCentiseconds(players[tab[i].num].realtime)));
else
V_DrawRightAlignedThinString(x+128, y, ((players[tab[i].num].health > 0) ? 0 : V_TRANSLUCENT), va("%u", tab[i].count));
}
else
V_DrawRightAlignedThinString(x+128, y, ((players[tab[i].num].health > 0) ? 0 : V_TRANSLUCENT), va("%i:%02i.%02i", G_TicsToMinutes(tab[i].count,true), G_TicsToSeconds(tab[i].count), G_TicsToCentiseconds(tab[i].count)));
}
else
V_DrawRightAlignedThinString(x+128, y, ((players[tab[i].num].health > 0) ? 0 : V_TRANSLUCENT), va("%u", tab[i].count));
y += 9;
if (i == 16)
{
y = 32;
x += BASEVIDWIDTH/2;
}
}
}
//
// HU_DrawEmeralds
//
void HU_DrawEmeralds(INT32 x, INT32 y, INT32 pemeralds)
{
//Draw the emeralds, in the CORRECT order, using tiny emerald sprites.
if (pemeralds & EMERALD1)
V_DrawSmallScaledPatch(x , y-6, 0, tinyemeraldpics[0]);
if (pemeralds & EMERALD2)
V_DrawSmallScaledPatch(x+4, y-3, 0, tinyemeraldpics[1]);
if (pemeralds & EMERALD3)
V_DrawSmallScaledPatch(x+4, y+3, 0, tinyemeraldpics[2]);
if (pemeralds & EMERALD4)
V_DrawSmallScaledPatch(x , y+6, 0, tinyemeraldpics[3]);
if (pemeralds & EMERALD5)
V_DrawSmallScaledPatch(x-4, y+3, 0, tinyemeraldpics[4]);
if (pemeralds & EMERALD6)
V_DrawSmallScaledPatch(x-4, y-3, 0, tinyemeraldpics[5]);
if (pemeralds & EMERALD7)
V_DrawSmallScaledPatch(x, y, 0, tinyemeraldpics[6]);
}*/
//
// HU_DrawSpectatorTicker
//
static inline void HU_DrawSpectatorTicker(void)
{
INT32 i;
INT32 length = 0, height = 174;
INT32 totallength = 0, templength = -8;
INT32 dupadjust = (vid.width/vid.dupx), duptweak = (dupadjust - BASEVIDWIDTH)/2;
for (i = 0; i < MAXPLAYERS; i++)
if (playeringame[i] && players[i].spectator)
totallength += (signed)strlen(player_names[i]) * 8 + 16;
length -= (leveltime % (totallength + dupadjust+8));
length += dupadjust;
for (i = 0; i < MAXPLAYERS; i++)
if (playeringame[i] && players[i].spectator)
{
char *pos;
char initial[MAXPLAYERNAME+1];
char current[MAXPLAYERNAME+1];
INT32 len;
len = ((signed)strlen(player_names[i]) * 8 + 16);
strcpy(initial, player_names[i]);
pos = initial;
if (length >= -len)
{
if (length < -8)
{
UINT8 eatenchars = (UINT8)(abs(length) / 8);
if (eatenchars <= strlen(initial))
{
// Eat one letter off the left side,
// then compensate the drawing position.
pos += eatenchars;
strcpy(current, pos);
templength = ((length + 8) % 8);
}
else
{
strcpy(current, " ");
templength = length;
}
}
else
{
strcpy(current, initial);
templength = length;
}
V_DrawString(templength - duptweak, height, V_TRANSLUCENT|V_ALLOWLOWERCASE, current);
}
if ((length += len) >= dupadjust+8)
break;
}
}
//
// HU_DrawRankings
//
static void HU_DrawRankings(void)
{
patch_t *p;
playersort_t tab[MAXPLAYERS];
INT32 i, j, scorelines, hilicol, numplayersingame = 0;
boolean completed[MAXPLAYERS];
UINT32 whiteplayer = MAXPLAYERS;
V_DrawFadeScreen(0xFF00, 16); // A little more readable, and prevents cheating the fades under other circumstances.
if (cons_menuhighlight.value)
hilicol = cons_menuhighlight.value;
else if (modeattacking)
hilicol = V_ORANGEMAP;
else
hilicol = ((gametype == GT_RACE) ? V_SKYMAP : V_REDMAP);
// draw the current gametype in the lower right
if (modeattacking)
V_DrawString(4, 188, hilicol|V_SNAPTOBOTTOM|V_SNAPTOLEFT, "Record Attack");
else
V_DrawString(4, 188, hilicol|V_SNAPTOBOTTOM|V_SNAPTOLEFT, gametype_cons_t[gametype].strvalue);
if (G_GametypeHasTeams())
{
if (gametype == GT_CTF)
p = bflagico;
else
p = bmatcico;
V_DrawSmallScaledPatch(128 - SHORT(p->width)/4, 4, 0, p);
V_DrawCenteredString(128, 16, 0, va("%u", bluescore));
if (gametype == GT_CTF)
p = rflagico;
else
p = rmatcico;
V_DrawSmallScaledPatch(192 - SHORT(p->width)/4, 4, 0, p);
V_DrawCenteredString(192, 16, 0, va("%u", redscore));
}
if (!G_RaceGametype())
{
if (cv_timelimit.value && timelimitintics > 0)
{
UINT32 timeval = (timelimitintics + starttime + 1 - leveltime);
if (timeval > timelimitintics+1)
timeval = timelimitintics;
timeval /= TICRATE;
if (leveltime <= (timelimitintics + starttime))
{
V_DrawCenteredString(64, 8, 0, "TIME LEFT");
V_DrawCenteredString(64, 16, hilicol, va("%u", timeval));
}
// overtime
if (!players[consoleplayer].exiting && (leveltime > (timelimitintics + starttime + TICRATE/2)) && cv_overtime.value)
{
V_DrawCenteredString(64, 8, 0, "TIME LEFT");
V_DrawCenteredString(64, 16, hilicol, "OVERTIME");
}
}
if (cv_pointlimit.value > 0)
{
V_DrawCenteredString(256, 8, 0, "POINT LIMIT");
V_DrawCenteredString(256, 16, hilicol, va("%d", cv_pointlimit.value));
}
}
/*else if (gametype == GT_COOP)
{
INT32 totalscore = 0;
for (i = 0; i < MAXPLAYERS; i++)
{
if (playeringame[i])
totalscore += players[i].score;
}
V_DrawCenteredString(256, 8, 0, "TOTAL SCORE");
V_DrawCenteredString(256, 16, 0, va("%u", totalscore));
}*/
else
{
if (circuitmap)
{
V_DrawCenteredString(64, 8, 0, "LAP COUNT");
V_DrawCenteredString(64, 16, hilicol, va("%d", cv_numlaps.value));
}
V_DrawCenteredString(256, 8, 0, "GAME SPEED");
V_DrawCenteredString(256, 16, hilicol, cv_kartspeed.string);
}
// When you play, you quickly see your score because your name is displayed in white.
// When playing back a demo, you quickly see who's the view.
if (!splitscreen)
whiteplayer = demo.playback ? displayplayers[0] : consoleplayer;
scorelines = 0;
memset(completed, 0, sizeof (completed));
memset(tab, 0, sizeof (playersort_t)*MAXPLAYERS);
for (i = 0; i < MAXPLAYERS; i++)
{
tab[i].num = -1;
tab[i].name = NULL;
tab[i].count = INT32_MAX;
if (!playeringame[i] || players[i].spectator || !players[i].mo)
continue;
numplayersingame++;
}
for (j = 0; j < numplayersingame; j++)
{
UINT8 lowestposition = MAXPLAYERS+1;
for (i = 0; i < MAXPLAYERS; i++)
{
if (completed[i] || !playeringame[i] || players[i].spectator || !players[i].mo)
continue;
if (players[i].kartstuff[k_position] >= lowestposition)
continue;
tab[scorelines].num = i;
lowestposition = players[i].kartstuff[k_position];
}
i = tab[scorelines].num;
completed[i] = true;
tab[scorelines].name = player_names[i];
if (G_RaceGametype())
{
if (circuitmap)
tab[scorelines].count = players[i].laps+1;
else
tab[scorelines].count = players[i].realtime;
}
else
tab[scorelines].count = players[i].marescore;
scorelines++;
#if MAXPLAYERS > 16
if (scorelines > 16)
break; //dont draw past bottom of screen, show the best only
#endif
}
/*if (G_GametypeHasTeams())
HU_DrawTeamTabRankings(tab, whiteplayer); //separate function for Spazzo's silly request -- gotta fix this up later
else if (scorelines > 10)*/
HU_DrawTabRankings(((scorelines > 8) ? 32 : 40), 33, tab, scorelines, whiteplayer, hilicol);
/*else
HU_DrawDualTabRankings(32, 32, tab, scorelines, whiteplayer);*/
// draw spectators in a ticker across the bottom
if (netgame && G_GametypeHasSpectators())
HU_DrawSpectatorTicker();
}
// Interface to CECHO settings for the outside world, avoiding the
// expense (and security problems) of going via the console buffer.
void HU_ClearCEcho(void)
{
cechotimer = 0;
}
void HU_SetCEchoDuration(INT32 seconds)
{
cechoduration = seconds * TICRATE;
}
void HU_SetCEchoFlags(INT32 flags)
{
// Don't allow cechoflags to contain any bits in V_PARAMMASK
cechoflags = (flags & ~V_PARAMMASK);
}
void HU_DoCEcho(const char *msg)
{
I_OutputMsg("%s\n", msg); // print to log
strncpy(cechotext, msg, sizeof(cechotext));
strncat(cechotext, "\\", sizeof(cechotext) - strlen(cechotext) - 1);
cechotext[sizeof(cechotext) - 1] = '\0';
cechotimer = cechoduration;
}