Add likely upcoming cards for Chill Season 2023 and option to disallow

This commit is contained in:
Andrio Celos 2023-11-24 12:53:09 +11:00
parent 750a430fa8
commit dd7366c7d4
12 changed files with 156 additions and 19 deletions

View File

@ -75,11 +75,15 @@
<p>Other players can join using a link to this page.<br/>
<button type="button" id="shareLinkButton">Share link</button><button type="button" id="showQrCodeButton">Show QR code</button></p>
<ul id="playerList"></ul>
<h3>Game rules</h3>
<label for="lobbyTimeLimitBox">
Turn time limit:
<input type="number" id="lobbyTimeLimitBox" min="10" max="120" step="10" placeholder="None"/>
<span id="lobbyTimeLimitUnit">seconds</span>
</label>
<label for="lobbyAllowUpcomingCardsBox">
<input type="checkbox" id="lobbyAllowUpcomingCardsBox"/> Allow upcoming cards
</label>
</section>
<section id="lobbySelectedStageSection">
<h3>Stage</h3>
@ -619,6 +623,9 @@
</select>
</label>
</p>
<p>
<label for="gameSetupAllowUpcomingCardsBox"><input type="checkbox" id="gameSetupAllowUpcomingCardsBox" checked/> Allow upcoming cards</label>
</p>
<p>Stage selection:</p>
<table>
<tr>

View File

