From cdff3e9e230acd10fd3395448adf87cba2ced60e Mon Sep 17 00:00:00 2001 From: Michael Pollind Date: Thu, 26 Mar 2026 07:55:15 -0700 Subject: [PATCH 1/6] feat: start on workshop support Signed-off-by: Michael Pollind --- assets/data0_21pure/ui/porkui/template.rml | 1 + assets/data0_21pure/ui/porkui/workshop.rml | 20 +++++ source/qcommon/steam.c | 20 ++--- source/steamshim/src/child/ServerBrowser.cpp | 1 - source/steamshim/src/child/child.cpp | 26 ++++-- source/steamshim/src/child/write_utils.h | 2 +- source/steamshim/src/parent/parent.cpp | 3 +- source/steamshim/src/parent/parent.h | 1 - source/steamshim/src/steamshim.h | 89 -------------------- source/steamshim/src/steamshim_types.h | 20 ++++- 10 files changed, 68 insertions(+), 115 deletions(-) create mode 100644 assets/data0_21pure/ui/porkui/workshop.rml delete mode 100644 source/steamshim/src/steamshim.h diff --git a/assets/data0_21pure/ui/porkui/template.rml b/assets/data0_21pure/ui/porkui/template.rml index e32b7c9c48..877430460a 100644 --- a/assets/data0_21pure/ui/porkui/template.rml +++ b/assets/data0_21pure/ui/porkui/template.rml @@ -190,6 +190,7 @@ Home Play + Workshop Options Credits Quit diff --git a/assets/data0_21pure/ui/porkui/workshop.rml b/assets/data0_21pure/ui/porkui/workshop.rml new file mode 100644 index 0000000000..16bc97b077 --- /dev/null +++ b/assets/data0_21pure/ui/porkui/workshop.rml @@ -0,0 +1,20 @@ + + + workshop + + + + + + +
+
+ + Mod + Workshop ID + Local + +
+
+ + + +
+
+

Publish a local mod to Steam Workshop. Place your mod folder in mods/ and add a preview.png for a thumbnail.

