Added groundwork for action system

+ "UnknownTable" universal type
~ Moved data shared by scripts on the client and server to "Shared.ts"
~ ESLint on this machine reformatted a ton of things
This commit is contained in:
loplkc loplkc 2022-02-27 21:28:11 -05:00
parent 1564575a7f
commit 846b86f74e
11 changed files with 164 additions and 86 deletions

View file

@ -1,4 +1,4 @@
import { Input, Output } from "shared/Remotes"; import { Input, Output } from "shared/Shared";
export function bindToServerMessage(functionToBind: Callback) { export function bindToServerMessage(functionToBind: Callback) {
assert(Output?.IsA("RemoteEvent"), 'Remote event "Input" is of incorrect class or nil'); assert(Output?.IsA("RemoteEvent"), 'Remote event "Input" is of incorrect class or nil');
Output.OnClientEvent.Connect(functionToBind); Output.OnClientEvent.Connect(functionToBind);

View file

@ -1,6 +1,14 @@
const TweenService = game.GetService("TweenService"); const TweenService = game.GetService("TweenService");
export type effectKeypoint = [time: number, color: Color3, size: Vector3, cframe: CFrame, transparency: number, easingStyle?: Enum.EasingStyle, easingDirection?: Enum.EasingDirection]; export type effectKeypoint = [
type effect = [number, effectKeypoint[], MeshPart, number] // Time since last keypoint, effect keypoints, effect meshPart, effect priority time: number,
color: Color3,
size: Vector3,
cframe: CFrame,
transparency: number,
easingStyle?: Enum.EasingStyle,
easingDirection?: Enum.EasingDirection,
];
type effect = [number, effectKeypoint[], MeshPart, number]; // Time since last keypoint, effect keypoints, effect meshPart, effect priority
export interface effectMaker { export interface effectMaker {
meshPartEffect: ( meshPartEffect: (
@ -23,7 +31,7 @@ interface effectHandler extends effectMaker, effectRunner {
*/ */
class effectHandler implements effectMaker, effectRunner { class effectHandler implements effectMaker, effectRunner {
constructor(effectFolder: Folder) { constructor(effectFolder: Folder) {
this.EFFECT_FOLDER = effectFolder this.EFFECT_FOLDER = effectFolder;
} }
meshPartEffect(meshPart: MeshPart, material: Enum.Material, effectKeypoints: effectKeypoint[], priority?: number) { meshPartEffect(meshPart: MeshPart, material: Enum.Material, effectKeypoints: effectKeypoint[], priority?: number) {
const effectMeshPart = meshPart.Clone(); const effectMeshPart = meshPart.Clone();
@ -32,32 +40,37 @@ class effectHandler implements effectMaker, effectRunner {
effectMeshPart.Size = effectKeypoints[0][2]; effectMeshPart.Size = effectKeypoints[0][2];
effectMeshPart.CFrame = effectKeypoints[0][3]; effectMeshPart.CFrame = effectKeypoints[0][3];
effectMeshPart.Transparency = effectKeypoints[0][4]; effectMeshPart.Transparency = effectKeypoints[0][4];
let effectDuration = 0 let effectDuration = 0;
effectKeypoints.forEach(effectKeypoint => { effectKeypoints.forEach((effectKeypoint) => {
effectDuration += effectKeypoint[0] effectDuration += effectKeypoint[0];
}); });
effectMeshPart.CastShadow = false; effectMeshPart.CastShadow = false;
effectMeshPart.CanCollide = false; effectMeshPart.CanCollide = false;
effectMeshPart.Anchored = true; effectMeshPart.Anchored = true;
effectMeshPart.Parent = this.EFFECT_FOLDER; effectMeshPart.Parent = this.EFFECT_FOLDER;
// Insert the effect before the effect that will end after it // Insert the effect before the effect that will end after it
const effectsToRun = this.effectsToRun const effectsToRun = this.effectsToRun;
for (let index = 0; index < effectsToRun.size(); index += 1) { for (let index = 0; index < effectsToRun.size(); index += 1) {
const effectInArray = effectsToRun[index]; const effectInArray = effectsToRun[index];
let effectInArrayDuration = -effectInArray[0] let effectInArrayDuration = -effectInArray[0];
effectInArray[1].forEach(effectKeypoint => { effectInArray[1].forEach((effectKeypoint) => {
effectInArrayDuration += effectKeypoint[0] effectInArrayDuration += effectKeypoint[0];
}); });
if (effectInArrayDuration > effectDuration) { if (effectInArrayDuration > effectDuration) {
effectsToRun.insert(index, [0, effectKeypoints, effectMeshPart, priority || 0] as effect); effectsToRun.insert(index, [0, effectKeypoints, effectMeshPart, priority !== undefined || 0] as effect);
break break;
} }
} }
effectsToRun.insert(effectsToRun.size(), [0, effectKeypoints, effectMeshPart, priority || 0] as effect); effectsToRun.insert(effectsToRun.size(), [
0,
effectKeypoints,
effectMeshPart,
priority !== undefined || 0,
] as effect);
} }
particleEffect() {} particleEffect() {}
runEffects(timeSinceLastFrame: number) { runEffects(timeSinceLastFrame: number) {
let effectsToRun = this.effectsToRun; const effectsToRun = this.effectsToRun;
print(tostring(effectsToRun.size()) + " effects to run."); print(tostring(effectsToRun.size()) + " effects to run.");
for (const effect of effectsToRun) { for (const effect of effectsToRun) {
// Update the effect time // Update the effect time

View file

@ -7,14 +7,14 @@ assert(CAMERA, 'Camera of "' + Players.LocalPlayer.DisplayName + '"does not exis
function enumTypeIs<EnumAsType>(value: unknown, EnumAsObject: Enum): value is EnumAsType { function enumTypeIs<EnumAsType>(value: unknown, EnumAsObject: Enum): value is EnumAsType {
if (typeIs(value, "EnumItem")) { if (typeIs(value, "EnumItem")) {
return value.Name in EnumAsObject return value.Name in EnumAsObject;
} else { } else {
return false return false;
} }
} }
type validInput = Enum.KeyCode // + Include controller "keys" type validInput = Enum.KeyCode; // + Include controller "keys"
function isValidInput(value: unknown): value is validInput { function isValidInput(value: unknown): value is validInput {
return enumTypeIs<Enum.KeyCode>(value, Enum.KeyCode) return enumTypeIs<Enum.KeyCode>(value, Enum.KeyCode);
} }
const actionAssignmentsReference: string[] = [ const actionAssignmentsReference: string[] = [
"clicker1", // What is used to click on things (enemies in game, UI elements) "clicker1", // What is used to click on things (enemies in game, UI elements)
@ -24,8 +24,9 @@ const actionAssignmentsReference: string[] = [
"diamond4", "diamond4",
"special1", // Special controls "special1", // Special controls
"special2", "special2",
] ];
export interface actionAssignments { // Based on the reference array export interface actionAssignments {
// Based on the reference array
clicker1?: validInput; // What is used to click on things (enemies in game, UI elements) clicker1?: validInput; // What is used to click on things (enemies in game, UI elements)
diamond1?: validInput; // Diamond controls diamond1?: validInput; // Diamond controls
diamond2?: validInput; diamond2?: validInput;
@ -34,20 +35,17 @@ export interface actionAssignments { // Based on the reference array
special1?: validInput; // Special controls special1?: validInput; // Special controls
special2?: validInput; special2?: validInput;
} }
type action = keyof actionAssignments type action = keyof actionAssignments;
function isValidAction(value: string): value is keyof actionAssignments { function isValidAction(value: string): value is keyof actionAssignments {
return value in actionAssignmentsReference; // uh oh return value in actionAssignmentsReference; // uh oh
} }
type actionBinding = [action, ((actionName?: string, state?: Enum.UserInputState, inputObject?: InputObject) => void)]; type actionBinding = [action, (actionName: string, state: Enum.UserInputState, inputObject: InputObject) => void];
type unknownTable = {[numberKey: number]: unknown, [stringKey: string]: unknown}
export function isUnknownTable(thing: unknown): thing is unknownTable {
return typeIs(thing, "table")
}
function getMouseLocation(filterDescendantsInstances: any[]): [Vector3, Vector3, Instance | undefined] { // May be unnecessary function getMouseLocation(filterDescendantsInstances: Instance[]): [Vector3, Vector3, Instance | undefined] {
// May be unnecessary
const hitParams = new RaycastParams(); const hitParams = new RaycastParams();
hitParams.FilterType = Enum.RaycastFilterType.Blacklist; hitParams.FilterType = Enum.RaycastFilterType.Blacklist;
hitParams.FilterDescendantsInstances = filterDescendantsInstances hitParams.FilterDescendantsInstances = filterDescendantsInstances;
const mouseLocation = UserInputService.GetMouseLocation(); const mouseLocation = UserInputService.GetMouseLocation();
const unitRay = CAMERA.ViewportPointToRay(mouseLocation.X, mouseLocation.Y); const unitRay = CAMERA.ViewportPointToRay(mouseLocation.X, mouseLocation.Y);
const cast = Workspace.Raycast(unitRay.Origin, unitRay.Direction.mul(1000), hitParams); const cast = Workspace.Raycast(unitRay.Origin, unitRay.Direction.mul(1000), hitParams);
@ -58,10 +56,17 @@ function getMouseLocation(filterDescendantsInstances: any[]): [Vector3, Vector3,
} }
} }
export function translateInputState(state: any) { export function translateInputState(state: unknown) {
if (enumTypeIs<Enum.UserInputState>(state, Enum.UserInputState)) { // if (enumTypeIs<Enum.UserInputState>(state, Enum.UserInputState)) {
// + Translate to simple boolean if (state === Enum.UserInputState.Begin) {
return true;
} else if (state === Enum.UserInputState.End) {
return false;
} else {
warn("Non-standard UserInputState");
return false;
} }
// }
} }
export interface actionBinder { export interface actionBinder {
@ -70,25 +75,26 @@ export interface actionBinder {
unbindFunctionsFromActions: (actions: action[]) => void; unbindFunctionsFromActions: (actions: action[]) => void;
} }
class actionHandler implements actionBinder { // + Needs a semaphore if concurrency issues arise class actionHandler implements actionBinder {
// + Needs a semaphore if concurrency issues arise
constructor() { constructor() {
// Fortnite // Fortnite
} }
assignInputsToActions(actionAssignments: unknownTable) { assignInputsToActions(actionAssignments: unknownTable) {
let newActionAssignments: actionAssignments = {} const newActionAssignments: actionAssignments = {};
actionAssignmentsReference.forEach(action => { actionAssignmentsReference.forEach((action) => {
const input: unknown = actionAssignments[action] const input: unknown = actionAssignments[action];
if (isValidAction(action) && isValidInput(input)) { if (isValidAction(action) && isValidInput(input)) {
newActionAssignments[action] = input newActionAssignments[action] = input;
} }
}) });
} }
bindFunctionsToActions(actionBindings: actionBinding[]) { bindFunctionsToActions(actionBindings: actionBinding[]) {
const actionAssignments = this.actionAssignments; const actionAssignments = this.actionAssignments;
const boundActions = this.boundActions; const boundActions = this.boundActions;
if (actionAssignments) { if (actionAssignments) {
actionBindings.forEach(actionBinding => { actionBindings.forEach((actionBinding) => {
const action = actionBinding[0] const action = actionBinding[0];
const input = actionAssignments[action]; const input = actionAssignments[action];
if (!boundActions[action] && input) { if (!boundActions[action] && input) {
boundActions[action] = true; boundActions[action] = true;
@ -100,15 +106,15 @@ class actionHandler implements actionBinder { // + Needs a semaphore if concurre
} }
} }
unbindFunctionsFromActions(actions: action[]) { unbindFunctionsFromActions(actions: action[]) {
const boundActions = this.boundActions const boundActions = this.boundActions;
actions.forEach(action => { actions.forEach((action) => {
if (boundActions[action]) { if (boundActions[action]) {
boundActions[action] = undefined; boundActions[action] = undefined;
ContextActionService.UnbindAction(action); ContextActionService.UnbindAction(action);
} else { } else {
// ??? // ???
} }
}) });
} }
actionAssignments?: actionAssignments; actionAssignments?: actionAssignments;

View file

@ -1,10 +1,11 @@
// "init": The main client-side thread. // "init": The main client-side thread.
const Players = game.GetService("Players"); const Players = game.GetService("Players");
const RunService = game.GetService("RunService"); const RunService = game.GetService("RunService");
import { isUnknownTable } from "shared/Shared";
import { bindToServerMessage, messageServer } from "./ClientMessenger"; import { bindToServerMessage, messageServer } from "./ClientMessenger";
import { handleGuiInput, drawGui, closeGui } from "./GuiHandler"; import { handleGuiInput, drawGui, closeGui } from "./GuiHandler";
import { makeEffectRunner, effectRunner } from "./EffectMaker"; import { makeEffectRunner, effectRunner } from "./EffectMaker";
import { makeActionBinder, actionBinder, isUnknownTable } from "./InputHandler"; import { makeActionBinder, actionBinder, translateInputState } from "./InputHandler";
const LOCALPLAYER = Players.LocalPlayer; const LOCALPLAYER = Players.LocalPlayer;
const PLAYERGUI = LOCALPLAYER.WaitForChild("PlayerGui", 1) as PlayerGui; const PLAYERGUI = LOCALPLAYER.WaitForChild("PlayerGui", 1) as PlayerGui;
assert( assert(
@ -23,30 +24,42 @@ function openMainMenu(playerGui: PlayerGui) {
} }
} }
function handlePlayerAction(action: string, state: Enum.UserInputState, inputObject: InputObject) {
messageServer("PlayerAction", [action, translateInputState(state)]);
}
function handleGuiAction() {}
function handleServerMessage(messageType: unknown, messageContent: unknown) { function handleServerMessage(messageType: unknown, messageContent: unknown) {
if (messageType === "init") { if (messageType === "init") {
openMainMenu(PLAYERGUI); openMainMenu(PLAYERGUI);
inMainMenu = true; inMainMenu = true;
mainActionBinder.bindFunctionsToActions([["clicker1", handleGuiAction]]);
} else if (messageType === "enterGame") { } else if (messageType === "enterGame") {
closeGui(PLAYERGUI, "MainMenu"); closeGui(PLAYERGUI, "MainMenu");
inMainMenu = false; inMainMenu = false;
mainActionBinder.unbindFunctionsFromActions(["clicker1"]);
mainActionBinder.bindFunctionsToActions([
["clicker1", handlePlayerAction],
["special1", handlePlayerAction],
["special2", handlePlayerAction],
]);
} else if (messageType === "bindActions") { } else if (messageType === "bindActions") {
if (isUnknownTable(messageContent)) { if (isUnknownTable(messageContent)) {
mainActionBinder.assignInputsToActions(messageContent) mainActionBinder.assignInputsToActions(messageContent);
} }
} }
} }
// Bind functions // Bind functions
const effectRunners: effectRunner[] = []; const effectRunners: effectRunner[] = [];
// + Put stuff in the effectRunners table // + Put stuff in the effectRunners table
RunService.RenderStepped.Connect(function (deltaTime) { RunService.RenderStepped.Connect(function (deltaTime) {
effectRunners.forEach(effectRunner => { effectRunners.forEach((effectRunner) => {
effectRunner.runEffects(deltaTime) effectRunner.runEffects(deltaTime);
});
}); });
})
const mainActionBinder: actionBinder = makeActionBinder() const mainActionBinder: actionBinder = makeActionBinder();
bindToServerMessage(handleServerMessage); bindToServerMessage(handleServerMessage);

View file

@ -1,4 +1,4 @@
import { Input, Output } from "shared/Remotes"; import { Input, Output } from "shared/Shared";
export function bindToClientMessage(functionToBind: Callback) { export function bindToClientMessage(functionToBind: Callback) {
assert(Input?.IsA("RemoteEvent"), 'Remote event "Input" is of incorrect class or nil'); assert(Input?.IsA("RemoteEvent"), 'Remote event "Input" is of incorrect class or nil');
Input.OnServerEvent.Connect(functionToBind); Input.OnServerEvent.Connect(functionToBind);

View file

@ -1,20 +1,21 @@
// "main": This is the core of reality. It serves as the highest-level abstraction. // "main": This is the core of reality. It serves as the highest-level abstraction.
// + Prevent this from coupling with the entity manager, if possible // + Prevent this from coupling with the entity manager, if possible
const Players = game.GetService("Players"); const Players = game.GetService("Players");
import { isUnknownTable } from "shared/Shared";
import { makePlayerStorage, playerStorage, storedPlayer } from "shared/PlayerManager"; import { makePlayerStorage, playerStorage, storedPlayer } from "shared/PlayerManager";
import { bindToClientMessage, messageClient, messageAllClients } from "./ServerMessenger"; import { bindToClientMessage, messageClient, messageAllClients } from "./ServerMessenger";
const playerStorage: playerStorage = makePlayerStorage(); const mainPlayerStorage: playerStorage = makePlayerStorage();
function addPlayer(player: Player) { function addPlayer(player: Player) {
playerStorage.initPlayer(player); mainPlayerStorage.initPlayer(player);
messageClient(player, "init", "idk"); messageClient(player, "init", "idk");
} }
function removePlayer(player: Player) { function removePlayer(player: Player) {
playerStorage.deinitPlayer(player); mainPlayerStorage.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 // 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 handleClientMessage(player: Player, messageType: unknown, messageContent: unknown) { function handleClientMessage(player: Player, messageType: unknown, messageContent: unknown) {
const storedPlayer = playerStorage.fetchPlayer(player); const storedPlayer = mainPlayerStorage.fetchPlayer(player);
if (messageType === "EnterGame") { if (messageType === "EnterGame") {
try { try {
storedPlayer.loadIn(); storedPlayer.loadIn();
@ -27,9 +28,17 @@ function handleClientMessage(player: Player, messageType: unknown, messageConten
messageClient(player, "promptError", errorMessage); messageClient(player, "promptError", errorMessage);
} }
} }
} else if (messageType === "PlayerAction") {
if (isUnknownTable(messageContent)) {
const action = messageContent[0];
const state = messageContent[1];
if (typeIs(action, "string") && typeIs(state, "boolean")) {
storedPlayer.ability(action, state);
}
}
} else if (messageType === "move") { } else if (messageType === "move") {
if (typeIs(messageContent, "Vector3")) { if (typeIs(messageContent, "Vector3")) {
storedPlayer.setPosition(messageContent) storedPlayer.setPosition(messageContent);
} }
} }
} }

4
src/services.d.ts vendored
View file

@ -9,9 +9,7 @@ type effectState = [CFrame, Vector3, Color3, number]; // The number is transpare
type effectEntry = [meshType, EnumItem, effectState[]]; // The enumitem is material type effectEntry = [meshType, EnumItem, effectState[]]; // The enumitem is material
*/ */
// Genuinely require being on both sides - but in the services file? No shot! type unknownTable = { [numberKey: number]: unknown; [stringKey: string]: unknown };
/*interface hookInEntry { /*interface hookInEntry {
name: string; name: string;
guiObject: GuiObject; guiObject: GuiObject;

View file

@ -0,0 +1,4 @@
export interface ability {
use: () => void; // ? Does it pass back information to the entity manager that it does something with? Is an ability really just a transform of entities?
// ... this transform would need the positions and stats of the entities involved as well as potential obstacles
}

View file

@ -1,11 +1,13 @@
// "EntityManager": Create entities objects and deck them out with functions to use. // "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. // + Functions are here, as to avoid storing unecessary data in the server store.
import { makePuppet, puppet, puppetEntry } from "./Puppetmaster"; 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 import { ability } from "./AbilityManager";
type amounts = [health: number, barrier: number] // values used to store an entity's current status (more existential than stats) 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 { export interface entity {
setPosition: (location: Vector3) => void; setPosition: (location: Vector3) => void;
ability: (ability: string, state: boolean) => void;
} }
class entityHandler implements entity { class entityHandler implements entity {
@ -13,15 +15,37 @@ class entityHandler implements entity {
this.baseStats = baseStats; this.baseStats = baseStats;
this.baseAmounts = baseAmounts; this.baseAmounts = baseAmounts;
this.puppet = makePuppet(puppetEntry); this.puppet = makePuppet(puppetEntry);
}; }
setPosition(location: Vector3) { setPosition(location: Vector3) {
this.puppet.movePuppet(location); this.puppet.movePuppet(location);
} }
baseStats: stats; ability(abilityName: string, activated: boolean) {
baseAmounts: amounts; // Health, Barrier (things of indescribable importance) const abilities = this.abilities;
puppet: puppet; if (abilities) {
if (activated) {
const ability = abilities[abilityName];
if (ability !== undefined) {
ability.use();
}
} else {
// + Ability cancellation - perhaps the useAbility inside the entity returns a function to store that the ability watches that ceases the ability when executed
}
}
// Blah blah blah
} }
export function makeEntity(puppetEntry: puppetEntry, baseStats = [100, 1, 16, 0] as stats, baseAmounts = [100, 0] as amounts) { baseStats: stats;
baseAmounts: amounts;
puppet: puppet;
abilities?: {
[key: string]: ability;
};
}
export function makeEntity(
puppetEntry: puppetEntry,
baseStats = [100, 1, 16, 0] as stats,
baseAmounts = [100, 0] as amounts,
) {
return new entityHandler(baseStats, baseAmounts, puppetEntry); return new entityHandler(baseStats, baseAmounts, puppetEntry);
} }

View file

@ -1,12 +1,14 @@
// "PlayerManager": Handle the data of players. This involves receiving them when they arrive, cleaning up after they exit, teleporting them, etc. // "PlayerManager": Handle the data of players. This involves receiving them when they arrive, cleaning up after they exit, teleporting them, etc.
import { makeEntity, entity } from "./EntityManager"; import { makeEntity, entity } from "./EntityManager";
interface saveDataEntry { // + May need to move this to archiver interface saveDataEntry {
// + May need to move this to archiver
placeholder: string; placeholder: string;
} }
export interface storedPlayer { export interface storedPlayer {
teleportToServer: () => void; teleportToServer: () => void;
setPosition: (location: Vector3) => void; setPosition: (location: Vector3) => void;
ability: (ability: string, state: boolean) => void;
loadIn: () => void; loadIn: () => void;
} }
@ -19,20 +21,25 @@ class storedPlayerHandler implements storedPlayer {
teleportToServer() { teleportToServer() {
// + Do checking related to where the player is allowed to go // + 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 // + Teleport player to other server, sending a message to have them load in automatically
}; }
setPosition(location: Vector3) { setPosition(location: Vector3) {
if (this.entity) { if (this.entity) {
this.entity.setPosition(location); this.entity.setPosition(location);
} }
}; }
ability(ability: string, state: boolean) {
if (this.entity) {
this.entity.ability(ability, state);
}
}
loadIn() { loadIn() {
this.entity = makeEntity(["Character", this.player]); this.entity = makeEntity(["Character", this.player]);
// + Give the entity the stats it's supposed to have, load from save data maybe? // + Give the entity the stats it's supposed to have, load from save data maybe?
}; }
player: Player; player: Player;
inMainMenu: boolean; inMainMenu: boolean;
// + Other data that is unique to players but does not persist between sessions // + Other data that is unique to players but does not persist between sessions
saveData: saveDataEntry; saveData: saveDataEntry; // This gets synced with the actual datastore
entity?: entity; entity?: entity;
} }
@ -43,18 +50,18 @@ export interface playerStorage {
} }
class playerStorageHandler implements playerStorage { class playerStorageHandler implements playerStorage {
constructor() {}; constructor() {}
initPlayer(player: Player) { initPlayer(player: Player) {
this.playerStorageArray[player.UserId] = new storedPlayerHandler(player); this.playerStorageArray[player.UserId] = new storedPlayerHandler(player);
// + Load player's datastore into server store // + Load player's datastore into server store
}; }
deinitPlayer(player: Player) { deinitPlayer(player: Player) {
const entry = this.playerStorageArray[player.UserId]; const entry = this.playerStorageArray[player.UserId];
assert(entry, "Trying to remove entry of player " + player.DisplayName + ", but entry does not exist!"); 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) // ? 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 // + 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 return undefined; // A nil entry to replace the entry to be wiped and maybe a success value in a wrapper
}; }
fetchPlayer(player: Player) { fetchPlayer(player: Player) {
return this.playerStorageArray[player.UserId]; return this.playerStorageArray[player.UserId];
} }

View file

@ -2,3 +2,7 @@
const ReplicatedStorage = game.GetService("ReplicatedStorage"); const ReplicatedStorage = game.GetService("ReplicatedStorage");
export const Input = ReplicatedStorage.WaitForChild("Input", 1); export const Input = ReplicatedStorage.WaitForChild("Input", 1);
export const Output = ReplicatedStorage.WaitForChild("Output", 1); export const Output = ReplicatedStorage.WaitForChild("Output", 1);
export function isUnknownTable(thing: unknown): thing is unknownTable {
return typeIs(thing, "table");
}