diff --git a/src/Sourcefile b/src/Sourcefile index 010dffeee..581140c50 100644 --- a/src/Sourcefile +++ b/src/Sourcefile @@ -135,4 +135,5 @@ k_director.c k_follower.c k_mapuser.c k_stats.c +h_timers.cpp stun.c diff --git a/src/h_timers.cpp b/src/h_timers.cpp new file mode 100644 index 000000000..9a3a1276f --- /dev/null +++ b/src/h_timers.cpp @@ -0,0 +1,486 @@ +// BLANKART +//----------------------------------------------------------------------------- +// Copyright (C) 2025 by haya3218. +// Copyright (C) 2018-2024 by Kart Krew +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- +/// \file h_timers.cpp +/// \brief C++ port of Home's item timers + +#include "h_timers.h" +#include "k_hud.h" +#include "r_draw.h" +#include "z_zone.h" +#include "v_video.h" +#include "k_kart.h" +#include "k_color.h" +#include "g_game.h" +#include "p_local.h" +#include "k_kart.h" +#include "g_game.h" +#include "r_main.h" +#include "d_netcmd.h" +#include "m_easing.h" +#include "k_boss.h" +#include "st_stuff.h" +#include "lua_hud.h" +#include "r_main.h" +#include "r_fps.h" +#include "s_sound.h" +#include "fastcmp.h" +#include "m_cond.h" +#include +#include +#include + +consvar_t cv_itemtimers = CVAR_INIT ("itemtimers", "On", CV_SAVE, CV_OnOff, NULL); + +typedef struct itimer_s +{ + const char* name; // name of timer + INT32 timer; // current time + std::vector patches; // timer graphics (big/freeplay) + std::vector patches_small; // timer graphics (small) + INT32 anim_frames; // tic duration for each graphic + INT32 xoffs = 0, yoffs = 0; // offsets for freeplay mode + INT32 xoffs_small = 0, yoffs_small = 0; // offsets for normal mode + boolean counter; // not a timer, show a counter instead + boolean badtimer; // timer is colored red + INT32 *timer_p; // if defined, uses a pointer for the timer instead +} itimer_t; + +static bool sorttimers(itimer_t a, itimer_t b) +{ + return a.timer < b.timer; +} + +static inline INT32 G_TicsToDeciseconds(tic_t tics) +{ + return (INT32)((tics%TICRATE) * (10.00f/TICRATE)); +} + +tic_t spbTimers[MAXPLAYERS] = {0}; + +UINT8 K_GetBestRank(void) +{ + UINT8 best = UINT8_MAX; + INT32 i; + + for (i = 0; i < MAXPLAYERS; i++) + { + if (!playeringame[i] || players[i].exiting || players[i].spectator) + continue; + best = std::min(best, players[i].position); + } + + return best; +} + +// SPB Timer related +void K_UpdateSPBTimer(void) +{ + if (!(cv_itemtimers.value)) + { + // no need to attempt to account for the timers + memset(spbTimers, 0, sizeof spbTimers); + return; + } + INT32 i, bestrank = 255; + for (i = 0; i < MAXPLAYERS; i++) + { + if (!playeringame[i] || players[i].exiting || players[i].spectator) + continue; + spbTimers[i] = 0; + bestrank = std::min(bestrank, players[i].position); + + // check for spb + mobj_t *mobj, *next; + for (mobj = kitemcap; mobj; mobj = next) + { + next = mobj->itnext; + + // Look I don need other shit here besides the SPB + if (mobj->type != MT_SPB) + continue; + + // Don't bother SPBs with no player yet + if (mobj->tracer == NULL || P_MobjWasRemoved(mobj->tracer)) + continue; + if (mobj->tracer->player == NULL) + continue; + + player_t *spbplayer = mobj->tracer->player; + if (spbplayer->position > bestrank) + { + spbTimers[spbplayer-players] = mobj->extravalue2; + if (players[i].position == bestrank) + spbTimers[i] = mobj->extravalue2; + else if (&players[i] != spbplayer) + spbTimers[i] = 0; + } + else + spbTimers[i] = 0; + } + } +} + +static std::unordered_map cachedPatches; + +static patch_t *qche(const char* p) +{ + if (cachedPatches.find(p) != cachedPatches.end()) + return cachedPatches[p]; + cachedPatches[p] = (patch_t*)W_CachePatchLongName(p, PU_CACHE); + return cachedPatches[p]; +} + +static std::vector addTimers; +static std::vector addTimers_unsorted; + +void K_AddItemTimerEx( + const char *name, + INT32 *timer, + char **patches, char **patches_small, + INT32 patches_size, INT32 patches_small_size, + INT32 anim_duration, + INT32 xoffs, INT32 yoffs, + INT32 xoffs_small, INT32 yoffs_small, + boolean counter, boolean unsorted) +{ + itimer_t t; // woo yeah baby + + t.name = name; + t.timer_p = timer; + INT32 i; + + for (i = 0; i < patches_size; i++) + t.patches.push_back(qche(patches[i])); + for (i = 0; i < patches_small_size; i++) + t.patches_small.push_back(qche(patches[i])); + + t.anim_frames = anim_duration; + t.xoffs = xoffs; t.yoffs = yoffs; + t.xoffs_small = xoffs_small; t.yoffs_small = yoffs_small; + t.counter = counter; + + // yeah + if (unsorted) + addTimers_unsorted.push_back(t); + else + addTimers.push_back(t); +} + +void K_DisplayItemTimers(void) +{ + if (!cv_itemtimers.value) return; + // nooope + if (stplyr->mo == NULL || P_MobjWasRemoved(stplyr->mo)) return; + // I love and hate you C++ + std::vector timers = + { + { // sneaker + "shoe", + stplyr->sneakertimer, + {qche("K_ISSHOE")}, + {qche("HL_SNKR")}, + 1, -15, -15, -4, -2, + }, + { // invinc + "invincible", + stplyr->invincibilitytimer, + {qche("K_ISINV1"), qche("K_ISINV2"), qche("K_ISINV3"), qche("K_ISINV4"), qche("K_ISINV5"), qche("K_ISINV6")}, + {qche("HL_INVNC")}, + 3, -15, -15, -4, -2, + }, + { // grow + "grow", + std::max(0, stplyr->growshrinktimer), + {qche("K_ISGROW")}, + {qche("HL_GROW")}, + 1, -15, -15, -4, -2, + }, + { // rocket sneakers + "rocketsneakers", + stplyr->rocketsneakertimer, + {qche("K_ISRSHE")}, + {qche("ISPYRSHE")}, + 1, -15, -15, -4, -2, + }, + { // hyudoro + "hyudoro", + stplyr->hyudorotimer, + {qche("K_ISHYUD")}, + {qche("HL_HYU")}, + 1, -15, -15, -4, -2, + }, + { // drift boost + "driftsparkboost", + stplyr->driftboost, + {qche("IT_ORB0"), qche("IT_ORB1"), qche("IT_ORB2"), qche("IT_ORB3"), qche("IT_ORB4"), qche("IT_ORB3"), qche("IT_ORB2"), qche("IT_ORB1")}, + {qche("MSO_SPH1"), qche("MSO_SPH2"), qche("MSO_SPH3"), qche("MSO_SPH4")}, + 3, + }, + { // start boost + "startboost", + stplyr->startboost, + {qche("K_ISSTB")}, + {qche("K_SSSTB")}, + 1, + }, + { // ring boost + "ringboost", + stplyr->ringboost, + {qche("IT_RING")}, + {qche("MSO_RNG")}, + 1, + }, + }; + + // insert sortable timers + timers.insert(timers.end(), addTimers.begin(), addTimers.end()); + + // Sort them based on time + std::sort(timers.begin(), timers.end(), sorttimers); + + // Spinout and shrink are always last + timers.push_back( + { // spinout + "spinout", + std::max(stplyr->spinouttimer, stplyr->wipeoutslow), + {qche("K_DIZZ1"), qche("K_DIZZ2"), qche("K_DIZZ3"), qche("K_DIZZ4")}, + {qche("MSO_HRT1"), qche("MSO_HRT2"), qche("MSO_HRT3"), qche("MSO_HRT4")}, + 3, .badtimer = true, + } + ); + timers.push_back( + { // shrink + "shrink", + std::max(0, -stplyr->growshrinktimer), + {qche("K_ISSHRK")}, + {qche("HL_SHRNK")}, + 3, -15, -15, -4, -3, .badtimer = true, + } + ); + timers.push_back( + { // spb + "blueshell", + (INT32)spbTimers[stplyr-players], + {qche("IT_SPB1"), qche("IT_SPB2")}, + {qche("IT_SPBS1"), qche("IT_SPBS2")}, + 1, .badtimer = (stplyr->position == K_GetBestRank()), + } + ); + // Same with boost stacks + timers.push_back( + { // boosts + "boosts", + K_StackingActive() ? stplyr->numboosts : 0, + {qche("IT_BOOST0"), qche("IT_BOOST1"), qche("IT_BOOST2"), qche("IT_BOOST3"), qche("IT_BOOST4"), qche("IT_BOOST3"), qche("IT_BOOST2"), qche("IT_BOOST1")}, + {qche("MSO_SPD1"), qche("MSO_SPD2"), qche("MSO_SPD3"), qche("MSO_SPD4"), qche("MSO_SPD3"), qche("MSO_SPD2")}, + 3, .counter = true, + } + ); + + timers.push_back( + { // dead + "spr_realisticexplosion", + stplyr->deadtimer ? std::max(0, TICRATE - stplyr->deadtimer) : 0, + {qche("IT_DED1"), qche("IT_DED2"), qche("IT_DED3"), qche("IT_DED4"), qche("IT_DED5"), qche("IT_DED6"), qche("IT_DED7"), qche("IT_DED8")}, + {qche("IT_DEAD1"), qche("IT_DEAD2"), qche("IT_DEAD3"), qche("IT_DEAD4"), qche("IT_DEAD5"), qche("IT_DEAD6"), qche("IT_DEAD7"), qche("IT_DEAD8")}, + 1, .yoffs = -4, .badtimer = true, + } + ); + + // insert unsortable timers + timers.insert(timers.end(), addTimers_unsorted.begin(), addTimers_unsorted.end()); + + if ((K_NotFreePlay() && !modeattacking) || r_splitscreen > 0) // "HOME" style, with icons at the bottom + { + INT32 itemx = 160, itemy = 160; + INT32 flags = V_SNAPTOBOTTOM|V_SLIDEIN|V_SPLITSCREEN; + INT32 stepx = 25; + INT32 viewnum = R_GetViewNumber(); + + if (r_splitscreen == 1) + { + flags &= ~V_SNAPTOBOTTOM; + itemy = 75; + } + else if (r_splitscreen > 1) + { + flags &= ~V_SNAPTOBOTTOM; + itemy = 75; + itemx = 80; + } + + if (r_splitscreen > 0) + flags |= V_30TRANS; + + // Move it up to account for viewpoint text + if (cv_showviewpointtext.value && !demo.title && !P_IsLocalPlayer(stplyr) && !camera[viewnum].freecam && !r_splitscreen) + itemy -= 12; + + // flags |= V_HUDTRANS; + + INT32 offs = -stepx; + for (itimer_t& t : timers) + { + INT32 timer = t.timer; + if (t.timer_p) timer = *t.timer_p; + if (stplyr->deadtimer && !fastcmp(t.name, "spr_realisticexplosion")) continue; + if (timer > 0) + offs += stepx; + } + + itemx -= offs/2; + // draw relevant timers + for (itimer_t& t : timers) + { + if (t.timer <= 0) continue; + if (stplyr->deadtimer && !fastcmp(t.name, "spr_realisticexplosion")) continue; + INT32 timer = t.timer; + if (t.timer_p) timer = *t.timer_p; + INT32 seconds = timer / TICRATE, _centiseconds = G_TicsToCentiseconds(timer); + + const char* str = va("%d.%02d", seconds, _centiseconds); + if (seconds > 9) // exceeds 9 seconds? + str = va("%d.%01d", seconds, G_TicsToDeciseconds(timer)); + if (seconds > 99) // exceeds 100 seconds? + str = va("%d", seconds); + INT32 animsize = t.patches_small.size(); + INT32 patchnum = (leveltime % (t.anim_frames * animsize) / t.anim_frames); + UINT8 *cmap = NULL, *textcmap = NULL; + INT32 font = TINY_FONT; + + if (fastcmp(t.name, "driftsparkboost")) + { + if (timer > 85) // rainbow + cmap = R_GetTranslationColormap(TC_DEFAULT, (skincolornum_t)K_RainbowColor(leveltime), GTC_CACHE); + else if (timer > 50 && K_PurpleDriftActive()) // purple + cmap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_PURPLE, GTC_CACHE); + else if (timer > 25) // ketchup + cmap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_KETCHUP, GTC_CACHE); + else // blu + cmap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_SAPPHIRE, GTC_CACHE); + textcmap = cmap; + } + + if (fastcmp(t.name, "invincibilitytimer")) + { + cmap = R_GetTranslationColormap(TC_RAINBOW, (skincolornum_t)K_RainbowColor(leveltime), GTC_CACHE); + } + + if (t.counter) // don't show up as time + { + str = va("%d", timer); + font = HU_FONT; + } + INT32 width = V_StringScaledWidth(FRACUNIT, FRACUNIT, FRACUNIT, flags|V_MONOSPACE, font, str) >> FRACBITS; + + // very bad! + if (t.badtimer) + textcmap = R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_CRIMSON, GTC_CACHE); + + patch_t *item = t.patches_small[patchnum]; + V_DrawFixedPatch((itemx - (item->width/2))<leftoffset) << FRACBITS, + (itemy + 12) << FRACBITS, + FRACUNIT, + FRACUNIT, + FRACUNIT, + FRACUNIT, + flags|V_MONOSPACE, + textcmap, + font, + str + ); + + itemx += stepx; + } + } + else // FREEPLAY - Move these to the left side, where the rankings usually are + { + if (r_splitscreen) // How? + return; + + INT32 fx = 9, fy = 92, step = 20; + INT32 flags = V_SNAPTOLEFT|V_SLIDEIN; + + // center it + INT32 offs = -step; + for (itimer_t& t : timers) + { + INT32 timer = t.timer; + if (t.timer_p) timer = *t.timer_p; + if (stplyr->deadtimer && !fastcmp(t.name, "spr_realisticexplosion")) continue; + if (timer > 0) + offs += step; + } + fy -= offs/2; + + // draw relevant timers + for (itimer_t& t : timers) + { + if (t.timer <= 0) continue; + if (stplyr->deadtimer && !fastcmp(t.name, "spr_realisticexplosion")) continue; + INT32 timer = t.timer; + if (t.timer_p) timer = *t.timer_p; + INT32 seconds = timer / TICRATE, _centiseconds = G_TicsToCentiseconds(timer); + INT32 patchnum = (leveltime % (t.anim_frames * t.patches.size()) / t.anim_frames); + UINT8 *cmap = NULL, *textcmap = NULL; + + const char* str = va("%d.%02d", seconds, _centiseconds); + if (seconds > 9) // exceeds 9 seconds? + str = va("%d.%01d", seconds, G_TicsToDeciseconds(timer)); + if (seconds > 99) // exceeds 100 seconds? + str = va("%d", seconds); + boolean bost = false; + if (fastcmp(t.name, "boosts")) // don't show up as time + { + str = va("%d", timer); + bost = true; + } + INT32 width = V_StringScaledWidth(FRACUNIT, FRACUNIT, FRACUNIT, flags|V_MONOSPACE, OPPRNK_FONT, str) >> FRACBITS; + + if (fastcmp(t.name, "driftsparkboost")) + { + if (timer > 85) // rainbow + cmap = R_GetTranslationColormap(TC_DEFAULT, (skincolornum_t)K_RainbowColor(leveltime), GTC_CACHE); + else if (timer > 50 && K_PurpleDriftActive()) // purple + cmap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_PURPLE, GTC_CACHE); + else if (timer > 25) // ketchup + cmap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_KETCHUP, GTC_CACHE); + else // blu + cmap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_SAPPHIRE, GTC_CACHE); + textcmap = cmap; + } + + // very bad! + if (t.badtimer) + textcmap = R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_CRIMSON, GTC_CACHE); + + patch_t *item = t.patches[patchnum]; + V_DrawFixedPatch((fx+t.xoffs)<width/2) - (width/2) - item->leftoffset + t.xoffs) << FRACBITS, + (fy+5) << FRACBITS, + FRACUNIT, + FRACUNIT, + FRACUNIT, + FRACUNIT, + flags|V_MONOSPACE, + textcmap, + TINY_FONT, + str + ); + + + fy += step; + } + } +} diff --git a/src/h_timers.h b/src/h_timers.h new file mode 100644 index 000000000..03de64462 --- /dev/null +++ b/src/h_timers.h @@ -0,0 +1,42 @@ +// BLANKART +//----------------------------------------------------------------------------- +// Copyright (C) 2024 by haya3218. +// +// 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 h_main.h +/// \brief HEP3 timers library. + +#ifndef __H_TIMERS__ +#define __H_TIMERS__ + +#ifdef __cplusplus +extern "C" { +#endif + +#include "command.h" +#include "d_player.h" + +extern consvar_t cv_itemtimers; +extern tic_t spbTimers[MAXPLAYERS]; +UINT8 K_GetBestRank(void); +void K_UpdateSPBTimer(void); +// Adds an item timer. +void K_AddItemTimerEx( + const char *name, + INT32 *timer, + char **patches, char **patches_small, + INT32 patches_size, INT32 patches_small_size, + INT32 anim_duration, + INT32 xoffs, INT32 yoffs, + INT32 xoffs_small, INT32 yoffs_small, + boolean counter, boolean unsorted); +void K_DisplayItemTimers(void); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif diff --git a/src/k_hud.c b/src/k_hud.c index 444d6416f..1311f386a 100644 --- a/src/k_hud.c +++ b/src/k_hud.c @@ -39,6 +39,7 @@ #include "r_fps.h" #include "m_random.h" #include "g_party.h" +#include "h_timers.h" #define NUMPOSNUMS 10 #define NUMPOSFRAMES 7 // White, three blues, three reds @@ -5187,6 +5188,8 @@ void K_drawKartHUD(void) K_drawLapStartAnim(); } + K_DisplayItemTimers(); + if (modeattacking || freecam) // everything after here is MP and debug only return; diff --git a/src/k_kart.c b/src/k_kart.c index 5ad2e3979..29fdff8a6 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -65,6 +65,7 @@ #include "k_grandprix.h" #include "k_cluster.hpp" +#include "h_timers.h" #include "blan/b_soc.h" consvar_t cv_kartstacking_colorflame = CVAR_INIT ("kartstacking_colorflame", "On", CV_SAVE, CV_OnOff, NULL); @@ -374,6 +375,7 @@ void K_RegisterKartStuff(void) CV_RegisterVar(&cv_kartdriftsounds); CV_RegisterVar(&cv_kartdriftefx); CV_RegisterVar(&cv_driftsparkpulse); + CV_RegisterVar(&cv_itemtimers); CV_RegisterVar(&cv_saltyhop); CV_RegisterVar(&cv_test1); @@ -11908,6 +11910,41 @@ void K_UpdateMobjItemOverlay(mobj_t *part, SINT8 itemType, UINT8 itemCount) } } +boolean K_NotFreePlay(void) +{ + UINT8 i; + UINT8 nump = 0; + + if (K_CanChangeRules() == false) + { + // Rounds with direction are never FREE PLAY. + return true; + } + + if (itembreaker) + { + // Prison Break is battle's FREE PLAY. + return false; + } + + for (i = 0; i < MAXPLAYERS; i++) + { + if (playeringame[i] == false || players[i].spectator == true) + { + continue; + } + + nump++; + + if (nump > 1) + { + return true; + } + } + + return false; +} + void K_QuiteSaltyHop(player_t *player) { if (player->mo == NULL || P_MobjWasRemoved(player->mo)) diff --git a/src/k_kart.h b/src/k_kart.h index 2c359b9d0..13a5626a4 100644 --- a/src/k_kart.h +++ b/src/k_kart.h @@ -329,6 +329,7 @@ void K_UpdateMobjItemOverlay(mobj_t *part, SINT8 itemType, UINT8 itemCount); void K_DoBoost(player_t *player, fixed_t speedboost, fixed_t accelboost, boolean stack, boolean visible); void K_ClearBoost(player_t *player); +boolean K_NotFreePlay(void); typedef enum { ASR_ITEMBOX = 0, diff --git a/src/m_cond.c b/src/m_cond.c index fc4590174..a54f16b33 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -20,6 +20,7 @@ #include "g_game.h" // record info #include "r_skins.h" // numskins #include "r_draw.h" // R_GetColorByName +#include "k_kart.h" #include "k_pwrlv.h" #include "k_stats.h" diff --git a/src/v_video.c b/src/v_video.c index b4db05e2a..5d0713d79 100644 --- a/src/v_video.c +++ b/src/v_video.c @@ -2159,6 +2159,11 @@ INT32 V_DanceYOffset(INT32 counter) return abs(step - (duration / 2)) - (duration / 4); } +static boolean V_CharacterValid(font_t *font, int c) +{ + return (c >= 0 && c < font->size && font->font[c] != NULL); +} + // Writes a single character (draw WHITE if bit 7 set) // void V_DrawCharacter(INT32 x, INT32 y, INT32 c, boolean lowercaseallowed) @@ -2497,34 +2502,146 @@ static inline fixed_t BunchedCharacterDim( return 0; } -void V_DrawStringScaled( +typedef struct +{ + fixed_t chw; + fixed_t spacew; + fixed_t lfh; + fixed_t (*dim_fn)(fixed_t,fixed_t,INT32,INT32,fixed_t *); +} fontspec_t; + +static void V_GetFontSpecification(int fontno, INT32 flags, fontspec_t *result) +{ + /* + Hardcoded until a better system can be implemented + for determining how fonts space. + */ + + // All other properties are guaranteed to be set + result->chw = 0; + + const INT32 spacing = ( flags & V_SPACINGMASK ); + + switch (fontno) + { + default: + case HU_FONT: + result->spacew = 4; + switch (spacing) + { + case V_MONOSPACE: + result->spacew = 8; + /* FALLTHRU */ + case V_OLDSPACING: + result->chw = 8; + break; + case V_6WIDTHSPACE: + result->spacew = 6; + } + break; + case TINY_FONT: + result->spacew = 2; + switch (spacing) + { + case V_MONOSPACE: + result->spacew = 5; + /* FALLTHRU */ + case V_OLDSPACING: + result->chw = 5; + break; + // Out of video flags, so we're reusing this for alternate charwidth instead + /*case V_6WIDTHSPACE: + spacewidth = 3;*/ + } + break; + case KART_FONT: + result->spacew = 12; + switch (spacing) + { + case V_MONOSPACE: + result->spacew = 12; + /* FALLTHRU */ + case V_OLDSPACING: + result->chw = 12; + break; + case V_6WIDTHSPACE: + result->spacew = 6; + } + break; + case LT_FONT: + result->spacew = 12; + break; + case CRED_FONT: + result->spacew = 16; + break; + } + switch (fontno) + { + default: + case HU_FONT: + case TINY_FONT: + case KART_FONT: + result->lfh = 12; + break; + case LT_FONT: + case CRED_FONT: + result->lfh = 12; + break; + } + + switch (fontno) + { + case TINY_FONT: + { + if (result->chw) + result->dim_fn = FixedCharacterDim; + else + { + /* Reuse this flag for the alternate bunched-up spacing. */ + if (( flags & V_6WIDTHSPACE )) + result->dim_fn = BunchedCharacterDim; + else + result->dim_fn = VariableCharacterDim; + } + break; + } + default: + { + if (result->chw) + result->dim_fn = CenteredCharacterDim; + else + result->dim_fn = VariableCharacterDim; + break; + } + } +} + +void V_DrawStringScaledEx( fixed_t x, fixed_t y, fixed_t scale, + fixed_t vscale, fixed_t spacescale, fixed_t lfscale, INT32 flags, + const UINT8 *colormap, int fontno, const char *s) { - fixed_t chw; INT32 hchw;/* half-width for centering */ - fixed_t spacew; - fixed_t lfh; INT32 dupx; + INT32 dupy; fixed_t right; fixed_t bot; - fixed_t (*dim_fn)(fixed_t,fixed_t,INT32,INT32,fixed_t *); - font_t *font; boolean uppercase; boolean notcolored; + boolean vflip; - const UINT8 *colormap = NULL; boolean dance; boolean nodanceoverride; INT32 dancecounter; @@ -2534,7 +2651,6 @@ void V_DrawStringScaled( fixed_t cxoff, cyoff; fixed_t cw; - INT32 spacing; fixed_t left; int c; @@ -2545,126 +2661,59 @@ void V_DrawStringScaled( dance = (flags & V_STRINGDANCE) != 0; nodanceoverride = !dance; dancecounter = 0; + vflip = (flags & V_VFLIP) != 0; /* Some of these flags get overloaded in this function so don't pass them on. */ flags &= ~(V_PARAMMASK); + if (vflip) flags |= V_VFLIP; + if (colormap == NULL) { colormap = V_GetStringColormap(( flags & V_CHARCOLORMASK )); } - colormap = V_GetStringColormap(( flags & V_CHARCOLORMASK )); notcolored = !colormap; font = &fontv[fontno]; - chw = 0; + fontspec_t fontspec; - spacing = ( flags & V_SPACINGMASK ); + V_GetFontSpecification(fontno, flags, &fontspec); - /* - Hardcoded until a better system can be implemented - for determining how fonts space. - */ - switch (fontno) - { - default: - case HU_FONT: - spacew = 4; - switch (spacing) - { - case V_MONOSPACE: - spacew = 8; - /* FALLTHRU */ - case V_OLDSPACING: - chw = 8; - break; - case V_6WIDTHSPACE: - spacew = 6; - } - break; - case TINY_FONT: - spacew = 2; - switch (spacing) - { - case V_MONOSPACE: - spacew = 5; - /* FALLTHRU */ - case V_OLDSPACING: - chw = 5; - break; - // Out of video flags, so we're reusing this for alternate charwidth instead - /*case V_6WIDTHSPACE: - spacewidth = 3;*/ - } - break; - case KART_FONT: - spacew = 12; - switch (spacing) - { - case V_MONOSPACE: - spacew = 12; - /* FALLTHRU */ - case V_OLDSPACING: - chw = 12; - break; - case V_6WIDTHSPACE: - spacew = 6; - } - break; - case LT_FONT: - spacew = 12; - break; - case CRED_FONT: - spacew = 16; - break; - } - switch (fontno) - { - default: - case HU_FONT: - case TINY_FONT: - case KART_FONT: - lfh = 12; - break; - case LT_FONT: - case CRED_FONT: - lfh = 12; - break; - } + hchw = fontspec.chw >> 1; - hchw = chw >> 1; - - chw <<= FRACBITS; - spacew <<= FRACBITS; - lfh <<= FRACBITS; + fontspec.chw <<= FRACBITS; + fontspec.spacew <<= FRACBITS; + fontspec.lfh <<= FRACBITS; #define Mul( id, scale ) ( id = FixedMul (scale, id) ) - Mul (chw, scale); - Mul (spacew, scale); - Mul (lfh, scale); + Mul (fontspec.chw, scale); + Mul (fontspec.spacew, scale); + Mul (fontspec.lfh, scale); - Mul (spacew, spacescale); - Mul (lfh, lfscale); + Mul (fontspec.spacew, spacescale); + Mul (fontspec.lfh, lfscale); #undef Mul if (( flags & V_NOSCALESTART )) { dupx = vid.dupx; + dupy = vid.dupy; hchw *= dupx; - chw *= dupx; - spacew *= dupx; - lfh *= vid.dupy; + fontspec.chw *= dupx; + fontspec.spacew *= dupx; + fontspec.lfh *= dupy; right = vid.width; } else { dupx = 1; + dupy = 1; right = ( vid.width / vid.dupx ); if (!( flags & V_SNAPTOLEFT )) @@ -2677,27 +2726,6 @@ void V_DrawStringScaled( right <<= FRACBITS; bot = vid.height << FRACBITS; - if (fontno == TINY_FONT) - { - if (chw) - dim_fn = FixedCharacterDim; - else - { - /* Reuse this flag for the alternate bunched-up spacing. */ - if (( flags & V_6WIDTHSPACE )) - dim_fn = BunchedCharacterDim; - else - dim_fn = VariableCharacterDim; - } - } - else - { - if (chw) - dim_fn = CenteredCharacterDim; - else - dim_fn = VariableCharacterDim; - } - cx = x; cy = y; cyoff = 0; @@ -2707,13 +2735,13 @@ void V_DrawStringScaled( switch (c) { case '\n': - cy += lfh; + cy += fontspec.lfh; if (cy >= bot) return; cx = x; break; default: - if (( c & 0x80 )) + if (( c & 0xF0 ) == 0x80) { if (notcolored) { @@ -2733,26 +2761,329 @@ void V_DrawStringScaled( else if (cx < right) { if (uppercase) + { c = toupper(c); + } + else if (V_CharacterValid(font, c - font->start) == false) + { + // Try the other case if it doesn't exist + if (c >= 'A' && c <= 'Z') + { + c = tolower(c); + } + else if (c >= 'a' && c <= 'z') + { + c = toupper(c); + } + } + if (dance) + { cyoff = V_DanceYOffset(dancecounter) * FRACUNIT; + } c -= font->start; - if (c >= 0 && c < font->size && font->font[c]) + if (V_CharacterValid(font, c) == true) { + // Remove offsets from patch + fixed_t patchxofs = SHORT (font->font[c]->leftoffset) * dupx * scale; cw = SHORT (font->font[c]->width) * dupx; - cxoff = (*dim_fn)(scale, chw, hchw, dupx, &cw); - V_DrawFixedPatch(cx + cxoff, cy + cyoff, scale, + cxoff = (*fontspec.dim_fn)(scale, fontspec.chw, hchw, dupx, &cw); + V_DrawStretchyFixedPatch(cx + cxoff + patchxofs, cy + cyoff, scale, vscale, flags, font->font[c], colormap); cx += cw; } else - cx += spacew; + { + cx += fontspec.spacew; + } } } } } + +fixed_t V_StringScaledWidth( + fixed_t scale, + fixed_t spacescale, + fixed_t lfscale, + INT32 flags, + int fontno, + const char *s) +{ + INT32 hchw;/* half-width for centering */ + + INT32 dupx; + + font_t *font; + + boolean uppercase; + + fixed_t cx; + fixed_t right; + + fixed_t cw; + + int c; + + fixed_t fullwidth = 0; + + uppercase = !( flags & V_ALLOWLOWERCASE ); + flags &= ~(V_FLIP);/* These two (V_ALLOWLOWERCASE) share a bit. */ + + font = &fontv[fontno]; + + fontspec_t fontspec; + + V_GetFontSpecification(fontno, flags, &fontspec); + + hchw = fontspec.chw >> 1; + + fontspec.chw <<= FRACBITS; + fontspec.spacew <<= FRACBITS; + +#define Mul( id, scale ) ( id = FixedMul (scale, id) ) + Mul (fontspec.chw, scale); + Mul (fontspec.spacew, scale); + Mul (fontspec.lfh, scale); + + Mul (fontspec.spacew, spacescale); + Mul (fontspec.lfh, lfscale); +#undef Mul + + if (( flags & V_NOSCALESTART )) + { + dupx = vid.dupx; + + hchw *= dupx; + + fontspec.chw *= dupx; + fontspec.spacew *= dupx; + fontspec.lfh *= vid.dupy; + } + else + { + dupx = 1; + } + + cx = 0; + right = 0; + + for (; ( c = *s ); ++s) + { + switch (c) + { + case '\n': + cx = 0; + break; + default: + if (( c & 0xF0 ) == 0x80 || c == V_STRINGDANCE) + continue; + + if (uppercase) + { + c = toupper(c); + } + else if (V_CharacterValid(font, c - font->start) == false) + { + // Try the other case if it doesn't exist + if (c >= 'A' && c <= 'Z') + { + c = tolower(c); + } + else if (c >= 'a' && c <= 'z') + { + c = toupper(c); + } + } + + c -= font->start; + if (V_CharacterValid(font, c) == true) + { + cw = SHORT (font->font[c]->width) * dupx; + + // How bunched dims work is by incrementing cx slightly less than a full character width. + // This causes the next character to be drawn overlapping the previous. + // We need to count the full width to get the rightmost edge of the string though. + right = cx + (cw * scale); + + (*fontspec.dim_fn)(scale, fontspec.chw, hchw, dupx, &cw); + cx += cw; + } + else + cx += fontspec.spacew; + } + + fullwidth = max(right, max(cx, fullwidth)); + } + + return fullwidth; +} + +// Modify a string to wordwrap at any given width. +char * V_ScaledWordWrap( + fixed_t w, + fixed_t scale, + fixed_t spacescale, + fixed_t lfscale, + INT32 flags, + int fontno, + const char *s) +{ + INT32 hchw;/* half-width for centering */ + + INT32 dupx; + + font_t *font; + + boolean uppercase; + + fixed_t cx; + fixed_t right; + + fixed_t cw; + + int c; + + uppercase = !( flags & V_ALLOWLOWERCASE ); + flags &= ~(V_FLIP);/* These two (V_ALLOWLOWERCASE) share a bit. */ + + font = &fontv[fontno]; + + fontspec_t fontspec; + + V_GetFontSpecification(fontno, flags, &fontspec); + + hchw = fontspec.chw >> 1; + + fontspec.chw <<= FRACBITS; + fontspec.spacew <<= FRACBITS; + +#define Mul( id, scale ) ( id = FixedMul (scale, id) ) + Mul (fontspec.chw, scale); + Mul (fontspec.spacew, scale); + Mul (fontspec.lfh, scale); + + Mul (fontspec.spacew, spacescale); + Mul (fontspec.lfh, lfscale); +#undef Mul + + if (( flags & V_NOSCALESTART )) + { + dupx = vid.dupx; + + hchw *= dupx; + + fontspec.chw *= dupx; + fontspec.spacew *= dupx; + fontspec.lfh *= vid.dupy; + } + else + { + dupx = 1; + } + + cx = 0; + right = 0; + + size_t reader = 0, writer = 0, startwriter = 0; + fixed_t cxatstart = 0; + + size_t len = strlen(s) + 1; + size_t potentialnewlines = 8; + size_t sparenewlines = potentialnewlines; + + char *newstring = Z_Malloc(len + sparenewlines, PU_STATIC, NULL); + + for (; ( c = s[reader] ); ++reader, ++writer) + { + newstring[writer] = s[reader]; + + right = 0; + + switch (c) + { + case '\n': + cx = 0; + cxatstart = 0; + startwriter = 0; + break; + default: + if (( c & 0xF0 ) == 0x80 || c == V_STRINGDANCE) + ; + else + { + if (uppercase) + { + c = toupper(c); + } + else if (V_CharacterValid(font, c - font->start) == false) + { + // Try the other case if it doesn't exist + if (c >= 'A' && c <= 'Z') + { + c = tolower(c); + } + else if (c >= 'a' && c <= 'z') + { + c = toupper(c); + } + } + + c -= font->start; + if (V_CharacterValid(font, c) == true) + { + cw = SHORT (font->font[c]->width) * dupx; + + // How bunched dims work is by incrementing cx slightly less than a full character width. + // This causes the next character to be drawn overlapping the previous. + // We need to count the full width to get the rightmost edge of the string though. + right = cx + (cw * scale); + + (*fontspec.dim_fn)(scale, fontspec.chw, hchw, dupx, &cw); + cx += cw; + } + else + { + cx += fontspec.spacew; + cxatstart = cx; + startwriter = writer; + } + } + } + + // Start trying to wrap if presumed length exceeds the space we have on-screen. + if (right && right > w) + { + if (startwriter != 0) + { + newstring[startwriter] = '\n'; + cx -= cxatstart; + cxatstart = 0; + startwriter = 0; + } + else + { + if (sparenewlines == 0) + { + sparenewlines = (potentialnewlines *= 2); + newstring = Z_Realloc(newstring, len + sparenewlines, PU_STATIC, NULL); + } + + sparenewlines--; + len++; + + newstring[writer++] = '\n'; // Over-write previous + cx = cw; // Valid value in the only case right is currently set + newstring[writer] = s[reader]; // Re-add + } + } + } + + newstring[writer] = '\0'; + + return newstring; +} // void V_DrawCenteredString(INT32 x, INT32 y, INT32 option, const char *string) diff --git a/src/v_video.h b/src/v_video.h index b404c1ff0..be9c1fa43 100644 --- a/src/v_video.h +++ b/src/v_video.h @@ -235,6 +235,10 @@ void V_EncoreInvertScreen(void); void V_DrawPromptBack(INT32 boxheight, INT32 color); /* Convenience macros for leagacy string function macros. */ + +/* Yes I'm lazy RN, deal with it. */ +#define V_DrawStringScaled( x,y,scale,space_scale,linefeed_scale,option,font,string ) \ + V_DrawStringScaledEx(x,y,scale,scale,space_scale,linefeed_scale,option,NULL,font,string) #define V__DrawOneScaleString( x,y,scale,option,font,string ) \ V_DrawStringScaled(x,y,scale,FRACUNIT,FRACUNIT,option,font,string) #define V__DrawDupxString( x,y,scale,option,font,string )\ @@ -256,16 +260,35 @@ UINT8 *V_GetStringColormap(INT32 colorflags); char *V_WordWrap(INT32 x, INT32 w, INT32 option, const char *string); // draw a string using a font -void V_DrawStringScaled( +void V_DrawStringScaledEx( fixed_t x, fixed_t y, fixed_t scale, + fixed_t vscale, fixed_t space_scale, fixed_t linefeed_scale, INT32 flags, + const UINT8 *colormap, int font, const char *text); +fixed_t V_StringScaledWidth( + fixed_t scale, + fixed_t spacescale, + fixed_t lfscale, + INT32 flags, + int fontno, + const char *s); + +char * V_ScaledWordWrap( + fixed_t w, + fixed_t scale, + fixed_t spacescale, + fixed_t lfscale, + INT32 flags, + int fontno, + const char *s); + // draw a string using the hu_font #define V_DrawString( x,y,option,string ) \ V__DrawDupxString (x,y,FRACUNIT,option,HU_FONT,string)