+
+
Mod folder
+ +
+
Title
+ +
+
Description
+ +
+
Tags
+ +
+
Visibility
+ +
+
Change note
+ +
+
+ +
+
+ +
diff --git a/source/qcommon/common.c b/source/qcommon/common.c index a173424311..c87dd4dd96 100644 --- a/source/qcommon/common.c +++ b/source/qcommon/common.c @@ -34,6 +34,9 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include "crashpad.h" +#define STB_DS_IMPLEMENTATION 1 +#include "../extern/stb/stb_ds.h" + #define MAX_NUM_ARGVS 50 static bool dynvars_initialized = false; diff --git a/source/qcommon/files.c b/source/qcommon/files.c index 6a4f2ebe56..25a966ea59 100644 --- a/source/qcommon/files.c +++ b/source/qcommon/files.c @@ -29,7 +29,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include "../qalgo/md5.h" #include "../qalgo/q_trie.h" #include "../gameshared/q_sds.h" - +#include "../extern/stb/stb_ds.h" /* ============================================================================= @@ -153,6 +153,7 @@ typedef struct searchpath_s pack_t *pack; struct searchpath_s *base; // parent basepath struct searchpath_s *next; + struct searchpath_s *group_next; // will be grouping the search paths together bool append_basegame; } searchpath_t; @@ -162,6 +163,11 @@ typedef struct searchpath_t *searchPath; } searchfile_t; +struct steam_fs_mod { + uint64_t mod_id; + struct searchpath_s* search_path; +}; + static searchfile_t *fs_searchfiles; static int fs_numsearchfiles; static int fs_cursearchfiles; @@ -182,6 +188,9 @@ static searchpath_t *fs_root_searchpath; // base path directory static searchpath_t *fs_write_searchpath; // write directory static searchpath_t *fs_downloads_searchpath; // write directory for downloads from game servers +static searchpath_t *fs_base_steam_paths = NULL; +static struct steam_fs_mod *fs_mod_paths = NULL; + static mempool_t *fs_mempool; #define FS_Malloc( size ) Mem_Alloc( fs_mempool, size ) @@ -4111,13 +4120,46 @@ bool FS_SetGameDirectory( const char *dir, bool force ) return true; } -/* -* FS_AddBasePath -*/ +static searchpath_t* FS_AddSearchPath(searchpath_t* base, const char *path, bool append_basegame) { + assert(base); + assert(base->next); + + searchpath_t *newpath = ( searchpath_t* )FS_Malloc( sizeof( searchpath_t ) ); + newpath->base = base; + newpath->next = base->next; + base->next = newpath; + if(newpath->next) { + newpath->next->base = newpath; + } + newpath->path = FS_CopyString( path ); + newpath->append_basegame = append_basegame; + COM_SanitizeFilePath( newpath->path ); + return newpath; +} + +static void FS_RemoveSearchPath(searchpath_t *search_path) { + assert(search_path); + if(search_path->base) { + search_path->base->next = search_path->next; + } + else { + fs_searchpaths = search_path->next; + } + if(search_path->next) { + search_path->next->base = search_path->base; + } + + if(search_path->group_next) { + FS_RemoveSearchPath(search_path->group_next); + } + + FS_Free( search_path->path ); + FS_Free( search_path ); +} + static void FS_AddBasePath( const char *path, bool append_basegame ) { searchpath_t *newpath; - newpath = ( searchpath_t* )FS_Malloc( sizeof( searchpath_t ) ); newpath->path = FS_CopyString( path ); newpath->next = fs_basepaths; @@ -4132,9 +4174,101 @@ void FS_AddExtraPK3Directory( const char *path ) } +void FS_UnRegisterSteamModPath( uint64_t mod ) +{ + QMutex_Lock( fs_searchpaths_mutex ); + + for(size_t i = 0; i < stbds_arrlen(fs_mod_paths); i++) { + if(fs_mod_paths[i].mod_id == mod) { + FS_RemoveSearchPath(fs_mod_paths[i].search_path); + stbds_arrdel(fs_mod_paths, i); + break; + } + } + + QMutex_Unlock( fs_searchpaths_mutex ); +} + +void FS_RegisterSteamModPath( uint64_t mod, const char *path ) +{ + int i, totalpaks; + char **paknames; + pack_t *pak; + searchpath_t *search, *prev, *next; + + QMutex_Lock( fs_searchpaths_mutex ); + for(size_t i = 0; i < stbds_arrlen(fs_mod_paths); i++) { + if(fs_mod_paths[i].mod_id == mod) { + QMutex_Unlock( fs_searchpaths_mutex ); + return; + } + } + + struct steam_fs_mod steam_mod = { 0 }; + steam_mod.mod_id = mod; + steam_mod.search_path = FS_AddSearchPath(fs_base_steam_paths, path, false); + searchpath_t* head = steam_mod.search_path; + + totalpaks = 0; + if( ( paknames = FS_GamePathPaks( steam_mod.search_path, "", &totalpaks ) ) != 0 ) + { + for( i = 0; i < totalpaks; i++ ) + { + searchpath_t *compare = fs_searchpaths; + while( compare ) + { + if( compare->pack ) + { + int cmp = Q_stricmp( COM_FileBase( compare->pack->filename ), COM_FileBase( paknames[i] ) ); + if( !cmp ) + { + if( !Q_stricmp( compare->pack->filename, paknames[i] ) ) + goto freename; + } + } + compare = compare->next; + } + + if( !FS_FindPackFilePos( paknames[i], NULL, NULL, NULL ) ) + continue; + + pak = ( pack_t* )FS_Malloc( sizeof( *pak ) ); + pak->filename = FS_CopyString( paknames[i] ); + pak->deferred_pack = NULL; + pak->deferred_load = true; + + if( FS_FindPackFilePos( paknames[i], &search, &prev, &next ) ) + { + search->base = prev; + search->pack = pak; + if( !prev ) + { + search->next = fs_searchpaths; + fs_searchpaths = search; + } + else + { + prev->next = search; + search->next = next; + } + if( search->next ) { + search->next->base = search; + } + head->group_next = search; + head = head->group_next; + } +freename: + Mem_ZoneFree( paknames[i] ); + } + Mem_ZoneFree( paknames ); + } + stbds_arrpush(fs_mod_paths, steam_mod); + QMutex_Unlock( fs_searchpaths_mutex ); +} + /* -* FS_FreeSearchFiles -*/ + * FS_FreeSearchFiles + */ static void FS_FreeSearchFiles( void ) { int i; @@ -4348,6 +4482,7 @@ void FS_Init( void ) fs_numsearchfiles = FS_MIN_SEARCHFILES; fs_searchfiles = ( searchfile_t* )FS_Malloc( sizeof( searchfile_t ) * fs_numsearchfiles ); + memset( fs_filehandles, 0, sizeof( fs_filehandles ) ); // @@ -4385,6 +4520,7 @@ void FS_Init( void ) FS_AddBasePath( downloadsdir, true ); fs_downloads_searchpath = fs_basepaths; } + fs_base_steam_paths = fs_basepaths; if( fs_cdpath->string[0] ) FS_AddBasePath( fs_cdpath->string, true ); @@ -4508,6 +4644,7 @@ void FS_Shutdown( void ) Sys_VFS_Shutdown(); + stbds_arrfree(fs_mod_paths); Mem_FreePool( &fs_mempool ); QMutex_Destroy( &fs_fh_mutex ); diff --git a/source/qcommon/mod_fs.h b/source/qcommon/mod_fs.h index 98c46c769b..4d5d080968 100644 --- a/source/qcommon/mod_fs.h +++ b/source/qcommon/mod_fs.h @@ -39,6 +39,8 @@ DECLARE_TYPEDEF_METHOD( void, FS_CreateAbsolutePath, const char *path ); DECLARE_TYPEDEF_METHOD( const char *, FS_AbsoluteNameForFile, const char *filename ); DECLARE_TYPEDEF_METHOD( const char *, FS_AbsoluteNameForBaseFile, const char *filename ); DECLARE_TYPEDEF_METHOD( void, FS_AddExtraPK3Directory, const char *path ); +DECLARE_TYPEDEF_METHOD( void, FS_RegisterSteamModPath, uint64_t mod, const char *path ); +DECLARE_TYPEDEF_METHOD( void, FS_UnRegisterSteamModPath, uint64_t mod ); DECLARE_TYPEDEF_METHOD( int, FS_FOpenFile, const char *filename, int *filenum, int mode ); DECLARE_TYPEDEF_METHOD( int, FS_FOpenFileGroup, const char *filename, int *filenum, int mode, group_handle_t *group ); @@ -124,6 +126,8 @@ struct fs_import_s { FS_AbsoluteNameForFileFn FS_AbsoluteNameForFile; FS_AbsoluteNameForBaseFileFn FS_AbsoluteNameForBaseFile; FS_AddExtraPK3DirectoryFn FS_AddExtraPK3Directory; + FS_RegisterSteamModPathFn FS_RegisterSteamModPath; + FS_UnRegisterSteamModPathFn FS_UnRegisterSteamModPath; FS_LoadFileExtFn FS_LoadFileExt; FS_LoadBaseFileExtFn FS_LoadBaseFileExt; FS_FreeFileFn FS_FreeFile; @@ -198,6 +202,8 @@ void FS_CreateAbsolutePath( const char *path ){ fs_import.FS_CreateAbsolutePath( const char * FS_AbsoluteNameForFile(const char *filename ){ return fs_import.FS_AbsoluteNameForFile(filename);} const char * FS_AbsoluteNameForBaseFile(const char *filename ){ return fs_import.FS_AbsoluteNameForBaseFile(filename);} void FS_AddExtraPK3Directory(const char *path ){ fs_import.FS_AddExtraPK3Directory(path);} +void FS_RegisterSteamModPath( uint64_t mod, const char *path ) { fs_import.FS_RegisterSteamModPath( mod, path ); } +void FS_UnRegisterSteamModPath( uint64_t mod ) { fs_import.FS_UnRegisterSteamModPath( mod ); } int FS_FOpenFile(const char *filename, int *filenum, int mode ){ return fs_import.FS_FOpenFile(filename, filenum, mode);} int FS_FOpenFileGroup(const char *filename, int *filenum, int mode, group_handle_t *group ){ return fs_import.FS_FOpenFileGroup(filename, filenum, mode, group);} int FS_FOpenBaseFile(const char *filename, int *filenum, int mode ){ return fs_import.FS_FOpenBaseFile(filename, filenum, mode);} @@ -245,4 +251,3 @@ static inline void Q_ImportFsModule(struct fs_import_s* mod) { #endif - diff --git a/source/qcommon/qcommon.h b/source/qcommon/qcommon.h index 34818195d9..077d2cc8fc 100644 --- a/source/qcommon/qcommon.h +++ b/source/qcommon/qcommon.h @@ -737,6 +737,8 @@ static const struct fs_import_s default_fs_imports_s = { .FS_AbsoluteNameForFile = FS_AbsoluteNameForFile, .FS_AbsoluteNameForBaseFile = FS_AbsoluteNameForBaseFile, .FS_AddExtraPK3Directory = FS_AddExtraPK3Directory, + .FS_RegisterSteamModPath = FS_RegisterSteamModPath, + .FS_UnRegisterSteamModPath = FS_UnRegisterSteamModPath, .FS_LoadFileExt = FS_LoadFileExt, .FS_LoadBaseFileExt = FS_LoadBaseFileExt, .FS_FreeFile = FS_FreeFile, @@ -781,6 +783,8 @@ bool FS_SetGameDirectory( const char *dir, bool force ); int FS_GetGameDirectoryList( char *buf, size_t bufsize ); int FS_GetExplicitPurePakList( char ***paknames ); bool FS_IsExplicitPurePak( const char *pakname, bool *wrongver ); +void FS_RegisterSteamModPath( uint64_t mod, const char *path ); +void FS_UnRegisterSteamModPath( uint64_t mod ); /** * Maps an existing file on disk for reading. diff --git a/source/qcommon/steam.c b/source/qcommon/steam.c index 3a75784505..6315d6976a 100644 --- a/source/qcommon/steam.c +++ b/source/qcommon/steam.c @@ -17,13 +17,484 @@ along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ +#include "steam.h" +#include "../extern/stb/stb_ds.h" #include "../qcommon/qcommon.h" #include "../steamshim/src/parent/parent.h" +#include "../steamshim/src/mod_steam.h" #include "cvar.h" -#include "steam.h" +#include "sys_fs.h" +#include +#include #include cvar_t *steam_debug; +static struct steam_workshop_mod_s *steam_workshop_mods; +static uint32_t steam_workshop_refresh_cookie; + +static char *Steam_CopyString( const char *in ) +{ + int size; + char *out; + + size = sizeof( char ) * ( strlen( in ) + 1 ); + out = (char *)malloc( size ); + Q_strncpyz( out, in, size ); + + return out; +} + +static struct steam_workshop_mod_s *__UpsertWorkshopMod( uint64_t id ) +{ + for( size_t i = 0; i < arrlen( steam_workshop_mods ); i++ ) { + if( steam_workshop_mods[i].workshop_id == id ) { + return &steam_workshop_mods[i]; + } + } + struct steam_workshop_mod_s mod = { 0 }; + mod.workshop_id = id; + arrput( steam_workshop_mods, mod ); + return &steam_workshop_mods[arrlen( steam_workshop_mods ) - 1]; +} + +static void __RemoveWorkshopMod( uint64_t id ) +{ + for( size_t i = 0; i < arrlen( steam_workshop_mods ); i++ ) { + if( steam_workshop_mods[i].workshop_id != id ) + continue; + FS_UnRegisterSteamModPath( steam_workshop_mods[i].workshop_id ); + if( steam_workshop_mods[i].path ) { + free( (void *)steam_workshop_mods[i].path ); + } + free( (void *)steam_workshop_mods[i].title ); + free( (void *)steam_workshop_mods[i].description ); + free( (void *)steam_workshop_mods[i].tags ); + free( (void *)steam_workshop_mods[i].preview_url ); + arrdel( steam_workshop_mods, i ); + return; + } +} + +static void Steam_WorkshopInstallInfoCallback( void *self, struct steam_rpc_pkt_s *pkt ) +{ + struct steam_workshop_install_info_s *info = &pkt->workshop_install_info; + struct steam_workshop_mod_s *mod = __UpsertWorkshopMod( info->workshop_id ); + if( !info->success ) { + return; + } + + if( mod->path ) { + FS_UnRegisterSteamModPath( mod->workshop_id ); + free( (void *)mod->path ); + } + + mod->path = Steam_CopyString( pkt->workshop_install_info.folder ); + FS_RegisterSteamModPath( mod->workshop_id, mod->path ); +} + +static void Steam_EVT_WorkshopRefreshItems( void *self, struct steam_evt_pkt_s *pkt ) +{ + if( pkt->workshop_refresh_items.cookie != steam_workshop_refresh_cookie ) { + return; + } + + for( size_t i = 0; i < pkt->workshop_refresh_items.num_ids; i++ ) { + uint64_t workshop_id = pkt->workshop_refresh_items.ids[i]; + __UpsertWorkshopMod( workshop_id ); + } + + if( pkt->workshop_refresh_items.num_ids > 0 ) { + uint16_t num_ids = pkt->workshop_refresh_items.num_ids; + size_t req_size = sizeof( struct steam_workshop_req_rpcs_s ) + sizeof( uint64_t ) * num_ids; + struct steam_workshop_req_rpcs_s *request = (struct steam_workshop_req_rpcs_s *)malloc( req_size ); + request->cmd = RPC_WORKSHOP_QUERY_ITEM_DETAILS; + request->num_ids = num_ids; + for( uint16_t i = 0; i < num_ids; i++ ) { + request->workshop_ids[i] = pkt->workshop_refresh_items.ids[i]; + } + STEAMSHIM_sendRPC( request, req_size, NULL, NULL, NULL ); + free( request ); + } +} + +static void Steam_EVT_WorkshopItemSubscribed( void *self, struct steam_evt_pkt_s *pkt ) +{ + if( pkt->workshop_item.app_id != APP_STEAMID ) + return; + + __UpsertWorkshopMod( pkt->workshop_item.workshop_id ); +} + +static void Steam_EVT_WorkshopItemUnSubscribed( void *self, struct steam_evt_pkt_s *pkt ) +{ + if( pkt->workshop_item.app_id != APP_STEAMID ) { + return; + } + + __RemoveWorkshopMod( pkt->workshop_item.workshop_id ); +} + +static void Steam_EVT_WorkshopItemInstalled( void *self, struct steam_evt_pkt_s *pkt ) +{ + if( pkt->workshop_item.app_id != APP_STEAMID ) { + return; + } + + struct steam_workshop_mod_s *mod = __UpsertWorkshopMod( pkt->workshop_item.workshop_id ); + struct steam_workshop_req_rpc_s request; + request.cmd = RPC_WORKSHOP_INSTALLED_INFO; + request.workshop_id = mod->workshop_id; + STEAMSHIM_sendRPC( &request, sizeof( request ), NULL, Steam_WorkshopInstallInfoCallback, NULL ); + + struct { + struct steam_workshop_req_rpcs_s req; + uint64_t id; + } details_request; + details_request.req.cmd = RPC_WORKSHOP_QUERY_ITEM_DETAILS; + details_request.req.num_ids = 1; + details_request.id = mod->workshop_id; + STEAMSHIM_sendRPC( &details_request, sizeof( details_request ), NULL, NULL, NULL ); +} + +static void Steam_EVT_WorkshopDetail( void *self, struct steam_evt_pkt_s *pkt ) +{ + struct workshop_item_details_evt_s *evt = &pkt->workshop_item_details; + if( !evt->success ) { + return; + } + + struct steam_workshop_mod_s *mod = __UpsertWorkshopMod( evt->workshop_id ); + + free( (void *)mod->title ); + free( (void *)mod->description ); + free( (void *)mod->tags ); + free( (void *)mod->preview_url ); + + + const char *buf = evt->buffer; + mod->title = Steam_CopyString( buf ); + buf += evt->title_len + 1; + mod->description = Steam_CopyString( buf ); + buf += evt->description_len + 1; + mod->tags = Steam_CopyString( buf ); + buf += evt->tags_len + 1; + mod->preview_url = Steam_CopyString( buf ); + Com_Printf("Mod: %s Description: %s preview: %s", mod->title, mod->description, mod->preview_url); +} + +void Steam_RefreshWorkshopMods( void ) +{ + struct steam_workshop_list_rpc_s request; + uint32_t sync = 0; + request.cmd = RPC_WORKSHOP_REFRESH_SUBSCRIBED_ITEMS; + request.cookie = ++steam_workshop_refresh_cookie; + STEAMSHIM_sendRPC( &request, sizeof( request ), NULL, NULL, &sync ); +} + +/* + * Steam_ScanLocalMods + * + * Scans /mods/ for publishable mod folders. Each immediate + * subdirectory is treated as one mod. If the folder contains a + * .steam/workshop_id file the stored uint64 is used as the existing + * Workshop item ID (update flow); otherwise workshop_id is 0 (create flow). + * Results are appended to steam_workshop_mods with is_local = true. + */ +static void Steam_ScanLocalMods( void ) +{ + const char *writedir = FS_WriteDirectory(); + + if( !writedir ) + return; + + char modsdir[1024]; + Q_snprintfz( modsdir, sizeof( modsdir ), "%s/mods/*", writedir ); + + uint64_t* workshop_ids = NULL; + const char *entry = Sys_FS_FindFirst( modsdir, SFF_SUBDIR, SFF_HIDDEN | SFF_SYSTEM ); + while( entry ) { + // strip trailing slash added by FindFirst for directories + size_t len = strlen( entry ); + char modpath[1024]; + if( len > 0 && entry[len - 1] == '/' ) + Q_snprintfz( modpath, sizeof( modpath ), "%.*s", (int)( len - 1 ), entry ); + else + Q_snprintfz( modpath, sizeof( modpath ), "%s", entry ); + + // derive mod name from last path component + const char *name = strrchr( modpath, '/' ); + name = name ? name + 1 : modpath; + + // try to read an existing Workshop ID from .steam/workshop_id + uint64_t workshop_id = 0; + char idpath[1024]; + Q_snprintfz( idpath, sizeof( idpath ), "%s/workshop_id", modpath ); + FILE *f = Sys_FS_fopen( idpath, "r" ); + if( f ) { + fscanf( f, "%" SCNu64, &workshop_id ); + fclose( f ); + } + + if(workshop_id > 0) { + stbds_arrpush(workshop_ids, workshop_id); + } + + struct steam_workshop_mod_s mod = { 0 }; + mod.is_local = true; + mod.workshop_id = workshop_id; + mod.path = Steam_CopyString( modpath ); + mod.name = Steam_CopyString( name ); + arrput( steam_workshop_mods, mod ); + + entry = Sys_FS_FindNext( SFF_SUBDIR, SFF_HIDDEN | SFF_SYSTEM ); + } + const size_t pkt_len = sizeof(struct steam_workshop_req_rpcs_s) + (stbds_arrlen(workshop_ids) * sizeof(uint64_t)); + struct steam_workshop_req_rpcs_s* req = malloc(pkt_len); + req->cmd = RPC_WORKSHOP_QUERY_ITEM_DETAILS; + req->num_ids = stbds_arrlen(workshop_ids); + for(size_t i = 0; i < stbds_arrlen(workshop_ids); i++) { + req->workshop_ids[i] = workshop_ids[i]; + } + STEAMSHIM_sendRPC( req, pkt_len, NULL, NULL, NULL ); + free(req); + stbds_arrfree(workshop_ids); + Sys_FS_FindClose(); +} + +// --------------------------------------------------------------------------- +// Callbacks for synchronous RPC round-trips +// --------------------------------------------------------------------------- +typedef struct { + uint32_t result; + uint64_t fileId; +} __publish_item_result_t; +typedef struct { + uint64_t handle; + bool success; +} __publish_update_t; +typedef struct { + bool success; +} __publish_ok_t; + +static void __on_create_item( __publish_item_result_t *self, struct steam_rpc_pkt_s *rec ) +{ + self->result = rec->create_item_recv.result; + self->fileId = rec->create_item_recv.file_id; +} +static void __on_submit_item( __publish_item_result_t *self, struct steam_rpc_pkt_s *rec ) +{ + self->result = rec->submit_item_result.result; + self->fileId = rec->submit_item_result.file_id; +} +static void __on_begin_update( __publish_update_t *self, struct steam_rpc_pkt_s *rec ) +{ + self->handle = rec->workshop_start_item_update_recv.handle; + self->success = rec->workshop_start_item_update_recv.success; +} +static void __on_success( __publish_ok_t *self, struct steam_rpc_pkt_s *rec ) +{ + self->success = rec->success.success; +} + +struct steam_workshop_publish_result_s Steam_PublishLocalMod( int modIndex, const struct steam_workshop_publish_s *params ) +{ + struct steam_workshop_publish_result_s res = { 0 }; + + if( !STEAMSHIM_active() ) { + res.res = STEAM_PUBLISH_ERR_STEAM_UNAVAILABLE; + return res; + } + if( !params->title || params->title[0] == '\0' ) { + res.res = STEAM_PUBLISH_ERR_TITLE_REQUIRED; + return res; + } + + const struct steam_workshop_mod_s *mods = Steam_GetWorkshopMods(); + size_t count = Steam_GetWorkshopModCount(); + if( modIndex < 0 || (size_t)modIndex >= count || !mods[modIndex].is_local || !mods[modIndex].path ) { + res.res = STEAM_PUBLISH_ERR_INVALID_MOD; + return res; + } + const char *contentPath = mods[modIndex].path; + + // Check for preview.png + char previewPath[1024]; + Q_snprintfz( previewPath, sizeof( previewPath ), "%s/preview.png", contentPath ); + bool hasPreview; + { + FILE *f = Sys_FS_fopen( previewPath, "rb" ); + hasPreview = ( f != NULL ); + if( f ) + fclose( f ); + } + + uint64_t fileId = mods[modIndex].workshop_id; + res.is_new_item = ( fileId == 0 ); + + // Create a new Workshop item if this is our first submission + if( res.is_new_item ) { + struct steam_rpc_shim_common_s request = { 0 }; + request.cmd = RPC_WORKSHOP_CREATE_ITEM; + __publish_item_result_t createResult = { 0, 0 }; + uint32_t sync = 0; + STEAMSHIM_sendRPC( &request, sizeof( request ), &createResult, (STEAMSHIM_rpc_handle)__on_create_item, &sync ); + if( STEAMSHIM_waitDispatchSync( sync ) != 0 ) { + res.res = STEAM_PUBLISH_ERR_CREATE_FAILED; + return res; + } + if( createResult.result != STEAMSHIM_EResultOK ) { + res.res = STEAM_PUBLISH_ERR_CREATE_FAILED; + res.steam_result = createResult.result; + return res; + } + fileId = createResult.fileId; + } + + // Begin the update transaction to get an UGC handle + uint64_t updateHandle; + { + struct start_item_update_req_s request = { 0 }; + request.cmd = RPC_WORKSHOP_BEGIN_ITEM_UPDATE; + request.publish_item_file = fileId; + __publish_update_t updateResult = { 0, false }; + uint32_t sync = 0; + STEAMSHIM_sendRPC( &request, sizeof( request ), &updateResult, (STEAMSHIM_rpc_handle)__on_begin_update, &sync ); + if( STEAMSHIM_waitDispatchSync( sync ) != 0 || !updateResult.success || updateResult.handle == 0 ) { + res.res = STEAM_PUBLISH_ERR_UPDATE_FAILED; + return res; + } + updateHandle = updateResult.handle; + } + +// Helper macro: send a buffer-based workshop field RPC; returns on failure +#define SEND_BUFFER_FIELD( _cmd, _handle, _value, _err ) \ + { \ + const char *_val = ( _value ); \ + size_t _valLen = strlen( _val ) + 1; \ + size_t _sz = sizeof( struct buffer_workshop_rpc_s ) + _valLen; \ + struct buffer_workshop_rpc_s *_req = (struct buffer_workshop_rpc_s *)malloc( _sz ); \ + if( !_req ) { \ + res.res = ( _err ); \ + return res; \ + } \ + memset( _req, 0, sizeof( struct buffer_workshop_rpc_s ) ); \ + _req->cmd = ( _cmd ); \ + _req->handle = ( _handle ); \ + memcpy( _req->buf, _val, _valLen ); \ + __publish_ok_t _result = { false }; \ + uint32_t _sync = 0; \ + STEAMSHIM_sendRPC( _req, _sz, &_result, (STEAMSHIM_rpc_handle)__on_success, &_sync ); \ + bool _ok = STEAMSHIM_waitDispatchSync( _sync ) == 0 && _result.success; \ + free( _req ); \ + if( !_ok ) { \ + res.res = ( _err ); \ + return res; \ + } \ + } + + SEND_BUFFER_FIELD( RPC_WORKSHOP_ITEM_SET_TITLE, updateHandle, params->title, STEAM_PUBLISH_ERR_SET_TITLE ); + SEND_BUFFER_FIELD( RPC_WORKSHOP_ITEM_SET_DESCRIPTION, updateHandle, params->description ? params->description : "", STEAM_PUBLISH_ERR_SET_DESCRIPTION ); + SEND_BUFFER_FIELD( RPC_WORKSHOP_ITEM_SET_ITEM_CONTENT, updateHandle, contentPath, STEAM_PUBLISH_ERR_SET_CONTENT ); + if( hasPreview ) { + SEND_BUFFER_FIELD( RPC_WORKSHOP_ITEM_SET_PREVIEW, updateHandle, previewPath, STEAM_PUBLISH_ERR_SET_PREVIEW ); + } + +#undef SEND_BUFFER_FIELD + + // Set visibility + { + struct item_visiblity_req_s request = { 0 }; + request.cmd = RPC_WORKSHOP_ITEM_SET_VISIBILITY; + request.handle = updateHandle; + const char *vis = params->visibility ? params->visibility : "private"; + if( Q_stricmp( vis, "public" ) == 0 ) + request.visibility = STEAM_VISIBILITY_PUBLIC; + else if( Q_stricmp( vis, "friends" ) == 0 ) + request.visibility = STEAM_VISIBILITY_FRIENDS_ONLY; + else + request.visibility = STEAM_VISIBILITY_PRIVATE; + __publish_ok_t result = { false }; + uint32_t sync = 0; + STEAMSHIM_sendRPC( &request, sizeof( request ), &result, (STEAMSHIM_rpc_handle)__on_success, &sync ); + if( STEAMSHIM_waitDispatchSync( sync ) != 0 || !result.success ) { + res.res = STEAM_PUBLISH_ERR_SET_VISIBILITY; + return res; + } + } + + // Set tags (comma-separated input → packed NUL-separated buffer) + if( params->tags != NULL ) { + + size_t buf_size = 0; + buf_size += strlen(params->tags) + 1; + size_t reqSz = sizeof( struct tags_req_s ) + buf_size; + struct tags_req_s *req = (struct tags_req_s *)malloc( reqSz ); + memset( req, 0, sizeof( struct tags_req_s ) ); + req->cmd = RPC_WORKSHOP_ITEM_SET_TAGS; + req->handle = updateHandle; + + size_t cursor = 0; + for(const char* c = params->tags; *c != '\0'; c++) { + if (*c == ',') { + req->buffer[cursor++] = '\0'; + req->num_tags++; + continue; + } + req->buffer[cursor++] = *c; + } + __publish_ok_t result = { false }; + uint32_t sync = 0; + STEAMSHIM_sendRPC( req, reqSz, &result, (STEAMSHIM_rpc_handle)__on_success, &sync ); + bool ok = STEAMSHIM_waitDispatchSync( sync ) == 0 && result.success; + free( req ); + if( !ok ) { + res.res = STEAM_PUBLISH_ERR_SET_TAGS; + return res; + } + } + + // Submit the item update + { + const char *note = params->change_note ? params->change_note : ""; + size_t noteLen = strlen( note ) + 1; + size_t reqSz = sizeof( struct buffer_workshop_rpc_s ) + noteLen; + struct buffer_workshop_rpc_s *req = (struct buffer_workshop_rpc_s *)malloc( reqSz ); + if( !req ) { + res.res = STEAM_PUBLISH_ERR_SUBMIT_FAILED; + return res; + } + memset( req, 0, sizeof( struct buffer_workshop_rpc_s ) ); + req->cmd = RPC_WORKSHOP_SUBMIT_ITEM; + req->handle = updateHandle; + memcpy( req->buf, note, noteLen ); + __publish_item_result_t submitResult = { 0, 0 }; + uint32_t sync = 0; + STEAMSHIM_sendRPC( req, reqSz, &submitResult, (STEAMSHIM_rpc_handle)__on_submit_item, &sync ); + bool ok = STEAMSHIM_waitDispatchSync( sync ) == 0; + free( req ); + if( !ok || submitResult.result != STEAMSHIM_EResultOK ) { + res.res = STEAM_PUBLISH_ERR_SUBMIT_FAILED; + res.steam_result = submitResult.result; + return res; + } + res.file_id = submitResult.fileId; + } + + // Persist the new Workshop ID for future update runs + if( res.is_new_item ) { + char idpath[1024]; + Q_snprintfz( idpath, sizeof( idpath ), "%s/workshop_id", contentPath ); + FILE *f = Sys_FS_fopen( idpath, "w" ); + if( f ) { + fprintf( f, "%" PRIu64, res.file_id ); + fclose( f ); + } + } + + res.res = STEAM_PUBLISH_OK; + return res; +} + /* * Steam_Init */ @@ -32,7 +503,7 @@ void Steam_Init( void ) steam_debug = Cvar_Get( "steam_debug", "0", 0 ); SteamshimOptions opts; - opts.debug = steam_debug->integer; + opts.debug = true;//steam_debug->integer; opts.runserver = 1; opts.runclient = !dedicated->integer; int r = STEAMSHIM_init( &opts ); @@ -40,6 +511,19 @@ void Steam_Init( void ) Com_Printf( "Steam initialization failed.\n" ); return; } + + if( dedicated->integer ) { + return; + } + + STEAMSHIM_subscribeEvent( EVT_WORKSHOP_REFRESH_SUBSCRIBE_ITEMS, NULL, Steam_EVT_WorkshopRefreshItems ); + STEAMSHIM_subscribeEvent( EVT_WORKSHOP_ITEM_SUBSCRIBED, NULL, Steam_EVT_WorkshopItemSubscribed ); + STEAMSHIM_subscribeEvent( EVT_WORKSHOP_ITEM_UNSUBSCRIBED, NULL, Steam_EVT_WorkshopItemUnSubscribed ); + STEAMSHIM_subscribeEvent( EVT_WORKSHOP_ITEM_INSTALLED, NULL, Steam_EVT_WorkshopItemInstalled ); + STEAMSHIM_subscribeEvent( EVT_WORKSHOP_DETAIL, NULL, Steam_EVT_WorkshopDetail ); + + Steam_ScanLocalMods(); + Steam_RefreshWorkshopMods(); } /* @@ -47,5 +531,33 @@ void Steam_Init( void ) */ void Steam_Shutdown( void ) { + STEAMSHIM_unsubscribeEvent( EVT_WORKSHOP_REFRESH_SUBSCRIBE_ITEMS, Steam_EVT_WorkshopRefreshItems ); + STEAMSHIM_unsubscribeEvent( EVT_WORKSHOP_ITEM_SUBSCRIBED, Steam_EVT_WorkshopItemSubscribed ); + STEAMSHIM_unsubscribeEvent( EVT_WORKSHOP_ITEM_UNSUBSCRIBED, Steam_EVT_WorkshopItemUnSubscribed ); + STEAMSHIM_unsubscribeEvent( EVT_WORKSHOP_ITEM_INSTALLED, Steam_EVT_WorkshopItemInstalled ); + STEAMSHIM_unsubscribeEvent( EVT_WORKSHOP_DETAIL, Steam_EVT_WorkshopDetail ); + + for( size_t i = 0; i < arrlen( steam_workshop_mods ); i++ ) { + FS_UnRegisterSteamModPath( steam_workshop_mods[i].workshop_id ); + free( (void *)steam_workshop_mods[i].path ); + free( (void *)steam_workshop_mods[i].name ); + free( (void *)steam_workshop_mods[i].title ); + free( (void *)steam_workshop_mods[i].description ); + free( (void *)steam_workshop_mods[i].tags ); + free( (void *)steam_workshop_mods[i].preview_url ); + } + arrfree( steam_workshop_mods ); + steam_workshop_mods = NULL; + STEAMSHIM_deinit(); } + +const struct steam_workshop_mod_s *Steam_GetWorkshopMods( void ) +{ + return steam_workshop_mods; +} + +size_t Steam_GetWorkshopModCount( void ) +{ + return arrlen( steam_workshop_mods ); +} diff --git a/source/qcommon/steam.h b/source/qcommon/steam.h index 38b95158f4..a45e86cbb8 100644 --- a/source/qcommon/steam.h +++ b/source/qcommon/steam.h @@ -24,6 +24,9 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include #define STEAMID_CHARS 18 + +typedef struct cvar_s cvar_t; + void Steam_Init( void ); void Steam_Shutdown( void ); diff --git a/source/steamshim/src/child/child.cpp b/source/steamshim/src/child/child.cpp index 4f3a6c5b58..93d0c72168 100644 --- a/source/steamshim/src/child/child.cpp +++ b/source/steamshim/src/child/child.cpp @@ -20,11 +20,13 @@ freely, subject to the following restrictions: #include #include +#include #include #include #include #include #include +#include #include "../os.h" #include "../steamshim_private.h" @@ -126,64 +128,87 @@ static void processRPC( steam_rpc_pkt_s *req, size_t size ) { switch( req->common.cmd ) { case RPC_PUMP: { - time( &time_since_last_pump ); struct steam_rpc_shim_common_s recv; prepared_rpc_packet( &req->common, &recv ); write_packet( GPipeWrite, &recv, sizeof( steam_rpc_shim_common_s ) ); break; } case RPC_WORKSHOP_SUBMIT_ITEM: { + dbgprintf( "RPC_WORKSHOP_SUBMIT_ITEM handle=%" PRIu64 " note=\"%s\"\n", + (uint64_t)req->workshop_submit_item.handle, (const char *)req->workshop_submit_item.buf ); SteamAPICall_t call = GSteamUGC->SubmitItemUpdate( req->workshop_submit_item.handle, (const char *)req->workshop_submit_item.buf ); + dbgprintf( " -> api_call=%" PRIu64 "\n", (uint64_t)call ); steam_async_push_rpc_shim( call, &req->common ); break; } case RPC_WORKSHOP_BEGIN_ITEM_UPDATE: { + dbgprintf( "RPC_WORKSHOP_BEGIN_ITEM_UPDATE file_id=%" PRIu64 "\n", + (uint64_t)req->workshop_start_item_update.publish_item_file ); STEAM_UGCUpdateHandle_t handle = GSteamUGC->StartItemUpdate( GAppID, req->workshop_start_item_update.publish_item_file ); + dbgprintf( " -> handle=%" PRIu64 " valid=%d\n", (uint64_t)handle, handle != k_UGCUpdateHandleInvalid ); struct start_item_update_recv_s recv; recv.handle = handle; + recv.success = handle != k_UGCUpdateHandleInvalid; prepared_rpc_packet( &req->common, &recv ); write_packet( GPipeWrite, &recv, sizeof( start_item_update_recv_s ) ); break; } case RPC_WORKSHOP_CREATE_ITEM: { + dbgprintf( "RPC_WORKSHOP_CREATE_ITEM app_id=%u\n", (unsigned)GAppID ); SteamAPICall_t call = GSteamUGC->CreateItem( GAppID, k_EWorkshopFileTypeGameManagedItem ); + dbgprintf( " -> api_call=%" PRIu64 "\n", (uint64_t)call ); steam_async_push_rpc_shim( call, &req->common ); break; } case RPC_WORKSHOP_ITEM_SET_TITLE: { + dbgprintf( "RPC_WORKSHOP_ITEM_SET_TITLE handle=%" PRIu64 " title=\"%s\"\n", + (uint64_t)req->workshop_set_title.handle, (const char *)req->workshop_set_title.buf ); struct success_recv_s recv; recv.success = GSteamUGC->SetItemTitle( req->workshop_set_title.handle, (const char *)req->workshop_set_title.buf ); + dbgprintf( " -> success=%d\n", recv.success ); prepared_rpc_packet( &req->common, &recv ); write_packet( GPipeWrite, &recv, sizeof( success_recv_s ) ); break; } case RPC_WORKSHOP_ITEM_SET_DESCRIPTION: { + dbgprintf( "RPC_WORKSHOP_ITEM_SET_DESCRIPTION handle=%" PRIu64 "\n", + (uint64_t)req->workshop_set_description.handle ); struct success_recv_s recv; recv.success = GSteamUGC->SetItemDescription( req->workshop_set_description.handle, (const char *)req->workshop_set_description.buf ); + dbgprintf( " -> success=%d\n", recv.success ); prepared_rpc_packet( &req->common, &recv ); write_packet( GPipeWrite, &recv, sizeof( success_recv_s ) ); break; } case RPC_WORKSHOP_ITEM_SET_ITEM_CONTENT: { + dbgprintf( "RPC_WORKSHOP_ITEM_SET_ITEM_CONTENT handle=%" PRIu64 " path=\"%s\"\n", + (uint64_t)req->workshop_set_item_content.handle, (const char *)req->workshop_set_item_content.buf ); struct success_recv_s recv; recv.success = GSteamUGC->SetItemContent( req->workshop_set_item_content.handle, (const char *)req->workshop_set_item_content.buf ); + dbgprintf( " -> success=%d\n", recv.success ); prepared_rpc_packet( &req->common, &recv ); write_packet( GPipeWrite, &recv, sizeof( success_recv_s ) ); break; } case RPC_WORKSHOP_ITEM_SET_VISIBILITY: { + dbgprintf( "RPC_WORKSHOP_ITEM_SET_VISIBILITY item_id=%" PRIu64 " visibility=%d\n", + (uint64_t)req->workshop_set_visibility.handle, (int)req->workshop_set_visibility.visibility ); struct success_recv_s recv; - recv.success = GSteamUGC->SetItemVisibility( req->workshop_set_visibility.itemID, (ERemoteStoragePublishedFileVisibility)req->workshop_set_visibility.visibility ); + recv.success = GSteamUGC->SetItemVisibility( req->workshop_set_visibility.handle, (ERemoteStoragePublishedFileVisibility)req->workshop_set_visibility.visibility ); + dbgprintf( " -> success=%d\n", recv.success ); prepared_rpc_packet( &req->common, &recv ); write_packet( GPipeWrite, &recv, sizeof( success_recv_s ) ); break; } case RPC_WORKSHOP_ITEM_SET_TAGS: { + dbgprintf( "RPC_WORKSHOP_ITEM_SET_TAGS handle=%" PRIu64 " num_tags=%u\n", + (uint64_t)req->workshop_set_tags.handle, (unsigned)req->workshop_set_tags.num_tags ); struct SteamParamStringArray_t tags; tags.m_ppStrings = (const char **)malloc( sizeof( char * ) * req->workshop_set_tags.num_tags ); tags.m_nNumStrings = 0; const char *c = req->workshop_set_tags.buffer; for( size_t i = 0; i < req->workshop_set_tags.num_tags; i++ ) { + dbgprintf( " tag[%zu]=\"%s\"\n", i, c ); tags.m_ppStrings[tags.m_nNumStrings++] = c; while( *c != '\0' ) { c++; @@ -191,17 +216,80 @@ static void processRPC( steam_rpc_pkt_s *req, size_t size ) } struct success_recv_s recv; recv.success = GSteamUGC->SetItemTags( req->workshop_set_tags.handle, &tags ); + dbgprintf( " -> success=%d\n", recv.success ); free( tags.m_ppStrings ); + prepared_rpc_packet( &req->common, &recv ); write_packet( GPipeWrite, &recv, sizeof( success_recv_s ) ); break; } case RPC_WORKSHOP_ITEM_SET_PREVIEW: { + dbgprintf( "RPC_WORKSHOP_ITEM_SET_PREVIEW handle=%" PRIu64 " path=\"%s\"\n", + (uint64_t)req->workshop_set_preview.handle, (const char *)req->workshop_set_preview.buf ); struct success_recv_s recv; recv.success = GSteamUGC->SetItemPreview( req->workshop_set_preview.handle, (const char *)req->workshop_set_preview.buf ); + dbgprintf( " -> success=%d\n", recv.success ); prepared_rpc_packet( &req->common, &recv ); write_packet( GPipeWrite, &recv, sizeof( success_recv_s ) ); break; } + case RPC_WORKSHOP_INSTALLED_INFO: { + dbgprintf( "RPC_WORKSHOP_INSTALLED_INFO workshop_id=%" PRIu64 "\n", + (uint64_t)req->steam_workshop_item.workshop_id ); + struct steam_workshop_install_info_s recv = {}; + char folder[1024] = { 0 }; + + prepared_rpc_packet( &req->common, &recv ); + recv.workshop_id = req->steam_workshop_item.workshop_id; + if( GSteamUGC != NULL ) { + uint64 size_on_disk; + recv.success = GSteamUGC->GetItemInstallInfo( req->steam_workshop_item.workshop_id, &size_on_disk, folder, sizeof( folder ), &recv.time_stamp ); + recv.size_on_disk = size_on_disk; + } + + dbgprintf( " -> success=%d folder=\"%s\"\n", recv.success, folder ); + const size_t folderLen = recv.success ? strlen( folder ) : 0; + const uint32_t bufferSize = sizeof( recv ) + folderLen + 1; + writePipe( GPipeWrite, &bufferSize, sizeof( uint32_t ) ); + writePipe( GPipeWrite, &recv, sizeof( recv ) ); + writePipe( GPipeWrite, folder, folderLen + 1 ); + break; + } + case RPC_WORKSHOP_QUERY_ITEM_DETAILS: { + dbgprintf( "RPC_WORKSHOP_QUERY_ITEM_DETAILS num_ids=%u\n", (unsigned)req->steam_workshop_items.num_ids ); + UGCQueryHandle_t query = GSteamUGC->CreateQueryUGCDetailsRequest( (uint64 *)req->steam_workshop_items.workshop_ids, req->steam_workshop_items.num_ids ); + SteamAPICall_t call = GSteamUGC->SendQueryUGCRequest( query ); + dbgprintf( " -> api_call=%" PRIu64 " valid=%d\n", (uint64_t)call, call != k_uAPICallInvalid ); + struct steam_result_recv_s recv; + prepared_rpc_packet( &req->common, &recv ); + recv.result = call; + if( call == k_uAPICallInvalid ) { + GSteamUGC->ReleaseQueryUGCRequest( query ); + } else { + steam_async_push_rpc_shim( call, &req->common ); + } + write_packet( GPipeWrite, &recv, sizeof( struct steam_result_recv_s ) ); + break; + } + case RPC_WORKSHOP_REFRESH_SUBSCRIBED_ITEMS: { + dbgprintf( "RPC_WORKSHOP_REFRESH_SUBSCRIBED_ITEMS cookie=%u\n", (unsigned)req->stream_workshop_refresh.cookie ); + struct success_recv_s recv; + std::vector items( GSteamUGC->GetNumSubscribedItems() ); + const uint32 numItems = GSteamUGC->GetSubscribedItems( items.data(), items.size() ); + dbgprintf( " -> num_subscribed=%u\n", (unsigned)numItems ); + prepared_rpc_packet( &req->common, &recv ); + write_packet( GPipeWrite, &recv, sizeof( success_recv_s ) ); + + for( size_t i = 0; i < numItems; i++ ) { + struct workshop_refresh_items_evt_s evt_res = {}; + evt_res.cookie = req->stream_workshop_refresh.cookie; + evt_res.cmd = EVT_WORKSHOP_REFRESH_SUBSCRIBE_ITEMS; + for( ; evt_res.num_ids < 256 && i < numItems; i++ ) { + evt_res.ids[evt_res.num_ids++] = items[i]; + } + write_packet( GPipeWrite, &evt_res, sizeof( workshop_refresh_items_evt_s ) + ( sizeof( uint64_t ) * evt_res.num_ids ) ); + } + break; + } case RPC_AUTHSESSION_TICKET: { struct auth_session_ticket_recv_s recv; prepared_rpc_packet( &req->common, &recv ); @@ -606,7 +694,6 @@ static void processSteamDispatch() SteamAPI_ManualDispatch_FreeLastCallback( steamPipe ); continue; } - id = callCompleted->m_iCallback; data = callResultData; // dmLogInfo( "SteamAPICallCompleted_t %llu result %d", callCompleted->m_hAsyncCall, id ); @@ -644,6 +731,61 @@ static void processSteamDispatch() write_packet( GPipeWrite, &recv, sizeof( struct create_item_recv_s ) ); break; } + case SteamUGCQueryCompleted_t::k_iCallback: { + SteamUGCQueryCompleted_t *result = (SteamUGCQueryCompleted_t *)data; + SteamUGCDetails_t details = {}; + char preview_url[4096] = { 0 }; + const bool got_result = result->m_eResult == k_EResultOK && result->m_unNumResultsReturned > 0 && GSteamUGC->GetQueryUGCResult( result->m_handle, 0, &details ); + + { + struct workshop_item_details_evt_s recv = {}; + recv.cmd = EVT_WORKSHOP_DETAIL; + const char *title = ""; + const char *description = ""; + const char *tags = ""; + const char *preview = ""; + + if( got_result ) { + GSteamUGC->GetQueryUGCPreviewURL( result->m_handle, 0, preview_url, sizeof( preview_url ) ); + title = details.m_rgchTitle; + description = details.m_rgchDescription; + tags = details.m_rgchTags; + preview = preview_url; + + recv.workshop_id = details.m_nPublishedFileId; + recv.success = true; + recv.owner_id = details.m_ulSteamIDOwner; + recv.creator_app_id = details.m_nCreatorAppID; + recv.consumer_app_id = details.m_nConsumerAppID; + recv.file_type = details.m_eFileType; + recv.visibility = details.m_eVisibility; + recv.time_created = details.m_rtimeCreated; + recv.time_updated = details.m_rtimeUpdated; + recv.votes_up = details.m_unVotesUp; + recv.votes_down = details.m_unVotesDown; + recv.num_children = details.m_unNumChildren; + recv.item_state = GSteamUGC->GetItemState( details.m_nPublishedFileId ); + recv.score = details.m_flScore; + } + + recv.result = result->m_eResult; + recv.title_len = strlen( title ); + recv.description_len = strlen( description ); + recv.tags_len = strlen( tags ); + recv.preview_url_len = strlen( preview ); + + const uint32_t buffer_size = sizeof( recv ) + recv.title_len + 1 + recv.description_len + 1 + recv.tags_len + 1 + recv.preview_url_len + 1; + writePipe( GPipeWrite, &buffer_size, sizeof( uint32_t ) ); + writePipe( GPipeWrite, &recv, sizeof( recv ) ); + writePipe( GPipeWrite, title, recv.title_len + 1 ); + writePipe( GPipeWrite, description, recv.description_len + 1 ); + writePipe( GPipeWrite, tags, recv.tags_len + 1 ); + writePipe( GPipeWrite, preview, recv.preview_url_len + 1 ); + } + + GSteamUGC->ReleaseQueryUGCRequest( result->m_handle ); + break; + } case GameRichPresenceJoinRequested_t::k_iCallback: { GameRichPresenceJoinRequested_t *pCallback = (GameRichPresenceJoinRequested_t *)data; join_request_evt_s evt; @@ -662,6 +804,33 @@ static void processSteamDispatch() write_packet( GPipeWrite, &evt, sizeof( struct persona_changes_evt_s ) ); break; } + case RemoteStoragePublishedFileSubscribed_t::k_iCallback: { + RemoteStoragePublishedFileSubscribed_t *pCallback = (RemoteStoragePublishedFileSubscribed_t *)data; + struct workshop_item_evt_s evt; + evt.cmd = EVT_WORKSHOP_ITEM_SUBSCRIBED; + evt.app_id = pCallback->m_nAppID; + evt.workshop_id = pCallback->m_nPublishedFileId; + write_packet( GPipeWrite, &evt, sizeof( workshop_item_evt_s ) ); + break; + } + case RemoteStoragePublishedFileUnsubscribed_t::k_iCallback: { + RemoteStoragePublishedFileUnsubscribed_t *pCallback = (RemoteStoragePublishedFileUnsubscribed_t *)data; + struct workshop_item_evt_s evt; + evt.cmd = EVT_WORKSHOP_ITEM_UNSUBSCRIBED; + evt.app_id = pCallback->m_nAppID; + evt.workshop_id = pCallback->m_nPublishedFileId; + write_packet( GPipeWrite, &evt, sizeof( workshop_item_evt_s ) ); + break; + } + case ItemInstalled_t::k_iCallback: { + ItemInstalled_t *pCallback = (ItemInstalled_t *)data; + struct workshop_item_evt_s evt; + evt.cmd = EVT_WORKSHOP_ITEM_INSTALLED; + evt.app_id = pCallback->m_unAppID; + evt.workshop_id = pCallback->m_nPublishedFileId; + write_packet( GPipeWrite, &evt, sizeof( workshop_item_evt_s ) ); + break; + } } free( call_result_data ); SteamAPI_ManualDispatch_FreeLastCallback( steamPipe ); @@ -671,7 +840,7 @@ static void processSteamDispatch() static void processCommands() { - static struct steam_packet_buf packet; + static std::vector packet_buf; static size_t cursor = 0; while( 1 ) { if( time_since_last_pump != 0 ) { @@ -680,14 +849,16 @@ static void processCommands() return; } - assert( sizeof( struct steam_packet_buf ) == STEAM_PACKED_RESERVE_SIZE ); - if( !pipeReady( GPipeRead ) ) { std::this_thread::sleep_for( std::chrono::microseconds( 1000 ) ); continue; } - const int bytesRead = readPipe( GPipeRead, packet.buffer + cursor, STEAM_PACKED_RESERVE_SIZE - cursor ); + if( packet_buf.empty() ) { + packet_buf.resize( STEAM_PACKED_RESERVE_SIZE ); + } + + const int bytesRead = readPipe( GPipeRead, packet_buf.data() + cursor, packet_buf.size() - cursor ); if( bytesRead > 0 ) { cursor += bytesRead; } else { @@ -699,25 +870,34 @@ static void processCommands() processSteamDispatch(); continue_processing: - if( packet.size > STEAM_PACKED_RESERVE_SIZE - sizeof( uint32_t ) ) { - // the packet is larger then the reserved size + if( cursor < sizeof( uint32_t ) ) { + continue; + } + + struct steam_packet_buf *packet = reinterpret_cast( packet_buf.data() ); + const size_t packetlen = packet->size + sizeof( uint32_t ); + if( packetlen < sizeof( uint32_t ) ) { return; } - if( cursor < packet.size + sizeof( uint32_t ) ) { + if( packetlen > packet_buf.size() ) { + packet_buf.resize( packetlen ); + packet = reinterpret_cast( packet_buf.data() ); + } + + if( cursor < packetlen ) { continue; } - if( packet.common.cmd >= RPC_BEGIN && packet.common.cmd < RPC_END ) { - processRPC( &packet.rpc_payload, packet.size ); - } else if( packet.common.cmd >= EVT_BEGIN && packet.common.cmd < EVT_END ) { - processEVT( &packet.evt_payload, packet.size ); + if( packet->common.cmd >= RPC_BEGIN && packet->common.cmd < RPC_END ) { + processRPC( &packet->rpc_payload, packet->size ); + } else if( packet->common.cmd >= EVT_BEGIN && packet->common.cmd < EVT_END ) { + processEVT( &packet->evt_payload, packet->size ); } - if( cursor > packet.size + sizeof( uint32_t ) ) { - const size_t packetlen = packet.size + sizeof( uint32_t ); + if( cursor > packetlen ) { const size_t remainingLen = cursor - packetlen; - memmove( packet.buffer, packet.buffer + packet.size + sizeof( uint32_t ), remainingLen ); + memmove( packet_buf.data(), packet_buf.data() + packetlen, remainingLen ); cursor = remainingLen; goto continue_processing; } else { diff --git a/source/steamshim/src/mod_steam.h b/source/steamshim/src/mod_steam.h index e20ebe8e47..fbe0aa4725 100644 --- a/source/steamshim/src/mod_steam.h +++ b/source/steamshim/src/mod_steam.h @@ -3,16 +3,28 @@ #include "./steamshim_types.h" +struct steam_workshop_mod_s { + uint64_t workshop_id; + bool is_remote; + bool is_local; // discovered from the local mods folder, may be unpublished (workshop_id == 0) + const char *path; + const char *name; // folder name for local mods, NULL for remote-only mods + const char *title; + const char *description; + const char *tags; + const char *preview_url; +}; + #ifdef __cplusplus #define DECLARE_TYPEDEF_METHOD( ret, name, ... ) \ typedef ret ( *name##Fn )( __VA_ARGS__ ); \ - extern "C" { \ - ret name( __VA_ARGS__ ); \ - } + extern "C" { \ + ret name( __VA_ARGS__ ); \ + } #else #define DECLARE_TYPEDEF_METHOD( ret, name, ... ) \ typedef ret ( *name##Fn )( __VA_ARGS__ ); \ - ret name( __VA_ARGS__ ); + ret name( __VA_ARGS__ ); #endif DECLARE_TYPEDEF_METHOD( int, STEAMSHIM_dispatch ); @@ -22,6 +34,45 @@ DECLARE_TYPEDEF_METHOD( int, STEAMSHIM_waitDispatchSync, uint32_t syncIndex ); / DECLARE_TYPEDEF_METHOD( void, STEAMSHIM_subscribeEvent, uint32_t id, void *self, STEAMSHIM_evt_handle evt ); DECLARE_TYPEDEF_METHOD( void, STEAMSHIM_unsubscribeEvent, uint32_t id, STEAMSHIM_evt_handle evt ); DECLARE_TYPEDEF_METHOD( bool, STEAMSHIM_active ); +typedef enum { + STEAM_PUBLISH_OK = 0, + STEAM_PUBLISH_ERR_STEAM_UNAVAILABLE, + STEAM_PUBLISH_ERR_TITLE_REQUIRED, + STEAM_PUBLISH_ERR_INVALID_MOD, + STEAM_PUBLISH_ERR_TOO_MANY_TAGS, + STEAM_PUBLISH_ERR_CREATE_FAILED, + STEAM_PUBLISH_ERR_UPDATE_FAILED, + STEAM_PUBLISH_ERR_SET_TITLE, + STEAM_PUBLISH_ERR_SET_DESCRIPTION, + STEAM_PUBLISH_ERR_SET_CONTENT, + STEAM_PUBLISH_ERR_SET_PREVIEW, + STEAM_PUBLISH_ERR_SET_VISIBILITY, + STEAM_PUBLISH_ERR_SET_TAGS, + STEAM_PUBLISH_ERR_SUBMIT_FAILED, +} steam_publish_error_e; + +// Input parameters for Steam_PublishLocalMod. +// All string fields are borrowed — they must remain valid for the duration of the call. +struct steam_workshop_publish_s { + const char *title; + const char *description; + char* tags; // comma-separated + const char *visibility; // "public" | "friends" | "private" + const char *change_note; +}; + +// Returned by Steam_PublishLocalMod. +struct steam_workshop_publish_result_s { + steam_publish_error_e res; // STEAM_PUBLISH_OK on success + uint32_t steam_result; // raw Steam EResult, valid on create/submit failures + uint64_t file_id; // assigned Workshop item ID + bool is_new_item; // true if a new item was created (vs updated) +}; + +DECLARE_TYPEDEF_METHOD( const struct steam_workshop_mod_s *, Steam_GetWorkshopMods, void ); +DECLARE_TYPEDEF_METHOD( size_t, Steam_GetWorkshopModCount, void ); +DECLARE_TYPEDEF_METHOD( void, Steam_RefreshWorkshopMods, void ); +DECLARE_TYPEDEF_METHOD( struct steam_workshop_publish_result_s, Steam_PublishLocalMod, int modIndex, const struct steam_workshop_publish_s *params ); #undef DECLARE_TYPEDEF_METHOD @@ -33,16 +84,14 @@ struct steam_import_s { STEAMSHIM_subscribeEventFn STEAMSHIM_subscribeEvent; STEAMSHIM_unsubscribeEventFn STEAMSHIM_unsubscribeEvent; STEAMSHIM_activeFn STEAMSHIM_active; + Steam_GetWorkshopModsFn Steam_GetWorkshopMods; + Steam_GetWorkshopModCountFn Steam_GetWorkshopModCount; + Steam_RefreshWorkshopModsFn Steam_RefreshWorkshopMods; + Steam_PublishLocalModFn Steam_PublishLocalMod; }; -#define DECLARE_STEAM_STRUCT() { \ - STEAMSHIM_dispatch, \ - STEAMSHIM_sendRPC, \ - STEAMSHIM_sendEVT, \ - STEAMSHIM_waitDispatchSync, \ - STEAMSHIM_subscribeEvent, \ - STEAMSHIM_unsubscribeEvent, \ - STEAMSHIM_active \ -}; +#define DECLARE_STEAM_STRUCT() \ + { STEAMSHIM_dispatch, STEAMSHIM_sendRPC, STEAMSHIM_sendEVT, STEAMSHIM_waitDispatchSync, STEAMSHIM_subscribeEvent, \ + STEAMSHIM_unsubscribeEvent, STEAMSHIM_active, Steam_GetWorkshopMods, Steam_GetWorkshopModCount, Steam_RefreshWorkshopMods, Steam_PublishLocalMod }; #ifdef STEAM_DEFINE_INTERFACE_IMPL static struct steam_import_s steam_import; @@ -50,12 +99,49 @@ static inline void Q_ImportSteamModule( const struct steam_import_s *imp ) { steam_import = *imp; } -int STEAMSHIM_dispatch() { return steam_import.STEAMSHIM_dispatch();} -int STEAMSHIM_sendRPC( void *req, uint32_t size, void *self, STEAMSHIM_rpc_handle rpc, uint32_t *syncIndex ) { return steam_import.STEAMSHIM_sendRPC(req, size, self, rpc, syncIndex);} -int STEAMSHIM_sendEVT( void *packet, uint32_t size) { return steam_import.STEAMSHIM_sendEVT(packet, size);} -int STEAMSHIM_waitDispatchSync( uint32_t syncIndex ){ return steam_import.STEAMSHIM_waitDispatchSync(syncIndex);} // wait on the dispatch loop -void STEAMSHIM_subscribeEvent( uint32_t id, void *self, STEAMSHIM_evt_handle evt ){ return steam_import.STEAMSHIM_subscribeEvent(id, self, evt);} // wait on the dispatch loop -void STEAMSHIM_unsubscribeEvent( uint32_t id, STEAMSHIM_evt_handle evt){ return steam_import.STEAMSHIM_unsubscribeEvent(id, evt);} // wait on the dispatch loop -bool STEAMSHIM_active(){ return steam_import.STEAMSHIM_active();} // wait on the dispatch loop +int STEAMSHIM_dispatch() +{ + return steam_import.STEAMSHIM_dispatch(); +} +int STEAMSHIM_sendRPC( void *req, uint32_t size, void *self, STEAMSHIM_rpc_handle rpc, uint32_t *syncIndex ) +{ + return steam_import.STEAMSHIM_sendRPC( req, size, self, rpc, syncIndex ); +} +int STEAMSHIM_sendEVT( void *packet, uint32_t size ) +{ + return steam_import.STEAMSHIM_sendEVT( packet, size ); +} +int STEAMSHIM_waitDispatchSync( uint32_t syncIndex ) +{ + return steam_import.STEAMSHIM_waitDispatchSync( syncIndex ); +} // wait on the dispatch loop +void STEAMSHIM_subscribeEvent( uint32_t id, void *self, STEAMSHIM_evt_handle evt ) +{ + return steam_import.STEAMSHIM_subscribeEvent( id, self, evt ); +} // wait on the dispatch loop +void STEAMSHIM_unsubscribeEvent( uint32_t id, STEAMSHIM_evt_handle evt ) +{ + return steam_import.STEAMSHIM_unsubscribeEvent( id, evt ); +} // wait on the dispatch loop +bool STEAMSHIM_active() +{ + return steam_import.STEAMSHIM_active(); +} // wait on the dispatch loop +const struct steam_workshop_mod_s *Steam_GetWorkshopMods() +{ + return steam_import.Steam_GetWorkshopMods(); +} +size_t Steam_GetWorkshopModCount() +{ + return steam_import.Steam_GetWorkshopModCount(); +} +void Steam_RefreshWorkshopMods() +{ + steam_import.Steam_RefreshWorkshopMods(); +} +struct steam_workshop_publish_result_s Steam_PublishLocalMod( int modIndex, const struct steam_workshop_publish_s *params ) +{ + return steam_import.Steam_PublishLocalMod( modIndex, params ); +} #endif #endif diff --git a/source/steamshim/src/parent/parent.cpp b/source/steamshim/src/parent/parent.cpp index df5d06d8f1..584c703fc9 100644 --- a/source/steamshim/src/parent/parent.cpp +++ b/source/steamshim/src/parent/parent.cpp @@ -25,6 +25,7 @@ freely, subject to the following restrictions: #include #include #include +#include #define DEBUGPIPE 1 #include "../os.h" #include "../steamshim_private.h" @@ -37,7 +38,7 @@ int GArgc = 0; char **GArgv = NULL; #define NUM_RPC_ASYNC_HANDLE 2048 -#define NUM_EVT_HANDLE 8 +#define NUM_EVT_HANDLE 8 struct steam_rpc_async_s { uint32_t token; @@ -54,9 +55,10 @@ struct event_subscriber_s { }; static std::atomic SyncToken; -static size_t currentSync; +static size_t currentSync = 0; static struct steam_rpc_async_s rpc_handles[NUM_RPC_ASYNC_HANDLE]; static struct event_subscriber_s evt_handles[STEAM_EVT_LEN]; +static std::vector packet_buf; std::mutex writeGuard; @@ -89,10 +91,10 @@ static bool setEnvironmentVars( PipeType pipeChildRead, PipeType pipeChildWrite void taskHeartbeat() { - while (STEAMSHIM_active()) { + while( STEAMSHIM_active() ) { struct steam_shim_common_s pkt; pkt.cmd = EVT_HEART_BEAT; - STEAMSHIM_sendEVT( &pkt, sizeof( struct steam_shim_common_s )); + STEAMSHIM_sendEVT( &pkt, sizeof( struct steam_shim_common_s ) ); std::this_thread::sleep_for( std::chrono::milliseconds( 1000 ) ); } } @@ -108,6 +110,8 @@ int STEAMSHIM_init( SteamshimOptions *options ) PipeType pipeChildWrite = NULLPIPE; ProcessType childPid; + packet_buf.resize( STEAM_PACKED_RESERVE_SIZE ); + if( options->runclient ) setEnvVar( "STEAMSHIM_RUNCLIENT", "1" ); if( options->runserver ) @@ -160,8 +164,10 @@ int STEAMSHIM_init( SteamshimOptions *options ) void STEAMSHIM_deinit( void ) { + packet_buf.clear(); + dbgprintf( "Child deinit.\n" ); - if( GPipeWrite != NULLPIPE ) + if( GPipeWrite != NULLPIPE ) closePipe( GPipeWrite ); if( GPipeRead != NULLPIPE ) @@ -214,11 +220,13 @@ int STEAMSHIM_waitDispatchSync( uint32_t syncIndex ) if( currentSync == syncIndex ) { return 0; // can't wait on dispatch if there is no RPC's staged } - static struct steam_packet_buf packet; static size_t cursor = 0; while( 1 ) { - assert( sizeof( struct steam_packet_buf ) == STEAM_PACKED_RESERVE_SIZE ); - int bytesRead = readPipe( GPipeRead, packet.buffer + cursor, STEAM_PACKED_RESERVE_SIZE - cursor ); + if( packet_buf.empty() ) { + packet_buf.resize( STEAM_PACKED_RESERVE_SIZE ); + } + + int bytesRead = readPipe( GPipeRead, packet_buf.data() + cursor, packet_buf.size() - cursor ); if( bytesRead > 0 ) { cursor += bytesRead; } else { @@ -226,33 +234,42 @@ int STEAMSHIM_waitDispatchSync( uint32_t syncIndex ) } continue_processing: - if( packet.size > STEAM_PACKED_RESERVE_SIZE - sizeof( uint32_t ) ) { - // the packet is larger then the reserved size + if( cursor < sizeof( uint32_t ) ) { + continue; + } + + struct steam_packet_buf *packet = reinterpret_cast( packet_buf.data() ); + const size_t packetlen = packet->size + sizeof( uint32_t ); + if( packetlen < sizeof( uint32_t ) ) { return -1; } - if( cursor < packet.size + sizeof( uint32_t ) ) { + if( packetlen > packet_buf.size() ) { + packet_buf.resize( packetlen ); + packet = reinterpret_cast( packet_buf.data() ); + } + + if( cursor < packetlen ) { continue; } - const bool rpcPacket = packet.common.cmd >= RPC_BEGIN && packet.common.cmd < RPC_END; + const bool rpcPacket = packet->common.cmd >= RPC_BEGIN && packet->common.cmd < RPC_END; if( rpcPacket ) { // assert(packet.rpc_payload.common.sync > currentSync); // rpc's are FIFO no out of order - struct steam_rpc_async_s *handle = rpc_handles + ( packet.rpc_payload.common.sync % NUM_RPC_ASYNC_HANDLE ); + struct steam_rpc_async_s *handle = rpc_handles + ( packet->rpc_payload.common.sync % NUM_RPC_ASYNC_HANDLE ); if( handle->cb ) { - handle->cb( handle->self, &packet.rpc_payload ); + handle->cb( handle->self, &packet->rpc_payload ); } - currentSync = packet.rpc_payload.common.sync; - } else if( packet.common.cmd >= EVT_BEGIN && packet.common.cmd < EVT_END ) { - struct event_subscriber_s *handle = evt_handles + ( packet.common.cmd - EVT_BEGIN ); + currentSync = packet->rpc_payload.common.sync; + } else if( packet->common.cmd >= EVT_BEGIN && packet->common.cmd < EVT_END ) { + struct event_subscriber_s *handle = evt_handles + ( packet->common.cmd - EVT_BEGIN ); for( size_t i = 0; i < handle->numSubscribers; i++ ) { - handle->handles[i].cb( handle->handles[i].self, &packet.evt_payload ); + handle->handles[i].cb( handle->handles[i].self, &packet->evt_payload ); } } - if( cursor > packet.size + sizeof( uint32_t ) ) { - const size_t packetlen = packet.size + sizeof( uint32_t ); + if( cursor > packetlen ) { const size_t remainingLen = cursor - packetlen; - memmove( packet.buffer, packet.buffer + packet.size + sizeof( uint32_t ), remainingLen ); + memmove( packet_buf.data(), packet_buf.data() + packetlen, remainingLen ); cursor = remainingLen; goto continue_processing; } else { diff --git a/source/steamshim/src/steamshim_types.h b/source/steamshim/src/steamshim_types.h index 7c9ccd275c..15e032668c 100644 --- a/source/steamshim/src/steamshim_types.h +++ b/source/steamshim/src/steamshim_types.h @@ -224,6 +224,10 @@ enum steam_cmd_s { RPC_WORKSHOP_ITEM_SET_TAGS, RPC_WORKSHOP_ITEM_SET_PREVIEW, + RPC_WORKSHOP_REFRESH_SUBSCRIBED_ITEMS, + RPC_WORKSHOP_INSTALLED_INFO, + RPC_WORKSHOP_QUERY_ITEM_DETAILS, + RPC_SRV_P2P_LISTEN, RPC_SRV_P2P_ACCEPT_CONNECTION, RPC_SRV_P2P_DISCONNECT, @@ -252,6 +256,12 @@ enum steam_cmd_s { EVT_GAME_JOIN, EVT_P2P_CONNECTION_CHANGED, + EVT_WORKSHOP_REFRESH_SUBSCRIBE_ITEMS, + EVT_WORKSHOP_ITEM_SUBSCRIBED, + EVT_WORKSHOP_ITEM_UNSUBSCRIBED, + EVT_WORKSHOP_ITEM_INSTALLED, + EVT_WORKSHOP_DETAIL, + EVT_SRV_P2P_CONNECTION_CHANGED, EVT_END, @@ -283,6 +293,32 @@ struct steam_id_rpc_s { uint64_t id; }; +struct steam_workshop_list_rpc_s { + STEAM_RPC_SHIM_COMMON() + uint32_t cookie; +}; + +struct steam_workshop_install_info_s { + STEAM_RPC_SHIM_COMMON() + uint64_t workshop_id; + bool success; + uint64_t size_on_disk; + uint32_t time_stamp; + char folder[]; +}; + + +struct steam_workshop_req_rpcs_s { + STEAM_RPC_SHIM_COMMON() + uint32_t num_ids; + uint64_t workshop_ids[]; +}; + +struct steam_workshop_req_rpc_s { + STEAM_RPC_SHIM_COMMON() + uint64_t workshop_id; +}; + struct buffer_workshop_rpc_s { STEAM_RPC_SHIM_COMMON() STEAM_UGCUpdateHandle_t handle; @@ -436,14 +472,14 @@ STEAM_RPC_REQ( tags ) { STEAM_RPC_SHIM_COMMON() STEAM_UGCUpdateHandle_t handle; - uint8_t num_tags; + uint32_t num_tags; char buffer[]; }; STEAM_RPC_REQ( item_visiblity ) { STEAM_RPC_SHIM_COMMON() - STEAM_PublishedFileId_t itemID; + STEAM_UGCUpdateHandle_t handle; enum steam_visibility_e visibility; // ERemoteStoragePublishedFileVisibility }; @@ -453,6 +489,12 @@ STEAM_RPC_RECV( success ) bool success; }; +STEAM_RPC_RECV( workshop_installed_maps ) +{ + STEAM_RPC_SHIM_COMMON() + char buffer[]; +}; + STEAM_RPC_RECV( recv_messages ) { STEAM_RPC_SHIM_COMMON() @@ -498,12 +540,19 @@ struct steam_rpc_pkt_s { struct buffer_rpc_s rich_presence; struct create_item_recv_s create_item_recv; + struct submit_item_result_recv_s submit_item_result; + struct steam_workshop_install_info_s workshop_install_info; + struct workshop_installed_maps_recv_s workshop_installed_maps_recv; struct auth_session_ticket_recv_s auth_session; + struct steam_workshop_list_rpc_s stream_workshop_refresh; struct p2p_accept_connect_req_s p2p_accept_connection_req; struct p2p_accept_connection_recv_s p2p_accept_connection_recv; + struct steam_workshop_req_rpc_s steam_workshop_item; + struct steam_workshop_req_rpcs_s steam_workshop_items; + struct p2p_connect_req_s p2p_connect; struct p2p_disconnect_req_s p2p_disconnect; struct p2p_connect_req_s p2p_listen; @@ -547,6 +596,45 @@ struct steam_rpc_pkt_s { }; }; +STEAM_EVT(workshop_item_details) { + STEAM_SHIM_COMMON() + uint64_t workshop_id; + bool success; + uint32_t result; + uint64_t owner_id; + uint32_t creator_app_id; + uint32_t consumer_app_id; + uint32_t file_type; + uint32_t visibility; + uint32_t time_created; + uint32_t time_updated; + uint32_t votes_up; + uint32_t votes_down; + uint32_t num_children; + uint32_t item_state; + float score; + uint32_t title_len; + uint32_t description_len; + uint32_t tags_len; + uint32_t preview_url_len; + char buffer[]; +}; + + +STEAM_EVT(workshop_refresh_items) { + STEAM_SHIM_COMMON() + uint32_t cookie; // marker cookie for the batch + uint16_t num_ids; + uint64_t ids[]; +}; + +STEAM_EVT( workshop_item ) +{ + STEAM_SHIM_COMMON() + uint32_t app_id; + uint64_t workshop_id; +}; + STEAM_EVT( policy_response ) { STEAM_SHIM_COMMON() @@ -613,22 +701,22 @@ struct steam_evt_pkt_s { struct p2p_connection_status_changed_evt_s p2p_connection_status_changed; struct p2p_net_connection_changed_evt_s p2p_net_connection_changed; struct policy_response_evt_s policy_response; + struct workshop_refresh_items_evt_s workshop_refresh_items; + struct workshop_item_evt_s workshop_item; + struct workshop_item_details_evt_s workshop_item_details; }; }; #define STEAM_PACKED_RESERVE_SIZE ( 16384 ) struct steam_packet_buf { - union { - struct { - uint32_t size; - union { - struct steam_shim_common_s common; - struct steam_rpc_shim_common_s rpc_common; - struct steam_rpc_pkt_s rpc_payload; - struct steam_evt_pkt_s evt_payload; - }; + struct { + uint32_t size; + union { + struct steam_shim_common_s common; + struct steam_rpc_shim_common_s rpc_common; + struct steam_rpc_pkt_s rpc_payload; + struct steam_evt_pkt_s evt_payload; }; - uint8_t buffer[STEAM_PACKED_RESERVE_SIZE]; }; }; diff --git a/source/ui/as/as_bind_game.cpp b/source/ui/as/as_bind_game.cpp index ea6b6f1a5e..698ead154a 100644 --- a/source/ui/as/as_bind_game.cpp +++ b/source/ui/as/as_bind_game.cpp @@ -26,6 +26,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include "as/asui.h" #include "as/asui_local.h" #include "../../steamshim/src/mod_steam.h" +#include "../../extern/stb/stb_ds.h" namespace ASUI { @@ -111,6 +112,70 @@ static void Game_SteamOpenProfile( Game *game, asstring_t* steamid ) STEAMSHIM_sendRPC( &request, sizeof( struct steam_id_rpc_s ), NULL, NULL, NULL); } +static bool Game_SteamAvaliable( Game *game ) +{ + return STEAMSHIM_active(); +} + +static void Game_WorkshopRefresh( Game *game ) +{ + Steam_RefreshWorkshopMods(); +} + +// Returns one entry per local (publishable) mod: "name\tindex\n" +// index is the position in Steam_GetWorkshopMods() so workshopSubmitMap can look it up. +static asstring_t *Game_WorkshopLocalMods( Game *game ) +{ + const struct steam_workshop_mod_s *mods = Steam_GetWorkshopMods(); + size_t count = Steam_GetWorkshopModCount(); + std::ostringstream stream; + for( size_t i = 0; i < count; i++ ) { + if( !mods[i].is_local ) + continue; + const char *name = mods[i].name ? mods[i].name : ( mods[i].path ? mods[i].path : "" ); + stream << name << "\t" << i << "\n"; + } + return ASSTR( stream.str() ); +} + +static asstring_t *Game_WorkshopSubmitMap( Game *game, int modIndex, const asstring_t &title, + const asstring_t &description, const asstring_t &tags, + const asstring_t &visibility, const asstring_t &changeNote ) +{ + + struct steam_workshop_publish_s params; + params.title = title.buffer; + params.description = description.buffer; + params.tags = tags.buffer; + params.visibility = visibility.buffer; + params.change_note = changeNote.buffer; + + struct steam_workshop_publish_result_s result = Steam_PublishLocalMod( modIndex, ¶ms ); + + switch( result.res ) { + case STEAM_PUBLISH_OK: { + std::ostringstream stream; + stream << "Workshop item " << ( result.is_new_item ? "published" : "updated" ) + << " (" << result.file_id << ")."; + return ASSTR( stream.str() ); + } + case STEAM_PUBLISH_ERR_STEAM_UNAVAILABLE: return ASSTR( "Steam is not available." ); + case STEAM_PUBLISH_ERR_TITLE_REQUIRED: return ASSTR( "Title is required." ); + case STEAM_PUBLISH_ERR_INVALID_MOD: return ASSTR( "Invalid mod selection." ); + case STEAM_PUBLISH_ERR_TOO_MANY_TAGS: return ASSTR( "Too many tags." ); + case STEAM_PUBLISH_ERR_CREATE_FAILED: return ASSTR( "Creating workshop item failed." ); + case STEAM_PUBLISH_ERR_UPDATE_FAILED: return ASSTR( "Starting workshop update failed." ); + case STEAM_PUBLISH_ERR_SET_TITLE: return ASSTR( "Setting workshop title failed." ); + case STEAM_PUBLISH_ERR_SET_DESCRIPTION: return ASSTR( "Setting workshop description failed." ); + case STEAM_PUBLISH_ERR_SET_CONTENT: return ASSTR( "Setting workshop content directory failed." ); + case STEAM_PUBLISH_ERR_SET_PREVIEW: return ASSTR( "Setting workshop preview image failed." ); + case STEAM_PUBLISH_ERR_SET_VISIBILITY: return ASSTR( "Setting workshop visibility failed." ); + case STEAM_PUBLISH_ERR_SET_TAGS: return ASSTR( "Setting workshop tags failed." ); + case STEAM_PUBLISH_ERR_SUBMIT_FAILED: return ASSTR( "Submitting workshop item failed." ); + default: return ASSTR( "Unknown error." ); + } +} + static int Game_ClientState( Game *game ) { return UI_Main::Get()->getRefreshState().clientState; @@ -215,6 +280,12 @@ void BindGame( ASInterface *as ) .constmethod( Game_Cvar, "cvar", true ) .constmethod( Game_SteamOpenProfile, "steamopenprofile", true ) + .constmethod( Game_SteamAvaliable, "steamAvaliable", true ) + .constmethod( Game_WorkshopRefresh, "workshopRefresh", true ) + .constmethod( Game_WorkshopLocalMods, "workshopLocalMods", true ) + .method2( Game_WorkshopSubmitMap, + "String @workshopSubmitMap( int modIndex, const String &in title, const String &in description, const String &in tags, const String &in visibility, const String &in changeNote ) const", + true ) .constmethod( Game_PlayerNum, "get_playerNum", true ) diff --git a/source/ui/datasources/ui_workshop_datasource.cpp b/source/ui/datasources/ui_workshop_datasource.cpp new file mode 100644 index 0000000000..1d4e8a97fa --- /dev/null +++ b/source/ui/datasources/ui_workshop_datasource.cpp @@ -0,0 +1,116 @@ +#include "ui_precompiled.h" +#include "kernel/ui_common.h" +#include "datasources/ui_workshop_datasource.h" +#include "../../steamshim/src/mod_steam.h" +#include "../../steamshim/src/steamshim_types.h" + +#include + +#define WORKSHOP_SOURCE "workshop" +#define TABLE_INSTALLED "installed" +#define COL_TITLE "title" +#define COL_WORKSHOP_ID "workshop_id" +#define COL_DISPLAY_NAME "display_name" +#define COL_IS_LOCAL "is_local" + +namespace WSWUI +{ + +WorkshopDataSource::WorkshopDataSource() : + Rocket::Controls::DataSource( WORKSHOP_SOURCE ) +{ + STEAMSHIM_subscribeEvent( EVT_WORKSHOP_DETAIL, this, OnWorkshopDetail ); + STEAMSHIM_subscribeEvent( EVT_WORKSHOP_ITEM_INSTALLED, this, OnWorkshopItemInstalled ); + STEAMSHIM_subscribeEvent( EVT_WORKSHOP_ITEM_UNSUBSCRIBED, this, OnWorkshopItemUnsubscribed ); + UpdateMods(); +} + +WorkshopDataSource::~WorkshopDataSource() +{ + STEAMSHIM_unsubscribeEvent( EVT_WORKSHOP_DETAIL, OnWorkshopDetail ); + STEAMSHIM_unsubscribeEvent( EVT_WORKSHOP_ITEM_INSTALLED, OnWorkshopItemInstalled ); + STEAMSHIM_unsubscribeEvent( EVT_WORKSHOP_ITEM_UNSUBSCRIBED, OnWorkshopItemUnsubscribed ); +} + +void WorkshopDataSource::Refresh() +{ + Steam_RefreshWorkshopMods(); +} + +void WorkshopDataSource::UpdateMods() +{ + installedMods.clear(); + + if( !STEAMSHIM_active() ) { + NotifyRowChange( TABLE_INSTALLED ); + return; + } + + const struct steam_workshop_mod_s *mods = Steam_GetWorkshopMods(); + size_t count = Steam_GetWorkshopModCount(); + + for( size_t i = 0; i < count; i++ ) { + if( !mods[i].path ) { + continue; + } + + InstalledMod mod; + mod.title = mods[i].title ? mods[i].title : ""; + mod.name = mods[i].name ? mods[i].name : ""; + mod.is_local = mods[i].is_local; + + std::ostringstream oss; + oss << mods[i].workshop_id; + mod.workshop_id = oss.str(); + + installedMods.push_back( mod ); + } + + NotifyRowChange( TABLE_INSTALLED ); +} + +void WorkshopDataSource::GetRow( Rocket::Core::StringList &row, const Rocket::Core::String &table, int row_index, const Rocket::Core::StringList &cols ) +{ + if( table != TABLE_INSTALLED ) + return; + if( row_index < 0 || (size_t)row_index >= installedMods.size() ) + return; + + const InstalledMod &mod = installedMods[row_index]; + for( size_t i = 0; i < cols.size(); i++ ) { + if( cols[i] == COL_TITLE ) + row.push_back( mod.title.c_str() ); + else if( cols[i] == COL_WORKSHOP_ID ) + row.push_back( mod.workshop_id.c_str() ); + else if( cols[i] == COL_DISPLAY_NAME ) + row.push_back( mod.is_local && !mod.name.empty() ? mod.name.c_str() : mod.title.c_str() ); + else if( cols[i] == COL_IS_LOCAL ) + row.push_back( mod.is_local ? "1" : "0" ); + else + row.push_back( "" ); + } +} + +int WorkshopDataSource::GetNumRows( const Rocket::Core::String &table ) +{ + if( table == TABLE_INSTALLED ) + return (int)installedMods.size(); + return 0; +} + +void WorkshopDataSource::OnWorkshopDetail( void *self, struct steam_evt_pkt_s *pkt ) +{ + static_cast( self )->UpdateMods(); +} + +void WorkshopDataSource::OnWorkshopItemInstalled( void *self, struct steam_evt_pkt_s *pkt ) +{ + static_cast( self )->UpdateMods(); +} + +void WorkshopDataSource::OnWorkshopItemUnsubscribed( void *self, struct steam_evt_pkt_s *pkt ) +{ + static_cast( self )->UpdateMods(); +} + +} diff --git a/source/ui/datasources/ui_workshop_datasource.h b/source/ui/datasources/ui_workshop_datasource.h new file mode 100644 index 0000000000..69c61f154b --- /dev/null +++ b/source/ui/datasources/ui_workshop_datasource.h @@ -0,0 +1,41 @@ +#ifndef __UI_WORKSHOP_DATASOURCE_H__ +#define __UI_WORKSHOP_DATASOURCE_H__ + +#include +#include +#include + +struct steam_evt_pkt_s; + +namespace WSWUI +{ + class WorkshopDataSource : public Rocket::Controls::DataSource + { + public: + WorkshopDataSource(); + ~WorkshopDataSource(); + + // Triggers a Steam refresh; datasource updates reactively via events. + void Refresh(); + + void GetRow( Rocket::Core::StringList &row, const Rocket::Core::String &table, int row_index, const Rocket::Core::StringList &cols ) override; + int GetNumRows( const Rocket::Core::String &table ) override; + + private: + struct InstalledMod { + std::string title; + std::string name; + std::string workshop_id; + bool is_local = false; + }; + std::vector installedMods; + + void UpdateMods(); + + static void OnWorkshopDetail( void *self, struct steam_evt_pkt_s *pkt ); + static void OnWorkshopItemInstalled( void *self, struct steam_evt_pkt_s *pkt ); + static void OnWorkshopItemUnsubscribed( void *self, struct steam_evt_pkt_s *pkt ); + }; +} + +#endif // __UI_WORKSHOP_DATASOURCE_H__ diff --git a/source/ui/kernel/ui_main.cpp b/source/ui/kernel/ui_main.cpp index 5f75ba74c8..325a58c763 100644 --- a/source/ui/kernel/ui_main.cpp +++ b/source/ui/kernel/ui_main.cpp @@ -6,6 +6,7 @@ */ #include "datasources/ui_blockedplayers_datasource.h" +#include "datasources/ui_workshop_datasource.h" #include "ui_precompiled.h" #include "kernel/ui_common.h" #include "kernel/ui_main.h" @@ -393,6 +394,7 @@ void UI_Main::createDataSources( void ) tvchannels = __new__( TVChannelsDataSource )(); gameajax = __new__( GameAjaxDataSource )(); blockedplayers = __new__( BlockedPlayersDataSource )(); + workshop = __new__( WorkshopDataSource )(); playerModels = __new__( ModelsDataSource )(); vidProfiles = __new__( ProfilesDataSource )(); } @@ -409,6 +411,7 @@ void UI_Main::destroyDataSources( void ) __SAFE_DELETE_NULLIFY( tvchannels ); __SAFE_DELETE_NULLIFY( gameajax ); __SAFE_DELETE_NULLIFY( blockedplayers ); + __SAFE_DELETE_NULLIFY( workshop ); __SAFE_DELETE_NULLIFY( playerModels ); __SAFE_DELETE_NULLIFY( vidProfiles ); } diff --git a/source/ui/kernel/ui_main.h b/source/ui/kernel/ui_main.h index ffeb733c99..bee1867fa1 100644 --- a/source/ui/kernel/ui_main.h +++ b/source/ui/kernel/ui_main.h @@ -43,6 +43,7 @@ class ModelsDataSource; class TVChannelsDataSource; class GameAjaxDataSource; class BlockedPlayersDataSource; +class WorkshopDataSource; class LevelShotFormatter; class DatetimeFormatter; @@ -208,6 +209,7 @@ class UI_Main TVChannelsDataSource *tvchannels; GameAjaxDataSource *gameajax; BlockedPlayersDataSource *blockedplayers; + WorkshopDataSource *workshop; UI_Navigation navigations[UI_NUM_CONTEXTS]; Rocket::Core::String quickMenuURL; From 4c0d9b5403e6efc019e81c1be7acd7c050fa0889 Mon Sep 17 00:00:00 2001 From: Michael Pollind Date: Mon, 27 Apr 2026 23:10:03 -0700 Subject: [PATCH 4/6] feat: update mod path logic Signed-off-by: Michael Pollind --- .../data0_21pure/ui/porkui/css/workshop.rcss | 9 +- assets/data0_21pure/ui/porkui/workshop.rml | 25 ++-- source/client/cl_main.c | 2 +- source/qcommon/files.c | 129 +++--------------- source/qcommon/mod_fs.h | 13 +- source/qcommon/qcommon.h | 9 +- source/qcommon/steam.c | 54 +++++--- source/steamshim/src/mod_steam.h | 4 + source/ui/as/as_bind_game.cpp | 2 +- .../ui/datasources/ui_workshop_datasource.cpp | 11 +- .../ui/datasources/ui_workshop_datasource.h | 1 + 11 files changed, 104 insertions(+), 155 deletions(-) diff --git a/assets/data0_21pure/ui/porkui/css/workshop.rcss b/assets/data0_21pure/ui/porkui/css/workshop.rcss index 4e51748369..5221057524 100644 --- a/assets/data0_21pure/ui/porkui/css/workshop.rcss +++ b/assets/data0_21pure/ui/porkui/css/workshop.rcss @@ -54,11 +54,8 @@ color: #b9c7d6; } -.workshop-input { - max-width: 44%; - width: 44%; -} -.workshop-select { - width: 44%; +.required { + color: #eb8c8a; + font-weight: bold; } diff --git a/assets/data0_21pure/ui/porkui/workshop.rml b/assets/data0_21pure/ui/porkui/workshop.rml index 6b937ff1a9..c0cb4d7e8d 100644 --- a/assets/data0_21pure/ui/porkui/workshop.rml +++ b/assets/data0_21pure/ui/porkui/workshop.rml @@ -68,6 +68,14 @@ } int modIndex = modSelect.value.toInt(); + if( modIndex == -1 ) { + notificationPopup( 'Please select a mod folder.', true ); + return; + } + if( titleInput.value.trim() == '' ) { + notificationPopup( 'Title is required.', true ); + return; + } String result = game.workshopSubmitMap( modIndex, @@ -99,7 +107,7 @@ Mod Workshop ID - Local + Uploaded @@ -115,26 +123,27 @@

