things+stuff
This commit is contained in:
@@ -1,184 +0,0 @@
|
||||
import * as msg from "../utils/messages.js";
|
||||
import * as security from "../utils/security.js";
|
||||
import { CreatePlayerState } from "./createPlayer.js";
|
||||
import { JustLoggedInState } from "./justLoggedIn.js";
|
||||
import { Session } from "../models/session.js";
|
||||
import { Config } from "../config.js";
|
||||
|
||||
const STATE_EXPECT_USERNAME = "promptUsername";
|
||||
const STATE_EXPECT_PASSWORD = "promptPassword";
|
||||
const USERNAME_PROMPT = [
|
||||
"Please enter your _username_:",
|
||||
"((type *:create* if you want to create a new user))",
|
||||
];
|
||||
const PASSWORD_PROMPT = "Please enter your password";
|
||||
const ERROR_INSANE_PASSWORD = "Invalid password.";
|
||||
const ERROR_INSANE_USERNAME = "Username invalid, must be at 4-20 characters, and may only contain [a-z], [A-Z], [0-9] and underscore"
|
||||
const ERROR_INCORRECT_PASSWOD = "Incorrect password.";
|
||||
|
||||
/** @property {Session} session */
|
||||
export class AuthState {
|
||||
|
||||
subState = STATE_EXPECT_USERNAME;
|
||||
|
||||
/**
|
||||
* @param {Session} session
|
||||
*/
|
||||
constructor(session) {
|
||||
/** @type {Session} */
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
onAttach() {
|
||||
this.session.sendFigletMessage("M U U H D");
|
||||
|
||||
this.session.sendPrompt("username", USERNAME_PROMPT);
|
||||
}
|
||||
|
||||
/** @param {msg.ClientMessage} message */
|
||||
onMessage(message) {
|
||||
if (this.subState === STATE_EXPECT_USERNAME) {
|
||||
this.receiveUsername(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.subState === STATE_EXPECT_PASSWORD) {
|
||||
this.receivePassword(message);
|
||||
return;
|
||||
}
|
||||
|
||||
console.error("Logic error, we received a message after we should have been logged in");
|
||||
this.session.sendError("I received a message didn't know what to do with!");
|
||||
}
|
||||
|
||||
/** @param {msg.ClientMessage} message */
|
||||
receiveUsername(message) {
|
||||
|
||||
//
|
||||
// handle invalid message types
|
||||
if (!message.isUsernameResponse()) {
|
||||
console.debug("what?!", message);
|
||||
this.session.sendError("Incorrect message type!");
|
||||
this.session.sendPrompt("username", USERNAME_PROMPT);
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Handle the creation of new players
|
||||
if (message.username === ":create") {
|
||||
// TODO:
|
||||
// Set gamestate = CreateNewPlayer
|
||||
//
|
||||
// Also check if player creation is allowed in config/env
|
||||
this.session.setState(new CreatePlayerState(this.session));
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// do basic syntax checks on usernames
|
||||
if (!security.isUsernameSane(message.username)) {
|
||||
this.session.sendError(ERROR_INSANE_USERNAME);
|
||||
this.session.sendPrompt("username", USERNAME_PROMPT);
|
||||
return;
|
||||
}
|
||||
|
||||
this.player = this.session.game.getPlayer(message.username);
|
||||
|
||||
//
|
||||
// handle invalid username
|
||||
if (!this.player) {
|
||||
|
||||
//
|
||||
// This is a security risk. In the perfect world we would allow the player to enter both
|
||||
// username and password before kicking them out, but since the player's username is not
|
||||
// an email address, and we discourage from using “important” usernames, then we tell the
|
||||
// player that they entered an invalid username right away.
|
||||
//
|
||||
// NOTE FOR ACCOUNT CREATION
|
||||
// Do adult-word checks, so we dont have Fucky_McFuckFace
|
||||
// https://www.npmjs.com/package/glin-profanity
|
||||
this.session.sendError("Incorrect username, try again");
|
||||
this.session.sendPrompt("username", USERNAME_PROMPT);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// username was correct, proceed to next step
|
||||
this.subState = STATE_EXPECT_PASSWORD;
|
||||
this.session.sendSystemMessage("salt", this.player.salt);
|
||||
this.session.sendPrompt("password", PASSWORD_PROMPT);
|
||||
}
|
||||
|
||||
/** @param {msg.ClientMessage} message */
|
||||
receivePassword(message) {
|
||||
|
||||
//
|
||||
// handle invalid message types
|
||||
if (!message.isPasswordResponse()) {
|
||||
this.session.sendError("Incorrect message type!");
|
||||
this.session.sendPrompt("password", PASSWORD_PROMPT);
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Check of the password is sane. This is both bad from a security point
|
||||
// of view, and technically not necessary as insane passwords couldn't
|
||||
// reside in the player lists. However, let's save some CPU cycles on
|
||||
// not hashing an insane password 1000+ times.
|
||||
// This is technically bad practice, but since this is just a game,
|
||||
// do it anyway.
|
||||
if (!security.isPasswordSane(message.password)) {
|
||||
this.session.sendError(ERROR_INSANE_PASSWORD);
|
||||
this.session.sendPrompt("password", PASSWORD_PROMPT);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Block users who enter bad passwords too many times.
|
||||
if (this.player.failedPasswordsSinceLastLogin > Config.maxFailedLogins) {
|
||||
this.blockedUntil = new Date() + Config.maxFailedLogins,
|
||||
this.session.sendCalamity("You have been locked out for too many failed password attempts, come back later");
|
||||
this.session.close();
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Handle blocked users.
|
||||
// They don't even get to have their password verified.
|
||||
if (this.player.blockedUntil > (new Date())) {
|
||||
this.session.sendCalamity("You have been locked out for too many failed password attempts, come back later");
|
||||
this.session.close();
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Verify the password against the hash we've stored.
|
||||
if (!security.verifyPassword(message.password, this.player.passwordHash)) {
|
||||
this.session.sendError("Incorrect password!");
|
||||
this.session.sendPrompt("password", PASSWORD_PROMPT);
|
||||
this.player.failedPasswordsSinceLastLogin++;
|
||||
|
||||
this.session.sendDebug(`Failed login attempt #${this.player.failedPasswordsSinceLastLogin}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
this.player.lastSucessfulLoginAt = new Date();
|
||||
this.player.failedPasswordsSinceLastLogin = 0;
|
||||
|
||||
this.session.player = this.player;
|
||||
//
|
||||
// Password correct, check if player is an admin
|
||||
if (this.player.isAdmin) {
|
||||
// set state AdminJustLoggedIn
|
||||
}
|
||||
|
||||
//
|
||||
// Password was correct, go to main game
|
||||
this.session.setState(new JustLoggedInState(this.session));
|
||||
}
|
||||
}
|
||||
185
server/states/authState.js
Executable file
185
server/states/authState.js
Executable file
@@ -0,0 +1,185 @@
|
||||
import * as msg from "../utils/messages.js";
|
||||
import * as security from "../utils/security.js";
|
||||
import { PlayerCreationState } from "./playerCreationState.js";
|
||||
import { JustLoggedInState } from "./justLoggedIn.js";
|
||||
import { Session } from "../models/session.js";
|
||||
import { Config } from "../config.js";
|
||||
|
||||
const STATE_EXPECT_USERNAME = "promptUsername";
|
||||
const STATE_EXPECT_PASSWORD = "promptPassword";
|
||||
const USERNAME_PROMPT = [
|
||||
"Please enter your _username_:",
|
||||
"((type *:create* if you want to create a new user))",
|
||||
];
|
||||
const PASSWORD_PROMPT = "Please enter your password";
|
||||
const ERROR_INSANE_PASSWORD = "Invalid password.";
|
||||
const ERROR_INSANE_USERNAME =
|
||||
"Username invalid, must be at 4-20 characters, and may only contain [a-z], [A-Z], [0-9] and underscore";
|
||||
const ERROR_INCORRECT_PASSWOD = "Incorrect password.";
|
||||
|
||||
/** @property {Session} session */
|
||||
export class AuthState {
|
||||
subState = STATE_EXPECT_USERNAME;
|
||||
|
||||
/**
|
||||
* @param {Session} session
|
||||
*/
|
||||
constructor(session) {
|
||||
/** @type {Session} */
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
onAttach() {
|
||||
this.session.sendFigletMessage("M U U H D");
|
||||
|
||||
this.session.sendPrompt("username", USERNAME_PROMPT);
|
||||
}
|
||||
|
||||
/** @param {msg.ClientMessage} message */
|
||||
onMessage(message) {
|
||||
if (this.subState === STATE_EXPECT_USERNAME) {
|
||||
this.receiveUsername(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.subState === STATE_EXPECT_PASSWORD) {
|
||||
this.receivePassword(message);
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(
|
||||
"Logic error, we received a message after we should have been logged in",
|
||||
);
|
||||
this.session.sendError("I received a message didn't know what to do with!");
|
||||
}
|
||||
|
||||
/** @param {msg.ClientMessage} message */
|
||||
receiveUsername(message) {
|
||||
//
|
||||
// handle invalid message types
|
||||
if (!message.isUsernameResponse()) {
|
||||
console.debug("what?!", message);
|
||||
this.session.sendError("Incorrect message type!");
|
||||
this.session.sendPrompt("username", USERNAME_PROMPT);
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Handle the creation of new players
|
||||
if (message.username === ":create") {
|
||||
// TODO:
|
||||
// Set gamestate = CreateNewPlayer
|
||||
//
|
||||
// Also check if player creation is allowed in config/env
|
||||
this.session.setState(new PlayerCreationState(this.session));
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// do basic syntax checks on usernames
|
||||
if (!security.isUsernameSane(message.username)) {
|
||||
this.session.sendError(ERROR_INSANE_USERNAME);
|
||||
this.session.sendPrompt("username", USERNAME_PROMPT);
|
||||
return;
|
||||
}
|
||||
|
||||
this.player = this.session.game.getPlayer(message.username);
|
||||
|
||||
//
|
||||
// handle invalid username
|
||||
if (!this.player) {
|
||||
//
|
||||
// This is a security risk. In the perfect world we would allow the player to enter both
|
||||
// username and password before kicking them out, but since the player's username is not
|
||||
// an email address, and we discourage from using “important” usernames, then we tell the
|
||||
// player that they entered an invalid username right away.
|
||||
//
|
||||
// NOTE FOR ACCOUNT CREATION
|
||||
// Do adult-word checks, so we dont have Fucky_McFuckFace
|
||||
// https://www.npmjs.com/package/glin-profanity
|
||||
this.session.sendError("Incorrect username, try again");
|
||||
this.session.sendPrompt("username", USERNAME_PROMPT);
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// username was correct, proceed to next step
|
||||
this.subState = STATE_EXPECT_PASSWORD;
|
||||
this.session.sendSystemMessage("salt", this.player.salt);
|
||||
this.session.sendPrompt("password", PASSWORD_PROMPT);
|
||||
}
|
||||
|
||||
/** @param {msg.ClientMessage} message */
|
||||
receivePassword(message) {
|
||||
//
|
||||
// handle invalid message types
|
||||
if (!message.isPasswordResponse()) {
|
||||
this.session.sendError("Incorrect message type!");
|
||||
this.session.sendPrompt("password", PASSWORD_PROMPT);
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Check of the password is sane. This is both bad from a security point
|
||||
// of view, and technically not necessary as insane passwords couldn't
|
||||
// reside in the player lists. However, let's save some CPU cycles on
|
||||
// not hashing an insane password 1000+ times.
|
||||
// This is technically bad practice, but since this is just a game,
|
||||
// do it anyway.
|
||||
if (!security.isPasswordSane(message.password)) {
|
||||
this.session.sendError(ERROR_INSANE_PASSWORD);
|
||||
this.session.sendPrompt("password", PASSWORD_PROMPT);
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Block users who enter bad passwords too many times.
|
||||
if (this.player.failedPasswordsSinceLastLogin > Config.maxFailedLogins) {
|
||||
((this.blockedUntil = new Date() + Config.maxFailedLogins),
|
||||
this.session.sendCalamity(
|
||||
"You have been locked out for too many failed password attempts, come back later",
|
||||
));
|
||||
this.session.close();
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Handle blocked users.
|
||||
// They don't even get to have their password verified.
|
||||
if (this.player.blockedUntil > new Date()) {
|
||||
this.session.sendCalamity(
|
||||
"You have been locked out for too many failed password attempts, come back later",
|
||||
);
|
||||
this.session.close();
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Verify the password against the hash we've stored.
|
||||
if (!security.verifyPassword(message.password, this.player.passwordHash)) {
|
||||
this.session.sendError("Incorrect password!");
|
||||
this.session.sendPrompt("password", PASSWORD_PROMPT);
|
||||
this.player.failedPasswordsSinceLastLogin++;
|
||||
|
||||
this.session.sendDebug(
|
||||
`Failed login attempt #${this.player.failedPasswordsSinceLastLogin}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.player.lastSucessfulLoginAt = new Date();
|
||||
this.player.failedPasswordsSinceLastLogin = 0;
|
||||
|
||||
this.session.player = this.player;
|
||||
//
|
||||
// Password correct, check if player is an admin
|
||||
if (this.player.isAdmin) {
|
||||
// set state AdminJustLoggedIn
|
||||
}
|
||||
|
||||
//
|
||||
// Password was correct, go to main game
|
||||
this.session.setState(new JustLoggedInState(this.session));
|
||||
}
|
||||
}
|
||||
@@ -7,44 +7,44 @@ import { Session } from "../models/session.js";
|
||||
* It's here we listen for player commands.
|
||||
*/
|
||||
export class AwaitCommandsState {
|
||||
/**
|
||||
* @param {Session} session
|
||||
*/
|
||||
constructor(session) {
|
||||
/** @type {Session} */
|
||||
this.session = session;
|
||||
}
|
||||
/**
|
||||
* @param {Session} session
|
||||
*/
|
||||
constructor(session) {
|
||||
/** @type {Session} */
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
onAttach() {
|
||||
console.log("Session is entering the “main” state");
|
||||
this.session.sendMessage("Welcome to the game!");
|
||||
}
|
||||
onAttach() {
|
||||
console.log("Session is entering the “main” state");
|
||||
this.session.sendMessage("Welcome to the game!");
|
||||
}
|
||||
|
||||
/** @param {msg.ClientMessage} message */
|
||||
onMessage(message) {
|
||||
if (message.hasCommand()) {
|
||||
this.handleCommand(message);
|
||||
}
|
||||
/** @param {msg.ClientMessage} message */
|
||||
onMessage(message) {
|
||||
if (message.hasCommand()) {
|
||||
this.handleCommand(message);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {msg.ClientMessage} message */
|
||||
handleCommand(message) {
|
||||
switch (message.command) {
|
||||
case "help":
|
||||
this.session.sendFigletMessage("HELP");
|
||||
this.session.sendMessage([
|
||||
"---------------------------------------",
|
||||
" *:help* this help screen",
|
||||
" *:quit* quit the game",
|
||||
"---------------------------------------",
|
||||
]);
|
||||
break;
|
||||
case "quit":
|
||||
this.session.sendMessage("The quitting quitter quits, typical... Cya");
|
||||
this.session._websocket.close();
|
||||
break;
|
||||
default:
|
||||
this.session.sendMessage(`Unknown command: ${message.command}`);
|
||||
}
|
||||
/** @param {msg.ClientMessage} message */
|
||||
handleCommand(message) {
|
||||
switch (message.command) {
|
||||
case "help":
|
||||
this.session.sendFigletMessage("HELP");
|
||||
this.session.sendMessage([
|
||||
"---------------------------------------",
|
||||
" *:help* this help screen",
|
||||
" *:quit* quit the game",
|
||||
"---------------------------------------",
|
||||
]);
|
||||
break;
|
||||
case "quit":
|
||||
this.session.sendMessage("The quitting quitter quits, typical... Cya");
|
||||
this.session._websocket.close();
|
||||
break;
|
||||
default:
|
||||
this.session.sendMessage(`Unknown command: ${message.command}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import figlet from "figlet";
|
||||
import { Session } from "../models/session.js";
|
||||
import { ClientMessage } from "../utils/messages.js";
|
||||
import { frameText } from "../utils/tui.js";
|
||||
import { Config } from "../config.js";
|
||||
|
||||
export class CharacterCreationState {
|
||||
|
||||
/**
|
||||
* @proteted
|
||||
* @type {(msg: ClientMessage) => }
|
||||
*
|
||||
* NOTE: Should this be a stack?
|
||||
*/
|
||||
_dynamicMessageHandler;
|
||||
|
||||
/**
|
||||
* @param {Session} session
|
||||
*/
|
||||
constructor(session) {
|
||||
/** @type {Session} */
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
/**
|
||||
* We attach (and execute) the next state
|
||||
*/
|
||||
onAttach() {
|
||||
const charCount = this.session.player.characters.size;
|
||||
|
||||
//NOTE: could use async to optimize performance
|
||||
const createPartyLogo = frameText(
|
||||
figlet.textSync("Create Your Party"),
|
||||
{ vPadding: 0, frameChars: "§=§§§§§§" },
|
||||
);
|
||||
|
||||
this.session.sendMessage(createPartyLogo, { preformatted: true });
|
||||
|
||||
this.session.sendMessage([
|
||||
"",
|
||||
`Current party size: ${charCount}`,
|
||||
`Max party size: ${Config.maxPartySize}`,
|
||||
]);
|
||||
|
||||
const min = 1;
|
||||
const max = Config.maxPartySize - charCount;
|
||||
const prompt = [
|
||||
`Please enter an integer between ${min} - ${max}`,
|
||||
"((type *:help* to get more info about party size))",
|
||||
];
|
||||
|
||||
this.session.sendMessage(`You can create a party with ${min} - ${max} characters, how big should your party be?`);
|
||||
|
||||
this.session.sendPrompt("integer", prompt);
|
||||
|
||||
/** @param {ClientMessage} message */
|
||||
this._dynamicMessageHandler = (message) => {
|
||||
if (message.isHelpCommand()) {
|
||||
const mps = Config.maxPartySize; // short var name for easy doctype writing.
|
||||
this.session.sendMessage([
|
||||
`Your party can consist of 1 to ${mps} characters.`,
|
||||
"",
|
||||
"* Large parties tend live longer",
|
||||
`* If you have fewer than ${mps} characters, you can`,
|
||||
" hire extra characters in your local inn.",
|
||||
"* large parties level slower because there are more",
|
||||
" characters to share the Experience Points",
|
||||
"* The individual members of small parties get better",
|
||||
" loot because they don't have to share, but it",
|
||||
" a lot of skill to accumulate loot as fast a larger",
|
||||
" party can"
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!message.isIntegerResponse()) {
|
||||
this.session.sendError("You didn't enter a number");
|
||||
this.session.sendPrompt("integer", prompt);
|
||||
return;
|
||||
}
|
||||
|
||||
const numCharactersToCreate = message.integer;
|
||||
|
||||
if (numCharactersToCreate > max) {
|
||||
this.session.sendError("Number too high");
|
||||
this.session.sendPrompt("integer", prompt);
|
||||
return;
|
||||
}
|
||||
|
||||
if (numCharactersToCreate < min) {
|
||||
this.session.sendError("Number too low");
|
||||
this.session.sendPrompt("integer", prompt);
|
||||
return;
|
||||
}
|
||||
|
||||
this.session.sendMessage(`Let's create ${numCharactersToCreate} character(s) for you :)`);
|
||||
this._dynamicMessageHandler = undefined;
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {ClientMessage} message */
|
||||
onMessage(message) {
|
||||
if (this._dynamicMessageHandler) {
|
||||
this._dynamicMessageHandler(message);
|
||||
return;
|
||||
}
|
||||
this.session.sendMessage("pong", message.type);
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
import { Session } from "../models/session.js";
|
||||
import * as msg from "../utils/messages.js";
|
||||
import * as security from "../utils/security.js";
|
||||
import { Player } from "../models/player.js";
|
||||
import { AuthState } from "./Auth.js";
|
||||
import { Config } from "../config.js";
|
||||
|
||||
const USERNAME_PROMPT = "Enter a valid username (4-20 characters, [a-z], [A-Z], [0-9], and underscore)";
|
||||
const PASSWORD_PROMPT = "Enter a valid password";
|
||||
const PASSWORD_PROMPT2 = "Enter your password again";
|
||||
const ERROR_INSANE_PASSWORD = "Invalid password.";
|
||||
const ERROR_INSANE_USERNAME = "Invalid username. It must be 4-20 characters, and may only contain [a-z], [A-Z], [0-9] and underscore"
|
||||
const ERROR_INCORRECT_PASSWOD = "Incorrect password.";
|
||||
|
||||
/** @property {Session} session */
|
||||
export class CreatePlayerState {
|
||||
|
||||
/**
|
||||
* @proteted
|
||||
* @type {(msg: ClientMessage) => }
|
||||
*
|
||||
* Allows us to dynamically set which
|
||||
* method handles incoming messages.
|
||||
*/
|
||||
_dynamicMessageHandler;
|
||||
|
||||
/** @protected @type {Player} */
|
||||
_player;
|
||||
/** @protected @type {string} */
|
||||
_password;
|
||||
|
||||
/**
|
||||
* @param {Session} session
|
||||
*/
|
||||
constructor(session) {
|
||||
/** @type {Session} */
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
onAttach() {
|
||||
//
|
||||
// If there are too many players, stop allowing new players in.
|
||||
if (this.session.game._players.size >= Config.maxPlayers) {
|
||||
this.session.sendCalamity("Server is full, no more players can be created");
|
||||
this.session.close();
|
||||
}
|
||||
|
||||
this.session.sendFigletMessage("New Player");
|
||||
this.session.sendPrompt("username", USERNAME_PROMPT);
|
||||
|
||||
// our initial substate is to receive a username
|
||||
this.setMessageHandler(this.receiveUsername);
|
||||
}
|
||||
|
||||
/** @param {msg.ClientMessage} message */
|
||||
onMessage(message) {
|
||||
this._dynamicMessageHandler(message);
|
||||
}
|
||||
|
||||
/* @param {(msg: ClientMessage) => } handler */
|
||||
setMessageHandler(handler) {
|
||||
this._dynamicMessageHandler = handler;
|
||||
}
|
||||
|
||||
/** @param {msg.ClientMessage} message */
|
||||
receiveUsername(message) {
|
||||
|
||||
//
|
||||
// NOTE FOR ACCOUNT CREATION
|
||||
// Do adult-word checks, so we dont have Fucky_McFuckFace
|
||||
// https://www.npmjs.com/package/glin-profanity
|
||||
|
||||
//
|
||||
// handle invalid message types
|
||||
if (!message.isUsernameResponse()) {
|
||||
this.session.sendError("Incorrect message type!");
|
||||
this.session.sendPrompt("username", USERNAME_PROMPT);
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// do basic syntax checks on usernames
|
||||
if (!security.isUsernameSane(message.username)) {
|
||||
this.session.sendError(ERROR_INSANE_USERNAME);
|
||||
this.session.sendPrompt("username", USERNAME_PROMPT);
|
||||
return;
|
||||
}
|
||||
|
||||
const player = this.session.game.createPlayer(message.username);
|
||||
|
||||
//
|
||||
// handle taken/occupied username
|
||||
if (player === false) {
|
||||
// Telling the user right away that the username is taken can
|
||||
// lead to data leeching. But fukkit.
|
||||
|
||||
this.session.sendError(`Username _${message.username}_ was taken by another player.`);
|
||||
this.session.sendPrompt("username", USERNAME_PROMPT);
|
||||
return;
|
||||
}
|
||||
|
||||
this._player = player;
|
||||
|
||||
this.session.sendSystemMessage("salt", player.salt);
|
||||
this.session.sendMessage(`Username _*${message.username}*_ is available, and I've reserved it for you :)`);
|
||||
this.session.sendPrompt("password", PASSWORD_PROMPT);
|
||||
this.setMessageHandler(this.receivePassword);
|
||||
}
|
||||
|
||||
/** @param {msg.ClientMessage} message */
|
||||
receivePassword(message) {
|
||||
|
||||
//
|
||||
// handle invalid message types
|
||||
if (!message.isPasswordResponse()) {
|
||||
console.log("Invalid message type, expected password reply", message);
|
||||
this.session.sendError("Incorrect message type!");
|
||||
this.session.sendPrompt("password", PASSWORD_PROMPT);
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Check that it's been hashed thoroughly before being sent here.
|
||||
if (!security.isPasswordSane(message.password)) {
|
||||
this.session.sendError(ERROR_INSANE_PASSWORD);
|
||||
this.session.sendPrompt("password", PASSWORD_PROMPT);
|
||||
return;
|
||||
}
|
||||
|
||||
this._password = message.password; // it's relatively safe to store the PW here temporarily. The client already hashed the hell out of it.
|
||||
this.session.sendPrompt("password", PASSWORD_PROMPT2);
|
||||
|
||||
this.setMessageHandler(this.receivePasswordConfirmation);
|
||||
}
|
||||
|
||||
/** @param {msg.ClientMessage} memssage */
|
||||
receivePasswordConfirmation(message) {
|
||||
|
||||
//
|
||||
// handle invalid message types
|
||||
if (!message.isPasswordResponse()) {
|
||||
console.log("Invalid message type, expected password reply", message);
|
||||
this.session.sendError("Incorrect message type!");
|
||||
this.session.sendPrompt("password", PASSWORD_PROMPT);
|
||||
this.setMessageHandler(this.receivePassword);
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Handle mismatching passwords
|
||||
if (message.password !== this._password) {
|
||||
this.session.sendError("Incorrect, you have to enter your password twice in a row successfully");
|
||||
this.session.sendPrompt("password", PASSWORD_PROMPT);
|
||||
this.setMessageHandler(this.receivePassword);
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Success!
|
||||
// Take the user to the login screen.
|
||||
this.session.sendMessage("*_Success_* ✅ You will now be asked to log in again, sorry for that ;)");
|
||||
this._player.setPasswordHash(security.generateHash(this._password));
|
||||
this.session.setState(new AuthState(this.session));
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,11 @@ import { Session } from "../models/session.js";
|
||||
|
||||
/** @interface */
|
||||
export class StateInterface {
|
||||
/** @param {Session} session */
|
||||
constructor(session) { }
|
||||
/** @param {Session} session */
|
||||
constructor(session) {}
|
||||
|
||||
onAttach() { }
|
||||
onAttach() {}
|
||||
|
||||
/** @param {ClientMessage} message */
|
||||
onMessage(message) {}
|
||||
/** @param {ClientMessage} message */
|
||||
onMessage(message) {}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
import { Session } from "../models/session.js";
|
||||
import { CharacterCreationState } from "./characterCreation.js";
|
||||
import { PartyCreationState } from "./partyCreationState.js";
|
||||
import { AwaitCommandsState } from "./awaitCommands.js";
|
||||
|
||||
/** @interface */
|
||||
export class JustLoggedInState {
|
||||
/** @param {Session} session */
|
||||
constructor(session) {
|
||||
/** @type {Session} */
|
||||
this.session = session;
|
||||
/** @param {Session} session */
|
||||
constructor(session) {
|
||||
/** @type {Session} */
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
// Show welcome screen
|
||||
onAttach() {
|
||||
this.session.sendMessage([
|
||||
"",
|
||||
"Welcome",
|
||||
"",
|
||||
"You can type “:quit” at any time to quit the game",
|
||||
"",
|
||||
]);
|
||||
|
||||
//
|
||||
// Check if we need to create characters for the player
|
||||
if (this.session.player.characters.size === 0) {
|
||||
this.session.sendMessage(
|
||||
"You haven't got any characters, so let's make some\n\n",
|
||||
);
|
||||
this.session.setState(new PartyCreationState(this.session));
|
||||
return;
|
||||
}
|
||||
|
||||
// Show welcome screen
|
||||
onAttach() {
|
||||
this.session.sendMessage([
|
||||
"",
|
||||
"Welcome",
|
||||
"",
|
||||
"You can type “:quit” at any time to quit the game",
|
||||
"",
|
||||
]);
|
||||
|
||||
//
|
||||
// Check if we need to create characters for the player
|
||||
if (this.session.player.characters.size === 0) {
|
||||
this.session.sendMessage("You haven't got any characters, so let's make some\n\n");
|
||||
this.session.setState(new CharacterCreationState(this.session));
|
||||
return;
|
||||
}
|
||||
|
||||
this.session.setState(new AwaitCommandsState(this.session));
|
||||
}
|
||||
this.session.setState(new AwaitCommandsState(this.session));
|
||||
}
|
||||
}
|
||||
|
||||
108
server/states/partyCreationState.js
Executable file
108
server/states/partyCreationState.js
Executable file
@@ -0,0 +1,108 @@
|
||||
import figlet from "figlet";
|
||||
import { Session } from "../models/session.js";
|
||||
import { ClientMessage } from "../utils/messages.js";
|
||||
import { frameText } from "../utils/tui.js";
|
||||
import { Config } from "../config.js";
|
||||
|
||||
export class PartyCreationState {
|
||||
/**
|
||||
* @proteted
|
||||
* @type {(msg: ClientMessage) => }
|
||||
*
|
||||
* NOTE: Should this be a stack?
|
||||
*/
|
||||
_dynamicMessageHandler;
|
||||
|
||||
/** @param {Session} session */
|
||||
constructor(session) {
|
||||
/** @type {Session} */
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
/** We attach (and execute) the next state */
|
||||
onAttach() {
|
||||
const charCount = this.session.player.characters.size;
|
||||
|
||||
//NOTE: could use async to optimize performance
|
||||
const createPartyLogo = frameText(figlet.textSync("Create Your Party"), {
|
||||
vPadding: 0,
|
||||
frameChars: "§=§§§§§§",
|
||||
});
|
||||
|
||||
this.session.sendMessage(createPartyLogo, { preformatted: true });
|
||||
|
||||
this.session.sendMessage([
|
||||
"",
|
||||
`Current party size: ${charCount}`,
|
||||
`Max party size: ${Config.maxPartySize}`,
|
||||
]);
|
||||
|
||||
const min = 1;
|
||||
const max = Config.maxPartySize - charCount;
|
||||
const prompt = [
|
||||
`Please enter an integer between ${min} - ${max}`,
|
||||
"((type *:help* to get more info about party size))",
|
||||
];
|
||||
|
||||
this.session.sendMessage(
|
||||
`You can create a party with ${min} - ${max} characters, how big should your party be?`,
|
||||
);
|
||||
|
||||
this.session.sendPrompt("integer", prompt);
|
||||
|
||||
/** @param {ClientMessage} message */
|
||||
this._dynamicMessageHandler = (message) => {
|
||||
if (message.isHelpCommand()) {
|
||||
const mps = Config.maxPartySize; // short var name for easy doctype writing.
|
||||
this.session.sendMessage([
|
||||
`Your party can consist of 1 to ${mps} characters.`,
|
||||
"",
|
||||
"* Large parties tend live longer",
|
||||
`* If you have fewer than ${mps} characters, you can`,
|
||||
" hire extra characters in your local inn.",
|
||||
"* large parties level slower because there are more",
|
||||
" characters to share the Experience Points",
|
||||
"* The individual members of small parties get better",
|
||||
" loot because they don't have to share, but it",
|
||||
" a lot of skill to accumulate loot as fast a larger",
|
||||
" party can",
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!message.isIntegerResponse()) {
|
||||
this.session.sendError("You didn't enter a number");
|
||||
this.session.sendPrompt("integer", prompt);
|
||||
return;
|
||||
}
|
||||
|
||||
const numCharactersToCreate = message.integer;
|
||||
|
||||
if (numCharactersToCreate > max) {
|
||||
this.session.sendError("Number too high");
|
||||
this.session.sendPrompt("integer", prompt);
|
||||
return;
|
||||
}
|
||||
|
||||
if (numCharactersToCreate < min) {
|
||||
this.session.sendError("Number too low");
|
||||
this.session.sendPrompt("integer", prompt);
|
||||
return;
|
||||
}
|
||||
|
||||
this.session.sendMessage(
|
||||
`Let's create ${numCharactersToCreate} character(s) for you :)`,
|
||||
);
|
||||
this._dynamicMessageHandler = undefined;
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {ClientMessage} message */
|
||||
onMessage(message) {
|
||||
if (this._dynamicMessageHandler) {
|
||||
this._dynamicMessageHandler(message);
|
||||
return;
|
||||
}
|
||||
this.session.sendMessage("pong", message.type);
|
||||
}
|
||||
}
|
||||
173
server/states/playerCreationState.js
Executable file
173
server/states/playerCreationState.js
Executable file
@@ -0,0 +1,173 @@
|
||||
import { Session } from "../models/session.js";
|
||||
import * as msg from "../utils/messages.js";
|
||||
import * as security from "../utils/security.js";
|
||||
import { Player } from "../models/player.js";
|
||||
import { AuthState } from "./authState.js";
|
||||
import { Config } from "../config.js";
|
||||
|
||||
const USERNAME_PROMPT =
|
||||
"Enter a valid username (4-20 characters, [a-z], [A-Z], [0-9], and underscore)";
|
||||
const PASSWORD_PROMPT = "Enter a valid password";
|
||||
const PASSWORD_PROMPT2 = "Enter your password again";
|
||||
const ERROR_INSANE_PASSWORD = "Invalid password.";
|
||||
const ERROR_INSANE_USERNAME =
|
||||
"Invalid username. It must be 4-20 characters, and may only contain [a-z], [A-Z], [0-9] and underscore";
|
||||
const ERROR_INCORRECT_PASSWOD = "Incorrect password.";
|
||||
|
||||
/** @property {Session} session */
|
||||
export class PlayerCreationState {
|
||||
/**
|
||||
* @proteted
|
||||
* @type {(msg: ClientMessage) => }
|
||||
*
|
||||
* Allows us to dynamically set which
|
||||
* method handles incoming messages.
|
||||
*/
|
||||
_dynamicMessageHandler;
|
||||
|
||||
/** @protected @type {Player} */
|
||||
_player;
|
||||
/** @protected @type {string} */
|
||||
_password;
|
||||
|
||||
/**
|
||||
* @param {Session} session
|
||||
*/
|
||||
constructor(session) {
|
||||
/** @type {Session} */
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
onAttach() {
|
||||
//
|
||||
// If there are too many players, stop allowing new players in.
|
||||
if (this.session.game._players.size >= Config.maxPlayers) {
|
||||
this.session.sendCalamity(
|
||||
"Server is full, no more players can be created",
|
||||
);
|
||||
this.session.close();
|
||||
}
|
||||
|
||||
this.session.sendFigletMessage("New Player");
|
||||
this.session.sendPrompt("username", USERNAME_PROMPT);
|
||||
|
||||
// our initial substate is to receive a username
|
||||
this.setMessageHandler(this.receiveUsername);
|
||||
}
|
||||
|
||||
/** @param {msg.ClientMessage} message */
|
||||
onMessage(message) {
|
||||
this._dynamicMessageHandler(message);
|
||||
}
|
||||
|
||||
/* @param {(msg: ClientMessage) => } handler */
|
||||
setMessageHandler(handler) {
|
||||
this._dynamicMessageHandler = handler;
|
||||
}
|
||||
|
||||
/** @param {msg.ClientMessage} message */
|
||||
receiveUsername(message) {
|
||||
//
|
||||
// NOTE FOR ACCOUNT CREATION
|
||||
// Do adult-word checks, so we dont have Fucky_McFuckFace
|
||||
// https://www.npmjs.com/package/glin-profanity
|
||||
|
||||
//
|
||||
// handle invalid message types
|
||||
if (!message.isUsernameResponse()) {
|
||||
this.session.sendError("Incorrect message type!");
|
||||
this.session.sendPrompt("username", USERNAME_PROMPT);
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// do basic syntax checks on usernames
|
||||
if (!security.isUsernameSane(message.username)) {
|
||||
this.session.sendError(ERROR_INSANE_USERNAME);
|
||||
this.session.sendPrompt("username", USERNAME_PROMPT);
|
||||
return;
|
||||
}
|
||||
|
||||
const player = this.session.game.createPlayer(message.username);
|
||||
|
||||
//
|
||||
// handle taken/occupied username
|
||||
if (player === false) {
|
||||
// Telling the user right away that the username is taken can
|
||||
// lead to data leeching. But fukkit.
|
||||
|
||||
this.session.sendError(
|
||||
`Username _${message.username}_ was taken by another player.`,
|
||||
);
|
||||
this.session.sendPrompt("username", USERNAME_PROMPT);
|
||||
return;
|
||||
}
|
||||
|
||||
this._player = player;
|
||||
|
||||
this.session.sendSystemMessage("salt", player.salt);
|
||||
this.session.sendMessage(
|
||||
`Username _*${message.username}*_ is available, and I've reserved it for you :)`,
|
||||
);
|
||||
this.session.sendPrompt("password", PASSWORD_PROMPT);
|
||||
this.setMessageHandler(this.receivePassword);
|
||||
}
|
||||
|
||||
/** @param {msg.ClientMessage} message */
|
||||
receivePassword(message) {
|
||||
//
|
||||
// handle invalid message types
|
||||
if (!message.isPasswordResponse()) {
|
||||
console.log("Invalid message type, expected password reply", message);
|
||||
this.session.sendError("Incorrect message type!");
|
||||
this.session.sendPrompt("password", PASSWORD_PROMPT);
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Check that it's been hashed thoroughly before being sent here.
|
||||
if (!security.isPasswordSane(message.password)) {
|
||||
this.session.sendError(ERROR_INSANE_PASSWORD);
|
||||
this.session.sendPrompt("password", PASSWORD_PROMPT);
|
||||
return;
|
||||
}
|
||||
|
||||
this._password = message.password; // it's relatively safe to store the PW here temporarily. The client already hashed the hell out of it.
|
||||
this.session.sendPrompt("password", PASSWORD_PROMPT2);
|
||||
|
||||
this.setMessageHandler(this.receivePasswordConfirmation);
|
||||
}
|
||||
|
||||
/** @param {msg.ClientMessage} memssage */
|
||||
receivePasswordConfirmation(message) {
|
||||
//
|
||||
// handle invalid message types
|
||||
if (!message.isPasswordResponse()) {
|
||||
console.log("Invalid message type, expected password reply", message);
|
||||
this.session.sendError("Incorrect message type!");
|
||||
this.session.sendPrompt("password", PASSWORD_PROMPT);
|
||||
this.setMessageHandler(this.receivePassword);
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Handle mismatching passwords
|
||||
if (message.password !== this._password) {
|
||||
this.session.sendError(
|
||||
"Incorrect, you have to enter your password twice in a row successfully",
|
||||
);
|
||||
this.session.sendPrompt("password", PASSWORD_PROMPT);
|
||||
this.setMessageHandler(this.receivePassword);
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Success!
|
||||
// Take the user to the login screen.
|
||||
this.session.sendMessage(
|
||||
"*_Success_* ✅ You will now be asked to log in again, sorry for that ;)",
|
||||
);
|
||||
this._player.setPasswordHash(security.generateHash(this._password));
|
||||
this.session.setState(new AuthState(this.session));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user