EventNotifier should show dates/times in the client's timezone

This commit is contained in:
2026-09-29 18:58:22 +01:00
parent cbe461ac26
commit d6f3d622dd
6 changed files with 7787 additions and 14 deletions
+196 -1
View File
@@ -1,5 +1,6 @@
#include <sourcemod>
#include <ripext>
#include <geoip>
#define EVENTNOTIFIER_CORE
#include <EventNotifier>
@@ -7,11 +8,22 @@
#pragma semicolon 1
#pragma newdecls required
#define PLUGIN_VERSION "1.0"
#define PLUGIN_VERSION "1.1"
#define TIMEZONE_FILE "configs/eventnotifier_timezones.cfg"
#define TIMEZONE_NAME_LEN 64
enum struct TzPeriod
{
int start; // unix timestamp this offset applies from
int offset; // seconds east of UTC
char abbr[12];
}
ConVar g_cvUrl;
ConVar g_cvServer;
ConVar g_cvInterval;
ConVar g_cvFallbackZone;
ArrayList g_Events;
Handle g_hRefreshTimer;
@@ -20,6 +32,10 @@ bool g_bEverFetched;
GlobalForward g_fwdOnEventsUpdated;
// Zone name -> ArrayList of TzPeriod, ascending by start.
StringMap g_Timezones;
char g_sClientZone[MAXPLAYERS + 1][TIMEZONE_NAME_LEN];
public Plugin myinfo =
{
name = "EventNotifier",
@@ -36,6 +52,7 @@ public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max
CreateNative("EventNotifier_FindEventByNumber", Native_FindEventByNumber);
CreateNative("EventNotifier_GetEvent", Native_GetEvent);
CreateNative("EventNotifier_Refresh", Native_Refresh);
CreateNative("EventNotifier_FormatClientTime", Native_FormatClientTime);
RegPluginLibrary("EventNotifier");
return APLRes_Success;
}
@@ -51,6 +68,8 @@ public void OnPluginStart()
"Value of the 'server' query parameter sent to the events API");
g_cvInterval = CreateConVar("sm_event_refresh_interval", "1800",
"How often to re-fetch the event list, in seconds", _, true, 30.0);
g_cvFallbackZone = CreateConVar("sm_event_fallback_timezone", "UTC",
"Timezone (IANA name, e.g. Europe/London) used for players whose timezone cannot be looked up");
g_cvUrl.AddChangeHook(OnSourceChanged);
g_cvServer.AddChangeHook(OnSourceChanged);
@@ -59,6 +78,31 @@ public void OnPluginStart()
AutoExecConfig(true, "EventNotifier");
RegAdminCmd("sm_event_refresh", Command_EventRefresh, ADMFLAG_GENERIC, "Re-fetches the event list now");
LoadTimezones();
// Late load: look up everyone already on the server.
for (int client = 1; client <= MaxClients; client++)
{
if (IsClientConnected(client))
OnClientConnected(client);
}
}
public void OnClientConnected(int client)
{
g_sClientZone[client][0] = '\0';
if (IsFakeClient(client))
return;
char ip[32];
if (GetClientIP(client, ip, sizeof(ip)) && !GeoipTimezone(ip, g_sClientZone[client], sizeof(g_sClientZone[])))
g_sClientZone[client][0] = '\0';
}
public void OnClientDisconnect(int client)
{
g_sClientZone[client][0] = '\0';
}
public void OnConfigsExecuted()
@@ -131,6 +175,19 @@ public any Native_Refresh(Handle plugin, int numParams)
return 0;
}
public any Native_FormatClientTime(Handle plugin, int numParams)
{
int client = GetNativeCell(1);
if (client < 0 || client > MaxClients)
return ThrowNativeError(SP_ERROR_NATIVE, "Invalid client index %d", client);
int maxlen = GetNativeCell(4);
char[] buffer = new char[maxlen];
FormatClientTime(client, GetNativeCell(2), buffer, maxlen, GetNativeCell(5));
SetNativeString(3, buffer, maxlen);
return 0;
}
// ---------------------------------------------------------------------------
// Fetching
// ---------------------------------------------------------------------------
@@ -318,6 +375,130 @@ int FindEventByNumber(int number)
return -1;
}
// ---------------------------------------------------------------------------
// Timezones
// ---------------------------------------------------------------------------
// Loads the zone table generated by tools/gen_timezones.py.
void LoadTimezones()
{
g_Timezones = new StringMap();
char path[PLATFORM_MAX_PATH];
BuildPath(Path_SM, path, sizeof(path), TIMEZONE_FILE);
KeyValues kv = new KeyValues("Timezones");
if (!kv.ImportFromFile(path))
{
LogError("Could not read %s, all times will be shown in UTC", path);
delete kv;
return;
}
if (kv.GotoFirstSubKey())
{
do
{
char zone[TIMEZONE_NAME_LEN];
kv.GetSectionName(zone, sizeof(zone));
ArrayList periods = new ArrayList(sizeof(TzPeriod));
if (kv.GotoFirstSubKey(false))
{
do
{
char key[16], value[32], parts[2][16];
kv.GetSectionName(key, sizeof(key));
kv.GetString(NULL_STRING, value, sizeof(value));
if (ExplodeString(value, " ", parts, sizeof(parts), sizeof(parts[])) != 2)
continue;
TzPeriod period;
period.start = StringToInt(key);
period.offset = StringToInt(parts[0]);
strcopy(period.abbr, sizeof(period.abbr), parts[1]);
periods.PushArray(period);
} while (kv.GotoNextKey(false));
kv.GoBack();
}
if (periods.Length > 0)
{
periods.SortCustom(SortPeriodsByStart);
g_Timezones.SetValue(zone, periods);
}
else
{
delete periods;
}
} while (kv.GotoNextKey());
}
delete kv;
LogMessage("Loaded %d timezone(s)", g_Timezones.Size);
}
int SortPeriodsByStart(int index1, int index2, Handle array, Handle hndl)
{
ArrayList list = view_as<ArrayList>(array);
int a = list.Get(index1, TzPeriod::start);
int b = list.Get(index2, TzPeriod::start);
return (a == b) ? 0 : ((a < b) ? -1 : 1);
}
// Finds the offset and abbreviation in effect at stamp for the client's zone,
// falling back to sm_event_fallback_timezone and then plain UTC.
void GetClientZonePeriod(int client, int stamp, TzPeriod out)
{
ArrayList periods;
if (client <= 0 || !g_sClientZone[client][0] || !g_Timezones.GetValue(g_sClientZone[client], periods))
{
char fallback[TIMEZONE_NAME_LEN];
g_cvFallbackZone.GetString(fallback, sizeof(fallback));
if (!g_Timezones.GetValue(fallback, periods))
periods = null;
}
if (periods == null)
{
out.start = 0;
out.offset = 0;
strcopy(out.abbr, sizeof(out.abbr), "UTC");
return;
}
periods.GetArray(0, out);
int count = periods.Length;
for (int i = 1; i < count && periods.Get(i, TzPeriod::start) <= stamp; i++)
periods.GetArray(i, out);
}
// Formats stamp in the client's timezone as "Thu 01 Oct 2026, 20:00 BST"
// (or without the year). FormatTime can't be used, it only knows the server's zone.
void FormatClientTime(int client, int stamp, char[] buffer, int maxlen, bool withYear)
{
static const char weekdays[][] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
static const char months[][] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
TzPeriod period;
GetClientZonePeriod(client, stamp, period);
int local = stamp + period.offset;
int days = local / 86400;
int secs = local % 86400;
int year, month, day;
CivilFromDays(days, year, month, day);
// 1970-01-01 was a Thursday.
int weekday = (days + 4) % 7;
if (withYear)
Format(buffer, maxlen, "%s %02d %s %d, %02d:%02d %s", weekdays[weekday], day, months[month - 1], year, secs / 3600, (secs % 3600) / 60, period.abbr);
else
Format(buffer, maxlen, "%s %02d %s, %02d:%02d %s", weekdays[weekday], day, months[month - 1], secs / 3600, (secs % 3600) / 60, period.abbr);
}
// ---------------------------------------------------------------------------
// Time helpers
// ---------------------------------------------------------------------------
@@ -390,3 +571,17 @@ int DaysFromCivil(int y, int m, int d)
int doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
return era * 146097 + doe - 719468;
}
// Inverse of DaysFromCivil: date for a day count since 1970-01-01.
void CivilFromDays(int z, int &y, int &m, int &d)
{
z += 719468;
int era = ((z >= 0) ? z : z - 146096) / 146097;
int doe = z - era * 146097;
int yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
int doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
int mp = (5 * doy + 2) / 153;
d = doy - (153 * mp + 2) / 5 + 1;
m = (mp < 10) ? mp + 3 : mp - 9;
y = yoe + era * 400 + ((m <= 2) ? 1 : 0);
}