From 412c8f984e78af81a7f9424a995c0cb7ee2a7085 Mon Sep 17 00:00:00 2001 From: Marco Cawthorne Date: Fri, 18 Aug 2023 17:49:10 -0700 Subject: [PATCH] Platform/Menu-FN: Overhaul of game update handling. New Nuclide specific API to query update package states that avoids string comparisons and much more. --- .../menu_fallback.pk3dir/menu.dat.en_us.po | 32 +- src/menu-fn/entry.qc | 1 - src/menu-fn/m_updates.qc | 102 ++--- src/menu-fn/w_updatelist.qc | 76 ++-- src/platform/defs.h | 19 +- src/platform/gamelibrary.h | 4 +- src/platform/gamelibrary.qc | 156 +++++++- src/platform/modserver.qc | 29 +- src/platform/updates.h | 95 +++-- src/platform/updates.qc | 360 ++++++++++++++---- 10 files changed, 645 insertions(+), 229 deletions(-) diff --git a/platform/menu_fallback.pk3dir/menu.dat.en_us.po b/platform/menu_fallback.pk3dir/menu.dat.en_us.po index 461bb7ae..cb434d66 100644 --- a/platform/menu_fallback.pk3dir/menu.dat.en_us.po +++ b/platform/menu_fallback.pk3dir/menu.dat.en_us.po @@ -958,7 +958,7 @@ msgid "IDS_CONFIGURE_GOREHELP" msgstr "Disable visuals inappropriate for younger players and multiplayer." msgid "IDS_CONFIGURE_AUTOPATCHHELP" -msgstr "Download the latest version of %s." +msgstr "Download updates for %s." msgid "IDS_CHAT_NOSERVERS" msgstr "Could not locate any Frag-Net servers." @@ -1652,3 +1652,33 @@ msgstr "Do you want to uncompress the files for game '%s'?" msgid "IDS_FAVSVRS_CORRUPT" msgstr "The server data file favsvrs.dat appears to be corrupt.\n\nYou can request a new list of servers by pressing the Update button.\n\nDo you want to remove the corrupt file (you will have to re-enter your 'favorites' if you remove the file)?" + +msgid "UPDATE_DISABLED" +msgstr "Disabled" + +msgid "UPDATE_ENABLED" +msgstr "Enabled" + +msgid "UPDATE_CORRUPT" +msgstr "Corrupt" + +msgid "UPDATE_NOTINSTALLED" +msgstr "Not installed" + +msgid "UPDATE_PENDING_INSTALL" +msgstr "Install (pending)" + +msgid "UPDATE_PENDING_REINSTALL" +msgstr "Reinstall (pending)" + +msgid "UPDATE_PENDING_UNINSTALL" +msgstr "Uninstall (pending)" + +msgid "UPDATE_PENDING_AUTOINSTALL" +msgstr "Auto-install (pending)" + +msgid "UPDATE_PENDING_DISABLE" +msgstr "Disable (pending)" + +msgid "UPDATE_PENDING_RETAIN" +msgstr "Retain (pending)" \ No newline at end of file diff --git a/src/menu-fn/entry.qc b/src/menu-fn/entry.qc index 3f9e77ef..75708308 100644 --- a/src/menu-fn/entry.qc +++ b/src/menu-fn/entry.qc @@ -138,7 +138,6 @@ m_init(void) Colors_Init(); Strings_Init(); - Updates_Init(); if (GameLibrary_GetInfo(GAMEINFO_GAMEDIR) != "valve") { m_intro_skip(); diff --git a/src/menu-fn/m_updates.qc b/src/menu-fn/m_updates.qc index ee6a6ba5..f6966076 100644 --- a/src/menu-fn/m_updates.qc +++ b/src/menu-fn/m_updates.qc @@ -38,19 +38,19 @@ up_btndone_start(void) cvar_set("menu_updating", "0"); localsound("../media/launch_dnmenu1.wav"); g_menupage = PAGE_CONFIGURATION; - localcmd("seta menu_installedpackages 1;cfg_save\n"); + localcmd("pkg revert;seta menu_installedpackages 1;cfg_save\n"); } void up_btninstall_start(void) { - Updates_Remove(up_lbUpdates.GetSelected()); + Updates_Install(up_lbUpdates.GetSelected()); } void up_btnremove_start(void) { - Updates_Install(up_lbUpdates.GetSelected()); + Updates_Destroy(up_lbUpdates.GetSelected()); } void @@ -76,7 +76,7 @@ up_lbupdates_changed(void) if (pkgid == -1) return; - newpic = sprintf(FN_UPDATE_IMGURL, updates[pkgid].name); + newpic = Updates_GetInfo(pkgid, UPDATE_PREVIEWIMAGE); if not (newpic) return; @@ -94,7 +94,6 @@ up_sbupdates_changed(int val) void menu_updates_refresh(void) { - Updates_Refresh(); int updateCount = Updates_GetPackageCount(); up_sbUpdates.SetMax(updateCount); @@ -119,19 +118,19 @@ menu_updates_init(void) up_btnApply = spawn(CMainButton); up_btnApply.SetImage(BTN_UPDATE); up_btnApply.SetExecute(up_btnapply_start); - up_btnApply.SetPos(350+96,420+30); + up_btnApply.SetPos(50 + 160,420+13); Widget_Add(fn_updates, up_btnApply); up_btnInstall = spawn(CMainButton); up_btnInstall.SetImage(BTN_INSTALL); up_btnInstall.SetExecute(up_btninstall_start); - up_btnInstall.SetPos(350,420); + up_btnInstall.SetPos(380,400); Widget_Add(fn_updates, up_btnInstall); up_btnDelete = spawn(CMainButton); up_btnDelete.SetImage(BTN_DELETE); up_btnDelete.SetExecute(up_btnremove_start); - up_btnDelete.SetPos(350+200,420); + up_btnDelete.SetPos(380,400+30); Widget_Add(fn_updates, up_btnDelete); up_frUpdates = spawn(CFrame); @@ -158,59 +157,43 @@ menu_updates_init(void) up_frPreview.SetPos(350,160); up_frPreview.SetSize(256+6,128+6); Widget_Add(fn_updates, up_frPreview); - - Updates_Refresh(); #endif } -/* Drawing */ -int g_pkgname_updating; void menu_updates_draw(void) { #ifndef WEBMENU - static int old_enabled; - float fl = 0; - if (!g_updates_initialized) { - int pkg_ready = 0; - string packages = GameLibrary_GetInfo(GAMEINFO_PACKAGELIST); - - /* we have no hard-coded list of supported packages, so query frag-net.com */ - if (!packages && !g_pkgname_updating) { - string gamedir = GameLibrary_GetInfo(GAMEINFO_GAMEDIR); - print(sprintf("Querying package names for %s\n", gamedir)); - uri_get(sprintf("http://www.frag-net.com/dl/packages_%s", uri_escape(gamedir)), MODSERVER_REQ_PKGNAMES); - g_pkgname_updating = 1; - } - - /* don't query packages YET until we get a response */ - if (g_pkgname_updating == 1) { - return; - } - - /* query until 1 package is ready */ - for (int i = 0; (Updates_GetInfo(i, GPMI_NAME)); i++) { - string installed = Updates_GetInfo(i, GPMI_INSTALLED); - /* increment to keep track */ - if (installed == "enabled") - old_enabled++; - - pkg_ready = 1; - } - - if (pkg_ready == 1) { - menu_updates_refresh(); - g_updates_initialized = TRUE; - } + /* first draw run */ + if (g_updates_initialized == 0) { + Updates_Init(); + g_updates_initialized = 2; return; } - Widget_Draw(fn_updates); + updaterStatus_t status = Updates_GetUpdaterStatus(); + Header_Draw(HEAD_CONFIG); + /* we're still initializing... */ + if (g_updates_initialized == 2) { + if (status == UPDATER_PENDING) { + customgame_dlgWait.Draw(); + WField_Static(162, 180, "Contacting update server...", 320, 260, + col_prompt_text, 1.0f, 2, font_label_p); + return; + } else if (status == UPDATER_INITIALIZED) { + menu_updates_refresh(); + g_updates_initialized = 1; + } + } + + Widget_Draw(fn_updates); + drawpic([g_menuofs[0]+550,g_menuofs[1]+10], g_bmp[FN_LOGO],[80,80], [1,1,1], 1.0f, 0); + WLabel_Static(50, 143, "Data files:", 11, 11, [1,1,1], 1.0f, 0, font_arial); @@ -231,37 +214,22 @@ menu_updates_draw(void) 1.0f, 0, font_arial); int i = up_lbUpdates.GetSelected(); - i = updates[i].uid; + fl = 310; - WLabel_Static(420,fl, getpackagemanagerinfo(i, GPMI_AUTHOR), 11, 11, [1,1,1], + WLabel_Static(420,fl, Updates_GetInfo(i, UPDATE_AUTHOR), 11, 11, [1,1,1], 1.0f, 0, font_arial); fl += 18; - WLabel_Static(420,fl, getpackagemanagerinfo(i, GPMI_INSTALLED), 11, 11, [1,1,1], + WLabel_Static(420,fl, Updates_GetInfo(i, UPDATE_STATUSSTRING), 11, 11, [1,1,1], 1.0f, 0, font_arial); fl += 18; - WLabel_Static(420,fl, getpackagemanagerinfo(i, GPMI_LICENSE), 11, 11, [1,1,1], + WLabel_Static(420,fl, Updates_GetInfo(i, UPDATE_LICENSE), 11, 11, [1,1,1], 1.0f, 0, font_arial); fl += 18; - WLabel_Static(420,fl, getpackagemanagerinfo(i, GPMI_WEBSITE), 11, 11, [1,1,1], + WLabel_Static(420,fl, Updates_GetInfo(i, UPDATE_WEBSITE), 11, 11, [1,1,1], 1.0f, 0, font_arial); fl += 18; - WLabel_Static(420,fl, getpackagemanagerinfo(i, GPMI_VERSION), 11, 11, [1,1,1], + WLabel_Static(420,fl, Updates_GetInfo(i, UPDATE_VERSION), 11, 11, [1,1,1], 1.0f, 0, font_arial); WLabel_Static(350, 143, "Preview:", 11, 11, [1,1,1], 1.0f, 0, font_arial); - /* check if we've got any more packages than upon init */ - int new_packages = 0; - for (int b = 0; (getpackagemanagerinfo(b, GPMI_NAME)); b++) { - string installed = getpackagemanagerinfo(b, GPMI_INSTALLED); - - /* increment to keep track */ - if (installed == "enabled") - new_packages++; - } - - if (old_enabled != new_packages) { - old_enabled = new_packages; - localcmd("menu_restart\nmenu_updates\n"); - } - if (g_updates_previewpic) drawpic([g_menuofs[0]+350+3,g_menuofs[1]+160+3], g_updates_previewpic, [256,128], [1,1,1], 1.0f); #endif diff --git a/src/menu-fn/w_updatelist.qc b/src/menu-fn/w_updatelist.qc index 4358c195..a79a62e1 100644 --- a/src/menu-fn/w_updatelist.qc +++ b/src/menu-fn/w_updatelist.qc @@ -66,6 +66,9 @@ CUpdateList::Draw(void) for (int i = m_scroll; i < (visible + m_scroll); i++) { vector colo; + string updateTitle = Updates_GetInfo(i, UPDATE_TITLE); + updateState_t updateState = Updates_GetInfo(i, UPDATE_STATE); + updateAction_t updateAction = Updates_GetInfo(i, UPDATE_ACTION); if (m_selected == i) { colo = ML_COL_2; @@ -75,60 +78,55 @@ CUpdateList::Draw(void) colo = ML_COL_1; } - int uid = updates[i].uid; - string status = Updates_GetInfo(uid, GPMI_INSTALLED); - switch (status) { - case "": - if (updates[i].installed == "") { - colo = [1,0,0]; - } else if (updates[i].installed == "pending") { - colo = [0,1,0]; - } else if (updates[i].installed == "enabled") { - colo = colo; - } + switch (updateState) { + case UPDATESTATE_ENABLED: + colo = ML_COL_1; break; - case "pending": - colo[0] *= 0.5; - colo[1] *= 0.5; - colo[2] *= 0.5; + case UPDATESTATE_CORRUPT: + colo = [1, 0, 0]; /* red */ break; - case "enabled": - colo = colo; - break; - case "present": - colo[0] *= 0.5; - colo[1] *= 0.5; - colo[2] *= 0.5; - break; - case "corrupt": - colo = [1,0,0] * sin(time); - break; - default: - float p = stof(status) / 100; + case UPDATESTATE_PENDING: + float p = Updates_GetInfo(i, UPDATE_DLPERCENTAGE) / 100; colo = [0,1,0] * p; drawfill([g_menuofs[0] + m_x, g_menuofs[1] + pos], [m_size[0] * p, 18], colo, 0.5f); colo = [0.25,0.25,0.25] + ([0.75,0.75,0.75] * p); break; + case UPDATESTATE_DISABLED: + case UPDATESTATE_NONE: + colo = [0.5, 0.5, 0.5]; /* grey */ + default: + break; } - /* TODO: make this integrate with the above better */ - if (updates[i].installed == "rem") { - if ((time*2) & 1) - colo = [1,0,0]; - else - colo = [0,0,0]; - } else if (updates[i].installed == "in") { - if ((time*2) & 1) + if ((time*2) & 1) { + switch (updateAction) { + case UPDATEACTION_INSTALL: /* blinking orange */ colo = [1,1,0]; - else - colo = [0,0,0]; + break; + case UPDATEACTION_REINSTALL: + colo = [0,1,0]; + break; + case UPDATEACTION_UNINSTALL: /* blinking red */ + colo = [1,0,0]; + break; + case UPDATEACTION_AUTOINSTALL: /* blinking orange/grey */ + colo = [0.5,0.5,0]; + break; + case UPDATEACTION_DISABLE: /* blinking grey/color */ + colo = [0.5,0.5,0.5]; + break; + case UPDATEACTION_RETAIN: + case UPDATEACTION_NONE: + default: + break; + } } /* Game */ - WLabel_Static(m_x + 3, pos + 3, updates[i].title, 11, 11, colo, + WLabel_Static(m_x + 3, pos + 3, updateTitle, 11, 11, colo, 1.0f, 0, font_arial); pos += 18; diff --git a/src/platform/defs.h b/src/platform/defs.h index cb76eba9..6706b818 100644 --- a/src/platform/defs.h +++ b/src/platform/defs.h @@ -23,4 +23,21 @@ #include "servers.h" #include "tcp.h" #include "updates.h" -#include "gamelibrary.h" \ No newline at end of file +#include "gamelibrary.h" + +/** Definitions for FTE's internal package manager. We don't want you to talk to this one directly within Nuclide. */ +typedef enum +{ + GPMI_NAME, /**< name of the package, for use with the pkg command. */ + GPMI_CATEGORY, /**< category text */ + GPMI_TITLE, /**< name of the package, for showing the user. */ + GPMI_VERSION, /**< version info (may have multiple with the same name but different versions) */ + GPMI_DESCRIPTION, /**< some blurb */ + GPMI_LICENSE, /**< what license its distributed under */ + GPMI_AUTHOR, /**< name of the person(s) who created it */ + GPMI_WEBSITE, /**< where to contribute/find out more info/etc */ + GPMI_INSTALLED, /**< current state */ + GPMI_ACTION, /**< desired state */ + GPMI_AVAILABLE, /**< whether it may be downloaded or not. */ + GPMI_FILESIZE, /**< size to download. */ +} packageType_t; \ No newline at end of file diff --git a/src/platform/gamelibrary.h b/src/platform/gamelibrary.h index bafebc80..b9ccf56c 100644 --- a/src/platform/gamelibrary.h +++ b/src/platform/gamelibrary.h @@ -61,6 +61,7 @@ float GameLibrary_InstallProgress(void); int GameLibrary_GetCurrentGame(void); /** Retrieves fields for a given game. See gameInfo_t for a list of fields you can query. */ __variant GameLibrary_GetGameInfo(int, gameInfo_t); +/** Retrieves fields for the currently running game. See gameInfo_t for a list of fields you can query. */ __variant GameLibrary_GetInfo(gameInfo_t); typedef enum @@ -68,7 +69,8 @@ typedef enum GAMEINFO_NONE, /**< No gameinfo available. This is probably the engine making assumptions. */ GAMEINFO_MANIFEST, /**< Game info was read from a manifest within the path. */ GAMEINFO_GITXT, /**< Game info stems from a Source Engine style gameinfo.txt file. */ - GAMEINFO_LIBLIST /**< Game info stems from a GoldSrc style liblist.gam file. */ + GAMEINFO_LIBLIST, /**< Game info stems from a GoldSrc style liblist.gam file. */ + GAMEINFO_PACKAGE, } gi_type; typedef struct diff --git a/src/platform/gamelibrary.qc b/src/platform/gamelibrary.qc index fdcae9c3..74fb1a9d 100644 --- a/src/platform/gamelibrary.qc +++ b/src/platform/gamelibrary.qc @@ -14,6 +14,29 @@ * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ +/* The GameLibrary concerns itself with everything around what a game is, +how to install, activate and deactivate them. Mods are included in this, +so I'll proceed calling them 'games' or 'custom games'. + +A game can be installed through two primary means: + +- Manual install, like from a .zip or some installer or archive +- Engine package manager install, through our own user interface + +And between these, they can come with different metadata/manifests. + +It assumed that every game has either a FTE Manifest description, +a gameinfo.txt (Source Engine format) or liblist.gam (GoldSrc format) +that describes various aspects of the game. Like which version it is, what +map will be loaded when you press 'New Game' and so on. + +If that info is not available, some placeholder data will be used instead. +However, games installed via the package manager will at least for the +custom game menus not use the on-disk manifest file, but information +provided by the package manager. Once you switch into said game everything +within will be pulled from a file on disk, such as a liblist.gam or gameinfo.txt. +*/ + int g_iModInstallCache; string g_strModInstallCache; @@ -28,6 +51,27 @@ GameLibrary_Set(int id) cvar_set("com_fullgamename", games[id].game); } +static int +GameLibrary_IDForPackageName(string packageName) +{ + 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", packageName, getpackagemanagerinfo(i, GPMI_VERSION)); + + if (name == f) { + return i; + } + } + + /* no package id whatsoever */ + return (-1i); +} + /** 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) @@ -188,7 +232,7 @@ GameLibrary_LibListParse(int id, string strKey, string strValue) /* newly added with Nuclide */ case "pkgname": games[id].pkgname = strValue; - games[id].pkgid = Updates_IDForName(games[id].pkgname); + games[id].pkgid = GameLibrary_IDForPackageName(games[id].pkgname); break; case "pkgfile": games[id].pkgfile = strValue; @@ -391,21 +435,61 @@ GameLibrary_SetDefaults(int id, string gamedirname) #endif } +/** Checks if a given game directory was installed manually. */ +static bool +GameLibrary_CheckLocalPresence(string gameDir) +{ + string testPkgDir = __NULL__; + bool returnSuccess = true; + + for (int x = 0i; (testPkgDir = getgamedirinfo(x, 0)); x++) { + if (gameDir == testPkgDir) { + return true; + } + } + + return false; +} + void GameLibrary_InitCustom(void) { int id; int foundself = 0; - string gamedirname; + string gamedirname = __NULL__; gameinfo_count = 0; + int packageinfo_count = 0i; + int c = 0i; + /* first count let's all manually installed mods */ for (id = 0; (gamedirname = getgamedirinfo(id, 0)); id++) { gameinfo_count++; } + /* count the package installed mods after */ + for (int i = 0; (getpackagemanagerinfo(i, GPMI_NAME)); i++) { + string packageName = getpackagemanagerinfo(i, GPMI_NAME); + string installStatus = getpackagemanagerinfo(i, GPMI_INSTALLED); + string prefix = substring(packageName, 0, 3); + + /* only care about installed mods (custom games) */ + if (prefix == "cg_" && installStatus == "enabled") { + string gameDir = substring(packageName, 3, -1); + tokenizebyseparator(gameDir, "="); + gameDir = argv(0); + + /* check if this mod was installed manually already */ + if (GameLibrary_CheckLocalPresence(gameDir) == true) { + continue; + } + + packageinfo_count++; + } + } + /* re-allocate the game list */ memfree(games); - games = memalloc(sizeof(gameinfo_t) * gameinfo_count); + games = memalloc(sizeof(gameinfo_t) * (gameinfo_count + packageinfo_count)); /* The things we do for frequent flyer mileage. */ if (!games) @@ -429,8 +513,48 @@ GameLibrary_InitCustom(void) NSLog("[MENU] Found nothing for %s", gamedirname); games[id].info_type = GAMEINFO_NONE; } + c = id + 1; } + /* iterate through all packages again */ + for (int i = 0i; (getpackagemanagerinfo(i, GPMI_NAME)); i++) { + string packageName = getpackagemanagerinfo(i, GPMI_NAME); + string installStatus = getpackagemanagerinfo(i, GPMI_INSTALLED); + string prefix = substring(packageName, 0, 3); + + /* same check as above in the counter */ + if (prefix == "cg_" && installStatus == "enabled") { + string gameDir = substring(packageName, 3, -1); + tokenizebyseparator(gameDir, "="); + gameDir = argv(0); + + if (GameLibrary_CheckLocalPresence(gameDir) == true) { + continue; + } + + string titleString = getpackagemanagerinfo(i, GPMI_TITLE); + string versionString = getpackagemanagerinfo(i, GPMI_VERSION); + string authorString = getpackagemanagerinfo(i, GPMI_AUTHOR); + string sizeString = getpackagemanagerinfo(i, GPMI_FILESIZE); + string websiteString = getpackagemanagerinfo(i, GPMI_WEBSITE); + + //print(sprintf("Adding packaged game %S\n", gameDir)); + GameLibrary_SetDefaults(c, gameDir); + + games[c].game = substring(titleString, 5, -1); /* strip 'Mod: '*/ + games[c].url_info = websiteString; + games[c].version = versionString; + games[c].size = (int)stof(sizeString); + games[c].type = "Both"; + games[c].info_type = GAMEINFO_PACKAGE; + games[c].pkgname = strcat("cg_", gameDir, ";game_", gameDir, ";"); + c++; + } + } + + /* now we can pretend that these weren't their own thing */ + gameinfo_count += packageinfo_count; + /* 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"); @@ -458,7 +582,6 @@ GameLibrary_Init(void) /* 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; @@ -521,8 +644,8 @@ GameLibrary_InstallProgress(void) int pkgid; /* package query */ - pkgid = Updates_IDForName(argv(i)); - st = Updates_GetInfo(pkgid, GPMI_INSTALLED); + pkgid = GameLibrary_IDForPackageName(argv(i)); + st = getpackagemanagerinfo(pkgid, GPMI_INSTALLED); /* filter out statuses so we can calculate percentage */ switch (st) { @@ -555,10 +678,11 @@ 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)); + int pkgid = GameLibrary_IDForPackageName(argv(i)); localcmd(sprintf("pkg add %s\n", argv(i))); print(sprintf("Marking package %s for install.\n", argv(i))); @@ -575,7 +699,12 @@ GameLibrary_Install(int gameID) { string st; - st = Updates_GetInfo(games[gameID].pkgid, GPMI_INSTALLED); + if (gameID >= gameinfo_count || gameID < 0i) { + print(sprintf("GameLibrary_Install: Invalid game id %i!\n", gameID)); + return; + } + + st = getpackagemanagerinfo(games[gameID].pkgid, GPMI_INSTALLED); print(st); print("\n"); @@ -593,6 +722,11 @@ GameLibrary_Install(int gameID) void GameLibrary_Activate(int gameID) { + if (gameID >= gameinfo_count || gameID < 0i) { + print(sprintf("GameLibrary_Activate: Invalid game id %i!\n", gameID)); + return; + } + GameLibrary_Set(gameID); if (games[gameID].info_type == GAMEINFO_MANIFEST) @@ -636,10 +770,14 @@ GameLibrary_GetInfo(gameInfo_t infoType) return GameLibrary_GetGameInfo(gameinfo_current, infoType); } -/** Retrieves info for a given game. */ __variant GameLibrary_GetGameInfo(int gameID, gameInfo_t infoType) { + if (gameID >= gameinfo_count || gameID < 0i) { + print(sprintf("GameLibrary_GetGameInfo: Invalid game id %i!\n", gameID)); + return __NULL__; + } + switch (infoType) { case GAMEINFO_TITLE: return (string)games[gameID].game; diff --git a/src/platform/modserver.qc b/src/platform/modserver.qc index ced7742c..f99bba16 100644 --- a/src/platform/modserver.qc +++ b/src/platform/modserver.qc @@ -15,6 +15,28 @@ */ #ifndef WEBMENU +/* the same as GameLibrary_IDForPackageName */ +static int +ModServer_IDForPackageName(string packageName) +{ + 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", packageName, getpackagemanagerinfo(i, GPMI_VERSION)); + + if (name == f) { + return i; + } + } + + /* no package id whatsoever */ + return (-1i); +} + void* memrealloc(__variant *oldptr, int elementsize, int old_num, int new_num) { @@ -161,7 +183,7 @@ ModServer_ParseItem(string data) break; case "gameinfo_pkgname": games[id].pkgname = argv(i+1); - games[id].pkgid = Updates_IDForName(games[id].pkgname); + games[id].pkgid = ModServer_IDForPackageName(games[id].pkgname); break; default: break; @@ -169,6 +191,8 @@ ModServer_ParseItem(string data) } } +void Updater_URI_Callback(float id, float code, string data, int resourcebytes); + /* Called as an eventual result of the uri_get builtin. */ void ModServer_URI_Callback(float id, float code, string data, int resourcebytes) @@ -202,8 +226,7 @@ ModServer_URI_Callback(float id, float code, string data, int resourcebytes) ModServer_ParseItem(data); break; case MODSERVER_REQ_PKGNAMES: - games[GameLibrary_GetCurrentGame()].pkgname = data; - g_pkgname_updating = 0; + Updater_URI_Callback(id, code, data, resourcebytes); break; default: print(sprintf("^1ModServer_URI_Callback^7: Unknown request id %d with code %d\n", id, code)); diff --git a/src/platform/updates.h b/src/platform/updates.h index 04496a96..a2e3e7b7 100644 --- a/src/platform/updates.h +++ b/src/platform/updates.h @@ -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 @@ -14,54 +14,75 @@ * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ -string(float id, float b) getgamedirinfo = #0; -string(int packageidx, int desiredfield) getpackagemanagerinfo = #0; - +/** Different types you can pass to `Updates_GetInfo(...)` to learn details about a given Update entry. */ typedef enum { - GPMI_NAME, /**< name of the package, for use with the pkg command. */ - GPMI_CATEGORY, /**< category text */ - GPMI_TITLE, /**< name of the package, for showing the user. */ - GPMI_VERSION, /**< version info (may have multiple with the same name but different versions) */ - GPMI_DESCRIPTION, /**< some blurb */ - GPMI_LICENSE, /**< what license its distributed under */ - GPMI_AUTHOR, /**< name of the person(s) who created it */ - GPMI_WEBSITE, /**< where to contribute/find out more info/etc */ - GPMI_INSTALLED, /**< current state */ - GPMI_ACTION, /**< desired state */ - GPMI_AVAILABLE, /**< whether it may be downloaded or not. */ - GPMI_FILESIZE, /**< size to download. */ + UPDATE_NAME, /**< (string) name of the package, for use with the pkg command. */ + UPDATE_CATEGORY, /**< (string) category text */ + UPDATE_TITLE, /**< (string) name of the package, for showing the user. */ + UPDATE_VERSION, /**< (string) version info (may have multiple with the same name but different versions) */ + UPDATE_DESCRIPTION, /**< (string) some blurb */ + UPDATE_LICENSE, /**< (string) what license its distributed under */ + UPDATE_AUTHOR, /**< (string) name of the person(s) who created it */ + UPDATE_WEBSITE, /**< (string) where to contribute/find out more info/etc */ + UPDATE_STATE, /**< (updateState_t) The current state of the update. */ + UPDATE_ACTION, /**< (updateAction_t) Pending action of the update. */ + UPDATE_FILESIZE, /**< (int) size to download in bytes. */ + UPDATE_PREVIEWIMAGE, /**< (string) Path to a preview image in 4:3 aspect ratio. */ + UPDATE_STATUSSTRING, /**< (string) Localizable string that gives you the update status. */ + UPDATE_DLPERCENTAGE, /**< (float) Download progress in percent (0-100). */ } updateType_t; -typedef struct +/** Return values from passing UPDATE_STATE to Updates_GetInfo() */ +typedef enum { - string name; - string category; - string title; - string version; - string description; - string license; - string author; - string website; - string installed; - int size; - int uid; -} update_t; + UPDATESTATE_NONE, /**< Update is not installed, or unavailable. */ + UPDATESTATE_DISABLED, /**< Update is installed, but disabled. */ + UPDATESTATE_ENABLED, /**< Update is installed and enabled. */ + UPDATESTATE_CORRUPT, /**< Update on disk is corrupted. */ + UPDATESTATE_PENDING /**< Update is pending a change. Usually when we're downloading it. */ +} updateState_t; -int g_platform_update_count; -update_t *updates; +/** Return values from passing UPDATE_ACTION to Updates_GetInfo() */ +typedef enum +{ + UPDATEACTION_NONE, /**< Update is not marked for any change. */ + UPDATEACTION_INSTALL, /**< Update marked for installation. */ + UPDATEACTION_REINSTALL, /**< Update marked as needing re-installation. */ + UPDATEACTION_UNINSTALL, /**< Update marked for removal. */ + UPDATEACTION_AUTOINSTALL, /**< Update marked as needing to be installed, due to a dependency. */ + UPDATEACTION_DISABLE, /**< Update has been marked for disabling. */ + UPDATEACTION_RETAIN /**< Update has been marked as being retained. */ +} updateAction_t; -#define FN_UPDATE_IMGURL "http://www.frag-net.com/dl/img/%s.jpg" +/** These are the possible return values from Updates_GetUpdaterStatus(). +That way you can put up a loading screen for when the updater is still initiliazing, +or be notified of when an updater is not available at all. */ +typedef enum +{ + UPDATER_NONE, /**< Nuclide's updater has not been initialized. You need to call Update_Init(). */ + UPDATER_UNAVAILABLE, /**< Nuclide's updater is unavailable. This may be due to the update server being offline. */ + UPDATER_PENDING, /**< Nuclide's updater is pending. May change to UNAVAILABLE or INITIALIZED. */ + UPDATER_INITIALIZED /**< Nuclide's updater is initialized and may have entries. Use Updates_GetUpdateCount() to query how many. */ +} updaterStatus_t; +/** Call this in order to contact the update server and fill the list of updates. */ void Updates_Init(void); -void Updates_Refresh(void); +/** Retrieve the status of the updater. See updaterStatus_t for valid return values. */ +updaterStatus_t Updates_GetUpdaterStatus(void); +/** Returns the total amount of updates available for the currently running game. */ int Updates_GetPackageCount(void); -int Updates_IDForName(string); -string Updates_NameForID(int); -string Updates_GetInfo(int, updateType_t); +/** Query a package (by ID) for its various info fields. See updateType_t for available fields. */ +__variant Updates_GetInfo(int, updateType_t); +/** Returns if our current game has updates available for any installed packages. */ bool Updates_Available(void); - +/** Toggle the installation/disabling of an update. May return true/false if it succeeded in marking the package. */ bool Updates_Toggle(int); +/** Mark an update as pending installion. May return true/false if it succeeded in marking the package. */ bool Updates_Install(int); +/** Mark an update as pending deletion. May return true/false if it succeeded in marking the package. */ bool Updates_Remove(int); +/** Mark an update as pending uninstallation. May return true/false if it succeeded in marking the package. */ +bool Updates_Destroy(int); +/** Apply all pending changes to packages. May return true/false if it succeeded in doing so. */ bool Updates_ApplyPendingChanges(void); \ No newline at end of file diff --git a/src/platform/updates.qc b/src/platform/updates.qc index 6e97746c..4b22dd7c 100644 --- a/src/platform/updates.qc +++ b/src/platform/updates.qc @@ -1,13 +1,72 @@ -/** needs to be called upon menu-init, and call Updates_Refresh() if auto-updates - are enabled. if a chooser does not want updates, then we won't. */ +/* + * 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. +*/ + +typedef struct +{ + string name; + string category; + string title; + string version; + string description; + string license; + string author; + string website; + string installed; + updateState_t state; + updateAction_t pending_action; + int size; + int uid; + string preview_image; + float dlpercentage; +} update_t; + +string(float id, float b) getgamedirinfo = #0; +string(int packageidx, int desiredfield) getpackagemanagerinfo = #0; + +#define FN_UPDATE_IMGURL "http://www.frag-net.com/dl/img/%s.jpg" +int g_platform_update_count; +update_t *updates; + +var updaterStatus_t updater_package_status = UPDATER_NONE; + void Updates_Init(void) { - /*localcmd("pkg addsource https://www.frag-net.com/pkgs/list\n");*/ - print("Update system initialized.\n"); + string packages; + + /* first, see if our game info sets any packages. */ + packages = GameLibrary_GetInfo(GAMEINFO_PACKAGELIST); + + /* we have no hard-coded list of supported packages, so query frag-net.com */ + if (!packages) { + string gamedir = cvar_string("fs_game"); + print(sprintf("Querying package names for %s\n", gamedir)); + uri_get(sprintf("http://www.frag-net.com/dl/packages_%s", uri_escape(gamedir)), MODSERVER_REQ_PKGNAMES); + updater_package_status = UPDATER_PENDING; + } else { + updater_package_status = UPDATER_INITIALIZED; + } +} + +updaterStatus_t +Updates_GetUpdaterStatus(void) +{ + return updater_package_status; } -/** will return a cached value */ int Updates_GetPackageCount(void) { @@ -43,8 +102,68 @@ Updates_IsRecommended(string packageName) return false; } -/** called whenever we need to re-initialize the updates struct */ -void +static void +Updates_RefreshStateValues(int packageID) +{ + int pkgUID = updates[packageID].uid; + string installedState = getpackagemanagerinfo(pkgUID, GPMI_INSTALLED); + string actionState = getpackagemanagerinfo(pkgUID, GPMI_ACTION); + + switch (actionState) { + case "user": + updates[packageID].pending_action = UPDATEACTION_INSTALL; + break; + case "reinstall": + updates[packageID].pending_action = UPDATEACTION_REINSTALL; + break; + case "purge": + updates[packageID].pending_action = UPDATEACTION_UNINSTALL; + break; + case "auto": + updates[packageID].pending_action = UPDATEACTION_AUTOINSTALL; + break; + case "disable": + updates[packageID].pending_action = UPDATEACTION_DISABLE; + break; + case "retain": + /*updates[packageID].pending_action = UPDATEACTION_RETAIN; + break;*/ + default: + updates[packageID].pending_action = UPDATEACTION_NONE; + } + + switch (installedState) { + case "present": + updates[packageID].state = UPDATESTATE_DISABLED; + break; + case "enabled": + updates[packageID].state = UPDATESTATE_ENABLED; + break; + case "corrupt": + updates[packageID].state = UPDATESTATE_CORRUPT; + break; + case "pending": + updates[packageID].state = UPDATESTATE_PENDING; + break; + default: + updates[packageID].state = UPDATESTATE_NONE; + } + + updates[packageID].dlpercentage = stof(installedState); + + /* HACK: the engine doesn't seem to set pending while installing, so let us do the job then */ + if (updates[packageID].dlpercentage > 0) + updates[packageID].state = UPDATESTATE_PENDING; + + /* HACK: enabled AND pending installation? smells like an engine bug! */ + if (updates[packageID].state == UPDATESTATE_ENABLED) { + if (updates[packageID].pending_action == UPDATEACTION_INSTALL) { + updates[packageID].pending_action = UPDATEACTION_NONE; + } + } +} + +static void Updates_Refresh(void) { int c = 0i; @@ -56,7 +175,7 @@ Updates_Refresh(void) } /* count all updates that we've got in our package sources */ - for (int i = 0i; (Updates_GetInfo(i, GPMI_NAME)); i++) { + for (int i = 0i; (getpackagemanagerinfo(i, GPMI_NAME)); i++) { g_platform_update_count++; } @@ -67,21 +186,23 @@ Updates_Refresh(void) int id = i; /* skip not recommended packages */ - if (Updates_IsRecommended(Updates_GetInfo(id, GPMI_NAME)) == false) + if (Updates_IsRecommended(getpackagemanagerinfo(id, GPMI_NAME)) == false) continue; - updates[c].name = Updates_GetInfo(id, GPMI_NAME); - updates[c].category = Updates_GetInfo(id, GPMI_CATEGORY); - updates[c].title = Updates_GetInfo(id, GPMI_TITLE); - updates[c].version = Updates_GetInfo(id, GPMI_VERSION); - updates[c].description = Updates_GetInfo(id, GPMI_DESCRIPTION); - updates[c].license = Updates_GetInfo(id, GPMI_LICENSE); - updates[c].author = Updates_GetInfo(id, GPMI_AUTHOR); - updates[c].website = Updates_GetInfo(id, GPMI_WEBSITE); - updates[c].installed = Updates_GetInfo(id, GPMI_INSTALLED); - updates[c].size = (int)stof(Updates_GetInfo(id, GPMI_FILESIZE)); + updates[c].name = getpackagemanagerinfo(id, GPMI_NAME); + updates[c].category = getpackagemanagerinfo(id, GPMI_CATEGORY); + updates[c].title = getpackagemanagerinfo(id, GPMI_TITLE); + updates[c].version = getpackagemanagerinfo(id, GPMI_VERSION); + updates[c].description = getpackagemanagerinfo(id, GPMI_DESCRIPTION); + updates[c].license = getpackagemanagerinfo(id, GPMI_LICENSE); + updates[c].author = getpackagemanagerinfo(id, GPMI_AUTHOR); + updates[c].website = getpackagemanagerinfo(id, GPMI_WEBSITE); + + updates[c].size = (int)stof(getpackagemanagerinfo(id, GPMI_FILESIZE)); updates[c].uid = i; - precache_pic(sprintf(FN_UPDATE_IMGURL, updates[c].name)); + updates[c].preview_image = sprintf(FN_UPDATE_IMGURL, updates[c].name); + Updates_RefreshStateValues(c); + c++; } @@ -89,94 +210,177 @@ Updates_Refresh(void) g_platform_update_count = c; } -/** Returns the package ID for a given name. Will return -1 when not available. */ -int -Updates_IDForName(string packageName) -{ - 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", packageName, getpackagemanagerinfo(i, GPMI_VERSION)); - - if (name == f) { - return i; - } - } - - /* no package id whatsoever */ - return (-1i); -} - -/** Returns the package name for a given ID. Returns __NULL__ when not available. */ -string -Updates_NameForID(int packageID) -{ - string packageName = getpackagemanagerinfo(packageID, GPMI_NAME); - - if not (packageName) - return __NULL__; - - return packageName; -} - -/** Query a package (by ID) for its various info fields. See updateType_t for available fields. */ -string +__variant Updates_GetInfo(int packageID, updateType_t fieldType) { - return getpackagemanagerinfo(packageID, (int)fieldType); + if (packageID >= g_platform_update_count || packageID < 0i) { + print(sprintf("Updates_GetInfo: Invalid package id %i!\n", packageID)); + return __NULL__; + } + + Updates_RefreshStateValues(packageID); + + switch (fieldType) { + case UPDATE_NAME: + return (string)updates[packageID].name; + break; + case UPDATE_CATEGORY: + return (string)updates[packageID].category; + break; + case UPDATE_TITLE: + return (string)updates[packageID].title; + break; + case UPDATE_VERSION: + return (string)updates[packageID].version; + break; + case UPDATE_DESCRIPTION: + return (string)updates[packageID].description; + break; + case UPDATE_LICENSE: + return (string)updates[packageID].license; + break; + case UPDATE_AUTHOR: + return (string)updates[packageID].author; + break; + case UPDATE_WEBSITE: + return (string)updates[packageID].website; + break; + case UPDATE_STATE: + return (updateState_t)updates[packageID].state; + break; + case UPDATE_ACTION: + return (updateAction_t)updates[packageID].pending_action; + break; + case UPDATE_FILESIZE: + return (int)updates[packageID].size; + break; + case UPDATE_PREVIEWIMAGE: + return (string)updates[packageID].preview_image; + break; + case UPDATE_STATUSSTRING: + /* if we have a action, focus on that */ + switch (updates[packageID].pending_action) { + case UPDATEACTION_INSTALL: + if (updates[packageID].dlpercentage > 0.0) { + return sprintf("%d %%", updates[packageID].dlpercentage); + } else { + return _("UPDATE_PENDING_INSTALL"); + } + break; + case UPDATEACTION_REINSTALL: + return _("UPDATE_PENDING_REINSTALL"); + break; + case UPDATEACTION_UNINSTALL: + return _("UPDATE_PENDING_UNINSTALL"); + break; + case UPDATEACTION_AUTOINSTALL: + return _("UPDATE_PENDING_AUTOINSTALL"); + break; + case UPDATEACTION_DISABLE: + return _("UPDATE_PENDING_DISABLE"); + break; + case UPDATEACTION_RETAIN: + return _("UPDATE_PENDING_RETAIN"); + break; + default: + switch (updates[packageID].state) { + case UPDATESTATE_DISABLED: + return _("UPDATE_DISABLED"); + break; + case UPDATESTATE_ENABLED: + return _("UPDATE_ENABLED"); + break; + case UPDATESTATE_CORRUPT: + return _("UPDATE_CORRUPT"); + break; + default: + return _("UPDATE_NOTINSTALLED"); + } + } + break; + break; + case UPDATE_DLPERCENTAGE: + return updates[packageID].dlpercentage; + break; + default: + return __NULL__; + } } -/** Returns if our current game has updates available for any installed packages. */ bool Updates_Available(void) { return true; } -/** Toggle the installation of a package. Will return true if it was done. */ bool Updates_Toggle(int packageID) { - switch (updates[packageID].installed) { - case "": - case "rem": + if (packageID >= g_platform_update_count || packageID < 0i) { + print(sprintf("Updates_Toggle: Invalid package id %i!\n", packageID)); + return false; + } + + switch (updates[packageID].pending_action) { + case UPDATEACTION_INSTALL: + case UPDATEACTION_REINSTALL: + localcmd(sprintf("pkg rem %s\n", updates[packageID].name)); + break; + case UPDATEACTION_UNINSTALL: + case UPDATEACTION_DISABLE: localcmd(sprintf("pkg add %s\n", updates[packageID].name)); - updates[packageID].installed = "pending"; break; default: - localcmd(sprintf("pkg rem %s\n", updates[packageID].name)); - updates[packageID].installed = "rem"; + if (updates[packageID].state == UPDATESTATE_ENABLED) { + localcmd(sprintf("pkg rem %s\n", updates[packageID].name)); + } else { + localcmd(sprintf("pkg add %s\n", updates[packageID].name)); + } + break; } return true; } -/** Mark a package as pending installion. May return true/false if it succeeded in marking the package. */ bool Updates_Install(int packageID) { + if (packageID >= g_platform_update_count || packageID < 0i) { + print(sprintf("Updates_Install: Invalid package id %i!\n", packageID)); + return false; + } + localcmd(sprintf("pkg add %s\n", updates[packageID].name)); - updates[packageID].installed = "pending"; print(sprintf("Marking package %s for install.\n", updates[packageID].title)); return true; } -/** Mark a package as pending deletion. May return true/false if it succeeded in marking the package. */ bool Updates_Remove(int packageID) { + if (packageID >= g_platform_update_count || packageID < 0i) { + print(sprintf("Updates_Remove: Invalid package id %i!\n", packageID)); + return false; + } localcmd(sprintf("pkg rem %s\n", updates[packageID].name)); - updates[packageID].installed = "rem"; print(sprintf("Marking package %s for 'removal'.\n", updates[packageID].title)); return true; } -/** Apply all pending changes to packages. May return true/false if it succeeded in doing so. */ +bool +Updates_Destroy(int packageID) +{ + if (packageID >= g_platform_update_count || packageID < 0i) { + print(sprintf("Updates_Destroy: Invalid package id %i!\n", packageID)); + return false; + } + + localcmd(sprintf("pkg del %s\n", updates[packageID].name)); + print(sprintf("Marking package %s for 'deletion'.\n", updates[packageID].title)); + return true; +} + bool Updates_ApplyPendingChanges(void) { @@ -184,4 +388,20 @@ Updates_ApplyPendingChanges(void) localcmd("pkg apply\n"); print("Applying package changes.\n"); return true; +} + +void +Updater_URI_Callback(float id, float code, string data, int resourcebytes) +{ + if (code == 404) { + string gameDir = cvar_string("fs_game"); + games[GameLibrary_GetCurrentGame()].pkgname = strcat("cg_", gameDir, ";game_", gameDir, ";"); + + } else { + print(sprintf("URI: %d %d %S %i\n", id, code, data, resourcebytes)); + games[GameLibrary_GetCurrentGame()].pkgname = data; + } + + updater_package_status = UPDATER_INITIALIZED; + Updates_Refresh(); } \ No newline at end of file