Files
bearstonem--openmohaa-vr/code/cgame/cg_drawtools.cpp
T
bearstonem d9968210aa fix(vr): correct visual glitches and reduce rendering overhead
Restore desktop HUD behavior and objective state, fix stereo text lifetimes, HUD placement and colors, sprite culling, and font shader passes. Handle swapchain timeouts and incomplete stereo frames safely, remove forced GPU waits, and correct rendering timing attribution.

Track client-game source additions in CMake. Validated desktop and Android GL1/GL2 builds plus targeted regression and sanitizer checks. Headset appearance and performance remain unverified.
2026-10-14 14:48:22 -07:00

1723 lines
50 KiB
C++

/*
===========================================================================
Copyright (C) 2025 the OpenMoHAA team
This file is part of OpenMoHAA source code.
OpenMoHAA source code is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of the License,
or (at your option) any later version.
OpenMoHAA source code is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with OpenMoHAA source code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
===========================================================================
*/
// DESCRIPTION:
// Some tools used to drawing 2d stuff
#include "cg_local.h"
/*
================
CG_AdjustFrom640
Adjusted for resolution and screen aspect ratio
================
*/
void CG_AdjustFrom640(float *x, float *y, float *w, float *h)
{
#if 0
// adjust for wide screens
if ( cgs.glconfig.vidWidth * 480 > cgs.glconfig.vidHeight * 640 ) {
*x += 0.5 * ( cgs.glconfig.vidWidth - ( cgs.glconfig.vidHeight * 640 / 480 ) );
}
#endif
// scale for screen sizes
*x *= cgs.screenXScale;
*y *= cgs.screenYScale;
*w *= cgs.screenXScale;
*h *= cgs.screenYScale;
}
/*
=============
CG_TileClearBox
This repeats a 64*64 tile graphic to fill the screen around a sized down
refresh window.
=============
*/
void CG_TileClearBox(int x, int y, int w, int h, qhandle_t hShader)
{
float s1, t1, s2, t2;
s1 = x / 64.0;
t1 = y / 64.0;
s2 = (x + w) / 64.0;
t2 = (y + h) / 64.0;
cgi.R_DrawStretchPic(x, y, w, h, s1, t1, s2, t2, hShader);
}
/*
==============
CG_TileClear
Clear around a sized down screen
==============
*/
void CG_TileClear(void)
{
int top, bottom, left, right;
int w, h;
w = cgs.glconfig.vidWidth;
h = cgs.glconfig.vidHeight;
if (cg.refdef.x == 0 && cg.refdef.y == 0 && cg.refdef.width == w && cg.refdef.height == h) {
return; // full screen rendering
}
top = cg.refdef.y;
bottom = top + cg.refdef.height - 1;
left = cg.refdef.x;
right = left + cg.refdef.width - 1;
// clear above view screen
CG_TileClearBox(0, 0, w, top, cgs.media.backTileShader);
// clear below view screen
CG_TileClearBox(0, bottom, w, h - bottom, cgs.media.backTileShader);
// clear left of view screen
CG_TileClearBox(0, top, left, bottom - top + 1, cgs.media.backTileShader);
// clear right of view screen
CG_TileClearBox(right, top, w - right, bottom - top + 1, cgs.media.backTileShader);
}
/*
===============================================================================
LAGOMETER
===============================================================================
*/
#define LAG_SAMPLES 128
typedef struct {
int frameSamples[LAG_SAMPLES];
int frameCount;
int snapshotFlags[LAG_SAMPLES];
int snapshotSamples[LAG_SAMPLES];
int snapshotCount;
} lagometer_t;
lagometer_t lagometer;
/*
==============
CG_AddLagometerFrameInfo
Adds the current interpolate / extrapolate bar for this frame
==============
*/
void CG_AddLagometerFrameInfo(void)
{
int offset;
offset = cg.time - cg.latestSnapshotTime;
lagometer.frameSamples[lagometer.frameCount & (LAG_SAMPLES - 1)] = offset;
lagometer.frameCount++;
}
/*
==============
CG_AddLagometerSnapshotInfo
Each time a snapshot is received, log its ping time and
the number of snapshots that were dropped before it.
Pass NULL for a dropped packet.
==============
*/
void CG_AddLagometerSnapshotInfo(snapshot_t *snap)
{
// dropped packet
if (!snap) {
lagometer.snapshotSamples[lagometer.snapshotCount & (LAG_SAMPLES - 1)] = -1;
lagometer.snapshotCount++;
return;
}
// add this snapshot's info
lagometer.snapshotSamples[lagometer.snapshotCount & (LAG_SAMPLES - 1)] = snap->ping;
lagometer.snapshotFlags[lagometer.snapshotCount & (LAG_SAMPLES - 1)] = snap->snapFlags;
lagometer.snapshotCount++;
}
/*
==============
CG_DrawDisconnect
==============
*/
void CG_DrawDisconnect(void)
{
float x, y;
float w, h;
int cmdNum;
qhandle_t handle;
usercmd_t cmd;
// draw the phone jack if we are completely past our buffers
cmdNum = cgi.GetCurrentCmdNumber() - CMD_BACKUP + 1;
cgi.GetUserCmd(cmdNum, &cmd);
if (!cg.snap || cmd.serverTime <= cg.snap->ps.commandTime
|| cmd.serverTime > cg.time) { // special check for map_restart
return;
}
// blink it
if ((cg.time >> 9) & 1) {
return;
}
handle = cgi.R_RegisterShader("gfx/2d/net.tga");
w = cgi.R_GetShaderWidth(handle) * cgs.uiHiResScale[0];
h = cgi.R_GetShaderHeight(handle) * cgs.uiHiResScale[1];
x = ((float)cgs.glconfig.vidWidth - w) * 0.5;
y = (float)cgs.glconfig.vidHeight - h;
cgi.R_DrawStretchPic(x, y, w, h, 0, 0, 1, 1, handle);
}
#define MAX_LAGOMETER_PING 900
#define MAX_LAGOMETER_RANGE 300
/*
==============
CG_DrawLagometer
==============
*/
void CG_DrawLagometer(void)
{
int a, i;
float v;
float ax, ay, aw, ah, mid, range;
int color;
float vscale;
if (!cg_lagometer->integer) {
CG_DrawDisconnect();
return;
}
//
// draw the graph
//
ax = 272.0;
ay = 432.0;
aw = 96.0;
ah = 48.0;
CG_AdjustFrom640(&ax, &ay, &aw, &ah);
cgi.R_SetColor(NULL);
cgi.R_DrawStretchPic(ax, ay, aw, ah, 0, 0, 1, 1, cgs.media.lagometerShader);
color = -1;
range = ah / 3;
mid = ay + range;
vscale = range / MAX_LAGOMETER_RANGE;
// draw the frame interpoalte / extrapolate graph
for (a = 0; a < aw; a++) {
i = (lagometer.frameCount - 1 - a) & (LAG_SAMPLES - 1);
v = lagometer.frameSamples[i];
v *= vscale;
if (v > 0) {
if (color != 1) {
color = 1;
cgi.R_SetColor(g_color_table[ColorIndex(COLOR_YELLOW)]);
}
if (v > range) {
v = range;
}
cgi.R_DrawBox(ax + aw - a, mid - v, 1, v);
} else if (v < 0) {
if (color != 2) {
color = 2;
cgi.R_SetColor(g_color_table[ColorIndex(COLOR_BLUE)]);
}
v = -v;
if (v > range) {
v = range;
}
cgi.R_DrawBox(ax + aw - a, mid, 1, v);
}
}
// draw the snapshot latency / drop graph
range = ah / 2;
vscale = range / MAX_LAGOMETER_PING;
for (a = 0; a < aw; a++) {
i = (lagometer.snapshotCount - 1 - a) & (LAG_SAMPLES - 1);
v = lagometer.snapshotSamples[i];
if (v > 0) {
if (lagometer.snapshotFlags[i] & SNAPFLAG_RATE_DELAYED) {
if (color != 5) {
color = 5; // YELLOW for rate delay
cgi.R_SetColor(g_color_table[ColorIndex(COLOR_YELLOW)]);
}
} else {
if (color != 3) {
color = 3;
cgi.R_SetColor(g_color_table[ColorIndex(COLOR_GREEN)]);
}
}
v = v * vscale;
if (v > range) {
v = range;
}
cgi.R_DrawBox(ax + aw - a, ay + ah - v, 1, v);
} else if (v < 0) {
if (color != 4) {
color = 4; // RED for dropped snapshots
cgi.R_SetColor(g_color_table[ColorIndex(COLOR_RED)]);
}
cgi.R_DrawBox(ax + aw - a, ay + ah - range, 1, range);
}
}
cgi.R_SetColor(NULL);
CG_DrawDisconnect();
}
static void CG_DrawPauseIcon()
{
qhandle_t handle;
float x, y, w, h;
if (paused->integer) {
handle = cgs.media.pausedShader;
} else {
if (cg.predicted_player_state.pm_flags & PMF_LEVELEXIT) {
// blink it
if ((cg.time >> 9) & 1) {
return;
}
handle = cgs.media.levelExitShader;
} else {
return;
}
}
w = cgi.R_GetShaderWidth(handle);
h = cgi.R_GetShaderHeight(handle);
if (cg.snap && cg.snap->ps.blend[3] > 0) {
y = cgs.glconfig.vidHeight * 0.45f - h / 2.f;
} else {
y = cgs.glconfig.vidHeight * 0.75f - h / 2.f;
}
x = (cgs.glconfig.vidWidth - w) / 2.f;
cgi.R_SetColor(colorWhite);
cgi.R_DrawStretchPic(x, y, w * cgs.uiHiResScale[0], h * cgs.uiHiResScale[1], 0, 0, 1, 1, handle);
}
static void CG_DrawServerLag()
{
float x, y;
float w, h;
qhandle_t handle;
if (cgs.gametype == GT_SINGLE_PLAYER) {
// Fixed in 2.0
// Don't draw server lag in single-player mode
return;
}
if (!cg_drawsvlag->integer) {
return;
}
if (!developer->integer) {
// Changed in 2.0
// Svlag should only be drawn when developer mode is enabled
return;
}
if (!cgs.serverLagTime) {
return;
}
if (cg.time - cgs.serverLagTime > 3000) {
return;
}
// blink it
if ((cg.time >> 9) & 1) {
return;
}
handle = cgi.R_RegisterShader("gfx/2d/slowserver");
w = (float)cgi.R_GetShaderWidth(handle) * cgs.uiHiResScale[0] / 4;
h = (float)cgi.R_GetShaderHeight(handle) * cgs.uiHiResScale[1] / 4;
x = ((float)cgs.glconfig.vidWidth - w) / 2;
y = (float)cgs.glconfig.vidHeight - h;
cgi.R_DrawStretchPic(x, y, w, h, 0.0, 0.0, 1.0, 1.0, handle);
}
/*
==============
CG_DrawIcons
==============
*/
void CG_DrawIcons(void)
{
if (!cg_hud->integer) {
return;
}
CG_DrawPauseIcon();
CG_DrawServerLag();
}
void CG_DrawOverlayTopBottom(qhandle_t handleTop, qhandle_t handleBottom, float fAlpha)
{
int iHalfWidth;
int iWidthOffset;
vec4_t color;
color[0] = 1.0;
color[1] = 1.0;
color[2] = 1.0;
color[3] = fAlpha;
cgi.R_SetColor(color);
iHalfWidth = cgs.glconfig.vidHeight >> 1;
iWidthOffset = (cgs.glconfig.vidWidth - cgs.glconfig.vidHeight) >> 1;
cgi.R_DrawStretchPic(iWidthOffset, 0.0, iHalfWidth, iHalfWidth, 1.0, 0.0, 0.0, 1.0, handleTop);
cgi.R_DrawStretchPic(iWidthOffset + iHalfWidth, 0.0, iHalfWidth, iHalfWidth, 0.0, 0.0, 1.0, 1.0, handleTop);
cgi.R_DrawStretchPic(iWidthOffset, iHalfWidth, iHalfWidth, iHalfWidth, 1.0, 0.0, 0.0, 1.0, handleBottom);
cgi.R_DrawStretchPic(
iWidthOffset + iHalfWidth, iHalfWidth, iHalfWidth, iHalfWidth, 0.0, 0.0, 1.0, 1.0, handleBottom
);
color[0] = 0.0;
color[1] = 0.0;
color[2] = 0.0;
cgi.R_SetColor(color);
cgi.R_DrawStretchPic(0.0, 0.0, iWidthOffset, cgs.glconfig.vidHeight, 0.0, 0.0, 1.0, 1.0, cgs.media.lagometerShader);
cgi.R_DrawStretchPic(
cgs.glconfig.vidWidth - iWidthOffset,
0.0,
iWidthOffset,
cgs.glconfig.vidHeight,
0.0,
0.0,
1.0,
1.0,
cgs.media.lagometerShader
);
}
void CG_DrawOverlayMiddle(qhandle_t handle, float fAlpha)
{
int iHalfWidth;
int iWidthOffset;
vec4_t color;
color[0] = 1.0;
color[1] = 1.0;
color[2] = 1.0;
color[3] = fAlpha;
cgi.R_SetColor(color);
iHalfWidth = cgs.glconfig.vidHeight >> 1;
iWidthOffset = (cgs.glconfig.vidWidth - cgs.glconfig.vidHeight) >> 1;
cgi.R_DrawStretchPic(iWidthOffset, 0.0, iHalfWidth, iHalfWidth, 0.0, 0.0, 1.0, 1.0, handle);
cgi.R_DrawStretchPic(iWidthOffset + iHalfWidth, 0.0, iHalfWidth, iHalfWidth, 1.0, 0.0, 0.0, 1.0, handle);
cgi.R_DrawStretchPic(iWidthOffset, iHalfWidth, iHalfWidth, iHalfWidth, 0.0, 1.0, 1.0, 0.0, handle);
cgi.R_DrawStretchPic(iWidthOffset + iHalfWidth, iHalfWidth, iHalfWidth, iHalfWidth, 1.0, 1.0, 0.0, 0.0, handle);
color[0] = 0.0;
color[1] = 0.0;
color[2] = 0.0;
cgi.R_SetColor(color);
cgi.R_DrawStretchPic(0.0, 0.0, iWidthOffset, cgs.glconfig.vidHeight, 0.0, 0.0, 1.0, 1.0, cgs.media.lagometerShader);
cgi.R_DrawStretchPic(
cgs.glconfig.vidWidth - iWidthOffset,
0.0,
iWidthOffset,
cgs.glconfig.vidHeight,
.0,
0.0,
1.0,
1.0,
cgs.media.lagometerShader
);
}
void CG_DrawOverlayFullScreen(qhandle_t handle, float fAlpha)
{
int iHalfWidth, iHalfHeight;
vec4_t color;
color[0] = 1.0;
color[1] = 1.0;
color[2] = 1.0;
color[3] = fAlpha;
cgi.R_SetColor(color);
iHalfHeight = cgs.glconfig.vidHeight >> 1;
iHalfWidth = cgs.glconfig.vidWidth >> 1;
cgi.R_DrawStretchPic(0.0, 0.0, iHalfWidth, iHalfHeight, 0.0, 0.0, 1.0, 1.0, handle);
cgi.R_DrawStretchPic(iHalfWidth, 0.0, iHalfWidth, iHalfHeight, 1.0, 0.0, 0.0, 1.0, handle);
cgi.R_DrawStretchPic(0.0, iHalfHeight, iHalfWidth, iHalfHeight, 0.0, 1.0, 1.0, 0.0, handle);
cgi.R_DrawStretchPic(iHalfWidth, iHalfHeight, iHalfWidth, iHalfHeight, 1.0, 1.0, 0.0, 0.0, handle);
}
void CG_DrawZoomOverlay()
{
static int zoomType;
static float fAlpha;
const char *weaponstring;
qboolean bDrawOverlay;
weaponstring = "";
bDrawOverlay = qtrue;
if (!cg.snap) {
return;
}
if (cg.snap->ps.activeItems[1] >= 0) {
weaponstring = CG_ConfigString(CS_WEAPONS + cg.snap->ps.activeItems[1]);
}
if (!Q_stricmp(weaponstring, "Spy Camera")) {
zoomType = 2;
} else if (!Q_stricmp(weaponstring, "Binoculars")) {
zoomType = 3;
} else {
if (cg.snap->ps.stats[STAT_INZOOM] && cg.snap->ps.stats[STAT_INZOOM] <= 30) {
if (!Q_stricmp(weaponstring, "KAR98 - Sniper")) {
zoomType = 1;
} else {
zoomType = 0;
}
} else {
bDrawOverlay = qfalse;
}
}
if (bDrawOverlay) {
fAlpha += cg.frametime * 0.015;
if (fAlpha > 1.0) {
fAlpha = 1.0;
}
} else {
fAlpha -= cg.frametime * 0.015;
if (fAlpha < 0.0) {
fAlpha = 0.0;
}
if (!fAlpha) {
return;
}
}
switch (zoomType) {
case 1:
CG_DrawOverlayTopBottom(cgs.media.kar98TopOverlayShader, cgs.media.kar98BottomOverlayShader, fAlpha);
break;
case 3:
CG_DrawOverlayFullScreen(cgs.media.binocularsOverlayShader, fAlpha);
break;
default:
CG_DrawOverlayMiddle(cgs.media.zoomOverlayShader, fAlpha);
break;
}
}
void CG_HudDrawShader(int iInfo)
{
if (cgi.HudDrawElements[iInfo].shaderName[0]) {
cgi.HudDrawElements[iInfo].hShader = cgi.R_RegisterShaderNoMip(cgi.HudDrawElements[iInfo].shaderName);
} else {
cgi.HudDrawElements[iInfo].hShader = 0;
}
}
void CG_HudDrawFont(int iInfo)
{
if (cgi.HudDrawElements[iInfo].fontName[0]) {
cgi.HudDrawElements[iInfo].pFont = cgi.R_LoadFont(cgi.HudDrawElements[iInfo].fontName);
} else {
cgi.HudDrawElements[iInfo].pFont = nullptr;
}
}
void CG_RefreshHudDrawElements()
{
int i;
for (i = 0; i < MAX_HUDDRAW_ELEMENTS; ++i) {
CG_HudDrawShader(i);
CG_HudDrawFont(i);
}
}
void CG_HudDrawElements()
{
int i;
float fX, fY;
float fWidth, fHeight;
vec2_t virtualScale;
if (!cg_huddraw_force->integer && !cg_hud->integer) {
return;
}
virtualScale[0] = cgs.glconfig.vidWidth / 640.0;
virtualScale[1] = cgs.glconfig.vidHeight / 480.0;
for (i = 0; i < MAX_HUDDRAW_ELEMENTS; i++) {
if ((!cgi.HudDrawElements[i].hShader && !cgi.HudDrawElements[i].string[0])
|| !cgi.HudDrawElements[i].vColor[3]) {
// skip invisible elements
continue;
}
fX = cgi.HudDrawElements[i].iX;
fY = cgi.HudDrawElements[i].iY;
fWidth = cgi.HudDrawElements[i].iWidth;
fHeight = cgi.HudDrawElements[i].iHeight;
if (!cgi.HudDrawElements[i].bVirtualScreen) {
fWidth *= cgs.uiHiResScale[0];
fHeight *= cgs.uiHiResScale[1];
fX *= cgs.uiHiResScale[0];
fY *= cgs.uiHiResScale[1];
}
if (cgi.HudDrawElements[i].iHorizontalAlign == HUD_ALIGN_X_CENTER) {
if (cgi.HudDrawElements[i].bVirtualScreen) {
fX += 320.0 - fWidth * 0.5;
} else {
fX += cgs.glconfig.vidWidth * 0.5 - fWidth * 0.5;
}
} else if (cgi.HudDrawElements[i].iHorizontalAlign == HUD_ALIGN_X_RIGHT) {
if (cgi.HudDrawElements[i].bVirtualScreen) {
fX += 640.0;
} else {
fX += cgs.glconfig.vidWidth;
}
}
if (cgi.HudDrawElements[i].iVerticalAlign == HUD_ALIGN_Y_CENTER) {
if (cgi.HudDrawElements[i].bVirtualScreen) {
fY += 240.0 - fHeight * 0.5;
} else {
fY += cgs.glconfig.vidHeight * 0.5 - fHeight * 0.5;
}
} else if (cgi.HudDrawElements[i].iVerticalAlign == HUD_ALIGN_Y_BOTTOM) {
if (cgi.HudDrawElements[i].bVirtualScreen) {
fY += 480.0;
} else {
fY += cgs.glconfig.vidHeight;
}
}
cgi.R_SetColor(cgi.HudDrawElements[i].vColor);
if (cgi.HudDrawElements[i].string[0]) {
fontheader_t *pFont = cgi.HudDrawElements[i].pFont;
if (!pFont) {
pFont = cgs.media.hudDrawFont;
}
if (cgi.HudDrawElements[i].bVirtualScreen) {
cgi.R_DrawString(pFont, cgi.LV_ConvertString(cgi.HudDrawElements[i].string), fX, fY, -1, virtualScale);
} else {
cgi.R_DrawString(
pFont,
cgi.LV_ConvertString(cgi.HudDrawElements[i].string),
fX / cgs.uiHiResScale[0],
fY / cgs.uiHiResScale[1],
-1,
cgs.uiHiResScale
);
}
} else {
if (cgi.HudDrawElements[i].bVirtualScreen) {
CG_AdjustFrom640(&fX, &fY, &fWidth, &fHeight);
}
cgi.R_DrawStretchPic(fX, fY, fWidth, fHeight, 0.0, 0.0, 1.0, 1.0, cgi.HudDrawElements[i].hShader);
}
}
}
void CG_InitializeObjectives()
{
int i;
cg.ObjectivesAlphaTime = 0.0;
cg.ObjectivesBaseAlpha = 0.0;
cg.ObjectivesDesiredAlpha = 0.0;
cg.ObjectivesCurrentAlpha = 0.0;
for (i = 0; i < MAX_OBJECTIVES; i++) {
// A new gamestate may already contain objectives (for example after
// loading a save). Subsequent changes arrive through configstrings.
CG_ProcessConfigString(CS_OBJECTIVES + i, qfalse);
}
}
void CG_DrawObjectives()
{
float vColor[4];
float fX, fY;
int iNumObjectives;
float fObjectivesTop;
static float fWidth;
float fHeight;
int iNumLines[20];
int iTotalNumLines;
int i;
int iCurrentObjective;
float fTimeDelta;
const char *pszLocalizedText;
const char *pszLine;
float fAlpha;
iTotalNumLines = 0;
iCurrentObjective = atoi(CG_ConfigString(CS_CURRENT_OBJECTIVE));
fTimeDelta = cg.ObjectivesAlphaTime - cg.time;
cg.ObjectivesCurrentAlpha = cg.ObjectivesDesiredAlpha;
if (fTimeDelta > 0) {
cg.ObjectivesCurrentAlpha =
(cg.ObjectivesBaseAlpha - cg.ObjectivesDesiredAlpha) * sin(fTimeDelta / (M_PI * 50.f + 2.f))
+ cg.ObjectivesDesiredAlpha;
}
//
// Added in OPM
// Full on the wrist panel, whatever the fade says. In the flat game the
// objectives are a hold-the-key overlay - CG_ScoresDown_f fades them in
// while the key is held - and a transient overlay on a panel that is
// itself only visible while the player is looking at their wrist would
// almost never be seen at all. Turning your wrist up *is* the gesture
// that asks for them, so the fade has nothing left to do there.
//
// Held in a local rather than written back to cg.ObjectivesCurrentAlpha:
// CG_DrawStopwatch reads that field to decide the stopwatch is covered
// and removes it, and CG_SetDesiredObjectiveAlpha animates from it.
//
fAlpha = cg.ObjectivesCurrentAlpha;
//
// Added in OPM
// Always up in VR, whatever the fade says. In the flat game the objectives
// are a hold-the-key overlay - CG_ScoresDown_f fades them in while the key
// is held - and there is no key to hold in a headset.
//
// Its own switch rather than a test on the draw pass. It was written as
// pass == 1 while the HUD lived on the wrist panel; when the HUD moved
// back into the eye buffers the pass became 0 and the objectives went
// straight back to invisible, which took a device run to notice.
//
if (vr_hudPass && vr_hudPass->integer && vr_objectivesAlways && vr_objectivesAlways->integer) {
fAlpha = 1.0f;
}
//
// Added in OPM
// The objectives were reported missing from the wrist panel and there are
// three quite different reasons they could be, so this says which. The
// pass names where it is drawing; alpha under 0.02 is the fade still
// holding them off; and a set count of zero means the level never sent any
// - which a map entered straight through vr_startMap, past the scripting
// that hands them out, is a very good way to arrange.
//
if (vr_traceObjectives && vr_traceObjectives->integer) {
static int lastReport;
if (!lastReport || cg.time - lastReport > 2000) {
int iSet = 0;
int k;
for (k = 0; k < MAX_OBJECTIVES; k++) {
if (cg.Objectives[k].flags != OBJ_FLAG_NONE && !(cg.Objectives[k].flags & OBJ_FLAG_HIDDEN)) {
iSet++;
}
}
lastReport = cg.time;
cgi.Printf(
"objectives: pass %d alpha %.2f (raw %.2f) set %d current %d\n",
vr_hudPass ? vr_hudPass->integer : -1,
fAlpha,
cg.ObjectivesCurrentAlpha,
iSet,
iCurrentObjective
);
}
}
if (fAlpha < 0.02) {
return;
}
// Added in 2.0
// Get the minimum Y value, it should be below the compass
fObjectivesTop = cgi.UI_GetObjectivesTop();
iNumObjectives = 0;
for (i = 0; i < MAX_OBJECTIVES; i++) {
if ((cg.Objectives[i].flags == OBJ_FLAG_NONE) || (cg.Objectives[i].flags & OBJ_FLAG_HIDDEN)) {
continue;
}
iNumObjectives++;
iNumLines[i] = 0;
pszLocalizedText = cgi.LV_ConvertString(cg.Objectives[i].text);
for (pszLine = strchr(pszLocalizedText, '\n'); pszLine; pszLine = strchr(pszLine + 1, '\n')) {
iNumLines[i]++;
}
iTotalNumLines += iNumLines[i];
}
fX = 25.0;
fY = fObjectivesTop + 5;
fWidth = (float)(iTotalNumLines * 12 + fObjectivesTop + iNumObjectives * 25 + 32) - fY;
vColor[2] = 0.2f;
vColor[1] = 0.2f;
vColor[0] = 0.2f;
vColor[3] = fAlpha * 0.75;
cgi.R_SetColor(vColor);
cgi.R_DrawStretchPic(
fX,
fY,
450.0 * cgs.uiHiResScale[0],
fWidth * cgs.uiHiResScale[1],
0.0,
0.0,
1.0,
1.0,
cgs.media.objectivesBackShader
);
fX = 30.0;
fY = fObjectivesTop + 10;
vColor[0] = 1.0;
vColor[1] = 1.0;
vColor[2] = 1.0;
vColor[3] = fAlpha;
cgi.R_SetColor(vColor);
cgi.R_DrawString(
cgs.media.objectiveFont,
cgi.LV_ConvertString("Mission Objectives:"),
fX,
fY / cgs.uiHiResScale[1],
-1,
cgs.uiHiResScale
);
fY = fY + 5.0;
cgi.R_DrawString(
cgs.media.objectiveFont,
"_______________________________________________________",
fX,
fY / cgs.uiHiResScale[1],
-1,
cgs.uiHiResScale
);
fHeight = fObjectivesTop + 35 * cgs.uiHiResScale[1];
for (i = 0; i < MAX_OBJECTIVES; ++i) {
qhandle_t hBoxShader;
if ((cg.Objectives[i].flags == OBJ_FLAG_NONE) || (cg.Objectives[i].flags & OBJ_FLAG_HIDDEN)) {
continue;
}
if ((cg.Objectives[i].flags & OBJ_FLAG_CURRENT) != 0) {
vColor[0] = 1.0;
vColor[1] = 1.0;
vColor[2] = 1.0;
vColor[3] = fAlpha;
hBoxShader = cgs.media.uncheckedBoxShader;
} else if ((cg.Objectives[i].flags & OBJ_FLAG_COMPLETED) != 0) {
vColor[0] = 0.75;
vColor[1] = 0.75;
vColor[2] = 0.75;
vColor[3] = fAlpha;
hBoxShader = cgs.media.checkedBoxShader;
} else {
vColor[0] = 1.0;
vColor[1] = 1.0;
vColor[2] = 1.0;
vColor[3] = fAlpha;
hBoxShader = cgs.media.uncheckedBoxShader;
}
if (i == iCurrentObjective && !(cg.Objectives[i].flags & OBJ_FLAG_COMPLETED)) {
vColor[0] = 1.0;
vColor[1] = 1.0;
vColor[2] = 0.0;
vColor[3] = fAlpha;
}
cgi.R_SetColor(vColor);
fX = 55.0;
fY = fHeight;
cgi.R_DrawString(
cgs.media.objectiveFont,
cgi.LV_ConvertString(cg.Objectives[i].text),
55.0,
fY / cgs.uiHiResScale[1],
-1,
cgs.uiHiResScale
);
fX = 30.0;
fY = fHeight;
vColor[0] = 1.0;
vColor[1] = 1.0;
vColor[2] = 1.0;
vColor[3] = fAlpha;
cgi.R_SetColor(vColor);
cgi.R_DrawStretchPic(
fX * cgs.uiHiResScale[0],
fY,
16.0 * cgs.uiHiResScale[0],
16.0 * cgs.uiHiResScale[1],
0.0,
0.0,
1.0,
1.0,
hBoxShader
);
fHeight += iNumLines[i] * 12 + 25 * cgs.uiHiResScale[1];
}
}
void CG_DrawPlayerTeam()
{
qhandle_t handle;
if (!cg_hud->integer) {
return;
}
if (!cg.snap || cgs.gametype <= GT_FFA) {
return;
}
handle = 0;
if (cg.snap->ps.stats[STAT_TEAM] == 3) {
handle = cgi.R_RegisterShader("textures/hud/allies");
} else if (cg.snap->ps.stats[STAT_TEAM] == 4) {
handle = cgi.R_RegisterShader("textures/hud/axis");
}
if (handle) {
cgi.R_SetColor(NULL);
cgi.R_DrawStretchPic(
96.0 * cgs.uiHiResScale[0],
cgs.glconfig.vidHeight - 46 * cgs.uiHiResScale[1],
24.0 * cgs.uiHiResScale[0],
24.0 * cgs.uiHiResScale[1],
0.0,
0.0,
1.0,
1.0,
handle
);
}
}
void CG_DrawPlayerEntInfo()
{
int iClientNum;
const char *pszClientInfo;
const char *pszName;
float fX, fY;
float color[4];
qhandle_t handle;
if (!cg_hud->integer) {
return;
}
if (!cg.snap || cg.snap->ps.stats[STAT_INFOCLIENT] == -1) {
return;
}
iClientNum = cg.snap->ps.stats[STAT_INFOCLIENT];
handle = 0;
pszClientInfo = CG_ConfigString(iClientNum + CS_PLAYERS);
pszName = Info_ValueForKey(pszClientInfo, "name");
color[0] = 0.5;
color[1] = 1.0;
color[2] = 0.5;
color[3] = 1.0;
fX = 56.0;
fY = (float)cgs.glconfig.vidHeight * 0.5;
if (cg.clientinfo[iClientNum].team == TEAM_ALLIES) {
handle = cgi.R_RegisterShader("textures/hud/allies");
} else if (cg.clientinfo[iClientNum].team == TEAM_AXIS) {
handle = cgi.R_RegisterShader("textures/hud/axis");
}
if (handle) {
cgi.R_SetColor(0);
cgi.R_DrawStretchPic(
fX, fY, 16.0 * cgs.uiHiResScale[0], 16.0 * cgs.uiHiResScale[1], 0.0, 0.0, 1.0, 1.0, handle
);
}
cgi.R_SetColor(color);
cgi.R_DrawString(
cgs.media.hudDrawFont,
(char *)pszName,
fX / cgs.uiHiResScale[0] + 24.0,
fY / cgs.uiHiResScale[1],
-1,
cgs.uiHiResScale
);
cgi.R_DrawString(
cgs.media.hudDrawFont,
va("%i", cg.snap->ps.stats[STAT_INFOCLIENT_HEALTH]),
fX / cgs.uiHiResScale[0] + 24.0,
fY / cgs.uiHiResScale[1] + 20.0,
-1,
cgs.uiHiResScale
);
}
void CG_UpdateAttackerDisplay()
{
int iClientNum;
const char *pszClientInfo;
const char *pszName;
float fX, fY;
float color[4];
if (!cg_hud->integer) {
return;
}
if (!cg.snap || cg.snap->ps.stats[STAT_ATTACKERCLIENT] == -1) {
return;
}
iClientNum = cg.snap->ps.stats[STAT_ATTACKERCLIENT];
pszClientInfo = CG_ConfigString(CS_PLAYERS + iClientNum);
pszName = Info_ValueForKey(pszClientInfo, "name");
color[3] = 1.0;
fY = (float)(cgs.glconfig.vidHeight - 90);
fX = 56.0;
if (cgs.gametype > GT_FFA) {
qhandle_t handle;
handle = 0;
if (cg.clientinfo[iClientNum].team == TEAM_ALLIES) {
handle = cgi.R_RegisterShader("textures/hud/allies");
} else if (cg.clientinfo[iClientNum].team == TEAM_AXIS) {
handle = cgi.R_RegisterShader("textures/hud/axis");
}
if (handle) {
cgi.R_SetColor(0);
cgi.R_DrawStretchPic(
56.0 * cgs.uiHiResScale[0],
fY,
24.0 * cgs.uiHiResScale[0],
24.0 * cgs.uiHiResScale[1],
0.0,
0.0,
1.0,
1.0,
handle
);
}
if ((cg.snap->ps.stats[STAT_TEAM] == TEAM_ALLIES || cg.snap->ps.stats[STAT_TEAM] == TEAM_AXIS)
&& cg.clientinfo[iClientNum].team == cg.snap->ps.stats[STAT_TEAM]) {
color[0] = 0.5;
color[1] = 1.0;
color[2] = 0.5;
} else {
color[0] = 1.0;
color[1] = 0.5;
color[2] = 0.5;
}
fX = 56.0;
} else {
color[0] = 1.0;
color[1] = 0.5;
color[2] = 0.5;
}
cgi.R_SetColor(color);
cgi.R_DrawString(
cgs.media.attackerFont, pszName, fX / cgs.uiHiResScale[0] + 32.0, fY / cgs.uiHiResScale[1], -1, cgs.uiHiResScale
);
}
void CG_UpdateCountdown()
{
const char *message = "";
if (!cg.snap) {
return;
}
if (cg.matchStartTime != -1) {
if (cgs.gametype != GT_LIBERATION) {
int iSecondsLeft, iMinutesLeft;
iSecondsLeft = (cgs.matchEndTime - cg.time) / 1000;
if (iSecondsLeft >= 0) {
iMinutesLeft = iSecondsLeft / 60;
message = va("%s %2i:%02i", cgi.LV_ConvertString("Time Left:"), iMinutesLeft, iSecondsLeft % 60);
} else if (!cgs.matchEndTime) {
message = "";
}
} else {
// No clock on liberation game mode
message = "";
}
} else {
// The match has not started yet
message = "Waiting For Players";
}
if (strcmp(ui_timemessage->string, message)) {
cgi.Cvar_Set("ui_timemessage", message);
}
}
static void CG_RemoveStopwatch()
{
cgi.Cmd_Execute(EXEC_NOW, "ui_removehud hud_stopwatch\n");
cgi.Cmd_Execute(EXEC_NOW, "ui_removehud hud_fuse\n");
cgi.Cmd_Execute(EXEC_NOW, "ui_removehud hud_fuse_wet\n");
}
void CG_DrawStopwatch()
{
int iFraction;
if (!cg_hud->integer) {
CG_RemoveStopwatch();
return;
}
if (!cgi.stopWatch->iStartTime) {
CG_RemoveStopwatch();
return;
}
if (cgi.stopWatch->iStartTime >= cgi.stopWatch->iEndTime) {
CG_RemoveStopwatch();
return;
}
if (cgi.stopWatch->iEndTime <= cg.time) {
CG_RemoveStopwatch();
return;
}
if (cg.ObjectivesCurrentAlpha >= 0.02) {
CG_RemoveStopwatch();
return;
}
if (cg.snap && cg.snap->ps.stats[STAT_HEALTH] <= 0) {
CG_RemoveStopwatch();
return;
}
if (cgi.stopWatch->eType >= SWT_FUSE_WET) {
iFraction = cgi.stopWatch->iEndTime - cgi.stopWatch->iStartTime;
} else {
iFraction = cgi.stopWatch->iEndTime - cg.time;
}
cgi.Cvar_Set("ui_stopwatch", va("%i", iFraction));
switch (cgi.stopWatch->eType) {
case SWT_NORMAL:
default:
cgi.Cmd_Execute(EXEC_NOW, "ui_addhud hud_stopwatch\n");
break;
case SWT_FUSE:
cgi.Cmd_Execute(EXEC_NOW, "ui_addhud hud_fuse\n");
break;
case SWT_FUSE_WET:
cgi.Cmd_Execute(EXEC_NOW, "ui_removehud hud_fuse\n");
cgi.Cmd_Execute(EXEC_NOW, "ui_addhud hud_fuse_wet\n");
break;
}
}
void CG_DrawInstantMessageMenu()
{
float w, h;
float x, y;
qhandle_t handle;
if (!cg.iInstaMessageMenu) {
return;
}
if (cg.iInstaMessageMenu > 0) {
handle = cgi.R_RegisterShader(va("textures/hud/instamsg_group_%c", cg.iInstaMessageMenu + 96));
} else {
handle = cgi.R_RegisterShader("textures/hud/instamsg_main");
}
w = cgi.R_GetShaderWidth(handle);
h = cgi.R_GetShaderHeight(handle);
x = 8.0;
y = ((float)cgs.glconfig.vidHeight - h) * 0.5;
cgi.R_SetColor(0);
cgi.R_DrawStretchPic(
x * cgs.uiHiResScale[0], y, w * cgs.uiHiResScale[0], h * cgs.uiHiResScale[1], 0.0, 0.0, 1.0, 1.0, handle
);
}
void CG_DrawSpectatorView_ver_15()
{
const char *pszString;
int iKey1, iKey2;
int iKey1b, iKey2b;
float fX, fY;
qboolean bOnTeam;
if (!(cg.predicted_player_state.pm_flags & PMF_SPECTATING)) {
return;
}
bOnTeam = qfalse;
if (cg.snap->ps.stats[STAT_TEAM] == TEAM_ALLIES || cg.snap->ps.stats[STAT_TEAM] == TEAM_AXIS) {
bOnTeam = 1;
}
if (!bOnTeam) {
cgi.Key_GetKeysForCommand("+attackprimary", &iKey1, &iKey2);
pszString = cgi.LV_ConvertString(va("Press Fire(%s) to join the battle!", cgi.Key_KeynumToBindString(iKey1)));
fX = (float)(cgs.glconfig.vidWidth
- cgi.UI_FontStringWidth(cgs.media.attackerFont, pszString, -1) * cgs.uiHiResScale[0])
* 0.5;
fY = cgs.glconfig.vidHeight - 64.0 * cgs.uiHiResScale[1];
cgi.R_SetColor(NULL);
cgi.R_DrawString(
cgs.media.attackerFont, pszString, fX / cgs.uiHiResScale[0], fY / cgs.uiHiResScale[1], -1, cgs.uiHiResScale
);
}
if (cg.predicted_player_state.pm_flags & PMF_CAMERA_VIEW) {
cgi.Key_GetKeysForCommand("+moveup", &iKey1, &iKey2);
cgi.Key_GetKeysForCommand("+movedown", &iKey1b, &iKey2b);
pszString = cgi.LV_ConvertString(
va("Press Jump(%s) or Duck(%s) to follow a different player.",
cgi.Key_KeynumToBindString(iKey1),
cgi.Key_KeynumToBindString(iKey1b))
);
fX = (float)(cgs.glconfig.vidWidth
- cgi.UI_FontStringWidth(cgs.media.attackerFont, pszString, -1) * cgs.uiHiResScale[0])
* 0.5;
fY = (float)cgs.glconfig.vidHeight - 40.0 * cgs.uiHiResScale[1];
cgi.R_SetColor(0);
cgi.R_DrawString(
cgs.media.attackerFont, pszString, fX / cgs.uiHiResScale[0], fY / cgs.uiHiResScale[1], -1, cgs.uiHiResScale
);
}
if (!bOnTeam && (cg.predicted_player_state.pm_flags & PMF_CAMERA_VIEW)) {
cgi.Key_GetKeysForCommand("+use", &iKey1, &iKey2);
pszString =
cgi.LV_ConvertString(va("Press Use(%s) to enter free spectate mode.", cgi.Key_KeynumToBindString(iKey1)));
fX = (float)(cgs.glconfig.vidWidth
- cgi.UI_FontStringWidth(cgs.media.attackerFont, pszString, -1) * cgs.uiHiResScale[0])
* 0.5;
fY = (float)cgs.glconfig.vidHeight - 24.0 * cgs.uiHiResScale[1];
cgi.R_SetColor(0);
cgi.R_DrawString(
cgs.media.attackerFont, pszString, fX / cgs.uiHiResScale[0], fY / cgs.uiHiResScale[1], -1, cgs.uiHiResScale
);
}
if (!(cg.predicted_player_state.pm_flags & PMF_CAMERA_VIEW)) {
cgi.Key_GetKeysForCommand("+use", &iKey1, &iKey2);
pszString = cgi.LV_ConvertString(
va("Press Use(%s) to enter player following spectate mode.", cgi.Key_KeynumToBindString(iKey1))
);
fX = (float)(cgs.glconfig.vidWidth
- cgi.UI_FontStringWidth(cgs.media.attackerFont, pszString, -1) * cgs.uiHiResScale[0])
* 0.5;
fY = (float)cgs.glconfig.vidHeight - 24.0 * cgs.uiHiResScale[1];
cgi.R_SetColor(0);
cgi.R_DrawString(
cgs.media.attackerFont, pszString, fX / cgs.uiHiResScale[0], fY / cgs.uiHiResScale[1], -1, cgs.uiHiResScale
);
}
if ((cg.predicted_player_state.pm_flags & PMF_CAMERA_VIEW) != 0 && cg.snap
&& cg.snap->ps.stats[STAT_INFOCLIENT] != -1) {
int iClientNum;
qhandle_t hShader;
vec4_t color;
char buf[128];
iClientNum = cg.snap->ps.stats[STAT_INFOCLIENT];
Com_sprintf(
buf, sizeof(buf), "%s : %i", cg.clientinfo[iClientNum].name, cg.snap->ps.stats[STAT_INFOCLIENT_HEALTH]
);
hShader = 0;
color[0] = 0.5;
color[1] = 1.0;
color[2] = 0.5;
color[3] = 1.0;
fX = (float)(cgs.glconfig.vidWidth
- (cgi.UI_FontStringWidth(cgs.media.attackerFont, pszString, -1) - 16) * cgs.uiHiResScale[0])
* 0.5;
fY = (float)cgs.glconfig.vidHeight - 80.0 * cgs.uiHiResScale[1];
cgi.R_SetColor(color);
cgi.R_DrawString(
cgs.media.attackerFont, buf, fX / cgs.uiHiResScale[0], fY / cgs.uiHiResScale[1], -1, cgs.uiHiResScale
);
if (cg.clientinfo[iClientNum].team == TEAM_ALLIES) {
hShader = cgi.R_RegisterShader("textures/hud/allies");
} else if (cg.clientinfo[iClientNum].team == TEAM_AXIS) {
hShader = cgi.R_RegisterShader("textures/hud/axis");
}
if (hShader) {
cgi.R_SetColor(NULL);
cgi.R_DrawStretchPic(
fX - 20.0 * cgs.uiHiResScale[0],
fY,
16.0 * cgs.uiHiResScale[0],
16.0 * cgs.uiHiResScale[1],
0.0,
0.0,
1.0,
1.0,
hShader
);
}
}
}
void CG_DrawSpectatorView_ver_6()
{
const char *pszString;
int iKey1, iKey2;
int iKey1b, iKey2b;
float fX, fY;
qboolean bOnTeam;
if (!(cg.predicted_player_state.pm_flags & PMF_SPECTATING)) {
return;
}
bOnTeam = qfalse;
if (cg.snap->ps.stats[STAT_TEAM] == TEAM_ALLIES || cg.snap->ps.stats[STAT_TEAM] == TEAM_AXIS) {
bOnTeam = 1;
}
// retrieve keys for +use
cgi.Key_GetKeysForCommand("+use", &iKey1, &iKey2);
if (cg.predicted_player_state.pm_flags & PMF_CAMERA_VIEW) {
pszString =
cgi.LV_ConvertString(va("Press Use(%s) to follow a different player.", cgi.Key_KeynumToBindString(iKey1)));
} else {
pszString = cgi.LV_ConvertString(va("Press Use(%s) to follow a player.", cgi.Key_KeynumToBindString(iKey1)));
}
fX = (float)(cgs.glconfig.vidWidth
- cgi.UI_FontStringWidth(cgs.media.attackerFont, pszString, -1) * cgs.uiHiResScale[0])
* 0.5;
fY = (float)cgs.glconfig.vidHeight - 40.0 * cgs.uiHiResScale[1];
cgi.R_SetColor(0);
cgi.R_DrawString(
cgs.media.attackerFont, pszString, fX / cgs.uiHiResScale[0], fY / cgs.uiHiResScale[1], -1, cgs.uiHiResScale
);
if (!bOnTeam && (cg.predicted_player_state.pm_flags & PMF_CAMERA_VIEW)) {
cgi.Key_GetKeysForCommand("+moveup", &iKey1, &iKey2);
cgi.Key_GetKeysForCommand("+movedown", &iKey1b, &iKey2b);
pszString = cgi.LV_ConvertString(
va("Press Jump(%s) or Duck(%s) to free spectate.",
cgi.Key_KeynumToBindString(iKey1),
cgi.Key_KeynumToBindString(iKey1b))
);
fX = (float)(cgs.glconfig.vidWidth
- cgi.UI_FontStringWidth(cgs.media.attackerFont, pszString, -1) * cgs.uiHiResScale[0])
* 0.5;
fY = (float)cgs.glconfig.vidHeight - 24.0 * cgs.uiHiResScale[1];
cgi.R_SetColor(0);
cgi.R_DrawString(
cgs.media.attackerFont, pszString, fX / cgs.uiHiResScale[0], fY / cgs.uiHiResScale[1], -1, cgs.uiHiResScale
);
}
}
void CG_DrawSpectatorView()
{
if (cg_protocol >= PROTOCOL_MOHTA_MIN) {
CG_DrawSpectatorView_ver_15();
} else {
CG_DrawSpectatorView_ver_6();
}
}
void CG_DrawCrosshair()
{
centity_t *friendEnt;
qhandle_t shader;
vec3_t forward;
vec3_t end;
vec3_t mins, maxs;
trace_t trace;
float x, y;
float width, height;
shader = (qhandle_t)0;
if (!cg_hud->integer || !ui_crosshair->integer) {
return;
}
if (!cg.snap) {
return;
}
if ((cg.snap->ps.pm_flags & PMF_NO_HUD) || (cg.snap->ps.pm_flags & PMF_INTERMISSION)) {
return;
}
if (!cg.snap->ps.stats[STAT_CROSSHAIR]
&& (!cg.snap->ps.stats[STAT_INZOOM] || cg.snap->ps.stats[STAT_INZOOM] > 30)) {
return;
}
// Fixed in OPM: R_RegisterShaderNoMip
// Use R_RegisterShaderNoMip, as it's UI stuff
if (cgs.gametype != GT_FFA) {
AngleVectorsLeft(cg.refdefViewAngles, forward, NULL, NULL);
VectorMA(cg.refdef.vieworg, 8192, forward, end);
VectorClear(mins);
VectorClear(maxs);
CG_Trace(&trace, cg.refdef.vieworg, mins, maxs, end, 9999, MASK_SOLID, qfalse, qtrue, "CG_DrawCrosshair");
// ENTITYNUM_WORLD check added in OPM
if ((trace.entityNum != ENTITYNUM_NONE && trace.entityNum != ENTITYNUM_WORLD)
&& trace.entityNum != cg.snap->ps.clientNum) {
int myFlags;
friendEnt = &cg_entities[trace.entityNum];
if (cgs.gametype != GT_SINGLE_PLAYER) {
myFlags = cg_entities[cg.snap->ps.clientNum].currentState.eFlags & EF_ANY_TEAM;
} else {
// the player will always be considered as an allied
// in single-player
myFlags = EF_ALLIES;
}
if (((myFlags & EF_ALLIES) && (friendEnt->currentState.eFlags & EF_ALLIES))
|| ((myFlags & EF_AXIS) && (friendEnt->currentState.eFlags & EF_AXIS))) {
// friend
if (cg.snap->ps.stats[STAT_CROSSHAIR]) {
shader = cgi.R_RegisterShaderNoMip(cg_crosshair_friend->string);
if (!shader) {
// Fixed in OPM
// Fallback to normal crosshair texture if it doesn't exist
shader = cgi.R_RegisterShaderNoMip(cg_crosshair->string);
}
}
} else {
// enemy
if (cg.snap->ps.stats[STAT_CROSSHAIR]) {
shader = cgi.R_RegisterShaderNoMip(cg_crosshair->string);
}
}
} else {
if (cg.snap->ps.stats[STAT_CROSSHAIR]) {
shader = cgi.R_RegisterShaderNoMip(cg_crosshair->string);
}
}
} else {
// FFA
if (cg.snap->ps.stats[STAT_CROSSHAIR]) {
shader = cgi.R_RegisterShaderNoMip(cg_crosshair->string);
}
}
if (shader) {
width = cgi.R_GetShaderWidth(shader) * cgs.uiHiResScale[0];
height = cgi.R_GetShaderHeight(shader) * cgs.uiHiResScale[1];
x = (cgs.glconfig.vidWidth - width) * 0.5f;
y = (cgs.glconfig.vidHeight - height) * 0.5f;
cgi.R_SetColor(NULL);
cgi.R_DrawStretchPic(x, y, width, height, 0, 0, 1, 1, shader);
}
}
void CG_DrawVote()
{
const char *text;
int seconds;
int percentYes;
int percentNo;
int percentUndecided;
float x, y;
vec4_t col;
if (!cgs.voteTime) {
return;
}
if (cgs.voteRefreshed) {
cgs.voteRefreshed = qfalse;
}
seconds = (30000 - (cg.time - cgs.voteTime)) / 1000 + 1;
if (seconds < 0) {
seconds = 0;
}
percentYes = cgs.numVotesYes * 100 / (cgs.numUndecidedVotes + cgs.numVotesNo + cgs.numVotesYes);
percentNo = cgs.numVotesNo * 100 / (cgs.numUndecidedVotes + cgs.numVotesNo + cgs.numVotesYes);
percentUndecided = cgs.numUndecidedVotes * 100 / (cgs.numUndecidedVotes + cgs.numVotesNo + cgs.numVotesYes);
x = 8 * cgs.uiHiResScale[0];
y = ((cgs.glconfig.vidHeight > 480) ? (cgs.glconfig.vidHeight * 0.725f) : (cgs.glconfig.vidHeight * 0.75f));
cgi.R_SetColor(NULL);
text = va("%s: %s", cgi.LV_ConvertString("Vote Running"), cgs.voteString);
cgi.R_DrawString(
cgs.media.attackerFont, text, x / cgs.uiHiResScale[0], y / cgs.uiHiResScale[1], -1, cgs.uiHiResScale
);
y += 12 * cgs.uiHiResScale[1];
text =
va("%s: %isec %s: %i%% %s: %i%% %s: %i%%",
cgi.LV_ConvertString("Time"),
seconds,
cgi.LV_ConvertString("Yes"),
percentYes,
cgi.LV_ConvertString("No"),
percentNo,
cgi.LV_ConvertString("Undecided"),
percentUndecided);
cgi.R_DrawString(
cgs.media.attackerFont, text, x / cgs.uiHiResScale[0], y / cgs.uiHiResScale[1], -1, cgs.uiHiResScale
);
if (cg.snap && !cg.snap->ps.voted) {
col[0] = 0.5;
col[1] = 1.0;
col[2] = 0.5;
col[3] = 1.0;
cgi.R_SetColor(col);
y += 12 * cgs.uiHiResScale[1];
text = cgi.LV_ConvertString("Vote now, it's your patriotic duty!");
cgi.R_DrawString(
cgs.media.attackerFont, text, x / cgs.uiHiResScale[0], y / cgs.uiHiResScale[1], -1, cgs.uiHiResScale
);
y += 12 * cgs.uiHiResScale[1];
text = cgi.LV_ConvertString("To vote Yes, press F1. To vote No, press F2.");
cgi.R_DrawString(
cgs.media.attackerFont, text, x / cgs.uiHiResScale[0], y / cgs.uiHiResScale[1], -1, cgs.uiHiResScale
);
cgi.R_SetColor(NULL);
}
}
/*
==============
CG_Draw2D
==============
*/
/*
=================
CG_DrawVRWheel
The weapon wheel, while the player is holding it open.
Deliberately plain: names in a ring with the one under the controller lit up.
A wheel is only worth having if it can be read at a glance while something is
shooting at you, and the thing that makes it readable is the highlight moving
the moment the wrist does - not what the segments are made of.
Drawn with the world rather than on the wrist panel, because the hand aiming it
is out in front of the player and a menu that lives somewhere else would have
them looking in two places at once.
=================
*/
static void CG_DrawVRWheel(void)
{
const char *names[8];
int selection = -1;
int count = 0;
int i;
float cx, cy, radius;
vec2_t textScale;
if (!cgi.VR_GetWheel || !cgi.VR_GetWheel(&selection, names, (int)ARRAY_LEN(names), &count)) {
return;
}
if (count <= 0 || !cgs.media.objectiveFont) {
return;
}
// Lay out the ring in eye pixels, then undo the text scale on both axes.
// Mixing virtual x with pixel y placed it at y=240 in a 1760-line eye.
textScale[0] = textScale[1] = cgs.glconfig.vidHeight / 1080.0f;
if (textScale[0] <= 0.0f) {
return;
}
cx = cgs.glconfig.vidWidth * 0.5f;
cy = cgs.glconfig.vidHeight * 0.5f;
radius = 120.0f * textScale[0];
if (count > (int)ARRAY_LEN(names)) {
count = (int)ARRAY_LEN(names);
}
for (i = 0; i < count; i++) {
// Clockwise from straight up, matching the order the VR layer numbers
// the segments in - the two have to agree or the highlight lands on
// the wrong word.
const float angle = (float)i * (2.0f * M_PI / (float)count);
const float width = cgi.UI_FontStringWidth(cgs.media.objectiveFont, names[i], -1) * textScale[0];
const float x = cx + sinf(angle) * radius - width * 0.5f;
const float y = cy - cosf(angle) * radius;
vec4_t colour;
if (i == selection) {
colour[0] = 1.0f; colour[1] = 0.85f; colour[2] = 0.4f; colour[3] = 1.0f;
} else {
colour[0] = 0.75f; colour[1] = 0.75f; colour[2] = 0.75f; colour[3] = 0.6f;
}
cgi.R_SetColor(colour);
cgi.R_DrawString(cgs.media.objectiveFont, names[i], x / textScale[0], y / textScale[1], -1, textScale);
}
cgi.R_SetColor(NULL);
}
void CG_Draw2D(void)
{
//
// Added in OPM
// 0 is the flat frame, 1 the wrist panel, 2 the world without its HUD,
// and 3 a VR eye carrying both the world and the HUD.
//
const int pass = vr_hudPass ? vr_hudPass->integer : 0;
if (pass) {
// Keep damage feedback current even when the wrist panel is hidden.
CG_UpdateVRHud();
}
if (pass != 2) {
CG_UpdateCountdown();
CG_DrawLagometer();
CG_HudDrawElements();
//
// Added in OPM
// The gathered VR HUD carries the objectives itself, laid out beside
// the compass rather than across the top left of a screen nobody in a
// headset is looking at. Drawing both would put the same text on the
// view twice, in two places, at two sizes.
//
if (pass && vr_hud && vr_hud->integer) {
CG_DrawVRHud();
} else {
CG_DrawObjectives();
}
CG_DrawIcons();
CG_DrawStopwatch();
CG_DrawSpectatorView();
CG_DrawPlayerTeam();
CG_DrawPlayerEntInfo();
CG_UpdateAttackerDisplay();
CG_DrawVote();
CG_DrawInstantMessageMenu();
}
if (pass != 1) {
CG_DrawVRWheel();
// The scope surround has to sit around the magnified view, so it stays
// with the world rather than travelling to the wrist.
CG_DrawZoomOverlay();
//
// Added in OPM
// The reticle is drawn at the centre of the screen, which in a headset
// is the centre of the player's head - and the weapon is aimed by the
// controller, not by where they are looking. So it marks a spot the
// gun is not pointing at, pinned to their face wherever they turn.
//
// pass 0 is the flat frame, where the reticle means what it always
// meant and is drawn as before. vr_crosshair brings it back in VR for
// anyone who wants it.
//
if (!pass || (vr_crosshair && vr_crosshair->integer)) {
CG_DrawCrosshair();
}
}
}