summaryrefslogtreecommitdiff
path: root/sourcemod/scripting/gokz-racing
diff options
context:
space:
mode:
Diffstat (limited to 'sourcemod/scripting/gokz-racing')
-rw-r--r--sourcemod/scripting/gokz-racing/announce.sp229
-rw-r--r--sourcemod/scripting/gokz-racing/api.sp107
-rw-r--r--sourcemod/scripting/gokz-racing/commands.sp47
-rw-r--r--sourcemod/scripting/gokz-racing/duel_menu.sp534
-rw-r--r--sourcemod/scripting/gokz-racing/race.sp221
-rw-r--r--sourcemod/scripting/gokz-racing/race_menu.sp464
-rw-r--r--sourcemod/scripting/gokz-racing/racer.sp439
7 files changed, 2041 insertions, 0 deletions
diff --git a/sourcemod/scripting/gokz-racing/announce.sp b/sourcemod/scripting/gokz-racing/announce.sp
new file mode 100644
index 0000000..7b6a920
--- /dev/null
+++ b/sourcemod/scripting/gokz-racing/announce.sp
@@ -0,0 +1,229 @@
+/*
+ Chat messages of race and racer events.
+*/
+
+
+
+// =====[ PUBLIC ]=====
+
+/**
+ * Prints a message to chat for all clients in a race, formatting colours
+ * and optionally adding the chat prefix. If using the chat prefix, specify
+ * a colour at the beginning of the message e.g. "{default}Hello!".
+ *
+ * @param raceID ID of the race.
+ * @param specs Whether to also include racer spectators.
+ * @param addPrefix Whether to add the chat prefix.
+ * @param format Formatting rules.
+ * @param any Variable number of format parameters.
+ */
+void PrintToChatAllInRace(int raceID, bool specs, bool addPrefix, const char[] format, any...)
+{
+ char buffer[1024];
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsClientInGame(client) && GetRaceID(client) == raceID)
+ {
+ SetGlobalTransTarget(client);
+ VFormat(buffer, sizeof(buffer), format, 5);
+ GOKZ_PrintToChat(client, addPrefix, buffer);
+
+ if (specs)
+ {
+ for (int target = 1; target <= MaxClients; target++)
+ {
+ if (IsClientInGame(target) && GetObserverTarget(target) == client && GetRaceID(target) != raceID)
+ {
+ SetGlobalTransTarget(target);
+ VFormat(buffer, sizeof(buffer), format, 5);
+ GOKZ_PrintToChat(target, addPrefix, buffer);
+ }
+ }
+ }
+ }
+ }
+}
+
+
+
+// =====[ EVENTS ]=====
+
+void OnFinish_Announce(int client, int raceID, int place)
+{
+ switch (GetRaceInfo(raceID, RaceInfo_Type))
+ {
+ case RaceType_Normal:
+ {
+ if (place == 1)
+ {
+ PrintToChatAllInRace(raceID, true, true, "%t", "Race Won", client);
+ }
+ else
+ {
+ ArrayList unfinishedRacers = GetUnfinishedRacers(raceID);
+ if (unfinishedRacers.Length >= 1)
+ {
+ PrintToChatAllInRace(raceID, true, true, "%t", "Race Placed", client, place);
+ }
+ else
+ {
+ PrintToChatAllInRace(raceID, true, true, "%t", "Race Lost", client, place);
+ }
+ delete unfinishedRacers;
+ }
+ }
+ case RaceType_Duel:
+ {
+ ArrayList unfinishedRacers = GetUnfinishedRacers(raceID);
+ if (unfinishedRacers.Length == 1)
+ {
+ int opponent = unfinishedRacers.Get(0);
+ GOKZ_PrintToChatAll(true, "%t", "Duel Won", client, opponent);
+ }
+ delete unfinishedRacers;
+ }
+ }
+}
+
+void OnSurrender_Announce(int client, int raceID)
+{
+ switch (GetRaceInfo(raceID, RaceInfo_Type))
+ {
+ case RaceType_Normal:
+ {
+ PrintToChatAllInRace(raceID, true, true, "%t", "Race Surrendered", client);
+ }
+ case RaceType_Duel:
+ {
+ ArrayList unfinishedRacers = GetUnfinishedRacers(raceID);
+ if (unfinishedRacers.Length == 1)
+ {
+ int opponent = unfinishedRacers.Get(0);
+ GOKZ_PrintToChatAll(true, "%t", "Duel Surrendered", client, opponent);
+ }
+ delete unfinishedRacers;
+ }
+ }
+}
+
+void OnRequestReceived_Announce(int client, int raceID)
+{
+ int host = GetRaceHost(raceID);
+
+ switch (GetRaceInfo(raceID, RaceInfo_Type))
+ {
+ case RaceType_Normal:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Race Request Received", host);
+ }
+ case RaceType_Duel:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Duel Request Received", host);
+ }
+ }
+
+ int cpRule = GetRaceInfo(raceID, RaceInfo_CheckpointRule);
+ int cdRule = GetRaceInfo(raceID, RaceInfo_CooldownRule);
+ int mode = GetRaceInfo(raceID, RaceInfo_Mode);
+ int course = GetRaceInfo(raceID, RaceInfo_Course);
+
+ char courseStr[32];
+ if (course == 0)
+ {
+ FormatEx(courseStr, sizeof(courseStr), "%T", "Race Rules - Main Course", client);
+ }
+ else
+ {
+ FormatEx(courseStr, sizeof(courseStr), "%T %d", "Race Rules - Bonus Course", client, course);
+ }
+
+ if (cpRule == -1 && cdRule == 0)
+ {
+ GOKZ_PrintToChat(client, false, "%t", "Race Rules - Unlimited", gC_ModeNames[mode], courseStr);
+ }
+ if (cpRule == -1 && cdRule > 0)
+ {
+ GOKZ_PrintToChat(client, false, "%t", "Race Rules - Limited Cooldown", gC_ModeNames[mode], courseStr, cdRule);
+ }
+ if (cpRule == 0)
+ {
+ GOKZ_PrintToChat(client, false, "%t", "Race Rules - No Checkpoints", gC_ModeNames[mode], courseStr);
+ }
+ if (cpRule > 0 && cdRule == 0)
+ {
+ GOKZ_PrintToChat(client, false, "%t", "Race Rules - Limited Checkpoints", gC_ModeNames[mode], courseStr, cpRule);
+ }
+ if (cpRule > 0 && cdRule > 0)
+ {
+ GOKZ_PrintToChat(client, false, "%t", "Race Rules - Limited", gC_ModeNames[mode], courseStr, cpRule, cdRule);
+ }
+
+ GOKZ_PrintToChat(client, false, "%t", "You Have Seconds To Accept", RoundFloat(RC_REQUEST_TIMEOUT_TIME));
+}
+
+void OnRequestAccepted_Announce(int client, int raceID)
+{
+ int host = GetRaceHost(raceID);
+
+ switch (GetRaceInfo(raceID, RaceInfo_Type))
+ {
+ case RaceType_Normal:
+ {
+ PrintToChatAllInRace(raceID, true, true, "%t", "Race Request Accepted", client, host);
+ }
+ case RaceType_Duel:
+ {
+ GOKZ_PrintToChatAll(true, "%t", "Duel Request Accepted", client, host);
+ }
+ }
+}
+
+void OnRequestDeclined_Announce(int client, int raceID, bool timeout)
+{
+ int host = GetRaceHost(raceID);
+
+ if (timeout)
+ {
+ switch (GetRaceInfo(raceID, RaceInfo_Type))
+ {
+ case RaceType_Normal:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Race Request Not Accepted In Time (Target)");
+ GOKZ_PrintToChat(host, true, "%t", "Race Request Not Accepted In Time (Host)", client);
+ }
+ case RaceType_Duel:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Duel Request Not Accepted In Time (Target)");
+ GOKZ_PrintToChat(host, true, "%t", "Duel Request Not Accepted In Time (Host)", client);
+ }
+ }
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "You Have Declined");
+ GOKZ_PrintToChat(host, true, "%t", "Player Has Declined", client);
+ }
+}
+
+void OnRaceStarted_Announce(int raceID)
+{
+ if (GetRaceInfo(raceID, RaceInfo_Type) == RaceType_Normal)
+ {
+ PrintToChatAllInRace(raceID, true, true, "%t", "Race Host Started Countdown", GetRaceHost(raceID));
+ }
+}
+
+void OnRaceAborted_Announce(int raceID)
+{
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsClientInGame(client) && GetRaceID(client) == raceID)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Race Has Been Aborted");
+ if (GetStatus(client) == RacerStatus_Racing)
+ {
+ GOKZ_PlayErrorSound(client);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-racing/api.sp b/sourcemod/scripting/gokz-racing/api.sp
new file mode 100644
index 0000000..13d82a3
--- /dev/null
+++ b/sourcemod/scripting/gokz-racing/api.sp
@@ -0,0 +1,107 @@
+static GlobalForward H_OnFinish;
+static GlobalForward H_OnSurrender;
+static GlobalForward H_OnRequestReceived;
+static GlobalForward H_OnRequestAccepted;
+static GlobalForward H_OnRequestDeclined;
+static GlobalForward H_OnRaceRegistered;
+static GlobalForward H_OnRaceInfoChanged;
+
+
+
+// =====[ FORWARDS ]=====
+
+void CreateGlobalForwards()
+{
+ H_OnFinish = new GlobalForward("GOKZ_RC_OnFinish", ET_Ignore, Param_Cell, Param_Cell, Param_Cell);
+ H_OnSurrender = new GlobalForward("GOKZ_RC_OnSurrender", ET_Ignore, Param_Cell, Param_Cell);
+ H_OnRequestReceived = new GlobalForward("GOKZ_RC_OnRequestReceived", ET_Ignore, Param_Cell, Param_Cell);
+ H_OnRequestAccepted = new GlobalForward("GOKZ_RC_OnRequestAccepted", ET_Ignore, Param_Cell, Param_Cell);
+ H_OnRequestDeclined = new GlobalForward("GOKZ_RC_OnRequestDeclined", ET_Ignore, Param_Cell, Param_Cell, Param_Cell);
+ H_OnRaceRegistered = new GlobalForward("GOKZ_RC_OnRaceRegistered", ET_Ignore, Param_Cell);
+ H_OnRaceInfoChanged = new GlobalForward("GOKZ_RC_OnRaceInfoChanged", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell);
+}
+
+void Call_OnFinish(int client, int raceID, int place)
+{
+ Call_StartForward(H_OnFinish);
+ Call_PushCell(client);
+ Call_PushCell(raceID);
+ Call_PushCell(place);
+ Call_Finish();
+}
+
+void Call_OnSurrender(int client, int raceID)
+{
+ Call_StartForward(H_OnSurrender);
+ Call_PushCell(client);
+ Call_PushCell(raceID);
+ Call_Finish();
+}
+
+void Call_OnRequestReceived(int client, int raceID)
+{
+ Call_StartForward(H_OnRequestReceived);
+ Call_PushCell(client);
+ Call_PushCell(raceID);
+ Call_Finish();
+}
+
+void Call_OnRequestAccepted(int client, int raceID)
+{
+ Call_StartForward(H_OnRequestAccepted);
+ Call_PushCell(client);
+ Call_PushCell(raceID);
+ Call_Finish();
+}
+
+void Call_OnRequestDeclined(int client, int raceID, bool timeout)
+{
+ Call_StartForward(H_OnRequestDeclined);
+ Call_PushCell(client);
+ Call_PushCell(raceID);
+ Call_PushCell(timeout);
+ Call_Finish();
+}
+
+void Call_OnRaceRegistered(int raceID)
+{
+ Call_StartForward(H_OnRaceRegistered);
+ Call_PushCell(raceID);
+ Call_Finish();
+}
+
+void Call_OnRaceInfoChanged(int raceID, RaceInfo infoIndex, int oldValue, int newValue)
+{
+ Call_StartForward(H_OnRaceInfoChanged);
+ Call_PushCell(raceID);
+ Call_PushCell(infoIndex);
+ Call_PushCell(oldValue);
+ Call_PushCell(newValue);
+ Call_Finish();
+}
+
+
+
+// =====[ NATIVES ]=====
+
+void CreateNatives()
+{
+ CreateNative("GOKZ_RC_GetRaceInfo", Native_GetRaceInfo);
+ CreateNative("GOKZ_RC_GetStatus", Native_GetStatus);
+ CreateNative("GOKZ_RC_GetRaceID", Native_GetRaceID);
+}
+
+public int Native_GetRaceInfo(Handle plugin, int numParams)
+{
+ return GetRaceInfo(GetNativeCell(1), GetNativeCell(2));
+}
+
+public int Native_GetStatus(Handle plugin, int numParams)
+{
+ return GetStatus(GetNativeCell(1));
+}
+
+public int Native_GetRaceID(Handle plugin, int numParams)
+{
+ return GetRaceID(GetNativeCell(1));
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-racing/commands.sp b/sourcemod/scripting/gokz-racing/commands.sp
new file mode 100644
index 0000000..9fbd7ab
--- /dev/null
+++ b/sourcemod/scripting/gokz-racing/commands.sp
@@ -0,0 +1,47 @@
+void RegisterCommands()
+{
+ RegConsoleCmd("sm_accept", CommandAccept, "[KZ] Accept an incoming race request.");
+ RegConsoleCmd("sm_decline", CommandDecline, "[KZ] Decline an incoming race request.");
+ RegConsoleCmd("sm_surrender", CommandSurrender, "[KZ] Surrender your race.");
+ RegConsoleCmd("sm_duel", CommandDuel, "[KZ] Open the duel menu.");
+ RegConsoleCmd("sm_challenge", CommandDuel, "[KZ] Open the duel menu.");
+ RegConsoleCmd("sm_abort", CommandAbort, "[KZ] Abort the race you are hosting.");
+
+ RegAdminCmd("sm_race", CommandRace, ADMFLAG_RESERVATION, "[KZ] Open the race hosting menu.");
+}
+
+public Action CommandAccept(int client, int args)
+{
+ AcceptRequest(client);
+ return Plugin_Handled;
+}
+
+public Action CommandDecline(int client, int args)
+{
+ DeclineRequest(client);
+ return Plugin_Handled;
+}
+
+public Action CommandSurrender(int client, int args)
+{
+ SurrenderRacer(client);
+ return Plugin_Handled;
+}
+
+public Action CommandDuel(int client, int args)
+{
+ DisplayDuelMenu(client);
+ return Plugin_Handled;
+}
+
+public Action CommandAbort(int client, int args)
+{
+ AbortHostedRace(client);
+ return Plugin_Handled;
+}
+
+public Action CommandRace(int client, int args)
+{
+ DisplayRaceMenu(client);
+ return Plugin_Handled;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-racing/duel_menu.sp b/sourcemod/scripting/gokz-racing/duel_menu.sp
new file mode 100644
index 0000000..44c519f
--- /dev/null
+++ b/sourcemod/scripting/gokz-racing/duel_menu.sp
@@ -0,0 +1,534 @@
+/*
+ A menu for initiating 1v1 races.
+*/
+
+
+
+#define ITEM_INFO_CHALLENGE "ch"
+#define ITEM_INFO_ABORT "ab"
+#define ITEM_INFO_MODE "md"
+#define ITEM_INFO_COURSE "co"
+#define ITEM_INFO_TELEPORT "tp"
+
+static int duelMenuMode[MAXPLAYERS + 1];
+static int duelMenuCourse[MAXPLAYERS + 1];
+static int duelMenuCheckpointLimit[MAXPLAYERS + 1];
+static int duelMenuCheckpointCooldown[MAXPLAYERS + 1];
+
+
+
+// =====[ PICK MODE ]=====
+
+void DisplayDuelMenu(int client, bool reset = true)
+{
+ if (InRace(client) && (!IsRaceHost(client) || GetRaceInfo(GetRaceID(client), RaceInfo_Type) != RaceType_Duel))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "You Are Already Part Of A Race");
+ GOKZ_PlayErrorSound(client);
+ return;
+ }
+
+ if (reset)
+ {
+ duelMenuMode[client] = GOKZ_GetCoreOption(client, Option_Mode);
+ }
+
+ Menu menu = new Menu(MenuHandler_Duel);
+ menu.SetTitle("%T", "Duel Menu - Title", client);
+ DuelMenuAddItems(client, menu);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+public int MenuHandler_Duel(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ char info[16];
+ menu.GetItem(param2, info, sizeof(info));
+
+ if (StrEqual(info, ITEM_INFO_CHALLENGE, false))
+ {
+ if (!DisplayDuelOpponentMenu(param1))
+ {
+ DisplayDuelMenu(param1, false);
+ }
+ }
+ else if (StrEqual(info, ITEM_INFO_ABORT, false))
+ {
+ AbortHostedRace(param1);
+ DisplayDuelMenu(param1, false);
+ }
+ else if (StrEqual(info, ITEM_INFO_MODE, false))
+ {
+ DisplayDuelModeMenu(param1);
+ }
+ else if (StrEqual(info, ITEM_INFO_COURSE, false))
+ {
+ int course = duelMenuCourse[param1];
+ do
+ {
+ course++;
+ if (!GOKZ_IsValidCourse(course))
+ {
+ course = 0;
+ }
+ } while (!GOKZ_GetCourseRegistered(course) && course != duelMenuCourse[param1]);
+ duelMenuCourse[param1] = course;
+ DisplayDuelMenu(param1, false);
+ }
+ else if (StrEqual(info, ITEM_INFO_TELEPORT, false))
+ {
+ DisplayDuelCheckpointMenu(param1);
+ }
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+void DuelMenuAddItems(int client, Menu menu)
+{
+ char display[64];
+
+ menu.RemoveAllItems();
+
+ FormatEx(display, sizeof(display), "%T", "Duel Menu - Choose Opponent", client);
+ menu.AddItem(ITEM_INFO_CHALLENGE, display, InRace(client) ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT);
+
+ FormatEx(display, sizeof(display), "%T\n \n%T", "Race Menu - Abort Race", client, "Race Menu - Rules", client);
+ menu.AddItem(ITEM_INFO_ABORT, display, InRace(client) ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED);
+
+ FormatEx(display, sizeof(display), "%s", gC_ModeNames[duelMenuMode[client]]);
+ menu.AddItem(ITEM_INFO_MODE, display, InRace(client) ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT);
+
+ if (duelMenuCourse[client] == 0)
+ {
+ FormatEx(display, sizeof(display), "%T", "Race Rules - Main Course", client);
+ }
+ else
+ {
+ FormatEx(display, sizeof(display), "%T %d", "Race Rules - Bonus Course", client, duelMenuCourse[client]);
+ }
+ menu.AddItem(ITEM_INFO_COURSE, display, InRace(client) ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT);
+
+ FormatEx(display, sizeof(display), "%s", GetDuelRuleSummary(client, duelMenuCheckpointLimit[client], duelMenuCheckpointCooldown[client]));
+ menu.AddItem(ITEM_INFO_TELEPORT, display, InRace(client) ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT);
+}
+
+
+
+// =====[ MODE MENU ]=====
+
+static void DisplayDuelModeMenu(int client)
+{
+ Menu menu = new Menu(MenuHandler_DuelMode);
+ menu.ExitButton = false;
+ menu.ExitBackButton = true;
+ menu.SetTitle("%T", "Mode Rule Menu - Title", client);
+ GOKZ_MenuAddModeItems(client, menu, true);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+public int MenuHandler_DuelMode(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ duelMenuMode[param1] = param2;
+ DisplayDuelMenu(param1, false);
+ }
+ else if (action == MenuAction_Cancel)
+ {
+ DisplayDuelMenu(param1, false);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+
+
+// =====[ CHECKPOINT MENU ]=====
+
+static void DisplayDuelCheckpointMenu(int client)
+{
+ Menu menu = new Menu(MenuHandler_DuelCheckpoint);
+ menu.ExitButton = false;
+ menu.ExitBackButton = true;
+ menu.SetTitle("%T", "Checkpoint Rule Menu - Title", client);
+ DuelCheckpointMenuAddItems(client, menu);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+public int MenuHandler_DuelCheckpoint(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ switch (param2)
+ {
+ case CheckpointRule_None:
+ {
+ duelMenuCheckpointCooldown[param1] = 0;
+ duelMenuCheckpointLimit[param1] = 0;
+ DisplayDuelMenu(param1, false);
+ }
+ case CheckpointRule_Limit:
+ {
+ DisplayCheckpointLimitMenu(param1);
+ }
+ case CheckpointRule_Cooldown:
+ {
+ DisplayCheckpointCooldownMenu(param1);
+ }
+ case CheckpointRule_Unlimited:
+ {
+ duelMenuCheckpointCooldown[param1] = 0;
+ duelMenuCheckpointLimit[param1] = -1;
+ DisplayDuelMenu(param1, false);
+ }
+ }
+ }
+ else if (action == MenuAction_Cancel)
+ {
+ DisplayDuelMenu(param1, false);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+void DuelCheckpointMenuAddItems(int client, Menu menu)
+{
+ char display[32];
+
+ menu.RemoveAllItems();
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - None", client);
+ menu.AddItem("", display);
+
+ if (duelMenuCheckpointLimit[client] == -1)
+ {
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - No Checkpoint Limit", client);
+ }
+ else
+ {
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - Checkpoint Limit", client, duelMenuCheckpointLimit[client]);
+ }
+ menu.AddItem("", display);
+
+ if (duelMenuCheckpointCooldown[client] == 0)
+ {
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - No Checkpoint Cooldown", client);
+ }
+ else
+ {
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - Checkpoint Cooldown", client, duelMenuCheckpointCooldown[client]);
+ }
+ menu.AddItem("", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - Unlimited", client);
+ menu.AddItem("", display);
+}
+
+
+
+// =====[ CP LIMIT MENU ]=====
+
+static void DisplayCheckpointLimitMenu(int client)
+{
+ char display[32];
+
+ Menu menu = new Menu(MenuHandler_DuelCheckpointLimit);
+ menu.ExitButton = false;
+ menu.ExitBackButton = true;
+
+ if (duelMenuCheckpointLimit[client] == -1)
+ {
+ menu.SetTitle("%T", "Checkpoint Limit Menu - Title Unlimited", client);
+ }
+ else
+ {
+ menu.SetTitle("%T", "Checkpoint Limit Menu - Title Limited", client, duelMenuCheckpointLimit[client]);
+ }
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Add One", client);
+ menu.AddItem("+1", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Add Five", client);
+ menu.AddItem("+5", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Remove One", client);
+ menu.AddItem("-1", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Remove Five", client);
+ menu.AddItem("-5", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Unlimited", client);
+ menu.AddItem("Unlimited", display);
+
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+public int MenuHandler_DuelCheckpointLimit(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ char item[32];
+ menu.GetItem(param2, item, sizeof(item));
+ if (StrEqual(item, "+1"))
+ {
+ if (duelMenuCheckpointLimit[param1] == -1)
+ {
+ duelMenuCheckpointLimit[param1]++;
+ }
+ duelMenuCheckpointLimit[param1]++;
+ }
+ if (StrEqual(item, "+5"))
+ {
+ if (duelMenuCheckpointLimit[param1] == -1)
+ {
+ duelMenuCheckpointLimit[param1]++;
+ }
+ duelMenuCheckpointLimit[param1] += 5;
+ }
+ if (StrEqual(item, "-1"))
+ {
+ duelMenuCheckpointLimit[param1]--;
+ }
+ if (StrEqual(item, "-5"))
+ {
+ duelMenuCheckpointLimit[param1] -= 5;
+ }
+ if (StrEqual(item, "Unlimited"))
+ {
+ duelMenuCheckpointLimit[param1] = -1;
+ DisplayDuelCheckpointMenu(param1);
+ return 0;
+ }
+
+ duelMenuCheckpointLimit[param1] = duelMenuCheckpointLimit[param1] < 0 ? 0 : duelMenuCheckpointLimit[param1];
+ DisplayCheckpointLimitMenu(param1);
+ }
+ else if (action == MenuAction_Cancel)
+ {
+ DisplayDuelCheckpointMenu(param1);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+
+
+// =====[ CP COOLDOWN MENU ]=====
+
+static void DisplayCheckpointCooldownMenu(int client)
+{
+ char display[32];
+
+ Menu menu = new Menu(MenuHandler_DuelCPCooldown);
+ menu.ExitButton = false;
+ menu.ExitBackButton = true;
+
+ if (duelMenuCheckpointCooldown[client] == -1)
+ {
+ menu.SetTitle("%T", "Checkpoint Cooldown Menu - Title None", client);
+ }
+ else
+ {
+ menu.SetTitle("%T", "Checkpoint Cooldown Menu - Title Limited", client, duelMenuCheckpointCooldown[client]);
+ }
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - Add One Second", client);
+ menu.AddItem("+1", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - Add Five Seconds", client);
+ menu.AddItem("+5", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - Remove One Second", client);
+ menu.AddItem("-1", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - Remove Five Seconds", client);
+ menu.AddItem("-5", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - None", client);
+ menu.AddItem("None", display);
+
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+public int MenuHandler_DuelCPCooldown(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ char item[32];
+ menu.GetItem(param2, item, sizeof(item));
+ if (StrEqual(item, "+1"))
+ {
+ if (duelMenuCheckpointCooldown[param1] == -1)
+ {
+ duelMenuCheckpointCooldown[param1]++;
+ }
+ duelMenuCheckpointCooldown[param1]++;
+ }
+ if (StrEqual(item, "+5"))
+ {
+ if (duelMenuCheckpointCooldown[param1] == -1)
+ {
+ duelMenuCheckpointCooldown[param1]++;
+ }
+ duelMenuCheckpointCooldown[param1] += 5;
+ }
+ if (StrEqual(item, "-1"))
+ {
+ duelMenuCheckpointCooldown[param1]--;
+ }
+ if (StrEqual(item, "-5"))
+ {
+ duelMenuCheckpointCooldown[param1] -= 5;
+ }
+ if (StrEqual(item, "None"))
+ {
+ duelMenuCheckpointCooldown[param1] = 0;
+ DisplayDuelCheckpointMenu(param1);
+ return 0;
+ }
+
+ duelMenuCheckpointCooldown[param1] = duelMenuCheckpointCooldown[param1] < 0 ? 0 : duelMenuCheckpointCooldown[param1];
+ DisplayCheckpointCooldownMenu(param1);
+ }
+ else if (action == MenuAction_Cancel)
+ {
+ DisplayDuelCheckpointMenu(param1);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+
+
+// =====[ OPPONENT MENU ]=====
+
+static bool DisplayDuelOpponentMenu(int client)
+{
+ Menu menu = new Menu(MenuHandler_DuelOpponent);
+ menu.ExitButton = false;
+ menu.ExitBackButton = true;
+ menu.SetTitle("%T", "Duel Opponent Selection Menu - Title", client);
+ if (DuelOpponentMenuAddItems(client, menu) == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Opponents Available");
+ GOKZ_PlayErrorSound(client);
+ delete menu;
+ return false;
+ }
+ menu.Display(client, MENU_TIME_FOREVER);
+
+ return true;
+}
+
+static int DuelOpponentMenuAddItems(int client, Menu menu)
+{
+ int count = 0;
+ for (int i = 1; i <= MaxClients; i++)
+ {
+ char display[MAX_NAME_LENGTH];
+ if (i != client && IsClientInGame(i) && !IsFakeClient(i) && !InRace(i))
+ {
+ FormatEx(display, sizeof(display), "%N", i);
+ menu.AddItem(IntToStringEx(GetClientUserId(i)), display);
+ count++;
+ }
+ }
+ return count;
+}
+
+public int MenuHandler_DuelOpponent(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ char info[16];
+ menu.GetItem(param2, info, sizeof(info));
+ int target = GetClientOfUserId(StringToInt(info));
+ if (IsValidClient(target))
+ {
+ if (SendDuelRequest(param1, target))
+ {
+ GOKZ_PrintToChat(param1, true, "%t", "Duel Request Sent", target);
+ }
+ else
+ {
+ DisplayDuelOpponentMenu(param1);
+ }
+ }
+ else
+ {
+ GOKZ_PrintToChat(param1, true, "%t", "Player No Longer Valid");
+ GOKZ_PlayErrorSound(param1);
+ DisplayDuelOpponentMenu(param1);
+ }
+ }
+ else if (action == MenuAction_Cancel)
+ {
+ DisplayDuelMenu(param1, false);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+static bool SendDuelRequest(int host, int target)
+{
+ if (InRace(target))
+ {
+ GOKZ_PrintToChat(host, true, "%t", "Player Already In A Race", target);
+ GOKZ_PlayErrorSound(host);
+ return false;
+ }
+
+ HostRace(host, RaceType_Duel, duelMenuCourse[host], duelMenuMode[host], duelMenuCheckpointLimit[host], duelMenuCheckpointCooldown[host]);
+ return SendRequest(host, target);
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+char[] GetDuelRuleSummary(int client, int checkpointLimit, int checkpointCooldown)
+{
+ char rulesString[64];
+ if (checkpointLimit == -1 && checkpointCooldown == 0)
+ {
+ FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - Unlimited", client);
+ }
+ else if (checkpointLimit > 0 && checkpointCooldown == 0)
+ {
+ FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - Limited Checkpoints", client, checkpointLimit);
+ }
+ else if (checkpointLimit == -1 && checkpointCooldown > 0)
+ {
+ FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - Limited Cooldown", client, checkpointCooldown);
+ }
+ else if (checkpointLimit > 0 && checkpointCooldown > 0)
+ {
+ FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - Limited Everything", client, checkpointLimit, checkpointCooldown);
+ }
+ else if (checkpointLimit == 0)
+ {
+ FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - No Checkpoints", client);
+ }
+
+ return rulesString;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-racing/race.sp b/sourcemod/scripting/gokz-racing/race.sp
new file mode 100644
index 0000000..5ddc2a8
--- /dev/null
+++ b/sourcemod/scripting/gokz-racing/race.sp
@@ -0,0 +1,221 @@
+/*
+ Race info storing and accessing using a StringMap.
+ Each race is given a unique race ID when created.
+ See the RaceInfo enum for what information is accessible.
+ See the RaceStatus enum for possible race states.
+*/
+
+
+
+static StringMap raceInfo;
+static int lastRaceID;
+
+
+
+// =====[ GENERAL ]=====
+
+int GetRaceInfo(int raceID, RaceInfo prop)
+{
+ ArrayList info;
+ if (raceInfo.GetValue(IntToStringEx(raceID), info))
+ {
+ return info.Get(view_as<int>(prop));
+ }
+ else
+ {
+ return -1;
+ }
+}
+
+static bool SetRaceInfo(int raceID, RaceInfo prop, int value)
+{
+ ArrayList info;
+ if (raceInfo.GetValue(IntToStringEx(raceID), info))
+ {
+ int oldValue = info.Get(view_as<int>(prop));
+ if (oldValue != value)
+ {
+ info.Set(view_as<int>(prop), value);
+ Call_OnRaceInfoChanged(raceID, prop, oldValue, value);
+ }
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+}
+
+int IncrementFinishedRacerCount(int raceID)
+{
+ int finishedRacers = GetRaceInfo(raceID, RaceInfo_FinishedRacerCount) + 1;
+ SetRaceInfo(raceID, RaceInfo_FinishedRacerCount, finishedRacers);
+ return finishedRacers;
+}
+
+int GetRaceHost(int raceID)
+{
+ return GetClientOfUserId(GetRaceInfo(raceID, RaceInfo_HostUserID));
+}
+
+ArrayList GetUnfinishedRacers(int raceID)
+{
+ ArrayList racers = new ArrayList();
+ for (int i = 1; i <= MaxClients; i++)
+ {
+ if (GetRaceID(i) == raceID && !IsFinished(i))
+ {
+ racers.Push(i);
+ }
+ }
+ return racers;
+}
+
+int GetUnfinishedRacersCount(int raceID)
+{
+ ArrayList racers = GetUnfinishedRacers(raceID);
+ int count = racers.Length;
+ delete racers;
+ return count;
+}
+
+ArrayList GetAcceptedRacers(int raceID)
+{
+ ArrayList racers = new ArrayList();
+ for (int i = 1; i <= MaxClients; i++)
+ {
+ if (GetRaceID(i) == raceID && IsAccepted(i))
+ {
+ racers.Push(i);
+ }
+ }
+ return racers;
+}
+
+int GetAcceptedRacersCount(int raceID)
+{
+ ArrayList racers = GetAcceptedRacers(raceID);
+ int count = racers.Length;
+ delete racers;
+ return count;
+}
+
+
+
+// =====[ REGISTRATION ]=====
+
+int RegisterRace(int host, int type, int course, int mode, int checkpointRule, int cooldownRule)
+{
+ int raceID = ++lastRaceID;
+
+ ArrayList info = new ArrayList(1, view_as<int>(RACEINFO_COUNT));
+ info.Set(view_as<int>(RaceInfo_ID), raceID);
+ info.Set(view_as<int>(RaceInfo_Status), RaceStatus_Pending);
+ info.Set(view_as<int>(RaceInfo_HostUserID), GetClientUserId(host));
+ info.Set(view_as<int>(RaceInfo_FinishedRacerCount), 0);
+ info.Set(view_as<int>(RaceInfo_Type), type);
+ info.Set(view_as<int>(RaceInfo_Course), course);
+ info.Set(view_as<int>(RaceInfo_Mode), mode);
+ info.Set(view_as<int>(RaceInfo_CheckpointRule), checkpointRule);
+ info.Set(view_as<int>(RaceInfo_CooldownRule), cooldownRule);
+
+ raceInfo.SetValue(IntToStringEx(raceID), info);
+
+ Call_OnRaceRegistered(raceID);
+
+ return raceID;
+}
+
+static void UnregisterRace(int raceID)
+{
+ ArrayList info;
+ if (raceInfo.GetValue(IntToStringEx(raceID), info))
+ {
+ delete info;
+ raceInfo.Remove(IntToStringEx(raceID));
+ }
+}
+
+
+
+// =====[ START ]=====
+
+bool StartRace(int raceID)
+{
+ SetRaceInfo(raceID, RaceInfo_Status, RaceStatus_Countdown);
+
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (GetRaceID(client) == raceID)
+ {
+ StartRacer(client);
+ GOKZ_PrintToChat(client, true, "%t", "Race Countdown Started");
+ }
+ }
+
+ CreateTimer(RC_COUNTDOWN_TIME, Timer_EndCountdown, raceID);
+
+ return true;
+}
+
+public Action Timer_EndCountdown(Handle timer, int raceID)
+{
+ SetRaceInfo(raceID, RaceInfo_Status, RaceStatus_Started);
+ return Plugin_Continue;
+}
+
+
+
+// =====[ ABORT ]=====
+
+bool AbortRace(int raceID)
+{
+ SetRaceInfo(raceID, RaceInfo_Status, RaceStatus_Aborting);
+
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (GetRaceID(client) == raceID)
+ {
+ AbortRacer(client);
+ GOKZ_PrintToChat(client, true, "%t", "Race Has Been Aborted");
+ GOKZ_PlayErrorSound(client);
+ }
+ }
+
+ UnregisterRace(raceID);
+
+ return true;
+}
+
+
+
+// =====[ EVENTS ]=====
+
+void OnPluginStart_Race()
+{
+ raceInfo = new StringMap();
+}
+
+void OnFinish_Race(int raceID)
+{
+ if (GetUnfinishedRacersCount(raceID) == 0)
+ {
+ UnregisterRace(raceID);
+ }
+}
+
+void OnRequestAccepted_Race(int raceID)
+{
+ if (GetRaceInfo(raceID, RaceInfo_Type) == RaceType_Duel)
+ {
+ StartRace(raceID);
+ }
+}
+
+void OnRequestDeclined_Race(int raceID)
+{
+ if (GetRaceInfo(raceID, RaceInfo_Type) == RaceType_Duel)
+ {
+ AbortRace(raceID);
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-racing/race_menu.sp b/sourcemod/scripting/gokz-racing/race_menu.sp
new file mode 100644
index 0000000..0518a2a
--- /dev/null
+++ b/sourcemod/scripting/gokz-racing/race_menu.sp
@@ -0,0 +1,464 @@
+/*
+ A menu for hosting big races.
+*/
+
+
+
+#define ITEM_INFO_START "st"
+#define ITEM_INFO_ABORT "ab"
+#define ITEM_INFO_INVITE "iv"
+#define ITEM_INFO_MODE "md"
+#define ITEM_INFO_COURSE "co"
+#define ITEM_INFO_TELEPORT "tp"
+
+static int raceMenuMode[MAXPLAYERS + 1];
+static int raceMenuCourse[MAXPLAYERS + 1];
+static int raceMenuCheckpointLimit[MAXPLAYERS + 1];
+static int raceMenuCheckpointCooldown[MAXPLAYERS + 1];
+
+
+
+// =====[ PICK MODE ]=====
+
+void DisplayRaceMenu(int client, bool reset = true)
+{
+ if (InRace(client) && (!IsRaceHost(client) || GetRaceInfo(GetRaceID(client), RaceInfo_Type) != RaceType_Normal))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "You Are Already Part Of A Race");
+ GOKZ_PlayErrorSound(client);
+ return;
+ }
+
+ if (reset)
+ {
+ raceMenuMode[client] = GOKZ_GetCoreOption(client, Option_Mode);
+ raceMenuCourse[client] = 0;
+ }
+
+ Menu menu = new Menu(MenuHandler_Race);
+ menu.SetTitle("%T", "Race Menu - Title", client);
+ RaceMenuAddItems(client, menu);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+public int MenuHandler_Race(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ char info[16];
+ menu.GetItem(param2, info, sizeof(info));
+
+ if (StrEqual(info, ITEM_INFO_START, false))
+ {
+ if (!StartHostedRace(param1))
+ {
+ DisplayRaceMenu(param1, false);
+ }
+ }
+ else if (StrEqual(info, ITEM_INFO_ABORT, false))
+ {
+ AbortHostedRace(param1);
+ DisplayRaceMenu(param1, false);
+ }
+ else if (StrEqual(info, ITEM_INFO_INVITE, false))
+ {
+ if (!InRace(param1))
+ {
+ HostRace(param1, RaceType_Normal, raceMenuCourse[param1], raceMenuMode[param1], raceMenuCheckpointLimit[param1], raceMenuCheckpointCooldown[param1]);
+ }
+
+ SendRequestAll(param1);
+ GOKZ_PrintToChat(param1, true, "%t", "You Invited Everyone");
+ DisplayRaceMenu(param1, false);
+ }
+ else if (StrEqual(info, ITEM_INFO_MODE, false))
+ {
+ DisplayRaceModeMenu(param1);
+ }
+ else if (StrEqual(info, ITEM_INFO_COURSE, false))
+ {
+ int course = raceMenuCourse[param1];
+ do
+ {
+ course++;
+ if (!GOKZ_IsValidCourse(course))
+ {
+ course = 0;
+ }
+ } while (!GOKZ_GetCourseRegistered(course) && course != raceMenuCourse[param1]);
+ raceMenuCourse[param1] = course;
+ DisplayRaceMenu(param1, false);
+ }
+ else if (StrEqual(info, ITEM_INFO_TELEPORT, false))
+ {
+ DisplayRaceCheckpointMenu(param1);
+ }
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+void RaceMenuAddItems(int client, Menu menu)
+{
+ char display[32];
+
+ menu.RemoveAllItems();
+
+ bool pending = GetRaceInfo(GetRaceID(client), RaceInfo_Status) == RaceStatus_Pending;
+ FormatEx(display, sizeof(display), "%T", "Race Menu - Start Race", client);
+ menu.AddItem(ITEM_INFO_START, display, (InRace(client) && pending) ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED);
+
+ FormatEx(display, sizeof(display), "%T", "Race Menu - Invite Everyone", client);
+ menu.AddItem(ITEM_INFO_INVITE, display, (!InRace(client) || pending) ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED);
+
+ FormatEx(display, sizeof(display), "%T\n \n%T", "Race Menu - Abort Race", client, "Race Menu - Rules", client);
+ menu.AddItem(ITEM_INFO_ABORT, display, InRace(client) ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED);
+
+ FormatEx(display, sizeof(display), "%s", gC_ModeNames[raceMenuMode[client]]);
+ menu.AddItem(ITEM_INFO_MODE, display, InRace(client) ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT);
+
+ if (raceMenuCourse[client] == 0)
+ {
+ FormatEx(display, sizeof(display), "%T", "Race Rules - Main Course", client);
+ }
+ else
+ {
+ FormatEx(display, sizeof(display), "%T %d", "Race Rules - Bonus Course", client, raceMenuCourse[client]);
+ }
+ menu.AddItem(ITEM_INFO_COURSE, display, InRace(client) ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT);
+
+ FormatEx(display, sizeof(display), "%s", GetRaceRuleSummary(client, raceMenuCheckpointLimit[client], raceMenuCheckpointCooldown[client]));
+ menu.AddItem(ITEM_INFO_TELEPORT, display, InRace(client) ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT);
+}
+
+
+
+// =====[ MODE MENU ]=====
+
+static void DisplayRaceModeMenu(int client)
+{
+ Menu menu = new Menu(MenuHandler_RaceMode);
+ menu.ExitButton = false;
+ menu.ExitBackButton = true;
+ menu.SetTitle("%T", "Mode Rule Menu - Title", client);
+ GOKZ_MenuAddModeItems(client, menu, true);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+public int MenuHandler_RaceMode(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ raceMenuMode[param1] = param2;
+ DisplayRaceMenu(param1, false);
+ }
+ else if (action == MenuAction_Cancel)
+ {
+ DisplayRaceMenu(param1, false);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+
+
+// =====[ CHECKPOINT MENU ]=====
+
+static void DisplayRaceCheckpointMenu(int client)
+{
+ Menu menu = new Menu(MenuHandler_RaceCheckpoint);
+ menu.ExitButton = false;
+ menu.ExitBackButton = true;
+ menu.SetTitle("%T", "Checkpoint Rule Menu - Title", client);
+ RaceCheckpointMenuAddItems(client, menu);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+public int MenuHandler_RaceCheckpoint(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ switch (param2)
+ {
+ case CheckpointRule_None:
+ {
+ raceMenuCheckpointLimit[param1] = 0;
+ raceMenuCheckpointCooldown[param1] = 0;
+ DisplayRaceMenu(param1, false);
+ }
+ case CheckpointRule_Limit:
+ {
+ DisplayCheckpointLimitMenu(param1);
+ }
+ case CheckpointRule_Cooldown:
+ {
+ DisplayCheckpointCooldownMenu(param1);
+ }
+ case CheckpointRule_Unlimited:
+ {
+ raceMenuCheckpointLimit[param1] = -1;
+ raceMenuCheckpointCooldown[param1] = 0;
+ DisplayRaceMenu(param1, false);
+ }
+ }
+ }
+ else if (action == MenuAction_Cancel)
+ {
+ DisplayRaceMenu(param1, false);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+void RaceCheckpointMenuAddItems(int client, Menu menu)
+{
+ char display[32];
+
+ menu.RemoveAllItems();
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - None", client);
+ menu.AddItem("", display);
+
+ if (raceMenuCheckpointLimit[client] == -1)
+ {
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - No Checkpoint Limit", client);
+ }
+ else
+ {
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - Checkpoint Limit", client, raceMenuCheckpointLimit[client]);
+ }
+ menu.AddItem("", display);
+
+ if (raceMenuCheckpointCooldown[client] == 0)
+ {
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - No Checkpoint Cooldown", client);
+ }
+ else
+ {
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - Checkpoint Cooldown", client, raceMenuCheckpointCooldown[client]);
+ }
+ menu.AddItem("", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - Unlimited", client);
+ menu.AddItem("", display);
+}
+
+
+
+// =====[ CP LIMIT MENU ]=====
+
+static void DisplayCheckpointLimitMenu(int client)
+{
+ char display[32];
+
+ Menu menu = new Menu(MenuHandler_RaceCheckpointLimit);
+ menu.ExitButton = false;
+ menu.ExitBackButton = true;
+
+ if (raceMenuCheckpointLimit[client] == -1)
+ {
+ menu.SetTitle("%T", "Checkpoint Limit Menu - Title Unlimited", client);
+ }
+ else
+ {
+ menu.SetTitle("%T", "Checkpoint Limit Menu - Title Limited", client, raceMenuCheckpointLimit[client]);
+ }
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Add One", client);
+ menu.AddItem("+1", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Add Five", client);
+ menu.AddItem("+5", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Remove One", client);
+ menu.AddItem("-1", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Remove Five", client);
+ menu.AddItem("-5", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Unlimited", client);
+ menu.AddItem("Unlimited", display);
+
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+public int MenuHandler_RaceCheckpointLimit(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ char item[32];
+ menu.GetItem(param2, item, sizeof(item));
+ if (StrEqual(item, "+1"))
+ {
+ if (raceMenuCheckpointLimit[param1] == -1)
+ {
+ raceMenuCheckpointLimit[param1]++;
+ }
+ raceMenuCheckpointLimit[param1]++;
+ }
+ if (StrEqual(item, "+5"))
+ {
+ if (raceMenuCheckpointLimit[param1] == -1)
+ {
+ raceMenuCheckpointLimit[param1]++;
+ }
+ raceMenuCheckpointLimit[param1] += 5;
+ }
+ if (StrEqual(item, "-1"))
+ {
+ raceMenuCheckpointLimit[param1]--;
+ }
+ if (StrEqual(item, "-5"))
+ {
+ raceMenuCheckpointLimit[param1] -= 5;
+ }
+ if (StrEqual(item, "Unlimited"))
+ {
+ raceMenuCheckpointLimit[param1] = -1;
+ DisplayRaceCheckpointMenu(param1);
+ return 0;
+ }
+
+ raceMenuCheckpointLimit[param1] = raceMenuCheckpointLimit[param1] < 0 ? 0 : raceMenuCheckpointLimit[param1];
+ DisplayCheckpointLimitMenu(param1);
+ }
+ else if (action == MenuAction_Cancel)
+ {
+ DisplayRaceCheckpointMenu(param1);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+
+
+// =====[ CP COOLDOWN MENU ]=====
+
+static void DisplayCheckpointCooldownMenu(int client)
+{
+ char display[32];
+
+ Menu menu = new Menu(MenuHandler_RaceCPCooldown);
+ menu.ExitButton = false;
+ menu.ExitBackButton = true;
+
+ if (raceMenuCheckpointCooldown[client] == -1)
+ {
+ menu.SetTitle("%T", "Checkpoint Cooldown Menu - Title None", client);
+ }
+ else
+ {
+ menu.SetTitle("%T", "Checkpoint Cooldown Menu - Title Limited", client, raceMenuCheckpointCooldown[client]);
+ }
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - Add One Second", client);
+ menu.AddItem("+1", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - Add Five Seconds", client);
+ menu.AddItem("+5", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - Remove One Second", client);
+ menu.AddItem("-1", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - Remove Five Seconds", client);
+ menu.AddItem("-5", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - None", client);
+ menu.AddItem("None", display);
+
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+public int MenuHandler_RaceCPCooldown(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ char item[32];
+ menu.GetItem(param2, item, sizeof(item));
+ if (StrEqual(item, "+1"))
+ {
+ if (raceMenuCheckpointCooldown[param1] == -1)
+ {
+ raceMenuCheckpointCooldown[param1]++;
+ }
+ raceMenuCheckpointCooldown[param1]++;
+ }
+ if (StrEqual(item, "+5"))
+ {
+ if (raceMenuCheckpointCooldown[param1] == -1)
+ {
+ raceMenuCheckpointCooldown[param1]++;
+ }
+ raceMenuCheckpointCooldown[param1] += 5;
+ }
+ if (StrEqual(item, "-1"))
+ {
+ raceMenuCheckpointCooldown[param1]--;
+ }
+ if (StrEqual(item, "-5"))
+ {
+ raceMenuCheckpointCooldown[param1] -= 5;
+ }
+ if (StrEqual(item, "None"))
+ {
+ raceMenuCheckpointCooldown[param1] = 0;
+ DisplayRaceCheckpointMenu(param1);
+ return 0;
+ }
+
+ raceMenuCheckpointCooldown[param1] = raceMenuCheckpointCooldown[param1] < 0 ? 0 : raceMenuCheckpointCooldown[param1];
+ DisplayCheckpointCooldownMenu(param1);
+ }
+ else if (action == MenuAction_Cancel)
+ {
+ DisplayRaceCheckpointMenu(param1);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+char[] GetRaceRuleSummary(int client, int checkpointLimit, int checkpointCooldown)
+{
+ char rulesString[64];
+ if (checkpointLimit == -1 && checkpointCooldown == 0)
+ {
+ FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - Unlimited", client);
+ }
+ else if (checkpointLimit > 0 && checkpointCooldown == 0)
+ {
+ FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - Limited Checkpoints", client, checkpointLimit);
+ }
+ else if (checkpointLimit == -1 && checkpointCooldown > 0)
+ {
+ FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - Limited Cooldown", client, checkpointCooldown);
+ }
+ else if (checkpointLimit > 0 && checkpointCooldown > 0)
+ {
+ FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - Limited Everything", client, checkpointLimit, checkpointCooldown);
+ }
+ else if (checkpointLimit == 0)
+ {
+ FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - No Checkpoints", client);
+ }
+
+ return rulesString;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-racing/racer.sp b/sourcemod/scripting/gokz-racing/racer.sp
new file mode 100644
index 0000000..eb4ff3f
--- /dev/null
+++ b/sourcemod/scripting/gokz-racing/racer.sp
@@ -0,0 +1,439 @@
+/*
+ Functions that affect the state of clients participating in a race.
+ See the RacerStatus enum for possible states.
+*/
+
+
+
+static int racerStatus[MAXPLAYERS + 1];
+static int racerRaceID[MAXPLAYERS + 1];
+static float lastTimerStartTime[MAXPLAYERS + 1];
+static float lastCheckpointTime[MAXPLAYERS + 1];
+
+
+
+// =====[ EVENTS ]=====
+
+Action OnTimerStart_Racer(int client, int course)
+{
+ if (InCountdown(client)
+ || InStartedRace(client) && (!InRaceMode(client) || !IsRaceCourse(client, course)))
+ {
+ return Plugin_Stop;
+ }
+
+ return Plugin_Continue;
+}
+
+Action OnTimerStart_Post_Racer(int client)
+{
+ lastTimerStartTime[client] = GetGameTime();
+ return Plugin_Continue;
+}
+
+Action OnMakeCheckpoint_Racer(int client)
+{
+ if (GOKZ_GetTimerRunning(client) && InStartedRace(client))
+ {
+ int checkpointRule = GetRaceInfo(GetRaceID(client), RaceInfo_CheckpointRule);
+ if (checkpointRule == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Checkpoints Not Allowed During Race");
+ GOKZ_PlayErrorSound(client);
+ return Plugin_Handled;
+ }
+ else if (checkpointRule != -1 && GOKZ_GetCheckpointCount(client) >= checkpointRule)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Checkpoints Left");
+ GOKZ_PlayErrorSound(client);
+ return Plugin_Handled;
+ }
+
+ float cooldownRule = float(GetRaceInfo(GetRaceID(client), RaceInfo_CooldownRule));
+ float timeSinceLastCheckpoint = FloatMin(
+ GetGameTime() - lastTimerStartTime[client],
+ GetGameTime() - lastCheckpointTime[client]);
+ if (timeSinceLastCheckpoint < cooldownRule)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Checkpoint On Cooldown", (cooldownRule - timeSinceLastCheckpoint));
+ GOKZ_PlayErrorSound(client);
+ return Plugin_Handled;
+ }
+ }
+
+ return Plugin_Continue;
+}
+
+void OnMakeCheckpoint_Post_Racer(int client)
+{
+ lastCheckpointTime[client] = GetGameTime();
+}
+
+Action OnUndoTeleport_Racer(int client)
+{
+ if (GOKZ_GetTimerRunning(client)
+ && InStartedRace(client)
+ && GetRaceInfo(GetRaceID(client), RaceInfo_CheckpointRule) != -1)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Undo TP Not Allowed During Race");
+ GOKZ_PlayErrorSound(client);
+ return Plugin_Handled;
+ }
+
+ return Plugin_Continue;
+}
+
+
+
+// =====[ GENERAL ]=====
+
+int GetStatus(int client)
+{
+ return racerStatus[client];
+}
+
+int GetRaceID(int client)
+{
+ return racerRaceID[client];
+}
+
+bool InRace(int client)
+{
+ return GetStatus(client) != RacerStatus_Available;
+}
+
+bool InStartedRace(int client)
+{
+ return GetStatus(client) == RacerStatus_Racing;
+}
+
+bool InCountdown(int client)
+{
+ return GetRaceInfo(GetRaceID(client), RaceInfo_Status) == RaceStatus_Countdown;
+}
+
+bool InRaceMode(int client)
+{
+ return GOKZ_GetCoreOption(client, Option_Mode) == GetRaceInfo(GetRaceID(client), RaceInfo_Mode);
+}
+
+bool IsRaceCourse(int client, int course)
+{
+ return course == GetRaceInfo(GetRaceID(client), RaceInfo_Course);
+}
+
+bool IsFinished(int client)
+{
+ int status = GetStatus(client);
+ return status == RacerStatus_Finished || status == RacerStatus_Surrendered;
+}
+
+bool IsAccepted(int client)
+{
+ return GetStatus(client) == RacerStatus_Accepted;
+}
+
+bool IsRaceHost(int client)
+{
+ return GetRaceHost(GetRaceID(client)) == client;
+}
+
+static void ResetRacer(int client)
+{
+ racerStatus[client] = RacerStatus_Available;
+ racerRaceID[client] = -1;
+ lastTimerStartTime[client] = 0.0;
+ lastCheckpointTime[client] = 0.0;
+}
+
+static void ResetRacersInRace(int raceID)
+{
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (racerRaceID[client] == raceID)
+ {
+ ResetRacer(client);
+ }
+ }
+}
+
+
+
+// =====[ RACING ]=====
+
+void StartRacer(int client)
+{
+ if (racerStatus[client] == RacerStatus_Pending)
+ {
+ DeclineRequest(client, true);
+ return;
+ }
+
+ if (racerStatus[client] != RacerStatus_Accepted)
+ {
+ return;
+ }
+
+ racerStatus[client] = RacerStatus_Racing;
+
+ // Prepare the racer
+ GOKZ_StopTimer(client);
+ GOKZ_SetCoreOption(client, Option_Mode, GetRaceInfo(racerRaceID[client], RaceInfo_Mode));
+
+ int course = GetRaceInfo(racerRaceID[client], RaceInfo_Course);
+ if (GOKZ_SetStartPositionToMapStart(client, course))
+ {
+ GOKZ_TeleportToStart(client);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Start Found", course);
+ }
+}
+
+bool FinishRacer(int client, int course)
+{
+ if (racerStatus[client] != RacerStatus_Racing ||
+ course != GetRaceInfo(racerRaceID[client], RaceInfo_Course))
+ {
+ return false;
+ }
+
+ racerStatus[client] = RacerStatus_Finished;
+
+ int raceID = racerRaceID[client];
+ int place = IncrementFinishedRacerCount(raceID);
+
+ Call_OnFinish(client, raceID, place);
+
+ CheckRaceFinished(raceID);
+
+ return true;
+}
+
+bool SurrenderRacer(int client)
+{
+ if (racerStatus[client] == RacerStatus_Available
+ || racerStatus[client] == RacerStatus_Surrendered)
+ {
+ return false;
+ }
+
+ racerStatus[client] = RacerStatus_Surrendered;
+
+ int raceID = racerRaceID[client];
+
+ Call_OnSurrender(client, raceID);
+
+ CheckRaceFinished(raceID);
+
+ return true;
+}
+
+// Auto-finish last remaining racer, and reset everyone if no one is left
+static void CheckRaceFinished(int raceID)
+{
+ ArrayList remainingRacers = GetUnfinishedRacers(raceID);
+ if (remainingRacers.Length == 1)
+ {
+ int lastRacer = remainingRacers.Get(0);
+ FinishRacer(lastRacer, GetRaceInfo(racerRaceID[lastRacer], RaceInfo_Course));
+ }
+ else if (remainingRacers.Length == 0)
+ {
+ ResetRacersInRace(raceID);
+ }
+ delete remainingRacers;
+}
+
+bool AbortRacer(int client)
+{
+ if (racerStatus[client] == RacerStatus_Available)
+ {
+ return false;
+ }
+
+ ResetRacer(client);
+
+ return true;
+}
+
+
+
+// =====[ HOSTING ]=====
+
+int HostRace(int client, int type, int course, int mode, int checkpointRule, int cooldownRule)
+{
+ if (InRace(client))
+ {
+ return -1;
+ }
+
+ int raceID = RegisterRace(client, type, course, mode, checkpointRule, cooldownRule);
+ racerRaceID[client] = raceID;
+ racerStatus[client] = RacerStatus_Accepted;
+
+ return raceID;
+}
+
+bool StartHostedRace(int client)
+{
+ if (!InRace(client) || !IsRaceHost(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "You Are Not Hosting A Race");
+ GOKZ_PlayErrorSound(client);
+ return false;
+ }
+
+ int raceID = racerRaceID[client];
+
+ if (GetRaceInfo(raceID, RaceInfo_Status) != RaceStatus_Pending)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Race Already Started");
+ GOKZ_PlayErrorSound(client);
+ return false;
+ }
+
+ if (GetAcceptedRacersCount(raceID) <= 1)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No One Accepted");
+ GOKZ_PlayErrorSound(client);
+ return false;
+ }
+
+ return StartRace(raceID);
+}
+
+bool AbortHostedRace(int client)
+{
+ if (!InRace(client) || !IsRaceHost(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "You Are Not Hosting A Race");
+ GOKZ_PlayErrorSound(client);
+ return false;
+ }
+
+ int raceID = racerRaceID[client];
+
+ return AbortRace(raceID);
+}
+
+
+
+// =====[ REQUESTS ]=====
+
+bool SendRequest(int host, int target)
+{
+ if (IsFakeClient(target) || target == host || InRace(target)
+ || !IsRaceHost(host) || GetRaceInfo(racerRaceID[host], RaceInfo_Status) != RaceStatus_Pending)
+ {
+ return false;
+ }
+
+ int raceID = racerRaceID[host];
+
+ racerRaceID[target] = raceID;
+ racerStatus[target] = RacerStatus_Pending;
+
+ // Host callback
+ DataPack data = new DataPack();
+ data.WriteCell(GetClientUserId(host));
+ data.WriteCell(GetClientUserId(target));
+ data.WriteCell(raceID);
+ CreateTimer(RC_REQUEST_TIMEOUT_TIME, Timer_RequestTimeout, data);
+
+ Call_OnRequestReceived(target, raceID);
+
+ return true;
+}
+
+public Action Timer_RequestTimeout(Handle timer, DataPack data)
+{
+ data.Reset();
+ int host = GetClientOfUserId(data.ReadCell());
+ int target = GetClientOfUserId(data.ReadCell());
+ int raceID = data.ReadCell();
+ delete data;
+
+ if (!IsValidClient(host) || racerRaceID[host] != raceID
+ || !IsValidClient(target) || racerRaceID[target] != raceID)
+ {
+ return Plugin_Continue;
+ }
+
+ // If haven't accepted by now, auto decline the race
+ if (racerStatus[target] == RacerStatus_Pending)
+ {
+ DeclineRequest(target, true);
+ }
+ return Plugin_Continue;
+}
+
+int SendRequestAll(int host)
+{
+ int sentCount = 0;
+ for (int target = 1; target <= MaxClients; target++)
+ {
+ if (IsClientInGame(target) && SendRequest(host, target))
+ {
+ sentCount++;
+ }
+ }
+ return sentCount;
+}
+
+bool AcceptRequest(int client)
+{
+ if (GetStatus(client) != RacerStatus_Pending)
+ {
+ return false;
+ }
+
+ racerStatus[client] = RacerStatus_Accepted;
+
+ Call_OnRequestAccepted(client, racerRaceID[client]);
+
+ return true;
+}
+
+bool DeclineRequest(int client, bool timeout = false)
+{
+ if (GetStatus(client) != RacerStatus_Pending)
+ {
+ return false;
+ }
+
+ int raceID = racerRaceID[client];
+ ResetRacer(client);
+
+ Call_OnRequestDeclined(client, raceID, timeout);
+
+ return true;
+}
+
+
+
+// =====[ EVENTS ]=====
+
+void OnClientPutInServer_Racer(int client)
+{
+ ResetRacer(client);
+}
+
+void OnClientDisconnect_Racer(int client)
+{
+ // Abort if player was the host of the race, else surrender
+ if (InRace(client))
+ {
+ if (IsRaceHost(client) && GetRaceInfo(racerRaceID[client], RaceInfo_Status) == RaceStatus_Pending)
+ {
+ AbortRace(racerRaceID[client]);
+ }
+ else
+ {
+ SurrenderRacer(client);
+ }
+ }
+
+ ResetRacer(client);
+} \ No newline at end of file