parent
162aa1422b
commit
48b0502cd7
@ -0,0 +1,5 @@ |
||||
<component name="ProjectCodeStyleConfiguration"> |
||||
<state> |
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" /> |
||||
</state> |
||||
</component> |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,92 @@ |
||||
import {SavedWebhook} from "./SavedWebhook.js"; |
||||
import fastify, {FastifyInstance} from "fastify"; |
||||
import {SlashCreator, SlashCreatorOptions} from "slash-create"; |
||||
import fastifyCookie from "fastify-cookie"; |
||||
import {FastifyServerButItWorksUnlikeTheRealOne} from "./FastifyHelpers.js"; |
||||
|
||||
export interface BaseServerDeps { |
||||
appId: string |
||||
botToken: string |
||||
clientSecret: string |
||||
publicKey: string |
||||
listenPort: number |
||||
listenAddress: string |
||||
cookieSecret: string |
||||
gameWebhook: SavedWebhook |
||||
adminWebhook: SavedWebhook |
||||
} |
||||
|
||||
export class BaseServer { |
||||
readonly server: FastifyInstance |
||||
readonly gameWebhook: SavedWebhook |
||||
readonly adminWebhook: SavedWebhook |
||||
readonly appId: string |
||||
readonly clientSecret: string |
||||
readonly listenPort: number |
||||
readonly listenAddress: string |
||||
readonly slashcmd: SlashCreator |
||||
|
||||
constructor({ |
||||
cookieSecret, |
||||
appId, |
||||
clientSecret, |
||||
listenPort, |
||||
listenAddress, |
||||
gameWebhook, |
||||
adminWebhook, |
||||
publicKey, |
||||
botToken, |
||||
slashCreatorOptions = {} |
||||
}: BaseServerDeps & { slashCreatorOptions?: Partial<SlashCreatorOptions> }) { |
||||
this.slashcmd = new SlashCreator({ |
||||
allowedMentions: {everyone: false, roles: false, users: false}, |
||||
applicationID: appId, |
||||
defaultImageFormat: "webp", |
||||
handleCommandsManually: false, |
||||
maxSignatureTimestamp: 3000, |
||||
publicKey, |
||||
requestTimeout: 10000, |
||||
token: botToken, |
||||
unknownCommandResponse: true, |
||||
endpointPath: "/interactions", |
||||
...slashCreatorOptions, |
||||
}) |
||||
this.server = fastify({logger: true}) |
||||
this.slashcmd.withServer(new FastifyServerButItWorksUnlikeTheRealOne(this.server, {alreadyListening: true})) |
||||
this.server.register(fastifyCookie, { |
||||
secret: cookieSecret, |
||||
}) |
||||
this.appId = appId |
||||
this.clientSecret = clientSecret |
||||
this.listenPort = listenPort |
||||
this.listenAddress = listenAddress |
||||
this.gameWebhook = gameWebhook |
||||
this.adminWebhook = adminWebhook |
||||
|
||||
} |
||||
|
||||
async _initInternal(): Promise<void> { |
||||
return |
||||
} |
||||
|
||||
async initialize(): Promise<void> { |
||||
await this._initInternal() |
||||
await this.slashcmd.syncCommandsAsync({ |
||||
syncGuilds: true, |
||||
syncPermissions: true, |
||||
skipGuildErrors: false, |
||||
deleteCommands: true, |
||||
}) |
||||
await this.slashcmd.startServer() |
||||
await this.server.listen(this.listenPort, this.listenAddress) |
||||
} |
||||
|
||||
async _shutdownInternal(): Promise<void> { |
||||
return |
||||
} |
||||
|
||||
async shutdown(): Promise<void> { |
||||
await this._shutdownInternal() |
||||
return this.server.close() |
||||
} |
||||
} |
@ -0,0 +1,32 @@ |
||||
import {FastifyReply, FastifyRequest} from "fastify"; |
||||
import {RouteGenericInterface} from "fastify/types/route"; |
||||
import cryptoRandomString from "crypto-random-string"; |
||||
|
||||
export interface XSRFRoute extends RouteGenericInterface { |
||||
Querystring: { [key in typeof XSRFParameter]: string | string[] | undefined } |
||||
} |
||||
|
||||
export const XSRFCookie = "__Host-XSRF-Cookie"; |
||||
export const XSRFParameter = "state" as const; |
||||
|
||||
export function generateXSRFCookie(res: FastifyReply): string { |
||||
const newState = cryptoRandomString({ |
||||
length: 32, |
||||
type: 'url-safe' |
||||
}) |
||||
res.setCookie(XSRFCookie, newState, { |
||||
path: "/", |
||||
sameSite: "strict", |
||||
httpOnly: true, |
||||
signed: true, |
||||
secure: true, |
||||
}) |
||||
return newState |
||||
} |
||||
|
||||
export function checkAndClearXSRFCookie(req: FastifyRequest<XSRFRoute>, res: FastifyReply): boolean { |
||||
const queryState = req.query[XSRFParameter] ?? null |
||||
const cookieState = req.cookies[XSRFCookie] ?? null |
||||
res.clearCookie(XSRFCookie) |
||||
return cookieState !== null && queryState !== null && cookieState === queryState |
||||
} |
@ -1,100 +1,109 @@ |
||||
import {SavedWebhook} from "./SavedWebhook.js"; |
||||
import fastify, {FastifyInstance} from "fastify"; |
||||
import {DiscordWebhookHandler} from "./DiscordWebhookHandler.js"; |
||||
import {getBaseUrl, RouteWithQuerystring} from "./FastifyHelpers.js"; |
||||
import pug from "pug"; |
||||
import {SetupServer} from "./SetupServer.js"; |
||||
import {checkAndClearXSRFCookie, generateXSRFCookie, XSRFRoute} from "./CookieHelpers.js"; |
||||
import {renderError} from "./PugRenderer.js"; |
||||
import {getBaseUrl} from "./FastifyHelpers.js"; |
||||
import pug from "pug"; |
||||
import {BaseServer, BaseServerDeps} from "./BaseServer.js"; |
||||
import {PullCommand} from "./commands/game/PullCommand.js"; |
||||
|
||||
export class GameServer { |
||||
readonly server: FastifyInstance |
||||
readonly gameWebhook: SavedWebhook |
||||
readonly adminWebhook: SavedWebhook |
||||
readonly appId: string |
||||
readonly secret: string |
||||
readonly port: number |
||||
constructor({appId, secret, port}: {appId: string, secret: string, port: number}) { |
||||
this.server = fastify({ |
||||
logger: true |
||||
}) |
||||
this.appId = appId |
||||
this.secret = secret |
||||
this.port = port |
||||
this.gameWebhook = new SavedWebhook("broadcasthook.json", {logger: this.server.log}) |
||||
this.adminWebhook = new SavedWebhook("logginghook.json", {logger: this.server.log}) |
||||
export class GameServer extends BaseServer { |
||||
readonly setupFactory: () => SetupServer |
||||
|
||||
constructor(deps: BaseServerDeps & { setupFactory: () => SetupServer }) { |
||||
super(deps) |
||||
this.setupFactory = deps.setupFactory |
||||
} |
||||
|
||||
async initialize(): Promise<void> { |
||||
const gameHandler = new DiscordWebhookHandler({ |
||||
webhook: this.gameWebhook, |
||||
templateFilename: "setupGame.pug", |
||||
appId: this.appId, |
||||
secret: this.secret, |
||||
destinationFunc: () => { |
||||
if (this.adminWebhook.isPresent) { |
||||
return "clear" |
||||
} else { |
||||
return "adminChannel" |
||||
} |
||||
}, |
||||
async _initInternal(): Promise<void> { |
||||
this.slashcmd.registerCommand(new PullCommand(this.slashcmd, this.gameWebhook)) |
||||
this.server.get("/game/started", async (req, res) => { |
||||
const token = generateXSRFCookie(res) |
||||
res.code(200) |
||||
res.type("text/html") |
||||
res.send(pug.renderFile("static/pages/game/running.pug", { |
||||
baseUrl: getBaseUrl(req), |
||||
setupModeUrl: `game/stop?token=${token}`, |
||||
shutdownUrl: `shutdown?token=${token}`, |
||||
})) |
||||
}) |
||||
const adminHandler = new DiscordWebhookHandler({ |
||||
webhook: this.adminWebhook, |
||||
templateFilename: "setupAdmin.pug", |
||||
appId: this.appId, |
||||
secret: this.secret, |
||||
destinationFunc: () => { |
||||
if (this.gameWebhook.isPresent) { |
||||
return "clear" |
||||
} else { |
||||
return "gameChannel" |
||||
} |
||||
}, |
||||
this.server.get("/game", async (req, res) => { |
||||
res.redirect("game/started") |
||||
}) |
||||
this.server.get<RouteWithQuerystring>("/setup", async (req, res) => { |
||||
if (!this.gameWebhook.isPresent) { |
||||
res.redirect(`setup/gameChannel`) |
||||
} else if (!this.adminWebhook.isPresent) { |
||||
res.redirect(`setup/adminChannel`) |
||||
} else { |
||||
res.redirect(`setup/done`) |
||||
} |
||||
this.server.get("/game/start", async (req, res) => { |
||||
res.redirect("started") |
||||
}) |
||||
this.server.get("/setup", async (req, res) => { |
||||
res.redirect("game/started") |
||||
}) |
||||
this.server.get("/setup/gameChannel", async (req, res) => { |
||||
res.redirect("../game/started") |
||||
}) |
||||
this.server.get<RouteWithQuerystring>("/setup/gameChannel", async (req, res) => { |
||||
return await gameHandler.handleRequest(req, res) |
||||
this.server.get("/setup/adminChannel", async (req, res) => { |
||||
res.redirect("../game/started") |
||||
}) |
||||
this.server.get<RouteWithQuerystring>("/setup/adminChannel", async (req, res) => { |
||||
return await adminHandler.handleRequest(req, res) |
||||
this.server.get("/setup/clear", async (req, res) => { |
||||
res.redirect("../game/started") |
||||
}) |
||||
this.server.get<RouteWithQuerystring>("/setup/clear", async (req, res) => { |
||||
try { |
||||
await Promise.all([this.gameWebhook, this.adminWebhook] |
||||
.filter((item) => item.isPresent) |
||||
.map((item) => item.clearHook())) |
||||
} catch (e) { |
||||
this.server.get("/setup/done", async (req, res) => { |
||||
res.redirect("../game/started") |
||||
}) |
||||
this.server.get<XSRFRoute>("/game/stop", async (req, res) => { |
||||
if (!checkAndClearXSRFCookie(req, res)) { |
||||
return renderError({ |
||||
baseUrl: getBaseUrl(req), |
||||
res, |
||||
error: e, |
||||
context: "clearing the broadcast webhook" |
||||
code: 400, |
||||
error: "Token was incorrect or not set.", |
||||
context: "stopping the game", |
||||
buttonText: "Return to Game", |
||||
buttonUrl: "game/started" |
||||
}) |
||||
} |
||||
res.redirect(".") |
||||
res.code(200) |
||||
res.type("text/html") |
||||
res.send(pug.renderFile("static/pages/game/stop.pug", { |
||||
startedUrl: "setup" |
||||
})) |
||||
setImmediate(async () => { |
||||
this.server.log.info("Shutting down the game server and switching to the setup server.") |
||||
try { |
||||
await this.server.close() |
||||
} catch (e) { |
||||
this.server.log.error(e, "Failed to shut down the game server") |
||||
} |
||||
try { |
||||
await this.setupFactory().initialize() |
||||
} catch (e) { |
||||
this.server.log.error(e, "Failed to start up the setup server") |
||||
} |
||||
this.server.log.info("Successfully switched from the game server to the setup server.") |
||||
}) |
||||
|
||||
}) |
||||
this.server.get<RouteWithQuerystring>("/setup/done", async (req, res) => { |
||||
if (!this.gameWebhook.isPresent) { |
||||
res.redirect("gameChannel") |
||||
} else if (!this.adminWebhook.isPresent) { |
||||
res.redirect("adminChannel") |
||||
} else { |
||||
res.code(200) |
||||
res.type("text/html") |
||||
res.send(pug.renderFile("static/pages/setupDone.pug", { |
||||
this.server.get<XSRFRoute>("/shutdown", async (req, res) => { |
||||
if (!checkAndClearXSRFCookie(req, res)) { |
||||
return renderError({ |
||||
baseUrl: getBaseUrl(req), |
||||
clearUrl: "setup/clear", |
||||
gameSetupUrl: "setup/gameChannel", |
||||
adminSetupUrl: "setup/adminChannel"})) |
||||
res, |
||||
code: 400, |
||||
error: "Token was incorrect or not set.", |
||||
context: "shutting down the game", |
||||
buttonText: "Return to Game", |
||||
buttonUrl: "game/started" |
||||
}) |
||||
} |
||||
res.code(200) |
||||
res.type("text/html") |
||||
res.send(pug.renderFile("static/pages/shutdown.pug")) |
||||
setImmediate(async () => { |
||||
this.server.log.info("Shutting down the game server.") |
||||
try { |
||||
await this.server.close() |
||||
} catch (e) { |
||||
this.server.log.error(e, "Failed to shut down the game server") |
||||
} |
||||
this.server.log.info("Shut down. Good night...") |
||||
}) |
||||
}) |
||||
await Promise.all([this.gameWebhook.load(), this.adminWebhook.load(), this.server.listen(this.port, "127.0.0.1")]) |
||||
} |
||||
} |
@ -0,0 +1,192 @@ |
||||
import {DiscordWebhookHandler, OAuthRoute} from "./DiscordWebhookHandler.js"; |
||||
import {getBaseUrl} from "./FastifyHelpers.js"; |
||||
import pug from "pug"; |
||||
import {renderError} from "./PugRenderer.js"; |
||||
import {GameServer} from "./GameServer.js"; |
||||
import {checkAndClearXSRFCookie, generateXSRFCookie, XSRFRoute} from "./CookieHelpers.js"; |
||||
import {SetupCommand, setupComponentInteractionHandler} from "./commands/SetupCommand.js"; |
||||
import {BaseServer, BaseServerDeps} from "./BaseServer.js"; |
||||
|
||||
export class SetupServer extends BaseServer { |
||||
readonly gameFactory: () => GameServer |
||||
|
||||
constructor(deps: BaseServerDeps & { gameFactory: () => GameServer }) { |
||||
super(deps) |
||||
this.gameFactory = deps.gameFactory |
||||
} |
||||
|
||||
async _initInternal(): Promise<void> { |
||||
this.slashcmd.registerCommand(new SetupCommand(this.slashcmd)) |
||||
this.slashcmd.on("componentInteraction", setupComponentInteractionHandler) |
||||
const gameHandler = new DiscordWebhookHandler({ |
||||
webhook: this.gameWebhook, |
||||
templateFilename: "setup/game.pug", |
||||
appId: this.appId, |
||||
secret: this.clientSecret, |
||||
destinationFunc: () => { |
||||
if (this.adminWebhook.isPresent) { |
||||
return "clear" |
||||
} else { |
||||
return "adminChannel" |
||||
} |
||||
}, |
||||
}) |
||||
const adminHandler = new DiscordWebhookHandler({ |
||||
webhook: this.adminWebhook, |
||||
templateFilename: "setup/admin.pug", |
||||
appId: this.appId, |
||||
secret: this.clientSecret, |
||||
destinationFunc: () => { |
||||
if (this.gameWebhook.isPresent) { |
||||
return "clear" |
||||
} else { |
||||
return "gameChannel" |
||||
} |
||||
}, |
||||
}) |
||||
this.server.get("/setup", async (req, res) => { |
||||
if (!this.gameWebhook.isPresent) { |
||||
res.redirect(`setup/gameChannel`) |
||||
} else if (!this.adminWebhook.isPresent) { |
||||
res.redirect(`setup/adminChannel`) |
||||
} else { |
||||
res.redirect(`setup/done`) |
||||
} |
||||
}) |
||||
this.server.get("/setup/start", async (req, res) => { |
||||
res.redirect(".") |
||||
}) |
||||
this.server.get("/game", async (req, res) => { |
||||
res.redirect("setup") |
||||
}) |
||||
this.server.get("/game/started", async (req, res) => { |
||||
res.redirect("../setup") |
||||
}) |
||||
this.server.get("/game/stop", async (req, res) => { |
||||
res.redirect("../setup") |
||||
}) |
||||
this.server.get<XSRFRoute>("/game/start", async (req, res) => { |
||||
if (!checkAndClearXSRFCookie(req, res)) { |
||||
return renderError({ |
||||
baseUrl: getBaseUrl(req), |
||||
res, |
||||
code: 400, |
||||
error: "Token was incorrect or not set.", |
||||
context: "starting the game", |
||||
buttonText: "Return to Setup", |
||||
buttonUrl: "setup" |
||||
}) |
||||
} |
||||
if (!this.gameWebhook.isPresent || !this.adminWebhook.isPresent) { |
||||
return renderError({ |
||||
baseUrl: getBaseUrl(req), |
||||
res, |
||||
code: 409, |
||||
error: "You can't start the game while one of the channels is not set!", |
||||
context: "starting the game", |
||||
buttonText: "Finish Setup", |
||||
buttonUrl: "setup" |
||||
}) |
||||
} |
||||
res.code(200) |
||||
res.type("text/html") |
||||
res.send(pug.renderFile("static/pages/setup/start.pug", { |
||||
startedUrl: "game/start" |
||||
})) |
||||
setImmediate(async () => { |
||||
this.server.log.info("Shutting down the setup server and switching to the game server.") |
||||
try { |
||||
await this.server.close() |
||||
} catch (e) { |
||||
this.server.log.error(e, "Failed to shut down the setup server") |
||||
} |
||||
try { |
||||
await this.gameFactory().initialize() |
||||
} catch (e) { |
||||
this.server.log.error(e, "Failed to start up the game server") |
||||
} |
||||
this.server.log.info("Successfully switched from the setup server to the game server.") |
||||
}) |
||||
|
||||
}) |
||||
this.server.get<OAuthRoute>("/setup/gameChannel", async (req, res) => { |
||||
return await gameHandler.handleRequest(req, res) |
||||
}) |
||||
this.server.get<OAuthRoute>("/setup/adminChannel", async (req, res) => { |
||||
return await adminHandler.handleRequest(req, res) |
||||
}) |
||||
this.server.get<XSRFRoute>("/setup/clear", async (req, res) => { |
||||
if (!checkAndClearXSRFCookie(req, res)) { |
||||
return renderError({ |
||||
baseUrl: getBaseUrl(req), |
||||
res, |
||||
code: 400, |
||||
error: "Token was incorrect or not set.", |
||||
context: "clearing the channel setup", |
||||
buttonText: "Return to Setup", |
||||
buttonUrl: "setup" |
||||
}) |
||||
} |
||||
try { |
||||
await Promise.all([this.gameWebhook, this.adminWebhook] |
||||
.filter((item) => item.isPresent) |
||||
.map((item) => item.clearHook())) |
||||
} catch (e) { |
||||
return renderError({ |
||||
baseUrl: getBaseUrl(req), |
||||
res, |
||||
code: 500, |
||||
error: e, |
||||
context: "clearing and deleting the webhooks", |
||||
buttonUrl: "setup/clear", |
||||
buttonText: "Try Again" |
||||
}) |
||||
} |
||||
res.redirect(".") |
||||
}) |
||||
this.server.get("/setup/done", async (req, res) => { |
||||
if (!this.gameWebhook.isPresent) { |
||||
res.redirect("gameChannel") |
||||
} else if (!this.adminWebhook.isPresent) { |
||||
res.redirect("adminChannel") |
||||
} else { |
||||
const token = generateXSRFCookie(res) |
||||
res.code(200) |
||||
res.type("text/html") |
||||
res.send(pug.renderFile("static/pages/setup/done.pug", { |
||||
baseUrl: getBaseUrl(req), |
||||
gameModeUrl: `game/start?token=${token}`, |
||||
clearUrl: `setup/clear?token=${token}`, |
||||
gameSetupUrl: `setup/gameChannel`, |
||||
adminSetupUrl: `setup/adminChannel`, |
||||
shutdownUrl: `shutdown?token=${token}` |
||||
})) |
||||
} |
||||
}) |
||||
this.server.get<XSRFRoute>("/shutdown", async (req, res) => { |
||||
if (!checkAndClearXSRFCookie(req, res)) { |
||||
return renderError({ |
||||
baseUrl: getBaseUrl(req), |
||||
res, |
||||
code: 400, |
||||
error: "Token was incorrect or not set.", |
||||
context: "shutting the server down", |
||||
buttonText: "Return to Setup", |
||||
buttonUrl: "setup" |
||||
}) |
||||
} |
||||
res.code(200) |
||||
res.type("text/html") |
||||
res.send(pug.renderFile("static/pages/shutdown.pug")) |
||||
setImmediate(async () => { |
||||
this.server.log.info("Shutting down the setup server.") |
||||
try { |
||||
await this.server.close() |
||||
} catch (e) { |
||||
this.server.log.error(e, "Failed to shut down the setup server") |
||||
} |
||||
this.server.log.info("Shut down. Good night...") |
||||
}) |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,28 @@ |
||||
import {AutocompleteContext, CommandContext, ComponentContext, Message, SlashCommand, SlashCreator} from "slash-create"; |
||||
|
||||
export class SetupCommand extends SlashCommand { |
||||
constructor(creator: SlashCreator) { |
||||
super(creator, { |
||||
name: "setup-commands-disabled", |
||||
unknown: true |
||||
}); |
||||
} |
||||
|
||||
autocomplete(ctx: AutocompleteContext): Promise<any> { |
||||
return ctx.sendResults([]) |
||||
} |
||||
|
||||
run(ctx: CommandContext): Promise<any> { |
||||
return ctx.send({ |
||||
ephemeral: true, |
||||
content: "The server is currently in setup mode! You can't run commands right now..." |
||||
}) |
||||
} |
||||
} |
||||
|
||||
export function setupComponentInteractionHandler(ctx: ComponentContext): Promise<boolean | Message> { |
||||
return ctx.send({ |
||||
ephemeral: true, |
||||
content: "The server is currently in setup mode! You can't interact with its messages right now..." |
||||
}) |
||||
} |
@ -0,0 +1,56 @@ |
||||
import {CommandContext, CommandOptionType, SlashCommand, SlashCreator} from "slash-create"; |
||||
import {SavedWebhook} from "../../SavedWebhook.js"; |
||||
import {Chance} from "chance"; |
||||
|
||||
const rand = Chance() |
||||
|
||||
export class PullCommand extends SlashCommand { |
||||
readonly gameWebhook: SavedWebhook |
||||
|
||||
constructor(creator: SlashCreator, gameWebhook: SavedWebhook) { |
||||
super(creator, { |
||||
name: "pull", |
||||
guildIDs: gameWebhook.state?.guild_id, |
||||
description: "Pulls one or more new heroines from the ether.", |
||||
options: [ |
||||
{ |
||||
name: "count", |
||||
description: "The number of heroines to pull.", |
||||
required: false, |
||||
max_value: 10, |
||||
min_value: 1, |
||||
type: CommandOptionType.NUMBER, |
||||
} |
||||
] |
||||
}); |
||||
this.gameWebhook = gameWebhook |
||||
} |
||||
|
||||
run(ctx: CommandContext): Promise<any> { |
||||
if (ctx.guildID !== this.gameWebhook.state?.guild_id) { |
||||
return ctx.send({ |
||||
content: "Sorry, you can't do that in this guild.", |
||||
ephemeral: true, |
||||
}) |
||||
} |
||||
if (ctx.channelID !== this.gameWebhook.state?.channel_id) { |
||||
return ctx.send({ |
||||
content: `Sorry, you can't do that here. You have to do it in <#${this.gameWebhook.state?.channel_id}>.`, |
||||
ephemeral: true, |
||||
}) |
||||
} |
||||
const count: number = ctx.options.count ?? 1 |
||||
const results: string[] = [] |
||||
for (let x = 0; x < count; x += 1) { |
||||
results.push(rand.weighted(["**Nicole**: D tier Predator Podcaster", |
||||
"**Herja**: C tier Viking Warrior", |
||||
"**Sharla**: B tier Skark Girl", |
||||
"**Melpomene**: A tier Muse of Tragedy", |
||||
"**Lady Bootstrap**: S tier Time Traveler"], [20, 15, 10, 5, 1])) |
||||
} |
||||
return ctx.send({ |
||||
content: `_${ctx.user.mention}_, you pulled...\n \\* ${results.join("\n \\* ")}`, |
||||
ephemeral: false, |
||||
}) |
||||
} |
||||
} |
@ -1,10 +0,0 @@ |
||||
extends ../template/template |
||||
|
||||
block title |
||||
| Setup Error |
||||
block content |
||||
| Something went wrong while #{context}. What did you do? |
||||
br |
||||
| The error was: #{error} |
||||
block link |
||||
a(href="setup") Return to Setup Page |
@ -0,0 +1,11 @@ |
||||
extends ../../template/template |
||||
|
||||
block title |
||||
| Game Running |
||||
block content |
||||
| The game is up and running. |
||||
br |
||||
| To make changes to its configuration setup, click Return to Setup. |
||||
block link |
||||
a(href=setupModeUrl, rel="nofollow") Return to Setup |
||||
a(href=shutdownUrl, rel="nofollow") Shut Down Server |
@ -0,0 +1,13 @@ |
||||
extends ../../template/template |
||||
|
||||
block title |
||||
| Game Stopping |
||||
block content |
||||
| You got it! The game is being stopped and returned to the setup phase now... |
||||
| If you aren't automatically redirected after a few seconds, click the button below. |
||||
script. |
||||
setTimeout(() => { |
||||
document.getElementById("stopButton").click() |
||||
}, 3000) |
||||
block link |
||||
a(href=stoppedUrl, id="stopButton", rel="nofollow") Verify Game Stopped |
@ -1,4 +1,4 @@ |
||||
extends ../template/template |
||||
extends ../../template/template |
||||
|
||||
block title |
||||
| Admin Setup |
@ -0,0 +1,14 @@ |
||||
extends ../../template/template |
||||
|
||||
block title |
||||
| Setup Complete! |
||||
block content |
||||
| You're all done! Time to just play the game. |
||||
br |
||||
| If you'd like to reconfigure the game's channels, click one of the buttons below. |
||||
block link |
||||
a(href=gameModeUrl, rel="nofollow") Start the Game! |
||||
a(href=gameSetupUrl, rel="nofollow") Reconfigure Game Channel |
||||
a(href=adminSetupUrl, rel="nofollow") Reconfigure Admin Channel |
||||
a(href=clearUrl, rel="nofollow") Clear All Channels |
||||
a(href=shutdownUrl, rel="nofollow") Shut Down Server |
@ -0,0 +1,13 @@ |
||||
extends ../../template/template |
||||
|
||||
block title |
||||
| Setup Error |
||||
block content |
||||
| Something went wrong while #{context}. What did you do? |
||||
br |
||||
if errorUrl |
||||
| The error was: #[a(href=errorUrl) #{error}] |
||||
else |
||||
| The error was: #{error} |
||||
block link |
||||
a(href=buttonUrl, rel="nofollow") #{buttonText} |
@ -0,0 +1,13 @@ |
||||
extends ../../template/template |
||||
|
||||
block title |
||||
| Game Starting |
||||
block content |
||||
| You got it! The game is being started up now... |
||||
| If you aren't automatically redirected after a few seconds, click the button below. |
||||
script. |
||||
setTimeout(() => { |
||||
document.getElementById("startButton").click() |
||||
}, 3000) |
||||
block link |
||||
a(href=startedUrl, id="startButton", rel="nofollow") Verify Game Started |
@ -1,12 +0,0 @@ |
||||
extends ../template/template |
||||
|
||||
block title |
||||
| Setup Complete! |
||||
block content |
||||
| You're all done! Time to just play the game. |
||||
br |
||||
| If you'd like to reconfigure the game's channels, click one of the buttons below. |
||||
block link |
||||
a(href=gameSetupUrl) Reconfigure Game Channel |
||||
a(href=adminSetupUrl) Reconfigure Admin Channel |
||||
a(href=clearUrl) Clear All Channels |
@ -0,0 +1,6 @@ |
||||
extends ../template/template |
||||
|
||||
block title |
||||
| Server Shutdown |
||||
block content |
||||
| You got it. The server has been shut down. Good night... |
Loading…
Reference in new issue