From 8fb3a63a6d8b1980068fba7bec25d0a79a4a8c0e Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 21 Jan 2023 14:30:20 +0300 Subject: [PATCH 1/2] Add support for extended game API. A long standing issue when developing Q2 mods has been lack of access to server VFS. This fixes it. --- inc/server/server.h | 1 + inc/shared/game.h | 47 +++++++++++++ src/common/files.c | 3 + src/server/game.c | 157 ++++++++++++++++++++++++++++---------------- src/server/main.c | 6 ++ src/server/server.h | 3 +- 6 files changed, 161 insertions(+), 56 deletions(-) diff --git a/inc/server/server.h b/inc/server/server.h index 293806d77..ca9929553 100644 --- a/inc/server/server.h +++ b/inc/server/server.h @@ -41,6 +41,7 @@ unsigned SV_Frame(unsigned msec); void SV_SetConsoleTitle(void); #endif //void SV_ConsoleOutput(const char *msg); +void SV_RestartFilesystem(void); #if USE_MVD_CLIENT && USE_CLIENT bool MVD_GetDemoStatus(float *progress, bool *paused, int *framenum); diff --git a/inc/shared/game.h b/inc/shared/game.h index 7cb3d2cc5..e5f69700a 100644 --- a/inc/shared/game.h +++ b/inc/shared/game.h @@ -240,4 +240,51 @@ typedef struct { int max_edicts; } game_export_t; +typedef game_export_t *(*game_entry_t)(game_import_t *); + +//=============================================================== + +/* + * GetExtendedGameAPI() is guaranteed to be called after GetGameAPI() and + * before ge->Init(). + * + * Unlike GetGameAPI(), passed game_import_ex_t * is valid as long as game + * library is loaded. Pointed to structure should be considered unknown length + * and must not be copied locally. + * + * New fields can be safely added at the end of game_import_ex_t and + * game_export_ex_t structures, provided GAME_API_VERSION_EX is also bumped. + */ + +#define GAME_API_VERSION_EX 1 + +typedef struct { + int apiversion; + + int64_t (*OpenFile)(const char *path, qhandle_t *f, unsigned mode); // returns file length + int (*CloseFile)(qhandle_t f); + int (*LoadFile)(const char *path, void **buffer, unsigned flags, unsigned tag); + + int (*ReadFile)(void *buffer, size_t len, qhandle_t f); + int (*WriteFile)(const void *buffer, size_t len, qhandle_t f); + int (*FlushFile)(qhandle_t f); + int64_t (*TellFile)(qhandle_t f); + int (*SeekFile)(qhandle_t f, int64_t offset, int whence); + int (*ReadLine)(qhandle_t f, char *buffer, size_t size); + + void **(*ListFiles)(const char *path, const char *filter, unsigned flags, int *count_p); + void (*FreeFileList)(void **list); + + const char *(*ErrorString)(int error); + void *(*TagRealloc)(void *ptr, size_t size); +} game_import_ex_t; + +typedef struct { + int apiversion; + + void (*RestartFilesystem)(void); // called when fs_restart is issued +} game_export_ex_t; + +typedef const game_export_ex_t *(*game_entry_ex_t)(const game_import_ex_t *); + #endif // GAME_H diff --git a/src/common/files.c b/src/common/files.c index 06bf78cde..d862ed8ae 100644 --- a/src/common/files.c +++ b/src/common/files.c @@ -27,6 +27,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "common/intreadwrite.h" #include "system/system.h" #include "client/client.h" +#include "server/server.h" #include "format/pak.h" #include @@ -3586,6 +3587,8 @@ void FS_Restart(bool total) setup_game_paths(); + SV_RestartFilesystem(); + FS_Path_f(); Com_Printf("----------------------\n"); diff --git a/src/server/game.c b/src/server/game.c index 7f5008489..50ccd6037 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -19,7 +19,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "server.h" -game_export_t *ge; +const game_export_t *ge; +const game_export_ex_t *gex; static void PF_configstring(int index, const char *val); @@ -730,8 +731,100 @@ static void PF_DebugGraph(float value, int color) { } +static int PF_LoadFile(const char *path, void **buffer, unsigned flags, unsigned tag) +{ + if (tag > UINT16_MAX - TAG_MAX) { + Com_Error(ERR_DROP, "%s: bad tag", __func__); + } + return FS_LoadFileEx(path, buffer, flags, tag + TAG_MAX); +} + +static void *PF_TagRealloc(void *ptr, size_t size) +{ + if (!ptr && size) { + Com_Error(ERR_DROP, "%s: untagged allocation not allowed", __func__); + } + return Z_Realloc(ptr, size); +} + //============================================== +static const game_import_t game_import = { + .multicast = SV_Multicast, + .unicast = PF_Unicast, + .bprintf = PF_bprintf, + .dprintf = PF_dprintf, + .cprintf = PF_cprintf, + .centerprintf = PF_centerprintf, + .error = PF_error, + + .linkentity = PF_LinkEdict, + .unlinkentity = PF_UnlinkEdict, + .BoxEdicts = SV_AreaEdicts, + .trace = SV_Trace, + .pointcontents = SV_PointContents, + .setmodel = PF_setmodel, + .inPVS = PF_inPVS, + .inPHS = PF_inPHS, + .Pmove = PF_Pmove, + + .modelindex = PF_ModelIndex, + .soundindex = PF_SoundIndex, + .imageindex = PF_ImageIndex, + + .configstring = PF_configstring, + .sound = PF_StartSound, + .positioned_sound = SV_StartSound, + + .WriteChar = MSG_WriteChar, + .WriteByte = MSG_WriteByte, + .WriteShort = MSG_WriteShort, + .WriteLong = MSG_WriteLong, + .WriteFloat = PF_WriteFloat, + .WriteString = MSG_WriteString, + .WritePosition = MSG_WritePos, + .WriteDir = MSG_WriteDir, + .WriteAngle = MSG_WriteAngle, + + .TagMalloc = PF_TagMalloc, + .TagFree = Z_Free, + .FreeTags = PF_FreeTags, + + .cvar = PF_cvar, + .cvar_set = Cvar_UserSet, + .cvar_forceset = Cvar_Set, + + .argc = Cmd_Argc, + .argv = Cmd_Argv, + .args = Cmd_RawArgs, + .AddCommandString = PF_AddCommandString, + + .DebugGraph = PF_DebugGraph, + .SetAreaPortalState = PF_SetAreaPortalState, + .AreasConnected = PF_AreasConnected, +}; + +static const game_import_ex_t game_import_ex = { + .apiversion = GAME_API_VERSION_EX, + + .OpenFile = FS_OpenFile, + .CloseFile = FS_CloseFile, + .LoadFile = PF_LoadFile, + + .ReadFile = FS_Read, + .WriteFile = FS_Write, + .FlushFile = FS_Flush, + .TellFile = FS_Tell, + .SeekFile = FS_Seek, + .ReadLine = FS_ReadLine, + + .ListFiles = FS_ListFiles, + .FreeFileList = FS_FreeList, + + .ErrorString = Q_ErrorString, + .TagRealloc = PF_TagRealloc, +}; + static void *game_library; /* @@ -744,6 +837,7 @@ it is changing to a different game directory. */ void SV_ShutdownGameProgs(void) { + gex = NULL; if (ge) { ge->Shutdown(); ge = NULL; @@ -799,7 +893,7 @@ Init the game subsystem for a new map void SV_InitGameProgs(void) { game_import_t import; - game_export_t *(*entry)(game_import_t *) = NULL; + game_entry_t entry = NULL; // unload anything we have now SV_ShutdownGameProgs(); @@ -829,59 +923,7 @@ void SV_InitGameProgs(void) Com_Error(ERR_DROP, "Failed to load game library"); // load a new game dll - import.multicast = SV_Multicast; - import.unicast = PF_Unicast; - import.bprintf = PF_bprintf; - import.dprintf = PF_dprintf; - import.cprintf = PF_cprintf; - import.centerprintf = PF_centerprintf; - import.error = PF_error; - - import.linkentity = PF_LinkEdict; - import.unlinkentity = PF_UnlinkEdict; - import.BoxEdicts = SV_AreaEdicts; - import.trace = SV_Trace; - import.pointcontents = SV_PointContents; - import.setmodel = PF_setmodel; - import.inPVS = PF_inPVS; - import.inPHS = PF_inPHS; - import.Pmove = PF_Pmove; - - import.modelindex = PF_ModelIndex; - import.soundindex = PF_SoundIndex; - import.imageindex = PF_ImageIndex; - - import.configstring = PF_configstring; - import.sound = PF_StartSound; - import.positioned_sound = SV_StartSound; - - import.WriteChar = MSG_WriteChar; - import.WriteByte = MSG_WriteByte; - import.WriteShort = MSG_WriteShort; - import.WriteLong = MSG_WriteLong; - import.WriteFloat = PF_WriteFloat; - import.WriteString = MSG_WriteString; - import.WritePosition = MSG_WritePos; - import.WriteDir = MSG_WriteDir; - import.WriteAngle = MSG_WriteAngle; - - import.TagMalloc = PF_TagMalloc; - import.TagFree = Z_Free; - import.FreeTags = PF_FreeTags; - - import.cvar = PF_cvar; - import.cvar_set = Cvar_UserSet; - import.cvar_forceset = Cvar_Set; - - import.argc = Cmd_Argc; - import.argv = Cmd_Argv; - // original Cmd_Args() did actually return raw arguments - import.args = Cmd_RawArgs; - import.AddCommandString = PF_AddCommandString; - - import.DebugGraph = PF_DebugGraph; - import.SetAreaPortalState = PF_SetAreaPortalState; - import.AreasConnected = PF_AreasConnected; + import = game_import; ge = entry(&import); if (!ge) { @@ -893,6 +935,11 @@ void SV_InitGameProgs(void) ge->apiversion, GAME_API_VERSION); } + // get extended api if present + game_entry_ex_t entry_ex = Sys_GetProcAddress(game_library, "GetExtendedGameAPI"); + if (entry_ex) + gex = entry_ex(&game_import_ex); + // initialize ge->Init(); diff --git a/src/server/main.c b/src/server/main.c index 9f6233eff..7d0c7acc6 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -2052,6 +2052,12 @@ void SV_UserinfoChanged(client_t *cl) //============================================================================ +void SV_RestartFilesystem(void) +{ + if (gex && gex->RestartFilesystem) + gex->RestartFilesystem(); +} + #if USE_SYSCON void SV_SetConsoleTitle(void) { diff --git a/src/server/server.h b/src/server/server.h index f568434d3..7572901a8 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -758,7 +758,8 @@ void SV_WriteFrameToClient_Enhanced(client_t *client); // // sv_game.c // -extern game_export_t *ge; +extern const game_export_t *ge; +extern const game_export_ex_t *gex; void SV_InitGameProgs(void); void SV_ShutdownGameProgs(void); From 3c04d518685fdbee353347102d6237138fb55a03 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 21 Jan 2023 14:14:44 +0300 Subject: [PATCH 2/2] Split file open flags to shared header. Will be shared with game. Always use 64-bit time in file_info_t. Change FS_MODE_READ to 0. Add more comments. --- inc/common/files.h | 51 +------------------------------- inc/shared/files.h | 69 +++++++++++++++++++++++++++++++++++++++++++ src/client/ui/demos.c | 2 +- 3 files changed, 71 insertions(+), 51 deletions(-) create mode 100644 inc/shared/files.h diff --git a/inc/common/files.h b/inc/common/files.h index 21fb8a9fe..3f7bddd00 100644 --- a/inc/common/files.h +++ b/inc/common/files.h @@ -22,61 +22,12 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "common/cmd.h" #include "common/error.h" #include "common/zone.h" +#include "shared/files.h" #define MIN_LISTED_FILES 1024 #define MAX_LISTED_FILES 250000000 #define MAX_LISTED_DEPTH 8 -typedef struct file_info_s { - int64_t size; - time_t ctime; - time_t mtime; - char name[1]; -} file_info_t; - -// bits 0 - 1, enum -#define FS_MODE_APPEND 0x00000000 -#define FS_MODE_READ 0x00000001 -#define FS_MODE_WRITE 0x00000002 -#define FS_MODE_RDWR 0x00000003 -#define FS_MODE_MASK 0x00000003 - -// bits 2 - 3, enum -#define FS_BUF_DEFAULT 0x00000000 -#define FS_BUF_FULL 0x00000004 -#define FS_BUF_LINE 0x00000008 -#define FS_BUF_NONE 0x0000000c -#define FS_BUF_MASK 0x0000000c - -// bits 4 - 5, enum -#define FS_TYPE_ANY 0x00000000 -#define FS_TYPE_REAL 0x00000010 -#define FS_TYPE_PAK 0x00000020 -#define FS_TYPE_RESERVED 0x00000030 -#define FS_TYPE_MASK 0x00000030 - -// bits 6 - 7, flag -#define FS_PATH_ANY 0x00000000 -#define FS_PATH_BASE 0x00000040 -#define FS_PATH_GAME 0x00000080 -#define FS_PATH_MASK 0x000000c0 - -// bits 8 - 13, flag -#define FS_SEARCH_BYFILTER 0x00000100 -#define FS_SEARCH_SAVEPATH 0x00000200 -#define FS_SEARCH_EXTRAINFO 0x00000400 -#define FS_SEARCH_STRIPEXT 0x00000800 -#define FS_SEARCH_DIRSONLY 0x00001000 -#define FS_SEARCH_RECURSIVE 0x00002000 -#define FS_SEARCH_MASK 0x00003f00 - -// bits 8 - 12, flag -#define FS_FLAG_GZIP 0x00000100 // transparently (de)compress with gzip -#define FS_FLAG_EXCL 0x00000200 // create the file, fail if already exists -#define FS_FLAG_TEXT 0x00000400 // open in text mode if from disk -#define FS_FLAG_DEFLATE 0x00000800 // if compressed in .pkz, read raw deflate data, fail otherwise -#define FS_FLAG_LOADFILE 0x00001000 // open non-unique handle, must be closed very quickly - // // Limit the maximum file size FS_LoadFile can handle, as a protection from // malicious paks causing memory exhaustion. diff --git a/inc/shared/files.h b/inc/shared/files.h new file mode 100644 index 000000000..895726e05 --- /dev/null +++ b/inc/shared/files.h @@ -0,0 +1,69 @@ +/* +Copyright (C) 2023 Andrey Nazarov + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +typedef struct { + int64_t size; + int64_t ctime; + int64_t mtime; + char name[1]; +} file_info_t; + +// file opening mode +#define FS_MODE_READ 0x00000000 +#define FS_MODE_WRITE 0x00000001 +#define FS_MODE_APPEND 0x00000002 +#define FS_MODE_RDWR 0x00000003 // similar to FS_MODE_APPEND, but does not create the file +#define FS_MODE_MASK 0x00000003 + +// output buffering mode +#define FS_BUF_DEFAULT 0x00000000 // default mode (normally fully buffered) +#define FS_BUF_FULL 0x00000004 // fully buffered +#define FS_BUF_LINE 0x00000008 // line buffered +#define FS_BUF_NONE 0x0000000c // unbuffered +#define FS_BUF_MASK 0x0000000c + +// where to open file from +#define FS_TYPE_ANY 0x00000000 // open from anywhere +#define FS_TYPE_REAL 0x00000010 // open from disk only +#define FS_TYPE_PAK 0x00000020 // open from pack only +#define FS_TYPE_MASK 0x00000030 + +// where to look for a file +#define FS_PATH_ANY 0x00000000 // look in any search paths +#define FS_PATH_BASE 0x00000040 // look in base search paths +#define FS_PATH_GAME 0x00000080 // look in game search paths +#define FS_PATH_MASK 0x000000c0 + +// search mode for ListFiles() +#define FS_SEARCH_BYFILTER 0x00000100 // wildcard search instead of extension search +#define FS_SEARCH_SAVEPATH 0x00000200 // preserve file path +#define FS_SEARCH_EXTRAINFO 0x00000400 // return file_info_t *, not char * +#define FS_SEARCH_STRIPEXT 0x00000800 // strip file extension +#define FS_SEARCH_DIRSONLY 0x00001000 // search only directories (can't be mixed with other flags) +#define FS_SEARCH_RECURSIVE 0x00002000 // recursive search (implied by FS_SEARCH_BYFILTER) +#define FS_SEARCH_MASK 0x0000ff00 + +// misc flags for OpenFile() +#define FS_FLAG_GZIP 0x00000100 // transparently (de)compress with gzip +#define FS_FLAG_EXCL 0x00000200 // create the file, fail if already exists +#define FS_FLAG_TEXT 0x00000400 // open in text mode if from disk +#define FS_FLAG_DEFLATE 0x00000800 // if compressed, read raw deflate data, fail otherwise +#define FS_FLAG_LOADFILE 0x00001000 // open non-unique handle, must be closed very quickly +#define FS_FLAG_MASK 0x0000ff00 diff --git a/src/client/ui/demos.c b/src/client/ui/demos.c index 51bbb995a..5be178df1 100644 --- a/src/client/ui/demos.c +++ b/src/client/ui/demos.c @@ -123,7 +123,7 @@ static void BuildName(const file_info_t *info, char **cache) // format date len = 0; - if ((tm = localtime(&info->mtime)) != NULL) { + if ((tm = localtime(&(time_t){info->mtime})) != NULL) { if (tm->tm_year == m_demos.year) { len = strftime(date, sizeof(date), "%b %d %H:%M", tm); } else {