mirror of
https://github.com/ZDoom/qzdoom-gpl.git
synced 2024-12-11 04:41:06 +00:00
791 lines
15 KiB
C++
791 lines
15 KiB
C++
// Emacs style mode select -*- C++ -*-
|
|
//-----------------------------------------------------------------------------
|
|
//
|
|
// $Id:$
|
|
//
|
|
// Copyright (C) 1993-1996 by id Software, Inc.
|
|
//
|
|
// This source is available for distribution and/or modification
|
|
// only under the terms of the DOOM Source Code License as
|
|
// published by id Software. All rights reserved.
|
|
//
|
|
// The source is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// FITNESS FOR A PARTICULAR PURPOSE. See the DOOM Source Code License
|
|
// for more details.
|
|
//
|
|
//
|
|
// $Log:$
|
|
//
|
|
// DESCRIPTION:
|
|
// Default Config File.
|
|
// Screenshots.
|
|
//
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <fcntl.h>
|
|
#include <stdlib.h>
|
|
#include <errno.h>
|
|
#include <stdlib.h>
|
|
#include <time.h>
|
|
|
|
#include "doomtype.h"
|
|
#include "version.h"
|
|
|
|
#if defined(_WIN32)
|
|
#include <io.h>
|
|
#else
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#include <ctype.h>
|
|
|
|
#include "doomdef.h"
|
|
|
|
#include "m_swap.h"
|
|
#include "m_argv.h"
|
|
|
|
#include "w_wad.h"
|
|
|
|
#include "c_cvars.h"
|
|
#include "c_dispatch.h"
|
|
#include "c_bind.h"
|
|
|
|
#include "i_system.h"
|
|
#include "i_video.h"
|
|
#include "v_video.h"
|
|
#include "r_defs.h"
|
|
|
|
#include "hu_stuff.h"
|
|
|
|
// State.
|
|
#include "doomstat.h"
|
|
|
|
// Data.
|
|
#include "m_misc.h"
|
|
#include "m_png.h"
|
|
|
|
#include "cmdlib.h"
|
|
|
|
#include "g_game.h"
|
|
#include "gi.h"
|
|
|
|
#include "gameconfigfile.h"
|
|
|
|
FGameConfigFile *GameConfig;
|
|
|
|
CVAR(Bool, screenshot_quiet, false, CVAR_ARCHIVE|CVAR_GLOBALCONFIG);
|
|
CVAR(String, screenshot_type, "png", CVAR_ARCHIVE|CVAR_GLOBALCONFIG);
|
|
CVAR(String, screenshot_dir, "", CVAR_ARCHIVE|CVAR_GLOBALCONFIG);
|
|
EXTERN_CVAR(Bool, longsavemessages);
|
|
|
|
static long ParseCommandLine (const char *args, int *argc, char **argv);
|
|
|
|
//
|
|
// M_WriteFile
|
|
//
|
|
#ifndef O_BINARY
|
|
#define O_BINARY 0
|
|
#endif
|
|
|
|
bool M_WriteFile (char const *name, void *source, int length)
|
|
{
|
|
int handle;
|
|
int count;
|
|
|
|
handle = open ( name, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, 0666);
|
|
|
|
if (handle == -1)
|
|
return false;
|
|
|
|
count = write (handle, source, length);
|
|
close (handle);
|
|
|
|
if (count < length)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
//
|
|
// M_ReadFile
|
|
//
|
|
int M_ReadFile (char const *name, BYTE **buffer)
|
|
{
|
|
int handle, count, length;
|
|
struct stat fileinfo;
|
|
BYTE *buf;
|
|
|
|
handle = open (name, O_RDONLY | O_BINARY, 0666);
|
|
if (handle == -1)
|
|
I_Error ("Couldn't read file %s", name);
|
|
// [BL] Use stat instead of fstat for v140_xp hack
|
|
if (stat (name,&fileinfo) == -1)
|
|
I_Error ("Couldn't read file %s", name);
|
|
length = fileinfo.st_size;
|
|
buf = new BYTE[length];
|
|
count = read (handle, buf, length);
|
|
close (handle);
|
|
|
|
if (count < length)
|
|
I_Error ("Couldn't read file %s", name);
|
|
|
|
*buffer = buf;
|
|
return length;
|
|
}
|
|
|
|
//
|
|
// M_ReadFile (same as above but use malloc instead of new to allocate the buffer.)
|
|
//
|
|
int M_ReadFileMalloc (char const *name, BYTE **buffer)
|
|
{
|
|
int handle, count, length;
|
|
struct stat fileinfo;
|
|
BYTE *buf;
|
|
|
|
handle = open (name, O_RDONLY | O_BINARY, 0666);
|
|
if (handle == -1)
|
|
I_Error ("Couldn't read file %s", name);
|
|
// [BL] Use stat instead of fstat for v140_xp hack
|
|
if (stat (name,&fileinfo) == -1)
|
|
I_Error ("Couldn't read file %s", name);
|
|
length = fileinfo.st_size;
|
|
buf = (BYTE*)M_Malloc(length);
|
|
count = read (handle, buf, length);
|
|
close (handle);
|
|
|
|
if (count < length)
|
|
I_Error ("Couldn't read file %s", name);
|
|
|
|
*buffer = buf;
|
|
return length;
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
//
|
|
// PROC M_FindResponseFile
|
|
//
|
|
//---------------------------------------------------------------------------
|
|
|
|
void M_FindResponseFile (void)
|
|
{
|
|
const int limit = 100; // avoid infinite recursion
|
|
int added_stuff = 0;
|
|
int i = 1;
|
|
|
|
while (i < Args->NumArgs())
|
|
{
|
|
if (Args->GetArg(i)[0] != '@')
|
|
{
|
|
i++;
|
|
}
|
|
else
|
|
{
|
|
char **argv;
|
|
char *file = NULL;
|
|
int argc = 0;
|
|
FILE *handle;
|
|
int size;
|
|
long argsize = 0;
|
|
int index;
|
|
|
|
// Any more response files after the limit will be removed from the
|
|
// command line.
|
|
if (added_stuff < limit)
|
|
{
|
|
// READ THE RESPONSE FILE INTO MEMORY
|
|
handle = fopen (Args->GetArg(i) + 1,"rb");
|
|
if (!handle)
|
|
{ // [RH] Make this a warning, not an error.
|
|
Printf ("No such response file (%s)!\n", Args->GetArg(i) + 1);
|
|
}
|
|
else
|
|
{
|
|
Printf ("Found response file %s!\n", Args->GetArg(i) + 1);
|
|
fseek (handle, 0, SEEK_END);
|
|
size = ftell (handle);
|
|
fseek (handle, 0, SEEK_SET);
|
|
file = new char[size+1];
|
|
fread (file, size, 1, handle);
|
|
file[size] = 0;
|
|
fclose (handle);
|
|
|
|
argsize = ParseCommandLine (file, &argc, NULL);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Printf ("Ignored response file %s.\n", Args->GetArg(i) + 1);
|
|
}
|
|
|
|
if (argc != 0)
|
|
{
|
|
argv = (char **)M_Malloc (argc*sizeof(char *) + argsize);
|
|
argv[0] = (char *)argv + argc*sizeof(char *);
|
|
ParseCommandLine (file, NULL, argv);
|
|
|
|
// Create a new argument vector
|
|
DArgs *newargs = new DArgs;
|
|
|
|
// Copy parameters before response file.
|
|
for (index = 0; index < i; ++index)
|
|
newargs->AppendArg(Args->GetArg(index));
|
|
|
|
// Copy parameters from response file.
|
|
for (index = 0; index < argc; ++index)
|
|
newargs->AppendArg(argv[index]);
|
|
|
|
// Copy parameters after response file.
|
|
for (index = i + 1; index < Args->NumArgs(); ++index)
|
|
newargs->AppendArg(Args->GetArg(index));
|
|
|
|
// Use the new argument vector as the global Args object.
|
|
Args = newargs;
|
|
if (++added_stuff == limit)
|
|
{
|
|
Printf("Response file limit of %d hit.\n", limit);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Remove the response file from the Args object
|
|
Args->RemoveArg(i);
|
|
}
|
|
if (file != NULL)
|
|
{
|
|
delete[] file;
|
|
}
|
|
}
|
|
}
|
|
if (added_stuff > 0)
|
|
{
|
|
// DISPLAY ARGS
|
|
Printf ("Added %d response file%s, now have %d command-line args:\n",
|
|
added_stuff, added_stuff > 1 ? "s" : "", Args->NumArgs ());
|
|
for (int k = 1; k < Args->NumArgs (); k++)
|
|
Printf ("%s\n", Args->GetArg (k));
|
|
}
|
|
}
|
|
|
|
// ParseCommandLine
|
|
//
|
|
// This is just like the version in c_dispatch.cpp, except it does not
|
|
// do cvar expansion.
|
|
|
|
static long ParseCommandLine (const char *args, int *argc, char **argv)
|
|
{
|
|
int count;
|
|
char *buffplace;
|
|
|
|
count = 0;
|
|
buffplace = NULL;
|
|
if (argv != NULL)
|
|
{
|
|
buffplace = argv[0];
|
|
}
|
|
|
|
for (;;)
|
|
{
|
|
while (*args <= ' ' && *args)
|
|
{ // skip white space
|
|
args++;
|
|
}
|
|
if (*args == 0)
|
|
{
|
|
break;
|
|
}
|
|
else if (*args == '\"')
|
|
{ // read quoted string
|
|
char stuff;
|
|
if (argv != NULL)
|
|
{
|
|
argv[count] = buffplace;
|
|
}
|
|
count++;
|
|
args++;
|
|
do
|
|
{
|
|
stuff = *args++;
|
|
if (stuff == '\\' && *args == '\"')
|
|
{
|
|
stuff = '\"', args++;
|
|
}
|
|
else if (stuff == '\"')
|
|
{
|
|
stuff = 0;
|
|
}
|
|
else if (stuff == 0)
|
|
{
|
|
args--;
|
|
}
|
|
if (argv != NULL)
|
|
{
|
|
*buffplace = stuff;
|
|
}
|
|
buffplace++;
|
|
} while (stuff);
|
|
}
|
|
else
|
|
{ // read unquoted string
|
|
const char *start = args++, *end;
|
|
|
|
while (*args && *args > ' ' && *args != '\"')
|
|
args++;
|
|
end = args;
|
|
if (argv != NULL)
|
|
{
|
|
argv[count] = buffplace;
|
|
while (start < end)
|
|
*buffplace++ = *start++;
|
|
*buffplace++ = 0;
|
|
}
|
|
else
|
|
{
|
|
buffplace += end - start + 1;
|
|
}
|
|
count++;
|
|
}
|
|
}
|
|
if (argc != NULL)
|
|
{
|
|
*argc = count;
|
|
}
|
|
return (long)(buffplace - (char *)0);
|
|
}
|
|
|
|
|
|
//
|
|
// M_SaveDefaults
|
|
//
|
|
|
|
bool M_SaveDefaults (const char *filename)
|
|
{
|
|
FString oldpath;
|
|
bool success;
|
|
|
|
if (filename != NULL)
|
|
{
|
|
oldpath = GameConfig->GetPathName();
|
|
GameConfig->ChangePathName (filename);
|
|
}
|
|
GameConfig->ArchiveGlobalData ();
|
|
if (gameinfo.ConfigName.IsNotEmpty())
|
|
{
|
|
GameConfig->ArchiveGameData (gameinfo.ConfigName);
|
|
}
|
|
success = GameConfig->WriteConfigFile ();
|
|
if (filename != NULL)
|
|
{
|
|
GameConfig->ChangePathName (filename);
|
|
}
|
|
return success;
|
|
}
|
|
|
|
void M_SaveDefaultsFinal ()
|
|
{
|
|
while (!M_SaveDefaults (NULL) && I_WriteIniFailed ())
|
|
{
|
|
/* Loop until the config saves or I_WriteIniFailed() returns false */
|
|
}
|
|
delete GameConfig;
|
|
GameConfig = NULL;
|
|
}
|
|
|
|
CCMD (writeini)
|
|
{
|
|
const char *filename = (argv.argc() == 1) ? NULL : argv[1];
|
|
if (!M_SaveDefaults (filename))
|
|
{
|
|
Printf ("Writing config failed: %s\n", strerror(errno));
|
|
}
|
|
else
|
|
{
|
|
Printf ("Config saved.\n");
|
|
}
|
|
}
|
|
|
|
//
|
|
// M_LoadDefaults
|
|
//
|
|
|
|
void M_LoadDefaults ()
|
|
{
|
|
GameConfig = new FGameConfigFile;
|
|
GameConfig->DoGlobalSetup ();
|
|
atterm (M_SaveDefaultsFinal);
|
|
}
|
|
|
|
|
|
//
|
|
// SCREEN SHOTS
|
|
//
|
|
|
|
|
|
struct pcx_t
|
|
{
|
|
char manufacturer;
|
|
char version;
|
|
char encoding;
|
|
char bits_per_pixel;
|
|
|
|
unsigned short xmin;
|
|
unsigned short ymin;
|
|
unsigned short xmax;
|
|
unsigned short ymax;
|
|
|
|
unsigned short hdpi;
|
|
unsigned short vdpi;
|
|
|
|
unsigned char palette[48];
|
|
|
|
char reserved;
|
|
char color_planes;
|
|
unsigned short bytes_per_line;
|
|
unsigned short palette_type;
|
|
|
|
char filler[58];
|
|
};
|
|
|
|
|
|
inline void putc(unsigned char chr, FileWriter *file)
|
|
{
|
|
file->Write(&chr, 1);
|
|
}
|
|
|
|
//
|
|
// WritePCXfile
|
|
//
|
|
void WritePCXfile (FileWriter *file, const BYTE *buffer, const PalEntry *palette,
|
|
ESSType color_type, int width, int height, int pitch)
|
|
{
|
|
BYTE temprow[MAXWIDTH * 3];
|
|
const BYTE *data;
|
|
int x, y;
|
|
int runlen;
|
|
int bytes_per_row_minus_one;
|
|
BYTE color;
|
|
pcx_t pcx;
|
|
|
|
pcx.manufacturer = 10; // PCX id
|
|
pcx.version = 5; // 256 (or more) colors
|
|
pcx.encoding = 1;
|
|
pcx.bits_per_pixel = 8; // 256 (or more) colors
|
|
pcx.xmin = 0;
|
|
pcx.ymin = 0;
|
|
pcx.xmax = LittleShort((unsigned short)(width-1));
|
|
pcx.ymax = LittleShort((unsigned short)(height-1));
|
|
pcx.hdpi = LittleShort((unsigned short)75);
|
|
pcx.vdpi = LittleShort((unsigned short)75);
|
|
memset (pcx.palette, 0, sizeof(pcx.palette));
|
|
pcx.reserved = 0;
|
|
pcx.color_planes = (color_type == SS_PAL) ? 1 : 3; // chunky image
|
|
pcx.bytes_per_line = width + (width & 1);
|
|
pcx.palette_type = 1; // not a grey scale
|
|
memset (pcx.filler, 0, sizeof(pcx.filler));
|
|
|
|
file->Write(&pcx, 128);
|
|
|
|
bytes_per_row_minus_one = ((color_type == SS_PAL) ? width : width * 3) - 1;
|
|
|
|
// pack the image
|
|
for (y = height; y > 0; y--)
|
|
{
|
|
switch (color_type)
|
|
{
|
|
case SS_PAL:
|
|
data = buffer;
|
|
break;
|
|
|
|
case SS_RGB:
|
|
// Unpack RGB into separate planes.
|
|
for (int i = 0; i < width; ++i)
|
|
{
|
|
temprow[i ] = buffer[i*3];
|
|
temprow[i + width ] = buffer[i*3 + 1];
|
|
temprow[i + width * 2] = buffer[i*3 + 2];
|
|
}
|
|
data = temprow;
|
|
break;
|
|
|
|
case SS_BGRA:
|
|
// Unpack RGB into separate planes, discarding A.
|
|
for (int i = 0; i < width; ++i)
|
|
{
|
|
temprow[i ] = buffer[i*4 + 2];
|
|
temprow[i + width ] = buffer[i*4 + 1];
|
|
temprow[i + width * 2] = buffer[i*4];
|
|
}
|
|
data = temprow;
|
|
break;
|
|
|
|
default:
|
|
// Should never happen.
|
|
return;
|
|
}
|
|
buffer += pitch;
|
|
|
|
color = *data++;
|
|
runlen = 1;
|
|
|
|
for (x = bytes_per_row_minus_one; x > 0; x--)
|
|
{
|
|
if (*data == color)
|
|
{
|
|
runlen++;
|
|
}
|
|
else
|
|
{
|
|
if (runlen > 1 || color >= 0xc0)
|
|
{
|
|
while (runlen > 63)
|
|
{
|
|
putc (0xff, file);
|
|
putc (color, file);
|
|
runlen -= 63;
|
|
}
|
|
if (runlen > 0)
|
|
{
|
|
putc (0xc0 + runlen, file);
|
|
}
|
|
}
|
|
if (runlen > 0)
|
|
{
|
|
putc (color, file);
|
|
}
|
|
runlen = 1;
|
|
color = *data;
|
|
}
|
|
data++;
|
|
}
|
|
|
|
if (runlen > 1 || color >= 0xc0)
|
|
{
|
|
while (runlen > 63)
|
|
{
|
|
putc (0xff, file);
|
|
putc (color, file);
|
|
runlen -= 63;
|
|
}
|
|
if (runlen > 0)
|
|
{
|
|
putc (0xc0 + runlen, file);
|
|
}
|
|
}
|
|
if (runlen > 0)
|
|
{
|
|
putc (color, file);
|
|
}
|
|
|
|
if (width & 1)
|
|
putc (0, file);
|
|
}
|
|
|
|
// write the palette
|
|
if (color_type == SS_PAL)
|
|
{
|
|
putc (12, file); // palette ID byte
|
|
for (x = 0; x < 256; x++, palette++)
|
|
{
|
|
putc (palette->r, file);
|
|
putc (palette->g, file);
|
|
putc (palette->b, file);
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// WritePNGfile
|
|
//
|
|
void WritePNGfile (FileWriter *file, const BYTE *buffer, const PalEntry *palette,
|
|
ESSType color_type, int width, int height, int pitch)
|
|
{
|
|
char software[100];
|
|
mysnprintf(software, countof(software), GAMENAME " %s", GetVersionString());
|
|
if (!M_CreatePNG (file, buffer, palette, color_type, width, height, pitch) ||
|
|
!M_AppendPNGText (file, "Software", software) ||
|
|
!M_FinishPNG (file))
|
|
{
|
|
Printf ("Could not create screenshot.\n");
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
// M_ScreenShot
|
|
//
|
|
static bool FindFreeName (FString &fullname, const char *extension)
|
|
{
|
|
FString lbmname;
|
|
int i;
|
|
|
|
for (i = 0; i <= 9999; i++)
|
|
{
|
|
const char *gamename = gameinfo.ConfigName;
|
|
|
|
time_t now;
|
|
tm *tm;
|
|
|
|
time(&now);
|
|
tm = localtime(&now);
|
|
|
|
if (tm == NULL)
|
|
{
|
|
lbmname.Format ("%sScreenshot_%s_%04d.%s", fullname.GetChars(), gamename, i, extension);
|
|
}
|
|
else if (i == 0)
|
|
{
|
|
lbmname.Format ("%sScreenshot_%s_%04d%02d%02d_%02d%02d%02d.%s", fullname.GetChars(), gamename,
|
|
tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday,
|
|
tm->tm_hour, tm->tm_min, tm->tm_sec,
|
|
extension);
|
|
}
|
|
else
|
|
{
|
|
lbmname.Format ("%sScreenshot_%s_%04d%02d%02d_%02d%02d%02d_%02d.%s", fullname.GetChars(), gamename,
|
|
tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday,
|
|
tm->tm_hour, tm->tm_min, tm->tm_sec,
|
|
i, extension);
|
|
}
|
|
|
|
if (!FileExists (lbmname.GetChars()))
|
|
{
|
|
fullname = lbmname;
|
|
return true; // file doesn't exist
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void M_ScreenShot (const char *filename)
|
|
{
|
|
FileWriter *file;
|
|
FString autoname;
|
|
bool writepcx = (stricmp (screenshot_type, "pcx") == 0); // PNG is the default
|
|
|
|
// find a file name to save it to
|
|
if (filename == NULL || filename[0] == '\0')
|
|
{
|
|
size_t dirlen;
|
|
autoname = Args->CheckValue("-shotdir");
|
|
if (autoname.IsEmpty())
|
|
{
|
|
autoname = screenshot_dir;
|
|
}
|
|
dirlen = autoname.Len();
|
|
if (dirlen == 0)
|
|
{
|
|
autoname = M_GetScreenshotsPath();
|
|
dirlen = autoname.Len();
|
|
}
|
|
if (dirlen > 0)
|
|
{
|
|
if (autoname[dirlen-1] != '/' && autoname[dirlen-1] != '\\')
|
|
{
|
|
autoname += '/';
|
|
}
|
|
}
|
|
autoname = NicePath(autoname);
|
|
CreatePath(autoname);
|
|
if (!FindFreeName (autoname, writepcx ? "pcx" : "png"))
|
|
{
|
|
Printf ("M_ScreenShot: Delete some screenshots\n");
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
autoname = filename;
|
|
DefaultExtension (autoname, writepcx ? ".pcx" : ".png");
|
|
}
|
|
|
|
// save the screenshot
|
|
const BYTE *buffer;
|
|
int pitch;
|
|
ESSType color_type;
|
|
|
|
screen->GetScreenshotBuffer(buffer, pitch, color_type);
|
|
if (buffer != NULL)
|
|
{
|
|
PalEntry palette[256];
|
|
|
|
if (color_type == SS_PAL)
|
|
{
|
|
screen->GetFlashedPalette(palette);
|
|
}
|
|
file = FileWriter::Open(autoname);
|
|
if (file == NULL)
|
|
{
|
|
Printf ("Could not open %s\n", autoname.GetChars());
|
|
screen->ReleaseScreenshotBuffer();
|
|
return;
|
|
}
|
|
if (writepcx)
|
|
{
|
|
WritePCXfile(file, buffer, palette, color_type,
|
|
screen->GetWidth(), screen->GetHeight(), pitch);
|
|
}
|
|
else
|
|
{
|
|
WritePNGfile(file, buffer, palette, color_type,
|
|
screen->GetWidth(), screen->GetHeight(), pitch);
|
|
}
|
|
delete file;
|
|
screen->ReleaseScreenshotBuffer();
|
|
|
|
if (!screenshot_quiet)
|
|
{
|
|
int slash = -1;
|
|
if (!longsavemessages) slash = autoname.LastIndexOfAny(":/\\");
|
|
Printf ("Captured %s\n", autoname.GetChars()+slash+1);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!screenshot_quiet)
|
|
{
|
|
Printf ("Could not create screenshot.\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
CCMD (screenshot)
|
|
{
|
|
if (argv.argc() == 1)
|
|
G_ScreenShot (NULL);
|
|
else
|
|
G_ScreenShot (argv[1]);
|
|
}
|
|
|
|
//
|
|
// M_ZlibError
|
|
//
|
|
FString M_ZLibError(int zerr)
|
|
{
|
|
if (zerr >= 0)
|
|
{
|
|
return "OK";
|
|
}
|
|
else if (zerr < -6)
|
|
{
|
|
FString out;
|
|
out.Format("%d", zerr);
|
|
return out;
|
|
}
|
|
else
|
|
{
|
|
static const char *errs[6] =
|
|
{
|
|
"Errno",
|
|
"Stream Error",
|
|
"Data Error",
|
|
"Memory Error",
|
|
"Buffer Error",
|
|
"Version Error"
|
|
};
|
|
return errs[-zerr - 1];
|
|
}
|
|
}
|