Add EventNotifier
This commit is contained in:
@@ -0,0 +1,392 @@
|
|||||||
|
#include <sourcemod>
|
||||||
|
#include <ripext>
|
||||||
|
|
||||||
|
#define EVENTNOTIFIER_CORE
|
||||||
|
#include <EventNotifier>
|
||||||
|
|
||||||
|
#pragma semicolon 1
|
||||||
|
#pragma newdecls required
|
||||||
|
|
||||||
|
#define PLUGIN_VERSION "1.0"
|
||||||
|
|
||||||
|
ConVar g_cvUrl;
|
||||||
|
ConVar g_cvServer;
|
||||||
|
ConVar g_cvInterval;
|
||||||
|
|
||||||
|
ArrayList g_Events;
|
||||||
|
Handle g_hRefreshTimer;
|
||||||
|
bool g_bFetching;
|
||||||
|
bool g_bEverFetched;
|
||||||
|
|
||||||
|
GlobalForward g_fwdOnEventsUpdated;
|
||||||
|
|
||||||
|
public Plugin myinfo =
|
||||||
|
{
|
||||||
|
name = "EventNotifier",
|
||||||
|
author = "marisakirisame.net",
|
||||||
|
description = "Fetches upcoming Unloze events and exposes them to other plugins",
|
||||||
|
version = PLUGIN_VERSION,
|
||||||
|
url = "https://marisakirisame.net"
|
||||||
|
};
|
||||||
|
|
||||||
|
public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
|
||||||
|
{
|
||||||
|
CreateNative("EventNotifier_GetEventCount", Native_GetEventCount);
|
||||||
|
CreateNative("EventNotifier_HasFetched", Native_HasFetched);
|
||||||
|
CreateNative("EventNotifier_FindEventByNumber", Native_FindEventByNumber);
|
||||||
|
CreateNative("EventNotifier_GetEvent", Native_GetEvent);
|
||||||
|
CreateNative("EventNotifier_Refresh", Native_Refresh);
|
||||||
|
RegPluginLibrary("EventNotifier");
|
||||||
|
return APLRes_Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnPluginStart()
|
||||||
|
{
|
||||||
|
g_Events = new ArrayList(sizeof(EventInfo));
|
||||||
|
g_fwdOnEventsUpdated = new GlobalForward("EventNotifier_OnEventsUpdated", ET_Ignore);
|
||||||
|
|
||||||
|
g_cvUrl = CreateConVar("sm_event_url", "https://marisakirisame.net/unloze-events.json",
|
||||||
|
"Base URL of the Unloze events API (query parameters are appended automatically)");
|
||||||
|
g_cvServer = CreateConVar("sm_event_server", "ze",
|
||||||
|
"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_cvUrl.AddChangeHook(OnSourceChanged);
|
||||||
|
g_cvServer.AddChangeHook(OnSourceChanged);
|
||||||
|
g_cvInterval.AddChangeHook(OnIntervalChanged);
|
||||||
|
|
||||||
|
AutoExecConfig(true, "EventNotifier");
|
||||||
|
|
||||||
|
RegAdminCmd("sm_event_refresh", Command_EventRefresh, ADMFLAG_GENERIC, "Re-fetches the event list now");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnConfigsExecuted()
|
||||||
|
{
|
||||||
|
RestartRefreshTimer();
|
||||||
|
FetchEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnSourceChanged(ConVar convar, const char[] oldValue, const char[] newValue)
|
||||||
|
{
|
||||||
|
FetchEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnIntervalChanged(ConVar convar, const char[] oldValue, const char[] newValue)
|
||||||
|
{
|
||||||
|
RestartRefreshTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RestartRefreshTimer()
|
||||||
|
{
|
||||||
|
delete g_hRefreshTimer;
|
||||||
|
g_hRefreshTimer = CreateTimer(g_cvInterval.FloatValue, Timer_Refresh, _, TIMER_REPEAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Action Timer_Refresh(Handle timer)
|
||||||
|
{
|
||||||
|
FetchEvents();
|
||||||
|
return Plugin_Continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Natives
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
public any Native_GetEventCount(Handle plugin, int numParams)
|
||||||
|
{
|
||||||
|
return g_Events.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public any Native_HasFetched(Handle plugin, int numParams)
|
||||||
|
{
|
||||||
|
return g_bEverFetched;
|
||||||
|
}
|
||||||
|
|
||||||
|
public any Native_FindEventByNumber(Handle plugin, int numParams)
|
||||||
|
{
|
||||||
|
return FindEventByNumber(GetNativeCell(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public any Native_GetEvent(Handle plugin, int numParams)
|
||||||
|
{
|
||||||
|
int index = GetNativeCell(1);
|
||||||
|
int size = GetNativeCell(3);
|
||||||
|
|
||||||
|
if (size < sizeof(EventInfo))
|
||||||
|
return ThrowNativeError(SP_ERROR_NATIVE, "EventInfo buffer too small (%d < %d), recompile against the current EventNotifier.inc", size, sizeof(EventInfo));
|
||||||
|
|
||||||
|
if (index < 0 || index >= g_Events.Length)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
EventInfo ev;
|
||||||
|
g_Events.GetArray(index, ev);
|
||||||
|
SetNativeArray(2, ev, sizeof(EventInfo));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public any Native_Refresh(Handle plugin, int numParams)
|
||||||
|
{
|
||||||
|
FetchEvents();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Fetching
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void FetchEvents()
|
||||||
|
{
|
||||||
|
if (g_bFetching)
|
||||||
|
return;
|
||||||
|
|
||||||
|
char url[256];
|
||||||
|
g_cvUrl.GetString(url, sizeof(url));
|
||||||
|
if (!url[0])
|
||||||
|
{
|
||||||
|
LogError("sm_event_url is empty, cannot fetch events");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char server[64];
|
||||||
|
g_cvServer.GetString(server, sizeof(server));
|
||||||
|
|
||||||
|
HTTPRequest request = new HTTPRequest(url);
|
||||||
|
request.AppendQueryParam("server", "%s", server);
|
||||||
|
request.AppendQueryParam("future", "1");
|
||||||
|
request.SetHeader("Accept", "application/json");
|
||||||
|
request.Timeout = 20;
|
||||||
|
|
||||||
|
g_bFetching = true;
|
||||||
|
request.Get(OnEventsFetched);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnEventsFetched(HTTPResponse response, any value, const char[] error)
|
||||||
|
{
|
||||||
|
g_bFetching = false;
|
||||||
|
|
||||||
|
if (error[0])
|
||||||
|
{
|
||||||
|
LogError("Event fetch failed: %s", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.Status != HTTPStatus_OK)
|
||||||
|
{
|
||||||
|
LogError("Event fetch failed: HTTP %d", response.Status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSON data = response.Data;
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
LogError("Event fetch failed: response was not valid JSON");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject root = view_as<JSONObject>(data);
|
||||||
|
if (!root.HasKey("events") || root.IsNull("events"))
|
||||||
|
{
|
||||||
|
LogError("Event fetch failed: response has no 'events' array");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONArray events = view_as<JSONArray>(root.Get("events"));
|
||||||
|
ArrayList parsed = new ArrayList(sizeof(EventInfo));
|
||||||
|
|
||||||
|
int count = events.Length;
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
if (events.IsNull(i))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
JSONObject obj = view_as<JSONObject>(events.Get(i));
|
||||||
|
EventInfo ev;
|
||||||
|
ParseEvent(obj, ev);
|
||||||
|
delete obj;
|
||||||
|
|
||||||
|
parsed.PushArray(ev);
|
||||||
|
}
|
||||||
|
delete events;
|
||||||
|
|
||||||
|
// Soonest first so "the next N events" means something regardless of API order.
|
||||||
|
parsed.SortCustom(SortEventsByStart);
|
||||||
|
|
||||||
|
int previous = g_Events.Length;
|
||||||
|
delete g_Events;
|
||||||
|
g_Events = parsed;
|
||||||
|
|
||||||
|
if (!g_bEverFetched || previous != g_Events.Length)
|
||||||
|
LogMessage("Fetched %d upcoming event(s)", g_Events.Length);
|
||||||
|
g_bEverFetched = true;
|
||||||
|
|
||||||
|
Call_StartForward(g_fwdOnEventsUpdated);
|
||||||
|
Call_Finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ascending by start time, with unknown (0) start times last.
|
||||||
|
int SortEventsByStart(int index1, int index2, Handle array, Handle hndl)
|
||||||
|
{
|
||||||
|
ArrayList list = view_as<ArrayList>(array);
|
||||||
|
int a = list.Get(index1, EventInfo::start);
|
||||||
|
int b = list.Get(index2, EventInfo::start);
|
||||||
|
|
||||||
|
if (a == b)
|
||||||
|
return 0;
|
||||||
|
if (a == 0)
|
||||||
|
return 1;
|
||||||
|
if (b == 0)
|
||||||
|
return -1;
|
||||||
|
return (a < b) ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseEvent(JSONObject obj, EventInfo ev)
|
||||||
|
{
|
||||||
|
char buf[64];
|
||||||
|
|
||||||
|
ev.number = (obj.HasKey("eventNumber") && !obj.IsNull("eventNumber")) ? obj.GetInt("eventNumber") : 0;
|
||||||
|
|
||||||
|
GetJsonString(obj, "name", ev.name, sizeof(ev.name));
|
||||||
|
if (!ev.name[0])
|
||||||
|
GetJsonString(obj, "title", ev.name, sizeof(ev.name));
|
||||||
|
|
||||||
|
GetJsonString(obj, "server", ev.server, sizeof(ev.server));
|
||||||
|
GetJsonString(obj, "rewards", ev.rewards, sizeof(ev.rewards));
|
||||||
|
GetJsonString(obj, "leader", ev.leader, sizeof(ev.leader));
|
||||||
|
GetJsonString(obj, "kickers", ev.kickers, sizeof(ev.kickers));
|
||||||
|
|
||||||
|
ev.start = 0;
|
||||||
|
if (GetJsonString(obj, "start", buf, sizeof(buf)))
|
||||||
|
ev.start = ParseIso8601(buf);
|
||||||
|
|
||||||
|
ev.end = 0;
|
||||||
|
if (GetJsonString(obj, "end", buf, sizeof(buf)))
|
||||||
|
ev.end = ParseIso8601(buf);
|
||||||
|
|
||||||
|
ev.maps[0] = '\0';
|
||||||
|
if (obj.HasKey("maps") && !obj.IsNull("maps"))
|
||||||
|
{
|
||||||
|
JSONArray maps = view_as<JSONArray>(obj.Get("maps"));
|
||||||
|
int len = maps.Length;
|
||||||
|
for (int i = 0; i < len; i++)
|
||||||
|
{
|
||||||
|
char map[128];
|
||||||
|
if (!maps.GetString(i, map, sizeof(map)) || !map[0])
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (ev.maps[0])
|
||||||
|
StrCat(ev.maps, sizeof(ev.maps), ", ");
|
||||||
|
StrCat(ev.maps, sizeof(ev.maps), map);
|
||||||
|
}
|
||||||
|
delete maps;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GetJsonString(JSONObject obj, const char[] key, char[] buffer, int maxlen)
|
||||||
|
{
|
||||||
|
buffer[0] = '\0';
|
||||||
|
if (!obj.HasKey(key) || obj.IsNull(key))
|
||||||
|
return false;
|
||||||
|
return obj.GetString(key, buffer, maxlen) && buffer[0] != '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Commands
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
public Action Command_EventRefresh(int client, int args)
|
||||||
|
{
|
||||||
|
if (g_bFetching)
|
||||||
|
{
|
||||||
|
ReplyToCommand(client, "[Events] A fetch is already in progress.");
|
||||||
|
return Plugin_Handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
FetchEvents();
|
||||||
|
ReplyToCommand(client, "[Events] Refreshing the event list.");
|
||||||
|
return Plugin_Handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
int FindEventByNumber(int number)
|
||||||
|
{
|
||||||
|
int count = g_Events.Length;
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
if (g_Events.Get(i, EventInfo::number) == number)
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Time helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Parses dates into a UTC unix timestamp.
|
||||||
|
// Returns 0 when the string cannot be parsed.
|
||||||
|
int ParseIso8601(const char[] str)
|
||||||
|
{
|
||||||
|
int year, month, day, hour, minute, second;
|
||||||
|
if (!ReadFixedInt(str, 0, 4, year) || str[4] != '-'
|
||||||
|
|| !ReadFixedInt(str, 5, 2, month) || str[7] != '-'
|
||||||
|
|| !ReadFixedInt(str, 8, 2, day))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
int pos = 10;
|
||||||
|
if (str[pos] == 'T' || str[pos] == ' ')
|
||||||
|
{
|
||||||
|
if (!ReadFixedInt(str, 11, 2, hour) || str[13] != ':' || !ReadFixedInt(str, 14, 2, minute))
|
||||||
|
return 0;
|
||||||
|
pos = 16;
|
||||||
|
if (str[pos] == ':')
|
||||||
|
{
|
||||||
|
if (!ReadFixedInt(str, 17, 2, second))
|
||||||
|
return 0;
|
||||||
|
pos = 19;
|
||||||
|
}
|
||||||
|
if (str[pos] == '.')
|
||||||
|
{
|
||||||
|
pos++;
|
||||||
|
while (IsCharNumeric(str[pos]))
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int offset = 0;
|
||||||
|
if (str[pos] == '+' || str[pos] == '-')
|
||||||
|
{
|
||||||
|
int sign = (str[pos] == '-') ? -1 : 1;
|
||||||
|
int offHour, offMinute;
|
||||||
|
if (!ReadFixedInt(str, pos + 1, 2, offHour))
|
||||||
|
return 0;
|
||||||
|
int minPos = (str[pos + 3] == ':') ? pos + 4 : pos + 3;
|
||||||
|
ReadFixedInt(str, minPos, 2, offMinute);
|
||||||
|
offset = sign * (offHour * 3600 + offMinute * 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
int days = DaysFromCivil(year, month, day);
|
||||||
|
return days * 86400 + hour * 3600 + minute * 60 + second - offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReadFixedInt(const char[] str, int pos, int digits, int &out)
|
||||||
|
{
|
||||||
|
out = 0;
|
||||||
|
for (int i = 0; i < digits; i++)
|
||||||
|
{
|
||||||
|
if (!IsCharNumeric(str[pos + i]))
|
||||||
|
return false;
|
||||||
|
out = out * 10 + (str[pos + i] - '0');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Days since 1970-01-01 for a proper date.
|
||||||
|
int DaysFromCivil(int y, int m, int d)
|
||||||
|
{
|
||||||
|
y -= (m <= 2) ? 1 : 0;
|
||||||
|
int era = ((y >= 0) ? y : y - 399) / 400;
|
||||||
|
int yoe = y - era * 400;
|
||||||
|
int doy = (153 * (m + ((m > 2) ? -3 : 9)) + 2) / 5 + d - 1;
|
||||||
|
int doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
||||||
|
return era * 146097 + doe - 719468;
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
#include <sourcemod>
|
||||||
|
#include <multicolors>
|
||||||
|
#include <EventNotifier>
|
||||||
|
|
||||||
|
#pragma semicolon 1
|
||||||
|
#pragma newdecls required
|
||||||
|
|
||||||
|
#define PLUGIN_VERSION "1.0"
|
||||||
|
|
||||||
|
// Shortest automatic announcement interval, in seconds. Values below this are
|
||||||
|
// raised to it; 0 or below disables the timer entirely.
|
||||||
|
#define MIN_ANNOUNCE_INTERVAL 60.0
|
||||||
|
|
||||||
|
// Visible characters kept from the event name in chat.
|
||||||
|
#define ANNOUNCE_NAME_MAX 64
|
||||||
|
// Visible characters allowed for the map list before " +N more" is appended.
|
||||||
|
#define ANNOUNCE_MAPS_BUDGET 120
|
||||||
|
|
||||||
|
ConVar g_cvInterval;
|
||||||
|
ConVar g_cvCount;
|
||||||
|
|
||||||
|
Handle g_hAnnounceTimer;
|
||||||
|
int g_iRotation;
|
||||||
|
|
||||||
|
public Plugin myinfo =
|
||||||
|
{
|
||||||
|
name = "EventNotifier Announcements",
|
||||||
|
author = "marisakirisame.net",
|
||||||
|
description = "Periodically announces upcoming Unloze events in chat",
|
||||||
|
version = PLUGIN_VERSION,
|
||||||
|
url = "https://marisakirisame.net"
|
||||||
|
};
|
||||||
|
|
||||||
|
public void OnPluginStart()
|
||||||
|
{
|
||||||
|
g_cvInterval = CreateConVar("sm_event_announce_interval", "120",
|
||||||
|
"Seconds between event announcements in chat, -1 to disable automatic announcements (sm_event_announce still works)", _, true, -1.0);
|
||||||
|
g_cvCount = CreateConVar("sm_event_announce_count", "3",
|
||||||
|
"Rotate announcements through the next N upcoming events, one per announcement", _, true, 1.0, true, 20.0);
|
||||||
|
|
||||||
|
// CreateConVar keeps the old bounds when the cvar already exists (plugin reload),
|
||||||
|
// so apply the lower bound explicitly.
|
||||||
|
g_cvInterval.SetBounds(ConVarBound_Lower, true, -1.0);
|
||||||
|
g_cvInterval.AddChangeHook(OnIntervalChanged);
|
||||||
|
|
||||||
|
AutoExecConfig(true, "EventNotifierAnnouncements");
|
||||||
|
|
||||||
|
RegAdminCmd("sm_event_announce", Command_Announce, ADMFLAG_GENERIC, "Posts the next event announcement in chat now");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnConfigsExecuted()
|
||||||
|
{
|
||||||
|
RestartAnnounceTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnIntervalChanged(ConVar convar, const char[] oldValue, const char[] newValue)
|
||||||
|
{
|
||||||
|
RestartAnnounceTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RestartAnnounceTimer()
|
||||||
|
{
|
||||||
|
delete g_hAnnounceTimer;
|
||||||
|
|
||||||
|
float interval = g_cvInterval.FloatValue;
|
||||||
|
if (interval <= 0.0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (interval < MIN_ANNOUNCE_INTERVAL)
|
||||||
|
interval = MIN_ANNOUNCE_INTERVAL;
|
||||||
|
|
||||||
|
g_hAnnounceTimer = CreateTimer(interval, Timer_Announce, _, TIMER_REPEAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Action Timer_Announce(Handle timer)
|
||||||
|
{
|
||||||
|
PostAnnouncement();
|
||||||
|
return Plugin_Continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Action Command_Announce(int client, int args)
|
||||||
|
{
|
||||||
|
if (PostAnnouncement())
|
||||||
|
ReplyToCommand(client, "[Events] Announcement posted.");
|
||||||
|
else
|
||||||
|
ReplyToCommand(client, "[Events] No upcoming events to announce.");
|
||||||
|
return Plugin_Handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Announcement
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Posts one event from the rotation window. Returns false when there is nothing to post.
|
||||||
|
bool PostAnnouncement()
|
||||||
|
{
|
||||||
|
int count = EventNotifier_GetEventCount();
|
||||||
|
if (count <= 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
int window = g_cvCount.IntValue;
|
||||||
|
if (window > count)
|
||||||
|
window = count;
|
||||||
|
|
||||||
|
int index = g_iRotation % window;
|
||||||
|
g_iRotation = (index + 1) % window;
|
||||||
|
|
||||||
|
EventInfo ev;
|
||||||
|
if (!EventNotifier_GetEvent(index, ev))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
char name[ANNOUNCE_NAME_MAX + 1];
|
||||||
|
strcopy(name, sizeof(name), ev.name);
|
||||||
|
if (!name[0])
|
||||||
|
strcopy(name, sizeof(name), "Unnamed event");
|
||||||
|
|
||||||
|
int now = GetTime();
|
||||||
|
char span[32];
|
||||||
|
if (ev.start <= 0)
|
||||||
|
{
|
||||||
|
CPrintToChatAll("{darkorange}[Events] {yellow}#%d: {lightgreen}%s {grey}date TBA", ev.number, name);
|
||||||
|
}
|
||||||
|
else if (now < ev.start)
|
||||||
|
{
|
||||||
|
EventNotifier_FormatDuration(ev.start - now, span, sizeof(span));
|
||||||
|
CPrintToChatAll("{darkorange}[Events] {yellow}#%d: {lightgreen}%s {lime}starts in %s", ev.number, name, span);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EventNotifier_FormatDuration(now - ev.start, span, sizeof(span));
|
||||||
|
CPrintToChatAll("{darkorange}[Events] {yellow}#%d: {lightgreen}%s {orchid}started %s ago", ev.number, name, span);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ev.maps[0])
|
||||||
|
{
|
||||||
|
char mapsLine[ANNOUNCE_MAPS_BUDGET + 132];
|
||||||
|
int more = BuildMapsLine(ev.maps, mapsLine, sizeof(mapsLine), ANNOUNCE_MAPS_BUDGET);
|
||||||
|
if (more > 0)
|
||||||
|
CPrintToChatAll("{darkorange}[Events] {white}Maps: {lightgreen}%s {grey}+%d more", mapsLine, more);
|
||||||
|
else
|
||||||
|
CPrintToChatAll("{darkorange}[Events] {white}Maps: {lightgreen}%s", mapsLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
CPrintToChatAll("{darkorange}[Events] {white}Type {lime}!event %d {white}for details and the full map list.", ev.number);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copies ", "-separated items from maps into out while the visible length stays
|
||||||
|
// within budget. Always emits at least the first item. Returns how many items
|
||||||
|
// were left out (0 when everything fit).
|
||||||
|
int BuildMapsLine(const char[] maps, char[] out, int maxlen, int budget)
|
||||||
|
{
|
||||||
|
// Count from the raw string so "+N more" is right even past the explode limit.
|
||||||
|
int total = 1;
|
||||||
|
for (int i = 0; maps[i]; i++)
|
||||||
|
{
|
||||||
|
if (maps[i] == ',' && maps[i + 1] == ' ')
|
||||||
|
total++;
|
||||||
|
}
|
||||||
|
|
||||||
|
char parts[32][128];
|
||||||
|
int count = ExplodeString(maps, ", ", parts, sizeof(parts), sizeof(parts[]));
|
||||||
|
|
||||||
|
out[0] = '\0';
|
||||||
|
int used = 0;
|
||||||
|
int shown = 0;
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
int add = strlen(parts[i]) + ((shown > 0) ? 2 : 0);
|
||||||
|
if (shown > 0 && used + add > budget)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (shown > 0)
|
||||||
|
StrCat(out, maxlen, ", ");
|
||||||
|
StrCat(out, maxlen, parts[i]);
|
||||||
|
used += add;
|
||||||
|
shown++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return total - shown;
|
||||||
|
}
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
#include <sourcemod>
|
||||||
|
#include <EventNotifier>
|
||||||
|
|
||||||
|
#pragma semicolon 1
|
||||||
|
#pragma newdecls required
|
||||||
|
|
||||||
|
#define PLUGIN_VERSION "1.1"
|
||||||
|
#define CHAT_PREFIX "\x04[Events]\x01"
|
||||||
|
|
||||||
|
#define PANEL_LINE_WIDTH 60
|
||||||
|
|
||||||
|
#define KEY_BACK 8
|
||||||
|
#define KEY_EXIT 10
|
||||||
|
|
||||||
|
public Plugin myinfo =
|
||||||
|
{
|
||||||
|
name = "EventNotifier Menu",
|
||||||
|
author = "marisakirisame.net",
|
||||||
|
description = "Shows upcoming Unloze events in-game via sm_event",
|
||||||
|
version = PLUGIN_VERSION,
|
||||||
|
url = "https://marisakirisame.net"
|
||||||
|
};
|
||||||
|
|
||||||
|
public void OnPluginStart()
|
||||||
|
{
|
||||||
|
RegConsoleCmd("sm_event", Command_EventMenu, "Shows upcoming Unloze events. Usage: sm_event [event number]");
|
||||||
|
RegConsoleCmd("sm_events", Command_EventMenu, "Shows upcoming Unloze events. Usage: sm_events [event number]");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Commands
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
public Action Command_EventMenu(int client, int args)
|
||||||
|
{
|
||||||
|
if (client <= 0 || !IsClientInGame(client))
|
||||||
|
{
|
||||||
|
ReplyToCommand(client, "This command can only be used in-game.");
|
||||||
|
return Plugin_Handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EventNotifier_GetEventCount() == 0)
|
||||||
|
{
|
||||||
|
if (EventNotifier_HasFetched())
|
||||||
|
PrintToChat(client, "%s There are no upcoming events.", CHAT_PREFIX);
|
||||||
|
else
|
||||||
|
PrintToChat(client, "%s The event list is not available right now, try again later.", CHAT_PREFIX);
|
||||||
|
return Plugin_Handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args >= 1)
|
||||||
|
{
|
||||||
|
char arg[16];
|
||||||
|
GetCmdArg(1, arg, sizeof(arg));
|
||||||
|
int number = StringToInt(arg);
|
||||||
|
int index = EventNotifier_FindEventByNumber(number);
|
||||||
|
if (index == -1)
|
||||||
|
{
|
||||||
|
PrintToChat(client, "%s Event #%d is not in the upcoming list.", CHAT_PREFIX, number);
|
||||||
|
return Plugin_Handled;
|
||||||
|
}
|
||||||
|
ShowEventDetails(client, index);
|
||||||
|
return Plugin_Handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShowEventList(client);
|
||||||
|
return Plugin_Handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Menus
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void ShowEventList(int client)
|
||||||
|
{
|
||||||
|
Menu menu = new Menu(EventList_Handler);
|
||||||
|
menu.SetTitle("Upcoming Unloze events");
|
||||||
|
|
||||||
|
int count = EventNotifier_GetEventCount();
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
EventInfo ev;
|
||||||
|
if (!EventNotifier_GetEvent(i, ev))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
char info[16], display[EVENT_STR_LEN + 16];
|
||||||
|
IntToString(ev.number, info, sizeof(info));
|
||||||
|
Format(display, sizeof(display), "%d: %s", ev.number, ev.name);
|
||||||
|
menu.AddItem(info, display);
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.ExitButton = true;
|
||||||
|
menu.Display(client, MENU_TIME_FOREVER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int EventList_Handler(Menu menu, MenuAction action, int param1, int param2)
|
||||||
|
{
|
||||||
|
switch (action)
|
||||||
|
{
|
||||||
|
case MenuAction_Select:
|
||||||
|
{
|
||||||
|
char info[16];
|
||||||
|
menu.GetItem(param2, info, sizeof(info));
|
||||||
|
|
||||||
|
int index = EventNotifier_FindEventByNumber(StringToInt(info));
|
||||||
|
if (index == -1)
|
||||||
|
{
|
||||||
|
PrintToChat(param1, "%s That event is no longer in the upcoming list.", CHAT_PREFIX);
|
||||||
|
ShowEventList(param1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ShowEventDetails(param1, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case MenuAction_End:
|
||||||
|
{
|
||||||
|
delete menu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShowEventDetails(int client, int index)
|
||||||
|
{
|
||||||
|
EventInfo ev;
|
||||||
|
if (!EventNotifier_GetEvent(index, ev))
|
||||||
|
{
|
||||||
|
PrintToChat(client, "%s That event is no longer in the upcoming list.", CHAT_PREFIX);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char line[EVENT_STR_LEN + 32];
|
||||||
|
Panel panel = new Panel();
|
||||||
|
|
||||||
|
Format(line, sizeof(line), "Event #%d: %s", ev.number, ev.name);
|
||||||
|
panel.SetTitle(line);
|
||||||
|
panel.DrawText(" ");
|
||||||
|
|
||||||
|
if (ev.start > 0)
|
||||||
|
{
|
||||||
|
char when[64];
|
||||||
|
FormatTime(when, sizeof(when), "%a %d %b %Y, %H:%M %Z", ev.start);
|
||||||
|
Format(line, sizeof(line), "Date: %s", when);
|
||||||
|
panel.DrawText(line);
|
||||||
|
|
||||||
|
EventNotifier_FormatRelativeTime(ev.start, ev.end, line, sizeof(line));
|
||||||
|
panel.DrawText(line);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
panel.DrawText("Date: unknown");
|
||||||
|
}
|
||||||
|
|
||||||
|
Format(line, sizeof(line), "Server: %s", ev.server[0] ? ev.server : "unknown");
|
||||||
|
panel.DrawText(line);
|
||||||
|
|
||||||
|
if (ev.maps[0])
|
||||||
|
DrawWrappedList(panel, "Maps: ", ev.maps);
|
||||||
|
else
|
||||||
|
panel.DrawText("Maps: none listed");
|
||||||
|
|
||||||
|
Format(line, sizeof(line), "Rewards: %s", ev.rewards[0] ? ev.rewards : "none");
|
||||||
|
panel.DrawText(line);
|
||||||
|
Format(line, sizeof(line), "Leader: %s", ev.leader[0] ? ev.leader : "none");
|
||||||
|
panel.DrawText(line);
|
||||||
|
Format(line, sizeof(line), "Kickers: %s", ev.kickers[0] ? ev.kickers : "none");
|
||||||
|
panel.DrawText(line);
|
||||||
|
|
||||||
|
panel.DrawText(" ");
|
||||||
|
panel.CurrentKey = KEY_BACK;
|
||||||
|
panel.DrawItem("Back");
|
||||||
|
panel.CurrentKey = KEY_EXIT;
|
||||||
|
panel.DrawItem("Exit");
|
||||||
|
|
||||||
|
panel.Send(client, EventDetails_Handler, MENU_TIME_FOREVER);
|
||||||
|
delete panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int EventDetails_Handler(Menu menu, MenuAction action, int param1, int param2)
|
||||||
|
{
|
||||||
|
if (action == MenuAction_Select && param2 == KEY_BACK)
|
||||||
|
{
|
||||||
|
if (IsClientInGame(param1))
|
||||||
|
Command_EventMenu(param1, 0);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draws "prefix a, b, c" and continues onto new indented lines when a line
|
||||||
|
// would exceed PANEL_LINE_WIDTH characters, splitting only at the commas.
|
||||||
|
void DrawWrappedList(Panel panel, const char[] prefix, const char[] list)
|
||||||
|
{
|
||||||
|
char parts[32][128];
|
||||||
|
int count = ExplodeString(list, ", ", parts, sizeof(parts), sizeof(parts[]));
|
||||||
|
|
||||||
|
char line[PANEL_LINE_WIDTH + 160];
|
||||||
|
strcopy(line, sizeof(line), prefix);
|
||||||
|
bool lineHasItems = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
char item[132];
|
||||||
|
strcopy(item, sizeof(item), parts[i]);
|
||||||
|
if (i < count - 1)
|
||||||
|
StrCat(item, sizeof(item), ",");
|
||||||
|
|
||||||
|
if (lineHasItems && strlen(line) + 1 + strlen(item) > PANEL_LINE_WIDTH)
|
||||||
|
{
|
||||||
|
panel.DrawText(line);
|
||||||
|
strcopy(line, sizeof(line), " ");
|
||||||
|
lineHasItems = false;
|
||||||
|
}
|
||||||
|
else if (lineHasItems)
|
||||||
|
{
|
||||||
|
StrCat(line, sizeof(line), " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
StrCat(line, sizeof(line), item);
|
||||||
|
lineHasItems = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lineHasItems)
|
||||||
|
panel.DrawText(line);
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
#if defined _EventNotifier_included
|
||||||
|
#endinput
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define _EventNotifier_included
|
||||||
|
|
||||||
|
#define EVENT_STR_LEN 128
|
||||||
|
#define EVENT_MAPS_LEN 1024
|
||||||
|
|
||||||
|
enum struct EventInfo
|
||||||
|
{
|
||||||
|
int number;
|
||||||
|
int start; // unix timestamp (UTC), 0 if unknown
|
||||||
|
int end; // unix timestamp (UTC), 0 if unknown
|
||||||
|
char name[EVENT_STR_LEN];
|
||||||
|
char server[EVENT_STR_LEN];
|
||||||
|
char rewards[EVENT_STR_LEN];
|
||||||
|
char leader[EVENT_STR_LEN];
|
||||||
|
char kickers[EVENT_STR_LEN];
|
||||||
|
char maps[EVENT_MAPS_LEN]; // ", "-joined list
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !defined EVENTNOTIFIER_CORE
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of events in the last successful fetch (0 before the first fetch).
|
||||||
|
*
|
||||||
|
* @return Event count.
|
||||||
|
*/
|
||||||
|
native int EventNotifier_GetEventCount();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether at least one fetch has succeeded since the plugin loaded.
|
||||||
|
*
|
||||||
|
* @return True once the event list has been fetched.
|
||||||
|
*/
|
||||||
|
native bool EventNotifier_HasFetched();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds an event by its event number.
|
||||||
|
*
|
||||||
|
* @param number Event number.
|
||||||
|
* @return Index into the event list, or -1 if not found.
|
||||||
|
*/
|
||||||
|
native int EventNotifier_FindEventByNumber(int number);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies an event into the caller's EventInfo.
|
||||||
|
*
|
||||||
|
* @param index Index into the event list.
|
||||||
|
* @param ev EventInfo to fill.
|
||||||
|
* @param size Size of ev, must be sizeof(EventInfo).
|
||||||
|
* @return True on success, false if index is out of range.
|
||||||
|
* @error Buffer smaller than sizeof(EventInfo).
|
||||||
|
*/
|
||||||
|
native bool EventNotifier_GetEvent(int index, any[] ev, int size = sizeof(EventInfo));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a fetch of the event list now. No-op if a fetch is already running.
|
||||||
|
*/
|
||||||
|
native void EventNotifier_Refresh();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after every successful fetch, once the event list has been replaced.
|
||||||
|
*/
|
||||||
|
forward void EventNotifier_OnEventsUpdated();
|
||||||
|
|
||||||
|
#endif // EVENTNOTIFIER_CORE
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Display helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a duration in seconds as "1d 2h 3m", "2h 3m" or "3m".
|
||||||
|
*/
|
||||||
|
stock void EventNotifier_FormatDuration(int seconds, char[] buffer, int maxlen)
|
||||||
|
{
|
||||||
|
int days = seconds / 86400;
|
||||||
|
int hours = (seconds % 86400) / 3600;
|
||||||
|
int minutes = (seconds % 3600) / 60;
|
||||||
|
|
||||||
|
if (days > 0)
|
||||||
|
Format(buffer, maxlen, "%dd %dh %dm", days, hours, minutes);
|
||||||
|
else if (hours > 0)
|
||||||
|
Format(buffer, maxlen, "%dh %dm", hours, minutes);
|
||||||
|
else
|
||||||
|
Format(buffer, maxlen, "%dm", minutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats an event's status relative to now as a "Status: ..." line.
|
||||||
|
*/
|
||||||
|
stock void EventNotifier_FormatRelativeTime(int start, int end, char[] buffer, int maxlen)
|
||||||
|
{
|
||||||
|
int now = GetTime();
|
||||||
|
|
||||||
|
if (end > 0 && now >= start && now < end)
|
||||||
|
{
|
||||||
|
strcopy(buffer, maxlen, "Status: In progress now!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char span[32];
|
||||||
|
if (now >= start)
|
||||||
|
{
|
||||||
|
EventNotifier_FormatDuration(now - start, span, sizeof(span));
|
||||||
|
Format(buffer, maxlen, "Status: Started %s ago", span);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EventNotifier_FormatDuration(start - now, span, sizeof(span));
|
||||||
|
Format(buffer, maxlen, "Status: Starts in %s", span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !defined EVENTNOTIFIER_CORE
|
||||||
|
|
||||||
|
public SharedPlugin __pl_EventNotifier =
|
||||||
|
{
|
||||||
|
name = "EventNotifier",
|
||||||
|
file = "EventNotifier.smx",
|
||||||
|
|
||||||
|
#if defined REQUIRE_PLUGIN
|
||||||
|
required = 1
|
||||||
|
#else
|
||||||
|
required = 0
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
#if !defined REQUIRE_PLUGIN
|
||||||
|
public void __pl_EventNotifier_SetNTVOptional()
|
||||||
|
{
|
||||||
|
MarkNativeAsOptional("EventNotifier_GetEventCount");
|
||||||
|
MarkNativeAsOptional("EventNotifier_HasFetched");
|
||||||
|
MarkNativeAsOptional("EventNotifier_FindEventByNumber");
|
||||||
|
MarkNativeAsOptional("EventNotifier_GetEvent");
|
||||||
|
MarkNativeAsOptional("EventNotifier_Refresh");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // EVENTNOTIFIER_CORE
|
||||||
Reference in New Issue
Block a user