mirror of
https://github.com/ddnet/ddnet.git
synced 2024-11-19 06:28:19 +00:00
5528cde5f6
869: Complete rework of the ghost and race recorder r=Learath2 a=Redix This PR fixes a lot of issues with the ghost and race recorder, introduces a new ghost file format and implements some pretty useful features. **A quick overview:** - When you cross the start line multiple times, both (ghost and recorder) will only restart if it is a non-solo server - If available, both will use the race timer to recognize the start instead of searching for the start line - Fastcap support for both - The recorder immediately starts when the Tee spawns, so the preparation steps (which are quite important for speedruns) will be included in the demo. If you do not cross the start-line within 20 seconds, it will stop the demo until you really start. - Better ghost menu (colors, reloading, deleting and saving ghosts) - The ghosts are more resistant against lags (old ones were sometimes completely useless due to small lags) - New ghosts files are significantly smaller - Cleanup, bugfixes.. **About the new ghost format (version 4/5):** This format is used by Teerace for over a year now. The code for handling the binary files was moved to the engine. It includes an auto updater which creates a backup of all ghosts and converts them to the new format afterwards. The major differences from the format used by DDNet right now (version 2), are the ability to store multiple types of data, the usage of delta-encoding and a more portable header. For whatever reason, the ghost stores values for every predicted tick, but without tick information, so lost snapshots can make them unusable. The new code uses the original values from the snapshots including ticks but it can also handle the old ones without. Since hardly any server uses the high bandwidth option this practically reduces the file size. Like the demo recorder the ghost recorder directly stores the data to a file (every 50 snapshots) instead of writing the whole file at the end of the race. Indeed this can be changed with only a few lines if the old behavior is preferred. The updater can handle version 2 (DDNet) and 3 (old teerace format, only slightly different from version 2) files. The updating already happens when the files are scanned for generating the list in the menu and not only when you activate them. The change from version 4 to 5 was only needed due to a bug in the implementation, the ghost loader can read both. Some numbers about the file size: (map: hotrun, both about 30 seconds) - Old ghost: 30.4 kB (converted: 10.7 kB) - New ghost: 5.4 kB **One thing about the race recorder:** The old implementation compared the new file only with the first file it found for the particular map. The new one compares with all related demos and deletes them possibly, so that only the best demo is left. Since DDNet can also store the demos without name, this might also delete demos from other players, that you might have in your directory. To prevent this I at least check whether the demo contains the player name if `cl_demo_name` is on. In my opinion the better solution would be to remove `cl_demo_name` and always use the player name.
517 lines
13 KiB
C++
517 lines
13 KiB
C++
/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
|
|
/* If you are missing that file, acquire a complete release at teeworlds.com. */
|
|
#include <base/math.h>
|
|
#include <base/system.h>
|
|
#include <engine/storage.h>
|
|
#include "linereader.h"
|
|
|
|
// compiled-in data-dir path
|
|
#define DATA_DIR "data"
|
|
|
|
class CStorage : public IStorage
|
|
{
|
|
public:
|
|
enum
|
|
{
|
|
MAX_PATHS = 16,
|
|
MAX_PATH_LENGTH = 512
|
|
};
|
|
|
|
char m_aaStoragePaths[MAX_PATHS][MAX_PATH_LENGTH];
|
|
int m_NumPaths;
|
|
char m_aDatadir[MAX_PATH_LENGTH];
|
|
char m_aUserdir[MAX_PATH_LENGTH];
|
|
char m_aCurrentdir[MAX_PATH_LENGTH];
|
|
char m_aBinarydir[MAX_PATH_LENGTH];
|
|
|
|
CStorage()
|
|
{
|
|
mem_zero(m_aaStoragePaths, sizeof(m_aaStoragePaths));
|
|
m_NumPaths = 0;
|
|
m_aDatadir[0] = 0;
|
|
m_aUserdir[0] = 0;
|
|
}
|
|
|
|
int Init(const char *pApplicationName, int StorageType, int NumArgs, const char **ppArguments)
|
|
{
|
|
// get userdir
|
|
fs_storage_path(pApplicationName, m_aUserdir, sizeof(m_aUserdir));
|
|
|
|
// get datadir
|
|
FindDatadir(ppArguments[0]);
|
|
|
|
// get currentdir
|
|
if(!fs_getcwd(m_aCurrentdir, sizeof(m_aCurrentdir)))
|
|
m_aCurrentdir[0] = 0;
|
|
|
|
// load paths from storage.cfg
|
|
LoadPaths(ppArguments[0]);
|
|
|
|
if(!m_NumPaths)
|
|
{
|
|
dbg_msg("storage", "using standard paths");
|
|
AddDefaultPaths();
|
|
}
|
|
|
|
// add save directories
|
|
if(StorageType != STORAGETYPE_BASIC && m_NumPaths && (!m_aaStoragePaths[TYPE_SAVE][0] || !fs_makedir(m_aaStoragePaths[TYPE_SAVE])))
|
|
{
|
|
char aPath[MAX_PATH_LENGTH];
|
|
if(StorageType == STORAGETYPE_CLIENT)
|
|
{
|
|
fs_makedir(GetPath(TYPE_SAVE, "screenshots", aPath, sizeof(aPath)));
|
|
fs_makedir(GetPath(TYPE_SAVE, "screenshots/auto", aPath, sizeof(aPath)));
|
|
fs_makedir(GetPath(TYPE_SAVE, "screenshots/auto/stats", aPath, sizeof(aPath)));
|
|
fs_makedir(GetPath(TYPE_SAVE, "maps", aPath, sizeof(aPath)));
|
|
fs_makedir(GetPath(TYPE_SAVE, "downloadedmaps", aPath, sizeof(aPath)));
|
|
}
|
|
fs_makedir(GetPath(TYPE_SAVE, "dumps", aPath, sizeof(aPath)));
|
|
fs_makedir(GetPath(TYPE_SAVE, "demos", aPath, sizeof(aPath)));
|
|
fs_makedir(GetPath(TYPE_SAVE, "demos/auto", aPath, sizeof(aPath)));
|
|
fs_makedir(GetPath(TYPE_SAVE, "demos/auto/race", aPath, sizeof(aPath)));
|
|
fs_makedir(GetPath(TYPE_SAVE, "editor", aPath, sizeof(aPath)));
|
|
fs_makedir(GetPath(TYPE_SAVE, "ghosts", aPath, sizeof(aPath)));
|
|
fs_makedir(GetPath(TYPE_SAVE, "teehistorian", aPath, sizeof(aPath)));
|
|
}
|
|
|
|
return m_NumPaths ? 0 : 1;
|
|
}
|
|
|
|
void LoadPaths(const char *pArgv0)
|
|
{
|
|
// check current directory
|
|
IOHANDLE File = io_open("storage.cfg", IOFLAG_READ);
|
|
if(!File)
|
|
{
|
|
// check usable path in argv[0]
|
|
unsigned int Pos = ~0U;
|
|
for(unsigned i = 0; pArgv0[i]; i++)
|
|
if(pArgv0[i] == '/' || pArgv0[i] == '\\')
|
|
Pos = i;
|
|
if(Pos < MAX_PATH_LENGTH)
|
|
{
|
|
char aBuffer[MAX_PATH_LENGTH];
|
|
str_copy(aBuffer, pArgv0, Pos+1);
|
|
str_append(aBuffer, "/storage.cfg", sizeof(aBuffer));
|
|
File = io_open(aBuffer, IOFLAG_READ);
|
|
}
|
|
|
|
if(Pos >= MAX_PATH_LENGTH || !File)
|
|
{
|
|
dbg_msg("storage", "couldn't open storage.cfg");
|
|
return;
|
|
}
|
|
}
|
|
|
|
char *pLine;
|
|
CLineReader LineReader;
|
|
LineReader.Init(File);
|
|
|
|
while((pLine = LineReader.Get()))
|
|
{
|
|
if(str_length(pLine) > 9 && !str_comp_num(pLine, "add_path ", 9))
|
|
AddPath(pLine+9);
|
|
}
|
|
|
|
io_close(File);
|
|
|
|
if(!m_NumPaths)
|
|
dbg_msg("storage", "no paths found in storage.cfg");
|
|
}
|
|
|
|
void AddDefaultPaths()
|
|
{
|
|
AddPath("$USERDIR");
|
|
AddPath("$DATADIR");
|
|
AddPath("$CURRENTDIR");
|
|
}
|
|
|
|
void AddPath(const char *pPath)
|
|
{
|
|
if(m_NumPaths >= MAX_PATHS || !pPath[0])
|
|
return;
|
|
|
|
if(!str_comp(pPath, "$USERDIR"))
|
|
{
|
|
if(m_aUserdir[0])
|
|
{
|
|
str_copy(m_aaStoragePaths[m_NumPaths++], m_aUserdir, MAX_PATH_LENGTH);
|
|
dbg_msg("storage", "added path '$USERDIR' ('%s')", m_aUserdir);
|
|
}
|
|
}
|
|
else if(!str_comp(pPath, "$DATADIR"))
|
|
{
|
|
if(m_aDatadir[0])
|
|
{
|
|
str_copy(m_aaStoragePaths[m_NumPaths++], m_aDatadir, MAX_PATH_LENGTH);
|
|
dbg_msg("storage", "added path '$DATADIR' ('%s')", m_aDatadir);
|
|
}
|
|
}
|
|
else if(!str_comp(pPath, "$CURRENTDIR"))
|
|
{
|
|
m_aaStoragePaths[m_NumPaths++][0] = 0;
|
|
dbg_msg("storage", "added path '$CURRENTDIR' ('%s')", m_aCurrentdir);
|
|
}
|
|
else
|
|
{
|
|
if(fs_is_dir(pPath))
|
|
{
|
|
str_copy(m_aaStoragePaths[m_NumPaths++], pPath, MAX_PATH_LENGTH);
|
|
dbg_msg("storage", "added path '%s'", pPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FindDatadir(const char *pArgv0)
|
|
{
|
|
// 1) use data-dir in PWD if present
|
|
if(fs_is_dir("data/mapres"))
|
|
{
|
|
str_copy(m_aDatadir, "data", sizeof(m_aDatadir));
|
|
str_copy(m_aBinarydir, "", sizeof(m_aBinarydir));
|
|
return;
|
|
}
|
|
|
|
// 2) use compiled-in data-dir if present
|
|
if(fs_is_dir(DATA_DIR "/mapres"))
|
|
{
|
|
str_copy(m_aDatadir, DATA_DIR, sizeof(m_aDatadir));
|
|
str_copy(m_aBinarydir, "", sizeof(m_aBinarydir));
|
|
return;
|
|
}
|
|
|
|
// 3) check for usable path in argv[0]
|
|
{
|
|
unsigned int Pos = ~0U;
|
|
for(unsigned i = 0; pArgv0[i]; i++)
|
|
if(pArgv0[i] == '/' || pArgv0[i] == '\\')
|
|
Pos = i;
|
|
|
|
if(Pos < MAX_PATH_LENGTH)
|
|
{
|
|
char aBaseDir[MAX_PATH_LENGTH];
|
|
str_copy(aBaseDir, pArgv0, Pos+1);
|
|
str_copy(m_aBinarydir, aBaseDir, sizeof(m_aBinarydir));
|
|
str_format(m_aDatadir, sizeof(m_aDatadir), "%s/data", aBaseDir);
|
|
str_append(aBaseDir, "/data/mapres", sizeof(aBaseDir));
|
|
|
|
if(fs_is_dir(aBaseDir))
|
|
return;
|
|
else
|
|
m_aDatadir[0] = 0;
|
|
}
|
|
}
|
|
|
|
#if defined(CONF_FAMILY_UNIX)
|
|
// 4) check for all default locations
|
|
{
|
|
const char *aDirs[] = {
|
|
"/usr/share/ddnet",
|
|
"/usr/share/games/ddnet",
|
|
"/usr/local/share/ddnet",
|
|
"/usr/local/share/games/ddnet",
|
|
"/usr/pkg/share/ddnet",
|
|
"/usr/pkg/share/games/ddnet",
|
|
"/opt/ddnet"
|
|
};
|
|
const int DirsCount = sizeof(aDirs) / sizeof(aDirs[0]);
|
|
|
|
int i;
|
|
for (i = 0; i < DirsCount; i++)
|
|
{
|
|
char aBuf[128];
|
|
str_format(aBuf, sizeof(aBuf), "%s/data/mapres", aDirs[i]);
|
|
if(fs_is_dir(aBuf))
|
|
{
|
|
str_copy(m_aBinarydir, aDirs[i], sizeof(m_aDatadir));
|
|
str_format(m_aDatadir, sizeof(m_aDatadir), "%s/data", aDirs[i]);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// no data-dir found
|
|
dbg_msg("storage", "warning no data directory found");
|
|
}
|
|
|
|
|
|
virtual void ListDirectoryInfo(int Type, const char *pPath, FS_LISTDIR_INFO_CALLBACK pfnCallback, void *pUser)
|
|
{
|
|
char aBuffer[MAX_PATH_LENGTH];
|
|
if(Type == TYPE_ALL)
|
|
{
|
|
// list all available directories
|
|
for(int i = 0; i < m_NumPaths; ++i)
|
|
fs_listdir_info(GetPath(i, pPath, aBuffer, sizeof(aBuffer)), pfnCallback, i, pUser);
|
|
}
|
|
else if(Type >= 0 && Type < m_NumPaths)
|
|
{
|
|
// list wanted directory
|
|
fs_listdir_info(GetPath(Type, pPath, aBuffer, sizeof(aBuffer)), pfnCallback, Type, pUser);
|
|
}
|
|
}
|
|
|
|
virtual void ListDirectory(int Type, const char *pPath, FS_LISTDIR_CALLBACK pfnCallback, void *pUser)
|
|
{
|
|
char aBuffer[MAX_PATH_LENGTH];
|
|
if(Type == TYPE_ALL)
|
|
{
|
|
// list all available directories
|
|
for(int i = 0; i < m_NumPaths; ++i)
|
|
fs_listdir(GetPath(i, pPath, aBuffer, sizeof(aBuffer)), pfnCallback, i, pUser);
|
|
}
|
|
else if(Type >= 0 && Type < m_NumPaths)
|
|
{
|
|
// list wanted directory
|
|
fs_listdir(GetPath(Type, pPath, aBuffer, sizeof(aBuffer)), pfnCallback, Type, pUser);
|
|
}
|
|
}
|
|
|
|
virtual const char *GetPath(int Type, const char *pDir, char *pBuffer, unsigned BufferSize)
|
|
{
|
|
str_format(pBuffer, BufferSize, "%s%s%s", m_aaStoragePaths[Type], !m_aaStoragePaths[Type][0] ? "" : "/", pDir);
|
|
return pBuffer;
|
|
}
|
|
|
|
virtual IOHANDLE OpenFile(const char *pFilename, int Flags, int Type, char *pBuffer = 0, int BufferSize = 0)
|
|
{
|
|
char aBuffer[MAX_PATH_LENGTH];
|
|
if(!pBuffer)
|
|
{
|
|
pBuffer = aBuffer;
|
|
BufferSize = sizeof(aBuffer);
|
|
}
|
|
|
|
if(Type == TYPE_ABSOLUTE)
|
|
{
|
|
return io_open(pFilename, Flags);
|
|
}
|
|
if(str_comp_num(pFilename, "mapres/../skins/", 16) == 0) {
|
|
pFilename = pFilename + 10; // just start from skins/
|
|
}
|
|
if(pFilename[0] == '/' || pFilename[0] == '\\' || str_find(pFilename, "../") != NULL || str_find(pFilename, "..\\") != NULL
|
|
#ifdef CONF_FAMILY_WINDOWS
|
|
|| (pFilename[0] && pFilename[1] == ':')
|
|
#endif
|
|
)
|
|
{
|
|
// don't escape base directory
|
|
}
|
|
else if(Flags&IOFLAG_WRITE)
|
|
{
|
|
return io_open(GetPath(TYPE_SAVE, pFilename, pBuffer, BufferSize), Flags);
|
|
}
|
|
else
|
|
{
|
|
IOHANDLE Handle = 0;
|
|
|
|
if(Type <= TYPE_ALL)
|
|
{
|
|
// check all available directories
|
|
for(int i = 0; i < m_NumPaths; ++i)
|
|
{
|
|
Handle = io_open(GetPath(i, pFilename, pBuffer, BufferSize), Flags);
|
|
if(Handle)
|
|
return Handle;
|
|
}
|
|
}
|
|
else if(Type >= 0 && Type < m_NumPaths)
|
|
{
|
|
// check wanted directory
|
|
Handle = io_open(GetPath(Type, pFilename, pBuffer, BufferSize), Flags);
|
|
if(Handle)
|
|
return Handle;
|
|
}
|
|
}
|
|
|
|
pBuffer[0] = 0;
|
|
return 0;
|
|
}
|
|
|
|
struct CFindCBData
|
|
{
|
|
CStorage *pStorage;
|
|
const char *pFilename;
|
|
const char *pPath;
|
|
char *pBuffer;
|
|
int BufferSize;
|
|
};
|
|
|
|
static int FindFileCallback(const char *pName, int IsDir, int Type, void *pUser)
|
|
{
|
|
CFindCBData Data = *static_cast<CFindCBData *>(pUser);
|
|
if(IsDir)
|
|
{
|
|
if(pName[0] == '.')
|
|
return 0;
|
|
|
|
// search within the folder
|
|
char aBuf[MAX_PATH_LENGTH];
|
|
char aPath[MAX_PATH_LENGTH];
|
|
str_format(aPath, sizeof(aPath), "%s/%s", Data.pPath, pName);
|
|
Data.pPath = aPath;
|
|
fs_listdir(Data.pStorage->GetPath(Type, aPath, aBuf, sizeof(aBuf)), FindFileCallback, Type, &Data);
|
|
if(Data.pBuffer[0])
|
|
return 1;
|
|
}
|
|
else if(!str_comp(pName, Data.pFilename))
|
|
{
|
|
// found the file = end
|
|
str_format(Data.pBuffer, Data.BufferSize, "%s/%s", Data.pPath, Data.pFilename);
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
virtual bool FindFile(const char *pFilename, const char *pPath, int Type, char *pBuffer, int BufferSize)
|
|
{
|
|
if(BufferSize < 1)
|
|
return false;
|
|
|
|
pBuffer[0] = 0;
|
|
char aBuf[MAX_PATH_LENGTH];
|
|
CFindCBData Data;
|
|
Data.pStorage = this;
|
|
Data.pFilename = pFilename;
|
|
Data.pPath = pPath;
|
|
Data.pBuffer = pBuffer;
|
|
Data.BufferSize = BufferSize;
|
|
|
|
if(Type == TYPE_ALL)
|
|
{
|
|
// search within all available directories
|
|
for(int i = 0; i < m_NumPaths; ++i)
|
|
{
|
|
fs_listdir(GetPath(i, pPath, aBuf, sizeof(aBuf)), FindFileCallback, i, &Data);
|
|
if(pBuffer[0])
|
|
return true;
|
|
}
|
|
}
|
|
else if(Type >= 0 && Type < m_NumPaths)
|
|
{
|
|
// search within wanted directory
|
|
fs_listdir(GetPath(Type, pPath, aBuf, sizeof(aBuf)), FindFileCallback, Type, &Data);
|
|
}
|
|
|
|
return pBuffer[0] != 0;
|
|
}
|
|
|
|
virtual bool RemoveFile(const char *pFilename, int Type)
|
|
{
|
|
if(Type < 0 || Type >= m_NumPaths)
|
|
return false;
|
|
|
|
char aBuffer[MAX_PATH_LENGTH];
|
|
return !fs_remove(GetPath(Type, pFilename, aBuffer, sizeof(aBuffer)));
|
|
}
|
|
|
|
virtual bool RemoveBinaryFile(const char *pFilename)
|
|
{
|
|
char aBuffer[MAX_PATH_LENGTH];
|
|
return !fs_remove(GetBinaryPath(pFilename, aBuffer, sizeof(aBuffer)));
|
|
}
|
|
|
|
virtual bool RenameFile(const char *pOldFilename, const char *pNewFilename, int Type)
|
|
{
|
|
if(Type < 0 || Type >= m_NumPaths)
|
|
return false;
|
|
char aOldBuffer[MAX_PATH_LENGTH];
|
|
char aNewBuffer[MAX_PATH_LENGTH];
|
|
return !fs_rename(GetPath(Type, pOldFilename, aOldBuffer, sizeof(aOldBuffer)), GetPath(Type, pNewFilename, aNewBuffer, sizeof (aNewBuffer)));
|
|
}
|
|
|
|
virtual bool RenameBinaryFile(const char *pOldFilename, const char *pNewFilename)
|
|
{
|
|
char aOldBuffer[MAX_PATH_LENGTH];
|
|
char aNewBuffer[MAX_PATH_LENGTH];
|
|
|
|
GetBinaryPath(pOldFilename, aOldBuffer, sizeof(aOldBuffer));
|
|
GetBinaryPath(pNewFilename, aNewBuffer, sizeof(aNewBuffer));
|
|
|
|
if(fs_makedir_rec_for(aNewBuffer) < 0)
|
|
dbg_msg("storage", "cannot create folder for: %s", aNewBuffer);
|
|
|
|
return !fs_rename(aOldBuffer, aNewBuffer);
|
|
}
|
|
|
|
virtual bool CreateFolder(const char *pFoldername, int Type)
|
|
{
|
|
if(Type < 0 || Type >= m_NumPaths)
|
|
return false;
|
|
|
|
char aBuffer[MAX_PATH_LENGTH];
|
|
return !fs_makedir(GetPath(Type, pFoldername, aBuffer, sizeof(aBuffer)));
|
|
}
|
|
|
|
virtual void GetCompletePath(int Type, const char *pDir, char *pBuffer, unsigned BufferSize)
|
|
{
|
|
if(Type < 0 || Type >= m_NumPaths)
|
|
{
|
|
if(BufferSize > 0)
|
|
pBuffer[0] = 0;
|
|
return;
|
|
}
|
|
|
|
GetPath(Type, pDir, pBuffer, BufferSize);
|
|
}
|
|
|
|
virtual const char* GetBinaryPath(const char *pDir, char *pBuffer, unsigned BufferSize)
|
|
{
|
|
str_format(pBuffer, BufferSize, "%s%s%s", m_aBinarydir, !m_aBinarydir[0] ? "" : "/", pDir);
|
|
return pBuffer;
|
|
}
|
|
|
|
static IStorage *Create(const char *pApplicationName, int StorageType, int NumArgs, const char **ppArguments)
|
|
{
|
|
CStorage *p = new CStorage();
|
|
if(p && p->Init(pApplicationName, StorageType, NumArgs, ppArguments))
|
|
{
|
|
dbg_msg("storage", "initialisation failed");
|
|
delete p;
|
|
p = 0;
|
|
}
|
|
return p;
|
|
}
|
|
};
|
|
|
|
void IStorage::StripPathAndExtension(const char *pFilename, char *pBuffer, int BufferSize)
|
|
{
|
|
const char *pFilenameEnd = pFilename + str_length(pFilename);
|
|
const char *pExtractedName = pFilename;
|
|
const char *pEnd = pFilenameEnd;
|
|
for(const char *pIter = pFilename; *pIter; pIter++)
|
|
{
|
|
if(*pIter == '/' || *pIter == '\\')
|
|
{
|
|
pExtractedName = pIter + 1;
|
|
pEnd = pFilenameEnd;
|
|
}
|
|
else if(*pIter == '.')
|
|
{
|
|
pEnd = pIter;
|
|
}
|
|
}
|
|
|
|
int Length = min(BufferSize, (int)(pEnd - pExtractedName + 1));
|
|
str_copy(pBuffer, pExtractedName, Length);
|
|
}
|
|
|
|
IStorage *CreateStorage(const char *pApplicationName, int StorageType, int NumArgs, const char **ppArguments) { return CStorage::Create(pApplicationName, StorageType, NumArgs, ppArguments); }
|
|
|
|
IStorage *CreateLocalStorage()
|
|
{
|
|
CStorage *pStorage = new CStorage();
|
|
if(pStorage)
|
|
{
|
|
if(!fs_getcwd(pStorage->m_aCurrentdir, sizeof(pStorage->m_aCurrentdir)))
|
|
{
|
|
delete pStorage;
|
|
return NULL;
|
|
}
|
|
pStorage->AddPath("$CURRENTDIR");
|
|
}
|
|
return pStorage;
|
|
}
|