diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 740b5f7..8ac7fd3 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -49,6 +49,14 @@ jobs: cp -r /tmp/bhoptimer/addons/sourcemod/scripting/include/* addons/sourcemod/scripting/include/ rm -rf /tmp/bhoptimer + - name: Inject release version + shell: bash + run: | + raw_tag="${{ github.event.inputs.tag_name || github.ref_name }}" + version="${raw_tag#v}" + updater_inc="addons/sourcemod/scripting/include/offstyledb_updater.inc" + sed -i "s|#define PLUGIN_VERSION \".*\"|#define PLUGIN_VERSION \"${version}\"|" "$updater_inc" + grep "#define PLUGIN_VERSION" "$updater_inc" - name: Run compiler shell: bash @@ -91,6 +99,8 @@ jobs: mkdir -p addons mv sourcemod addons/ zip -rq "../$zip_name" addons/ + cp addons/sourcemod/plugins/offstyledb.smx ../offstyledb.smx + cp addons/sourcemod/plugins/offstyledb_v3.smx ../offstyledb_v3.smx cd .. echo "zip_file=$zip_name" >> $GITHUB_ENV @@ -141,7 +151,7 @@ jobs: tag: ${{ github.ref_name }} name: Release ${{ github.ref_name }} body: ${{ steps.changelog.outputs.body }} - artifacts: "*.zip" + artifacts: "*.zip,offstyledb.smx,offstyledb_v3.smx" draft: false prerelease: false diff --git a/addons/sourcemod/scripting/include/offstyledb_core.inc b/addons/sourcemod/scripting/include/offstyledb_core.inc index 437c95b..55cf5ef 100644 --- a/addons/sourcemod/scripting/include/offstyledb_core.inc +++ b/addons/sourcemod/scripting/include/offstyledb_core.inc @@ -111,8 +111,11 @@ ArrayList gA_AllRecords = null; int gI_BatchSize = 5000; ConVar gCV_ExtendedDebugging = null; ConVar gCV_SubmitMode = null; // 0=WRs only, 1=all times (default) -ConVar gCV_BulkUploadMode = null; // -1=no times, 0=WRs only (default), 1=all times +ConVar gCV_BulkUploadMode = null; // -1=no times, 0=WRs only (default), 1=all times ConVar gCV_ReplayMode = null; // -1=never, 0=WRs only, 1=all times (default) +ConVar gCV_AutoUpdate = null; // 0=off, 1=check only, 2=check + download + auto-apply +char gS_LatestTag[32]; +bool gB_UpdateInFlight = false; // Helper function for debug logging void DebugPrint(const char[] format, any ...) @@ -206,15 +209,17 @@ void EnsureTempReplayDir() } #endif +#include + public Plugin myinfo = { name = "Offstyle Database", author = "shavit (Modified by Jeft & Tommy)", description = "Provides Offstyles with a database of bhop records.", #if defined SHAVIT_V3 - version = "3.1.0", + version = PLUGIN_VERSION ... " (shavit v3)", #else - version = "4.1.0", + version = PLUGIN_VERSION ... " (shavit v4)", #endif url = "" }; @@ -262,6 +267,8 @@ public void OnPluginStart() gCV_PublicIP = new Convar("OSdb_public_ip", "127.0.0.1", "Input the IP:PORT of the game server here. It will be used to identify the game server.", 0); gCV_Authentication = new Convar("OSdb_private_key", "super_secret_key", "Fill in your Offstyles Database API access key here. This key can be used to submit records to the database using your server key - abuse will lead to removal.", 0); + Updater_OnPluginStart(); + Convar.AutoExecConfig(); sv_cheats = FindConVar("sv_cheats"); @@ -313,6 +320,7 @@ public void OnConfigsExecuted() gCV_Authentication.SetString(""); GetStyleMapping(); + Updater_OnConfigsExecuted(); } void GetStyleMapping(bool forceRefresh = false) diff --git a/addons/sourcemod/scripting/include/offstyledb_updater.inc b/addons/sourcemod/scripting/include/offstyledb_updater.inc new file mode 100644 index 0000000..cf2078c --- /dev/null +++ b/addons/sourcemod/scripting/include/offstyledb_updater.inc @@ -0,0 +1,268 @@ +#if defined _offstyledb_updater_included + #endinput +#endif +#define _offstyledb_updater_included + +#pragma newdecls required +#pragma semicolon 1 + +// Injected by CI from the git tag (e.g. tag "v5.0.1" -> "5.0.1"). Leave as +// "dev" for local/untagged builds — CI rewrites this line before compile. +#define PLUGIN_VERSION "dev" + +#define UPDATER_API_URL "https://api.github.com/repos/offstyles/offstyle-plugins/releases/latest" +#define UPDATER_CHECK_EVERY 21600.0 // 6 hours + +#if defined SHAVIT_V3 + #define UPDATER_SMX_NAME "offstyledb_v3.smx" + #define UPDATER_SM_NAME "offstyledb_v3" +#else + #define UPDATER_SMX_NAME "offstyledb.smx" + #define UPDATER_SM_NAME "offstyledb" +#endif + +void Updater_OnPluginStart() +{ + gCV_AutoUpdate = new Convar("OSdb_autoupdate", "2", "Autoupdater: off (0), check + notify only (1), check + download + auto-apply (2, default).", 0, true, 0.0, true, 2.0); + + RegConsoleCmd("osdb_check_update", Command_CheckUpdate); + RegConsoleCmd("osdb_apply_update", Command_ApplyUpdate); + + CreateTimer(UPDATER_CHECK_EVERY, Timer_CheckUpdate, _, TIMER_REPEAT); +} + +void Updater_OnConfigsExecuted() +{ + Updater_CheckForUpdate(true); +} + +public Action Timer_CheckUpdate(Handle timer) +{ + Updater_CheckForUpdate(true); + return Plugin_Continue; +} + +void Updater_CheckForUpdate(bool silent) +{ + if (gCV_AutoUpdate.IntValue == 0) + { + if (!silent) + { + PrintToServer("[OSdb] Autoupdater is disabled (OSdb_autoupdate 0)."); + } + return; + } + + if (gB_UpdateInFlight) + { + if (!silent) + { + PrintToServer("[OSdb] An update check is already in progress."); + } + DebugPrint("[OSdb] Skipping update check, one is already in flight"); + return; + } + + DebugPrint("[OSdb] Checking for updates (silent=%s, current=%s)", silent ? "true" : "false", PLUGIN_VERSION); + + gB_UpdateInFlight = true; + + HTTPRequest req = new HTTPRequest(UPDATER_API_URL); + req.SetHeader("Accept", "application/vnd.github+json"); + req.SetHeader("User-Agent", "offstyledb-updater"); + req.Get(Callback_OnLatestRelease, silent ? 1 : 0); +} + +public void Callback_OnLatestRelease(HTTPResponse resp, any value) +{ + bool silent = (value == 1); + + if (resp.Status != HTTPStatus_OK || resp.Data == null) + { + LogError("[OSdb] Update check failed: status = %d", resp.Status); + gB_UpdateInFlight = false; + return; + } + + JSONObject data = view_as(resp.Data); + char tag[32]; + + if (!data.GetString("tag_name", tag, sizeof(tag))) + { + LogError("[OSdb] Update check: response missing tag_name"); + delete data; + gB_UpdateInFlight = false; + return; + } + + strcopy(gS_LatestTag, sizeof(gS_LatestTag), tag); + + // strip leading 'v' so "v5" compares as "5" + int tagOffset = (tag[0] == 'v' || tag[0] == 'V') ? 1 : 0; + bool newer = !StrEqual(tag[tagOffset], PLUGIN_VERSION); + + DebugPrint("[OSdb] Latest release tag: %s (current: %s)", tag, PLUGIN_VERSION); + + if (!newer) + { + if (!silent) + { + PrintToServer("[OSdb] Already up to date (version %s).", PLUGIN_VERSION); + } + delete data; + gB_UpdateInFlight = false; + return; + } + + PrintToServer("[OSdb] New version available: %s (current: %s)", tag, PLUGIN_VERSION); + LogMessage("[OSdb] New version available: %s (current: %s)", tag, PLUGIN_VERSION); + + if (gCV_AutoUpdate.IntValue < 2) + { + PrintToServer("[OSdb] Set OSdb_autoupdate 2 to auto-download, or update manually: https://github.com/offstyles/offstyle-plugins/releases/tag/%s", tag); + delete data; + gB_UpdateInFlight = false; + return; + } + + char downloadUrl[256]; + downloadUrl[0] = '\0'; + JSONArray assets = view_as(data.Get("assets")); + + if (assets != null) + { + for (int i = 0; i < assets.Length; i++) + { + JSONObject asset = view_as(assets.Get(i)); + char name[64]; + asset.GetString("name", name, sizeof(name)); + + if (StrEqual(name, UPDATER_SMX_NAME)) + { + asset.GetString("browser_download_url", downloadUrl, sizeof(downloadUrl)); + delete asset; + break; + } + + delete asset; + } + } + delete assets; + delete data; + + if (downloadUrl[0] == '\0') + { + LogError("[OSdb] Update check: no %s asset in release %s. Update manually: https://github.com/offstyles/offstyle-plugins/releases/tag/%s", UPDATER_SMX_NAME, tag, tag); + gB_UpdateInFlight = false; + return; + } + + DebugPrint("[OSdb] Downloading %s from %s", UPDATER_SMX_NAME, downloadUrl); + + char stagePath[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, stagePath, sizeof(stagePath), "plugins/" ... UPDATER_SMX_NAME ... ".new"); + + HTTPRequest dl = new HTTPRequest(downloadUrl); + dl.SetHeader("User-Agent", "offstyledb-updater"); + dl.DownloadFile(stagePath, Callback_OnSMXDownloaded); +} + +public void Callback_OnSMXDownloaded(HTTPStatus status, any value) +{ + gB_UpdateInFlight = false; + + if (status != HTTPStatus_OK) + { + LogError("[OSdb] Update download failed with status %d", status); + gS_LatestTag[0] = '\0'; + return; + } + + PrintToServer("[OSdb] Update %s downloaded, applying...", gS_LatestTag); + Updater_ApplyStaged(); +} + +// Renames the staged .smx.new over the live .smx and reloads the plugin. +// Returns true if the rename succeeded (the reload runs asynchronously after +// this frame). Leaves the staged file in place on failure so it can be retried. +bool Updater_ApplyStaged() +{ + char livePath[PLATFORM_MAX_PATH]; + char stagePath[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, livePath, sizeof(livePath), "plugins/" ... UPDATER_SMX_NAME); + BuildPath(Path_SM, stagePath, sizeof(stagePath), "plugins/" ... UPDATER_SMX_NAME ... ".new"); + + if (!FileExists(stagePath)) + { + LogError("[OSdb] Apply update: staged file %s is missing", stagePath); + return false; + } + + if (FileExists(livePath) && !DeleteFile(livePath)) + { + LogError("[OSdb] Apply update failed: could not delete %s", livePath); + return false; + } + + if (!RenameFile(livePath, stagePath)) + { + LogError("[OSdb] Apply update failed: RenameFile %s -> %s", stagePath, livePath); + return false; + } + + LogMessage("[OSdb] Applied update to version %s, reloading plugin.", gS_LatestTag); + PrintToServer("[OSdb] Update %s applied, reloading plugin...", gS_LatestTag); + + ServerCommand("sm plugins reload " ... UPDATER_SM_NAME); + return true; +} + +public Action Command_CheckUpdate(int client, int args) +{ + if (!IsUpdaterAuthorized(client)) + { + ReplyToCommand(client, "[OSdb] You are not permitted to run update commands."); + return Plugin_Handled; + } + + ReplyToCommand(client, "[OSdb] Checking for updates..."); + Updater_CheckForUpdate(false); + return Plugin_Handled; +} + +public Action Command_ApplyUpdate(int client, int args) +{ + if (!IsUpdaterAuthorized(client)) + { + ReplyToCommand(client, "[OSdb] You are not permitted to run update commands."); + return Plugin_Handled; + } + + if (Updater_ApplyStaged()) + { + ReplyToCommand(client, "[OSdb] Update applied. Reloading plugin..."); + } + else + { + ReplyToCommand(client, "[OSdb] No staged update available, or apply failed. Check logs."); + } + return Plugin_Handled; +} + +bool IsUpdaterAuthorized(int client) +{ + if (client == 0) + { + return true; // server console + } + + int iSteamID = GetSteamAccountID(client); + for (int i = 0; i < sizeof(gI_SteamIDWhitelist); i++) + { + if (iSteamID == gI_SteamIDWhitelist[i]) + { + return true; + } + } + return false; +}