From 2576f803cd4c749a69ec13a864b29bd544709950 Mon Sep 17 00:00:00 2001 From: Marco Cawthorne Date: Wed, 16 Aug 2023 15:28:41 -0700 Subject: [PATCH] Menu-FN/Platform: Move all the custom game parsing/install code into platform/, also clean up a bunch of routines in there --- src/menu-fn/entry.qc | 2 +- src/menu-fn/m_customgame.qc | 791 +----------------------------------- src/platform/defs.h | 3 +- src/platform/gamelibrary.h | 33 ++ src/platform/gamelibrary.qc | 626 ++++++++++++++++++++++++++++ src/platform/includes.src | 1 + src/platform/modserver.h | 2 - src/platform/modserver.qc | 2 +- src/platform/updates.qc | 16 +- 9 files changed, 684 insertions(+), 792 deletions(-) create mode 100644 src/platform/gamelibrary.h create mode 100644 src/platform/gamelibrary.qc diff --git a/src/menu-fn/entry.qc b/src/menu-fn/entry.qc index b3934421..8bddc9ad 100644 --- a/src/menu-fn/entry.qc +++ b/src/menu-fn/entry.qc @@ -132,7 +132,7 @@ m_init(void) g_btnsize = drawgetimagesize(g_bmp[BTNS_MAIN]); g_btnofs = 26 / g_btnsize[1]; - games_init(); + GameLibrary_Init(); main_init(); Colors_Init(); diff --git a/src/menu-fn/m_customgame.qc b/src/menu-fn/m_customgame.qc index 39b6b851..ca4cc3f2 100644 --- a/src/menu-fn/m_customgame.qc +++ b/src/menu-fn/m_customgame.qc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 Vera Visions LLC. + * Copyright (c) 2016-2023 Vera Visions LLC. * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -29,657 +29,37 @@ CMainButton customgame_btnRefresh; CMainButton customgame_btnDeactivate; CMainButton customgame_btnDone; -int g_iModInstallCache; -string g_strModInstallCache; - -/* get installing id */ -void -game_getinstallcache(void) -{ - int ret; - filestream fs_cache; - - ret = 0; - fs_cache = fopen("mcache.dat", FILE_READ); - - if (fs_cache >= 0) { - g_iModInstallCache = (int)stof(fgets(fs_cache)); - g_strModInstallCache = fgets(fs_cache); - fclose(fs_cache); - } else { - g_iModInstallCache = -1; - g_strModInstallCache = ""; - } - - print(sprintf("id: %i, name: %s\n", ret, g_strModInstallCache)); -} - -/* write installing id */ -void -game_writeinstallcache(int id, string gamedir) -{ - filestream fs_cache; - - fs_cache = fopen("mcache.dat", FILE_WRITE); - g_iModInstallCache = id; - g_strModInstallCache = gamedir; - - if (fs_cache >= 0) { - fputs(fs_cache, sprintf("%i\n",id)); - fputs(fs_cache, gamedir); - fclose(fs_cache); - } -} - -int -game_getpackageid(string pkgname) -{ - string f; - - for (int i = 0; (getpackagemanagerinfo(i, GPMI_NAME)); i++) { - string name; - name = getpackagemanagerinfo(i, GPMI_NAME); - - /* Spike started randomly putting version numbers into package names */ - f = sprintf("%s=%s", pkgname, getpackagemanagerinfo(i, GPMI_VERSION)); - - if (name == f) { - return i; - } - } - - /* no package id whatsoever */ - return (-1); -} - -/* return 1 if any of the packages are pending, installing, or corrupt */ -int -game_updatesavailable(void) -{ - /* look for the valid packages in the gameinfo pkgname */ - int pkgcount = tokenize(games[gameinfo_current].pkgname); - - for (int i = 0i; i < pkgcount; i++) { - int id = game_getpackageid(argv(i)); - string status = getpackagemanagerinfo(id, GPMI_INSTALLED); - - if (id == -1) - continue; - - switch (status) { - case "": - if (updates[id].installed == "") { - return (1); - } else if (updates[id].installed == "pending") { - return (1); - } - break; - case "pending": - return (1); - break; - case "enabled": - break; - case "present": - return (1); - break; - case "corrupt": - return (1); - break; - default: - /* we're currently installing stuff */ - return (1); - break; - } - } - - /* everything is installed */ - return (0); -} - -int -game_updateinstallcount(void) -{ - int count = 0; - - /* always return something positive when no packages are defined */ - if (games[gameinfo_current].pkgname == "") { - return (1); - } - - /* look for the valid packages in the gameinfo pkgname */ - int pkgcount = tokenize(games[gameinfo_current].pkgname); - - for (int i = 0i; i < pkgcount; i++) { - int id = game_getpackageid(argv(i)); - string status = getpackagemanagerinfo(id, GPMI_INSTALLED); - - switch (status) { - case "": - break; - case "pending": - break; - case "enabled": - count++; - break; - case "present": - count++; - break; - case "corrupt": - count++; - break; - default: - /* we're currently installing stuff */ - break; - } - } - - /* everything is installed */ - return count; -} -#endif - -/* local game/mod info parsing */ -void -games_set(int id) -{ - gameinfo_current = id; - setwindowcaption(games[id].game); - cvar_set("com_fullgamename", games[id].game); -} - -#ifndef WEBMENU -void -customgame_liblist_parse(int id, string strKey, string strValue) -{ - switch(strKey) { - case "game": - games[id].game = strValue; - break; - case "gamedir": - games[id].gamedir = strValue; - break; - case "fallback_dir": - games[id].fallback_dir = strValue; - break; - case "url_info": - games[id].url_info = strValue; - break; - case "url_dl": - games[id].url_dl = strValue; - break; - case "version": - games[id].version = strValue; - break; - case "size": - games[id].size = (int)stof(strValue); - break; - case "svonly": - games[id].svonly = (int)stof(strValue); - break; - case "cldll": - games[id].cldll = (int)stof(strValue); - break; - case "type": - switch (strtolower(strValue)) { - case "multiplayer_only": - case "multiplayer only": - games[id].type = "Multiplayer"; - break; - case "singleplayer_only": - case "singleplayer only": - games[id].type = "Singleplayer"; - break; - /* this... kind of sucks, but some games (gearbox) never updated - * their liblist to reflect that they do multiplayer */ - case "sp": - case "single": - case "singleplayer": - case "mp": - case "multi": - case "multiplayer": - default: - games[id].type = "Both"; - } - break; - case "minversion": - case "hlversion": - games[id].hlversion = strValue; - break; - case "nomodels": - games[id].nomodels = (int)stof(strValue); - break; - case "nosprays": - games[id].nosprays = (int)stof(strValue); - break; - case "mpentity": - games[id].mpentity = strValue; - break; - case "gamedll": - games[id].gamedll = strValue; - break; - case "startmap": - games[id].startmap = strcat("map ", strValue, "\n"); - break; - case "trainingmap": - games[id].trainingmap = strcat("map ", strValue, "\n"); - break; - /* newly added with Nuclide */ - case "pkgname": - games[id].pkgname = strValue; - games[id].pkgid = game_getpackageid(games[id].pkgname); - break; - case "pkgfile": - games[id].pkgfile = strValue; - break; - case "chatroom": - games[id].chatroom = strValue; - break; - case "readme": - games[id].readme = strValue; - break; - case "menumap": - games[id].menumap = strValue; - break; - case "introvideo": - games[id].introvideo = strValue; - break; - case "steambg": - games[id].steambg = (int)stof(strValue); - break; - case "base_dir": - games[id].base_dir = strValue; - break; - default: - break; - } -} - -/* looks for a single find inside a gamedir, including its pk3s and returns - * a valid filehandle if it is found */ -filestream -games_find_in_gamedir(string filename, string gamedirname) -{ - searchhandle sh; - searchhandle gsh; - searchhandle psh; - filestream fh; - - /* first let's see if we've got a liblist.gam just floating inside the gamedir */ - gsh = search_begin(filename, SB_FULLPACKAGEPATH | SB_FORCESEARCH | SEARCH_ALLOWDUPES, FALSE, gamedirname); - fh = search_fopen(gsh, 0); - - /* we do not. let's search for pk3's to sift through */ - if (fh < 0) { - /* let's search for every pk3 in the gamedir and search for a liblist, one at a time. */ - psh = search_begin("*.pk3", SB_FULLPACKAGEPATH | SB_FORCESEARCH, FALSE, gamedirname); - - /* loop through each pk3 in reverse (newest to old) */ - for (int i = search_getsize(psh); i >= 0; i--) { - string full = search_getfilename(psh, i); - - if (!full) - continue; - - sh = search_begin(filename, SB_FULLPACKAGEPATH | SB_FORCESEARCH, FALSE, strcat(gamedirname, "/", full)); - fh = search_fopen(sh, 0); - //print(sprintf("looking for %s in %s\n", filename, strcat(gamedirname, "/", full))); - - /* we found one */ - if (fh >= 0) { - search_end(sh); - break; - } - search_end(sh); - } - search_end(psh); - } - - /* we do not. let's search for pk3's to sift through */ - if (fh < 0) { - /* let's search for every pk3 in the gamedir and search for a liblist, one at a time. */ - psh = search_begin("dlcache/*.pk3.*", SB_FULLPACKAGEPATH | SB_FORCESEARCH | SEARCH_ALLOWDUPES, FALSE, gamedirname); - - /* loop through each pk3 in reverse (newest to old) */ - for (int i = search_getsize(psh); i >= 0; i--) { - string full = search_getfilename(psh, i); - //print(sprintf("%s\n", full)); - - if (!full) - continue; - - sh = search_begin(filename, SB_FULLPACKAGEPATH | SB_FORCESEARCH | SEARCH_ALLOWDUPES, FALSE, strcat(gamedirname, "/", full)); - fh = search_fopen(sh, 0); - //print(sprintf("looking for %s in %s\n", filename, strcat(gamedirname, "/", full))); - - /* we found one */ - if (fh >= 0) { - search_end(sh); - break; - } - search_end(sh); - } - search_end(psh); - } - - /* still nothing. let's search for pk4's to sift through */ - if (fh < 0) { - /* let's search for every pk4 in the gamedir and search for a liblist, one at a time. */ - psh = search_begin("*.pk4", SB_FULLPACKAGEPATH | SB_FORCESEARCH | SEARCH_ALLOWDUPES, FALSE, gamedirname); - - /* loop through each pk4 in reverse (newest to old) */ - for (int i = search_getsize(psh); i >= 0; i--) { - string full = search_getfilename(psh, i); - - if (!full) - continue; - - sh = search_begin(filename, SB_FULLPACKAGEPATH | SB_FORCESEARCH | SEARCH_ALLOWDUPES, FALSE, strcat(gamedirname, "/", full)); - fh = search_fopen(sh, 0); - - /* we found one */ - if (fh >= 0) { - search_end(sh); - break; - } - search_end(sh); - } - search_end(psh); - } - - search_end(gsh); - return (fh); -} - -/* check if a gameinfo.txt for the gamedir contains gameinfo, parse it if present */ -int -games_check_gtxt(int id, string gamedirname) -{ - string temp; - filestream fh; - int ret = 0; - - fh = games_find_in_gamedir("gameinfo.txt", gamedirname); - - if (fh < 0) - fh = games_find_in_gamedir("GameInfo.txt", gamedirname); - - /* we have found a liblist.gam */ - if (fh >= 0) { - string gamedirchain = ""; - int braced = 0; - - while ((temp = fgets(fh))) { - string token; - tokenize_console(temp); - token = strtolower(argv(0)); - - if (!token) - continue; - - if (token == "{") - braced++; - - if (token == "}") - braced--; - - if (braced == 1) { - /* GameInfo */ - switch (token) { - case "game": - games[id].game = argv(1); - break; - case "type": - games[id].type = argv(1); - break; - } - } else if (braced == 2) { - /* FileSystem */ - switch (token) { - case "steamappid": - break; - case "toolsappid": - break; - } - } else if (braced == 3) { - /* SearchPaths */ - switch (token) { - case "game": - if (argv(1) == "|gameinfo_path|.") - gamedirchain = strcat(gamedirchain, games[id].gamedir, " "); - else - gamedirchain = strcat(gamedirchain, argv(1), " "); - break; - } - } - } - - /* in gameinfo.txt files, we list game load-order in reverse */ - if (gamedirchain) { - string maindir = games[id].gamedir; - float c = tokenize(gamedirchain); - - for (float i = c-1; i >= 0; i--) { - if (argv(i) == maindir) - continue; - - if (i == 0) - games[id].gamedir = strcat(games[id].gamedir, argv(i)); - else - games[id].gamedir = strcat(games[id].gamedir, argv(i), ";"); - } - } - - fclose(fh); - ret = 1; - } - - return (ret); -} - -/* check if a manifest for the gamedir contains gameinfo, parse it if present */ -int -games_check_manifest(int id, string gamedirname) -{ - int ret = 0; - float count; - - string gamedescription = getgamedirinfo(id, 2); - - /* no manifest, or no cvar strings inside */ - if (gamedescription == "") { - return (0); - } - - count = tokenize_console(gamedescription); - - for (int i = 0; i < count; i++) { - string full = argv(i); - string first = substring(full, 0, 9); - string second = substring(full, 9, -1); - - /* we may have a game manifest, but if it doesn't - * contains any gameinfo entries a different file - * should probably be parsed later */ - if (first == "gameinfo_") { - customgame_liblist_parse(id, second, argv(i+1)); - ret = 1; - } - } - - return (ret); -} - -/* check if a liblist, parse it if present */ -int -games_check_liblist(int id, string gamedirname) -{ - string temp; - filestream fh; - int ret = 0; - - /* first let's see if we've got a liblist.gam just floating inside the gamedir */ - fh = games_find_in_gamedir("liblist.gam", gamedirname); - - /* we have found a liblist.gam */ - if (fh >= 0) { - while ((temp = fgets(fh))) { - tokenize(temp); - customgame_liblist_parse(id, argv(0), argv(1)); - } - fclose(fh); - ret = 1; - } - - return (ret); -} - void customgame_btnactivate_start(void) { - int nextgame = customgame_lbMods.GetSelected(); - - games_set(nextgame); - - if (games[nextgame].info_type == GAMEINFO_MANIFEST) - localcmd(sprintf("gamedir %s %s.fmf\nfs_changegame %s -\n", games[nextgame].gamedir, games[nextgame].gamedir, games[nextgame].gamedir)); - else if (games[nextgame].info_type == GAMEINFO_LIBLIST) { - /* some games/mods inherit other directories */ - if (games[nextgame].fallback_dir) { - localcmd(sprintf("gamedir \"%s;%s;%s\"\n", games[nextgame].base_dir, games[nextgame].fallback_dir, games[nextgame].gamedir)); - } else { - localcmd(sprintf("gamedir \"%s;%s\"\n", games[nextgame].base_dir, games[nextgame].gamedir)); - } - } else - localcmd(sprintf("gamedir \"%s;%s\"\n", games[nextgame].base_dir, games[nextgame].gamedir)); - - localcmd("stopmusic\nsnd_restart\nwait\nvid_reload\n"); - localcmd("menu_restart\n"); - localcmd("menu_customgame\n"); - localcmd("menu_musicstart\n"); -} - -void -customgame_installstart(int gameid) -{ - int count; - count = tokenize(games[gameid].pkgname); - - for (int i = 0; i < count; i++) { - int pkgid = game_getpackageid(argv(i)); - localcmd(sprintf("pkg add %s\n", argv(i))); - print(sprintf("Marking package %s for install.\n", - argv(i))); - } - - game_writeinstallcache(gameid, games[gameid].gamedir); - localcmd("pkg apply\n"); - print("Starting installation of custom game packages\n"); -} - -void -customgame_installend(void) -{ - int gid = g_iModInstallCache; - print(sprintf("Installation ended for %S!\n", g_strModInstallCache)); - localcmd(sprintf("game %s\n", g_strModInstallCache)); - - localcmd("stopmusic\nsnd_restart\nwait\nvid_reload\n"); - localcmd("menu_restart\n"); - localcmd("menu_customgame\n"); - localcmd("menu_musicstart\n"); - game_writeinstallcache(-1, g_strModInstallCache); + GameLibrary_Activate(customgame_lbMods.GetSelected()); } void customgame_installframe(void) { - int id; - float perc; - float c; - int loading; - /* graphical frame */ customgame_dlgWait.Draw(); WField_Static(162, 180, "Installing mod data...", 320, 260, col_prompt_text, 1.0f, 2, font_label_p); - /* download percentage */ - perc = 0.0f; - loading = FALSE; - - /* a game can have multiple packages associated */ - id = g_iModInstallCache; - c = tokenize(games[id].pkgname); - - /* go through all invididual packages */ - for (float i = 0; i < c; i++) { - string st; - int pkgid; - - /* package query */ - pkgid = game_getpackageid(argv(i)); - st = getpackagemanagerinfo(pkgid, GPMI_INSTALLED); - - /* filter out statuses so we can calculate percentage */ - switch (st) { - case "": - case "pending": - case "enabled": - case "present": - case "corrupt": - break; - default: - perc += stof(st); - } - - /* all packages need to be 'enabled', else fail to end */ - if (st != "enabled") - loading = TRUE; - } - - /* display download percentage we calculated */ - perc = perc / c; - WField_Static(162, 220, sprintf("%d%%", perc), 320, 260, + WField_Static(162, 220, sprintf("%d%%", GameLibrary_InstallProgress()), 320, 260, [1,1,1], 1.0f, 2, font_label_p); WField_Static(162, 260, "Service provided by frag-net.com through archive.org", 320, 260, [1,1,1], 1.0f, 2, font_label); - - /* not everything has been downloaded */ - if (loading == TRUE) - return; - - customgame_installend(); } /* download the .fmf and switch games immediately */ void customgame_btninstall_start(void) { - int id = customgame_lbMods.GetSelected(); - string st; - - st = getpackagemanagerinfo(games[id].pkgid, GPMI_INSTALLED); - - print(st); - print("\n"); - - if (st != "enabled") { - customgame_installstart(id); - return; - } - - game_writeinstallcache(id, games[id].gamedir); - customgame_installend(); + GameLibrary_Install(customgame_lbMods.GetSelected()); } void customgame_btndeactivate_start(void) { - localcmd(sprintf("gamedir %s\n", GAME_DIR)); - localcmd("stopmusic\nsnd_restart\nwait\nvid_reload\n"); - localcmd("menu_restart\n"); - localcmd("menu_customgame\n"); - localcmd("menu_musicstart\n"); + GameLibrary_Deactivate(); } void @@ -717,160 +97,6 @@ customgame_lbmods_changed(void) } } -/* set some sane default packages, so that we don't need to ship a custom liblist */ -void -games_setdefaultpkgs(int id) -{ -#if 0 - if (games[id].gamedir == "valve") { - games[id].pkgname = "valve_patch;addon_furtherdata;addon_holidaymodels"; - games[id].pkgfile = "maps/crossfire.bsp"; /* only found in patches */ - } -#endif -} - -void -games_init_mods(void) -{ - int id; - int foundself = 0; - string gamedirname; - gameinfo_count = 0; - - for (id = 0; (gamedirname = getgamedirinfo(id, 0)); id++) { - gameinfo_count++; - } - - /* re-allocate the game list */ - memfree(games); - games = memalloc(sizeof(gameinfo_t) * gameinfo_count); - - /* The things we do for frequent flyer mileage. */ - if (!games) - error(sprintf("Attempting to allocate mod data for %i entries failed\n", gameinfo_count)); - - /* now loop through all the mods we found and load in the metadata */ - for (id = 0; id < gameinfo_count; id++) { - gamedirname = getgamedirinfo(id, 0); - - /* Fill in the defaults */ - games[id].game = gamedirname; - games[id].gamedir = gamedirname; - games[id].base_dir = GAME_DIR; - games[id].url_info = ""; - games[id].url_dl = ""; - games[id].version = "1.0"; - games[id].size = 0; - games[id].type = "Both"; - games[id].nomodels = 0; - games[id].nosprays = 0; - games[id].mpentity = "info_player_deathmatch"; - games[id].gamedll = "progs.dat"; - games[id].startmap = "map c0a0\n"; - games[id].trainingmap = "map t0a0\n"; - games[id].cldll = 1; - games[id].hlversion = "1000"; - games[id].svonly = 0; - games[id].installed = 1; - games[id].chatroom = gamedirname; - games[id].readme = "readme.txt"; - games[id].pkgid = -1; - games[id].steambg = 0; - - games_setdefaultpkgs(id); - - if (games_check_manifest(id, gamedirname) == 1) { - NSLog("[MENU] Found manifest for %s", gamedirname); - games[id].info_type = GAMEINFO_MANIFEST; - } else if (games_check_gtxt(id, gamedirname) == 1) { - NSLog("[MENU] Found gameinfo for %s", gamedirname); - games[id].info_type = GAMEINFO_GITXT; - } else if (games_check_liblist(id, gamedirname) == 1) { - NSLog("[MENU] Found liblist for %s", gamedirname); - games[id].info_type = GAMEINFO_LIBLIST; - } else { - NSLog("[MENU] Found nothing for %s", gamedirname); - games[id].info_type = GAMEINFO_NONE; - } - - /* if we're this mod, make sure to let the rest of the menu know */ - if (games[id].gamedir == cvar_string("game")) { - games_set(id); - } - } - - /* we may have some mods, but we're not running any of them. Fatal */ - if (gameinfo_current == -1) { - print("^1FATAL ERROR: NO LIBLIST.GAM FOR CURRENT MOD FOUND!\n"); - crash(); - return; - } -} -#endif - -void -games_init(void) -{ - int id = 0; - string gamedirname = cvar_string("game"); - - gameinfo_count = 1; /* we start at 1 game, ours */ - games = memalloc(sizeof(gameinfo_t) * gameinfo_count); - - /* Fill in the defaults */ - games[id].game = gamedirname; - games[id].gamedir = gamedirname; - games[id].base_dir = GAME_DIR; - games[id].url_info = ""; - games[id].url_dl = ""; - games[id].version = "1.0"; - games[id].size = 0; - games[id].type = "Both"; - games[id].nomodels = 0; - games[id].nosprays = 0; - games[id].mpentity = "info_player_deathmatch"; - games[id].gamedll = "progs.dat"; - games[id].startmap = "map c0a0\n"; - games[id].trainingmap = "map t0a0\n"; - games[id].cldll = 1; - games[id].hlversion = "1000"; - games[id].svonly = 0; - games[id].installed = 1; - games[id].chatroom = cvar_string("gameinfo_chatroom"); - - if not (games[id].chatroom) { - games[id].chatroom = gamedirname; - } - - games[id].readme = "readme.txt"; - games[id].pkgid = -1; - games[id].steambg = 0; - gameinfo_current = 0; - - /* only run this when not in web-client mode */ -#ifndef WEBMENU - /* set the default packages for a given game */ - games_setdefaultpkgs(id); - - if (games_check_manifest(id, gamedirname) == 1) { - NSLog("[MENU] Found manifest for %s", gamedirname); - games[id].info_type = GAMEINFO_MANIFEST; - } else if (games_check_gtxt(id, gamedirname) == 1) { - NSLog("[MENU] Found gameinfo for %s", gamedirname); - games[id].info_type = GAMEINFO_GITXT; - } else if (games_check_liblist(id, gamedirname) == 1) { - NSLog("[MENU] Found liblist for %s", gamedirname); - games[id].info_type = GAMEINFO_LIBLIST; - } else { - NSLog("[MENU] Found nothing for %s", gamedirname); - games[id].info_type = GAMEINFO_NONE; - } -#endif - - /* set the current game to be us */ - games_set(id); -} - void menu_customgame_init(void) { @@ -930,7 +156,6 @@ menu_customgame_init(void) customgame_sbMods.SetCallback(customgame_sbmods_changed); customgame_sbMods.SetMax(gameinfo_count); Widget_Add(fn_customgame, customgame_sbMods); - g_iModInstallCache = -1; #endif } @@ -950,9 +175,9 @@ menu_customgame_draw(void) WField_Static(162, 180, "Indexing all local games, please wait...", 320, 260, col_prompt_text, 1.0f, 2, font_label_p); - /* once we started rendering the message, start parsing mods */ + /* once we started rendering the message, start parsing mods (stalls) */ if (g_customgame_initialized == 1) - games_init_mods(); + GameLibrary_InitCustom(); g_customgame_initialized++; return; @@ -986,7 +211,7 @@ menu_customgame_draw(void) /* draw a frame with progress during package install */ - if (g_iModInstallCache >= 0i) { + if (GameLibrary_IsInstalling()) { customgame_installframe(); } diff --git a/src/platform/defs.h b/src/platform/defs.h index 2c31b191..cb76eba9 100644 --- a/src/platform/defs.h +++ b/src/platform/defs.h @@ -22,4 +22,5 @@ #include "richpresence.h" #include "servers.h" #include "tcp.h" -#include "updates.h" \ No newline at end of file +#include "updates.h" +#include "gamelibrary.h" \ No newline at end of file diff --git a/src/platform/gamelibrary.h b/src/platform/gamelibrary.h new file mode 100644 index 00000000..e8e4c4f1 --- /dev/null +++ b/src/platform/gamelibrary.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-2023 Vera Visions LLC. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +/** Called when initializing the current game. Does not cache custom game/mod info. */ +void GameLibrary_Init(void); +/** Called when you want to initialize custom games/mods. Might want to call this later +if the amount of locally installed mods is overwhelming. */ +void GameLibrary_InitCustom(void); + +/** Install the specified game. */ +void GameLibrary_Install(int); +/** Activate the specified game. */ +void GameLibrary_Activate(int); +/** Deactivate the currently running mod. Switching back to the base game. */ +void GameLibrary_Deactivate(void); + +/** Returns true/false depending on if a Game installation is in progress. */ +bool GameLibrary_IsInstalling(void); +/** Returns a 0-100% value of game install progress, tracking across multiple packages. */ +float GameLibrary_InstallProgress(void); \ No newline at end of file diff --git a/src/platform/gamelibrary.qc b/src/platform/gamelibrary.qc new file mode 100644 index 00000000..8025d1cc --- /dev/null +++ b/src/platform/gamelibrary.qc @@ -0,0 +1,626 @@ +/* + * Copyright (c) 2016-2023 Vera Visions LLC. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +int g_iModInstallCache; +string g_strModInstallCache; + +/* local game/mod info parsing */ +static void +GameLibrary_Set(int id) +{ + gameinfo_current = id; + setwindowcaption(games[id].game); + cvar_set("com_fullgamename", games[id].game); +} + +/** Looks for a single file inside a gamedir, including its pk3s and returns a valid filehandle if it is found. */ +static filestream +GameLibrary_FindInGameDir(string filename, string gamedirname) +{ + searchhandle sh; + searchhandle gsh; + searchhandle psh; + filestream fh; + + /* if we're querying a file in our mounted game, we can exit early */ + if (gamedirname == cvar_string("fs_game")) { + fh = fopen(filename, FILE_READ); + + if (fh >= 0) { + return fh; + } + } + + /* first let's see if we've got a liblist.gam just floating inside the gamedir */ + gsh = search_begin(filename, SB_FULLPACKAGEPATH | SB_FORCESEARCH | SEARCH_ALLOWDUPES, FALSE, gamedirname); + fh = search_fopen(gsh, 0); + + /* we do not. let's search for pk3's to sift through */ + if (fh < 0) { + /* let's search for every pk3 in the gamedir and search for a liblist, one at a time. */ + psh = search_begin("*.pk3", SB_FULLPACKAGEPATH | SB_FORCESEARCH, FALSE, gamedirname); + + /* loop through each pk3 in reverse (newest to old) */ + for (int i = search_getsize(psh); i >= 0; i--) { + string full = search_getfilename(psh, i); + + if (!full) + continue; + + sh = search_begin(filename, SB_FULLPACKAGEPATH | SB_FORCESEARCH, FALSE, strcat(gamedirname, "/", full)); + fh = search_fopen(sh, 0); + //print(sprintf("looking for %s in %s\n", filename, strcat(gamedirname, "/", full))); + + /* we found one */ + if (fh >= 0) { + search_end(sh); + break; + } + search_end(sh); + } + search_end(psh); + } + + /* still nothing. let's search for pk4's to sift through */ + if (fh < 0) { + /* let's search for every pk4 in the gamedir and search for a liblist, one at a time. */ + psh = search_begin("*.pk4", SB_FULLPACKAGEPATH | SB_FORCESEARCH | SEARCH_ALLOWDUPES, FALSE, gamedirname); + + /* loop through each pk4 in reverse (newest to old) */ + for (int i = search_getsize(psh); i >= 0; i--) { + string full = search_getfilename(psh, i); + + if (!full) + continue; + + sh = search_begin(filename, SB_FULLPACKAGEPATH | SB_FORCESEARCH | SEARCH_ALLOWDUPES, FALSE, strcat(gamedirname, "/", full)); + fh = search_fopen(sh, 0); + + /* we found one */ + if (fh >= 0) { + search_end(sh); + break; + } + search_end(sh); + } + search_end(psh); + } + + search_end(gsh); + return (fh); +} + +#ifndef WEBMENU +/** Parses a key/value pair from liblist.gam files */ +static void +GameLibrary_LibListParse(int id, string strKey, string strValue) +{ + if (id == 0) + print(sprintf("%i %S %S\n", id, strKey, strValue)); + + switch(strKey) { + case "game": + games[id].game = strValue; + break; + case "gamedir": + games[id].gamedir = strValue; + break; + case "fallback_dir": + games[id].fallback_dir = strValue; + break; + case "url_info": + games[id].url_info = strValue; + break; + case "url_dl": + games[id].url_dl = strValue; + break; + case "version": + games[id].version = strValue; + break; + case "size": + games[id].size = (int)stof(strValue); + break; + case "svonly": + games[id].svonly = (int)stof(strValue); + break; + case "cldll": + games[id].cldll = (int)stof(strValue); + break; + case "type": + switch (strtolower(strValue)) { + case "multiplayer_only": + case "multiplayer only": + games[id].type = "Multiplayer"; + break; + case "singleplayer_only": + case "singleplayer only": + games[id].type = "Singleplayer"; + break; + /* this... kind of sucks, but some games (gearbox) never updated + * their liblist to reflect that they do multiplayer */ + case "sp": + case "single": + case "singleplayer": + case "mp": + case "multi": + case "multiplayer": + default: + games[id].type = "Both"; + } + break; + case "minversion": + case "hlversion": + games[id].hlversion = strValue; + break; + case "nomodels": + games[id].nomodels = (int)stof(strValue); + break; + case "nosprays": + games[id].nosprays = (int)stof(strValue); + break; + case "mpentity": + games[id].mpentity = strValue; + break; + case "gamedll": + games[id].gamedll = strValue; + break; + case "startmap": + games[id].startmap = strcat("map ", strValue, "\n"); + break; + case "trainingmap": + games[id].trainingmap = strcat("map ", strValue, "\n"); + break; + /* newly added with Nuclide */ + case "pkgname": + games[id].pkgname = strValue; + games[id].pkgid = Updates_IDForName(games[id].pkgname); + break; + case "pkgfile": + games[id].pkgfile = strValue; + break; + case "chatroom": + games[id].chatroom = strValue; + break; + case "readme": + games[id].readme = strValue; + break; + case "menumap": + games[id].menumap = strValue; + break; + case "introvideo": + games[id].introvideo = strValue; + break; + case "steambg": + games[id].steambg = (int)stof(strValue); + break; + case "base_dir": + games[id].base_dir = strValue; + break; + default: + break; + } +} + +/** Check if a gameinfo.txt for the gamedir contains gameinfo, parse it if present. Returns true on success. */ +static bool +GameLibrary_CheckGameInfo(int id, string gamedirname) +{ + string temp; + filestream fh; + int ret = 0; + + fh = GameLibrary_FindInGameDir("gameinfo.txt", gamedirname); + + if (fh < 0) + fh = GameLibrary_FindInGameDir("GameInfo.txt", gamedirname); + + /* we have found a liblist.gam */ + if (fh >= 0) { + string gamedirchain = ""; + int braced = 0; + + while ((temp = fgets(fh))) { + string token; + tokenize_console(temp); + token = strtolower(argv(0)); + + if (!token) + continue; + + if (token == "{") + braced++; + + if (token == "}") + braced--; + + if (braced == 1) { + /* GameInfo */ + switch (token) { + case "game": + games[id].game = argv(1); + break; + case "type": + games[id].type = argv(1); + break; + } + } else if (braced == 2) { + /* FileSystem */ + switch (token) { + case "steamappid": + break; + case "toolsappid": + break; + } + } else if (braced == 3) { + /* SearchPaths */ + switch (token) { + case "game": + if (argv(1) == "|gameinfo_path|.") + gamedirchain = strcat(gamedirchain, games[id].gamedir, " "); + else + gamedirchain = strcat(gamedirchain, argv(1), " "); + break; + } + } + } + + /* in gameinfo.txt files, we list game load-order in reverse */ + if (gamedirchain) { + string maindir = games[id].gamedir; + float c = tokenize(gamedirchain); + + for (float i = c-1; i >= 0; i--) { + if (argv(i) == maindir) + continue; + + if (i == 0) + games[id].gamedir = strcat(games[id].gamedir, argv(i)); + else + games[id].gamedir = strcat(games[id].gamedir, argv(i), ";"); + } + } + + fclose(fh); + ret = 1; + } + + return (ret); +} + +/** Check if a manifest for the gamedir contains gameinfo, parse it if present. Returns true on success. */ +static bool +GameLibrary_CheckManifest(int id, string gamedirname) +{ + int ret = 0; + float count; + + string gamedescription = getgamedirinfo(id, 2); + + /* no manifest, or no cvar strings inside */ + if (gamedescription == "") { + return (0); + } + + count = tokenize_console(gamedescription); + + for (int i = 0; i < count; i++) { + string full = argv(i); + string first = substring(full, 0, 9); + string second = substring(full, 9, -1); + + /* we may have a game manifest, but if it doesn't + * contains any gameinfo entries a different file + * should probably be parsed later */ + if (first == "gameinfo_") { + GameLibrary_LibListParse(id, second, argv(i+1)); + ret = 1; + } + } + + return (ret); +} + +/** Check if a liblist exists, and parse it if present. Returns true on success. */ +static bool +GameLibrary_CheckLibList(int id, string gamedirname) +{ + string temp; + filestream fh; + int ret = 0; + + /* first let's see if we've got a liblist.gam just floating inside the gamedir */ + fh = GameLibrary_FindInGameDir("liblist.gam", gamedirname); + + /* we have found a liblist.gam */ + if (fh >= 0) { + while ((temp = fgets(fh))) { + tokenize(temp); + GameLibrary_LibListParse(id, argv(0), argv(1)); + } + fclose(fh); + ret = 1; + } + + return (ret); +} + +/** Set some sane game defaults into a game id slot based on gamedir name. */ +static void +GameLibrary_SetDefaults(int id, string gamedirname) +{ + /* Fill in the defaults */ + games[id].game = gamedirname; + games[id].gamedir = gamedirname; + games[id].base_dir = GAME_DIR; + games[id].url_info = ""; + games[id].url_dl = ""; + games[id].version = "1.0"; + games[id].size = 0; + games[id].type = "Both"; + games[id].nomodels = 0; + games[id].nosprays = 0; + games[id].mpentity = "info_player_deathmatch"; + games[id].gamedll = "progs.dat"; + games[id].startmap = "map c0a0\n"; + games[id].trainingmap = "map t0a0\n"; + games[id].cldll = 1; + games[id].hlversion = "1000"; + games[id].svonly = 0; + games[id].installed = 1; + games[id].chatroom = gamedirname; + games[id].readme = "readme.txt"; + games[id].pkgid = -1; + games[id].steambg = 0; + +#if 0 + if (games[id].gamedir == "valve") { + games[id].pkgname = "valve_patch;addon_furtherdata;addon_holidaymodels"; + games[id].pkgfile = "maps/crossfire.bsp"; /* only found in patches */ + } +#endif +} + +void +GameLibrary_InitCustom(void) +{ + int id; + int foundself = 0; + string gamedirname; + gameinfo_count = 0; + + for (id = 0; (gamedirname = getgamedirinfo(id, 0)); id++) { + gameinfo_count++; + } + + /* re-allocate the game list */ + memfree(games); + games = memalloc(sizeof(gameinfo_t) * gameinfo_count); + + /* The things we do for frequent flyer mileage. */ + if (!games) + error(sprintf("Attempting to allocate mod data for %i entries failed\n", gameinfo_count)); + + /* now loop through all the mods we found and load in the metadata */ + for (id = 0; id < gameinfo_count; id++) { + gamedirname = getgamedirinfo(id, 0); + GameLibrary_SetDefaults(id, gamedirname); + + if (GameLibrary_CheckManifest(id, gamedirname) == true) { + NSLog("[MENU] Found manifest for %s", gamedirname); + games[id].info_type = GAMEINFO_MANIFEST; + } else if (GameLibrary_CheckGameInfo(id, gamedirname) == true) { + NSLog("[MENU] Found gameinfo for %s", gamedirname); + games[id].info_type = GAMEINFO_GITXT; + } else if (GameLibrary_CheckLibList(id, gamedirname) == true) { + NSLog("[MENU] Found liblist for %s", gamedirname); + games[id].info_type = GAMEINFO_LIBLIST; + } else { + NSLog("[MENU] Found nothing for %s", gamedirname); + games[id].info_type = GAMEINFO_NONE; + } + } + + /* we may have some mods, but we're not running any of them. Fatal */ + if (gameinfo_current == -1) { + print("^1FATAL ERROR: NO LIBLIST.GAM FOR CURRENT MOD FOUND!\n"); + crash(); + return; + } +} +#endif + +void +GameLibrary_Init(void) +{ + int id = 0i; + string gamedirname = cvar_string("game"); + + g_iModInstallCache = -1; + g_strModInstallCache = __NULL__; + gameinfo_count = 1; /* we start at 1 game, ours */ + games = memalloc(sizeof(gameinfo_t) * gameinfo_count); + + /* set the default packages for a given game */ + GameLibrary_SetDefaults(id, gamedirname); + + gameinfo_current = 0i; + + /* only run this when not in web-client mode */ +#ifndef WEBMENU + + if (GameLibrary_CheckManifest(id, gamedirname) == 1) { + NSLog("[MENU] Found manifest for %s", gamedirname); + games[id].info_type = GAMEINFO_MANIFEST; + } else if (GameLibrary_CheckGameInfo(id, gamedirname) == 1) { + NSLog("[MENU] Found gameinfo for %s", gamedirname); + games[id].info_type = GAMEINFO_GITXT; + } else if (GameLibrary_CheckLibList(id, gamedirname) == 1) { + NSLog("[MENU] Found liblist for %s", gamedirname); + games[id].info_type = GAMEINFO_LIBLIST; + } else { + NSLog("[MENU] Found nothing for %s", gamedirname); + games[id].info_type = GAMEINFO_NONE; + } +#endif + + /* set the current game to be us */ + GameLibrary_Set(id); +} + +static void +GameLibrary_EndInstall(void) +{ + int gid = g_iModInstallCache; + print(sprintf("Installation ended for %S!\n", g_strModInstallCache)); + localcmd(sprintf("game %s\n", g_strModInstallCache)); + + localcmd("stopmusic\nsnd_restart\nwait\nvid_reload\n"); + localcmd("menu_restart\n"); + localcmd("menu_customgame\n"); + localcmd("menu_musicstart\n"); + g_iModInstallCache = -1i; + g_strModInstallCache = __NULL__; +} + +bool +GameLibrary_IsInstalling(void) +{ + return (g_iModInstallCache == -1i) ? false : true; +} + +float +GameLibrary_InstallProgress(void) +{ + int id; + float perc; + float c; + bool loading = false; + + /* download percentage */ + perc = 0.0f; + loading = FALSE; + + /* a game can have multiple packages associated */ + id = g_iModInstallCache; + c = tokenize(games[id].pkgname); + + /* go through all invididual packages */ + for (float i = 0; i < c; i++) { + string st; + int pkgid; + + /* package query */ + pkgid = Updates_IDForName(argv(i)); + st = Updates_GetInfo(pkgid, GPMI_INSTALLED); + + /* filter out statuses so we can calculate percentage */ + switch (st) { + case "": + case "pending": + case "enabled": + case "present": + case "corrupt": + break; + default: + perc += stof(st); + } + + /* all packages need to be 'enabled', else fail to end */ + if (st != "enabled") + loading = true; + } + + /* not everything has been downloaded */ + if (loading == TRUE) + return perc / c; + + GameLibrary_EndInstall(); + + return 0.0f; +} + + +static void +GameLibrary_InstallStart(int gameid) +{ + int count; + count = tokenize(games[gameid].pkgname); + + for (int i = 0; i < count; i++) { + int pkgid = Updates_IDForName(argv(i)); + localcmd(sprintf("pkg add %s\n", argv(i))); + print(sprintf("Marking package %s for install.\n", + argv(i))); + } + + g_iModInstallCache = gameid; + g_strModInstallCache = games[gameid].gamedir; + localcmd("pkg apply\n"); + print("Starting installation of custom game packages\n"); +} + +void +GameLibrary_Install(int gameID) +{ + string st; + + st = Updates_GetInfo(games[gameID].pkgid, GPMI_INSTALLED); + + print(st); + print("\n"); + + if (st != "enabled") { + GameLibrary_InstallStart(gameID); + return; + } + + g_iModInstallCache = gameID; + g_strModInstallCache = games[gameID].gamedir; + GameLibrary_EndInstall(); +} + +void +GameLibrary_Activate(int gameID) +{ + GameLibrary_Set(gameID); + + if (games[gameID].info_type == GAMEINFO_MANIFEST) + localcmd(sprintf("gamedir %s %s.fmf\nfs_changegame %s -\n", games[gameID].gamedir, games[gameID].gamedir, games[gameID].gamedir)); + else if (games[gameID].info_type == GAMEINFO_LIBLIST) { + /* some games/mods inherit other directories */ + if (games[gameID].fallback_dir) { + localcmd(sprintf("gamedir \"%s;%s;%s\"\n", games[gameID].base_dir, games[gameID].fallback_dir, games[gameID].gamedir)); + } else { + localcmd(sprintf("gamedir \"%s;%s\"\n", games[gameID].base_dir, games[gameID].gamedir)); + } + } else + localcmd(sprintf("gamedir \"%s;%s\"\n", games[gameID].base_dir, games[gameID].gamedir)); + + localcmd("stopmusic\nsnd_restart\nwait\nvid_reload\n"); + localcmd("menu_restart\n"); + localcmd("menu_customgame\n"); + localcmd("menu_musicstart\n"); +} + +void +GameLibrary_Deactivate(void) +{ + localcmd(sprintf("gamedir %s\n", GAME_DIR)); + localcmd("stopmusic\nsnd_restart\nwait\nvid_reload\n"); + localcmd("menu_restart\n"); + localcmd("menu_customgame\n"); + localcmd("menu_musicstart\n"); +} \ No newline at end of file diff --git a/src/platform/includes.src b/src/platform/includes.src index 84d14557..9c1c0e92 100644 --- a/src/platform/includes.src +++ b/src/platform/includes.src @@ -8,4 +8,5 @@ servers.qc tcp.qc util.qc updates.qc +gamelibrary.qc #endlist diff --git a/src/platform/modserver.h b/src/platform/modserver.h index 0b6c9f5e..b8ebdf22 100644 --- a/src/platform/modserver.h +++ b/src/platform/modserver.h @@ -29,8 +29,6 @@ var int g_iModServerLoading; var int g_iModServerReqCount; -int game_getpackageid(string pkgname); - /** Call this to initiate a lengthy process of updating the list of mods we can install in the custom game menu. */ void ModServer_Refresh(void); diff --git a/src/platform/modserver.qc b/src/platform/modserver.qc index 1c9497c1..59a2a6ca 100644 --- a/src/platform/modserver.qc +++ b/src/platform/modserver.qc @@ -161,7 +161,7 @@ ModServer_ParseItem(string data) break; case "gameinfo_pkgname": games[id].pkgname = argv(i+1); - games[id].pkgid = game_getpackageid(games[id].pkgname); + games[id].pkgid = Updates_IDForName(games[id].pkgname); break; default: break; diff --git a/src/platform/updates.qc b/src/platform/updates.qc index 010402cc..524870f2 100644 --- a/src/platform/updates.qc +++ b/src/platform/updates.qc @@ -92,14 +92,22 @@ Updates_Refresh(void) int Updates_IDForName(string packageName) { - string tempString = ""; + string f; - for (int i = 0i; (tempString = getpackagemanagerinfo(i, GPMI_NAME)); i++) { - if (tempString == packageName) + for (int i = 0; (getpackagemanagerinfo(i, GPMI_NAME)); i++) { + string name; + name = getpackagemanagerinfo(i, GPMI_NAME); + + /* Spike started randomly putting version numbers into package names */ + f = sprintf("%s=%s", packageName, getpackagemanagerinfo(i, GPMI_VERSION)); + + if (name == f) { return i; + } } - return -1i; + /* no package id whatsoever */ + return (-1i); } /** Returns the package name for a given ID. Returns __NULL__ when not available. */