blankart/src/m_misc.cpp
GenericHeroGuy e92362f36c (WIP) Rewrite screen capture code for GIFs
Downscaling is now performed during screen capture instead of GIF writing
This may or may not make GIF recording faster?
Maybe it would be faster if I knew how OpenGL worked...
Regardless, the m*th in the GIF code is gone, fixing the high/odd res issues

Also includes a little deduped hardware code, and GL 4.3 debugging code
2025-09-09 23:09:51 +02:00

2562 lines
65 KiB
C++

// BLANKART
//-----------------------------------------------------------------------------
// Copyright (C) 1993-1996 by id Software, Inc.
// Copyright (C) 1998-2000 by DooM Legacy Team.
// Copyright (C) 1999-2020 by Sonic Team Junior.
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
/// \file 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 <unistd.h>
#endif
#include <algorithm>
#include <errno.h>
// Extended map support.
#include <ctype.h>
#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"
// So that the screenshot menu auto-updates...
#include "m_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
static CV_PossibleValue_t screenshot_cons_t[] = {{0, "Default"}, {1, "HOME"}, {2, "SRB2"}, {3, "CUSTOM"}, {0, NULL}};
consvar_t cv_screenshot_option = CVAR_INIT ("screenshot_option", "Default", CV_SAVE|CV_CALL|CV_NOINIT, screenshot_cons_t, Screenshot_option_Onchange);
consvar_t cv_screenshot_folder = CVAR_INIT ("screenshot_folder", "", CV_SAVE, NULL, NULL);
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|CV_NOINIT, 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);
static 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}};
static 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}};
static 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}};
static 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}};
static CV_PossibleValue_t apng_delay_t[] = {
{1, "1x"},
{2, "1/2x"},
{3, "1/3x"},
{4, "1/4x"},
{0, NULL}};
// zlib memory usage is as follows:
// (1 << (zlib_window_bits+2)) + (1 << (zlib_level+9))
consvar_t cv_zlib_memory = CVAR_INIT ("png_memory_level", "7", CV_SAVE, zlib_mem_level_t, NULL);
consvar_t cv_zlib_level = CVAR_INIT ("png_compress_level", "(Optimal) 6", CV_SAVE, zlib_level_t, NULL);
consvar_t cv_zlib_strategy = CVAR_INIT ("png_strategy", "Normal", CV_SAVE, zlib_strategy_t, NULL);
consvar_t cv_zlib_window_bits = CVAR_INIT ("png_window_size", "32k", CV_SAVE, zlib_window_bits_t, NULL);
consvar_t cv_zlib_memorya = CVAR_INIT ("apng_memory_level", "(Max Memory) 9", CV_SAVE, zlib_mem_level_t, NULL);
consvar_t cv_zlib_levela = CVAR_INIT ("apng_compress_level", "4", CV_SAVE, zlib_level_t, NULL);
consvar_t cv_zlib_strategya = CVAR_INIT ("apng_strategy", "RLE", CV_SAVE, zlib_strategy_t, NULL);
consvar_t cv_zlib_window_bitsa = CVAR_INIT ("apng_window_size", "32k", CV_SAVE, zlib_window_bits_t, NULL);
consvar_t cv_apng_delay = CVAR_INIT ("apng_speed", "1x", CV_SAVE, apng_delay_t, NULL);
consvar_t cv_apng_downscale = CVAR_INIT ("apng_downscale", "On", CV_SAVE, CV_OnOff, 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;
// ==========================================================================
// 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<UINT8*>(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;
}
/** 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
}
/** Copies contents of one file to another
*
* \param src File to copy from
* \param dst File to write into
* \return true if copy was succesful, false if it doesn't.
*/
boolean FIL_CopyFile(const char *src, const char *dst)
{
FILE *srcFile = fopen(src, "rb");
if (srcFile == NULL)
return false;
FILE *dstFile = fopen(dst, "wb");
if (dstFile == NULL)
{
fclose(srcFile);
return false;
}
char buffer[1024];
size_t n;
while ((n = fread(buffer, 1, sizeof(buffer), srcFile)) > 0)
fwrite(buffer, 1, n, dstFile);
fclose(srcFile);
fclose(dstFile);
return true;
}
/** 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;
}
// ==========================================================================
// 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 <filename[.cfg]> [-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 <filename[.cfg]> : 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_ResetControls(i);
}
// 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 <filename[.cfg]> : 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);
}
for (i = 0; i < MAXSPLITSCREENPLAYERS; i++)
{
G_ResetControls(i);
}
// register execversion here before we load any configs
CV_RegisterVar(&cv_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 *filepath;
char backupfile[MAX_WADPATH+4];
// make sure not to write back the config until it's been correctly loaded
if (!gameconfig_loaded)
return;
// Create backup of the config file
snprintf(backupfile, sizeof backupfile, "%s.bak", configfile);
backupfile[sizeof backupfile - 1] = '\0';
FILE *config = fopen(configfile, "r");
if (config != NULL)
{
fclose(config);
if (FIL_CopyFile(configfile, backupfile) == false)
{
CONS_Alert(CONS_WARNING,"Failed to create a backup of the configuration file. Will not attempt to write to file\n");
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))
filepath = va(pandf,srb2home, filename);
else
filepath = Z_StrDup(filename);
f = fopen(filepath, "w");
// change it only if valid
if (f)
strcpy(configfile, filepath);
else
{
CONS_Alert(CONS_ERROR, M_GetText("Couldn't save game config file %s\n"), filepath);
return;
}
}
else
{
if (!strstr(configfile, ".cfg"))
{
CONS_Alert(CONS_NOTICE, M_GetText("Config filename must be .cfg\n"));
return;
}
f = fopen(configfile, "w");
if (!f)
{
CONS_Alert(CONS_ERROR, M_GetText("Couldn't save game config file %s\n"), configfile);
return;
}
}
// header message
fprintf(f, "// SRB2Kart 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);
}
// ==========================================================================
// 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[13] = "kartXXXX.ext";
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
strcpy(freename+9,ext);
for (;;)
{
freename[4] = (char)('0' + (char)(i/1000));
freename[5] = (char)('0' + (char)((i/100)%10));
freename[6] = (char)('0' + (char)((i/10)%10));
freename[7] = (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[4] = (char)('0' + (char)((i-1)/1000));
freename[5] = (char)('0' + (char)(((i-1)/100)%10));
freename[6] = (char)('0' + (char)(((i-1)/10)%10));
freename[7] = (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[4] = (char)('0' + (char)(i/1000));
freename[5] = (char)('0' + (char)((i/100)%10));
freename[6] = (char)('0' + (char)((i/10)%10));
freename[7] = (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_colorp>(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[] = "SRB2Kart " VERSIONSTRING;
png_charp playertxt = cv_playername[0].zstring;
char desctxt[] = "SRB2Kart Screenshot";
char Movietxt[] = "SRB2Kart 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)
snprintf(maptext, MAXMAPLUMPNAME, "%s", G_BuildMapName(gamemap));
else
snprintf(maptext, 8, "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(" %s",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<const png_info*>(png_info_ptr));
png_bytepp row_pointers = static_cast<png_bytepp>(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
void M_StartMovie(void)
{
#if NUMSCREENS > 2
char pathname[MAX_WADPATH];
if (moviemode)
return;
if (cv_movie_option.value == 0)
strcpy(pathname, usehome ? srb2home : srb2path);
else if (cv_movie_option.value == 1)
strcpy(pathname, srb2home);
else if (cv_movie_option.value == 2)
strcpy(pathname, srb2path);
else if (cv_movie_option.value == 3 && *cv_movie_folder.string != '\0')
strcpy(pathname, cv_movie_folder.string);
if (cv_movie_option.value != 3)
{
strcat(pathname, PATHSEP "media" PATHSEP "movies" PATHSEP);
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 (cv_moviemode.value)
{
case MM_GIF:
moviemode = M_StartMovieGIF(pathname);
break;
case MM_APNG:
moviemode = M_StartMovieAPNG(pathname);
break;
case MM_SCREENSHOT:
moviemode = MM_SCREENSHOT;
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");
//singletics = (moviemode != MM_OFF);
#endif
}
void M_SaveFrame(void)
{
#if NUMSCREENS > 2
// paranoia: should be unnecessary without singletics
static tic_t oldtic = 0;
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, 1);
}
#ifdef HWRENDER
else
linear = HWR_GetScreenshot();
#endif
M_PNGFrame(apng_ptr, apng_info_ptr, (png_bytep)linear);
#ifdef HWRENDER
if (rendermode != render_soft && 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;
default:
return;
}
#endif
}
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;
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, 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, static_cast<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;
}
/** 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(void)
{
#if NUMSCREENS > 2
const char *freename = NULL;
char pathname[MAX_WADPATH];
boolean ret = false;
UINT8 *linear = NULL;
UINT8 *palette;
// Don't take multiple screenshots, obviously
takescreenshot = false;
// how does one take a screenshot without a render system?
if (rendermode == render_none)
return;
if (cv_screenshot_option.value == 0)
strcpy(pathname, usehome ? srb2home : srb2path);
else if (cv_screenshot_option.value == 1)
strcpy(pathname, srb2home);
else if (cv_screenshot_option.value == 2)
strcpy(pathname, srb2path);
else if (cv_screenshot_option.value == 3 && *cv_screenshot_folder.string != '\0')
strcpy(pathname, cv_screenshot_folder.string);
if (cv_screenshot_option.value != 3)
{
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;
if (rendermode == render_soft)
{
// munge planar buffer to linear
linear = screens[2];
I_ReadScreen(linear, 1);
M_CreateScreenShotPalette();
palette = screenshot_palette;
}
#ifdef HWRENDER
else if (rendermode == render_opengl)
{
linear = HWR_GetScreenshot(1);
palette = nullptr;
}
#endif
#ifdef USE_PNG
ret = M_SavePNG(va(pandf,pathname,freename), linear, vid.width, vid.height, palette);
#else
ret = WritePCXfile(va(pandf,pathname,freename), linear, vid.width, vid.height, 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
}
boolean M_ScreenshotResponder(event_t *ev)
{
INT32 ch = -1;
if (dedicated || ev->type != ev_keydown)
return false;
ch = ev->data1;
if (ch >= NUMKEYS && menustack[0]) // If it's not a keyboard key, then don't allow it in the menus!
return false;
if (G_ControlBoundToKey(0, gc_screenshot, ch, true))
M_ScreenShot();
else if (G_ControlBoundToKey(0, gc_recordgif, ch, true))
((moviemode) ? M_StopMovie : M_StartMovie)();
else
return false;
return true;
}
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 "MINIMAP-%d.png", srb2home, mul);
}
else
{
filepath = va("%s" PATHSEP "MINIMAP.png", srb2home);
}
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;
}
/** Allocates and returns a string made out of varargs.
*
* \param format Format string.
* \return Pointer to the resulting string.
*/
char *xva(const char *format, ...)
{
va_list argptr;
char *string;
va_start(argptr, format);
int size = vsnprintf(NULL, 0, format, argptr);
if (size < 0)
I_Error("xva: can't format string");
string = static_cast<char *>(malloc((unsigned)size+1));
if (!string)
I_Error("xva: out of memory");
va_end(argptr);
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!
* <Inuyasha> 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 ((stringToUse[startPos] == ' '
|| stringToUse[startPos] == '\t'
|| stringToUse[startPos] == '\r'
|| stringToUse[startPos] == '\n'
|| stringToUse[startPos] == '\0'
|| stringToUse[startPos] == '=' || stringToUse[startPos] == ';' // UDMF TEXTMAP.
|| inComment != 0)
&& startPos < stringLength)
{
// 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 (stringToUse[endPos] != '"' && endPos < stringLength)
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 ((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 < stringLength)
{
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 (tokenizerStartPos < tokenizerInputLength
&& (tokenizerInput[tokenizerStartPos] == ' '
|| tokenizerInput[tokenizerStartPos] == '\t'
|| tokenizerInput[tokenizerStartPos] == '\r'
|| tokenizerInput[tokenizerStartPos] == '\n'
|| tokenizerInput[tokenizerStartPos] == '\0'
|| tokenizerInput[tokenizerStartPos] == '=' || tokenizerInput[tokenizerStartPos] == ';' // UDMF TEXTMAP.
|| tokenizerInComment != 0))
{
// 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 */
}
}