diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e34d8ea3dc..672267e541 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -728,8 +728,10 @@ add_executable( zdoom WIN32 s_sound.cpp sc_man.cpp st_stuff.cpp + statistics.cpp stats.cpp stringtable.cpp + strnatcmp.c tables.cpp teaminfo.cpp tempfiles.cpp diff --git a/src/d_main.cpp b/src/d_main.cpp index 9dc18f473d..cbed77f800 100644 --- a/src/d_main.cpp +++ b/src/d_main.cpp @@ -116,6 +116,7 @@ void DrawHUD(); // EXTERNAL FUNCTION PROTOTYPES -------------------------------------------- +extern void ReadStatistics(); extern void M_RestoreMode (); extern void M_SetDefaultMode (); extern void R_ExecuteSetViewSize (); @@ -2073,6 +2074,7 @@ void D_DoomMain (void) // [RH] Parse through all loaded mapinfo lumps Printf ("G_ParseMapInfo: Load map definitions.\n"); G_ParseMapInfo (iwad_info->MapInfo); + ReadStatistics(); // [RH] Parse any SNDINFO lumps Printf ("S_InitData: Load sound definitions.\n"); diff --git a/src/g_game.cpp b/src/g_game.cpp index caf43cd6f0..35e740f400 100644 --- a/src/g_game.cpp +++ b/src/g_game.cpp @@ -104,6 +104,9 @@ void G_DoWorldDone (void); void G_DoSaveGame (bool okForQuicksave, FString filename, const char *description); void G_DoAutoSave (); +void STAT_Write(FILE *file); +void STAT_Read(PNGHandle *png); + FIntCVar gameskill ("skill", 2, CVAR_SERVERINFO|CVAR_LATCH); CVAR (Int, deathmatch, 0, CVAR_SERVERINFO|CVAR_LATCH); CVAR (Bool, chasedemo, false, 0); @@ -1791,6 +1794,7 @@ void G_DoLoadGame () } G_ReadSnapshots (png); + STAT_Read(png); FRandom::StaticReadRNGState (png); P_ReadACSDefereds (png); @@ -2052,6 +2056,7 @@ void G_DoSaveGame (bool okForQuicksave, FString filename, const char *descriptio } G_WriteSnapshots (stdfile); + STAT_Write(stdfile); FRandom::StaticWriteRNGState (stdfile); P_WriteACSDefereds (stdfile); diff --git a/src/g_level.cpp b/src/g_level.cpp index 1c125324ae..610a1108a7 100644 --- a/src/g_level.cpp +++ b/src/g_level.cpp @@ -83,18 +83,9 @@ #include "g_hub.h" +void STAT_StartNewGame(const char *lev); +void STAT_ChangeLevel(const char *newl); -#ifndef STAT -#define STAT_NEW(map) -#define STAT_END(newl) -#define STAT_READ(png) -#define STAT_WRITE(f) -#else -void STAT_NEW(const char *lev); -void STAT_END(const char *newl); -void STAT_READ(PNGHandle *png); -void STAT_WRITE(FILE *f); -#endif EXTERN_CVAR (Float, sv_gravity) EXTERN_CVAR (Float, sv_aircontrol) @@ -502,7 +493,7 @@ void G_InitNew (const char *mapname, bool bTitleLevel) for (i = 0; i < MAXPLAYERS; i++) players[i].playerstate = PST_ENTER; // [BC] - STAT_NEW(mapname); + STAT_StartNewGame(mapname); } usergame = !bTitleLevel; // will be set false if a demo @@ -614,7 +605,7 @@ void G_ChangeLevel(const char *levelname, int position, int flags, int nextSkill FBehavior::StaticStartTypedScripts (SCRIPT_Unloading, NULL, false, 0, true); unloading = false; - STAT_END(nextlevel); + STAT_ChangeLevel(nextlevel); if (thiscluster && (thiscluster->flags & CLUSTER_HUB)) { @@ -1652,7 +1643,6 @@ void G_WriteSnapshots (FILE *file) { unsigned int i; - STAT_WRITE(file); for (i = 0; i < wadlevelinfos.Size(); i++) { if (wadlevelinfos[i].snapshot) @@ -1803,7 +1793,6 @@ void G_ReadSnapshots (PNGHandle *png) arc << pnum; } } - STAT_READ(png); png->File->ResetFilePtr(); } diff --git a/src/g_level.h b/src/g_level.h index cf7fd2d482..f04e222d5f 100644 --- a/src/g_level.h +++ b/src/g_level.h @@ -202,6 +202,9 @@ enum ELevelFlags LEVEL2_POLYGRIND = 0x02000000, // Polyobjects grind corpses to gibs. LEVEL2_RESETINVENTORY = 0x04000000, // Resets player inventory when starting this level (unless in a hub) LEVEL2_RESETHEALTH = 0x08000000, // Resets player health when starting this level (unless in a hub) + + LEVEL2_NOSTATISTICS = 0x10000000, // This level should not have statistics collected + LEVEL2_ENDGAME = 0x20000000, // This is an epilogue level that cannot be quit. }; diff --git a/src/g_mapinfo.cpp b/src/g_mapinfo.cpp index 6b3faa78a5..42beba8732 100644 --- a/src/g_mapinfo.cpp +++ b/src/g_mapinfo.cpp @@ -1377,6 +1377,8 @@ MapFlagHandlers[] = { "no_grinding_polyobj", MITYPE_CLRFLAG2, LEVEL2_POLYGRIND, 0 }, { "resetinventory", MITYPE_SETFLAG2, LEVEL2_RESETINVENTORY, 0 }, { "resethealth", MITYPE_SETFLAG2, LEVEL2_RESETHEALTH, 0 }, + { "endofgame", MITYPE_SETFLAG2, LEVEL2_ENDGAME, 0 }, + { "nostatistics", MITYPE_SETFLAG2, LEVEL2_NOSTATISTICS, 0 }, { "unfreezesingleplayerconversations",MITYPE_SETFLAG2, LEVEL2_CONV_SINGLE_UNFREEZE, 0 }, { "nobotnodes", MITYPE_IGNORE, 0, 0 }, // Skulltag option: nobotnodes { "compat_shorttex", MITYPE_COMPATFLAG, COMPATF_SHORTTEX}, diff --git a/src/statistics.cpp b/src/statistics.cpp new file mode 100644 index 0000000000..a5c85bcd5c --- /dev/null +++ b/src/statistics.cpp @@ -0,0 +1,606 @@ +/* +** +** statistics.cpp +** Save game statistics to a file +** +**--------------------------------------------------------------------------- +** Copyright 2010 Christoph Oelckers +** All rights reserved. +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions +** are met: +** +** 1. Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** 2. Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in the +** documentation and/or other materials provided with the distribution. +** 3. The name of the author may not be used to endorse or promote products +** derived from this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**--------------------------------------------------------------------------- +** +*/ + +#include +#include + +#include "strnatcmp.h" +#include "gi.h" +#include "g_level.h" +#include "gstrings.h" +#include "doomstat.h" +#include "configfile.h" +#include "c_dispatch.h" +#include "c_console.h" +#include "d_gui.h" +#include "d_dehacked.h" +#include "g_game.h" +#include "m_png.h" +#include "m_misc.h" +#include "doomerrors.h" +#include "w_wad.h" +#include "hu_stuff.h" +#include "p_local.h" +#include "m_png.h" +#include "p_setup.h" +#include "s_Sound.h" +#include "wi_stuff.h" +#include "sc_man.h" +#include "cmdlib.h" +#include "p_terrain.h" +#include "decallib.h" +#include "a_doomglobal.h" +#include "autosegs.h" +#include "i_cd.h" +#include "stats.h" +#include "a_sharedglobal.h" +#include "v_text.h" +#include "r_sky.h" +#include "p_lnspec.h" +#include "m_crc32.h" + +CVAR(Int, savestatistics, 0, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) +CVAR(String, statfile, "zdoomstat.txt", CVAR_ARCHIVE|CVAR_GLOBALCONFIG) + +//========================================================================== +// +// Global statistics data +// +//========================================================================== + +// This struct is used to track statistics data in game +struct OneLevel +{ + int totalkills, killcount; + int totalsecrets, secretcount; + int leveltime; + char levelname[9]; +}; + +// Current game's statistics +static TArray LevelData; +static FEpisode *StartEpisode; + +// The statistics for one level +struct FLevelStatistics +{ + char info[30]; + short skill; + short playerclass; + char name[12]; + int timeneeded; +}; + +// Statistics for one episode playthrough +struct FSessionStatistics : public FLevelStatistics +{ + TArray levelstats; +}; + +// Collected statistics for one episode +struct FStatistics +{ + TArray stats; + FString epi_name; + FString epi_header; +}; + +// All statistics ever collected +static TArray EpisodeStatistics; + +extern TArray wadlevelinfos; + +//========================================================================== +// +// Initializes statistics data from external file +// +//========================================================================== + +static void ParseStatistics(const char *fn, TArray &statlist) +{ + try + { + FScanner sc; + sc.OpenFile(fn); + + while (sc.GetString()) + { + FStatistics &ep_entry = statlist[statlist.Reserve(1)]; + + ep_entry.epi_header = sc.String; + sc.MustGetString(); + ep_entry.epi_name = sc.String; + + sc.MustGetStringName("{"); + while (!sc.CheckString("}")) + { + FSessionStatistics &session = ep_entry.stats[ep_entry.stats.Reserve(1)]; + + sc.MustGetString(); + sc.MustGetString(); + strncpy(session.name, sc.String, 12); + sc.MustGetString(); + strncpy(session.info, sc.String, 30); + + int h,m,s; + sc.MustGetString(); + sscanf(sc.String, "%d:%d:%d", &h, &m, &s); + session.timeneeded= ((((h*60)+m)*60)+s)*TICRATE; + + sc.MustGetNumber(); + session.skill=sc.Number; + if (sc.CheckString("{")) + { + while (!sc.CheckString("}")) + { + FLevelStatistics &lstats = session.levelstats[session.levelstats.Reserve(1)]; + + sc.MustGetString(); + strncpy(lstats.name, sc.String, 12); + sc.MustGetString(); + strncpy(lstats.info, sc.String, 30); + + int h,m,s; + sc.MustGetString(); + sscanf(sc.String, "%d:%d:%d", &h, &m, &s); + lstats.timeneeded= ((((h*60)+m)*60)+s)*TICRATE; + + lstats.skill = 0; + } + } + } + } + } + catch(CRecoverableError &) + { + } +} + + +// ==================================================================== +// +// Reads the statistics file +// +// ==================================================================== + +void ReadStatistics() +{ + ParseStatistics(statfile, EpisodeStatistics); +} + +// ==================================================================== +// +// Saves the statistics file +// Sorting helpers. +// +// ==================================================================== + +int STACK_ARGS compare_episode_names(const void *a, const void *b) +{ + FStatistics *A = (FStatistics*)a; + FStatistics *B = (FStatistics*)b; + + return strnatcasecmp(A->epi_header, B->epi_header); +} + +int STACK_ARGS compare_level_names(const void *a, const void *b) +{ + FLevelStatistics *A = (FLevelStatistics*)a; + FLevelStatistics *B = (FLevelStatistics*)b; + + return strnatcasecmp(A->name, B->name); +} + +int STACK_ARGS compare_dates(const void *a, const void *b) +{ + FLevelStatistics *A = (FLevelStatistics*)a; + FLevelStatistics *B = (FLevelStatistics*)b; + char *p; + + int aday = strtol(A->name, &p, 10); + int amonth = strtol(p+1, &p, 10); + int ayear = strtol(p+1, &p, 10); + int av = aday + 100 * amonth + 2000*ayear; + + int bday = strtol(B->name, &p, 10); + int bmonth = strtol(p+1, &p, 10); + int byear = strtol(p+1, &p, 10); + int bv = bday + 100 * bmonth + 2000*byear; + + return av-bv; +} + + +// ==================================================================== +// +// Main save routine +// +// ==================================================================== + +inline int hours(int v) { return v / (60*60*TICRATE); } +inline int minutes(int v) { return (v % (60*60*TICRATE)) / (60*TICRATE); } +inline int seconds(int v) { return (v % (60*TICRATE))/TICRATE; } + +static void SaveStatistics(const char *fn, TArray &statlist) +{ + unsigned int j; + + FILE * f = fopen(fn, "wt"); + if (f==NULL) return; + + qsort(&statlist[0], statlist.Size(), sizeof(statlist[0]), compare_episode_names); + for(unsigned i=0;iinfo[0]>0) + { + fprintf(f,"\t%2i. %10s \"%-22s\" %02d:%02d:%02d %i\n", j+1, sst->name, sst->info, + hours(sst->timeneeded), minutes(sst->timeneeded), seconds(sst->timeneeded), sst->skill); + + TArray &ls = sst->levelstats; + if (ls.Size() > 0) + { + fprintf(f,"\t{\n"); + + qsort(&ls[0], ls.Size(), sizeof(ls[0]), compare_level_names); + + for(unsigned k=0;k &statlist, const char *section, const char *fullname) +{ + for(unsigned int i=0;iepi_header = section; + stats->epi_name = fullname; + return stats; +} + +// ==================================================================== +// +// Adds a statistics entry +// +// ==================================================================== +static FSessionStatistics *StatisticsEntry(FStatistics *stats, const char *text, int playtime) +{ + FSessionStatistics s; + time_t clock; + struct tm *lt; + + time (&clock); + lt = localtime (&clock); + + if (lt != NULL) + mysnprintf(s.name, countof(s.name), "%02d.%02d.%04d",lt->tm_mday, lt->tm_mon+1, lt->tm_year+1900); + else + strcpy(s.name,"00.00.0000"); + + s.skill=G_SkillProperty(SKILLP_ACSReturn); + strcpy(s.info, text); + s.timeneeded=playtime; + + stats->stats.Push(s); + return &stats->stats[stats->stats.Size()-1]; +} + +// ==================================================================== +// +// Adds a statistics entry +// +// ==================================================================== +static void LevelStatEntry(FSessionStatistics *es, const char *level, const char *text, int playtime) +{ + FLevelStatistics s; + time_t clock; + struct tm *lt; + + time (&clock); + lt = localtime (&clock); + + strcpy(s.name, level); + strcpy(s.info, text); + s.timeneeded=playtime; + es->levelstats.Push(s); +} + + + +//========================================================================== +// +// STAT_StartNewGame: called when a new game starts. Sets the current episode +// +//========================================================================== + +void STAT_StartNewGame(const char *mapname) +{ + LevelData.Clear(); + if (!deathmatch && !multiplayer) + { + for(unsigned int j=0;j it; + AActor *ac; + int mc = 0; + + while ((ac = it.Next())) + { + if ((ac->flags & MF_COUNTKILL) && ac->health > 0) mc++; + } + if (mc == 0) LevelData[i].killcount = LevelData[i].totalkills; + } +} + +//========================================================================== +// +// STAT_ChangeLevel: called when the level changes or the current statistics are +// requested +// +//========================================================================== + +void STAT_ChangeLevel(const char *newl) +{ + // record the current level's stats. + StoreLevelStats(); + + level_info_t *thisinfo = level.info; + level_info_t *nextinfo = NULL; + + if (strncmp(newl, "enDSeQ", 6)) + { + level_info_t *l = FindLevelInfo (newl); + nextinfo = l->CheckLevelRedirect (); + if (nextinfo == NULL) nextinfo = l; + + } + + if (savestatistics == 1) + { + if ((nextinfo == NULL || (nextinfo->flags2 & LEVEL2_ENDGAME)) && StartEpisode != NULL) + { + // we reached the end of this episode + int wad = 0; + MapData * map = P_OpenMapData(StartEpisode->mEpisodeMap); + if (map != NULL) + { + wad = Wads.GetLumpFile(map->lumpnum); + delete map; + } + const char * name = Wads.GetWadName(wad); + FString section = ExtractFileBase(name) + "." + StartEpisode->mEpisodeMap; + section.ToUpper(); + + const char *ep_name = StartEpisode->mEpisodeName; + if (*ep_name == '$') ep_name = GStrings[ep_name+1]; + FStatistics *sl = GetStatisticsList(EpisodeStatistics, section, ep_name); + + int statvals[4] = {0,0,0,0}; + FString infostring; + int validlevels = LevelData.Size(); + for(unsigned i = 0; i < LevelData.Size(); i++) + { + statvals[0] += LevelData[i].killcount; + statvals[1] += LevelData[i].totalkills; + statvals[2] += LevelData[i].secretcount; + statvals[3] += LevelData[i].totalsecrets; + } + + infostring.Format("%4d/%4d, %3d/%3d, %2d", statvals[0], statvals[1], statvals[2], statvals[3], validlevels); + FSessionStatistics *es = StatisticsEntry(sl, infostring, level.totaltime); + + for(unsigned i = 0; i < LevelData.Size(); i++) + { + FString lsection = LevelData[i].levelname; + lsection.ToUpper(); + infostring.Format("%4d/%4d, %3d/%3d", + LevelData[i].killcount, LevelData[i].totalkills, LevelData[i].secretcount, LevelData[i].totalsecrets); + + LevelStatEntry(es, lsection, infostring, LevelData[i].leveltime); + } + SaveStatistics(statfile, EpisodeStatistics); + } + } + else if (savestatistics == 2) // todo: handle single level statistics. + { + } +} + + +//========================================================================== +// +// saves statistics info to savegames +// +//========================================================================== + +static void SerializeStatistics(FArchive &arc) +{ + FString startlevel; + int i = LevelData.Size(); + + arc << i; + + if (arc.IsLoading()) + { + arc << startlevel; + StartEpisode = NULL; + for(unsigned int j=0;jmEpisodeMap; + arc << startlevel; + } + for(int j = 0; j < i; j++) + { + OneLevel &l = LevelData[j]; + + arc << l.totalkills + << l.killcount + << l.totalsecrets + << l.secretcount + << l.leveltime; + + if (arc.IsStoring()) arc.WriteName(l.levelname); + else strcpy(l.levelname, arc.ReadName()); + } +} + +#define STAT_ID MAKE_ID('s','T','a','t') + +void STAT_Write(FILE *file) +{ + FPNGChunkArchive arc (file, STAT_ID); + SerializeStatistics(arc); +} + +void STAT_Read(PNGHandle *png) +{ + DWORD chunkLen = (DWORD)M_FindPNGChunk (png, STAT_ID); + if (chunkLen != 0) + { + FPNGChunkArchive arc (png->File->GetFile(), STAT_ID, chunkLen); + SerializeStatistics(arc); + } +} + +//========================================================================== +// +// show statistics +// +//========================================================================== + +FString GetStatString() +{ + FString compose; + for(unsigned i = 0; i < LevelData.Size(); i++) + { + OneLevel *l = &LevelData[i]; + compose.AppendFormat("Level %s - Kills: %d/%d - Secrets: %d/%d - Time: %d:%02d\n", + l->levelname, l->killcount, l->totalkills, l->secretcount, l->totalsecrets, + l->leveltime/(60*TICRATE), (l->leveltime/TICRATE)%60); + } + return compose; +} + +CCMD(printstats) +{ + StoreLevelStats(); // Get the current level's current results. + Printf("%s", GetStatString()); +} + + +CCMD(finishgame) +{ + // This CCMD simulates an end-of-game action and exists to end mods that never exit their last level. + G_SetForEndGame (level.nextmap); + G_ExitLevel (0, false); +} + diff --git a/src/strnatcmp.c b/src/strnatcmp.c new file mode 100644 index 0000000000..fb8636d87a --- /dev/null +++ b/src/strnatcmp.c @@ -0,0 +1,178 @@ +/* -*- mode: c; c-file-style: "k&r" -*- + + strnatcmp.c -- Perform 'natural order' comparisons of strings in C. + Copyright (C) 2000, 2004 by Martin Pool + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + + +/* partial change history: + * + * 2004-10-10 mbp: Lift out character type dependencies into macros. + * + * Eric Sosman pointed out that ctype functions take a parameter whose + * value must be that of an unsigned int, even on platforms that have + * negative chars in their default char type. + */ + +#include +#include +#include +#include + +#include "strnatcmp.h" + + +/* These are defined as macros to make it easier to adapt this code to + * different characters types or comparison functions. */ +static __inline int +nat_isdigit(nat_char a) +{ + return isdigit((unsigned char) a); +} + + +static __inline int +nat_isspace(nat_char a) +{ + return isspace((unsigned char) a); +} + + +static __inline nat_char +nat_toupper(nat_char a) +{ + return toupper((unsigned char) a); +} + + + +static int +compare_right(nat_char const *a, nat_char const *b) +{ + int bias = 0; + + /* The longest run of digits wins. That aside, the greatest + value wins, but we can't know that it will until we've scanned + both numbers to know that they have the same magnitude, so we + remember it in BIAS. */ + for (;; a++, b++) { + if (!nat_isdigit(*a) && !nat_isdigit(*b)) + return bias; + else if (!nat_isdigit(*a)) + return -1; + else if (!nat_isdigit(*b)) + return +1; + else if (*a < *b) { + if (!bias) + bias = -1; + } else if (*a > *b) { + if (!bias) + bias = +1; + } else if (!*a && !*b) + return bias; + } + + return 0; +} + + +static int +compare_left(nat_char const *a, nat_char const *b) +{ + /* Compare two left-aligned numbers: the first to have a + different value wins. */ + for (;; a++, b++) { + if (!nat_isdigit(*a) && !nat_isdigit(*b)) + return 0; + else if (!nat_isdigit(*a)) + return -1; + else if (!nat_isdigit(*b)) + return +1; + else if (*a < *b) + return -1; + else if (*a > *b) + return +1; + } + + return 0; +} + + +static int strnatcmp0(nat_char const *a, nat_char const *b, int fold_case) +{ + int ai, bi; + nat_char ca, cb; + int fractional, result; + + assert(a && b); + ai = bi = 0; + while (1) { + ca = a[ai]; cb = b[bi]; + + /* skip over leading spaces or zeros */ + while (nat_isspace(ca)) + ca = a[++ai]; + + while (nat_isspace(cb)) + cb = b[++bi]; + + /* process run of digits */ + if (nat_isdigit(ca) && nat_isdigit(cb)) { + fractional = (ca == '0' || cb == '0'); + + if (fractional) { + if ((result = compare_left(a+ai, b+bi)) != 0) + return result; + } else { + if ((result = compare_right(a+ai, b+bi)) != 0) + return result; + } + } + + if (!ca && !cb) { + /* The strings compare the same. Perhaps the caller + will want to call strcmp to break the tie. */ + return 0; + } + + if (fold_case) { + ca = nat_toupper(ca); + cb = nat_toupper(cb); + } + + if (ca < cb) + return -1; + else if (ca > cb) + return +1; + + ++ai; ++bi; + } +} + + + +int strnatcmp(nat_char const *a, nat_char const *b) { + return strnatcmp0(a, b, 0); +} + + +/* Compare, recognizing numeric string and ignoring case. */ +int strnatcasecmp(nat_char const *a, nat_char const *b) { + return strnatcmp0(a, b, 1); +} diff --git a/src/strnatcmp.h b/src/strnatcmp.h new file mode 100644 index 0000000000..7d74623493 --- /dev/null +++ b/src/strnatcmp.h @@ -0,0 +1,39 @@ +/* -*- mode: c; c-file-style: "k&r" -*- + + strnatcmp.c -- Perform 'natural order' comparisons of strings in C. + Copyright (C) 2000, 2004 by Martin Pool + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* CUSTOMIZATION SECTION + * + * You can change this typedef, but must then also change the inline + * functions in strnatcmp.c */ +typedef char nat_char; + +int strnatcmp(nat_char const *a, nat_char const *b); +int strnatcasecmp(nat_char const *a, nat_char const *b); + +#ifdef __cplusplus +} +#endif diff --git a/zdoom.vcproj b/zdoom.vcproj index 94a7a92dd8..11e243a83e 100644 --- a/zdoom.vcproj +++ b/zdoom.vcproj @@ -1000,6 +1000,10 @@ RelativePath=".\src\st_stuff.cpp" > + + @@ -1008,6 +1012,10 @@ RelativePath=".\src\stringtable.cpp" > + + @@ -1533,6 +1541,10 @@ RelativePath=".\src\stringtable.h" > + +