diff --git a/.clang-format b/.clang-format index 15b53c893..e1599848d 100644 --- a/.clang-format +++ b/.clang-format @@ -20,7 +20,7 @@ AlwaysBreakTemplateDeclarations: Yes BinPackArguments: false BinPackParameters: false BreakBeforeBraces: Allman # Always break before braces, to match existing SRB2 code -BreakConstructorInitializers: BeforeComma +BreakConstructorInitializers: AfterColon CompactNamespaces: true ConstructorInitializerAllOnOneLineOrOnePerLine: true Cpp11BracedListStyle: true @@ -55,3 +55,4 @@ SpacesInConditionalStatement: false SpacesInContainerLiterals: false SpacesInParentheses: false SpacesInSquareBrackets: false +PenaltyReturnTypeOnItsOwnLine: 1000 diff --git a/CMakeLists.txt b/CMakeLists.txt index 151a7a389..455c4b0b4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -132,6 +132,9 @@ if("${SRB2_CONFIG_SYSTEM_LIBRARIES}") find_package(SDL2 REQUIRED) find_package(CURL REQUIRED) find_package(GME REQUIRED) + find_package(VPX REQUIRED) + find_package(Vorbis REQUIRED) + find_package(VorbisEnc REQUIRED) endif() if(${PROJECT_SOURCE_DIR} MATCHES ${PROJECT_BINARY_DIR}) diff --git a/cmake/Modules/FindVPX.cmake b/cmake/Modules/FindVPX.cmake new file mode 100644 index 000000000..f47aee65f --- /dev/null +++ b/cmake/Modules/FindVPX.cmake @@ -0,0 +1,33 @@ +include(LibFindMacros) + +libfind_pkg_check_modules(VPX_PKGCONF VPX) + +find_path(VPX_INCLUDE_DIR + NAMES vpx/vp8.h + PATHS + ${VPX_PKGCONF_INCLUDE_DIRS} + "/usr/include" + "/usr/local/include" +) + +find_library(VPX_LIBRARY + NAMES vpx + PATHS + ${VPX_PKGCONF_LIBRARY_DIRS} + "/usr/lib" + "/usr/local/lib" +) + +set(VPX_PROCESS_INCLUDES VPX_INCLUDE_DIR) +set(VPX_PROCESS_LIBS VPX_LIBRARY) +libfind_process(VPX) + +if(VPX_FOUND AND NOT TARGET webm::libvpx) + add_library(webm::libvpx UNKNOWN IMPORTED) + set_target_properties( + webm::libvpx + PROPERTIES + IMPORTED_LOCATION "${VPX_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${VPX_INCLUDE_DIR}" + ) +endif() diff --git a/cmake/Modules/FindVorbis.cmake b/cmake/Modules/FindVorbis.cmake new file mode 100644 index 000000000..c24a57ca2 --- /dev/null +++ b/cmake/Modules/FindVorbis.cmake @@ -0,0 +1,33 @@ +include(LibFindMacros) + +libfind_pkg_check_modules(Vorbis_PKGCONF Vorbis) + +find_path(Vorbis_INCLUDE_DIR + NAMES vorbis/codec.h + PATHS + ${Vorbis_PKGCONF_INCLUDE_DIRS} + "/usr/include" + "/usr/local/include" +) + +find_library(Vorbis_LIBRARY + NAMES vorbis + PATHS + ${Vorbis_PKGCONF_LIBRARY_DIRS} + "/usr/lib" + "/usr/local/lib" +) + +set(Vorbis_PROCESS_INCLUDES Vorbis_INCLUDE_DIR) +set(Vorbis_PROCESS_LIBS Vorbis_LIBRARY) +libfind_process(Vorbis) + +if(Vorbis_FOUND AND NOT TARGET Vorbis::vorbis) + add_library(Vorbis::vorbis UNKNOWN IMPORTED) + set_target_properties( + Vorbis::vorbis + PROPERTIES + IMPORTED_LOCATION "${Vorbis_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${Vorbis_INCLUDE_DIR}" + ) +endif() diff --git a/cmake/Modules/FindVorbisEnc.cmake b/cmake/Modules/FindVorbisEnc.cmake new file mode 100644 index 000000000..f429001fc --- /dev/null +++ b/cmake/Modules/FindVorbisEnc.cmake @@ -0,0 +1,33 @@ +include(LibFindMacros) + +libfind_pkg_check_modules(VorbisEnc_PKGCONF VorbisEnc) + +find_path(VorbisEnc_INCLUDE_DIR + NAMES vorbis/vorbisenc.h + PATHS + ${VorbisEnc_PKGCONF_INCLUDE_DIRS} + "/usr/include" + "/usr/local/include" +) + +find_library(VorbisEnc_LIBRARY + NAMES vorbisenc + PATHS + ${VorbisEnc_PKGCONF_LIBRARY_DIRS} + "/usr/lib" + "/usr/local/lib" +) + +set(VorbisEnc_PROCESS_INCLUDES VorbisEnc_INCLUDE_DIR) +set(VorbisEnc_PROCESS_LIBS VorbisEnc_LIBRARY) +libfind_process(VorbisEnc) + +if(VorbisEnc_FOUND AND NOT TARGET Vorbis::vorbisenc) + add_library(Vorbis::vorbisenc UNKNOWN IMPORTED) + set_target_properties( + Vorbis::vorbisenc + PROPERTIES + IMPORTED_LOCATION "${VorbisEnc_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${VorbisEnc_INCLUDE_DIR}" + ) +endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ac592f89d..898fcc789 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -32,6 +32,7 @@ add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32 m_aatree.c m_anigif.c m_argv.c + m_avrecorder.cpp m_bbox.c m_cheat.c m_cond.c @@ -229,6 +230,9 @@ target_link_libraries(SRB2SDL2 PRIVATE xmp-lite::xmp-lite) target_link_libraries(SRB2SDL2 PRIVATE glad::glad) target_link_libraries(SRB2SDL2 PRIVATE fmt) target_link_libraries(SRB2SDL2 PRIVATE imgui::imgui) +target_link_libraries(SRB2SDL2 PRIVATE webm::libwebm webm::libvpx) +target_link_libraries(SRB2SDL2 PRIVATE libyuv::libyuv) +target_link_libraries(SRB2SDL2 PRIVATE Vorbis::vorbis Vorbis::vorbisenc) target_link_libraries(SRB2SDL2 PRIVATE acsvm) @@ -546,6 +550,7 @@ if(SRB2_CONFIG_ENABLE_TESTS) add_subdirectory(tests) endif() add_subdirectory(menus) +add_subdirectory(media) # strip debug symbols into separate file when using gcc. # to be consistent with Makefile, don't generate for OS X. diff --git a/src/d_netcmd.c b/src/d_netcmd.c index 96b8cce4c..7fc0cf2a7 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -62,6 +62,7 @@ #include "deh_tables.h" #include "m_perfstats.h" #include "k_specialstage.h" +#include "m_avrecorder.h" #ifdef HAVE_DISCORDRPC #include "discord.h" @@ -903,6 +904,7 @@ void D_RegisterClientCommands(void) CV_RegisterVar(&cv_moviemode); CV_RegisterVar(&cv_movie_option); CV_RegisterVar(&cv_movie_folder); + M_AVRecorder_AddCommands(); // PNG variables CV_RegisterVar(&cv_zlib_level); CV_RegisterVar(&cv_zlib_memory); @@ -4942,23 +4944,30 @@ static void PointLimit_OnChange(void) return; } - // Don't allow pointlimit in non-pointlimited gametypes! - if (server && Playing() && !(gametyperules & GTR_POINTLIMIT)) + if (gamestate == GS_LEVEL && leveltime < starttime) { if (cv_pointlimit.value) - CV_StealthSetValue(&cv_pointlimit, 0); - return; - } + { + CONS_Printf(M_GetText("Point limit has been set to %d.\n"), cv_pointlimit.value); + } + else + { + CONS_Printf(M_GetText("Point limit has been disabled.\n")); + } - if (cv_pointlimit.value) - { - CONS_Printf(M_GetText("Levels will end after %s scores %d point%s.\n"), - G_GametypeHasTeams() ? M_GetText("a team") : M_GetText("someone"), - cv_pointlimit.value, - cv_pointlimit.value > 1 ? "s" : ""); + g_pointlimit = cv_pointlimit.value; } else - CONS_Printf(M_GetText("Point limit disabled\n")); + { + if (cv_pointlimit.value) + { + CONS_Printf(M_GetText("Point limit will be %d next round.\n"), cv_pointlimit.value); + } + else + { + CONS_Printf(M_GetText("Point limit will be disabled next round.\n")); + } + } } static void NetTimeout_OnChange(void) @@ -4983,6 +4992,8 @@ UINT32 timelimitintics = 0; UINT32 extratimeintics = 0; UINT32 secretextratime = 0; +UINT32 g_pointlimit = 0; + /** Deals with a timelimit change by printing the change to the console. * If the gametype is single player, cooperative, or race, the timelimit is * silently disabled again. @@ -6463,7 +6474,7 @@ static void Command_ShowScores_f(void) // FIXME: %lu? what's wrong with %u? ~Callum (produces warnings...) CONS_Printf(M_GetText("%s's score is %u\n"), player_names[i], players[i].score); } - CONS_Printf(M_GetText("The pointlimit is %d\n"), cv_pointlimit.value); + CONS_Printf(M_GetText("The pointlimit is %d\n"), g_pointlimit); } diff --git a/src/d_netcmd.h b/src/d_netcmd.h index c9006a7ba..61171d393 100644 --- a/src/d_netcmd.h +++ b/src/d_netcmd.h @@ -61,6 +61,7 @@ extern consvar_t cv_pointlimit; extern consvar_t cv_timelimit; extern consvar_t cv_numlaps; extern UINT32 timelimitintics, extratimeintics, secretextratime; +extern UINT32 g_pointlimit; extern consvar_t cv_allowexitlevel; extern consvar_t cv_autobalance; diff --git a/src/d_player.h b/src/d_player.h index e71063fe4..12cab4a9a 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -661,6 +661,8 @@ struct player_t UINT8 eggmanTransferDelay; + UINT8 tripwireReboundDelay; // When failing Tripwire, brieftly lock out speed-based tripwire pass (anti-cheese) + mobj_t *stumbleIndicator; #ifdef HWRENDER diff --git a/src/g_game.c b/src/g_game.c index e522d82e2..1919ecb0c 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -4359,7 +4359,7 @@ void G_LoadGameSettings(void) } #define GD_VERSIONCHECK 0xBA5ED123 // Change every major version, as usual -#define GD_VERSIONMINOR 0 // Change every format update +#define GD_VERSIONMINOR 1 // Change every format update // G_LoadGameData // Loads the main data file, which stores information such as emblems found, etc. @@ -4369,6 +4369,7 @@ void G_LoadGameData(void) UINT32 versionID; UINT8 versionMinor; UINT8 rtemp; + boolean gridunusable = false; savebuffer_t save = {0}; //For records @@ -4423,6 +4424,10 @@ void G_LoadGameData(void) P_SaveBufferFree(&save); I_Error("Game data is from the future! (expected %d, got %d)", GD_VERSIONMINOR, versionMinor); } + if (versionMinor == 0) + { + gridunusable = true; + } gamedata->totalplaytime = READUINT32(save.p); gamedata->matchesplayed = READUINT32(save.p); @@ -4469,21 +4474,34 @@ void G_LoadGameData(void) i += j; } - gamedata->challengegridwidth = READUINT16(save.p); - Z_Free(gamedata->challengegrid); - if (gamedata->challengegridwidth) + if (gridunusable) { - gamedata->challengegrid = Z_Malloc( - (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT * sizeof(UINT8)), - PU_STATIC, NULL); - for (i = 0; i < (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT); i++) - { - gamedata->challengegrid[i] = READUINT8(save.p); - } + UINT16 burn = READUINT16(save.p); // Previous challengegridwidth + UINT8 height = (versionMinor > 0) ? CHALLENGEGRIDHEIGHT : 5; + save.p += (burn * height * sizeof(UINT8)); // Step over previous grid data + + gamedata->challengegridwidth = 0; + Z_Free(gamedata->challengegrid); + gamedata->challengegrid = NULL; } else { - gamedata->challengegrid = NULL; + gamedata->challengegridwidth = READUINT16(save.p); + Z_Free(gamedata->challengegrid); + if (gamedata->challengegridwidth) + { + gamedata->challengegrid = Z_Malloc( + (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT * sizeof(UINT8)), + PU_STATIC, NULL); + for (i = 0; i < (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT); i++) + { + gamedata->challengegrid[i] = READUINT8(save.p); + } + } + else + { + gamedata->challengegrid = NULL; + } } gamedata->timesBeaten = READUINT32(save.p); diff --git a/src/hu_stuff.c b/src/hu_stuff.c index a457fe774..cd4277dc6 100644 --- a/src/hu_stuff.c +++ b/src/hu_stuff.c @@ -2453,10 +2453,10 @@ static void HU_DrawRankings(void) timedone = true; } - else if ((gametyperules & GTR_POINTLIMIT) && cv_pointlimit.value > 0) + else if ((gametyperules & GTR_POINTLIMIT) && g_pointlimit > 0) { V_DrawCenteredString(64, 8, 0, "POINT LIMIT"); - V_DrawCenteredString(64, 16, hilicol, va("%d", cv_pointlimit.value)); + V_DrawCenteredString(64, 16, hilicol, va("%d", g_pointlimit)); pointsdone = true; } else if (gametyperules & GTR_CIRCUIT) @@ -2494,10 +2494,10 @@ static void HU_DrawRankings(void) V_DrawCenteredString(256, 16, hilicol, "OVERTIME"); } } - else if (!pointsdone && (gametyperules & GTR_POINTLIMIT) && cv_pointlimit.value > 0) + else if (!pointsdone && (gametyperules & GTR_POINTLIMIT) && g_pointlimit > 0) { V_DrawCenteredString(256, 8, 0, "POINT LIMIT"); - V_DrawCenteredString(256, 16, hilicol, va("%d", cv_pointlimit.value)); + V_DrawCenteredString(256, 16, hilicol, va("%d", g_pointlimit)); } else if (gametyperules & GTR_CIRCUIT) { diff --git a/src/hwr2/pass_software.cpp b/src/hwr2/pass_software.cpp index 4876a52cd..f3bfd69ef 100644 --- a/src/hwr2/pass_software.cpp +++ b/src/hwr2/pass_software.cpp @@ -10,6 +10,7 @@ #include "../discord.h" #endif #include "../doomstat.h" +#include "../m_avrecorder.h" #include "../st_stuff.h" #include "../s_sound.h" #include "../v_video.h" @@ -121,6 +122,8 @@ static void temp_legacy_finishupdate_draws() } if (cv_mindelay.value && consoleplayer == serverplayer && Playing()) SCR_DisplayLocalPing(); + + M_AVRecorder_DrawFrameRate(); } if (marathonmode) diff --git a/src/i_sound.h b/src/i_sound.h index cacb96923..7df4b5346 100644 --- a/src/i_sound.h +++ b/src/i_sound.h @@ -55,6 +55,10 @@ void I_StartupSound(void); */ void I_ShutdownSound(void); +/** \brief Update instance of AVRecorder for audio capture. +*/ +void I_UpdateAudioRecorder(void); + /// ------------------------ /// SFX I/O /// ------------------------ diff --git a/src/info.c b/src/info.c index 2c22b4bf4..4807869e6 100644 --- a/src/info.c +++ b/src/info.c @@ -8037,7 +8037,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] = S_NULL, // missilestate S_SPRK1, // deathstate S_NULL, // xdeathstate - sfx_ncitem, // deathsound + sfx_None, // deathsound 1, // speed 16*FRACUNIT, // radius 30*FRACUNIT, // height diff --git a/src/k_hud.h b/src/k_hud.h index b80760c2c..e9b0603f2 100644 --- a/src/k_hud.h +++ b/src/k_hud.h @@ -46,7 +46,6 @@ void K_DrawMapThumbnail(INT32 x, INT32 y, INT32 width, UINT32 flags, UINT16 map, void K_DrawLikeMapThumbnail(INT32 x, INT32 y, INT32 width, UINT32 flags, patch_t *patch, UINT8 *colormap); void K_drawTargetHUD(const vector3_t *origin, player_t *player); -extern patch_t *kp_facehighlight[8]; extern patch_t *kp_capsuletarget_arrow[2][2]; extern patch_t *kp_capsuletarget_icon[2]; extern patch_t *kp_capsuletarget_far[2]; diff --git a/src/k_kart.c b/src/k_kart.c index fa042f235..61f7dc2f9 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -100,6 +100,7 @@ void K_TimerReset(void) numbulbs = 1; inDuel = rainbowstartavailable = false; timelimitintics = extratimeintics = secretextratime = 0; + g_pointlimit = 0; } void K_TimerInit(void) @@ -206,6 +207,11 @@ void K_TimerInit(void) } } + if (gametyperules & GTR_POINTLIMIT) + { + g_pointlimit = cv_pointlimit.value; + } + if (inDuel == true) { K_SpawnDuelOnlyItems(); @@ -2683,7 +2689,7 @@ tripwirepass_t K_TripwirePassConditions(player_t *player) if ( player->flamedash || - player->speed > 2 * K_GetKartSpeed(player, false, false) + (player->speed > 2 * K_GetKartSpeed(player, false, false) && player->tripwireReboundDelay == 0) ) return TRIPWIRE_BOOST; @@ -4080,10 +4086,17 @@ void K_TumbleInterrupt(player_t *player) void K_ApplyTripWire(player_t *player, tripwirestate_t state) { + // We are either softlocked or wildly misbehaving. Stop that! + if (state == TRIPSTATE_BLOCKED && player->tripwireReboundDelay && (player->speed > 5 * K_GetKartSpeed(player, false, false))) + K_TumblePlayer(player, NULL, NULL); + if (state == TRIPSTATE_PASSED) S_StartSound(player->mo, sfx_ssa015); else if (state == TRIPSTATE_BLOCKED) + { S_StartSound(player->mo, sfx_kc40); + player->tripwireReboundDelay = 60; + } player->tripwireState = state; K_AddHitLag(player->mo, 10, false); @@ -7715,6 +7728,9 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd) if (player->eggmanTransferDelay) player->eggmanTransferDelay--; + if (player->tripwireReboundDelay) + player->tripwireReboundDelay--; + if (player->ringdelay) player->ringdelay--; diff --git a/src/k_menu.h b/src/k_menu.h index ec6a96111..fb08ff4ab 100644 --- a/src/k_menu.h +++ b/src/k_menu.h @@ -1137,6 +1137,8 @@ void M_DrawAddons(void); #define CC_ANIM 3 #define CC_MAX 4 +#define TILEFLIP_MAX 16 + // Keep track of some pause menu data for visual goodness. extern struct challengesmenu_s { @@ -1151,7 +1153,7 @@ extern struct challengesmenu_s { SINT8 row, hilix, focusx; UINT8 col, hiliy; - UINT8 *extradata; + challengegridextradata_t *extradata; boolean pending; boolean requestnew; diff --git a/src/k_menudraw.c b/src/k_menudraw.c index 32476585a..5f5f69726 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -1479,7 +1479,7 @@ static void M_DrawCharSelectPreview(UINT8 num) static void M_DrawCharSelectExplosions(boolean charsel, INT16 basex, INT16 basey) { UINT8 i; - INT16 quadx = 0, quady = 0; + INT16 quadx = 2, quady = 2, mul = 22; for (i = 0; i < CSEXPLOSIONS; i++) { @@ -1495,13 +1495,14 @@ static void M_DrawCharSelectExplosions(boolean charsel, INT16 basex, INT16 basey { quadx = 4 * (setup_explosions[i].x / 3); quady = 4 * (setup_explosions[i].y / 3); + mul = 16; } colormap = R_GetTranslationColormap(TC_DEFAULT, setup_explosions[i].color, GTC_MENUCACHE); V_DrawMappedPatch( - basex + (setup_explosions[i].x*16) + quadx - 6, - basey + (setup_explosions[i].y*16) + quady - 6, + basex + (setup_explosions[i].x*mul) + quadx - 6, + basey + (setup_explosions[i].y*mul) + quady - 6, 0, W_CachePatchName(va("CHCNFRM%d", frame), PU_CACHE), colormap ); @@ -4516,10 +4517,11 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili { unlockable_t *ref = NULL; patch_t *pat = missingpat; - UINT8 *colormap = NULL; - fixed_t siz; + UINT8 *colormap = NULL, *bgmap = NULL; + fixed_t siz, accordion; UINT8 id, num; - UINT32 edgelength; + boolean unlockedyet; + boolean categoryside; id = (i * CHALLENGEGRIDHEIGHT) + j; num = gamedata->challengegrid[id]; @@ -4533,18 +4535,116 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili // Okay, this is what we want to draw. ref = &unlockables[num]; - edgelength = (ref->majorunlock ? 30 : 14); + unlockedyet = !((gamedata->unlocked[num] == false) + || (challengesmenu.pending && num == challengesmenu.currentunlock && challengesmenu.unlockanim <= UNLOCKTIME)); - // ...unless we simply aren't unlocked yet. - if ((gamedata->unlocked[num] == false) - || (challengesmenu.pending && num == challengesmenu.currentunlock && challengesmenu.unlockanim <= UNLOCKTIME)) + // If we aren't unlocked yet, return early. + if (!unlockedyet) { - V_DrawFill(x+1, y+1, edgelength, edgelength, - ((challengesmenu.extradata[id] == CHE_HINT) ? 132 : 11)); + UINT32 flags = 0; + + if (challengesmenu.extradata[id].flags != CHE_HINT) + { + colormap = R_GetTranslationColormap(TC_BLINK, SKINCOLOR_BLACK, GTC_CACHE); + flags = V_SUBTRACT|V_90TRANS; + } + + pat = W_CachePatchName( + va("UN_HNT%c%c", + (hili && !colormap) ? '1' : '2', + ref->majorunlock ? 'B' : 'A' + ), + PU_CACHE); + + V_DrawFixedPatch( + x*FRACUNIT, y*FRACUNIT, + FRACUNIT, + flags, pat, + colormap + ); + + pat = missingpat; + colormap = NULL; + goto drawborder; } - if (ref->icon != NULL && ref->icon[0]) + accordion = FRACUNIT; + + if (challengesmenu.extradata[id].flip != 0 + && challengesmenu.extradata[id].flip != (TILEFLIP_MAX/2)) + { + angle_t bad = (FixedAngle((fixed_t)(challengesmenu.extradata[id].flip) * (360*FRACUNIT/TILEFLIP_MAX)) >> ANGLETOFINESHIFT) & FINEMASK; + accordion = FINECOSINE(bad); + if (accordion < 0) + accordion = -accordion; + } + + pat = W_CachePatchName( + (ref->majorunlock ? "UN_BORDB" : "UN_BORDA"), + PU_CACHE); + + bgmap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_SILVER, GTC_MENUCACHE); + + V_DrawStretchyFixedPatch( + (x*FRACUNIT) + (SHORT(pat->width)*(FRACUNIT-accordion)/2), y*FRACUNIT, + accordion, + FRACUNIT, + 0, pat, + bgmap + ); + + pat = missingpat; + + categoryside = (challengesmenu.extradata[id].flip <= TILEFLIP_MAX/4 + || challengesmenu.extradata[id].flip > (3*TILEFLIP_MAX)/4); + + if (categoryside) + { + char categoryid = '8'; + colormap = bgmap; + switch (ref->type) + { + case SECRET_SKIN: + categoryid = '1'; + break; + case SECRET_FOLLOWER: + categoryid = '2'; + break; + /*case SECRET_COLOR: + categoryid = '3'; + break;*/ + case SECRET_CUP: + categoryid = '4'; + break; + //case SECRET_MASTERBOTS: + case SECRET_HARDSPEED: + case SECRET_ENCORE: + categoryid = '5'; + break; + case SECRET_ALTTITLE: + case SECRET_SOUNDTEST: + categoryid = '6'; + break; + case SECRET_TIMEATTACK: + case SECRET_BREAKTHECAPSULES: + case SECRET_SPECIALATTACK: + categoryid = '7'; + break; + } + pat = W_CachePatchName(va("UN_RR0%c%c", + categoryid, + (ref->majorunlock) ? 'B' : 'A'), + PU_CACHE); + if (pat == missingpat) + { + pat = W_CachePatchName(va("UN_RR0%c%c", + categoryid, + (ref->majorunlock) ? 'A' : 'B'), + PU_CACHE); + } + } + else if (ref->icon != NULL && ref->icon[0]) { pat = W_CachePatchName(ref->icon, PU_CACHE); if (ref->color != SKINCOLOR_NONE && ref->color < numskincolors) @@ -4552,58 +4652,110 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili colormap = R_GetTranslationColormap(TC_DEFAULT, ref->color, GTC_MENUCACHE); } } - else switch (ref->type) + else { - case SECRET_SKIN: + UINT8 iconid = 0; + switch (ref->type) { - INT32 skin = M_UnlockableSkinNum(ref); - if (skin != -1) + case SECRET_SKIN: { - colormap = R_GetTranslationColormap(skin, skins[skin].prefcolor, GTC_MENUCACHE); - pat = faceprefix[skin][(ref->majorunlock) ? FACE_WANTED : FACE_RANK]; + INT32 skin = M_UnlockableSkinNum(ref); + if (skin != -1) + { + colormap = R_GetTranslationColormap(skin, skins[skin].prefcolor, GTC_MENUCACHE); + pat = faceprefix[skin][(ref->majorunlock) ? FACE_WANTED : FACE_RANK]; + } + break; + } + case SECRET_FOLLOWER: + { + INT32 skin = M_UnlockableFollowerNum(ref); + if (skin != -1) + { + UINT16 col = K_GetEffectiveFollowerColor(followers[skin].defaultcolor, cv_playercolor[0].value); + colormap = R_GetTranslationColormap(TC_DEFAULT, col, GTC_MENUCACHE); + pat = W_CachePatchName(followers[skin].icon, PU_CACHE); + } + break; + } + + /*case SECRET_MASTERBOTS: + iconid = 4; + break;*/ + case SECRET_HARDSPEED: + iconid = 3; + break; + case SECRET_ENCORE: + iconid = 5; + break; + + case SECRET_ALTTITLE: + iconid = 6; + break; + case SECRET_SOUNDTEST: + iconid = 1; + break; + + case SECRET_TIMEATTACK: + iconid = 7; + break; + case SECRET_BREAKTHECAPSULES: + iconid = 8; + break; + case SECRET_SPECIALATTACK: + iconid = 9; + break; + + default: + { + if (!colormap && ref->color != SKINCOLOR_NONE && ref->color < numskincolors) + { + colormap = R_GetTranslationColormap(TC_RAINBOW, ref->color, GTC_MENUCACHE); + } + break; } - break; } - case SECRET_FOLLOWER: + + if (pat == missingpat) { - INT32 skin = M_UnlockableFollowerNum(ref); - if (skin != -1) + pat = W_CachePatchName(va("UN_IC%02u%c", + iconid, + ref->majorunlock ? 'B' : 'A'), + PU_CACHE); + if (pat == missingpat) { - UINT16 col = K_GetEffectiveFollowerColor(followers[skin].defaultcolor, cv_playercolor[0].value); - colormap = R_GetTranslationColormap(TC_DEFAULT, col, GTC_MENUCACHE); - pat = W_CachePatchName(followers[skin].icon, PU_CACHE); + pat = W_CachePatchName(va("UN_IC%02u%c", + iconid, + ref->majorunlock ? 'A' : 'B'), + PU_CACHE); } - break; - } - default: - { - pat = W_CachePatchName(va("UN_RR00%c", ref->majorunlock ? 'B' : 'A'), PU_CACHE); - if (ref->color != SKINCOLOR_NONE && ref->color < numskincolors) - { - //CONS_Printf(" color for %d is %s\n", num, skincolors[unlockables[num].color].name); - colormap = R_GetTranslationColormap(TC_RAINBOW, ref->color, GTC_MENUCACHE); - } - break; } } siz = (SHORT(pat->width) << FRACBITS); - siz = FixedDiv(((ref->majorunlock) ? 32 : 16) << FRACBITS, siz); - V_SetClipRect( - (x+1) << FRACBITS, (y+1) << FRACBITS, - edgelength << FRACBITS, edgelength << FRACBITS, - 0 - ); - - V_DrawFixedPatch( - x*FRACUNIT, y*FRACUNIT, - siz, - 0, pat, - colormap - ); - - V_ClearClipRect(); + if (!siz) + ; // prevent div/0 + else if (ref->majorunlock) + { + V_DrawStretchyFixedPatch( + ((x + 5)*FRACUNIT) + (32*(FRACUNIT-accordion)/2), (y + 5)*FRACUNIT, + FixedDiv(32*accordion, siz), + FixedDiv(32 << FRACBITS, siz), + 0, pat, + colormap + ); + } + else + { + V_DrawStretchyFixedPatch( + ((x + 2)*FRACUNIT) + (16*(FRACUNIT-accordion)/2), (y + 2)*FRACUNIT, + FixedDiv(16*accordion, siz), + FixedDiv(16 << FRACBITS, siz), + 0, pat, + colormap + ); + } drawborder: if (!hili) @@ -4611,12 +4763,23 @@ drawborder: return; } - V_DrawFixedPatch( - x*FRACUNIT, y*FRACUNIT, - ((ref != NULL && ref->majorunlock) ? FRACUNIT*2 : FRACUNIT), - 0, kp_facehighlight[(challengesmenu.ticker / 4) % 8], - NULL - ); + { + boolean maj = (ref != NULL && ref->majorunlock); + char buffer[9]; + sprintf(buffer, "UN_RETA1"); + buffer[6] = maj ? 'B' : 'A'; + buffer[7] = (skullAnimCounter/5) ? '2' : '1'; + pat = W_CachePatchName(buffer, PU_CACHE); + + colormap = R_GetTranslationColormap(TC_DEFAULT, cv_playercolor[0].value, GTC_MENUCACHE); + + V_DrawFixedPatch( + x*FRACUNIT, y*FRACUNIT, + FRACUNIT, + 0, pat, + colormap + ); + } } static void M_DrawChallengePreview(INT32 x, INT32 y) @@ -4819,6 +4982,9 @@ static void M_DrawChallengePreview(INT32 x, INT32 y) } } +#define challengetransparentstrength 8 +#define challengesgridstep 22 + void M_DrawChallenges(void) { INT32 x = currentMenu->x, explodex, selectx; @@ -4828,8 +4994,25 @@ void M_DrawChallenges(void) INT16 offset; { - patch_t *bg = W_CachePatchName("BGUNLCK2", PU_CACHE); +#define questionslow 4 // slows down the scroll by this factor +#define questionloop (questionslow*100) // modulo + INT32 questionoffset = (challengesmenu.ticker % questionloop); + patch_t *bg = W_CachePatchName("BGUNLCKG", PU_CACHE); + patch_t *qm = W_CachePatchName("BGUNLSC", PU_CACHE); + + // Background gradient V_DrawFixedPatch(0, 0, FRACUNIT, 0, bg, NULL); + + // Scrolling question mark overlay + V_DrawFixedPatch( + -((160 + questionoffset)*FRACUNIT)/questionslow, + -(4*FRACUNIT) - (245*(FixedDiv((questionloop - questionoffset)*FRACUNIT, questionloop*FRACUNIT))), + FRACUNIT, + V_MODULATE, + qm, + NULL); +#undef questionslow +#undef questionloop } if (gamedata->challengegrid == NULL || challengesmenu.extradata == NULL) @@ -4838,43 +5021,45 @@ void M_DrawChallenges(void) goto challengedesc; } - x -= 16; + V_DrawFadeFill(0, y-2, BASEVIDWIDTH, 90, 0, 31, challengetransparentstrength); + + x -= (challengesgridstep-1); x += challengesmenu.offset; if (challengegridloops) { if (!challengesmenu.col && challengesmenu.hilix) - x -= gamedata->challengegridwidth*16; + x -= gamedata->challengegridwidth*challengesgridstep; i = challengesmenu.col + challengesmenu.focusx; - explodex = x - (i*16); + explodex = x - (i*challengesgridstep); - while (x < BASEVIDWIDTH-16) + while (x < BASEVIDWIDTH-challengesgridstep) { i = (i + 1) % gamedata->challengegridwidth; - x += 16; + x += challengesgridstep; } } else { if (gamedata->challengegridwidth & 1) - x += 8; + x += (challengesgridstep/2); i = gamedata->challengegridwidth-1; - explodex = x - (i*16)/2; - x += (i*16)/2; + explodex = x - (i*challengesgridstep)/2; + x += (i*challengesgridstep)/2; } - selectx = explodex + (challengesmenu.hilix*16); + selectx = explodex + (challengesmenu.hilix*challengesgridstep); - while (i >= 0 && x >= -32) + while (i >= 0 && x >= -(challengesgridstep*2)) { - y = currentMenu->y-16; + y = currentMenu->y-challengesgridstep; for (j = 0; j < CHALLENGEGRIDHEIGHT; j++) { - y += 16; + y += challengesgridstep; - if (challengesmenu.extradata[(i * CHALLENGEGRIDHEIGHT) + j] & CHE_DONTDRAW) + if (challengesmenu.extradata[(i * CHALLENGEGRIDHEIGHT) + j].flags & CHE_DONTDRAW) { continue; } @@ -4887,7 +5072,7 @@ void M_DrawChallenges(void) M_DrawChallengeTile(i, j, x, y, false); } - x -= 16; + x -= challengesgridstep; i--; if (challengegridloops && i < 0) { @@ -4903,7 +5088,7 @@ void M_DrawChallenges(void) challengesmenu.hilix, challengesmenu.hiliy, selectx, - currentMenu->y + (challengesmenu.hiliy*16), + currentMenu->y + (challengesmenu.hiliy*challengesgridstep), true); M_DrawCharSelectExplosions(false, explodex, currentMenu->y); @@ -4922,6 +5107,12 @@ challengedesc: { y = 120; + V_DrawScaledPatch(0, y, + (10-challengetransparentstrength)<unlocked[challengesmenu.currentunlock] == true) || ((challengesmenu.extradata != NULL) - && (challengesmenu.extradata[i] & CHE_HINT)) + && (challengesmenu.extradata[i].flags & CHE_HINT)) ) ) { @@ -4961,6 +5152,9 @@ challengedesc: } } +#undef challengetransparentstrength +#undef challengesgridstep + // Statistics menu #define STATSSTEP 10 diff --git a/src/lua_playerlib.c b/src/lua_playerlib.c index 7195c90f0..211f83867 100644 --- a/src/lua_playerlib.c +++ b/src/lua_playerlib.c @@ -306,6 +306,8 @@ static int player_get(lua_State *L) lua_pushinteger(L, plr->tripwirePass); else if (fastcmp(field,"tripwireLeniency")) lua_pushinteger(L, plr->tripwireLeniency); + else if (fastcmp(field,"tripwireReboundDelay")) + lua_pushinteger(L, plr->tripwireReboundDelay); /* else if (fastcmp(field,"itemroulette")) lua_pushinteger(L, plr->itemroulette); @@ -684,6 +686,8 @@ static int player_set(lua_State *L) plr->tripwirePass = luaL_checkinteger(L, 3); else if (fastcmp(field,"tripwireLeniency")) plr->tripwireLeniency = luaL_checkinteger(L, 3); + else if (fastcmp(field,"tripwireReboundDelay")) + plr->tripwireReboundDelay = luaL_checkinteger(L, 3); /* else if (fastcmp(field,"itemroulette")) plr->itemroulette = luaL_checkinteger(L, 3); diff --git a/src/lua_script.c b/src/lua_script.c index 17dd61a8d..ed35d9e69 100644 --- a/src/lua_script.c +++ b/src/lua_script.c @@ -205,7 +205,7 @@ int LUA_PushGlobals(lua_State *L, const char *word) lua_pushinteger(L, timelimitintics); return 1; } else if (fastcmp(word,"pointlimit")) { - lua_pushinteger(L, cv_pointlimit.value); + lua_pushinteger(L, g_pointlimit); return 1; // begin map vars } else if (fastcmp(word,"titlemap")) { diff --git a/src/m_avrecorder.cpp b/src/m_avrecorder.cpp new file mode 100644 index 000000000..7491fb195 --- /dev/null +++ b/src/m_avrecorder.cpp @@ -0,0 +1,252 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "cxxutil.hpp" +#include "m_avrecorder.hpp" +#include "media/options.hpp" + +#include "command.h" +#include "i_sound.h" +#include "m_avrecorder.h" +#include "m_fixed.h" +#include "screen.h" // vid global +#include "st_stuff.h" // st_palette +#include "v_video.h" // pLocalPalette + +using namespace srb2::media; + +namespace +{ + +namespace Res +{ + +// Using an unscoped enum here so it can implicitly cast to +// int (in CV_PossibleValue_t). Wrap this in a namespace so +// access is still scoped. E.g. Res::kGame + +enum : int32_t +{ + kGame, // user chosen resolution, vid.width + kBase, // smallest version maintaining aspect ratio, vid.width / vid.dupx + kBase2x, + kBase4x, + kWindow, // window size (monitor in fullscreen), vid.realwidth + kCustom, // movie_custom_resolution +}; + +}; // namespace Res + +CV_PossibleValue_t movie_resolution_cons_t[] = { + {Res::kGame, "Native"}, + {Res::kBase, "Small"}, + {Res::kBase2x, "Medium"}, + {Res::kBase4x, "Large"}, + {Res::kWindow, "Window"}, + {Res::kCustom, "Custom"}, + {0, NULL}}; + +CV_PossibleValue_t movie_limit_cons_t[] = {{1, "MIN"}, {INT32_MAX, "MAX"}, {0, "Unlimited"}, {0, NULL}}; + +}; // namespace + +consvar_t cv_movie_resolution = CVAR_INIT("movie_resolution", "Medium", CV_SAVE, movie_resolution_cons_t, NULL); +consvar_t cv_movie_custom_resolution = CVAR_INIT("movie_custom_resolution", "640x400", CV_SAVE, NULL, NULL); + +consvar_t cv_movie_fps = CVAR_INIT("movie_fps", "60", CV_SAVE, CV_Natural, NULL); +consvar_t cv_movie_showfps = CVAR_INIT("movie_showfps", "Yes", CV_SAVE, CV_YesNo, NULL); + +consvar_t cv_movie_sound = CVAR_INIT("movie_sound", "On", CV_SAVE, CV_OnOff, NULL); + +consvar_t cv_movie_duration = CVAR_INIT("movie_duration", "Unlimited", CV_SAVE | CV_FLOAT, movie_limit_cons_t, NULL); +consvar_t cv_movie_size = CVAR_INIT("movie_size", "8.0", CV_SAVE | CV_FLOAT, movie_limit_cons_t, NULL); + +std::shared_ptr g_av_recorder; + +void M_AVRecorder_AddCommands(void) +{ + CV_RegisterVar(&cv_movie_custom_resolution); + CV_RegisterVar(&cv_movie_duration); + CV_RegisterVar(&cv_movie_fps); + CV_RegisterVar(&cv_movie_resolution); + CV_RegisterVar(&cv_movie_showfps); + CV_RegisterVar(&cv_movie_size); + CV_RegisterVar(&cv_movie_sound); + + srb2::media::Options::register_all(); +} + +static AVRecorder::Config configure() +{ + AVRecorder::Config cfg {}; + + if (cv_movie_duration.value > 0) + { + cfg.max_duration = std::chrono::duration(FixedToFloat(cv_movie_duration.value)); + } + + if (cv_movie_size.value > 0) + { + cfg.max_size = FixedToFloat(cv_movie_size.value) * 1024 * 1024; + } + + if (sound_started && cv_movie_sound.value) + { + cfg.audio = { + .sample_rate = 44100, + }; + } + + cfg.video = { + .frame_rate = cv_movie_fps.value, + }; + + AVRecorder::Config::Video& v = *cfg.video; + + auto basex = [&v](int scale) + { + v.width = vid.width / vid.dupx * scale; + v.height = vid.height / vid.dupy * scale; + }; + + switch (cv_movie_resolution.value) + { + case Res::kGame: + v.width = vid.width; + v.height = vid.height; + break; + + case Res::kBase: + basex(1); + break; + + case Res::kBase2x: + basex(2); + break; + + case Res::kBase4x: + basex(4); + break; + + case Res::kWindow: + v.width = vid.realwidth; + v.height = vid.realheight; + break; + + case Res::kCustom: + if (sscanf(cv_movie_custom_resolution.string, "%dx%d", &v.width, &v.height) != 2) + { + throw std::invalid_argument(fmt::format( + "Bad movie_custom_resolution '{}', should be x (e.g. 640x400)", + cv_movie_custom_resolution.string + )); + } + break; + + default: + SRB2_ASSERT(false); + } + + return cfg; +} + +boolean M_AVRecorder_Open(const char* filename) +{ + try + { + AVRecorder::Config cfg = configure(); + + cfg.file_name = filename; + + g_av_recorder = std::make_shared(cfg); + + I_UpdateAudioRecorder(); + + return true; + } + catch (const std::exception& ex) + { + CONS_Alert(CONS_ERROR, "Exception starting video recorder: %s\n", ex.what()); + return false; + } +} + +void M_AVRecorder_Close(void) +{ + g_av_recorder.reset(); + + I_UpdateAudioRecorder(); +} + +const char* M_AVRecorder_GetFileExtension(void) +{ + return AVRecorder::file_extension(); +} + +const char* M_AVRecorder_GetCurrentFormat(void) +{ + SRB2_ASSERT(g_av_recorder != nullptr); + + return g_av_recorder->format_name(); +} + +void M_AVRecorder_PrintCurrentConfiguration(void) +{ + SRB2_ASSERT(g_av_recorder != nullptr); + + g_av_recorder->print_configuration(); +} + +boolean M_AVRecorder_IsExpired(void) +{ + SRB2_ASSERT(g_av_recorder != nullptr); + + return g_av_recorder->invalid(); +} + +void M_AVRecorder_DrawFrameRate(void) +{ + if (!cv_movie_showfps.value || !g_av_recorder) + { + return; + } + + g_av_recorder->draw_statistics(); +} + +// TODO: remove once hwr2 twodee is finished +void M_AVRecorder_CopySoftwareScreen(void) +{ + SRB2_ASSERT(g_av_recorder != nullptr); + + auto frame = g_av_recorder->new_indexed_video_frame(vid.width, vid.height); + + if (!frame) + { + return; + } + + tcb::span pal(&pLocalPalette[std::max(st_palette, 0) * 256], 256); + tcb::span scr(screens[0], vid.width * vid.height); + + std::copy(pal.begin(), pal.end(), frame->palette.begin()); + std::copy(scr.begin(), scr.end(), frame->screen.begin()); + + g_av_recorder->push_indexed_video_frame(std::move(frame)); +} diff --git a/src/m_avrecorder.h b/src/m_avrecorder.h new file mode 100644 index 000000000..9cce8349c --- /dev/null +++ b/src/m_avrecorder.h @@ -0,0 +1,53 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef M_AVRECORDER_H +#define M_AVRECORDER_H + +#include "typedef.h" // consvar_t + +#ifdef __cplusplus +extern "C" { +#endif + +void M_AVRecorder_AddCommands(void); + +const char *M_AVRecorder_GetFileExtension(void); + +// True if successully opened. +boolean M_AVRecorder_Open(const char *filename); + +void M_AVRecorder_Close(void); + +// Check whether AVRecorder is still valid. Call M_AVRecorder_Close if expired. +boolean M_AVRecorder_IsExpired(void); + +const char *M_AVRecorder_GetCurrentFormat(void); + +void M_AVRecorder_PrintCurrentConfiguration(void); + +void M_AVRecorder_DrawFrameRate(void); + +// TODO: remove once hwr2 twodee is finished +void M_AVRecorder_CopySoftwareScreen(void); + +extern consvar_t + cv_movie_custom_resolution, + cv_movie_duration, + cv_movie_fps, + cv_movie_resolution, + cv_movie_showfps, + cv_movie_size, + cv_movie_sound; + +#ifdef __cplusplus +}; // extern "C" +#endif + +#endif/*M_AVRECORDER_H*/ diff --git a/src/m_avrecorder.hpp b/src/m_avrecorder.hpp new file mode 100644 index 000000000..0f032323a --- /dev/null +++ b/src/m_avrecorder.hpp @@ -0,0 +1,19 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __M_AVRECORDER_HPP__ +#define __M_AVRECORDER_HPP__ + +#include // shared_ptr + +#include "media/avrecorder.hpp" + +extern std::shared_ptr g_av_recorder; + +#endif // __M_AVRECORDER_HPP__ diff --git a/src/m_cond.c b/src/m_cond.c index abc228d3a..070c5b9ea 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1,5 +1,6 @@ // SONIC ROBO BLAST 2 //----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Vivian "toaster" Grannell. // Copyright (C) 2012-2016 by Matthew "Kaito Sinclaire" Walsh. // Copyright (C) 2012-2020 by Sonic Team Junior. // @@ -259,28 +260,34 @@ quickcheckagain: } } -UINT8 *M_ChallengeGridExtraData(void) +void M_UpdateChallengeGridExtraData(challengegridextradata_t *extradata) { UINT8 i, j, num, id, tempid, work; - UINT8 *extradata; boolean idchange; - if (!gamedata->challengegrid) + if (gamedata->challengegrid == NULL) { - return NULL; + return; } - extradata = Z_Malloc( - (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT * sizeof(UINT8)), - PU_STATIC, NULL); - - if (!extradata) + if (extradata == NULL) { - I_Error("M_ChallengeGridExtraData: was not able to allocate extradata"); + return; } //CONS_Printf(" --- \n"); + // Pre-wipe flags. + for (i = 0; i < gamedata->challengegridwidth; i++) + { + for (j = 0; j < CHALLENGEGRIDHEIGHT; j++) + { + id = (i * CHALLENGEGRIDHEIGHT) + j; + extradata[id].flags = CHE_NONE; + } + } + + // Populate extra data. for (i = 0; i < gamedata->challengegridwidth; i++) { for (j = 0; j < CHALLENGEGRIDHEIGHT; j++) @@ -289,8 +296,6 @@ UINT8 *M_ChallengeGridExtraData(void) num = gamedata->challengegrid[id]; idchange = false; - extradata[id] = CHE_NONE; - // Empty spots in the grid are always unconnected. if (num >= MAXUNLOCKABLES) { @@ -304,13 +309,13 @@ UINT8 *M_ChallengeGridExtraData(void) work = gamedata->challengegrid[tempid]; if (work == num) { - extradata[id] = CHE_CONNECTEDUP; + extradata[id].flags = CHE_CONNECTEDUP; // Get the id to write extra hint data to. // This check is safe because extradata's order of population - if (extradata[tempid] & CHE_CONNECTEDLEFT) + if (extradata[tempid].flags & CHE_CONNECTEDLEFT) { - extradata[id] |= CHE_CONNECTEDLEFT; + extradata[id].flags |= CHE_CONNECTEDLEFT; //CONS_Printf(" %d - %d above %d is invalid, check to left\n", num, tempid, id); if (i > 0) { @@ -327,14 +332,14 @@ UINT8 *M_ChallengeGridExtraData(void) id = tempid; idchange = true; - if (extradata[id] == CHE_HINT) + if (extradata[id].flags == CHE_HINT) { continue; } } else if (work < MAXUNLOCKABLES && gamedata->unlocked[work]) { - extradata[id] = CHE_HINT; + extradata[id].flags = CHE_HINT; } } @@ -356,11 +361,11 @@ UINT8 *M_ChallengeGridExtraData(void) { //CONS_Printf(" %d - %d to left of %d is valid\n", work, tempid, id); // If we haven't already updated our id, it's the one to our left. - if (extradata[id] == CHE_HINT) + if (extradata[id].flags == CHE_HINT) { - extradata[tempid] = CHE_HINT; + extradata[tempid].flags = CHE_HINT; } - extradata[id] = CHE_CONNECTEDLEFT; + extradata[id].flags = CHE_CONNECTEDLEFT; id = tempid; } /*else @@ -368,13 +373,13 @@ UINT8 *M_ChallengeGridExtraData(void) } else if (work < MAXUNLOCKABLES && gamedata->unlocked[work]) { - extradata[id] = CHE_HINT; + extradata[id].flags = CHE_HINT; continue; } } // Since we're not modifying id past this point, the conditions become much simpler. - if (extradata[id] == CHE_HINT) + if ((extradata[id].flags & (CHE_HINT|CHE_DONTDRAW)) == CHE_HINT) { continue; } @@ -391,7 +396,7 @@ UINT8 *M_ChallengeGridExtraData(void) } else if (work < MAXUNLOCKABLES && gamedata->unlocked[work]) { - extradata[id] = CHE_HINT; + extradata[id].flags = CHE_HINT; continue; } } @@ -414,14 +419,12 @@ UINT8 *M_ChallengeGridExtraData(void) } else if (work < MAXUNLOCKABLES && gamedata->unlocked[work]) { - extradata[id] = CHE_HINT; + extradata[id].flags = CHE_HINT; continue; } } } } - - return extradata; } void M_AddRawCondition(UINT8 set, UINT8 id, conditiontype_t c, INT32 r, INT16 x1, INT16 x2) @@ -909,7 +912,7 @@ boolean M_UpdateUnlockablesAndExtraEmblems(boolean loud) { if (loud) { - S_StartSound(NULL, sfx_ncitem); + S_StartSound(NULL, sfx_achiev); } return true; } diff --git a/src/m_cond.h b/src/m_cond.h index 735894c71..367f3d720 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -1,5 +1,6 @@ // SONIC ROBO BLAST 2 //----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Vivian "toaster" Grannell. // Copyright (C) 2012-2016 by Matthew "Kaito Sinclaire" Walsh. // Copyright (C) 2012-2020 by Sonic Team Junior. // @@ -138,7 +139,7 @@ typedef enum #define MAXEMBLEMS 512 #define MAXUNLOCKABLES MAXCONDITIONSETS -#define CHALLENGEGRIDHEIGHT 5 +#define CHALLENGEGRIDHEIGHT 4 #ifdef DEVELOP #define CHALLENGEGRIDLOOPWIDTH 3 #else @@ -192,12 +193,21 @@ void M_NewGameDataStruct(void); // Challenges menu stuff void M_PopulateChallengeGrid(void); -UINT8 *M_ChallengeGridExtraData(void); + +struct challengegridextradata_t +{ + UINT8 flags; + UINT8 flip; +}; + +void M_UpdateChallengeGridExtraData(challengegridextradata_t *extradata); + #define CHE_NONE 0 #define CHE_HINT 1 #define CHE_CONNECTEDLEFT (1<<1) #define CHE_CONNECTEDUP (1<<2) #define CHE_DONTDRAW (CHE_CONNECTEDLEFT|CHE_CONNECTEDUP) + char *M_BuildConditionSetString(UINT8 unlockid); #define DESCRIPTIONWIDTH 170 diff --git a/src/m_misc.c b/src/m_misc.c index 817ac152b..53e3534ac 100644 --- a/src/m_misc.c +++ b/src/m_misc.c @@ -44,6 +44,7 @@ #include "command.h" // cv_execversion #include "m_anigif.h" +#include "m_avrecorder.h" // So that the screenshot menu auto-updates... #include "k_menu.h" @@ -113,8 +114,8 @@ consvar_t cv_screenshot_folder = CVAR_INIT ("screenshot_folder", "", CV_SAVE, NU consvar_t cv_screenshot_colorprofile = CVAR_INIT ("screenshot_colorprofile", "Yes", CV_SAVE, CV_YesNo, NULL); -static CV_PossibleValue_t moviemode_cons_t[] = {{MM_GIF, "GIF"}, {MM_APNG, "aPNG"}, {MM_SCREENSHOT, "Screenshots"}, {0, NULL}}; -consvar_t cv_moviemode = CVAR_INIT ("moviemode_mode", "GIF", CV_SAVE|CV_CALL, moviemode_cons_t, Moviemode_mode_Onchange); +static CV_PossibleValue_t moviemode_cons_t[] = {{MM_GIF, "GIF"}, {MM_APNG, "aPNG"}, {MM_SCREENSHOT, "Screenshots"}, {MM_AVRECORDER, "WebM"}, {0, NULL}}; +consvar_t cv_moviemode = CVAR_INIT ("moviemode_mode", "WebM", CV_SAVE|CV_CALL, moviemode_cons_t, Moviemode_mode_Onchange); consvar_t cv_movie_option = CVAR_INIT ("movie_option", "Default", CV_SAVE|CV_CALL, screenshot_cons_t, Moviemode_option_Onchange); consvar_t cv_movie_folder = CVAR_INIT ("movie_folder", "", CV_SAVE, NULL, NULL); @@ -1295,6 +1296,25 @@ static inline moviemode_t M_StartMovieGIF(const char *pathname) } #endif +static inline moviemode_t M_StartMovieAVRecorder(const char *pathname) +{ + const char *ext = M_AVRecorder_GetFileExtension(); + const char *freename; + + if (!(freename = Newsnapshotfile(pathname, ext))) + { + CONS_Alert(CONS_ERROR, "Couldn't create %s file: no slots open in %s\n", ext, pathname); + return MM_OFF; + } + + if (!M_AVRecorder_Open(va(pandf,pathname,freename))) + { + return MM_OFF; + } + + return MM_AVRECORDER; +} + void M_StartMovie(void) { #if NUMSCREENS > 2 @@ -1332,6 +1352,9 @@ void M_StartMovie(void) case MM_SCREENSHOT: moviemode = MM_SCREENSHOT; break; + case MM_AVRECORDER: + moviemode = M_StartMovieAVRecorder(pathname); + break; default: //??? return; } @@ -1342,6 +1365,11 @@ void M_StartMovie(void) CONS_Printf(M_GetText("Movie mode enabled (%s).\n"), "GIF"); else if (moviemode == MM_SCREENSHOT) CONS_Printf(M_GetText("Movie mode enabled (%s).\n"), "screenshots"); + else if (moviemode == MM_AVRECORDER) + { + CONS_Printf(M_GetText("Movie mode enabled (%s).\n"), M_AVRecorder_GetCurrentFormat()); + M_AVRecorder_PrintCurrentConfiguration(); + } //singletics = (moviemode != MM_OFF); #endif @@ -1353,6 +1381,22 @@ void M_SaveFrame(void) // paranoia: should be unnecessary without singletics static tic_t oldtic = 0; + if (moviemode == MM_AVRECORDER) + { + // TODO: replace once hwr2 twodee is finished + if (rendermode == render_soft) + { + M_AVRecorder_CopySoftwareScreen(); + } + + if (M_AVRecorder_IsExpired()) + { + M_StopMovie(); + } + return; + } + + // skip interpolated frames for other modes if (oldtic == I_GetTime()) return; else @@ -1440,6 +1484,9 @@ void M_StopMovie(void) #endif case MM_SCREENSHOT: break; + case MM_AVRECORDER: + M_AVRecorder_Close(); + break; default: return; } diff --git a/src/m_misc.h b/src/m_misc.h index ec979dcb3..2df52fa96 100644 --- a/src/m_misc.h +++ b/src/m_misc.h @@ -29,7 +29,8 @@ typedef enum { MM_OFF = 0, MM_APNG, MM_GIF, - MM_SCREENSHOT + MM_SCREENSHOT, + MM_AVRECORDER, } moviemode_t; extern moviemode_t moviemode; diff --git a/src/media/CMakeLists.txt b/src/media/CMakeLists.txt new file mode 100644 index 000000000..014264d75 --- /dev/null +++ b/src/media/CMakeLists.txt @@ -0,0 +1,34 @@ +target_sources(SRB2SDL2 PRIVATE + audio_encoder.hpp + avrecorder.cpp + avrecorder.hpp + avrecorder_feedback.cpp + avrecorder_impl.hpp + avrecorder_indexed.cpp + avrecorder_queue.cpp + cfile.cpp + cfile.hpp + container.hpp + encoder.hpp + options.cpp + options.hpp + options_values.cpp + video_encoder.hpp + video_frame.hpp + vorbis.cpp + vorbis.hpp + vorbis_error.hpp + vp8.cpp + vp8.hpp + vpx_error.hpp + webm.hpp + webm_encoder.hpp + webm_container.cpp + webm_container.hpp + webm_vorbis.hpp + webm_vorbis_lace.cpp + webm_vp8.hpp + webm_writer.hpp + yuv420p.cpp + yuv420p.hpp +) diff --git a/src/media/audio_encoder.hpp b/src/media/audio_encoder.hpp new file mode 100644 index 000000000..3aa9f5cf5 --- /dev/null +++ b/src/media/audio_encoder.hpp @@ -0,0 +1,39 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_MEDIA_AUDIO_ENCODER_HPP__ +#define __SRB2_MEDIA_AUDIO_ENCODER_HPP__ + +#include + +#include "encoder.hpp" + +namespace srb2::media +{ + +class AudioEncoder : virtual public MediaEncoder +{ +public: + using sample_buffer_t = tcb::span; + + struct Config + { + int channels; + int sample_rate; + }; + + virtual void encode(sample_buffer_t samples) = 0; + + virtual int channels() const = 0; + virtual int sample_rate() const = 0; +}; + +}; // namespace srb2::media + +#endif // __SRB2_MEDIA_AUDIO_ENCODER_HPP__ diff --git a/src/media/avrecorder.cpp b/src/media/avrecorder.cpp new file mode 100644 index 000000000..fe5ada279 --- /dev/null +++ b/src/media/avrecorder.cpp @@ -0,0 +1,214 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../cxxutil.hpp" +#include "../i_time.h" +#include "../m_fixed.h" +#include "avrecorder_impl.hpp" +#include "webm_container.hpp" + +using namespace srb2::media; + +using Impl = AVRecorder::Impl; + +namespace +{ + +constexpr auto kBufferMethod = VideoFrame::BufferMethod::kEncoderAllocatedRGBA8888; + +}; // namespace + +Impl::Impl(Config cfg) : + max_size_(cfg.max_size), + max_duration_(cfg.max_duration), + + container_(std::make_unique(MediaContainer::Config { + cfg.file_name, + [this](const MediaContainer& container) { container_dtor_handler(container); }, + })), + + audio_encoder_(make_audio_encoder(cfg)), + video_encoder_(make_video_encoder(cfg)), + + epoch_(I_GetTime()), + + thread_([this] { worker(); }) +{ +} + +std::unique_ptr Impl::make_audio_encoder(const Config cfg) const +{ + if (!cfg.audio) + { + return nullptr; + } + + const Config::Audio& a = *cfg.audio; + + return container_->make_audio_encoder({2, a.sample_rate}); +} + +std::unique_ptr Impl::make_video_encoder(const Config cfg) const +{ + if (!cfg.video) + { + return nullptr; + } + + const Config::Video& v = *cfg.video; + + return container_->make_video_encoder({v.width, v.height, v.frame_rate, kBufferMethod}); +} + +Impl::~Impl() +{ + valid_ = false; + wake_up_worker(); + thread_.join(); + + try + { + // Finally flush encoders, unless queues were finished + // already due to time or size constraints. + + if (!audio_queue_.finished()) + { + audio_encoder_->flush(); + } + + if (!video_queue_.finished()) + { + video_encoder_->flush(); + } + } + catch (const std::exception& ex) + { + CONS_Alert(CONS_ERROR, "AVRecorder::Impl::~Impl: %s\n", ex.what()); + return; + } +} + +std::optional Impl::advance_video_pts() +{ + auto _ = queue_guard(); + + // Don't let this queue grow out of hand. It's normal + // for encoding time to vary by a small margin and + // spend longer than one frame rate on a single + // frame. It should normalize though. + + if (video_queue_.vec_.size() >= 3) + { + return {}; + } + + SRB2_ASSERT(video_encoder_ != nullptr); + + const float tic_pts = video_encoder_->frame_rate() / static_cast(TICRATE); + const int pts = ((I_GetTime() - epoch_) + FixedToFloat(g_time.timefrac)) * tic_pts; + + if (!video_queue_.advance(pts, 1)) + { + return {}; + } + + return pts; +} + +void Impl::worker() +{ + for (;;) + { + QueueState qs; + + try + { + while ((qs = encode_queues()) == QueueState::kFlushed) + ; + } + catch (const std::exception& ex) + { + CONS_Alert(CONS_ERROR, "AVRecorder::Impl::worker: %s\n", ex.what()); + break; + } + + if (qs != QueueState::kFinished && valid_) + { + std::unique_lock lock(queue_mutex_); + + queue_cond_.wait(lock); + } + else + { + break; + } + } + + // Breaking out of the loop ensures invalidation! + valid_ = false; +} + +const char* AVRecorder::file_extension() +{ + return "webm"; +} + +AVRecorder::AVRecorder(const Config config) : impl_(std::make_unique(config)) +{ +} + +AVRecorder::~AVRecorder() +{ + // impl_ is destroyed in a background thread so it doesn't + // block the thread AVRecorder was destroyed in. + // + // TODO: Save into a thread pool instead of detaching so + // the thread could be joined at program exit and + // not possibly terminate before fully destroyed? + + std::thread([_ = std::move(impl_)] {}).detach(); +} + +const char* AVRecorder::format_name() const +{ + return impl_->container_->name(); +} + +void AVRecorder::push_audio_samples(audio_buffer_t buffer) +{ + const auto _ = impl_->queue_guard(); + + auto& q = impl_->audio_queue_; + + if (!q.advance(q.pts(), buffer.size())) + { + return; + } + + using T = const float; + tcb::span p(reinterpret_cast(buffer.data()), buffer.size() * 2); // 2 channels + + std::copy(p.begin(), p.end(), std::back_inserter(q.vec_)); + + impl_->wake_up_worker(); +} + +bool AVRecorder::invalid() const +{ + return !impl_->valid_; +} diff --git a/src/media/avrecorder.hpp b/src/media/avrecorder.hpp new file mode 100644 index 000000000..fdee91b18 --- /dev/null +++ b/src/media/avrecorder.hpp @@ -0,0 +1,108 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_MEDIA_AVRECORDER_HPP__ +#define __SRB2_MEDIA_AVRECORDER_HPP__ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "../audio/sample.hpp" + +namespace srb2::media +{ + +class AVRecorder +{ +public: + using audio_sample_t = srb2::audio::Sample<2>; + using audio_buffer_t = tcb::span; + + class Impl; + + struct Config + { + struct Audio + { + int sample_rate; + }; + + struct Video + { + int width; + int height; + int frame_rate; + }; + + std::string file_name; + + std::optional max_size; // file size limit + std::optional> max_duration; + + std::optional