// DR. ROBOTNIK'S RING RACERS //----------------------------------------------------------------------------- // Copyright (C) 2025 by Kart Krew. // Copyright (C) 2020 by Sonic Team Junior. // Copyright (C) 2000 by DooM Legacy Team. // Copyright (C) 1996 by id Software, Inc. // // 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 m_misc.cpp /// \brief Commonly used routines /// Default config file, PCX screenshots, file i/o #ifdef __GNUC__ #if (__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 3) // Ignore "argument might be clobbered by longjmp" warning in GCC // (if libpng is compiled with setjmp error handling) #pragma GCC diagnostic ignored "-Wclobbered" #endif #include #include #endif #include #include // Extended map support. #include #include "doomdef.h" #include "g_game.h" #include "m_misc.h" #include "hu_stuff.h" #include "st_stuff.h" #include "v_video.h" #include "z_zone.h" #include "g_input.h" #include "i_time.h" #include "i_video.h" #include "d_main.h" #include "m_argv.h" #include "i_system.h" #include "command.h" // cv_execversion #include "m_anigif.h" #ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES #include "m_avrecorder.h" #include "m_avrecorder.hpp" #endif // So that the screenshot menu auto-updates... #include "k_menu.h" #ifdef HWRENDER #include "hardware/hw_main.h" #endif #ifdef HAVE_SDL #include "sdl/hwsym_sdl.h" #ifdef __linux__ #ifndef _LARGEFILE64_SOURCE typedef off_t off64_t; #endif #endif #endif #if defined(__MINGW32__) && ((__GNUC__ > 7) || (__GNUC__ == 6 && __GNUC_MINOR__ >= 3)) && (__GNUC__ < 8) #define PRIdS "u" #elif defined (_WIN32) // pedantic: %I is nonstandard, is it ok to assume // unsigned int? //#define PRIdS "Iu" #ifdef _WIN64 #define PRIdS "lu" #else #define PRIdS "u" #endif #else #define PRIdS "zu" #endif #ifdef HAVE_PNG #ifndef _MSC_VER #ifndef _LARGEFILE64_SOURCE #define _LARGEFILE64_SOURCE #endif #endif #ifndef _LFS64_LARGEFILE #define _LFS64_LARGEFILE #endif #ifndef _FILE_OFFSET_BITS #define _FILE_OFFSET_BITS 0 #endif #include "zlib.h" #include "png.h" #if (PNG_LIBPNG_VER_MAJOR > 1) || (PNG_LIBPNG_VER_MAJOR == 1 && PNG_LIBPNG_VER_MINOR >= 4) #define NO_PNG_DEBUG // 1.4.0 move png_debug to pngpriv.h #endif #ifdef PNG_WRITE_SUPPORTED #define USE_PNG // Only actually use PNG if write is supported. #if defined (PNG_WRITE_APNG_SUPPORTED) //|| !defined(PNG_STATIC) #include "apng.h" #define USE_APNG #endif // See hardware/hw_draw.c for a similar check to this one. #endif #endif CV_PossibleValue_t lossless_recorder_cons_t[] = {{MM_GIF, "GIF"}, {MM_APNG, "aPNG"}, {MM_SCREENSHOT, "Screenshots"}, {0, NULL}}; CV_PossibleValue_t zlib_mem_level_t[] = { {1, "(Min Memory) 1"}, {2, "2"}, {3, "3"}, {4, "4"}, {5, "5"}, {6, "6"}, {7, "7"}, {8, "(Optimal) 8"}, //libpng Default {9, "(Max Memory) 9"}, {0, NULL}}; CV_PossibleValue_t zlib_level_t[] = { {0, "No Compression"}, //Z_NO_COMPRESSION {1, "(Fastest) 1"}, //Z_BEST_SPEED {2, "2"}, {3, "3"}, {4, "4"}, {5, "5"}, {6, "(Optimal) 6"}, //Zlib Default {7, "7"}, {8, "8"}, {9, "(Maximum) 9"}, //Z_BEST_COMPRESSION {0, NULL}}; CV_PossibleValue_t zlib_strategy_t[] = { {0, "Normal"}, //Z_DEFAULT_STRATEGY {1, "Filtered"}, //Z_FILTERED {2, "Huffman Only"}, //Z_HUFFMAN_ONLY {3, "RLE"}, //Z_RLE {4, "Fixed"}, //Z_FIXED {0, NULL}}; CV_PossibleValue_t zlib_window_bits_t[] = { #ifdef WBITS_8_OK {8, "256"}, #endif {9, "512"}, {10, "1k"}, {11, "2k"}, {12, "4k"}, {13, "8k"}, {14, "16k"}, {15, "32k"}, {0, NULL}}; #ifdef USE_APNG static boolean apng_downscale = false; // So nobody can do something dumb like changing cvars mid output #endif boolean takescreenshot = false; // Take a screenshot this tic moviemode_t moviemode = MM_OFF; g_takemapthumbnail_t g_takemapthumbnail = TMT_NO; char joinedIPlist[NUMLOGIP][2][MAX_LOGIP]; char joinedIP[MAX_LOGIP]; // This initializes the above array to have NULL evrywhere it should. void M_InitJoinedIPArray(void) { UINT8 i; for (i=0; i < NUMLOGIP; i++) { joinedIPlist[i][0][0] = joinedIPlist[i][1][0] = '\0'; } } // This adds an entry to the above array void M_AddToJoinedIPs(char *address, char *servname) { UINT8 i = 0; // Check for dupes... for (i = 0; i < NUMLOGIP-1; i++) // intentionally not < NUMLOGIP { // Check the addresses if (strcmp(joinedIPlist[i][0], address) == 0) { break; } } CONS_Printf("Adding %s (%s) to list of manually joined IPs\n", servname, address); // Start by moving every IP up 1 slot (dropping the last IP in the table) for (; i; i--) { strlcpy(joinedIPlist[i][0], joinedIPlist[i-1][0], MAX_LOGIP); strlcpy(joinedIPlist[i][1], joinedIPlist[i-1][1], MAX_LOGIP); } // and add the new IP at the start of the table! strlcpy(joinedIPlist[0][0], address, MAX_LOGIP); strlcpy(joinedIPlist[0][1], servname, MAX_LOGIP); } // ========================================================================== // FILE INPUT / OUTPUT // ========================================================================== // some libcs has no access function, make our own #if 0 int access(const char *path, int amode) { int accesshandle = -1; FILE *handle = NULL; if (amode == 6) // W_OK|R_OK handle = fopen(path, "r+"); else if (amode == 4) // R_OK handle = fopen(path, "r"); else if (amode == 2) // W_OK handle = fopen(path, "a+"); else if (amode == 0) //F_OK handle = fopen(path, "rb"); if (handle) { accesshandle = 0; fclose(handle); } return accesshandle; } #endif // // FIL_WriteFile // #ifndef O_BINARY #define O_BINARY 0 #endif /** Writes out a file. * * \param name Name of the file to write. * \param source Memory location to write from. * \param length How many bytes to write. * \return True on success, false on failure. */ boolean FIL_WriteFile(char const *name, const void *source, size_t length) { FILE *handle = NULL; size_t count; //if (FIL_WriteFileOK(name)) handle = fopen(name, "w+b"); if (!handle) return false; count = fwrite(source, 1, length, handle); fclose(handle); if (count < length) return false; return true; } /** Reads in a file, appending a zero byte at the end. * * \param name Filename to read. * \param buffer Pointer to a pointer, which will be set to the location of a * newly allocated buffer holding the file's contents. * \return Number of bytes read, not counting the zero byte added to the end, * or 0 on error. */ size_t FIL_ReadFileTag(char const *name, UINT8 **buffer, INT32 tag) { FILE *handle = NULL; size_t count, length; UINT8 *buf; if (FIL_ReadFileOK(name)) handle = fopen(name, "rb"); if (!handle) return 0; fseek(handle,0,SEEK_END); length = ftell(handle); fseek(handle,0,SEEK_SET); buf = static_cast(Z_Malloc(length + 1, tag, NULL)); count = fread(buf, 1, length, handle); fclose(handle); if (count < length) { Z_Free(buf); return 0; } // append 0 byte for script text files buf[length] = 0; *buffer = buf; return length; } /** Makes a copy of a text file with all newlines converted into LF newlines. * * \param textfilename The name of the source file * \param binfilename The name of the destination file */ boolean FIL_ConvertTextFileToBinary(const char *textfilename, const char *binfilename) { FILE *textfile; FILE *binfile; UINT8 buffer[1024]; size_t count; boolean success; textfile = fopen(textfilename, "r"); if (!textfile) return false; binfile = fopen(binfilename, "wb"); if (!binfile) { fclose(textfile); return false; } do { count = fread(buffer, 1, sizeof(buffer), textfile); fwrite(buffer, 1, count, binfile); } while (count); success = !(ferror(textfile) || ferror(binfile)); fclose(textfile); fclose(binfile); return success; } boolean FIL_RenameFile(char const *old_name, char const *new_name) { int result = rename(old_name, new_name); return (result == 0); } /** Check if the filename exists * * \param name Filename to check. * \return true if file exists, false if it doesn't. */ boolean FIL_FileExists(char const *name) { return access(name,0)+1; //F_OK } /** Check if the filename OK to write * * \param name Filename to check. * \return true if file write-able, false if it doesn't. */ boolean FIL_WriteFileOK(char const *name) { return access(name,2)+1; //W_OK } /** Check if the filename OK to read * * \param name Filename to check. * \return true if file read-able, false if it doesn't. */ boolean FIL_ReadFileOK(char const *name) { return access(name,4)+1; //R_OK } /** Check if the filename OK to read/write * * \param name Filename to check. * \return true if file (read/write)-able, false if it doesn't. */ boolean FIL_FileOK(char const *name) { return access(name,6)+1; //R_OK|W_OK } /** Checks if a pathname has a file extension and adds the extension provided * if not. * * \param path Pathname to check. * \param extension Extension to add if no extension is there. */ void FIL_DefaultExtension(char *path, const char *extension) { char *src; // search for '.' from end to begin, add .EXT only when not found src = path + strlen(path) - 1; while (*src != '/' && src != path) { if (*src == '.') return; // it has an extension src--; } strcat(path, extension); } void FIL_ForceExtension(char *path, const char *extension) { char *src; // search for '.' from end to begin, add .EXT only when not found src = path + strlen(path) - 1; while (*src != '/' && src != path) { if (*src == '.') { *src = '\0'; break; // it has an extension } src--; } strcat(path, extension); } /** Checks if a filename extension is found. * Lump names do not contain dots. * * \param in String to check. * \return True if an extension is found, otherwise false. */ boolean FIL_CheckExtension(const char *in) { while (*in++) if (*in == '.') return true; return false; } // LAST IPs JOINED LOG FILE! // ...It won't be as overly engineered as the config file because let's be real there's 0 need to... // Save the file: void M_SaveJoinedIPs(void) { FILE *f = NULL; UINT8 i; const char *filepath = va("%s" PATHSEP "%s", srb2home, IPLOGFILE); if (!*joinedIPlist[0][0]) return; // Don't bother, there's nothing to save. f = fopen(filepath, "w"); if (!f) { CONS_Alert(CONS_WARNING, "Could not save recent IP list into %s\n", IPLOGFILE); return; } for (i = 0; i < NUMLOGIP; i++) { if (*joinedIPlist[i][0]) { fprintf(f, "%s%s%s\n", joinedIPlist[i][0], IPLOGFILESEP, joinedIPlist[i][1]); } } fclose(f); } // Load the file: void M_LoadJoinedIPs(void) { FILE *f = NULL; UINT8 i = 0; char *filepath; char *s; char buffer[2*(MAX_LOGIP+1)]; filepath = va("%s" PATHSEP "%s", srb2home, IPLOGFILE); f = fopen(filepath, "r"); if (f == NULL) return; // File doesn't exist? sure, just do nothing then. for (i = 0; fgets(buffer, (int)sizeof(buffer), f); i++) // Don't let us write more than we can chew! { if (i >= NUMLOGIP) break; if (!*buffer || *buffer == '\n') break; s = strtok(buffer, IPLOGFILESEP); // We got the address strlcpy(joinedIPlist[i][0], s, MAX_LOGIP); s = strtok(NULL, IPLOGFILESEP); // Let's get rid of this awful \n while we're here! if (s) { UINT16 j = 1; //strcpy(joinedIPlist[i][1], s); -- get rid of \n too... char *c = joinedIPlist[i][1]; while (*s && *s != '\n' && j < MAX_LOGIP) { *c = *s; s++; c++; j++; } *c = '\0'; } } fclose(f); // We're done here } // ========================================================================== // CONFIGURATION FILE // ========================================================================== // // DEFAULTS // char configfile[MAX_WADPATH]; // ========================================================================== // CONFIGURATION // ========================================================================== static boolean gameconfig_loaded = false; // true once config.cfg loaded AND executed /** Saves a player's config, possibly to a particular file. * * \sa Command_LoadConfig_f */ void Command_SaveConfig_f(void) { char tmpstr[MAX_WADPATH]; if (COM_Argc() < 2) { CONS_Printf(M_GetText("saveconfig [-silent] : save config to a file\n")); return; } strcpy(tmpstr, COM_Argv(1)); FIL_ForceExtension(tmpstr, ".cfg"); M_SaveConfig(tmpstr); if (stricmp(COM_Argv(2), "-silent")) CONS_Printf(M_GetText("config saved as %s\n"), configfile); } /** Loads a game config, possibly from a particular file. * * \sa Command_SaveConfig_f, Command_ChangeConfig_f */ void Command_LoadConfig_f(void) { UINT8 i; if (COM_Argc() != 2) { CONS_Printf(M_GetText("loadconfig : load config from a file\n")); return; } strcpy(configfile, COM_Argv(1)); FIL_ForceExtension(configfile, ".cfg"); // load default control G_ClearAllControlKeys(); for (i = 0; i < MAXSPLITSCREENPLAYERS; i++) { G_ApplyControlScheme(i, gamecontroldefault); } // temporarily reset execversion to default CV_ToggleExecVersion(true); COM_BufInsertText(va("%s \"%s\"\n", cv_execversion.name, cv_execversion.defaultvalue)); CV_InitFilterVar(); // exec the config COM_BufInsertText(va("exec \"%s\" -immediate\n", configfile)); // don't filter anymore vars and don't let this convsvar be changed COM_BufInsertText(va("%s \"%d\"\n", cv_execversion.name, EXECVERSION)); CV_ToggleExecVersion(false); } /** Saves the current configuration and loads another. * * \sa Command_LoadConfig_f, Command_SaveConfig_f */ void Command_ChangeConfig_f(void) { if (COM_Argc() != 2) { CONS_Printf(M_GetText("changeconfig : save current config and load another\n")); return; } COM_BufAddText(va("saveconfig \"%s\"\n", configfile)); COM_BufAddText(va("loadconfig \"%s\"\n", COM_Argv(1))); } /** Loads the default config file. * * \sa Command_LoadConfig_f */ void M_FirstLoadConfig(void) { UINT8 i; // configfile is initialised by d_main when searching for the wad? // check for a custom config file if (M_CheckParm("-config") && M_IsNextParm()) { strcpy(configfile, M_GetNextParm()); CONS_Printf(M_GetText("config file: %s\n"), configfile); } // load default control G_DefineDefaultControls(); for (i = 0; i < MAXSPLITSCREENPLAYERS; i++) { G_ApplyControlScheme(i, gamecontroldefault); } // register execversion here before we load any configs { extern struct CVarList *cvlist_execversion; CV_RegisterList(cvlist_execversion); } // temporarily reset execversion to default // we shouldn't need to do this, but JUST in case... CV_ToggleExecVersion(true); COM_BufInsertText(va("%s \"%s\"\n", cv_execversion.name, cv_execversion.defaultvalue)); CV_InitFilterVar(); // load config, make sure those commands doesnt require the screen... COM_BufInsertText(va("exec \"%s\" -immediate\n", configfile)); // no COM_BufExecute() needed; that does it right away // don't filter anymore vars and don't let this convsvar be changed COM_BufInsertText(va("%s \"%d\"\n", cv_execversion.name, EXECVERSION)); CV_ToggleExecVersion(false); // make sure I_Quit() will write back the correct config // (do not write back the config if it crash before) gameconfig_loaded = true; // reset to default player stuff #if 0 for (i = 0; i < MAXSPLITSCREENPLAYERS; i++) { COM_BufAddText (va("%s \"%s\"\n",cv_skin[i].name,cv_skin[i].defaultvalue)); COM_BufAddText (va("%s \"%s\"\n",cv_playercolor[i].name,cv_playercolor[i].defaultvalue)); } #endif } /** Saves the game configuration. * * \sa Command_SaveConfig_f */ void M_SaveConfig(const char *filename) { FILE *f; char tmppath[2048]; // make sure not to write back the config until it's been correctly loaded if (!gameconfig_loaded) return; // can change the file name if (filename) { if (!strstr(filename, ".cfg")) { CONS_Alert(CONS_NOTICE, M_GetText("Config filename must be .cfg\n")); return; } // append srb2home to beginning of filename // but check if srb2home isn't already there, first if (!strstr(filename, srb2home)) { sprintf(tmppath, "%s" PATHSEP "%s.tmp", srb2home, filename); } else { sprintf(tmppath, "%s", filename); } f = fopen(tmppath, "w"); // change it only if valid if (f) { strcpy(configfile, tmppath); } else { CONS_Alert(CONS_ERROR, M_GetText("Couldn't save game config file %s\n"), tmppath); return; } } else { if (!strstr(configfile, ".cfg")) { CONS_Alert(CONS_NOTICE, M_GetText("Config filename must be .cfg\n")); return; } sprintf(tmppath, "%s.tmp", configfile); f = fopen(tmppath, "w"); if (!f) { CONS_Alert(CONS_ERROR, M_GetText("Couldn't save game config file %s\n"), configfile); return; } } // header message fprintf(f, "// Dr. Robotnik's Ring Racers configuration file.\n"); // print execversion FIRST, because subsequent consvars need to be filtered // always print current EXECVERSION fprintf(f, "%s \"%d\"\n", cv_execversion.name, EXECVERSION); // FIXME: save key aliases if ever implemented.. CV_SaveVariables(f); if (!dedicated) { G_SaveKeySetting(f, gamecontrol[0], gamecontrol[1], gamecontrol[2], gamecontrol[3]); } fclose(f); { // Atomically replace the old config once the new one has been written. namespace fs = std::filesystem; fs::path tmp{tmppath}; fs::path real{configfile}; try { fs::rename(tmp, real); } catch (const fs::filesystem_error& ex) { CONS_Alert(CONS_ERROR, M_GetText("Failed to move temp config file to real destination\n")); } } } // ========================================================================== // SCREENSHOTS // ========================================================================== static UINT8 screenshot_palette[768]; static void M_CreateScreenShotPalette(void) { size_t i, j; for (i = 0, j = 0; i < 768; i += 3, j++) { RGBA_t locpal = ((cv_screenshot_colorprofile.value) ? pLocalPalette[(std::max(st_palette,0)*256)+j] : pMasterPalette[(std::max(st_palette,0)*256)+j]); screenshot_palette[i] = locpal.s.red; screenshot_palette[i+1] = locpal.s.green; screenshot_palette[i+2] = locpal.s.blue; } } #if NUMSCREENS > 2 static const char *Newsnapshotfile(const char *pathname, const char *ext) { static char freename[20] = "ringracersXXXX.exte"; int i = 5000; // start in the middle: num screenshots divided by 2 int add = i; // how much to add or subtract if wrong; gets divided by 2 each time int result; // -1 = guess too high, 0 = correct, 1 = guess too low // find a file name to save it to I_Assert(strlen(ext) < (sizeof freename) - 15); strcpy(freename+15,ext); for (;;) { freename[10] = (char)('0' + (char)(i/1000)); freename[11] = (char)('0' + (char)((i/100)%10)); freename[12] = (char)('0' + (char)((i/10)%10)); freename[13] = (char)('0' + (char)(i%10)); if (FIL_WriteFileOK(va(pandf,pathname,freename))) // access succeeds result = 1; // too low else // access fails: equal or too high { if (!i) break; // not too high, so it must be equal! YAY! freename[10] = (char)('0' + (char)((i-1)/1000)); freename[11] = (char)('0' + (char)(((i-1)/100)%10)); freename[12] = (char)('0' + (char)(((i-1)/10)%10)); freename[13] = (char)('0' + (char)((i-1)%10)); if (!FIL_WriteFileOK(va(pandf,pathname,freename))) // access fails result = -1; // too high else break; // not too high, so equal, YAY! } add /= 2; if (!add) // don't get stuck at 5 due to truncation! add = 1; i += add * result; if (i < 0 || i > 9999) return NULL; } freename[10] = (char)('0' + (char)(i/1000)); freename[11] = (char)('0' + (char)((i/100)%10)); freename[12] = (char)('0' + (char)((i/10)%10)); freename[13] = (char)('0' + (char)(i%10)); return freename; } #endif #ifdef HAVE_PNG FUNCNORETURN static void PNG_error(png_structp PNG, png_const_charp pngtext) { //CONS_Debug(DBG_RENDER, "libpng error at %p: %s", PNG, pngtext); I_Error("libpng error at %p: %s", (void*)PNG, pngtext); } static void PNG_warn(png_structp PNG, png_const_charp pngtext) { CONS_Debug(DBG_RENDER, "libpng warning at %p: %s", (void*)PNG, pngtext); } static void M_PNGhdr(png_structp png_ptr, png_infop png_info_ptr, PNG_CONST png_uint_32 width, PNG_CONST png_uint_32 height, PNG_CONST png_byte *palette) { const png_byte png_interlace = PNG_INTERLACE_NONE; //PNG_INTERLACE_ADAM7 if (palette) { png_colorp png_PLTE = static_cast(png_malloc(png_ptr, sizeof(png_color)*256)); //palette png_uint_16 i; const png_byte *pal = palette; for (i = 0; i < 256; i++) { png_PLTE[i].red = *pal; pal++; png_PLTE[i].green = *pal; pal++; png_PLTE[i].blue = *pal; pal++; } png_set_IHDR(png_ptr, png_info_ptr, width, height, 8, PNG_COLOR_TYPE_PALETTE, png_interlace, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); png_write_info_before_PLTE(png_ptr, png_info_ptr); png_set_PLTE(png_ptr, png_info_ptr, png_PLTE, 256); png_free(png_ptr, (png_voidp)png_PLTE); // safe in libpng-1.2.1+ png_set_filter(png_ptr, PNG_FILTER_TYPE_BASE, PNG_FILTER_NONE); png_set_compression_strategy(png_ptr, Z_DEFAULT_STRATEGY); } else { png_set_IHDR(png_ptr, png_info_ptr, width, height, 8, PNG_COLOR_TYPE_RGB, png_interlace, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); png_write_info_before_PLTE(png_ptr, png_info_ptr); png_set_compression_strategy(png_ptr, Z_FILTERED); } } static void M_PNGText(png_structp png_ptr, png_infop png_info_ptr, PNG_CONST png_byte movie) { #ifdef PNG_TEXT_SUPPORTED #define SRB2PNGTXT 11 //PNG_KEYWORD_MAX_LENGTH(79) is the max png_text png_infotext[SRB2PNGTXT]; char keytxt[SRB2PNGTXT][12] = { "Title", "Description", "Playername", "Mapnum", "Mapname", "Location", "Interface", "Render Mode", "Revision", "Build Date", "Build Time"}; char titletxt[] = "Dr. Robotnik's Ring Racers " VERSIONSTRING; png_charp playertxt = cv_playername[0].zstring; char desctxt[] = "Ring Racers Screenshot"; char Movietxt[] = "Ring Racers Movie"; size_t i; char interfacetxt[] = #ifdef HAVE_SDL "SDL"; #else "Unknown"; #endif char rendermodetxt[9]; char maptext[MAXMAPLUMPNAME]; char lvlttltext[48]; char locationtxt[40]; char ctrevision[40]; char ctdate[40]; char cttime[40]; switch (rendermode) { case render_soft: strcpy(rendermodetxt, "Software"); break; case render_opengl: strcpy(rendermodetxt, "OpenGL"); break; default: // Just in case strcpy(rendermodetxt, "None"); break; } if (gamestate == GS_LEVEL) { const char* mapname = G_BuildMapName(gamemap); if (mapname) snprintf(maptext, sizeof(maptext), "%s", mapname); else snprintf(maptext, sizeof(maptext), "Unknown"); } else snprintf(maptext, sizeof(maptext), "Unknown"); if (gamestate == GS_LEVEL && mapheaderinfo[gamemap-1]->lvlttl[0] != '\0') snprintf(lvlttltext, 48, "%s%s%s", mapheaderinfo[gamemap-1]->lvlttl, (mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE) ? "" : (mapheaderinfo[gamemap-1]->zonttl[0] != '\0') ? va(" %s",mapheaderinfo[gamemap-1]->zonttl) : " Zone", (mapheaderinfo[gamemap-1]->actnum > 0) ? va(" %d",mapheaderinfo[gamemap-1]->actnum) : ""); else snprintf(lvlttltext, 48, "Unknown"); if (gamestate == GS_LEVEL && players[g_localplayers[0]].mo) snprintf(locationtxt, 40, "X:%d Y:%d Z:%d A:%d", players[g_localplayers[0]].mo->x>>FRACBITS, players[g_localplayers[0]].mo->y>>FRACBITS, players[g_localplayers[0]].mo->z>>FRACBITS, FixedInt(AngleFixed(players[g_localplayers[0]].mo->angle))); else snprintf(locationtxt, 40, "Unknown"); memset(png_infotext,0x00,sizeof (png_infotext)); for (i = 0; i < SRB2PNGTXT; i++) png_infotext[i].key = keytxt[i]; png_infotext[0].text = titletxt; if (movie) png_infotext[1].text = Movietxt; else png_infotext[1].text = desctxt; png_infotext[2].text = playertxt; png_infotext[3].text = maptext; png_infotext[4].text = lvlttltext; png_infotext[5].text = locationtxt; png_infotext[6].text = interfacetxt; png_infotext[7].text = rendermodetxt; png_infotext[8].text = strncpy(ctrevision, comprevision, sizeof(ctrevision)-1); png_infotext[9].text = strncpy(ctdate, compdate, sizeof(ctdate)-1); png_infotext[10].text = strncpy(cttime, comptime, sizeof(cttime)-1); png_set_text(png_ptr, png_info_ptr, png_infotext, SRB2PNGTXT); #undef SRB2PNGTXT #endif } static inline void M_PNGImage(png_structp png_ptr, png_infop png_info_ptr, PNG_CONST png_uint_32 height, png_bytep png_buf) { png_uint_32 pitch = png_get_rowbytes(png_ptr, static_cast(png_info_ptr)); png_bytepp row_pointers = static_cast(png_malloc(png_ptr, height* sizeof (png_bytep))); png_uint_32 y; for (y = 0; y < height; y++) { row_pointers[y] = png_buf; png_buf += pitch; } png_write_image(png_ptr, row_pointers); png_free(png_ptr, (png_voidp)row_pointers); } #ifdef USE_APNG static png_structp apng_ptr = NULL; static png_infop apng_info_ptr = NULL; static apng_infop apng_ainfo_ptr = NULL; static png_FILE_p apng_FILE = NULL; static png_uint_32 apng_frames = 0; #ifdef PNG_STATIC // Win32 build have static libpng #define aPNG_set_acTL png_set_acTL #define aPNG_write_frame_head png_write_frame_head #define aPNG_write_frame_tail png_write_frame_tail #else // outside libpng may not have apng support #ifndef PNG_WRITE_APNG_SUPPORTED // libpng header may not have apng patch #ifndef PNG_INFO_acTL #define PNG_INFO_acTL 0x10000L #endif #ifndef PNG_INFO_fcTL #define PNG_INFO_fcTL 0x20000L #endif #ifndef PNG_FIRST_FRAME_HIDDEN #define PNG_FIRST_FRAME_HIDDEN 0x0001 #endif #ifndef PNG_DISPOSE_OP_NONE #define PNG_DISPOSE_OP_NONE 0x00 #endif #ifndef PNG_DISPOSE_OP_BACKGROUND #define PNG_DISPOSE_OP_BACKGROUND 0x01 #endif #ifndef PNG_DISPOSE_OP_PREVIOUS #define PNG_DISPOSE_OP_PREVIOUS 0x02 #endif #ifndef PNG_BLEND_OP_SOURCE #define PNG_BLEND_OP_SOURCE 0x00 #endif #ifndef PNG_BLEND_OP_OVER #define PNG_BLEND_OP_OVER 0x01 #endif #ifndef PNG_HAVE_acTL #define PNG_HAVE_acTL 0x4000 #endif #ifndef PNG_HAVE_fcTL #define PNG_HAVE_fcTL 0x8000L #endif #endif typedef png_uint_32 (*P_png_set_acTL) (png_structp png_ptr, png_infop info_ptr, png_uint_32 num_frames, png_uint_32 num_plays); typedef void (*P_png_write_frame_head) (png_structp png_ptr, png_infop info_ptr, png_bytepp row_pointers, png_uint_32 width, png_uint_32 height, png_uint_32 x_offset, png_uint_32 y_offset, png_uint_16 delay_num, png_uint_16 delay_den, png_byte dispose_op, png_byte blend_op); typedef void (*P_png_write_frame_tail) (png_structp png_ptr, png_infop info_ptr); static P_png_set_acTL aPNG_set_acTL = NULL; static P_png_write_frame_head aPNG_write_frame_head = NULL; static P_png_write_frame_tail aPNG_write_frame_tail = NULL; #endif static inline boolean M_PNGLib(void) { #ifdef PNG_STATIC // Win32 build have static libpng return true; #else static void *pnglib = NULL; if (aPNG_set_acTL && aPNG_write_frame_head && aPNG_write_frame_tail) return true; if (pnglib) return false; #ifdef _WIN32 pnglib = GetModuleHandleA("libpng.dll"); if (!pnglib) pnglib = GetModuleHandleA("libpng12.dll"); if (!pnglib) pnglib = GetModuleHandleA("libpng13.dll"); #elif defined (HAVE_SDL) #ifdef __APPLE__ pnglib = hwOpen("libpng.dylib"); #else pnglib = hwOpen("libpng.so"); #endif #endif if (!pnglib) return false; #ifdef HAVE_SDL aPNG_set_acTL = (P_png_set_acTL) hwSym("png_set_acTL", pnglib); aPNG_write_frame_head = (P_png_write_frame_head) hwSym("png_write_frame_head", pnglib); aPNG_write_frame_tail = (P_png_write_frame_tail) hwSym("png_write_frame_tail", pnglib); #endif #ifdef _WIN32 aPNG_set_acTL = (P_png_set_acTL) GetProcAddress("png_set_acTL", pnglib); aPNG_write_frame_head = (P_png_write_frame_head) GetProcAddress("png_write_frame_head", pnglib); aPNG_write_frame_tail = (P_png_write_frame_tail) GetProcAddress("png_write_frame_tail", pnglib); #endif return (aPNG_set_acTL && aPNG_write_frame_head && aPNG_write_frame_tail); #endif } static void M_PNGFrame(png_structp png_ptr, png_infop png_info_ptr, png_bytep png_buf) { png_uint_16 downscale = apng_downscale ? vid.dupx : 1; png_uint_32 pitch = png_get_rowbytes(png_ptr, png_info_ptr); PNG_CONST png_uint_32 width = vid.width / downscale; PNG_CONST png_uint_32 height = vid.height / downscale; png_bytepp row_pointers = (png_bytepp) png_malloc(png_ptr, height * sizeof (png_bytep)); png_uint_32 x, y; png_uint_16 framedelay = (png_uint_16)cv_apng_delay.value; apng_frames++; for (y = 0; y < height; y++) { row_pointers[y] = (png_bytep) malloc(pitch * sizeof(png_byte)); for (x = 0; x < width; x++) row_pointers[y][x] = png_buf[x * downscale]; png_buf += pitch * (downscale * downscale); } //for (x = 0; x < width; x++) //{ // printf("%d", x); // row_pointers[y][x] = 0; //} /* row_pointers[y] = calloc(1, sizeof(png_bytep)); png_buf += pitch * 2; }*/ #ifndef PNG_STATIC if (aPNG_write_frame_head) #endif aPNG_write_frame_head(apng_ptr, apng_info_ptr, row_pointers, width, /* width */ height, /* height */ 0, /* x offset */ 0, /* y offset */ framedelay, TICRATE,/* delay numerator and denominator */ PNG_DISPOSE_OP_BACKGROUND, /* dispose */ PNG_BLEND_OP_SOURCE /* blend */ ); png_write_image(png_ptr, row_pointers); #ifndef PNG_STATIC if (aPNG_write_frame_tail) #endif aPNG_write_frame_tail(apng_ptr, apng_info_ptr); png_free(png_ptr, (png_voidp)row_pointers); } static void M_PNGfix_acTL(png_structp png_ptr, png_infop png_info_ptr, apng_infop png_ainfo_ptr) { apng_set_acTL(png_ptr, png_info_ptr, png_ainfo_ptr, apng_frames, 0); #ifndef NO_PNG_DEBUG png_debug(1, "in png_write_acTL\n"); #endif } static boolean M_SetupaPNG(png_const_charp filename, png_bytep pal) { png_uint_16 downscale; apng_downscale = (!!cv_apng_downscale.value); downscale = apng_downscale ? vid.dupx : 1; apng_FILE = fopen(filename,"wb+"); // + mode for reading if (!apng_FILE) { CONS_Debug(DBG_RENDER, "M_StartMovie: Error on opening %s for write\n", filename); return false; } apng_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, PNG_error, PNG_warn); if (!apng_ptr) { CONS_Debug(DBG_RENDER, "M_StartMovie: Error on initialize libpng\n"); fclose(apng_FILE); remove(filename); return false; } apng_info_ptr = png_create_info_struct(apng_ptr); if (!apng_info_ptr) { CONS_Debug(DBG_RENDER, "M_StartMovie: Error on allocate for libpng\n"); png_destroy_write_struct(&apng_ptr, NULL); fclose(apng_FILE); remove(filename); return false; } apng_ainfo_ptr = apng_create_info_struct(apng_ptr); if (!apng_ainfo_ptr) { CONS_Debug(DBG_RENDER, "M_StartMovie: Error on allocate for apng\n"); png_destroy_write_struct(&apng_ptr, &apng_info_ptr); fclose(apng_FILE); remove(filename); return false; } png_init_io(apng_ptr, apng_FILE); #ifdef PNG_SET_USER_LIMITS_SUPPORTED png_set_user_limits(apng_ptr, MAXVIDWIDTH, MAXVIDHEIGHT); #endif //png_set_filter(apng_ptr, 0, PNG_ALL_FILTERS); png_set_compression_level(apng_ptr, cv_zlib_levela.value); png_set_compression_mem_level(apng_ptr, cv_zlib_memorya.value); png_set_compression_strategy(apng_ptr, cv_zlib_strategya.value); png_set_compression_window_bits(apng_ptr, cv_zlib_window_bitsa.value); M_PNGhdr(apng_ptr, apng_info_ptr, vid.width / downscale, vid.height / downscale, pal); M_PNGText(apng_ptr, apng_info_ptr, true); apng_set_set_acTL_fn(apng_ptr, apng_ainfo_ptr, aPNG_set_acTL); apng_set_acTL(apng_ptr, apng_info_ptr, apng_ainfo_ptr, PNG_UINT_31_MAX, 0); apng_write_info(apng_ptr, apng_info_ptr, apng_ainfo_ptr); apng_frames = 0; return true; } #endif #endif // ========================================================================== // MOVIE MODE // ========================================================================== #if NUMSCREENS > 2 static inline moviemode_t M_StartMovieAPNG(const char *pathname) { #ifdef USE_APNG UINT8 *palette = NULL; const char *freename = NULL; boolean ret = false; if (!M_PNGLib()) { CONS_Alert(CONS_ERROR, "Couldn't create aPNG: libpng not found\n"); return MM_OFF; } if (!(freename = Newsnapshotfile(pathname,"png"))) { CONS_Alert(CONS_ERROR, "Couldn't create aPNG: no slots open in %s\n", pathname); return MM_OFF; } if (rendermode == render_soft) { M_CreateScreenShotPalette(); palette = screenshot_palette; } ret = M_SetupaPNG(va(pandf,pathname,freename), palette); if (!ret) { CONS_Alert(CONS_ERROR, "Couldn't create aPNG: error creating %s in %s\n", freename, pathname); return MM_OFF; } return MM_APNG; #else // no APNG support exists (void)pathname; CONS_Alert(CONS_ERROR, "Couldn't create aPNG: this build lacks aPNG support\n"); return MM_OFF; #endif } static inline moviemode_t M_StartMovieGIF(const char *pathname) { #ifdef HAVE_ANIGIF const char *freename; if (!(freename = Newsnapshotfile(pathname,"gif"))) { CONS_Alert(CONS_ERROR, "Couldn't create GIF: no slots open in %s\n", pathname); return MM_OFF; } if (!GIF_open(va(pandf,pathname,freename))) { CONS_Alert(CONS_ERROR, "Couldn't create GIF: error creating %s in %s\n", freename, pathname); return MM_OFF; } return MM_GIF; #else // no GIF support exists (void)pathname; CONS_Alert(CONS_ERROR, "Couldn't create GIF: this build lacks GIF support\n"); return MM_OFF; #endif } #endif static inline moviemode_t M_StartMovieAVRecorder(const char *pathname) { #ifndef SRB2_CONFIG_ENABLE_WEBM_MOVIES return MM_OFF; #else 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; #endif } void M_StartMovie(moviemode_t mode) { #if NUMSCREENS > 2 const char *folder; char pathname[MAX_WADPATH]; if (moviemode) return; switch (mode) { case MM_GIF: folder = "gifs"; break; case MM_AVRECORDER: folder = "movies"; break; default: folder = "slideshows"; } sprintf(pathname, "%s" PATHSEP "media" PATHSEP "%s" PATHSEP, srb2home, folder); M_MkdirEach(pathname, M_PathParts(pathname) - 2, 0755); if (rendermode == render_none) I_Error("Can't make a movie without a render system\n"); switch (mode) { case MM_GIF: moviemode = M_StartMovieGIF(pathname); break; case MM_APNG: moviemode = M_StartMovieAPNG(pathname); break; case MM_SCREENSHOT: moviemode = MM_SCREENSHOT; break; case MM_AVRECORDER: moviemode = M_StartMovieAVRecorder(pathname); break; default: //??? return; } if (moviemode == MM_APNG) CONS_Printf(M_GetText("Movie mode enabled (%s).\n"), "aPNG"); else if (moviemode == MM_GIF) 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"); #ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES else if (moviemode == MM_AVRECORDER) { CONS_Printf(M_GetText("Movie mode enabled (%s).\n"), M_AVRecorder_GetCurrentFormat()); M_AVRecorder_PrintCurrentConfiguration(); } #endif //g_singletics = (moviemode != MM_OFF); #endif } static void M_SaveFrame_AVRecorder(uint32_t width, uint32_t height, tcb::span data); void M_LegacySaveFrame(void) { #if NUMSCREENS > 2 // TODO: until HWR2 replaces legacy OpenGL renderer, this // function still needs to called for OpenGL. #ifdef HWRENDER if (rendermode != render_opengl) #endif { return; } // paranoia: should be unnecessary without singletics static tic_t oldtic = 0; #ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES if (moviemode == MM_AVRECORDER) { if (M_AVRecorder_IsExpired()) { M_StopMovie(); return; } } #endif // skip interpolated frames for other modes if (oldtic == I_GetTime()) return; else oldtic = I_GetTime(); switch (moviemode) { case MM_SCREENSHOT: takescreenshot = true; return; case MM_GIF: GIF_frame(); return; case MM_APNG: #ifdef USE_APNG { UINT8 *linear = NULL; if (!apng_FILE) // should not happen!! { moviemode = MM_OFF; return; } if (rendermode == render_soft) { // munge planar buffer to linear linear = screens[2]; I_ReadScreen(linear); } #ifdef HWRENDER else linear = HWR_GetScreenshot(); #endif M_PNGFrame(apng_ptr, apng_info_ptr, (png_bytep)linear); #ifdef HWRENDER if (rendermode == render_opengl && linear) free(linear); #endif if (apng_frames == PNG_UINT_31_MAX) { CONS_Alert(CONS_NOTICE, M_GetText("Max movie size reached\n")); M_StopMovie(); } } #else moviemode = MM_OFF; #endif return; case MM_AVRECORDER: #if defined(SRB2_CONFIG_ENABLE_WEBM_MOVIES) && defined(HWRENDER) { UINT8 *linear = HWR_GetScreenshot(); M_SaveFrame_AVRecorder(vid.width, vid.height, tcb::as_bytes(tcb::span(linear, 3 * vid.width * vid.height))); free(linear); } #endif return; default: return; } #endif } static void M_SaveFrame_GIF(uint32_t width, uint32_t height, tcb::span data) { if (moviemode != MM_GIF) { return; } static tic_t oldtic = 0; // limit the recording to TICRATE if (oldtic == I_GetTime()) { return; } oldtic = I_GetTime(); GIF_frame_rgb24(width, height, reinterpret_cast(data.data())); } static void M_SaveFrame_AVRecorder(uint32_t width, uint32_t height, tcb::span data) { #ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES if (M_AVRecorder_IsExpired()) { M_StopMovie(); return; } auto frame = g_av_recorder->new_staging_video_frame(width, height); if (!frame) { // Not time to submit a frame! return; } auto data_begin = reinterpret_cast(data.data()); auto data_end = reinterpret_cast(data.data() + data.size_bytes()); std::copy(data_begin, data_end, frame->screen.begin()); g_av_recorder->push_staging_video_frame(std::move(frame)); #endif } void M_SaveFrame(uint32_t width, uint32_t height, tcb::span data) { switch (moviemode) { case MM_GIF: M_SaveFrame_GIF(width, height, data); break; case MM_AVRECORDER: M_SaveFrame_AVRecorder(width, height, data); break; default: break; } } void M_StopMovie(void) { #if NUMSCREENS > 2 switch (moviemode) { case MM_GIF: if (!GIF_close()) return; break; case MM_APNG: #ifdef USE_APNG if (!apng_FILE) return; if (apng_frames) { M_PNGfix_acTL(apng_ptr, apng_info_ptr, apng_ainfo_ptr); apng_write_end(apng_ptr, apng_info_ptr, apng_ainfo_ptr); } png_destroy_write_struct(&apng_ptr, &apng_info_ptr); fclose(apng_FILE); apng_FILE = NULL; CONS_Printf("aPNG closed; wrote %u frames\n", (UINT32)apng_frames); apng_frames = 0; break; #else return; #endif case MM_SCREENSHOT: break; #ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES case MM_AVRECORDER: M_AVRecorder_Close(); break; #endif default: return; } moviemode = MM_OFF; CONS_Printf(M_GetText("Movie mode disabled.\n")); #endif } // ========================================================================== // SCREEN SHOTS // ========================================================================== #ifdef USE_PNG /** Writes a PNG file to disk. * * \param filename Filename to write to. * \param data The image data. * \param width Width of the picture. * \param height Height of the picture. * \param palette Palette of image data. * \note if palette is NULL, BGR888 format */ boolean M_SavePNG(const char *filename, const void *data, int width, int height, const UINT8 *palette) { png_structp png_ptr; png_infop png_info_ptr; #ifdef PNG_SETJMP_SUPPORTED #ifdef USE_FAR_KEYWORD jmp_buf jmpbuf; #endif #endif png_FILE_p png_FILE; png_FILE = fopen(filename,"wb"); if (!png_FILE) { CONS_Debug(DBG_RENDER, "M_SavePNG: Error on opening %s for write\n", filename); return false; } png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, PNG_error, PNG_warn); if (!png_ptr) { CONS_Debug(DBG_RENDER, "M_SavePNG: Error on initialize libpng\n"); fclose(png_FILE); remove(filename); return false; } png_info_ptr = png_create_info_struct(png_ptr); if (!png_info_ptr) { CONS_Debug(DBG_RENDER, "M_SavePNG: Error on allocate for libpng\n"); png_destroy_write_struct(&png_ptr, NULL); fclose(png_FILE); remove(filename); return false; } #ifdef USE_FAR_KEYWORD if (setjmp(jmpbuf)) #else if (setjmp(png_jmpbuf(png_ptr))) #endif { //CONS_Debug(DBG_RENDER, "libpng write error on %s\n", filename); png_destroy_write_struct(&png_ptr, &png_info_ptr); fclose(png_FILE); remove(filename); return false; } #ifdef USE_FAR_KEYWORD png_memcpy(png_jmpbuf(png_ptr),jmpbuf, sizeof (jmp_buf)); #endif png_init_io(png_ptr, png_FILE); #ifdef PNG_SET_USER_LIMITS_SUPPORTED png_set_user_limits(png_ptr, MAXVIDWIDTH, MAXVIDHEIGHT); #endif //png_set_filter(png_ptr, 0, PNG_ALL_FILTERS); png_set_compression_level(png_ptr, cv_zlib_level.value); png_set_compression_mem_level(png_ptr, cv_zlib_memory.value); png_set_compression_strategy(png_ptr, cv_zlib_strategy.value); png_set_compression_window_bits(png_ptr, cv_zlib_window_bits.value); M_PNGhdr(png_ptr, png_info_ptr, width, height, palette); M_PNGText(png_ptr, png_info_ptr, false); png_write_info(png_ptr, png_info_ptr); M_PNGImage(png_ptr, png_info_ptr, height, (png_bytep)data); png_write_end(png_ptr, png_info_ptr); png_destroy_write_struct(&png_ptr, &png_info_ptr); fclose(png_FILE); return true; } #else /** PCX file structure. */ typedef struct { UINT8 manufacturer; UINT8 version; UINT8 encoding; UINT8 bits_per_pixel; UINT16 xmin, ymin; UINT16 xmax, ymax; UINT16 hres, vres; UINT8 palette[48]; UINT8 reserved; UINT8 color_planes; UINT16 bytes_per_line; UINT16 palette_type; char filler[58]; UINT8 data; ///< Unbounded; used for all picture data. } pcx_t; /** Writes a PCX file to disk. * * \param filename Filename to write to. * \param data The image data. * \param width Width of the picture. * \param height Height of the picture. * \param palette Palette of image data */ #if NUMSCREENS > 2 static boolean WritePCXfile(const char *filename, const UINT8 *data, int width, int height, const UINT8 *pal) { int i; size_t length; pcx_t *pcx; UINT8 *pack; pcx = Z_Malloc(width*height*2 + 1000, PU_STATIC, NULL); pcx->manufacturer = 0x0a; // PCX id pcx->version = 5; // 256 color pcx->encoding = 1; // uncompressed pcx->bits_per_pixel = 8; // 256 color pcx->xmin = pcx->ymin = 0; pcx->xmax = SHORT(width - 1); pcx->ymax = SHORT(height - 1); pcx->hres = SHORT(width); pcx->vres = SHORT(height); memset(pcx->palette, 0, sizeof (pcx->palette)); pcx->reserved = 0; pcx->color_planes = 1; // chunky image pcx->bytes_per_line = SHORT(width); pcx->palette_type = SHORT(1); // not a grey scale memset(pcx->filler, 0, sizeof (pcx->filler)); // pack the image pack = &pcx->data; for (i = 0; i < width*height; i++) { if ((*data & 0xc0) != 0xc0) *pack++ = *data++; else { *pack++ = 0xc1; *pack++ = *data++; } } // write the palette *pack++ = 0x0c; // palette ID byte // write color table { for (i = 0; i < 256; i++) { *pack++ = *pal; pal++; *pack++ = *pal; pal++; *pack++ = *pal; pal++; } } // write output file length = pack - (UINT8 *)pcx; i = FIL_WriteFile(filename, pcx, length); Z_Free(pcx); return i; } #endif #endif void M_ScreenShot(void) { takescreenshot = true; } void M_DoLegacyGLScreenShot(void) { const std::byte* fake_data = nullptr; M_DoScreenShot(vid.width, vid.height, tcb::span(fake_data, vid.width * vid.height)); } /** Takes a screenshot. * The screenshot is saved as "srb2xxxx.png" where xxxx is the lowest * four-digit number for which a file does not already exist. * * \sa HWR_ScreenShot */ void M_DoScreenShot(UINT32 width, UINT32 height, tcb::span data) { #if NUMSCREENS > 2 const char *freename = NULL; char pathname[MAX_WADPATH]; boolean ret = false; // Don't take multiple screenshots, obviously takescreenshot = false; // how does one take a screenshot without a render system? if (rendermode == render_none) return; strcpy(pathname, srb2home); strcat(pathname, PATHSEP "media" PATHSEP "screenshots" PATHSEP); M_MkdirEach(pathname, M_PathParts(pathname) - 2, 0755); #ifdef USE_PNG freename = Newsnapshotfile(pathname,"png"); #else if (rendermode == render_soft) freename = Newsnapshotfile(pathname,"pcx"); else if (rendermode == render_opengl) freename = Newsnapshotfile(pathname,"tga"); #endif if (!freename) goto failure; // save the pcx file #ifdef HWRENDER if (rendermode == render_opengl) ret = HWR_Screenshot(va(pandf,pathname,freename)); else #endif { const void* pixel_data = static_cast(data.data()); #ifdef USE_PNG ret = M_SavePNG(va(pandf,pathname,freename), pixel_data, width, height, NULL); #else ret = WritePCXfile(va(pandf,pathname,freename), linear, vid.width, vid.height, screenshot_palette); #endif } failure: if (ret) { if (moviemode != MM_SCREENSHOT) CONS_Printf(M_GetText("Screen shot %s saved in %s\n"), freename, pathname); } else { if (freename) CONS_Alert(CONS_ERROR, M_GetText("Couldn't create screen shot %s in %s\n"), freename, pathname); else CONS_Alert(CONS_ERROR, M_GetText("Couldn't create screen shot in %s (all 10000 slots used!)\n"), pathname); if (moviemode == MM_SCREENSHOT) M_StopMovie(); } #endif } void M_SaveMapThumbnail(UINT32 width, UINT32 height, tcb::span data) { #ifdef USE_PNG #if NUMSCREENS > 2 char *filepath; switch (g_takemapthumbnail) { case TMT_PICTURE: default: { filepath = va("%s" PATHSEP "PICTURE_%s.png", srb2home, G_BuildMapName(gamemap)); break; } case TMT_RICHPRES: { filepath = va("%s" PATHSEP "map_%s.png", srb2home, G_BuildMapName(gamemap)); break; } } // save the file const void* pixel_data = static_cast(data.data()); boolean ret = M_SavePNG(filepath, pixel_data, width, height, NULL); if (ret) { CONS_Printf(M_GetText("Created thumbnail at \"%s\"\n"), filepath); } else { CONS_Alert(CONS_ERROR, M_GetText("Couldn't create %s\n"), filepath); } g_takemapthumbnail = TMT_NO; #endif // #ifdef USE_PNG #endif // #if NUMSCREENS > 2 } void M_ScreenshotTicker(void) { const UINT8 pid = 0; // TODO: should splitscreen players be allowed to use this too? if (M_MenuButtonPressed(pid, MBT_SCREENSHOT)) { M_ScreenShot(); } else if (M_MenuButtonPressed(pid, MBT_STARTMOVIE)) { if (moviemode) { M_StopMovie(); } else { M_StartMovie(MM_AVRECORDER); } } else if (M_MenuButtonPressed(pid, MBT_STARTLOSSLESS)) { if (moviemode) { M_StopMovie(); } else { M_StartMovie(static_cast(cv_lossless_recorder.value)); } } } void M_MinimapGenerate(void) { #ifdef USE_PNG char *filepath; boolean ret = false; minigen_t *minigen = NULL; size_t option_scale; INT32 mul = 1; if (gamestate != GS_LEVEL) { CONS_Alert(CONS_ERROR, "You must be in a level to generate a preliminary minimap!\n"); return; } if (automapactive) { CONS_Alert(CONS_ERROR, "The automap is active! Please deactivate it and try again.\n"); return; } option_scale = COM_CheckPartialParm("-m"); if (option_scale) { if (COM_Argc() < option_scale + 2)/* no argument after? */ { CONS_Alert(CONS_ERROR, "No multiplier follows parameter '%s'.\n", COM_Argv(option_scale)); return; } mul = atoi(COM_Argv(option_scale + 1)); if (mul < 1 || mul > 10) { CONS_Alert(CONS_ERROR, "Multiplier %d must be within range 1-10.\n", mul); return; } filepath = va("%s" PATHSEP "%s-MINIMAP-%d.png", srb2home, G_BuildMapName(gamemap), mul); } else { filepath = va("%s" PATHSEP "%s-MINIMAP.png", srb2home, G_BuildMapName(gamemap)); } minigen = AM_MinimapGenerate(mul); if (minigen == NULL || minigen->buf == NULL) goto failure; M_CreateScreenShotPalette(); ret = M_SavePNG(filepath, minigen->buf, minigen->w, minigen->h, screenshot_palette); failure: if (minigen->buf != NULL) free(minigen->buf); if (ret) { CONS_Printf(M_GetText("%s saved.\nRemember that this is not a complete minimap,\nand must be edited before putting in-game.\n"), filepath); if (mul != 1) { CONS_Printf("You should divide its size by %d!\n", mul); } } else { CONS_Alert(CONS_ERROR, M_GetText("Couldn't create %s\n"), filepath); } #endif //#ifdef USE_PNG } // ========================================================================== // TRANSLATION FUNCTIONS // ========================================================================== // M_StartupLocale. // Sets up gettext to translate SRB2's strings. #ifdef GETTEXT #if defined (__unix__) || defined(__APPLE__) || defined (UNIXCOMMON) #define GETTEXTDOMAIN1 "/usr/share/locale" #define GETTEXTDOMAIN2 "/usr/local/share/locale" #elif defined (_WIN32) #define GETTEXTDOMAIN1 "." #endif #endif // GETTEXT void M_StartupLocale(void) { #ifdef GETTEXT char *textdomhandle = NULL; #endif //GETTEXT CONS_Printf("M_StartupLocale...\n"); setlocale(LC_ALL, ""); // Do not set numeric locale as that affects atof setlocale(LC_NUMERIC, "C"); #ifdef GETTEXT // FIXME: global name define anywhere? #ifdef GETTEXTDOMAIN1 textdomhandle = bindtextdomain("srb2", GETTEXTDOMAIN1); #endif #ifdef GETTEXTDOMAIN2 if (!textdomhandle) textdomhandle = bindtextdomain("srb2", GETTEXTDOMAIN2); #endif #ifdef GETTEXTDOMAIN3 if (!textdomhandle) textdomhandle = bindtextdomain("srb2", GETTEXTDOMAIN3); #endif #ifdef GETTEXTDOMAIN4 if (!textdomhandle) textdomhandle = bindtextdomain("srb2", GETTEXTDOMAIN4); #endif if (textdomhandle) textdomain("srb2"); else CONS_Printf("Could not find locale text domain!\n"); #endif //GETTEXT } // ========================================================================== // MISC STRING FUNCTIONS // ========================================================================== /** Returns a temporary string made out of varargs. * For use with CONS_Printf(). * * \param format Format string. * \return Pointer to a static buffer of 1024 characters, containing the * resulting string. */ char *va(const char *format, ...) { va_list argptr; static char string[1024]; va_start(argptr, format); vsprintf(string, format, argptr); va_end(argptr); return string; } /** Creates a string in the first argument that is the second argument followed * by the third argument followed by the first argument. * Useful for making filenames with full path. s1 = s2+s3+s1 * * \param s1 First string, suffix, and destination. * \param s2 Second string. Ends up first in the result. * \param s3 Third string. Ends up second in the result. */ void strcatbf(char *s1, const char *s2, const char *s3) { char tmp[1024]; strcpy(tmp, s1); strcpy(s1, s2); strcat(s1, s3); strcat(s1, tmp); } /** Converts an ASCII Hex string into an integer. Thanks, Borland! * I don't know if this belongs here specifically, but it sure * doesn't belong in p_spec.c, that's for sure * * \param hexStg Hexadecimal string. * \return an Integer based off the contents of the string. */ INT32 axtoi(const char *hexStg) { INT32 n = 0; INT32 m = 0; INT32 count; INT32 intValue = 0; INT32 digit[8]; while (n < 8) { if (hexStg[n] == '\0') break; if (hexStg[n] >= '0' && hexStg[n] <= '9') // 0-9 digit[n] = (hexStg[n] & 0x0f); else if (hexStg[n] >= 'a' && hexStg[n] <= 'f') // a-f digit[n] = (hexStg[n] & 0x0f) + 9; else if (hexStg[n] >= 'A' && hexStg[n] <= 'F') // A-F digit[n] = (hexStg[n] & 0x0f) + 9; else break; n++; } count = n; m = n - 1; n = 0; while (n < count) { intValue = intValue | (digit[n] << (m << 2)); m--; n++; } return intValue; } // Token parser variables static UINT32 oldendPos = 0; // old value of endPos, used by M_UnGetToken static UINT32 endPos = 0; // now external to M_GetToken, but still static /** Token parser for TEXTURES, ANIMDEFS, and potentially other lumps later down the line. * Was originally R_GetTexturesToken when I was coding up the TEXTURES parser, until I realized I needed it for ANIMDEFS too. * Parses up to the next whitespace character or comma. When finding the start of the next token, whitespace is skipped. * Commas are not; if a comma is encountered, then THAT'S returned as the token. * -Shadow Hog * * \param inputString The string to be parsed. If NULL is supplied instead of a string, it will continue parsing the last supplied one. * The pointer to the last string supplied is stored as a static variable, so be careful not to free it while this function is still using it! * \return A pointer to a string, containing the fetched token. This is in freshly allocated memory, so be sure to Z_Free() it as appropriate. */ char *M_GetToken(const char *inputString) { static const char *stringToUse = NULL; // Populated if inputString != NULL; used otherwise static UINT32 startPos = 0; // static UINT32 endPos = 0; static UINT32 stringLength = 0; static UINT8 inComment = 0; // 0 = not in comment, 1 = // Single-line, 2 = /* Multi-line */ char *texturesToken = NULL; UINT32 texturesTokenLength = 0; if (inputString != NULL) { stringToUse = inputString; startPos = 0; oldendPos = endPos = 0; stringLength = strlen(inputString); } else { startPos = oldendPos = endPos; } if (stringToUse == NULL) return NULL; // Try to detect comments now, in case we're pointing right at one if (startPos < stringLength - 1 && inComment == 0) { if (stringToUse[startPos] == '/' && stringToUse[startPos+1] == '/') { //Single-line comment start inComment = 1; } else if (stringToUse[startPos] == '/' && stringToUse[startPos+1] == '*') { //Multi-line comment start inComment = 2; } } // Find the first non-whitespace char, or else the end of the string trying while (startPos < stringLength && (stringToUse[startPos] == ' ' || stringToUse[startPos] == '\t' || stringToUse[startPos] == '\r' || stringToUse[startPos] == '\n' || stringToUse[startPos] == '\0' || stringToUse[startPos] == '=' || stringToUse[startPos] == ';' // UDMF TEXTMAP. || inComment != 0)) { // Try to detect comment endings now if (inComment == 1 && stringToUse[startPos] == '\n') { // End of line for a single-line comment inComment = 0; } else if (inComment == 2 && startPos < stringLength - 1 && stringToUse[startPos] == '*' && stringToUse[startPos+1] == '/') { // End of multi-line comment inComment = 0; startPos++; // Make damn well sure we're out of the comment ending at the end of it all } startPos++; // Try to detect comment starts now if (startPos < stringLength - 1 && inComment == 0) { if (stringToUse[startPos] == '/' && stringToUse[startPos+1] == '/') { //Single-line comment start inComment = 1; } else if (stringToUse[startPos] == '/' && stringToUse[startPos+1] == '*') { //Multi-line comment start inComment = 2; } } } // If the end of the string is reached, no token is to be read if (startPos >= stringLength) { endPos = stringLength; return NULL; } // Else, if it's one of these three symbols, capture only this one character else if (stringToUse[startPos] == ',' || stringToUse[startPos] == '{' || stringToUse[startPos] == '}') { endPos = startPos + 1; texturesToken = (char *)Z_Malloc(2*sizeof(char),PU_STATIC,NULL); texturesToken[0] = stringToUse[startPos]; texturesToken[1] = '\0'; return texturesToken; } // Return entire string within quotes, except without the quotes. else if (stringToUse[startPos] == '"') { endPos = ++startPos; while (endPos < stringLength && stringToUse[endPos] != '"') endPos++; texturesTokenLength = endPos++ - startPos; // Assign the memory. Don't forget an extra byte for the end of the string! texturesToken = (char *)Z_Malloc((texturesTokenLength+1)*sizeof(char),PU_STATIC,NULL); // Copy the string. M_Memcpy(texturesToken, stringToUse+startPos, (size_t)texturesTokenLength); // Make the final character NUL. texturesToken[texturesTokenLength] = '\0'; return texturesToken; } // Now find the end of the token. This includes several additional characters that are okay to capture as one character, but not trailing at the end of another token. endPos = startPos + 1; while (endPos < stringLength && (stringToUse[endPos] != ' ' && stringToUse[endPos] != '\t' && stringToUse[endPos] != '\r' && stringToUse[endPos] != '\n' && stringToUse[endPos] != ',' && stringToUse[endPos] != '{' && stringToUse[endPos] != '}' && stringToUse[endPos] != '=' && stringToUse[endPos] != ';' // UDMF TEXTMAP. && inComment == 0)) { endPos++; // Try to detect comment starts now; if it's in a comment, we don't want it in this token if (endPos < stringLength - 1 && inComment == 0) { if (stringToUse[endPos] == '/' && stringToUse[endPos+1] == '/') { //Single-line comment start inComment = 1; } else if (stringToUse[endPos] == '/' && stringToUse[endPos+1] == '*') { //Multi-line comment start inComment = 2; } } } texturesTokenLength = endPos - startPos; // Assign the memory. Don't forget an extra byte for the end of the string! texturesToken = (char *)Z_Malloc((texturesTokenLength+1)*sizeof(char),PU_STATIC,NULL); // Copy the string. M_Memcpy(texturesToken, stringToUse+startPos, (size_t)texturesTokenLength); // Make the final character NUL. texturesToken[texturesTokenLength] = '\0'; return texturesToken; } /** Undoes the last M_GetToken call * The current position along the string being parsed is reset to the last saved position. * This exists mostly because of R_ParseTexture/R_ParsePatch honestly, but could be useful elsewhere? * -Monster Iestyn (22/10/16) */ void M_UnGetToken(void) { endPos = oldendPos; } /** Returns the current token's position. */ UINT32 M_GetTokenPos(void) { return endPos; } #define NUMTOKENS 2 static const char *tokenizerInput = NULL; static UINT32 tokenCapacity[NUMTOKENS] = {0}; static char *tokenizerToken[NUMTOKENS] = {NULL}; static UINT32 tokenizerStartPos = 0; static UINT32 tokenizerEndPos = 0; static UINT32 tokenizerInputLength = 0; static UINT8 tokenizerInComment = 0; // 0 = not in comment, 1 = // Single-line, 2 = /* Multi-line */ static boolean tokenizerIsString = false; // did we strip quotes from this token? void M_TokenizerOpen(const char *inputString, size_t inputLength) { size_t i; tokenizerInput = inputString; for (i = 0; i < NUMTOKENS; i++) { tokenCapacity[i] = 1024; tokenizerToken[i] = (char*)Z_Malloc(tokenCapacity[i] * sizeof(char), PU_STATIC, NULL); } tokenizerInputLength = inputLength; } void M_TokenizerClose(void) { size_t i; tokenizerInput = NULL; for (i = 0; i < NUMTOKENS; i++) Z_Free(tokenizerToken[i]); tokenizerStartPos = 0; tokenizerEndPos = 0; tokenizerInComment = 0; tokenizerIsString = false; } static void M_DetectComment(UINT32 *pos) { if (tokenizerInComment) return; if (*pos >= tokenizerInputLength - 1) return; if (tokenizerInput[*pos] != '/') return; //Single-line comment start if (tokenizerInput[*pos + 1] == '/') tokenizerInComment = 1; //Multi-line comment start else if (tokenizerInput[*pos + 1] == '*') tokenizerInComment = 2; } static void M_ReadTokenString(UINT32 i) { UINT32 tokenLength = tokenizerEndPos - tokenizerStartPos; if (tokenLength + 1 > tokenCapacity[i]) { tokenCapacity[i] = tokenLength + 1; // Assign the memory. Don't forget an extra byte for the end of the string! tokenizerToken[i] = (char *)Z_Malloc(tokenCapacity[i] * sizeof(char), PU_STATIC, NULL); } // Copy the string. M_Memcpy(tokenizerToken[i], tokenizerInput + tokenizerStartPos, (size_t)tokenLength); // Make the final character NUL. tokenizerToken[i][tokenLength] = '\0'; } const char *M_TokenizerRead(UINT32 i) { if (!tokenizerInput) return NULL; tokenizerStartPos = tokenizerEndPos; // Reset string flag tokenizerIsString = false; // Try to detect comments now, in case we're pointing right at one M_DetectComment(&tokenizerStartPos); // Find the first non-whitespace char, or else the end of the string trying while ((tokenizerInput[tokenizerStartPos] == ' ' || tokenizerInput[tokenizerStartPos] == '\t' || tokenizerInput[tokenizerStartPos] == '\r' || tokenizerInput[tokenizerStartPos] == '\n' || tokenizerInput[tokenizerStartPos] == '\0' || tokenizerInput[tokenizerStartPos] == '=' || tokenizerInput[tokenizerStartPos] == ';' // UDMF TEXTMAP. || tokenizerInComment != 0) && tokenizerStartPos < tokenizerInputLength) { // Try to detect comment endings now if (tokenizerInComment == 1 && tokenizerInput[tokenizerStartPos] == '\n') tokenizerInComment = 0; // End of line for a single-line comment else if (tokenizerInComment == 2 && tokenizerStartPos < tokenizerInputLength - 1 && tokenizerInput[tokenizerStartPos] == '*' && tokenizerInput[tokenizerStartPos+1] == '/') { // End of multi-line comment tokenizerInComment = 0; tokenizerStartPos++; // Make damn well sure we're out of the comment ending at the end of it all } tokenizerStartPos++; M_DetectComment(&tokenizerStartPos); } // If the end of the string is reached, no token is to be read if (tokenizerStartPos == tokenizerInputLength) { tokenizerEndPos = tokenizerInputLength; return NULL; } // Else, if it's one of these three symbols, capture only this one character else if (tokenizerInput[tokenizerStartPos] == ',' || tokenizerInput[tokenizerStartPos] == '{' || tokenizerInput[tokenizerStartPos] == '}') { tokenizerEndPos = tokenizerStartPos + 1; tokenizerToken[i][0] = tokenizerInput[tokenizerStartPos]; tokenizerToken[i][1] = '\0'; return tokenizerToken[i]; } // Return entire string within quotes, except without the quotes. else if (tokenizerInput[tokenizerStartPos] == '"') { tokenizerEndPos = ++tokenizerStartPos; while (tokenizerInput[tokenizerEndPos] != '"' && tokenizerEndPos < tokenizerInputLength) tokenizerEndPos++; M_ReadTokenString(i); tokenizerEndPos++; // Tell us the the token was a string. tokenizerIsString = true; return tokenizerToken[i]; } // Now find the end of the token. This includes several additional characters that are okay to capture as one character, but not trailing at the end of another token. tokenizerEndPos = tokenizerStartPos + 1; while ((tokenizerInput[tokenizerEndPos] != ' ' && tokenizerInput[tokenizerEndPos] != '\t' && tokenizerInput[tokenizerEndPos] != '\r' && tokenizerInput[tokenizerEndPos] != '\n' && tokenizerInput[tokenizerEndPos] != ',' && tokenizerInput[tokenizerEndPos] != '{' && tokenizerInput[tokenizerEndPos] != '}' && tokenizerInput[tokenizerEndPos] != '=' && tokenizerInput[tokenizerEndPos] != ';' // UDMF TEXTMAP. && tokenizerInComment == 0) && tokenizerEndPos < tokenizerInputLength) { tokenizerEndPos++; // Try to detect comment starts now; if it's in a comment, we don't want it in this token M_DetectComment(&tokenizerEndPos); } M_ReadTokenString(i); return tokenizerToken[i]; } UINT32 M_TokenizerGetEndPos(void) { return tokenizerEndPos; } void M_TokenizerSetEndPos(UINT32 newPos) { tokenizerEndPos = newPos; } boolean M_TokenizerJustReadString(void) { return tokenizerIsString; } /** Count bits in a number. */ UINT8 M_CountBits(UINT32 num, UINT8 size) { UINT8 i, sum = 0; for (i = 0; i < size; ++i) if (num & (1 << i)) ++sum; return sum; } const char *GetRevisionString(void) { static char rev[9] = {0}; if (rev[0]) return rev; if (comprevision[0] == 'r') strncpy(rev, comprevision, 7); else snprintf(rev, 7, "r%s", comprevision); rev[7] = '\0'; return rev; } // Vector/matrix math TVector *VectorMatrixMultiply(TVector v, TMatrix m) { static TVector ret; ret[0] = FixedMul(v[0],m[0][0]) + FixedMul(v[1],m[1][0]) + FixedMul(v[2],m[2][0]) + FixedMul(v[3],m[3][0]); ret[1] = FixedMul(v[0],m[0][1]) + FixedMul(v[1],m[1][1]) + FixedMul(v[2],m[2][1]) + FixedMul(v[3],m[3][1]); ret[2] = FixedMul(v[0],m[0][2]) + FixedMul(v[1],m[1][2]) + FixedMul(v[2],m[2][2]) + FixedMul(v[3],m[3][2]); ret[3] = FixedMul(v[0],m[0][3]) + FixedMul(v[1],m[1][3]) + FixedMul(v[2],m[2][3]) + FixedMul(v[3],m[3][3]); return &ret; } TMatrix *RotateXMatrix(angle_t rad) { static TMatrix ret; const angle_t fa = rad>>ANGLETOFINESHIFT; const fixed_t cosrad = FINECOSINE(fa), sinrad = FINESINE(fa); ret[0][0] = FRACUNIT; ret[0][1] = 0; ret[0][2] = 0; ret[0][3] = 0; ret[1][0] = 0; ret[1][1] = cosrad; ret[1][2] = sinrad; ret[1][3] = 0; ret[2][0] = 0; ret[2][1] = -sinrad; ret[2][2] = cosrad; ret[2][3] = 0; ret[3][0] = 0; ret[3][1] = 0; ret[3][2] = 0; ret[3][3] = FRACUNIT; return &ret; } #if 0 TMatrix *RotateYMatrix(angle_t rad) { static TMatrix ret; const angle_t fa = rad>>ANGLETOFINESHIFT; const fixed_t cosrad = FINECOSINE(fa), sinrad = FINESINE(fa); ret[0][0] = cosrad; ret[0][1] = 0; ret[0][2] = -sinrad; ret[0][3] = 0; ret[1][0] = 0; ret[1][1] = FRACUNIT; ret[1][2] = 0; ret[1][3] = 0; ret[2][0] = sinrad; ret[2][1] = 0; ret[2][2] = cosrad; ret[2][3] = 0; ret[3][0] = 0; ret[3][1] = 0; ret[3][2] = 0; ret[3][3] = FRACUNIT; return &ret; } #endif TMatrix *RotateZMatrix(angle_t rad) { static TMatrix ret; const angle_t fa = rad>>ANGLETOFINESHIFT; const fixed_t cosrad = FINECOSINE(fa), sinrad = FINESINE(fa); ret[0][0] = cosrad; ret[0][1] = sinrad; ret[0][2] = 0; ret[0][3] = 0; ret[1][0] = -sinrad; ret[1][1] = cosrad; ret[1][2] = 0; ret[1][3] = 0; ret[2][0] = 0; ret[2][1] = 0; ret[2][2] = FRACUNIT; ret[2][3] = 0; ret[3][0] = 0; ret[3][1] = 0; ret[3][2] = 0; ret[3][3] = FRACUNIT; return &ret; } /** Set of functions to take in a size_t as an argument, * put the argument in a character buffer, and return the * pointer to that buffer. * This is to eliminate usage of PRIdS, so gettext can work * with *all* of SRB2's strings. */ char *sizeu1(size_t num) { static char sizeu1_buf[28]; sprintf(sizeu1_buf, "%" PRIdS, num); return sizeu1_buf; } char *sizeu2(size_t num) { static char sizeu2_buf[28]; sprintf(sizeu2_buf, "%" PRIdS, num); return sizeu2_buf; } char *sizeu3(size_t num) { static char sizeu3_buf[28]; sprintf(sizeu3_buf, "%" PRIdS, num); return sizeu3_buf; } char *sizeu4(size_t num) { static char sizeu4_buf[28]; sprintf(sizeu4_buf, "%" PRIdS, num); return sizeu4_buf; } char *sizeu5(size_t num) { static char sizeu5_buf[28]; sprintf(sizeu5_buf, "%" PRIdS, num); return sizeu5_buf; } /** Return the appropriate message for a file error or end of file. */ const char *M_FileError(FILE *fp) { if (ferror(fp)) return strerror(errno); else return "end-of-file"; } /** Return the number of parts of this path. */ int M_PathParts(const char *p) { int parts = 0; if (p == NULL) return 0; #ifdef _WIN32 if (!strncmp(&p[1], ":\\", 2)) p += 3; #endif while (*(p += strspn(p, PATHSEP))) { parts++; p += strcspn(p, PATHSEP); } return parts; } /** Check whether a path is an absolute path. */ boolean M_IsPathAbsolute(const char *path) { #ifdef _WIN32 return ( strncmp(&path[1], ":\\", 2) == 0 ); #else return ( path[0] == '/' ); #endif } /** I_mkdir for each part of the path. */ void M_MkdirEachUntil(const char *cpath, int start, int end, int mode) { char path[256]; char *p; int n; int c; if (end > 0 && end <= start) return; strlcpy(path, cpath, sizeof path); #ifdef _WIN32 if (!strncmp(&path[1], ":\\", 2)) p = &path[3]; else #endif p = path; while (end != 0 && *(p += strspn(p, PATHSEP))) { n = strcspn(p, PATHSEP); if (start > 0) start--; else { c = p[n]; p[n] = '\0'; I_mkdir(path, mode); p[n] = c; } p += n; if (end > 0) end--; } } void M_MkdirEach(const char *path, int start, int mode) { M_MkdirEachUntil(path, start, -1, mode); } int M_JumpWord(const char *line) { int c; c = line[0]; if (isspace(c)) return strspn(line, " "); else if (ispunct(c)) return strspn(line, PUNCTUATION); else { if (isspace(line[1])) return 1 + strspn(&line[1], " "); else return strcspn(line, " " PUNCTUATION); } } int M_JumpWordReverse(const char *line, int offset) { int (*is)(int); int c; c = line[--offset]; if (isspace(c)) is = isspace; else if (ispunct(c)) is = ispunct; else is = isalnum; c = (*is)(line[offset]); while (offset > 0 && (*is)(line[offset - 1]) == c) offset--; return offset; } const char * M_Ftrim (double f) { static char dig[9];/* "0." + 6 digits (6 is printf's default) */ int i; /* I know I said it's the default, but just in case... */ sprintf(dig, "%.6f", fabs(modf(f, &f))); /* trim trailing zeroes */ for (i = strlen(dig)-1; dig[i] == '0'; --i) ; if (dig[i] == '.')/* :NOTHING: */ return ""; else { dig[i + 1] = '\0'; return &dig[1];/* skip the 0 */ } }