Server structure defined + functional shift

! This commit will not compile or build or anything fancy like that
+ Server structure defined as having two concurrent actors, the global scene and
  the player manager, which chatter every frame
+ Entity manager and scene manager expanded
- Removed a bunch of shared state by not using classes
This commit is contained in:
loplkc loplkc 2022-05-08 11:29:10 -04:00
parent 436cbde19d
commit 052a37b36f
9 changed files with 429 additions and 61 deletions

7
src/game/globalScene.ts Normal file
View file

@ -0,0 +1,7 @@
import { sceneTemplate } from "shared/SceneManager"
export const globalSceneTemplate: sceneTemplate = {
sceneComplete() {
return false
},
onCompletion: []
} as const

View file

@ -1,5 +1,5 @@
import { Input, Output } from "shared/Shared";
export function bindToClientMessage(functionToBind: Callback) {
export function bindToClientMessage(functionToBind: (player: Player, ...messageContents: unknown[]) => void) {
assert(Input?.IsA("RemoteEvent"), 'Remote event "Input" is of incorrect class or nil');
Input.OnServerEvent.Connect(functionToBind);
}

View file

@ -1,20 +1,70 @@
// "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");
// "main": Initializes all state and handles the real world.
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 { makePlayerStorage, playerStorage, storedPlayer } from "shared/PlayerManager";
import { bindToClientMessage, messageClient, messageAllClients } from "./ServerMessenger";
const mainPlayerStorage: playerStorage = makePlayerStorage();
// 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"
// 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);
}
Players.PlayerAdded.Connect(function(player: Player) {
messagePlayerManager(["initPlayer", player])
});
Players.PlayerRemoving.Connect(function(player: Player) {
messagePlayerManager(["deinitPlayer", 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
function addPlayer(player: Player) {
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
}
busy = false
});
//const playerManager: actor<playerManagerRequest> = initPlayerManager(eventManager);
/* function addPlayer(player: Player) {
mainPlayerStorage.initPlayer(player);
messageClient(player, "init", "idk");
}
function removePlayer(player: 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
function handleClientMessage(player: Player, messageType: unknown, messageContent: unknown) {
}*/
// function handleClientMessage(player: Player, messageType: unknown, messageContent: unknown) {
//playerManager.message(["PlayerInput"])
/*
const storedPlayer = mainPlayerStorage.fetchPlayer(player);
if (messageType === "EnterGame") {
try {
@ -41,8 +91,5 @@ function handleClientMessage(player: Player, messageType: unknown, messageConten
storedPlayer.setPosition(messageContent);
}
}
}
// Action phase
Players.PlayerAdded.Connect(addPlayer);
Players.PlayerRemoving.Connect(removePlayer);
bindToClientMessage(handleClientMessage);
*/
// }

2
src/services.d.ts vendored
View file

@ -10,6 +10,8 @@ type effectEntry = [meshType, EnumItem, effectState[]]; // The enumitem is mater
*/
type unknownTable = { [numberKey: number]: unknown; [stringKey: string]: unknown };
type success<Wrapped> = [true, Wrapped] | [false, string]
type placeholder = "foo"
/*interface hookInEntry {
name: string;
guiObject: GuiObject;

View file

@ -1,18 +1,229 @@
// "EntityManager": Create entities objects and deck them out with functions to use.
// "EntityManager": Create entity objects and apply transformations to them.
// + Functions are here, as to avoid storing unecessary data in the server store.
import { makePuppet, puppet, puppetEntry } from "./Puppetmaster";
import { ability } from "./AbilityManager";
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)
// import { ability } from "./AbilityManager";
// I spent a lot of time thinking about the names for these types and they still suck
type modifiers = readonly [[maxHealth: number, defense: number], [power: number, speed: number, resistance: number]]; // values used for calculation, only modified by buffs and debuffs (the first group is additive, the second multiplicative)
type statuses = readonly [health: number, barrier: number]; // values used to store an entity's current status (more existential than stats)
type statusEffect = readonly [string, modifiers, (entity: entity) => entity];
export interface entityController {
setPosition: (location: Vector3) => void;
useAbility: (abilityName: string, activating: boolean) => void;
}
export interface entityModifier {}
/* Prototype
entityTransform(setOfAllEntities: entity[], actingEntity: entity, ability destructured to {hitBoxList, modificationList})
const entitiesEligible = getAffectedEntities(setOfAllEntities, actingEntity)
const entitiesHit = entitiesEligible ? getHitEntities(entitiesEligible, hitBoxList) : entitiesEligible // This would be a generic function that accepts an array or a single hitbox
const entityAlteration = entitiesHit ? applyEntityToAlteration(entityTransform, actingEntity) : entitiesHit
return modifiedEntities = entityAlteration ? alterEntity(entitiesHit, entityAlteration) : entitiesHit // entitiesHit could be an array of arrays containing entities or just an array of entities
useAbilityPart(abilityPart: abilityPart, setOfAllEntities: entity[], thisEntity: entity)
const transformedEntities = ableToUse ? entityTranform(setOfAllEntities, thisEntity) : ableToUse
const privateMessaged = transformedEntities ? messageClient(UNIMPLEMENTED) : transformedEntities
const globalMessaged = privateMessaged ? messageAllClients(UNIMPLEMENTED) : privateMessaged
return [globalMessaged, transformedEntities, schedulingForNextAbilityPart] // Might need to add some kind of reason here, not just an indiscriminate error
class entityHandler implements entityController {
useAbility(now: number, setOfAllEntities: entity[], thisEntity: entity, ability: ability)
const completeAbility = getAbilityToUse(ability, thisEntity) // Can return either [false, error] or [true, value]
const ableToUse = completeAbility ? canUseAbility(now, completeAbility) : completeAbility // canUseAbility should return the first abilityPart
return resultOfFirstAbilityPart = ableToUse ? useAbilityPart(ability.firstPart, setOfAllEntities, thisEntity) : [ableToUse]
*/
interface entityStats {
statuses: statuses // Health and stuff that change constantly
baseModifiers: modifiers // Base modifiers that change only when the player switches weapons or something
immunities: {
[statusEffectName: string]: boolean | undefined
}
statusEffects: statusEffect[] // Burn, poison, etc.
}
interface entityId {
readonly name: string
readonly team: "players" | "enemies",
readonly isMinion: boolean
}
export interface entity {
readonly id: entityId
stats: entityStats
puppet: puppet
}
type entityTransform = (entity: entity) => entity
// interface entityTransformTemplate {
type abuiloga = (entityPerformingId: entityId, entityReceivingId: entityId) => entityTransformTemplate | false
/*
allies: {
minions: {
}
}
opponents: {
}
affectsMinions: boolean,
excludesSelf: boolean,
affectsSameTeam: boolean,
*///}
type entityTransformType = "heal" | "attack"
interface entityTransformTemplate {
extraFunction: (entityPerformingTransform: entity, entityReceivingTransform: entity) => boolean
thingus: entityTransformType
magnitude: number, // Positive for heal, negative for damage
affectsHealth: boolean
affectsBarrier: boolean
statusEffectsGranted: placeholder[], // Stuff like burn, slow, stun, etc.*/
}
// type entityTransformTemplate = [entityTransformEligibilityTemplate, entityTransformApplicationTemplate]
/*function isEligibleForTransform(entityPerformingTransform: entity, entityReceivingTransform: entity, eligibilityTemplate: entityTransformEligibilityTemplate): false | [boolean] { // This function sucks
const entityReceivingId = entityReceivingTransform.id
const onSameTeam = entityPerformingTransform.id.team == entityReceivingId.team
if (onSameTeam && !eligibilityTemplate.affectsSameTeam) {
return false
}
if (entityPerformingTransform == entityReceivingTransform && eligibilityTemplate.excludesSelf) {
return false
}
if (entityReceivingId.isMinion && !eligibilityTemplate.affectsMinions) {
return false
}
if (!eligibilityTemplate.extraFunction(entityPerformingTransform, entityReceivingTransform)) {
return false
}
return [onSameTeam]
}*/
function applyEntityToAttack(entityModifiers: modifiers, entityStatusEffects: statusEffect[], magnitude: number): number {
const attack = applyModifiersToAttack(magnitude, entityModifiers);
// + Apply status effects of performing entity to attack (e.g. weaken)
return attack
}
function applyAttackToEntityStatuses(entityStatuses: statuses, entityModifiers: modifiers, entityStatusEffects: statusEffect[], attack: number, affectsHealth: boolean, affectsBarrier: boolean): statuses { // Not sure if this should return a whole entity
// + Apply status effects of receiving entity to damage (e.g. armor break)
const damage = applyModifiersToAttack(attack, entityModifiers);
const newStatuses = applyDamageToStatuses(entityStatuses, damage, affectsHealth, affectsBarrier);
return entityStatuses
}
function makeEntityTransform(entityPerformingTransformId: entityId, abuiloga: abuiloga): (entityReceivingTransform: entity) => entityStats {
return function(entityReceivingTransform: entity): entityStats {
const entityTransformTemplate = abuiloga(entityPerformingTransformId, entityReceivingTransform.id);
const newStats = transformEntityStatuses( // All of this stuff should be packed into one object, maybe a new entityTransformTemplate
entityReceivingTransform.stats.statuses,
finalCalculatedMaxHealth,
finalCalculatedMaxBarrier,
entityTransformTemplate.thingus,
finalCalculatedMagnitude,
finalCalculatedAffectsHealth,
finalCalculatedAffectsBarrier) // L
// const newEntity = entityTransformTemplate ? applyEntityTransform(entityPerformingTransform, entityReceivingTransform, onSameTeam, template[1]) : entityReceivingTransform;
return newStats
}
}
/*interface entityTransform extends entityTransformTemplate {
specificEntity?: string,
team: "players" | "enemies",
}*/
type abilityTemplate = [
[entityTransformTemplate[], number] // I guess the number is a delay until the next part or something
]
/*type ability = [
[entityTransform[], number]
]*/
// scene, player, aimLocation
// const ability = getAbility
// const entities = applyAbilityToScene(scene, ability)
function applyDamageToHealth(health: number, damage: number): [newHealth: number, excessDamage: number] { // Needs testing
const newHealth = health - damage
if (newHealth < 0) {
return [0, -newHealth]
} else {
return [newHealth, 0]
}
}
const applyDamageToBarrier = applyDamageToHealth
function applyDamageToStatuses(statuses: statuses, damage: number, affectsHealth: boolean, affectsBarrier: boolean): statuses {
if (affectsBarrier) {
const [newBarrier, excessBarrierDamage] = applyDamageToBarrier(statuses[1], damage)
const [newHealth, excessHealthDamage] = applyDamageToHealth(statuses[0], affectsHealth? excessBarrierDamage : 0)
return [newHealth, newBarrier]
} else if (affectsHealth) {
const [newHealth, excessHealthDamage] = applyDamageToHealth(statuses[0], damage)
return [newHealth, statuses[1]]
} else {
return statuses
}
}
function applyHealToHealth(currentHealth: number, heal: number, maxHealth: number): [newHealth: number, excessHeal: number] {
const newHealth = currentHealth + heal
if (newHealth > maxHealth) {
return [maxHealth, newHealth - maxHealth]
} else {
return [newHealth, 0]
}
}
const applyHealToBarrier = applyHealToHealth
function applyHealToStatuses(statuses: statuses, heal: number, maxHealth: number, maxBarrier: number, affectsHealth: boolean, affectsBarrier: boolean): statuses {
if (affectsHealth) {
const [newHealth, excessHealth] = applyHealToHealth(statuses[0], heal, maxHealth)
const [newBarrier, excessBarrier] = applyHealToBarrier(statuses[1], affectsBarrier ? excessHealth : 0, maxBarrier)
return [newHealth, newBarrier]
} else if (affectsBarrier) { // Using a branch isn't optimal, but as of writing I can't think of a better solution
const [newBarrier, excessBarrier] = applyHealToBarrier(statuses[1], heal, maxBarrier)
return [statuses[0], newBarrier]
} else {
return statuses
}
}
function transformEntityStatuses(entityStatuses: statuses, maxHealth: number, maxBarrier: number, transformType: string, magnitude: number, affectsHealth: boolean, affectsBarrier: boolean): statuses {
if (transformType == "heal") {
const newStatuses = applyHealToStatuses(entityStatuses, magnitude, maxHealth, maxBarrier, affectsHealth, affectsBarrier) // More chaining method calls...
return newStatuses
} else if (transformType == "attack") {
const newStatuses = applyDamageToStatuses(entityStatuses, magnitude, affectsHealth, affectsBarrier)
return newStatuses
} else {
throw "Unimplemented transformType " + transformType
}
}// Damage should come before status effects are applied
function applyPowerToAttack(attack: number, power: number) {
return attack*power
}
function applyModifiersToAttack(attack: number, modifiers: modifiers): number { // Get arguments they use (a bit sketchy)
return applyPowerToAttack(attack, modifiers[1][0])
}
function applyResistanceToDamage(damage: number, resistance: number): number {
return damage*(1 - resistance)
}
function applyDefenseToDamage(damage: number, defense: number): number {
return damage - defense
}
function applyModifiersToDamage(damage: number, modifiers: modifiers): number {
return applyResistanceToDamage(applyDefenseToDamage(damage, modifiers[0][1]), modifiers[1][2])
}
function modifiersArrayFunction(applyModifiersToNumber: (number: number, modifiers: modifiers) => number): (number: number, modifiers: modifiers[]) => number {
return function(number: number, modifiersArray: modifiers[]) {
let newNumber = number
modifiersArray.forEach(function(modifiers: modifiers) {
newNumber = applyModifiersToNumber(newNumber, modifiers)
})
return newNumber
}
}
const applyModifiersArrayToAttack = modifiersArrayFunction(applyModifiersToAttack);
const applyModifiersArrayToDamage = modifiersArrayFunction(applyModifiersToDamage);
function getEntityByName(entityList: entity[], entityName: string): success<entity> {
entityList.forEach(function(entity: entity) {
if (entity.id.name == entityName) { // Chained method calls are cringe
return [true, entity]
}
})
return [false, "Entity not found"]
}
function getAbility() {}
function useAbility(entityList: entity[], entityUsing: entity, aim: Vector3): [entity[], placeholder[]] {
}
/*class entityHandler implements entityController {
constructor(baseStats: stats, baseAmounts: amounts, puppetEntry: puppetEntry) {
this.baseStats = baseStats;
this.baseAmounts = baseAmounts;
@ -57,8 +268,8 @@ class entityHandler implements entityController {
} = {};
baseStats: stats;
baseAmounts: amounts;
}
}*/
/*
export function makeEntity(
puppetEntry: puppetEntry,
baseStats = [100, 1, 16, 0] as stats,
@ -66,3 +277,14 @@ export function makeEntity(
) {
return new entityHandler(baseStats, baseAmounts, puppetEntry);
}
class entityManager extends actorClass<entityManagerRequest> {
constructor() {
super()
}
entities: entity[] = [];
}
export function initEntityManager() {
return new entityManager();
}
*/

View file

@ -1,19 +0,0 @@
// "The": Handle events.
// WORST CONDITION RigHT NOW
// Consider that this is the second module coupled to the EntityManager
import { makeEntity, entityController } from "./EntityManager";
interface event {
winEvents?: event[]; // A list of events that need to return true (in sequence) to complete this event
winEntities?: entityController[]; // 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();
const endTime = 2;
while (!complete) {
if (event.timeout === 2) {
complete = true;
}
}
}

View file

@ -1,17 +1,39 @@
// "PlayerManager": Handle the data of players. This involves receiving them when they arrive, cleaning up after they exit, teleporting them, etc.
import { makeEntity, entityController } from "./EntityManager";
// 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.
// import { makeEntity, entityController } from "./EntityManager";
//import { actorClass } from "shared/Shared"
import { sceneManagerRequest } from "./SceneManager"
export type playerManagerRequest = ["initPlayer" | "deinitPlayer", Player] | ["playerInput", ...unknown[]] | ["foo", "bar"]
interface saveDataEntry {
// + May need to move this to archiver
placeholder: string;
}
interface storedPlayer {
// currentScene: event //Not sure about this
/*initPlayer: (player: Player) => void;
deinitPlayer: (player: Player) => void;
fetchPlayer: (player: Player) => storedPlayer;*/
}
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 function initPlayerStorage() {
return [{},{}];
}
/* Deprecated
export interface storedPlayer {
teleportToServer: () => void;
setPosition: (location: Vector3) => void;
ability: (ability: string, state: boolean) => void;
loadIn: () => void;
}
//setPosition: (location: Vector3) => void;
//ability: (ability: string, state: boolean) => void;
//loadIn: () => void;
}*/
/* Deprecated (may have useful information)
class storedPlayerHandler implements storedPlayer {
constructor(player: Player) {
this.player = player;
@ -40,16 +62,11 @@ class storedPlayerHandler implements storedPlayer {
inMainMenu: boolean;
// + Other data that is unique to players but does not persist between sessions
saveData: saveDataEntry; // This gets synced with the actual datastore
entity?: entityController;
}
//entity?: entityController;
}*/
export interface playerStorage {
initPlayer: (player: Player) => void;
deinitPlayer: (player: Player) => void;
fetchPlayer: (player: Player) => storedPlayer;
}
class playerStorageHandler implements playerStorage {
/*class playerStorageHandler implements playerStorage {
constructor() {}
initPlayer(player: Player) {
this.playerStorageArray[player.UserId] = new storedPlayerHandler(player);
@ -66,8 +83,4 @@ class playerStorageHandler implements playerStorage {
return this.playerStorageArray[player.UserId];
}
playerStorageArray: storedPlayerHandler[] = [];
}
export function makePlayerStorage() {
return new playerStorageHandler();
}
}*/

View file

@ -0,0 +1,82 @@
// "The": Handle events.
import { entity, getEntityByName/*entityManagerRequest /*makeEntity, entityController*/ } from "./EntityManager";
import { applyRequestsToPlayerStorage, playerManagerRequest } from "./PlayerManager"
export type sceneManagerRequest = [Player, "useAbility", Vector3] | [Player, "foo", "bar"]
type endConditionFunction = (containedScenes: scene[], containedEntities: entity[], timeElapsed: number) => boolean
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
}
export interface scene extends sceneTemplate {
containedScenes?: {
[sceneName: string]: scene | undefined
}; // Scenes within this scene that are isolated from each other and can be run in parallel
// Not necessarily "A list of events that need to return true (in sequence) to complete this event", but such events would go there
containedEntities: entity[]; // A list of entities that need to die to complete the event
players: {
[userId: number]: [inThisScene: true] | [inThisScene: false, subScene: string] | undefined
}
//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<string | false> {
let playerSceneLocation = scene.players[userId];
if (!playerSceneLocation) {
return [false, "Player does not exist"]; // Some kind of error needs to go here
} else if (playerSceneLocation[0]) {
return [true, false];
} else {
return [true, playerSceneLocation[1]]
}
}
function applyRequestToScene(scene: scene, now: number, request: sceneManagerRequest): [scene, playerManagerRequest[]] {
const playerSceneResult = getPlayerSceneName(scene, request[0].UserId)
if (!playerSceneResult[0]) {
return [scene, []]; // Some kind of error needs to go here
}
const playerSceneName = playerSceneResult[1]
if (!playerSceneName) {
const playerEntity = getEntityByName
if (request[1] == "useAbility") {
return useAbility(scene, )
} else {
throw("Invalid request to SceneManager")
}
} else { // Case needs testing once it becomes relevant (this code is a mess)
const containedScenes = scene.containedScenes
assert(containedScenes)
let playerScene = containedScenes[playerSceneName]
assert(playerScene)
const sceneRequestResult = applyRequestToScene(playerScene, now, request) // There should be no stack overflow unless you nest too many scenes
containedScenes[playerSceneName] = sceneRequestResult[0] // This is questionably object-oriented
scene.containedScenes = containedScenes
return [scene, sceneRequestResult[1]]
}
}
export function applyRequestsToScene(scene: scene, now: number, requests: sceneManagerRequest[]): success<[scene, playerManagerRequest[]]> {
try {
let newScene: scene = scene
let outgoingRequests: playerManagerRequest[] = []
requests.forEach(function(request: sceneManagerRequest) {
const sceneRequestResult = applyRequestToScene(newScene, now, request);
newScene = sceneRequestResult[0]
outgoingRequests = [...outgoingRequests, ...sceneRequestResult[1]]
})
return [true, [newScene, outgoingRequests]];
}
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;
};

View file

@ -1,4 +1,4 @@
// "Remotes"
// "Shared"
const ReplicatedStorage = game.GetService("ReplicatedStorage");
export const Input = ReplicatedStorage.WaitForChild("Input", 1);
export const Output = ReplicatedStorage.WaitForChild("Output", 1);
@ -6,3 +6,17 @@ export const Output = ReplicatedStorage.WaitForChild("Output", 1);
export function isUnknownTable(thing: unknown): thing is unknownTable {
return typeIs(thing, "table");
}
/*export class actorClass<MessageType> implements actor<MessageType> {
message(message: MessageType) {
this.mailbox.push(message)
if (!this.busy) {
this.busy = true
}
}
readMessage() {
return this.mailbox.shift()
}
mailbox: MessageType[] = [];
busy = false;
}*/