@ -24,6 +24,7 @@ interface CustomRoomConfig {
maxPlayers: number;
turnTimeLimit: number | null;
goalWinCount: number | null;
allowUpcomingCards: boolean;
stageSelectionMethodFirst: StageSelectionMethod;
stageSelectionMethodAfterWin: StageSelectionMethod | null;
stageSelectionMethodAfterDraw: StageSelectionMethod | null;

View File

@ -12,6 +12,8 @@ interface Game {
turnTimeLeft: number | null,
/** The number of game wins needed to win the set, or null if no goal win count is set. */
goalWinCount: number | null,
/** Whether upcoming cards may be used. */
allowUpcomingCards: boolean
}
/** A UUID used to identify the client. */

View File

@ -155,7 +155,7 @@ function initTest(stage: Stage) {
clear();
testMode = true;
gamePage.classList.add('deckTest');
currentGame = { id: 'test', game: { state: GameState.Ongoing, maxPlayers: 2, players: [ ], turnNumber: 1, turnTimeLimit: null, turnTimeLeft: null, goalWinCount: null }, me: { playerIndex: 0, move: null, deck: null, hand: null, cardsUsed: [ ], stageSelectionPrompt: null }, webSocket: null };
currentGame = { id: 'test', game: { state: GameState.Ongoing, maxPlayers: 2, players: [ ], turnNumber: 1, turnTimeLimit: null, turnTimeLeft: null, goalWinCount: null, allowUpcomingCards: true }, me: { playerIndex: 0, move: null, deck: null, hand: null, cardsUsed: [ ], stageSelectionPrompt: null }, webSocket: null };
board.resize(stage.copyGrid());
const startSpaces = stage.getStartSpaces(2);
board.startSpaces = startSpaces;

View File

@ -25,6 +25,7 @@ const lobbyDeckButtons = new CheckButtonGroup<SavedDeck>(lobbyDeckList);
const lobbyDeckSubmitButton = document.getElementById('submitDeckButton') as HTMLButtonElement;
const lobbyTimeLimitBox = document.getElementById('lobbyTimeLimitBox') as HTMLInputElement;
const lobbyAllowUpcomingCardsBox = document.getElementById('lobbyAllowUpcomingCardsBox') as HTMLInputElement;
const lobbyTimeLimitUnit = document.getElementById('lobbyTimeLimitUnit')!;
const qrCodeDialog = document.getElementById('qrCodeDialog') as HTMLDialogElement;
@ -57,6 +58,7 @@ function initLobbyPage(url: string) {
lobbyShareData = null;
shareLinkButton.innerText = 'Copy link';
}
lobbyDeckSection.hidden = true;
}
function showStageSelectionForm(prompt: StageSelectionPrompt | null, isReady: boolean) {
@ -179,6 +181,7 @@ function lobbyResetSlots() {
function lobbyLockSettings(lock: boolean) {
lobbyTimeLimitBox.readOnly = lock;
lobbyAllowUpcomingCardsBox.disabled = lock;
}
function clearReady() {
@ -289,11 +292,13 @@ function initDeckSelection() {
lobbyDeckButtons.add(button, deck);
buttonElement.addEventListener('click', () => {
selectedDeck = deck;
lobbyDeckSubmitButton.disabled = false;
if (button.enabled) {
selectedDeck = deck;
lobbyDeckSubmitButton.disabled = false;
}
});
if (!deck.isValid) {
if (!deck.isValid || (!currentGame.game.allowUpcomingCards && deck.cards.find(n => cardDatabase.get(n).number < 0))) {
button.enabled = false;
} else if (deck.name == lastDeckName) {
selectedDeck = deck;
@ -311,13 +316,22 @@ function initDeckSelection() {
lobbyTimeLimitBox.addEventListener('change', () => {
let req = new XMLHttpRequest();
req.open('POST', `${config.apiBaseUrl}/games/${currentGame!.id}/setTurnTimeLimit`);
req.open('POST', `${config.apiBaseUrl}/games/${currentGame!.id}/setGameSettings`);
let data = new URLSearchParams();
data.append('clientToken', clientToken);
data.append('turnTimeLimit', lobbyTimeLimitBox.value || '');
req.send(data.toString());
});
lobbyAllowUpcomingCardsBox.addEventListener('change', () => {
let req = new XMLHttpRequest();
req.open('POST', `${config.apiBaseUrl}/games/${currentGame!.id}/setGameSettings`);
let data = new URLSearchParams();
data.append('clientToken', clientToken);
data.append('allowUpcomingCards', lobbyAllowUpcomingCardsBox.checked.toString());
req.send(data.toString());
});
deckSelectionForm.addEventListener('submit', e => {
e.preventDefault();
if (selectedDeck == null) return;

View File

@ -18,6 +18,7 @@ const gameSetupForm = document.getElementById('gameSetupForm') as HTMLFormElemen
const maxPlayersBox = document.getElementById('maxPlayersBox') as HTMLSelectElement;
const turnTimeLimitBox = document.getElementById('turnTimeLimitBox') as HTMLInputElement;
const goalWinCountBox = document.getElementById('goalWinCountBox') as HTMLSelectElement;
const gameSetupAllowUpcomingCardsBox = document.getElementById('gameSetupAllowUpcomingCardsBox') as HTMLInputElement;
const stageSelectionRuleFirstBox = document.getElementById('stageSelectionRuleFirstBox') as HTMLSelectElement;
const stageSelectionRuleAfterWinBox = document.getElementById('stageSelectionRuleAfterWinBox') as HTMLSelectElement;
const stageSelectionRuleAfterDrawBox = document.getElementById('stageSelectionRuleAfterDrawBox') as HTMLSelectElement;
@ -153,10 +154,11 @@ function createRoom(useOptionsForm: boolean) {
data.append('name', name);
data.append('clientToken', clientToken);
if (useOptionsForm) {
const settings = {
const settings = <CustomRoomConfig> {
maxPlayers: parseInt(maxPlayersBox.value),
turnTimeLimit: turnTimeLimitBox.value ? turnTimeLimitBox.valueAsNumber : null,
goalWinCount: goalWinCountBox.value ? parseInt(goalWinCountBox.value) : null,
allowUpcomingCards: gameSetupAllowUpcomingCardsBox.checked,
stageSelectionMethodFirst: StageSelectionMethod[stageSelectionRuleFirstBox.value as keyof typeof StageSelectionMethod],
stageSelectionMethodAfterWin: stageSelectionRuleAfterWinBox.value == 'Inherit' ? null : StageSelectionMethod[stageSelectionRuleAfterWinBox.value as keyof typeof StageSelectionMethod],
stageSelectionMethodAfterDraw: stageSelectionRuleAfterDrawBox.value == 'Inherit' ? null : StageSelectionMethod[stageSelectionRuleAfterDrawBox.value as keyof typeof StageSelectionMethod],
@ -172,6 +174,7 @@ function createRoom(useOptionsForm: boolean) {
data.append('turnTimeLimit', turnTimeLimitBox.value);
if (goalWinCountBox.value)
data.append('goalWinCount', goalWinCountBox.value);
data.append('allowUpcomingCards', settings.allowUpcomingCards.toString());
const stageSelectionRuleFirst = {
method: settings.stageSelectionMethodFirst,
@ -363,6 +366,7 @@ window.addEventListener('popstate', () => {
maxPlayersBox.value = userConfig.lastCustomRoomConfig.maxPlayers.toString();
turnTimeLimitBox.value = userConfig.lastCustomRoomConfig.turnTimeLimit?.toString() ?? '';
goalWinCountBox.value = userConfig.lastCustomRoomConfig.goalWinCount?.toString() ?? '';
gameSetupAllowUpcomingCardsBox.checked = userConfig.lastCustomRoomConfig.allowUpcomingCards ?? true;
stageSelectionRuleFirstBox.value = StageSelectionMethod[userConfig.lastCustomRoomConfig.stageSelectionMethodFirst]
stageSelectionRuleAfterWinBox.value = userConfig.lastCustomRoomConfig.stageSelectionMethodAfterWin != null ? StageSelectionMethod[userConfig.lastCustomRoomConfig.stageSelectionMethodAfterWin] : 'Inherit';
stageSelectionRuleAfterDrawBox.value = userConfig.lastCustomRoomConfig.stageSelectionMethodAfterDraw != null ? StageSelectionMethod[userConfig.lastCustomRoomConfig.stageSelectionMethodAfterDraw] : 'Inherit';

View File

@ -183,7 +183,8 @@ class ReplayLoader {
turnNumber: 0,
turnTimeLimit: null,
turnTimeLeft: null,
goalWinCount: goalWinCount
goalWinCount: goalWinCount,
allowUpcomingCards: true
},
me: null,
webSocket: null

View File

@ -49,6 +49,8 @@ function onInitialise(callback: () => void) {
function initCardDatabase(cards: Card[]) {
deckEditInitCardDatabase(cards);
if (!cards.find(c => c.number < 0))
gameSetupAllowUpcomingCardsBox.parentElement!.hidden = true;
}
function initStageDatabase(stages: Stage[]) {
preGameInitStageDatabase(stages);
@ -108,6 +110,7 @@ function onGameSettingsChange() {
if (currentGame == null) return;
if (lobbyTimeLimitBox.value != currentGame.game.turnTimeLimit?.toString() ?? '')
lobbyTimeLimitBox.value = currentGame.game.turnTimeLimit?.toString() ?? '';
lobbyAllowUpcomingCardsBox.checked = currentGame.game.allowUpcomingCards;
}
function onGameStateChange(game: any, playerData: PlayerData | null) {
@ -271,6 +274,7 @@ function setupWebSocket(gameID: string) {
turnTimeLimit: payload.data.turnTimeLimit,
turnTimeLeft: payload.data.turnTimeLeft,
goalWinCount: payload.data.goalWinCount,
allowUpcomingCards: payload.data.allowUpcomingCards
},
me: payload.playerData,
webSocket: webSocket,
@ -330,6 +334,7 @@ function setupWebSocket(gameID: string) {
switch (payload.event) {
case 'settingsChange':
currentGame.game.turnTimeLimit = payload.data.turnTimeLimit;
currentGame.game.allowUpcomingCards = payload.data.allowUpcomingCards;
onGameSettingsChange();
break;
case 'join':

View File

@ -497,7 +497,7 @@ dialog::backdrop {
flex-grow: 1;
}
.cardButton:is([data-card-number="163"], [data-card-number="166"], [data-card-number="196"], [data-card-number="197"], [data-card-number="199"],
[data-card-number="202"], [data-card-number="216"]) .cardName {
[data-card-number="202"], [data-card-number="216"], [data-card-number="-20"]) .cardName {
position: absolute;
left: -1em;
right: -1em;

View File

@ -2216,14 +2216,94 @@ public static class CardDatabase {
{ 0, 0, 0, 0, 0, I, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0 }
}) { InkColour1 = new(84, 142, 122), InkColour2 = new(193, 111, 98) },
new(-13, "Foil Squeezer", Rarity.Common, 0.95f, "ShooterFlash01", new Space[,] {
{ 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, S, 0, 0, 0, 0, 0, 0 },
{ 0, 0, I, 0, 0, 0, 0, 0 },
{ 0, 0, 0, I, I, 0, 0, 0 },
{ 0, 0, 0, I, I, I, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0 }
}),
new(-14, "Snipewriter 5B", Rarity.Common, 0.86f, "ChargerPencil01", new Space[,] {
{ 0, 0, 0, I, 0, 0, 0, 0 },
{ 0, 0, 0, S, 0, 0, 0, 0 },
{ 0, 0, 0, I, 0, 0, 0, 0 },
{ 0, 0, 0, I, 0, 0, 0, 0 },
{ 0, 0, 0, I, I, 0, 0, 0 },
{ 0, 0, I, I, 0, 0, 0, 0 },
{ 0, 0, 0, I, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0 }
}),
new(-15, "Enperry\nSplat Dualies", Rarity.Common, 0.97f, "ManeuverNormal01", new Space[,] {
{ 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, I, 0, 0, 0, 0, 0 },
{ 0, 0, I, 0, 0, 0, 0, 0 },
{ 0, 0, I, S, 0, 0, 0, 0 },
{ 0, 0, I, I, I, 0, 0, 0 },
{ 0, 0, 0, 0, I, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0 }
}),
new(-16, "Undercover\nSorella Brella", Rarity.Common, 0.96f, "ShelterCompact01", new Space[,] {
{ 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, S, 0, 0, 0, 0 },
{ 0, 0, I, 0, I, 0, 0, 0 },
{ 0, I, 0, I, 0, I, 0, 0 },
{ 0, 0, 0, I, I, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0 }
}),
new(-17, "Custom Blaster", Rarity.Common, 0.8f, "BlasterMiddle01", new Space[,] {
{ 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, S, I, 0, 0, 0, 0 },
{ 0, 0, I, I, 0, 0, 0, 0 },
{ 0, 0, 0, I, 0, 0, 0, 0 },
{ 0, 0, 0, I, I, 0, 0, 0 },
{ 0, 0, I, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0 }
}),
new(-18, "New S-BLAST", Rarity.Common, 0.97f, "BlasterPrecision01", new Space[,] {
{ 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, S, I, 0, 0, 0, 0 },
{ 0, 0, I, I, 0, 0, 0, 0 },
{ 0, 0, I, I, 0, 0, 0, 0 },
{ 0, 0, 0, I, I, 0, 0, 0 },
{ 0, 0, 0, I, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0 }
}),
new(-19, "Painbrush\nNouveau", Rarity.Common, 1f, "BrushHeavy01", new Space[,] {
{ 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, I, 0, 0, 0, 0 },
{ 0, 0, S, I, 0, 0, 0, 0 },
{ 0, I, I, I, 0, 0, 0, 0 },
{ 0, 0, 0, 0, I, 0, 0, 0 },
{ 0, 0, 0, 0, I, I, 0, 0 },
{ 0, 0, 0, 0, 0, 0, I, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0 }
}),
new(-20, "Splatana Stamper\nNouveau", Rarity.Common, 0.69f, "SaberNormal01", new Space[,] {
{ 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, I, S, I, I, I, I, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0 }
}),
];
private static readonly Dictionary<int, Card> byAltNumber;
public static int LastOfficialCardNumber { get; }
public static Version Version { get; } = new(5, 0, 2, 0);
public static DateTime LastModified { get; } = new(2023, 10, 9, 22, 30, 0, DateTimeKind.Utc);
public static Version Version { get; } = new(5, 1, 0, 0);
public static DateTime LastModified { get; } = new(2023, 11, 24, 0, 0, 0, DateTimeKind.Utc);
public static string JSON { get; }
public static ReadOnlyCollection<Card> Cards { get; }

View File

@ -24,6 +24,8 @@ public class Game(int maxPlayers) {
[JsonIgnore]
internal DateTime abandonedSince = DateTime.UtcNow;
public bool AllowUpcomingCards { get; set; } = true;
public required StageSelectionRules StageSelectionRuleFirst { get; set; }
public required StageSelectionRules StageSelectionRuleAfterWin { get; set; }
public required StageSelectionRules StageSelectionRuleAfterDraw { get; set; }

View File

@ -15,7 +15,7 @@ using Timer = System.Timers.Timer;
namespace TableturfBattleServer;
internal class Program {
internal partial class Program {
internal static HttpServer? httpServer;
internal static Dictionary<Guid, Game> games = [];
@ -25,6 +25,7 @@ internal class Program {
private const int InactiveGameLimit = 1000;
private static readonly TimeSpan InactiveGameTimeout = TimeSpan.FromMinutes(5);
internal static readonly char[] DELIMITERS = new[] { ',', ' ' };
private static string? GetClientRootPath() {
var directory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
@ -160,6 +161,13 @@ internal class Program {
} else
clientToken = Guid.NewGuid();
var allowUpcomingCards = false;
if (d.TryGetValue("allowUpcomingCards", out var allowUpcomingCardsString)) {
if (!bool.TryParse(allowUpcomingCardsString, out allowUpcomingCards))
SetErrorResponse(e.Response, new(HttpStatusCode.UnprocessableEntity, "InvalidGameSettings", "allowUpcomingCards was invalid."));
} else
allowUpcomingCards = true;
StageSelectionRules? stageSelectionRuleFirst = null, stageSelectionRuleAfterWin = null, stageSelectionRuleAfterDraw = null;
if (d.TryGetValue("stageSelectionRuleFirst", out var json1)) {
if (!TryParseStageSelectionRule(json1, out stageSelectionRuleFirst) || stageSelectionRuleFirst.Method is StageSelectionMethod.Same or StageSelectionMethod.Counterpick) {
@ -197,7 +205,7 @@ internal class Program {
} else
spectate = false;
var game = new Game(maxPlayers) { GoalWinCount = goalWinCount, TurnTimeLimit = turnTimeLimit, StageSelectionRuleFirst = stageSelectionRuleFirst, StageSelectionRuleAfterWin = stageSelectionRuleAfterWin, StageSelectionRuleAfterDraw = stageSelectionRuleAfterDraw, ForceSameDeckAfterDraw = forceSameDeckAfterDraw };
var game = new Game(maxPlayers) { GoalWinCount = goalWinCount, TurnTimeLimit = turnTimeLimit, AllowUpcomingCards = allowUpcomingCards, StageSelectionRuleFirst = stageSelectionRuleFirst, StageSelectionRuleAfterWin = stageSelectionRuleAfterWin, StageSelectionRuleAfterDraw = stageSelectionRuleAfterDraw, ForceSameDeckAfterDraw = forceSameDeckAfterDraw };
if (!spectate)
game.TryAddPlayer(new(game, name, clientToken), out _, out _);
games.Add(game.ID, game);
@ -214,7 +222,7 @@ internal class Program {
} else if (e.Request.RawUrl == "/api/stages") {
SetStaticResponse(e.Request, e.Response, StageDatabase.JSON, StageDatabase.Version.ToString(), StageDatabase.LastModified);
} else {
var m = Regex.Match(e.Request.RawUrl, @"^/api/games/([\w-]+)(?:/(\w+)(?:\?clientToken=([\w-]+))?)?$", RegexOptions.Compiled);
var m = GamePathRegex().Match(e.Request.RawUrl);
if (m.Success) {
if (!Guid.TryParse(m.Groups[1].Value, out var gameID)) {
SetErrorResponse(e.Response, new(HttpStatusCode.BadRequest, "InvalidGameID", "Invalid game ID."));
@ -304,7 +312,7 @@ internal class Program {
}
break;
}
case "setTurnTimeLimit": {
case "setGameSettings": {
if (e.Request.HttpMethod != "POST") {
e.Response.AddHeader("Allow", "POST");
SetErrorResponse(e.Response, new(HttpStatusCode.MethodNotAllowed, "MethodNotAllowed", "Invalid request method for this endpoint."));
@ -324,17 +332,23 @@ internal class Program {
SetErrorResponse(e.Response, new(HttpStatusCode.Forbidden, "AccessDenied", "Only the host can do that."));
return;
}
if (d.TryGetValue("turnTimeLimit", out var turnTimeLimitString)) {
if (turnTimeLimitString == "")
game.TurnTimeLimit = null;
else if (!int.TryParse(turnTimeLimitString, out var turnTimeLimit2) || turnTimeLimit2 < 10) {
SetErrorResponse(e.Response, new(HttpStatusCode.UnprocessableEntity, "InvalidTurnTimeLimit", "Invalid turn time limit."));
SetErrorResponse(e.Response, new(HttpStatusCode.UnprocessableEntity, "InvalidGameSettings", "Invalid turn time limit."));
return;
} else
game.TurnTimeLimit = turnTimeLimit2;
} else {
SetErrorResponse(e.Response, new(HttpStatusCode.UnprocessableEntity, "InvalidTurnTimeLimit", "Invalid turn time limit."));
return;
}
if (d.TryGetValue("allowUpcomingCards", out var allowUpcomingCardsString)) {
if (!bool.TryParse(allowUpcomingCardsString, out var allowUpcomingCards)) {
SetErrorResponse(e.Response, new(HttpStatusCode.UnprocessableEntity, "InvalidGameSettings", "Invalid allowUpcomingCards."));
return;
} else
game.AllowUpcomingCards = allowUpcomingCards;
}
game.SendEvent("settingsChange", game, false);
@ -363,7 +377,7 @@ internal class Program {
}
var stages = new HashSet<int>();
foreach (var field in stagesString.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)) {
foreach (var field in stagesString.Split(DELIMITERS, StringSplitOptions.RemoveEmptyEntries)) {
if (!int.TryParse(field, out var i)) {
SetErrorResponse(e.Response, new(HttpStatusCode.BadRequest, "InvalidStage", "Invalid stages."));
return;
@ -445,6 +459,10 @@ internal class Program {
SetErrorResponse(e.Response, new(HttpStatusCode.UnprocessableEntity, "InvalidDeckCards", "Deck cannot have duplicates."));
return;
}
if (!game.AllowUpcomingCards && cardNumber < 0 && CardDatabase.GetCard(cardNumber).Number < 0) {
SetErrorResponse(e.Response, new(HttpStatusCode.UnprocessableEntity, "ForbiddenDeck", "Upcoming cards cannot be used in this game."));
return;
}
cards[i] = cardNumber;
}
@ -697,4 +715,7 @@ internal class Program {
return false;
}
}
[GeneratedRegex(@"^/api/games/([\w-]+)(?:/(\w+)(?:\?clientToken=([\w-]+))?)?$", RegexOptions.Compiled)]
private static partial Regex GamePathRegex();
}