From ee32742ae8d938b38a81c38583ee13c09ad10c3c Mon Sep 17 00:00:00 2001 From: loplkc Date: Thu, 14 Jul 2022 12:14:00 -0400 Subject: [PATCH] Infrastructure refactoring (Part 1) + Fun recursive functions for handling consecutive requests to playerStorage and stage ~ Renamed and redefined a bunch of communication types ~ Renamed globalScene to scene0 (since scenes are no longer nested) > I need to finish this and work on adding actual things to the game... --- src/game/globalScene.ts | 7 -- src/game/scene0.ts | 6 ++ src/server/main.server.ts | 153 +++++++++++++++++++++++++----------- src/shared/PlayerManager.ts | 85 ++++++++++++++++---- src/shared/SceneManager.ts | 72 +++++++++++------ src/shared/Shared.ts | 29 ++++++- 6 files changed, 257 insertions(+), 95 deletions(-) delete mode 100644 src/game/globalScene.ts create mode 100644 src/game/scene0.ts diff --git a/src/game/globalScene.ts b/src/game/globalScene.ts deleted file mode 100644 index 51165c4..0000000 --- a/src/game/globalScene.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { sceneTemplate } from "shared/SceneManager" -export const globalSceneTemplate: sceneTemplate = { - sceneComplete() { - return false - }, - onCompletion: [] -} as const \ No newline at end of file diff --git a/src/game/scene0.ts b/src/game/scene0.ts new file mode 100644 index 0000000..54e9ba9 --- /dev/null +++ b/src/game/scene0.ts @@ -0,0 +1,6 @@ +import { sceneTemplate } from "shared/SceneManager"; +export const scene0Template: sceneTemplate = { + sceneComplete() { + return [false]; + }, +} as const; diff --git a/src/server/main.server.ts b/src/server/main.server.ts index eb635f1..c77613a 100644 --- a/src/server/main.server.ts +++ b/src/server/main.server.ts @@ -1,58 +1,119 @@ // "main": Initializes all state and handles the real world. +const VERBOSE = true; +if (!VERBOSE) { + function print() {} + function warn() {} +} const Players = game.GetService("Players"); // This should be the only place on the server where the Players service is mentioned const RunService = game.GetService("RunService"); -import { isUnknownTable } from "shared/Shared"; +import { isUnknownTable, makeApplyConsecutiveRequestsToObjectFunction } from "shared/Shared"; import { bindToClientMessage, messageClient, messageAllClients } from "./ServerMessenger"; // Please note: This should not use any of the properties of "scene" or "playerStorage" (it only needs to know that they exist) -import { scene, initScene, runScene, applyRequestsToScene } from "shared/SceneManager" -import { playerStorage, initPlayerStorage, applyRequestsToPlayerStorage, playerManagerRequest} from "shared/PlayerManager"; -import { globalSceneTemplate } from "game/globalScene" +import { scene, initScene, runScene, sceneRequest, stageRequest, processSceneExternalInput } from "shared/SceneManager"; +import { + playerStorage, + initPlayerStorage, + handleConsecutivePlayerActivities, + playerActivity, + messageToPlayer, +} from "shared/PlayerManager"; +import { scene0Template } from "game/scene0"; +interface stage { + [sceneName: string]: scene | undefined; +} // Initialize all state let globalPlayerStorage: playerStorage = initPlayerStorage(); -let globalScene: scene = initScene(globalSceneTemplate) -// Handle the real world -let playerEvents: playerManagerRequest[] = []; -function messagePlayerManager(message: playerManagerRequest): void { - playerEvents.push(message); +let stage: stage = {}; +stage["scene0"] = initScene(scene0Template); +// Handle the real world (TERA I/O) +let playerManagerMailbox: playerActivity[] = []; +function messagePlayerManager(message: playerActivity): void { + playerManagerMailbox.push(message); } -Players.PlayerAdded.Connect(function(player: Player) { - messagePlayerManager(["initPlayer", player]) +Players.PlayerAdded.Connect(function (player: Player) { + messagePlayerManager(["player_joined", player]); }); -Players.PlayerRemoving.Connect(function(player: Player) { - messagePlayerManager(["deinitPlayer", player]) +Players.PlayerRemoving.Connect(function (player: Player) { + messagePlayerManager(["player_left", player]); }); -bindToClientMessage(function(player: Player, ...messageContents: unknown[]) { - messagePlayerManager(["playerInput", player, messageContents]) -}); -// Run everything sequentially to avoid concurrency issues -let busy = false -RunService.Heartbeat.Connect(function(delta: number) { - assert(!busy) - busy = true - - const now = os.clock() - const sceneResult = runScene(globalScene, now) - assert(sceneResult[0]); - globalScene = sceneResult[1][0] - - let thesePlayerEvents = playerEvents - playerEvents = [] - thesePlayerEvents.unshift(...sceneResult[1][1]) - - let repetitions = 0 - while (thesePlayerEvents[0]) { - const playerRequestsResult = applyRequestsToPlayerStorage(globalPlayerStorage, thesePlayerEvents) - assert(playerRequestsResult[0], playerRequestsResult[1] as string) // + The other actor should probably cleanup instead of just crashing - const sceneRequestsResult = applyRequestsToScene(globalScene,now,playerRequestsResult[1][1]); - assert(sceneRequestsResult[0], sceneRequestsResult[1] as string) - // Mutable section - globalPlayerStorage = playerRequestsResult[1][0]; - globalScene = sceneRequestsResult[1][0]; - thesePlayerEvents = sceneRequestsResult[1][1] - repetitions += 1 - assert(repetitions > 4)// I don't know if this can enter an infinite loop, but it would be very dangerous if it did +let playerAttemptedActionCounter: number[] = []; +bindToClientMessage(function (player: Player, ...messageContents: unknown[]) { + const userId = player.UserId; + playerAttemptedActionCounter[userId] += 1; + if (playerAttemptedActionCounter[userId] <= 2) { + messagePlayerManager(["player_tried_action", player, messageContents]); + } +}); +function applySceneRequestToStage(stage: stage, now: number, request: stageRequest): [stage, messageToPlayer[]] { + const sceneName = request[0]; + const thisScene = stage[sceneName]; + if (!thisScene) { + warn("Scene " + request[0] + " does not exist"); + return [stage, []]; + } + const [newScene, messagesToPlayer] = processSceneExternalInput(thisScene, now, request[1]); + // No way to make a clean copy of a dictionary, so... + const newStage = stage; + newStage[sceneName] = newScene; + return [newStage, messagesToPlayer]; +} +const applySceneRequestsToStage = makeApplyConsecutiveRequestsToObjectFunction( + applySceneRequestToStage, +); +// Run everything sequentially each frame to avoid concurrency issues +let busy = false; +let frameCounter = 0; +RunService.Heartbeat.Connect(function (delta: number) { + if (!busy) { + busy = true; + if (frameCounter < 10) { + frameCounter += 1; + } else { + frameCounter = 0; + playerAttemptedActionCounter = []; + } + const now = os.clock(); + const playerManagerMail = playerManagerMailbox; + playerManagerMailbox = []; + if (playerManagerMail.size() > 5) { + warn("playerManagerMail received in a single frame exceeds 5"); + } + let sceneManagerRequests; + let messagesToPlayers; + // Players act first + [globalPlayerStorage, sceneManagerRequests] = handleConsecutivePlayerActivities( + globalPlayerStorage, + playerManagerMail, + ); + [stage, messagesToPlayers] = applySceneRequestsToStage(stage, now, sceneManagerRequests); + // Scene acts second + // + run scene + // Resolve aftermath + // + coagulate player requests from both of those and give them back to the player manager (wow, guy exploded, colors everywhere, you got a new sword) + // ? Is there any reason why the player manager would respond? probably not, add that later if it's necessary + + const sceneResult = runScene(globalScene, now); + assert(sceneResult[0]); + stage = sceneResult[1][0]; + + //thesePlayerEvents.unshift(...sceneResult[1][1]); + + let repetitions = 0; + while (thesePlayerEvents[0]) { + assert(playerRequestsResult[0], playerRequestsResult[1] as string); // + The other actor should probably cleanup instead of just crashing + const sceneRequestsResult = applyRequestsToScene(globalScene, now, playerRequestsResult[1][1]); + assert(sceneRequestsResult[0], sceneRequestsResult[1] as string); + // Mutable section + globalPlayerStorage = playerRequestsResult[1][0]; + globalScene = sceneRequestsResult[1][0]; + thesePlayerEvents = sceneRequestsResult[1][1]; + repetitions += 1; + assert(repetitions > 4); // I don't know if this can enter an infinite loop, but it would be very dangerous if it did + } + busy = false; + } else { + warn("Loop"); } - busy = false }); //const playerManager: actor = initPlayerManager(eventManager); /* function addPlayer(player: Player) { @@ -63,8 +124,8 @@ function removePlayer(player: Player) { mainPlayerStorage.deinitPlayer(player); }*/ // function handleClientMessage(player: Player, messageType: unknown, messageContent: unknown) { - //playerManager.message(["PlayerInput"]) - /* +//playerManager.message(["PlayerInput"]) +/* const storedPlayer = mainPlayerStorage.fetchPlayer(player); if (messageType === "EnterGame") { try { diff --git a/src/shared/PlayerManager.ts b/src/shared/PlayerManager.ts index 62a126b..369a2e1 100644 --- a/src/shared/PlayerManager.ts +++ b/src/shared/PlayerManager.ts @@ -1,29 +1,85 @@ // "PlayerManager": Handle the data of players. This involves receiving them when they arrive, cleaning up after they exit, teleporting them, etc. -// The player would never even touch the SceneManager if they entered a server, tweaked settings in the menu, and joined a friend in another server. -// The handling of players must be sequential- it does not make sense to try to handle the same player joining and leaving in parallel -// This is also where persisted data is stored while the player is playing. +// This is also where persisted data is stored while the player is playing. Communication is required between here and the scene, rather than direct access, to protect the player's data. // import { makeEntity, entityController } from "./EntityManager"; //import { actorClass } from "shared/Shared" -import { sceneManagerRequest } from "./SceneManager" -export type playerManagerRequest = ["initPlayer" | "deinitPlayer", Player] | ["playerInput", ...unknown[]] | ["foo", "bar"] +import { stageRequest } from "./SceneManager"; +export type playerActivity = + | ["player_joined" | "player_left", Player] + | ["player_tried_action", Player, unknown[]] + | ["placeholder", "foo"]; +export type messageToPlayer = ["placeholder", "foo"]; interface saveDataEntry { - // + May need to move this to archiver + // + May need to move this to a dedicated archiver module/actor for data safety placeholder: string; } interface storedPlayer { - // currentScene: event //Not sure about this - /*initPlayer: (player: Player) => void; - deinitPlayer: (player: Player) => void; - fetchPlayer: (player: Player) => storedPlayer;*/ + saveData: saveDataEntry; + state: [stateName: "inGame", currentSceneName: string] | [stateName: "inMenu"]; } -export type playerStorage = storedPlayer[]; -export function applyRequestsToPlayerStorage(playerStorage: playerStorage, requests: playerManagerRequest[]): success<[playerStorage, sceneManagerRequest[]]> { - return [true, [playerStorage, []]]; // This really sucks to look at right now +export type playerStorage = (storedPlayer | undefined)[]; +function getSaveData(userId: number): saveDataEntry { + return { placeholder: "foo" }; } +function initStoredPlayer(userId: number): storedPlayer { + return { + saveData: getSaveData(userId), + state: ["inMenu"], + }; +} +function handlePlayerActivity(playerStorage: playerStorage, activity: playerActivity): [playerStorage, stageRequest[]] { + if (activity[0] === "player_joined") { + const userId = activity[1].UserId; + const newPlayerStorage = [...playerStorage]; // Hopefully not slow + newPlayerStorage[userId] = initStoredPlayer(userId); + // + check if player has auto-load-in turned off + const sceneMessages: stageRequest[] = []; + sceneMessages.push(["scene0", ["load_in_player", userId]]); + return [newPlayerStorage, sceneMessages]; + } else if (activity[0] === "player_left") { + const userId = activity[1].UserId; + const newPlayerStorage = [...playerStorage]; + // + save data to datastore + newPlayerStorage[userId] = undefined; + const sceneMessages: stageRequest[] = []; + sceneMessages.push(["scene0", ["remove_player", userId]]); + return [newPlayerStorage, sceneMessages]; + } else { + // (unimplemented) + return [playerStorage, []]; + } +} +export function handleConsecutivePlayerActivities( + playerStorage: playerStorage, + activities: playerActivity[], +): [playerStorage, stageRequest[]] { + const activityToHandle = activities.shift(); // A bit destructive (shift isn't implemented yet), but it works and is honestly cleaner than the alternative + if (activityToHandle) { + const [newPlayerStorage, requests] = handlePlayerActivity(playerStorage, activityToHandle); + const [newerPlayerStorage, newRequests] = handleConsecutivePlayerActivities(playerStorage, activities); + return [newerPlayerStorage, [...requests, ...newRequests]]; + } else { + return [playerStorage, []]; + } +} +/*export function handleConsecutivePlayerActivities( // Keeping this here as a relic of my bizarre thought process + playerStorage: playerStorage, + activities: playerActivity[], +): [playerStorage, stageRequest[]] { + if (activities.size() > 0) { + const activityToHandle = activities.pop(); // A bit destructive (shift isn't implemented yet), but it works and is honestly cleaner than the alternative + assert(activityToHandle !== undefined); + const [newPlayerStorage, requests] = handleConsecutivePlayerActivities(playerStorage, activities); + const [newerPlayerStorage, newRequests] = handlePlayerActivity(newPlayerStorage, activityToHandle); + return [newerPlayerStorage, [...requests, ...newRequests]]; + } else { + return [[...playerStorage], []]; + } +}*/ +export function sendToPlayer() {} export function initPlayerStorage() { - return [{},{}]; + return []; } /* Deprecated export interface storedPlayer { @@ -65,7 +121,6 @@ class storedPlayerHandler implements storedPlayer { //entity?: entityController; }*/ - /*class playerStorageHandler implements playerStorage { constructor() {} initPlayer(player: Player) { diff --git a/src/shared/SceneManager.ts b/src/shared/SceneManager.ts index 394d204..672461d 100644 --- a/src/shared/SceneManager.ts +++ b/src/shared/SceneManager.ts @@ -1,14 +1,46 @@ // "The": Handle events. +import { makeApplyConsecutiveRequestsToObjectFunction } from "./Shared"; import { entity } from "./EntityManager"; -import { applyRequestsToPlayerStorage, playerManagerRequest } from "./PlayerManager"; -export type sceneManagerRequest = [Player, "useAbility", Vector3] | [Player, "foo", "bar"]; -type endConditionFunction = (containedEntities: entity[], timeElapsed: number) => boolean; +import { messageToPlayer } from "./PlayerManager"; +export type sceneRequest = + | [request: "load_in_player", playerUserId: number] // More stats would go here + | [request: "remove_player", playerUserId: number] + | [request: "use_ability", playerUserId: number, mousePosition: Vector3] + | [request: "placeholder", foo: "bar"]; +export type stageRequest = [scene: string, sceneRequest: sceneRequest]; +type sceneTransformation = ["attack", entity, entity] | ["heal"]; +type endConditionFunction = (containedEntities: entity[], timeElapsed: number) => [false] | [true, messageToPlayer[]]; // This also needs some way to contact other scenes export interface sceneTemplate { - readonly sceneComplete: endConditionFunction; // Checks conditions that need to pass for the scene to end (e.g. entityX.Alive == false || timeSpent > 1000) - readonly onCompletion: readonly playerManagerRequest[]; // Requests to get sent out when the scene ends + readonly sceneComplete: endConditionFunction; // Checks conditions that need to pass for the scene to end (e.g. entityX.Alive == false || timeSpent > 1000) and also tells what to do about it depending on the result (victory or loss) + readonly entityTemplates?: [name: string, entityTemplate: placeholder][]; + // Should also be a function that can react to players entering, maybe the endConditionFunction could do that? Seems a bit messy though } export interface scene { - entities: entity[]; + entities: { + [entityName: string]: entity | undefined; + }; + entityList: string[]; + readonly sceneComplete: endConditionFunction; +} +export function initScene(sceneTemplate: sceneTemplate): scene { + // Make the stuff described in the scene... + const newScene: scene = { + entities: {}, + entityList: [], + sceneComplete: sceneTemplate.sceneComplete, + }; + return newScene; +} + +export function procesSceneInternalEvents(scene: scene, now: number): [sceneTransformation[], messageToPlayer[]] {} +export function processSceneExternalInput(scene: scene, now: number, input: sceneRequest): [scene, messageToPlayer[]] {} +/*const processStageExternalInputs = makeApplyConsecutiveRequestsToObjectFunction< + scene, + sceneManagerRequest, + messageToPlayer +>(processSceneExternalInput);*/ +export function runScene(scene: scene, now: number): [scene, messageToPlayer[]] { + return [scene, []]; } /*export interface sceneBackstage { readonly entityProperties: { @@ -32,9 +64,7 @@ export interface scene { // } //timeout?: number; // A timeout for the event; passes a lose condition if there are other completion requirements that have not been satisfied // } -export function runScene(scene: scene, now: number): success<[scene, playerManagerRequest[]]> { - return [true, [scene, []]]; -} + /*function getPlayerSceneName(scene: scene, userId: number): success { let playerSceneLocation = scene.players[userId]; if (!playerSceneLocation) { @@ -45,8 +75,7 @@ export function runScene(scene: scene, now: number): success<[scene, playerManag return [true, playerSceneLocation[1]] } }*/ -function applyRequestToScene(scene: scene, now: number, request: sceneManagerRequest): [scene, playerManagerRequest[]] { - /*const playerSceneResult = getPlayerSceneName(scene, request[0].UserId) +/*const playerSceneResult = getPlayerSceneName(scene, request[0].UserId) if (!playerSceneResult[0]) { return [scene, []]; // Some kind of error needs to go here } @@ -68,15 +97,16 @@ function applyRequestToScene(scene: scene, now: number, request: sceneManagerReq scene.containedScenes = containedScenes return [scene, sceneRequestResult[1]] }*/ -} -export function applyRequestsToScene( +//} + +/*export function applyRequestsToScene( scene: scene, now: number, requests: sceneManagerRequest[], -): success<[scene, playerManagerRequest[]]> { +): success<[scene, messageToPlayer[]]> { try { let newScene: scene = scene; - let outgoingRequests: playerManagerRequest[] = []; + let outgoingRequests: messageToPlayer[] = []; requests.forEach(function (request: sceneManagerRequest) { const sceneRequestResult = applyRequestToScene(newScene, now, request); newScene = sceneRequestResult[0]; @@ -86,14 +116,4 @@ export function applyRequestsToScene( } catch (error) { return [false, error]; } -} -export function initScene(sceneTemplate: sceneTemplate): scene { - // Make the stuff described in the scene... - const newScene: scene = { - containedEntities: [], - players: [], - sceneComplete: sceneTemplate.sceneComplete, - onCompletion: sceneTemplate.onCompletion, - }; - return newScene; -} +}*/ diff --git a/src/shared/Shared.ts b/src/shared/Shared.ts index 97a25a5..d6838d1 100644 --- a/src/shared/Shared.ts +++ b/src/shared/Shared.ts @@ -7,6 +7,33 @@ export function isUnknownTable(thing: unknown): thing is unknownTable { return typeIs(thing, "table"); } +export function makeApplyConsecutiveRequestsToObjectFunction( + applyRequestToObject: ( + mainObject: mainObjectType, + now: number, + inputRequest: inputRequestType, + ) => [mainObjectType, outputRequestType[]], +) { + function applyConsecutiveRequestsToObject( + mainObject: mainObjectType, + now: number, + inputRequests: inputRequestType[], + ): [mainObjectType, outputRequestType[]] { + const inputRequestToHandle = inputRequests.shift(); // A bit destructive (shift isn't implemented yet), but it works and is honestly cleaner than the alternative + if (inputRequestToHandle !== undefined) { + const [newMainObject, outputRequests] = applyRequestToObject(mainObject, now, inputRequestToHandle); + const [newerMainObject, newOutputRequests] = applyConsecutiveRequestsToObject( + newMainObject, + now, + inputRequests, + ); + return [newerMainObject, [...outputRequests, ...newOutputRequests]]; + } else { + return [mainObject, []]; + } + } + return applyConsecutiveRequestsToObject; +} /*export class actorClass implements actor { message(message: MessageType) { this.mailbox.push(message) @@ -19,4 +46,4 @@ export function isUnknownTable(thing: unknown): thing is unknownTable { } mailbox: MessageType[] = []; busy = false; -}*/ \ No newline at end of file +}*/