Publish a local mod to Steam Workshop. Place your mod folder in mods/ and add a preview.png for a thumbnail.


Mod folder
- +
-
Title
- +
Title *
+
Description
- +
Tags
- +
+
Visibility
-
Change note
- +

diff --git a/source/client/cl_main.c b/source/client/cl_main.c index c5d6e36566..b9357b47cf 100644 --- a/source/client/cl_main.c +++ b/source/client/cl_main.c @@ -2308,7 +2308,7 @@ static void CL_InitLocal( void ) if ( !CL_IsNameValid(name->string) ){ if ( STEAMSHIM_active() ){ struct steam_rpc_shim_common_s request; - request.cmd = RPC_REQUEST_STEAM_ID; + request.cmd = RPC_PERSONA_NAME; STEAMSHIM_sendRPC( &request, sizeof( struct steam_rpc_shim_common_s ), name, CL_RPC_cb_persona, &syncIndex ); } else { Cvar_Set( name->name, CL_RandomName() ); diff --git a/source/qcommon/files.c b/source/qcommon/files.c index 25a966ea59..ed64b438b6 100644 --- a/source/qcommon/files.c +++ b/source/qcommon/files.c @@ -153,7 +153,6 @@ typedef struct searchpath_s pack_t *pack; struct searchpath_s *base; // parent basepath struct searchpath_s *next; - struct searchpath_s *group_next; // will be grouping the search paths together bool append_basegame; } searchpath_t; @@ -163,11 +162,6 @@ typedef struct searchpath_t *searchPath; } searchfile_t; -struct steam_fs_mod { - uint64_t mod_id; - struct searchpath_s* search_path; -}; - static searchfile_t *fs_searchfiles; static int fs_numsearchfiles; static int fs_cursearchfiles; @@ -188,8 +182,7 @@ static searchpath_t *fs_root_searchpath; // base path directory static searchpath_t *fs_write_searchpath; // write directory static searchpath_t *fs_downloads_searchpath; // write directory for downloads from game servers -static searchpath_t *fs_base_steam_paths = NULL; -static struct steam_fs_mod *fs_mod_paths = NULL; +static searchpath_t *fs_base_mod_paths = NULL; static mempool_t *fs_mempool; @@ -4137,26 +4130,6 @@ static searchpath_t* FS_AddSearchPath(searchpath_t* base, const char *path, bool return newpath; } -static void FS_RemoveSearchPath(searchpath_t *search_path) { - assert(search_path); - if(search_path->base) { - search_path->base->next = search_path->next; - } - else { - fs_searchpaths = search_path->next; - } - if(search_path->next) { - search_path->next->base = search_path->base; - } - - if(search_path->group_next) { - FS_RemoveSearchPath(search_path->group_next); - } - - FS_Free( search_path->path ); - FS_Free( search_path ); -} - static void FS_AddBasePath( const char *path, bool append_basegame ) { searchpath_t *newpath; @@ -4173,97 +4146,34 @@ void FS_AddExtraPK3Directory( const char *path ) FS_AddBasePath( path, false ); } - -void FS_UnRegisterSteamModPath( uint64_t mod ) +void FS_UnRegisterModPath( searchpath_t *search_path ) { QMutex_Lock( fs_searchpaths_mutex ); - - for(size_t i = 0; i < stbds_arrlen(fs_mod_paths); i++) { - if(fs_mod_paths[i].mod_id == mod) { - FS_RemoveSearchPath(fs_mod_paths[i].search_path); - stbds_arrdel(fs_mod_paths, i); + searchpath_t* last_path = fs_basepaths; + for( searchpath_t *p = fs_basepaths; p != NULL; p = p->next ) { + if(p == search_path) { + last_path->next = p->next; + FS_Free( p->path ); + FS_Free( p); break; } + last_path = p; } - QMutex_Unlock( fs_searchpaths_mutex ); } -void FS_RegisterSteamModPath( uint64_t mod, const char *path ) +searchpath_t* FS_RegisterModPath( const char *path ) { - int i, totalpaks; - char **paknames; - pack_t *pak; - searchpath_t *search, *prev, *next; - QMutex_Lock( fs_searchpaths_mutex ); - for(size_t i = 0; i < stbds_arrlen(fs_mod_paths); i++) { - if(fs_mod_paths[i].mod_id == mod) { - QMutex_Unlock( fs_searchpaths_mutex ); - return; - } - } - - struct steam_fs_mod steam_mod = { 0 }; - steam_mod.mod_id = mod; - steam_mod.search_path = FS_AddSearchPath(fs_base_steam_paths, path, false); - searchpath_t* head = steam_mod.search_path; - - totalpaks = 0; - if( ( paknames = FS_GamePathPaks( steam_mod.search_path, "", &totalpaks ) ) != 0 ) - { - for( i = 0; i < totalpaks; i++ ) - { - searchpath_t *compare = fs_searchpaths; - while( compare ) - { - if( compare->pack ) - { - int cmp = Q_stricmp( COM_FileBase( compare->pack->filename ), COM_FileBase( paknames[i] ) ); - if( !cmp ) - { - if( !Q_stricmp( compare->pack->filename, paknames[i] ) ) - goto freename; - } - } - compare = compare->next; - } - - if( !FS_FindPackFilePos( paknames[i], NULL, NULL, NULL ) ) - continue; - - pak = ( pack_t* )FS_Malloc( sizeof( *pak ) ); - pak->filename = FS_CopyString( paknames[i] ); - pak->deferred_pack = NULL; - pak->deferred_load = true; - - if( FS_FindPackFilePos( paknames[i], &search, &prev, &next ) ) - { - search->base = prev; - search->pack = pak; - if( !prev ) - { - search->next = fs_searchpaths; - fs_searchpaths = search; - } - else - { - prev->next = search; - search->next = next; - } - if( search->next ) { - search->next->base = search; - } - head->group_next = search; - head = head->group_next; - } -freename: - Mem_ZoneFree( paknames[i] ); - } - Mem_ZoneFree( paknames ); - } - stbds_arrpush(fs_mod_paths, steam_mod); + searchpath_t *newpath = ( searchpath_t* )FS_Malloc( sizeof( searchpath_t ) ); + newpath->path = FS_CopyString( path ); + newpath->pack = NULL; + newpath->next = fs_base_mod_paths->next; + newpath->append_basegame = false; + fs_base_mod_paths->next = newpath; + COM_SanitizeFilePath( newpath->path ); QMutex_Unlock( fs_searchpaths_mutex ); + return newpath; } /* @@ -4520,7 +4430,7 @@ void FS_Init( void ) FS_AddBasePath( downloadsdir, true ); fs_downloads_searchpath = fs_basepaths; } - fs_base_steam_paths = fs_basepaths; + fs_base_mod_paths = fs_basepaths; if( fs_cdpath->string[0] ) FS_AddBasePath( fs_cdpath->string, true ); @@ -4644,7 +4554,6 @@ void FS_Shutdown( void ) Sys_VFS_Shutdown(); - stbds_arrfree(fs_mod_paths); Mem_FreePool( &fs_mempool ); QMutex_Destroy( &fs_fh_mutex ); diff --git a/source/qcommon/mod_fs.h b/source/qcommon/mod_fs.h index 4d5d080968..a362e6486d 100644 --- a/source/qcommon/mod_fs.h +++ b/source/qcommon/mod_fs.h @@ -39,8 +39,9 @@ DECLARE_TYPEDEF_METHOD( void, FS_CreateAbsolutePath, const char *path ); DECLARE_TYPEDEF_METHOD( const char *, FS_AbsoluteNameForFile, const char *filename ); DECLARE_TYPEDEF_METHOD( const char *, FS_AbsoluteNameForBaseFile, const char *filename ); DECLARE_TYPEDEF_METHOD( void, FS_AddExtraPK3Directory, const char *path ); -DECLARE_TYPEDEF_METHOD( void, FS_RegisterSteamModPath, uint64_t mod, const char *path ); -DECLARE_TYPEDEF_METHOD( void, FS_UnRegisterSteamModPath, uint64_t mod ); +struct searchpath_s; +DECLARE_TYPEDEF_METHOD( struct searchpath_s *, FS_RegisterModPath, const char *path ); +DECLARE_TYPEDEF_METHOD( void, FS_UnRegisterModPath, struct searchpath_s *search_path ); DECLARE_TYPEDEF_METHOD( int, FS_FOpenFile, const char *filename, int *filenum, int mode ); DECLARE_TYPEDEF_METHOD( int, FS_FOpenFileGroup, const char *filename, int *filenum, int mode, group_handle_t *group ); @@ -126,8 +127,8 @@ struct fs_import_s { FS_AbsoluteNameForFileFn FS_AbsoluteNameForFile; FS_AbsoluteNameForBaseFileFn FS_AbsoluteNameForBaseFile; FS_AddExtraPK3DirectoryFn FS_AddExtraPK3Directory; - FS_RegisterSteamModPathFn FS_RegisterSteamModPath; - FS_UnRegisterSteamModPathFn FS_UnRegisterSteamModPath; + FS_RegisterModPathFn FS_RegisterModPath; + FS_UnRegisterModPathFn FS_UnRegisterModPath; FS_LoadFileExtFn FS_LoadFileExt; FS_LoadBaseFileExtFn FS_LoadBaseFileExt; FS_FreeFileFn FS_FreeFile; @@ -202,8 +203,8 @@ void FS_CreateAbsolutePath( const char *path ){ fs_import.FS_CreateAbsolutePath( const char * FS_AbsoluteNameForFile(const char *filename ){ return fs_import.FS_AbsoluteNameForFile(filename);} const char * FS_AbsoluteNameForBaseFile(const char *filename ){ return fs_import.FS_AbsoluteNameForBaseFile(filename);} void FS_AddExtraPK3Directory(const char *path ){ fs_import.FS_AddExtraPK3Directory(path);} -void FS_RegisterSteamModPath( uint64_t mod, const char *path ) { fs_import.FS_RegisterSteamModPath( mod, path ); } -void FS_UnRegisterSteamModPath( uint64_t mod ) { fs_import.FS_UnRegisterSteamModPath( mod ); } +struct searchpath_s *FS_RegisterModPath( const char *path ) { return fs_import.FS_RegisterModPath( path ); } +void FS_UnRegisterModPath( struct searchpath_s *search_path ) { fs_import.FS_UnRegisterModPath( search_path ); } int FS_FOpenFile(const char *filename, int *filenum, int mode ){ return fs_import.FS_FOpenFile(filename, filenum, mode);} int FS_FOpenFileGroup(const char *filename, int *filenum, int mode, group_handle_t *group ){ return fs_import.FS_FOpenFileGroup(filename, filenum, mode, group);} int FS_FOpenBaseFile(const char *filename, int *filenum, int mode ){ return fs_import.FS_FOpenBaseFile(filename, filenum, mode);} diff --git a/source/qcommon/qcommon.h b/source/qcommon/qcommon.h index 077d2cc8fc..0646e5561b 100644 --- a/source/qcommon/qcommon.h +++ b/source/qcommon/qcommon.h @@ -737,8 +737,8 @@ static const struct fs_import_s default_fs_imports_s = { .FS_AbsoluteNameForFile = FS_AbsoluteNameForFile, .FS_AbsoluteNameForBaseFile = FS_AbsoluteNameForBaseFile, .FS_AddExtraPK3Directory = FS_AddExtraPK3Directory, - .FS_RegisterSteamModPath = FS_RegisterSteamModPath, - .FS_UnRegisterSteamModPath = FS_UnRegisterSteamModPath, + .FS_RegisterModPath = FS_RegisterModPath, + .FS_UnRegisterModPath = FS_UnRegisterModPath, .FS_LoadFileExt = FS_LoadFileExt, .FS_LoadBaseFileExt = FS_LoadBaseFileExt, .FS_FreeFile = FS_FreeFile, @@ -783,8 +783,9 @@ bool FS_SetGameDirectory( const char *dir, bool force ); int FS_GetGameDirectoryList( char *buf, size_t bufsize ); int FS_GetExplicitPurePakList( char ***paknames ); bool FS_IsExplicitPurePak( const char *pakname, bool *wrongver ); -void FS_RegisterSteamModPath( uint64_t mod, const char *path ); -void FS_UnRegisterSteamModPath( uint64_t mod ); +struct searchpath_s *FS_RegisterModPath( const char *path ); +void FS_UnRegisterModPath( struct searchpath_s *search_path ); + /** * Maps an existing file on disk for reading. diff --git a/source/qcommon/steam.c b/source/qcommon/steam.c index 6315d6976a..027adf50c1 100644 --- a/source/qcommon/steam.c +++ b/source/qcommon/steam.c @@ -62,10 +62,12 @@ static void __RemoveWorkshopMod( uint64_t id ) for( size_t i = 0; i < arrlen( steam_workshop_mods ); i++ ) { if( steam_workshop_mods[i].workshop_id != id ) continue; - FS_UnRegisterSteamModPath( steam_workshop_mods[i].workshop_id ); - if( steam_workshop_mods[i].path ) { - free( (void *)steam_workshop_mods[i].path ); + if( steam_workshop_mods[i].fs_search_path ) { + FS_UnRegisterModPath( steam_workshop_mods[i].fs_search_path ); + steam_workshop_mods[i].fs_search_path = NULL; } + free( (void *)steam_workshop_mods[i].local_path ); + free( (void *)steam_workshop_mods[i].path ); free( (void *)steam_workshop_mods[i].title ); free( (void *)steam_workshop_mods[i].description ); free( (void *)steam_workshop_mods[i].tags ); @@ -75,6 +77,17 @@ static void __RemoveWorkshopMod( uint64_t id ) } } +static void __RefreshModPath( struct steam_workshop_mod_s *mod ) { + if( mod->fs_search_path ) { + FS_UnRegisterModPath( mod->fs_search_path ); + mod->fs_search_path = NULL; + } + const char *active_path = mod->local_path ? mod->local_path : mod->path; + if( active_path ) { + mod->fs_search_path = FS_RegisterModPath( active_path ); + } +} + static void Steam_WorkshopInstallInfoCallback( void *self, struct steam_rpc_pkt_s *pkt ) { struct steam_workshop_install_info_s *info = &pkt->workshop_install_info; @@ -84,12 +97,11 @@ static void Steam_WorkshopInstallInfoCallback( void *self, struct steam_rpc_pkt_ } if( mod->path ) { - FS_UnRegisterSteamModPath( mod->workshop_id ); free( (void *)mod->path ); } - mod->path = Steam_CopyString( pkt->workshop_install_info.folder ); - FS_RegisterSteamModPath( mod->workshop_id, mod->path ); + mod->is_remote = true; + __RefreshModPath( mod ); } static void Steam_EVT_WorkshopRefreshItems( void *self, struct steam_evt_pkt_s *pkt ) @@ -235,16 +247,21 @@ static void Steam_ScanLocalMods( void ) fclose( f ); } - if(workshop_id > 0) { - stbds_arrpush(workshop_ids, workshop_id); + struct steam_workshop_mod_s *mod; + if( workshop_id > 0 ) { + stbds_arrpush( workshop_ids, workshop_id ); + mod = __UpsertWorkshopMod( workshop_id ); + } else { + struct steam_workshop_mod_s new_mod = { 0 }; + arrput( steam_workshop_mods, new_mod ); + mod = &steam_workshop_mods[arrlen( steam_workshop_mods ) - 1]; } - struct steam_workshop_mod_s mod = { 0 }; - mod.is_local = true; - mod.workshop_id = workshop_id; - mod.path = Steam_CopyString( modpath ); - mod.name = Steam_CopyString( name ); - arrput( steam_workshop_mods, mod ); + mod->is_local = true; + mod->local_path = Steam_CopyString( modpath ); + if( !mod->name ) + mod->name = Steam_CopyString( name ); + __RefreshModPath( mod ); entry = Sys_FS_FindNext( SFF_SUBDIR, SFF_HIDDEN | SFF_SYSTEM ); } @@ -311,11 +328,11 @@ struct steam_workshop_publish_result_s Steam_PublishLocalMod( int modIndex, cons const struct steam_workshop_mod_s *mods = Steam_GetWorkshopMods(); size_t count = Steam_GetWorkshopModCount(); - if( modIndex < 0 || (size_t)modIndex >= count || !mods[modIndex].is_local || !mods[modIndex].path ) { + if( modIndex < 0 || (size_t)modIndex >= count || !mods[modIndex].is_local || !mods[modIndex].local_path ) { res.res = STEAM_PUBLISH_ERR_INVALID_MOD; return res; } - const char *contentPath = mods[modIndex].path; + const char *contentPath = mods[modIndex].local_path; // Check for preview.png char previewPath[1024]; @@ -538,7 +555,10 @@ void Steam_Shutdown( void ) STEAMSHIM_unsubscribeEvent( EVT_WORKSHOP_DETAIL, Steam_EVT_WorkshopDetail ); for( size_t i = 0; i < arrlen( steam_workshop_mods ); i++ ) { - FS_UnRegisterSteamModPath( steam_workshop_mods[i].workshop_id ); + if( steam_workshop_mods[i].fs_search_path ) { + FS_UnRegisterModPath( steam_workshop_mods[i].fs_search_path ); + } + free( (void *)steam_workshop_mods[i].local_path ); free( (void *)steam_workshop_mods[i].path ); free( (void *)steam_workshop_mods[i].name ); free( (void *)steam_workshop_mods[i].title ); diff --git a/source/steamshim/src/mod_steam.h b/source/steamshim/src/mod_steam.h index fbe0aa4725..d242529e12 100644 --- a/source/steamshim/src/mod_steam.h +++ b/source/steamshim/src/mod_steam.h @@ -3,11 +3,15 @@ #include "./steamshim_types.h" +struct searchpath_s; + struct steam_workshop_mod_s { uint64_t workshop_id; bool is_remote; bool is_local; // discovered from the local mods folder, may be unpublished (workshop_id == 0) const char *path; + const char *local_path; + struct searchpath_s *fs_search_path; const char *name; // folder name for local mods, NULL for remote-only mods const char *title; const char *description; diff --git a/source/ui/as/as_bind_game.cpp b/source/ui/as/as_bind_game.cpp index 698ead154a..3fe5814019 100644 --- a/source/ui/as/as_bind_game.cpp +++ b/source/ui/as/as_bind_game.cpp @@ -132,7 +132,7 @@ static asstring_t *Game_WorkshopLocalMods( Game *game ) for( size_t i = 0; i < count; i++ ) { if( !mods[i].is_local ) continue; - const char *name = mods[i].name ? mods[i].name : ( mods[i].path ? mods[i].path : "" ); + const char *name = mods[i].name ? mods[i].name : ( mods[i].local_path ? mods[i].local_path : "" ); stream << name << "\t" << i << "\n"; } return ASSTR( stream.str() ); diff --git a/source/ui/datasources/ui_workshop_datasource.cpp b/source/ui/datasources/ui_workshop_datasource.cpp index 1d4e8a97fa..0f166a9289 100644 --- a/source/ui/datasources/ui_workshop_datasource.cpp +++ b/source/ui/datasources/ui_workshop_datasource.cpp @@ -19,6 +19,7 @@ namespace WSWUI WorkshopDataSource::WorkshopDataSource() : Rocket::Controls::DataSource( WORKSHOP_SOURCE ) { + STEAMSHIM_subscribeEvent( EVT_WORKSHOP_REFRESH_SUBSCRIBE_ITEMS, this, OnWorkshopRefresh ); STEAMSHIM_subscribeEvent( EVT_WORKSHOP_DETAIL, this, OnWorkshopDetail ); STEAMSHIM_subscribeEvent( EVT_WORKSHOP_ITEM_INSTALLED, this, OnWorkshopItemInstalled ); STEAMSHIM_subscribeEvent( EVT_WORKSHOP_ITEM_UNSUBSCRIBED, this, OnWorkshopItemUnsubscribed ); @@ -27,6 +28,7 @@ WorkshopDataSource::WorkshopDataSource() : WorkshopDataSource::~WorkshopDataSource() { + STEAMSHIM_unsubscribeEvent( EVT_WORKSHOP_REFRESH_SUBSCRIBE_ITEMS, OnWorkshopRefresh ); STEAMSHIM_unsubscribeEvent( EVT_WORKSHOP_DETAIL, OnWorkshopDetail ); STEAMSHIM_unsubscribeEvent( EVT_WORKSHOP_ITEM_INSTALLED, OnWorkshopItemInstalled ); STEAMSHIM_unsubscribeEvent( EVT_WORKSHOP_ITEM_UNSUBSCRIBED, OnWorkshopItemUnsubscribed ); @@ -50,12 +52,12 @@ void WorkshopDataSource::UpdateMods() size_t count = Steam_GetWorkshopModCount(); for( size_t i = 0; i < count; i++ ) { - if( !mods[i].path ) { + if( !mods[i].path && !mods[i].local_path ) { continue; } InstalledMod mod; - mod.title = mods[i].title ? mods[i].title : ""; + mod.title = mods[i].title ? mods[i].title : ( mods[i].name ? mods[i].name : "" ); mod.name = mods[i].name ? mods[i].name : ""; mod.is_local = mods[i].is_local; @@ -98,6 +100,11 @@ int WorkshopDataSource::GetNumRows( const Rocket::Core::String &table ) return 0; } +void WorkshopDataSource::OnWorkshopRefresh( void *self, struct steam_evt_pkt_s *pkt ) +{ + static_cast( self )->UpdateMods(); +} + void WorkshopDataSource::OnWorkshopDetail( void *self, struct steam_evt_pkt_s *pkt ) { static_cast( self )->UpdateMods(); diff --git a/source/ui/datasources/ui_workshop_datasource.h b/source/ui/datasources/ui_workshop_datasource.h index 69c61f154b..c6095eed63 100644 --- a/source/ui/datasources/ui_workshop_datasource.h +++ b/source/ui/datasources/ui_workshop_datasource.h @@ -32,6 +32,7 @@ namespace WSWUI void UpdateMods(); + static void OnWorkshopRefresh( void *self, struct steam_evt_pkt_s *pkt ); static void OnWorkshopDetail( void *self, struct steam_evt_pkt_s *pkt ); static void OnWorkshopItemInstalled( void *self, struct steam_evt_pkt_s *pkt ); static void OnWorkshopItemUnsubscribed( void *self, struct steam_evt_pkt_s *pkt ); From ab0fa8be75fd31eaaa5642cd8cbbca110101d329 Mon Sep 17 00:00:00 2001 From: Michael Pollind Date: Tue, 28 Apr 2026 22:48:03 -0700 Subject: [PATCH 5/6] fix build time errors Signed-off-by: Michael Pollind --- source/qcommon/steam.c | 15 +++++++-------- source/steamshim/src/child/child.cpp | 9 ++++++--- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/source/qcommon/steam.c b/source/qcommon/steam.c index 027adf50c1..656ef5c01e 100644 --- a/source/qcommon/steam.c +++ b/source/qcommon/steam.c @@ -158,14 +158,13 @@ static void Steam_EVT_WorkshopItemInstalled( void *self, struct steam_evt_pkt_s request.workshop_id = mod->workshop_id; STEAMSHIM_sendRPC( &request, sizeof( request ), NULL, Steam_WorkshopInstallInfoCallback, NULL ); - struct { - struct steam_workshop_req_rpcs_s req; - uint64_t id; - } details_request; - details_request.req.cmd = RPC_WORKSHOP_QUERY_ITEM_DETAILS; - details_request.req.num_ids = 1; - details_request.id = mod->workshop_id; - STEAMSHIM_sendRPC( &details_request, sizeof( details_request ), NULL, NULL, NULL ); + char details_request_buf[sizeof( struct steam_workshop_req_rpcs_s ) + sizeof( uint64_t )]; + struct steam_workshop_req_rpcs_s *details_req = (struct steam_workshop_req_rpcs_s *)details_request_buf; + memset( details_request_buf, 0, sizeof( details_request_buf ) ); + details_req->cmd = RPC_WORKSHOP_QUERY_ITEM_DETAILS; + details_req->num_ids = 1; + details_req->workshop_ids[0] = mod->workshop_id; + STEAMSHIM_sendRPC( details_req, sizeof( details_request_buf ), NULL, NULL, NULL ); } static void Steam_EVT_WorkshopDetail( void *self, struct steam_evt_pkt_s *pkt ) diff --git a/source/steamshim/src/child/child.cpp b/source/steamshim/src/child/child.cpp index 93d0c72168..21b524a33d 100644 --- a/source/steamshim/src/child/child.cpp +++ b/source/steamshim/src/child/child.cpp @@ -235,7 +235,8 @@ static void processRPC( steam_rpc_pkt_s *req, size_t size ) case RPC_WORKSHOP_INSTALLED_INFO: { dbgprintf( "RPC_WORKSHOP_INSTALLED_INFO workshop_id=%" PRIu64 "\n", (uint64_t)req->steam_workshop_item.workshop_id ); - struct steam_workshop_install_info_s recv = {}; + alignas( steam_workshop_install_info_s ) char recv_storage[sizeof( steam_workshop_install_info_s )] = {}; + steam_workshop_install_info_s &recv = *reinterpret_cast( recv_storage ); char folder[1024] = { 0 }; prepared_rpc_packet( &req->common, &recv ); @@ -280,7 +281,8 @@ static void processRPC( steam_rpc_pkt_s *req, size_t size ) write_packet( GPipeWrite, &recv, sizeof( success_recv_s ) ); for( size_t i = 0; i < numItems; i++ ) { - struct workshop_refresh_items_evt_s evt_res = {}; + alignas( workshop_refresh_items_evt_s ) char evt_res_storage[sizeof( workshop_refresh_items_evt_s ) + 256 * sizeof( uint64_t )] = {}; + workshop_refresh_items_evt_s &evt_res = *reinterpret_cast( evt_res_storage ); evt_res.cookie = req->stream_workshop_refresh.cookie; evt_res.cmd = EVT_WORKSHOP_REFRESH_SUBSCRIBE_ITEMS; for( ; evt_res.num_ids < 256 && i < numItems; i++ ) { @@ -738,7 +740,8 @@ static void processSteamDispatch() const bool got_result = result->m_eResult == k_EResultOK && result->m_unNumResultsReturned > 0 && GSteamUGC->GetQueryUGCResult( result->m_handle, 0, &details ); { - struct workshop_item_details_evt_s recv = {}; + alignas( workshop_item_details_evt_s ) char recv_storage[sizeof( workshop_item_details_evt_s )] = {}; + workshop_item_details_evt_s &recv = *reinterpret_cast( recv_storage ); recv.cmd = EVT_WORKSHOP_DETAIL; const char *title = ""; const char *description = ""; From e69a0bc3609d263a95f092332e3d0c4424cf506a Mon Sep 17 00:00:00 2001 From: Michael Pollind Date: Tue, 28 Apr 2026 23:45:40 -0700 Subject: [PATCH 6/6] feat: fix workshop query logic Signed-off-by: Michael Pollind --- assets/data0_21pure/ui/porkui/workshop.rml | 9 +- source/qcommon/steam.c | 38 +++-- source/ref_nri/ri_segment_alloc.c | 2 +- source/steamshim/src/child/child.cpp | 136 +++++++++--------- source/steamshim/src/mod_steam.h | 6 + source/steamshim/src/steamshim_types.h | 8 +- .../ui/datasources/ui_workshop_datasource.cpp | 67 ++++++--- .../ui/datasources/ui_workshop_datasource.h | 12 +- 8 files changed, 166 insertions(+), 112 deletions(-) diff --git a/assets/data0_21pure/ui/porkui/workshop.rml b/assets/data0_21pure/ui/porkui/workshop.rml index c0cb4d7e8d..fe02a688e6 100644 --- a/assets/data0_21pure/ui/porkui/workshop.rml +++ b/assets/data0_21pure/ui/porkui/workshop.rml @@ -105,9 +105,12 @@
- Mod - Workshop ID - Uploaded + Mod + Tags + Installed + Uploaded + Score + +
diff --git a/source/qcommon/steam.c b/source/qcommon/steam.c index 656ef5c01e..606262bcff 100644 --- a/source/qcommon/steam.c +++ b/source/qcommon/steam.c @@ -30,8 +30,6 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. cvar_t *steam_debug; static struct steam_workshop_mod_s *steam_workshop_mods; -static uint32_t steam_workshop_refresh_cookie; - static char *Steam_CopyString( const char *in ) { int size; @@ -104,25 +102,21 @@ static void Steam_WorkshopInstallInfoCallback( void *self, struct steam_rpc_pkt_ __RefreshModPath( mod ); } -static void Steam_EVT_WorkshopRefreshItems( void *self, struct steam_evt_pkt_s *pkt ) +static void Steam_WorkshopRefreshCallback( void *self, struct steam_rpc_pkt_s *pkt ) { - if( pkt->workshop_refresh_items.cookie != steam_workshop_refresh_cookie ) { - return; - } + struct workshop_subscribed_items_recv_s *recv = &pkt->workshop_subscribed_items; - for( size_t i = 0; i < pkt->workshop_refresh_items.num_ids; i++ ) { - uint64_t workshop_id = pkt->workshop_refresh_items.ids[i]; - __UpsertWorkshopMod( workshop_id ); + for( uint32_t i = 0; i < recv->num_ids; i++ ) { + __UpsertWorkshopMod( recv->ids[i] ); } - if( pkt->workshop_refresh_items.num_ids > 0 ) { - uint16_t num_ids = pkt->workshop_refresh_items.num_ids; - size_t req_size = sizeof( struct steam_workshop_req_rpcs_s ) + sizeof( uint64_t ) * num_ids; + if( recv->num_ids > 0 ) { + size_t req_size = sizeof( struct steam_workshop_req_rpcs_s ) + sizeof( uint64_t ) * recv->num_ids; struct steam_workshop_req_rpcs_s *request = (struct steam_workshop_req_rpcs_s *)malloc( req_size ); request->cmd = RPC_WORKSHOP_QUERY_ITEM_DETAILS; - request->num_ids = num_ids; - for( uint16_t i = 0; i < num_ids; i++ ) { - request->workshop_ids[i] = pkt->workshop_refresh_items.ids[i]; + request->num_ids = recv->num_ids; + for( uint32_t i = 0; i < recv->num_ids; i++ ) { + request->workshop_ids[i] = recv->ids[i]; } STEAMSHIM_sendRPC( request, req_size, NULL, NULL, NULL ); free( request ); @@ -190,16 +184,20 @@ static void Steam_EVT_WorkshopDetail( void *self, struct steam_evt_pkt_s *pkt ) mod->tags = Steam_CopyString( buf ); buf += evt->tags_len + 1; mod->preview_url = Steam_CopyString( buf ); - Com_Printf("Mod: %s Description: %s preview: %s", mod->title, mod->description, mod->preview_url); + + mod->votes_up = evt->votes_up; + mod->votes_down = evt->votes_down; + mod->score = evt->score; + mod->item_state = evt->item_state; + mod->visibility = evt->visibility; + mod->time_updated = evt->time_updated; } void Steam_RefreshWorkshopMods( void ) { struct steam_workshop_list_rpc_s request; - uint32_t sync = 0; request.cmd = RPC_WORKSHOP_REFRESH_SUBSCRIBED_ITEMS; - request.cookie = ++steam_workshop_refresh_cookie; - STEAMSHIM_sendRPC( &request, sizeof( request ), NULL, NULL, &sync ); + STEAMSHIM_sendRPC( &request, sizeof( request ), NULL, Steam_WorkshopRefreshCallback, NULL ); } /* @@ -532,7 +530,6 @@ void Steam_Init( void ) return; } - STEAMSHIM_subscribeEvent( EVT_WORKSHOP_REFRESH_SUBSCRIBE_ITEMS, NULL, Steam_EVT_WorkshopRefreshItems ); STEAMSHIM_subscribeEvent( EVT_WORKSHOP_ITEM_SUBSCRIBED, NULL, Steam_EVT_WorkshopItemSubscribed ); STEAMSHIM_subscribeEvent( EVT_WORKSHOP_ITEM_UNSUBSCRIBED, NULL, Steam_EVT_WorkshopItemUnSubscribed ); STEAMSHIM_subscribeEvent( EVT_WORKSHOP_ITEM_INSTALLED, NULL, Steam_EVT_WorkshopItemInstalled ); @@ -547,7 +544,6 @@ void Steam_Init( void ) */ void Steam_Shutdown( void ) { - STEAMSHIM_unsubscribeEvent( EVT_WORKSHOP_REFRESH_SUBSCRIBE_ITEMS, Steam_EVT_WorkshopRefreshItems ); STEAMSHIM_unsubscribeEvent( EVT_WORKSHOP_ITEM_SUBSCRIBED, Steam_EVT_WorkshopItemSubscribed ); STEAMSHIM_unsubscribeEvent( EVT_WORKSHOP_ITEM_UNSUBSCRIBED, Steam_EVT_WorkshopItemUnSubscribed ); STEAMSHIM_unsubscribeEvent( EVT_WORKSHOP_ITEM_INSTALLED, Steam_EVT_WorkshopItemInstalled ); diff --git a/source/ref_nri/ri_segment_alloc.c b/source/ref_nri/ri_segment_alloc.c index 4588d3d36e..5ef58ab786 100644 --- a/source/ref_nri/ri_segment_alloc.c +++ b/source/ref_nri/ri_segment_alloc.c @@ -35,7 +35,7 @@ bool RISegmentAlloc( uint32_t frameIndex, struct RISegmentAlloc_s *alloc, size_t assert( alloc->head != alloc->tail ); // this shouldn't happen } - assert( alloc->elementOffset < alloc->maxElements ); + assert( alloc->elementOffset <= alloc->maxElements ); // not enough total free space if( ( alloc->maxElements - alloc->elementsConsumed ) < numElements ) { diff --git a/source/steamshim/src/child/child.cpp b/source/steamshim/src/child/child.cpp index 21b524a33d..662e756642 100644 --- a/source/steamshim/src/child/child.cpp +++ b/source/steamshim/src/child/child.cpp @@ -260,36 +260,27 @@ static void processRPC( steam_rpc_pkt_s *req, size_t size ) UGCQueryHandle_t query = GSteamUGC->CreateQueryUGCDetailsRequest( (uint64 *)req->steam_workshop_items.workshop_ids, req->steam_workshop_items.num_ids ); SteamAPICall_t call = GSteamUGC->SendQueryUGCRequest( query ); dbgprintf( " -> api_call=%" PRIu64 " valid=%d\n", (uint64_t)call, call != k_uAPICallInvalid ); - struct steam_result_recv_s recv; - prepared_rpc_packet( &req->common, &recv ); - recv.result = call; if( call == k_uAPICallInvalid ) { GSteamUGC->ReleaseQueryUGCRequest( query ); } else { steam_async_push_rpc_shim( call, &req->common ); } - write_packet( GPipeWrite, &recv, sizeof( struct steam_result_recv_s ) ); break; } case RPC_WORKSHOP_REFRESH_SUBSCRIBED_ITEMS: { - dbgprintf( "RPC_WORKSHOP_REFRESH_SUBSCRIBED_ITEMS cookie=%u\n", (unsigned)req->stream_workshop_refresh.cookie ); - struct success_recv_s recv; std::vector items( GSteamUGC->GetNumSubscribedItems() ); const uint32 numItems = GSteamUGC->GetSubscribedItems( items.data(), items.size() ); - dbgprintf( " -> num_subscribed=%u\n", (unsigned)numItems ); - prepared_rpc_packet( &req->common, &recv ); - write_packet( GPipeWrite, &recv, sizeof( success_recv_s ) ); + dbgprintf( "RPC_WORKSHOP_REFRESH_SUBSCRIBED_ITEMS num_subscribed=%u\n", (unsigned)numItems ); - for( size_t i = 0; i < numItems; i++ ) { - alignas( workshop_refresh_items_evt_s ) char evt_res_storage[sizeof( workshop_refresh_items_evt_s ) + 256 * sizeof( uint64_t )] = {}; - workshop_refresh_items_evt_s &evt_res = *reinterpret_cast( evt_res_storage ); - evt_res.cookie = req->stream_workshop_refresh.cookie; - evt_res.cmd = EVT_WORKSHOP_REFRESH_SUBSCRIBE_ITEMS; - for( ; evt_res.num_ids < 256 && i < numItems; i++ ) { - evt_res.ids[evt_res.num_ids++] = items[i]; - } - write_packet( GPipeWrite, &evt_res, sizeof( workshop_refresh_items_evt_s ) + ( sizeof( uint64_t ) * evt_res.num_ids ) ); + const uint32_t pkt_size = sizeof( workshop_subscribed_items_recv_s ) + numItems * sizeof( uint64_t ); + std::vector pkt( pkt_size ); + workshop_subscribed_items_recv_s *recv = reinterpret_cast( pkt.data() ); + prepared_rpc_packet( &req->common, recv ); + recv->num_ids = numItems; + for( uint32 i = 0; i < numItems; i++ ) { + recv->ids[i] = items[i]; } + write_packet( GPipeWrite, pkt.data(), pkt_size ); break; } case RPC_AUTHSESSION_TICKET: { @@ -735,58 +726,71 @@ static void processSteamDispatch() } case SteamUGCQueryCompleted_t::k_iCallback: { SteamUGCQueryCompleted_t *result = (SteamUGCQueryCompleted_t *)data; - SteamUGCDetails_t details = {}; - char preview_url[4096] = { 0 }; - const bool got_result = result->m_eResult == k_EResultOK && result->m_unNumResultsReturned > 0 && GSteamUGC->GetQueryUGCResult( result->m_handle, 0, &details ); - - { - alignas( workshop_item_details_evt_s ) char recv_storage[sizeof( workshop_item_details_evt_s )] = {}; - workshop_item_details_evt_s &recv = *reinterpret_cast( recv_storage ); - recv.cmd = EVT_WORKSHOP_DETAIL; - const char *title = ""; - const char *description = ""; - const char *tags = ""; - const char *preview = ""; - - if( got_result ) { - GSteamUGC->GetQueryUGCPreviewURL( result->m_handle, 0, preview_url, sizeof( preview_url ) ); - title = details.m_rgchTitle; - description = details.m_rgchDescription; - tags = details.m_rgchTags; - preview = preview_url; - - recv.workshop_id = details.m_nPublishedFileId; - recv.success = true; - recv.owner_id = details.m_ulSteamIDOwner; - recv.creator_app_id = details.m_nCreatorAppID; - recv.consumer_app_id = details.m_nConsumerAppID; - recv.file_type = details.m_eFileType; - recv.visibility = details.m_eVisibility; - recv.time_created = details.m_rtimeCreated; - recv.time_updated = details.m_rtimeUpdated; - recv.votes_up = details.m_unVotesUp; - recv.votes_down = details.m_unVotesDown; - recv.num_children = details.m_unNumChildren; - recv.item_state = GSteamUGC->GetItemState( details.m_nPublishedFileId ); - recv.score = details.m_flScore; + dbgprintf( "SteamUGCQueryCompleted handle=%" PRIu64 " result=%d num_results=%u\n", + (uint64_t)result->m_handle, (int)result->m_eResult, (unsigned)result->m_unNumResultsReturned ); + + if( result->m_eResult == k_EResultOK ) { + SteamUGCDetails_t details = {}; + char preview_url[4096]; + for( uint32_t i = 0; i < result->m_unNumResultsReturned; i++ ) { + const char *title = ""; + const char *description = ""; + const char *tags = ""; + const char *preview = ""; + + alignas( workshop_item_details_evt_s ) char recv_storage[sizeof( workshop_item_details_evt_s )] = {}; + workshop_item_details_evt_s &recv = *reinterpret_cast( recv_storage ); + recv.cmd = EVT_WORKSHOP_DETAIL; + recv.result = result->m_eResult; + + if( GSteamUGC->GetQueryUGCResult( result->m_handle, i, &details ) ) { + preview_url[0] = '\0'; + GSteamUGC->GetQueryUGCPreviewURL( result->m_handle, i, preview_url, sizeof( preview_url ) ); + title = details.m_rgchTitle; + description = details.m_rgchDescription; + tags = details.m_rgchTags; + preview = preview_url; + + recv.workshop_id = details.m_nPublishedFileId; + recv.success = true; + recv.owner_id = details.m_ulSteamIDOwner; + recv.creator_app_id = details.m_nCreatorAppID; + recv.consumer_app_id = details.m_nConsumerAppID; + recv.file_type = details.m_eFileType; + recv.visibility = details.m_eVisibility; + recv.time_created = details.m_rtimeCreated; + recv.time_updated = details.m_rtimeUpdated; + recv.votes_up = details.m_unVotesUp; + recv.votes_down = details.m_unVotesDown; + recv.num_children = details.m_unNumChildren; + recv.item_state = GSteamUGC->GetItemState( details.m_nPublishedFileId ); + recv.score = details.m_flScore; + dbgprintf( " -> [%u] workshop_id=%" PRIu64 " title=\"%s\" tags=\"%s\" preview=\"%s\"\n", + i, (uint64_t)recv.workshop_id, title, tags, preview ); + } + + recv.title_len = strlen( title ); + recv.description_len = strlen( description ); + recv.tags_len = strlen( tags ); + recv.preview_url_len = strlen( preview ); + + const uint32_t buffer_size = sizeof( recv ) + recv.title_len + 1 + recv.description_len + 1 + recv.tags_len + 1 + recv.preview_url_len + 1; + std::vector pkt( buffer_size ); + char *cursor = pkt.data(); + memcpy( cursor, &recv, sizeof( recv ) ); cursor += sizeof( recv ); + memcpy( cursor, title, recv.title_len + 1 ); cursor += recv.title_len + 1; + memcpy( cursor, description, recv.description_len + 1 ); cursor += recv.description_len + 1; + memcpy( cursor, tags, recv.tags_len + 1 ); cursor += recv.tags_len + 1; + memcpy( cursor, preview, recv.preview_url_len + 1 ); + write_packet( GPipeWrite, pkt.data(), buffer_size ); } - - recv.result = result->m_eResult; - recv.title_len = strlen( title ); - recv.description_len = strlen( description ); - recv.tags_len = strlen( tags ); - recv.preview_url_len = strlen( preview ); - - const uint32_t buffer_size = sizeof( recv ) + recv.title_len + 1 + recv.description_len + 1 + recv.tags_len + 1 + recv.preview_url_len + 1; - writePipe( GPipeWrite, &buffer_size, sizeof( uint32_t ) ); - writePipe( GPipeWrite, &recv, sizeof( recv ) ); - writePipe( GPipeWrite, title, recv.title_len + 1 ); - writePipe( GPipeWrite, description, recv.description_len + 1 ); - writePipe( GPipeWrite, tags, recv.tags_len + 1 ); - writePipe( GPipeWrite, preview, recv.preview_url_len + 1 ); } GSteamUGC->ReleaseQueryUGCRequest( result->m_handle ); + + struct success_recv_s recv; + prepared_rpc_packet( &rpc_callback, &recv ); + write_packet( GPipeWrite, &recv, sizeof( struct success_recv_s ) ); break; } case GameRichPresenceJoinRequested_t::k_iCallback: { diff --git a/source/steamshim/src/mod_steam.h b/source/steamshim/src/mod_steam.h index d242529e12..647ab8770f 100644 --- a/source/steamshim/src/mod_steam.h +++ b/source/steamshim/src/mod_steam.h @@ -17,6 +17,12 @@ struct steam_workshop_mod_s { const char *description; const char *tags; const char *preview_url; + uint32_t votes_up; + uint32_t votes_down; + float score; + uint32_t item_state; + uint32_t visibility; + uint32_t time_updated; }; #ifdef __cplusplus diff --git a/source/steamshim/src/steamshim_types.h b/source/steamshim/src/steamshim_types.h index 15e032668c..781769639d 100644 --- a/source/steamshim/src/steamshim_types.h +++ b/source/steamshim/src/steamshim_types.h @@ -295,7 +295,12 @@ struct steam_id_rpc_s { struct steam_workshop_list_rpc_s { STEAM_RPC_SHIM_COMMON() - uint32_t cookie; +}; + +struct workshop_subscribed_items_recv_s { + STEAM_RPC_SHIM_COMMON() + uint32_t num_ids; + uint64_t ids[]; }; struct steam_workshop_install_info_s { @@ -546,6 +551,7 @@ struct steam_rpc_pkt_s { struct auth_session_ticket_recv_s auth_session; struct steam_workshop_list_rpc_s stream_workshop_refresh; + struct workshop_subscribed_items_recv_s workshop_subscribed_items; struct p2p_accept_connect_req_s p2p_accept_connection_req; struct p2p_accept_connection_recv_s p2p_accept_connection_recv; diff --git a/source/ui/datasources/ui_workshop_datasource.cpp b/source/ui/datasources/ui_workshop_datasource.cpp index 0f166a9289..b988364e56 100644 --- a/source/ui/datasources/ui_workshop_datasource.cpp +++ b/source/ui/datasources/ui_workshop_datasource.cpp @@ -6,12 +6,20 @@ #include -#define WORKSHOP_SOURCE "workshop" -#define TABLE_INSTALLED "installed" -#define COL_TITLE "title" -#define COL_WORKSHOP_ID "workshop_id" -#define COL_DISPLAY_NAME "display_name" -#define COL_IS_LOCAL "is_local" +#define WORKSHOP_SOURCE "workshop" +#define TABLE_INSTALLED "installed" +#define COL_TITLE "title" +#define COL_WORKSHOP_ID "workshop_id" +#define COL_DISPLAY_NAME "display_name" +#define COL_IS_LOCAL "is_local" +#define COL_IS_INSTALLED "is_installed" +#define COL_TAGS "tags" +#define COL_PREVIEW_URL "preview_url" +#define COL_VOTES_UP "votes_up" +#define COL_VOTES_DOWN "votes_down" +#define COL_SCORE "score" +#define COL_VISIBILITY "visibility" +#define COL_TIME_UPDATED "time_updated" namespace WSWUI { @@ -19,7 +27,6 @@ namespace WSWUI WorkshopDataSource::WorkshopDataSource() : Rocket::Controls::DataSource( WORKSHOP_SOURCE ) { - STEAMSHIM_subscribeEvent( EVT_WORKSHOP_REFRESH_SUBSCRIBE_ITEMS, this, OnWorkshopRefresh ); STEAMSHIM_subscribeEvent( EVT_WORKSHOP_DETAIL, this, OnWorkshopDetail ); STEAMSHIM_subscribeEvent( EVT_WORKSHOP_ITEM_INSTALLED, this, OnWorkshopItemInstalled ); STEAMSHIM_subscribeEvent( EVT_WORKSHOP_ITEM_UNSUBSCRIBED, this, OnWorkshopItemUnsubscribed ); @@ -28,7 +35,6 @@ WorkshopDataSource::WorkshopDataSource() : WorkshopDataSource::~WorkshopDataSource() { - STEAMSHIM_unsubscribeEvent( EVT_WORKSHOP_REFRESH_SUBSCRIBE_ITEMS, OnWorkshopRefresh ); STEAMSHIM_unsubscribeEvent( EVT_WORKSHOP_DETAIL, OnWorkshopDetail ); STEAMSHIM_unsubscribeEvent( EVT_WORKSHOP_ITEM_INSTALLED, OnWorkshopItemInstalled ); STEAMSHIM_unsubscribeEvent( EVT_WORKSHOP_ITEM_UNSUBSCRIBED, OnWorkshopItemUnsubscribed ); @@ -52,14 +58,23 @@ void WorkshopDataSource::UpdateMods() size_t count = Steam_GetWorkshopModCount(); for( size_t i = 0; i < count; i++ ) { - if( !mods[i].path && !mods[i].local_path ) { + // skip placeholder entries with no identity at all + if( mods[i].workshop_id == 0 && !mods[i].path && !mods[i].local_path ) { continue; } InstalledMod mod; - mod.title = mods[i].title ? mods[i].title : ( mods[i].name ? mods[i].name : "" ); - mod.name = mods[i].name ? mods[i].name : ""; - mod.is_local = mods[i].is_local; + mod.title = mods[i].title ? mods[i].title : ( mods[i].name ? mods[i].name : "" ); + mod.name = mods[i].name ? mods[i].name : ""; + mod.tags = mods[i].tags ? mods[i].tags : ""; + mod.preview_url = mods[i].preview_url ? mods[i].preview_url : ""; + mod.is_local = mods[i].is_local; + mod.is_installed = mods[i].path != nullptr; + mod.votes_up = mods[i].votes_up; + mod.votes_down = mods[i].votes_down; + mod.score = mods[i].score; + mod.visibility = mods[i].visibility; + mod.time_updated = mods[i].time_updated; std::ostringstream oss; oss << mods[i].workshop_id; @@ -88,7 +103,28 @@ void WorkshopDataSource::GetRow( Rocket::Core::StringList &row, const Rocket::Co row.push_back( mod.is_local && !mod.name.empty() ? mod.name.c_str() : mod.title.c_str() ); else if( cols[i] == COL_IS_LOCAL ) row.push_back( mod.is_local ? "1" : "0" ); - else + else if( cols[i] == COL_IS_INSTALLED ) + row.push_back( mod.is_installed ? "1" : "0" ); + else if( cols[i] == COL_TAGS ) + row.push_back( mod.tags.c_str() ); + else if( cols[i] == COL_PREVIEW_URL ) + row.push_back( mod.preview_url.c_str() ); + else if( cols[i] == COL_VOTES_UP ) { + std::ostringstream oss; oss << mod.votes_up; + row.push_back( oss.str().c_str() ); + } else if( cols[i] == COL_VOTES_DOWN ) { + std::ostringstream oss; oss << mod.votes_down; + row.push_back( oss.str().c_str() ); + } else if( cols[i] == COL_SCORE ) { + std::ostringstream oss; oss << mod.score; + row.push_back( oss.str().c_str() ); + } else if( cols[i] == COL_VISIBILITY ) { + std::ostringstream oss; oss << mod.visibility; + row.push_back( oss.str().c_str() ); + } else if( cols[i] == COL_TIME_UPDATED ) { + std::ostringstream oss; oss << mod.time_updated; + row.push_back( oss.str().c_str() ); + } else row.push_back( "" ); } } @@ -100,11 +136,6 @@ int WorkshopDataSource::GetNumRows( const Rocket::Core::String &table ) return 0; } -void WorkshopDataSource::OnWorkshopRefresh( void *self, struct steam_evt_pkt_s *pkt ) -{ - static_cast( self )->UpdateMods(); -} - void WorkshopDataSource::OnWorkshopDetail( void *self, struct steam_evt_pkt_s *pkt ) { static_cast( self )->UpdateMods(); diff --git a/source/ui/datasources/ui_workshop_datasource.h b/source/ui/datasources/ui_workshop_datasource.h index c6095eed63..ffa1f504d8 100644 --- a/source/ui/datasources/ui_workshop_datasource.h +++ b/source/ui/datasources/ui_workshop_datasource.h @@ -2,6 +2,7 @@ #define __UI_WORKSHOP_DATASOURCE_H__ #include +#include #include #include @@ -26,13 +27,20 @@ namespace WSWUI std::string title; std::string name; std::string workshop_id; - bool is_local = false; + std::string tags; + std::string preview_url; + bool is_local = false; + bool is_installed = false; + uint32_t votes_up = 0; + uint32_t votes_down = 0; + float score = 0.0f; + uint32_t visibility = 0; + uint32_t time_updated = 0; }; std::vector installedMods; void UpdateMods(); - static void OnWorkshopRefresh( void *self, struct steam_evt_pkt_s *pkt ); static void OnWorkshopDetail( void *self, struct steam_evt_pkt_s *pkt ); static void OnWorkshopItemInstalled( void *self, struct steam_evt_pkt_s *pkt ); static void OnWorkshopItemUnsubscribed( void *self, struct steam_evt_pkt_s *pkt );