From 79202b9034f993e26588e38ec5b6556c4a538ab6 Mon Sep 17 00:00:00 2001 From: loplkc Date: Sat, 29 Jan 2022 23:04:01 -0500 Subject: [PATCH] Moved some interfaces out of services to decouple + More OOP changes on the server side --- src/client/ClientMessenger.ts | 2 +- src/client/EffectMaker.ts | 5 +- src/client/InputHandler.ts | 96 +++++++++++++++++++++++++++++++++++ src/client/init.client.ts | 40 +++------------ src/server/ServerMessenger.ts | 6 +-- src/server/main.server.ts | 21 ++++---- src/services.d.ts | 41 +++++---------- src/shared/EntityManager.ts | 33 +++++++----- src/shared/EventManager.ts | 7 ++- src/shared/PlayerManager.ts | 82 ++++++++++++++++++++++-------- src/shared/Puppetmaster.ts | 61 ++++++++++++++-------- 11 files changed, 260 insertions(+), 134 deletions(-) create mode 100644 src/client/InputHandler.ts diff --git a/src/client/ClientMessenger.ts b/src/client/ClientMessenger.ts index a4ded34..723e9ca 100644 --- a/src/client/ClientMessenger.ts +++ b/src/client/ClientMessenger.ts @@ -1,5 +1,5 @@ import { Input, Output } from "shared/Remotes"; -export function bindToOutput(functionToBind: Callback) { +export function bindToServerMessage(functionToBind: Callback) { assert(Output?.IsA("RemoteEvent"), 'Remote event "Input" is of incorrect class or nil'); Output.OnClientEvent.Connect(functionToBind); } diff --git a/src/client/EffectMaker.ts b/src/client/EffectMaker.ts index 5849ef2..5e34a28 100644 --- a/src/client/EffectMaker.ts +++ b/src/client/EffectMaker.ts @@ -24,7 +24,7 @@ interface effectHandler extends effectMaker, effectRunner { } export function makeEffectHandler(effectFolder: Folder) { - const effectHandler: effectHandler = { + return { meshPartEffect: function(meshPart: MeshPart, material: Enum.Material, effectKeypoints: effectKeypoint[], priority?: number) { const effectMeshPart = meshPart.Clone(); effectMeshPart.Material = material; @@ -102,8 +102,7 @@ export function makeEffectHandler(effectFolder: Folder) { EFFECT_FOLDER: effectFolder, effectsToRun: [], - } - return effectHandler; + } as effectHandler; } /* local function horseEffModule(o,typ,a1,a2,a3,a4,a5,a6,a7) diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts new file mode 100644 index 0000000..5c89dbe --- /dev/null +++ b/src/client/InputHandler.ts @@ -0,0 +1,96 @@ +const Players = game.GetService("Players"); +const UserInputService = game.GetService("UserInputService"); +const ContextActionService = game.GetService("ContextActionService"); +const Workspace = game.GetService("Workspace"); +const CAMERA = Workspace.CurrentCamera as Camera; +assert(CAMERA, 'Camera of "' + Players.LocalPlayer.DisplayName + '"does not exist! (HOW???)'); + +export type inputBindings = { + [input in keyof controlBindings]?: [((argument1?: unknown, argument2?: unknown) => void), unknown, unknown]; +}; + +export interface inputBinder { + assignControlBindings: (controlBindings: controlBindings) => void; + assignInputBindings: (inputBindings: inputBindings) => void; + removeInputBindings: (inputBindings: inputBindings) => void; +} + +interface inputHandler extends inputBinder { + controlHandler: (actionName: string, state: Enum.UserInputState, inputObject: InputObject) => void; + controlBindings?: controlBindings + boundInputs: inputBindings + storedInput?: string // + compound inputs +} + +const hitParams = new RaycastParams(); +//hitParams.FilterDescendantsInstances = {efFolder,Plr.Character} +hitParams.FilterType = Enum.RaycastFilterType.Blacklist; +function getMouseLocation(): [Vector3, Vector3, Instance | undefined] { + //hitParams.FilterDescendantsInstances = {efFolder,Plr.Character} + const mouseLocation = UserInputService.GetMouseLocation(); + const unitRay = CAMERA.ViewportPointToRay(mouseLocation.X, mouseLocation.Y); + const cast = Workspace.Raycast(unitRay.Origin, unitRay.Direction.mul(1000), hitParams); + if (cast) { + return [cast.Position, cast.Normal, cast.Instance]; + } else { + return [unitRay.Origin.add(unitRay.Direction.mul(1000)), new Vector3(0, 0, 0), undefined]; + } +} + +function isValidInput(controlBindings: controlBindings, value: string): value is keyof controlBindings { + return value in controlBindings; // uh oh +} +//function(actionName: string, state: Enum.UserInputState, inputObject: InputObject) { +// inputParameters[0](inputParameters[1]) +//} + +export function makeInputHandler() { + const t: inputHandler = {//return { + assignControlBindings: function(controlBindings: controlBindings) { + this.controlBindings = controlBindings // WOW!!!!! + }, + assignInputBindings: function(inputBindings: inputBindings) { + const controlBindings = this.controlBindings; + if (controlBindings) { + for (let input in inputBindings) { + if (isValidInput(controlBindings, input)) { + // + Check for this.boundInputs[input] + const inputParameters = inputBindings[input] + if (inputParameters) { + this.boundInputs[input] = inputParameters; + const controlArray = controlBindings[input]; + for (const control of controlArray) { + ContextActionService.BindAction(input, this.controlHandler, false, control) + } + } + } + } + } + }, + removeInputBindings: function(inputBindings: inputBindings) {}, + controlHandler: function(actionName: string, state: Enum.UserInputState, inputObject: InputObject) { + const controlBindings = this.controlBindings; + if (controlBindings) { + if (isValidInput(controlBindings, actionName)) { + const inputParameters = this.boundInputs[actionName] + if (inputParameters) { + inputParameters[0](inputParameters[1], inputParameters[2]) + } + } + } + }, + + boundInputs: {} + } //as inputHandler; +} +/* +function handleInput(input: InputObject, otherInteraction: boolean) { + let mousePosition: Vector3, mouseNormal: Vector3, mouseInstance: Instance | undefined; + [mousePosition, mouseNormal, mouseInstance] = getMouseLocation(); // eslint-disable-line prefer-const + if (input.UserInputType === Enum.UserInputType.MouseButton1) { + messageServer("move", mousePosition); + } +} +*/ + +// UserInputService.InputBegan.Connect(handleInput); \ No newline at end of file diff --git a/src/client/init.client.ts b/src/client/init.client.ts index b76ffdb..2b6cf97 100644 --- a/src/client/init.client.ts +++ b/src/client/init.client.ts @@ -1,9 +1,7 @@ -// "init": The local script. This script doesn't have to account for any other players. +// "init": The main client-side thread. const Players = game.GetService("Players"); -const UserInputService = game.GetService("UserInputService"); const RunService = game.GetService("RunService"); -const Workspace = game.GetService("Workspace"); -import { bindToOutput, messageServer } from "./ClientMessenger"; +import { bindToServerMessage, messageServer } from "./ClientMessenger"; import { handleGuiInput, drawGui, closeGui } from "./GuiHandler"; import { makeEffectHandler, effectRunner } from "./EffectMaker"; const LOCALPLAYER = Players.LocalPlayer; @@ -12,8 +10,6 @@ assert( PLAYERGUI && classIs(PLAYERGUI, "PlayerGui"), 'PlayerGui of "' + LOCALPLAYER.DisplayName + '"does not exist! (HOW???)', ); -const CAMERA = Workspace.CurrentCamera as Camera; -assert(CAMERA, 'Camera of "' + LOCALPLAYER.DisplayName + '"does not exist! (HOW???)'); let inMainMenu = true; function openMainMenu(playerGui: PlayerGui) { @@ -26,29 +22,7 @@ function openMainMenu(playerGui: PlayerGui) { } } -const hitParams = new RaycastParams(); -//hitParams.FilterDescendantsInstances = {efFolder,Plr.Character} -//hitParams.FilterType = Enum.RaycastFilterType.Blacklist; -function getMouseLocation(): [Vector3, Vector3, Instance | undefined] { - //hitParams.FilterDescendantsInstances = {efFolder,Plr.Character} - const mouseLocation = UserInputService.GetMouseLocation(); - const unitRay = CAMERA.ViewportPointToRay(mouseLocation.X, mouseLocation.Y); - const cast = Workspace.Raycast(unitRay.Origin, unitRay.Direction.mul(1000), hitParams); - if (cast) { - return [cast.Position, cast.Normal, cast.Instance]; - } else { - return [unitRay.Origin.add(unitRay.Direction.mul(1000)), new Vector3(0, 0, 0), undefined]; - } -} - -function handleInput(input: InputObject, otherInteraction: boolean) { - let mousePosition: Vector3, mouseNormal: Vector3, mouseInstance: Instance | undefined; - [mousePosition, mouseNormal, mouseInstance] = getMouseLocation(); // eslint-disable-line prefer-const - if (input.UserInputType === Enum.UserInputType.MouseButton1) { - messageServer("move", mousePosition); - } -} -function handleOutput(messageType: unknown, messageContent: unknown) { +function handleServerMessage(messageType: unknown, messageContent: unknown) { if (messageType === "init") { openMainMenu(PLAYERGUI); inMainMenu = true; @@ -57,12 +31,12 @@ function handleOutput(messageType: unknown, messageContent: unknown) { inMainMenu = false; } } -// Action phase -UserInputService.InputBegan.Connect(handleInput); -bindToOutput(handleOutput); +// Bind functions + +bindToServerMessage(handleServerMessage); const effectRunners: effectRunner[] = []; -// Put stuff in the effectRunners table +// + Put stuff in the effectRunners table RunService.RenderStepped.Connect(function(deltaTime) { effectRunners.forEach(effectRunner => { effectRunner.runEffects(deltaTime) diff --git a/src/server/ServerMessenger.ts b/src/server/ServerMessenger.ts index 99d8bec..5426043 100644 --- a/src/server/ServerMessenger.ts +++ b/src/server/ServerMessenger.ts @@ -1,13 +1,13 @@ import { Input, Output } from "shared/Remotes"; -export function bindToInput(functionToBind: Callback) { +export function bindToClientMessage(functionToBind: Callback) { assert(Input?.IsA("RemoteEvent"), 'Remote event "Input" is of incorrect class or nil'); Input.OnServerEvent.Connect(functionToBind); } -export function messageClient(client: Player, messageType: serverMessageType, messageContent?: string) { +export function messageClient(client: Player, messageType: string, messageContent?: string) { assert(Output?.IsA("RemoteEvent"), 'Remote event "Output" is of incorrect class or nil'); Output.FireClient(client, messageType, messageContent); } -export function messageAllClients(messageType: serverMessageType, messageContent?: string) { +export function messageAllClients(messageType: string, messageContent?: string) { assert(Output?.IsA("RemoteEvent"), 'Remote event "Output" is of incorrect class or nil'); Output.FireAllClients(messageType, messageContent); } diff --git a/src/server/main.server.ts b/src/server/main.server.ts index 0b51891..ce5f73d 100644 --- a/src/server/main.server.ts +++ b/src/server/main.server.ts @@ -1,24 +1,23 @@ // "main": This is the core of reality. It serves as the highest-level abstraction. // + Prevent this from coupling with the entity manager, if possible const Players = game.GetService("Players"); -import { makeEntity, moveEntity } from "shared/EntityManager"; -import { initPlayer, deinitPlayer, loadInPlayer, teleportPlayer } from "shared/PlayerManager"; -import { bindToInput, messageClient, messageAllClients } from "./ServerMessenger"; -const playerStorage: (playerStorageEntry | undefined)[] = []; -const entityStorage: entity[] = []; +import { makePlayerStorage, playerStorage, storedPlayer } from "shared/PlayerManager"; +import { bindToClientMessage, messageClient, messageAllClients } from "./ServerMessenger"; +const playerStorage: playerStorage = makePlayerStorage(); function addPlayer(player: Player) { - playerStorage[player.UserId] = initPlayer(player); + playerStorage.initPlayer(player); messageClient(player, "init", "idk"); } function removePlayer(player: Player) { - playerStorage[player.UserId] = deinitPlayer(playerStorage[player.UserId], player); + playerStorage.deinitPlayer(player); } // Handling input is a complicated process that requires passing a large variety of data to a large variety of places, so it's staying here for now -function handleInput(player: Player, messageType: unknown, messageContent: unknown) { +function handleClientMessage(player: Player, messageType: unknown, messageContent: unknown) { + const storedPlayer = playerStorage.fetchPlayer(player); if (messageType === "EnterGame") { try { - entityStorage[player.UserId] = loadInPlayer(player); + storedPlayer.loadIn(); messageClient(player, "enterGame"); } catch (thrownError) { if (typeIs(thrownError, "string")) { @@ -30,11 +29,11 @@ function handleInput(player: Player, messageType: unknown, messageContent: unkno } } else if (messageType === "move") { if (typeIs(messageContent, "Vector3")) { - moveEntity(entityStorage[player.UserId], messageContent); + storedPlayer.setPosition(messageContent) } } } // Action phase Players.PlayerAdded.Connect(addPlayer); Players.PlayerRemoving.Connect(removePlayer); -bindToInput(handleInput); +bindToClientMessage(handleClientMessage); diff --git a/src/services.d.ts b/src/services.d.ts index 4cf32fc..f14894b 100644 --- a/src/services.d.ts +++ b/src/services.d.ts @@ -1,33 +1,8 @@ -type puppetEntry = ["Character", Player] | ["Placeholder", "Placeholder"]; +/* type bodyPart = "root" | "torso" | "head" | "leftArm" | "rightArm" | "leftLeg" | "rightLeg"; type serverMessageType = "init" | "promptError" | "enterGame"; type clientMessageType = "move" | "placeholder"; - -interface saveDataEntry { - placeholder: string; -} -interface playerStorageEntry { - inMainMenu: boolean; - // + Other data that is unique to players but does not persist between sessions - saveData: saveDataEntry; - entity?: entity; -} -interface puppet { - entry: puppetEntry; - model: Model; - rootPart: Part; - //placeholder: (x: string) => string; // + "Puppet string" functions will (not?) go here -} -interface entity { - baseStats: [number, number, number, number]; // MaxHealth, Attack, Speed, Defense (things used for calculation, only modified by buffs and debuffs) - baseAmounts: [number, number]; // Health, Barrier (things of indescribable importance) - puppet: puppet; -} -interface event { - winEvents?: event[]; // A list of events that need to return true (in sequence) to complete this event - winEntities?: entity[]; // A list of entities that need to die to complete the event - timeout?: number; // A timeout for the event; passes a lose condition if there are other completion requirements that have not been satisfied -} +// + Why can the client see all of these? type meshType = "Ball"; type effectState = [CFrame, Vector3, Color3, number]; // The number is transparency @@ -36,6 +11,18 @@ type effectEntry = [meshType, EnumItem, effectState[]]; // The enumitem is mater interface modeLocal { aura?: [effectEntry, bodyPart?, number?][]; // effect, part it is attached to (default root), how many times it should be called per frame (default 1) } +*/ +// Genuinely require being on both sides - but in the services file? No shot! +type acceptedControls = Enum.KeyCode[] // + Include controller "keys" +interface controlBindings { + clicker1: acceptedControls; // What is used to click on things (enemies in game, UI elements) + diamond1: acceptedControls; // Diamond controls + diamond2: acceptedControls; + diamond3: acceptedControls; + diamond4: acceptedControls; + special1: acceptedControls; // Special controls +} + /*interface hookInEntry { name: string; diff --git a/src/shared/EntityManager.ts b/src/shared/EntityManager.ts index e414b8e..f15361a 100644 --- a/src/shared/EntityManager.ts +++ b/src/shared/EntityManager.ts @@ -1,18 +1,27 @@ // "EntityManager": Create entities objects and deck them out with functions to use. // + Functions are here, as to avoid storing unecessary data in the server store. -import { makePuppet, movePuppet } from "./Puppetmaster"; -export function makeEntity(puppetEntry: puppetEntry) { - const newEntity: entity = { - baseStats: [0, 0, 0, 0], - baseAmounts: [0, 0], - puppet: makePuppet(puppetEntry), - }; - return newEntity; +import { makePuppet, puppet, puppetEntry } from "./Puppetmaster"; +type stats = [maxHealth: number, attack: number, speed: number, defense: number] // values used for calculation, only modified by buffs and debuffs +type amounts = [health: number, barrier: number] // values used to store an entity's current status (more existential than stats) + +export interface entity { + setPosition: (location: Vector3) => void; } -// This exists because the main server should never see the puppets, but it is a bit weird -export function moveEntity(entity: entity | undefined, location: Vector3) { - if (entity) { - entity.puppet = movePuppet(entity.puppet, location); +class entityHandler implements entity { + constructor(baseStats: stats, baseAmounts: amounts, puppetEntry: puppetEntry) { + this.baseStats = baseStats; + this.baseAmounts = baseAmounts; + this.puppet = makePuppet(puppetEntry); + }; + setPosition(location: Vector3) { + this.puppet.movePuppet(location); } + baseStats: stats; + baseAmounts: amounts; // Health, Barrier (things of indescribable importance) + puppet: puppet; +} + +export function makeEntity(puppetEntry: puppetEntry, baseStats = [100, 1, 16, 0] as stats, baseAmounts = [100, 0] as amounts) { + return new entityHandler(baseStats, baseAmounts, puppetEntry); } diff --git a/src/shared/EventManager.ts b/src/shared/EventManager.ts index b00d6ae..ebccc90 100644 --- a/src/shared/EventManager.ts +++ b/src/shared/EventManager.ts @@ -1,6 +1,11 @@ // "The": Handle events. // WORST CONDITION RigHT NOW -import { makeEntity } from "./EntityManager"; +import { makeEntity, entity } from "./EntityManager"; +interface event { + winEvents?: event[]; // A list of events that need to return true (in sequence) to complete this event + winEntities?: entity[]; // A list of entities that need to die to complete the event + timeout?: number; // A timeout for the event; passes a lose condition if there are other completion requirements that have not been satisfied +} export function runEvent(event: event) { let complete = false; const startTime = os.clock(); diff --git a/src/shared/PlayerManager.ts b/src/shared/PlayerManager.ts index 1714a75..4c61b3a 100644 --- a/src/shared/PlayerManager.ts +++ b/src/shared/PlayerManager.ts @@ -1,28 +1,66 @@ // "PlayerManager": Handle the data of players. This involves receiving them when they arrive, cleaning up after they exit, teleporting them, etc. -import { makeEntity } from "./EntityManager"; +import { makeEntity, entity } from "./EntityManager"; -export function initPlayer(player: Player) { - const newEntry: playerStorageEntry = { - inMainMenu: true, - saveData: { - placeholder: "placeholder", - }, +interface saveDataEntry { // + May need to move this to archiver + placeholder: string; +} +export interface storedPlayer { + teleportToServer: () => void; + setPosition: (location: Vector3) => void; + loadIn: () => void; +} + +class storedPlayerHandler implements storedPlayer { + constructor(player: Player) { + this.player = player; + this.inMainMenu = true; + this.saveData = {placeholder: "fortnite"}; + } + teleportToServer() { + // + Do checking related to where the player is allowed to go + // + Teleport player to other server, sending a message to have them load in automatically }; - // + Load player's datastore into server store - return newEntry; // Return the entry to be put into the server store + setPosition(location: Vector3) { + if (this.entity) { + this.entity.setPosition(location); + } + }; + loadIn() { + this.entity = makeEntity(["Character", this.player]); + // + Give the entity the stats it's supposed to have, load from save data maybe? + }; + player: Player; + inMainMenu: boolean; + // + Other data that is unique to players but does not persist between sessions + saveData: saveDataEntry; + entity?: entity; } -export function deinitPlayer(entry: playerStorageEntry | undefined, player: Player) { - assert(entry, "Trying to remove entry of player " + player.DisplayName + ", but entry does not exist!"); - // ? Tell the entity to unload, if it still exists (the entity will tell the other clients to remove the player) - // + Unload player's server store to datastores - return undefined; // A nil entry to replace the entry to be wiped and maybe a success value in a wrapper + +export interface playerStorage { + initPlayer: (player: Player) => void; + deinitPlayer: (player: Player) => void; + fetchPlayer: (player: Player) => storedPlayer; } -export function loadInPlayer(player: Player) { - const entity = makeEntity(["Character", player]); - // + Give the entity the stats it's supposed to have, load from save data maybe? - return entity; -} -export function teleportPlayer() { - // + Do checking related to where the player is allowed to go - // + Teleport player to other server, sending a message to have them load in automatically + +class playerStorageHandler implements playerStorage { + constructor() {}; + initPlayer(player: Player) { + this.playerStorageArray[player.UserId] = new storedPlayerHandler(player); + // + Load player's datastore into server store + }; + deinitPlayer(player: Player) { + const entry = this.playerStorageArray[player.UserId]; + assert(entry, "Trying to remove entry of player " + player.DisplayName + ", but entry does not exist!"); + // ? Tell the entity to unload, if it still exists (the entity will tell the other clients to remove the player) + // + Unload player's server store to datastores + return undefined; // A nil entry to replace the entry to be wiped and maybe a success value in a wrapper + }; + fetchPlayer(player: Player) { + return this.playerStorageArray[player.UserId]; + } + playerStorageArray: storedPlayerHandler[] = []; } + +export function makePlayerStorage() { + return new playerStorageHandler(); +} \ No newline at end of file diff --git a/src/shared/Puppetmaster.ts b/src/shared/Puppetmaster.ts index b68a755..09cb827 100644 --- a/src/shared/Puppetmaster.ts +++ b/src/shared/Puppetmaster.ts @@ -5,34 +5,53 @@ const puppetLibraries = { ["Placeholder"]: m1, }; -export function makePuppet(puppetEntry: puppetEntry) { +export type puppetEntry = ["Character", Player] | ["Placeholder", "Placeholder"]; + +export interface puppet { + movePuppet: (location: Vector3) => void; // + Success value maybe? +} +/* interface puppetHandler extends puppet { + entry: puppetEntry; +} */ +/* interface completePuppetHandler extends puppetHandler { + model: Model; + rootPart: Part; +} */ + +function makePuppetModel(puppetEntry: puppetEntry) { if (puppetEntry[0] === "Character") { - const model: [Model, Part] = puppetLibraries[puppetEntry[0]].makeModel(puppetEntry[1]); - return { - entry: puppetEntry, - model: model[0], - rootPart: model[1], - }; + return puppetLibraries[puppetEntry[0]].makeModel(puppetEntry[1]) as [Model, Part]; } else { throw 'Invalid puppet type "' + puppetEntry[0] + '"!'; } } -function verifyPuppetExistence(puppet: puppet) { - if (puppet.rootPart.Parent) { - // Placeholder; puppet integrity will include other body parts - return puppet; - } else { - print("No puppet!"); - return makePuppet(puppet.entry); +function verifyPuppetExistence(puppetHandler: puppetHandler)/*: puppetHandler is completePuppetHandler */{ // + Making this do type checking is currently beyond me + if (!puppetHandler.rootPart || !puppetHandler.rootPart.Parent) { + print("Regenerating puppet!"); + [puppetHandler.model, puppetHandler.rootPart] = makePuppetModel(puppetHandler.entry); } + // return true; } +class puppetHandler implements puppet { + constructor(puppetEntry: puppetEntry) { + this.entry = puppetEntry; + }; -export function movePuppet(puppet: puppet, location: Vector3) { - print("executing puppet move"); - puppet = verifyPuppetExistence(puppet); //const newPuppet = verifyPuppetExistence(puppet); - //puppet.rootPart = newPuppet.rootPart; - //puppet.model = newPuppet.model; - puppet.rootPart.CFrame = new CFrame(location); - return puppet; + movePuppet(location: Vector3) { + print("executing puppet move"); + verifyPuppetExistence(this); + if (!this.rootPart) { // + Remove this once you get better at typescript + throw "Fornite"; + } + this.rootPart.CFrame = new CFrame(location); + } + + entry: puppetEntry; + model?: Model; + rootPart?: Part; +}; + +export function makePuppet(puppetEntry: puppetEntry) { + return new puppetHandler(puppetEntry);// return newPuppet as puppet; }