From e70d96a953a6d37affa9e4188e0e9925b252679c Mon Sep 17 00:00:00 2001 From: ZareMate <0.zaremate@gmail.com> Date: Mon, 2 Mar 2026 22:23:43 +0100 Subject: [PATCH] init --- .gitignore | 11 + README.md | 17 + aftman.toml | 7 + coreography.rbxl | Bin 0 -> 118006 bytes default.project.json | 27 + rokit.toml | 8 + selene.toml | 1 + src/client/Start.client.luau | 15 + src/server/Modules/Classes/Round/Grid.luau | 186 +++ .../Modifier/SkibidiModifier.luau | 22 + .../Round/ModifierManager/Modifier/init.luau | 18 + .../Classes/Round/ModifierManager/init.luau | 39 + src/server/Modules/Classes/Round/Timer.luau | 35 + src/server/Modules/Classes/Round/init.luau | 101 ++ src/server/Modules/RoundManager.luau | 28 + src/server/Modules/RoundManager.meta.json | 7 + src/server/Start.server.luau | 16 + src/shared/ModuleLoader/ActorForClient.rbxm | Bin 0 -> 2181 bytes src/shared/ModuleLoader/ActorForServer.rbxm | Bin 0 -> 2130 bytes .../ModuleLoader/ParallelModuleLoader.luau | 83 + .../ModuleLoader/RelocatedTemplate.luau | 10 + src/shared/ModuleLoader/init.luau | 759 +++++++++ src/shared/ModuleLoader/init.meta.json | 10 + src/shared/Modules/Client/GetCharacter.luau | 57 + .../Modules/Client/GetCharacter.meta.json | 10 + src/shared/Modules/Client/UI/Display.luau | 22 + .../Modules/Client/UI/Display.meta.json | 10 + src/shared/Modules/Client/UI/Lock.luau | 42 + src/shared/Modules/Client/UI/Lock.meta.json | 10 + src/shared/Modules/Data/.gitkeep | 0 src/shared/Modules/Utilities/FerrUtils.luau | 198 +++ .../Observers/_observeAllAttributes.luau | 90 ++ .../Observers/_observeAttribute.luau | 101 ++ .../Observers/_observeCharacter.luau | 82 + .../Utilities/Observers/_observeChildren.luau | 104 ++ .../Observers/_observeDescendants.luau | 99 ++ .../Utilities/Observers/_observePlayer.luau | 80 + .../Utilities/Observers/_observeProperty.luau | 91 ++ .../Utilities/Observers/_observeTag.luau | 196 +++ .../Modules/Utilities/Observers/init.luau | 11 + .../Utilities/Observers/init.meta.json | 5 + src/shared/Modules/Utilities/Signal.luau | 180 +++ src/shared/Modules/Utilities/t.luau | 1350 +++++++++++++++++ src/shared/Modules/Utilities/throttle.luau | 27 + .../Modules/Utilities/throttle.meta.json | 5 + .../Modules/Utilities/waitWithTimeout.luau | 35 + .../Utilities/waitWithTimeout.meta.json | 5 + wally.toml | 7 + 48 files changed, 4217 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 aftman.toml create mode 100644 coreography.rbxl create mode 100644 default.project.json create mode 100644 rokit.toml create mode 100644 selene.toml create mode 100644 src/client/Start.client.luau create mode 100644 src/server/Modules/Classes/Round/Grid.luau create mode 100644 src/server/Modules/Classes/Round/ModifierManager/Modifier/SkibidiModifier.luau create mode 100644 src/server/Modules/Classes/Round/ModifierManager/Modifier/init.luau create mode 100644 src/server/Modules/Classes/Round/ModifierManager/init.luau create mode 100644 src/server/Modules/Classes/Round/Timer.luau create mode 100644 src/server/Modules/Classes/Round/init.luau create mode 100644 src/server/Modules/RoundManager.luau create mode 100644 src/server/Modules/RoundManager.meta.json create mode 100644 src/server/Start.server.luau create mode 100644 src/shared/ModuleLoader/ActorForClient.rbxm create mode 100644 src/shared/ModuleLoader/ActorForServer.rbxm create mode 100644 src/shared/ModuleLoader/ParallelModuleLoader.luau create mode 100644 src/shared/ModuleLoader/RelocatedTemplate.luau create mode 100644 src/shared/ModuleLoader/init.luau create mode 100644 src/shared/ModuleLoader/init.meta.json create mode 100644 src/shared/Modules/Client/GetCharacter.luau create mode 100644 src/shared/Modules/Client/GetCharacter.meta.json create mode 100644 src/shared/Modules/Client/UI/Display.luau create mode 100644 src/shared/Modules/Client/UI/Display.meta.json create mode 100644 src/shared/Modules/Client/UI/Lock.luau create mode 100644 src/shared/Modules/Client/UI/Lock.meta.json create mode 100644 src/shared/Modules/Data/.gitkeep create mode 100644 src/shared/Modules/Utilities/FerrUtils.luau create mode 100644 src/shared/Modules/Utilities/Observers/_observeAllAttributes.luau create mode 100644 src/shared/Modules/Utilities/Observers/_observeAttribute.luau create mode 100644 src/shared/Modules/Utilities/Observers/_observeCharacter.luau create mode 100644 src/shared/Modules/Utilities/Observers/_observeChildren.luau create mode 100644 src/shared/Modules/Utilities/Observers/_observeDescendants.luau create mode 100644 src/shared/Modules/Utilities/Observers/_observePlayer.luau create mode 100644 src/shared/Modules/Utilities/Observers/_observeProperty.luau create mode 100644 src/shared/Modules/Utilities/Observers/_observeTag.luau create mode 100644 src/shared/Modules/Utilities/Observers/init.luau create mode 100644 src/shared/Modules/Utilities/Observers/init.meta.json create mode 100644 src/shared/Modules/Utilities/Signal.luau create mode 100644 src/shared/Modules/Utilities/t.luau create mode 100644 src/shared/Modules/Utilities/throttle.luau create mode 100644 src/shared/Modules/Utilities/throttle.meta.json create mode 100644 src/shared/Modules/Utilities/waitWithTimeout.luau create mode 100644 src/shared/Modules/Utilities/waitWithTimeout.meta.json create mode 100644 wally.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..39de410 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Project place file +/shared-reality.rbxlx + +# Roblox Studio lock files +/*.rbxlx.lock +/*.rbxl.lock + +Packages/ +ServerPackages/ +sourcemap.json +wally.lock diff --git a/README.md b/README.md new file mode 100644 index 0000000..4ce28b8 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# coreography + +[Rojo](https://github.com/rojo-rbx/rojo) 7.7.0-rc.1. + +## Getting Started + +To use Rojo: + +Install [Rojo VS Code extention](https://marketplace.visualstudio.com/items?itemName=evaera.vscode-rojo) + +Next, open `coreography` in Roblox Studio and start the Rojo server: + +```bash +rojo serve +``` + +For more help, check out [the Rojo documentation](https://rojo.space/docs). diff --git a/aftman.toml b/aftman.toml new file mode 100644 index 0000000..c2de6f5 --- /dev/null +++ b/aftman.toml @@ -0,0 +1,7 @@ +# This file lists tools managed by Aftman, a cross-platform toolchain manager. +# For more information, see https://github.com/LPGhatguy/aftman + +# To add a new tool, add an entry to this table. +[tools] +rojo = "rojo-rbx/rojo@7.7.0-rc.1" +wally = "UpliftGames/wally@0.3.2" \ No newline at end of file diff --git a/coreography.rbxl b/coreography.rbxl new file mode 100644 index 0000000000000000000000000000000000000000..210898bd62fda195358cfb77a60702a86a4fe889 GIT binary patch literal 118006 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 in a terminal. + +[tools] +rojo = "rojo-rbx/rojo@7.7.0-rc.1" +wally = "UpliftGames/wally@0.3.2" \ No newline at end of file diff --git a/selene.toml b/selene.toml new file mode 100644 index 0000000..1f1e170 --- /dev/null +++ b/selene.toml @@ -0,0 +1 @@ +std = "roblox" \ No newline at end of file diff --git a/src/client/Start.client.luau b/src/client/Start.client.luau new file mode 100644 index 0000000..e16c01e --- /dev/null +++ b/src/client/Start.client.luau @@ -0,0 +1,15 @@ +--!strict +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Remotes = ReplicatedStorage.Remote +--local rev_PlayerLoaded = Remotes.PlayerLoaded +local ModuleLoader = require(ReplicatedStorage.Shared.ModuleLoader) + +-- you can optionally modify settings (also change it via the attributes on ModuleLoader) +ModuleLoader.ChangeSettings({ + FOLDER_SEARCH_DEPTH = 1, + YIELD_THRESHOLD = 10, + VERBOSE_LOADING = false, + WAIT_FOR_SERVER = true, +}) +-- pass any containers for your custom services to the Start() function +ModuleLoader.Start(script) diff --git a/src/server/Modules/Classes/Round/Grid.luau b/src/server/Modules/Classes/Round/Grid.luau new file mode 100644 index 0000000..b1d3343 --- /dev/null +++ b/src/server/Modules/Classes/Round/Grid.luau @@ -0,0 +1,186 @@ +local Grid = {} + +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local Assets = ReplicatedStorage.Assets +local GridPart = Assets.Models.Grid + +local GridCenter = workspace:WaitForChild("GridCenter") + +Grid.__index = Grid +type self = { + SizeX : number, + SizeY : number, + SizeZ : number, + CellSize : Vector3, + ValidCells : {[string] : {Player : Player}}, + + Center : Part, +} + +type CoordinateConfig = { + [string] : { + Depth : number, + Ignore : boolean + } +} + +export type Grid = typeof(setmetatable({} :: self, Grid)) + +function Grid.new(sizeX, sizeY, sizeZ, cellSize) + local self = setmetatable({}, Grid) + + self.SizeX = sizeX + self.SizeY = sizeY + self.SizeZ = sizeZ + self.CellSize = cellSize + + + self.Cells = {} + + + + self.Center = workspace.GridCenter + self.Center.Size = Vector3.new(sizeX,.5,sizeZ) + self.Center.Position = Vector3.new(0, 2, 0) + return self +end + +function Grid:SetValidCells(positions : {Vector3}) + +end + +function Grid:Translate(pos : Vector3) + +end + +function Grid:GetWorldPositionFromCell(x,y,z) + local snappedPos = self.Center.Position + Vector3.new( + (x - self.SizeX/2 - 0.5) * self.CellSize, + (y - self.SizeY/2 - 0.5) * self.CellSize, + (z - self.SizeZ/2 - 0.5) * self.CellSize + ) + return snappedPos +end +function Grid:GetCellFromWorldPosition(worldPos) + local relative = worldPos - self.Center.Position + + local x = math.floor(relative.X / self.CellSize + self.SizeX/2) + 1 + local y = math.floor(relative.Y / self.CellSize + self.SizeY/2) + 1 + local z = math.floor(relative.Z / self.CellSize + self.SizeZ/2) + 1 + + return x, y, z +end +function Grid:SnapPlayer(player) + local character = player.Character + if not character then return end + + local root = character:FindFirstChild("HumanoidRootPart") + if not root then return end + + local x, y, z = self:GetCellFromWorldPosition(root.Position) + + if not self.Cells[x] + or not self.Cells[x][y] + or not self.Cells[x][y][z] then + return + end + + print(x,y,z) + self:Visualize(5,5,5) + + task.wait(5) + + if self.Cells[x][y][z].player then + return -- cell occupied + end + + self.Cells[x][y][z].player = player + + local snappedPos = self:GetWorldPositionFromCell(x,y,z) + + local sizeChar = Vector3.new(2,4,2) + + local difX = (sizeChar.X / 2) + local minX = x - difX + local maxX = x + difX + + local difY = (sizeChar.Y / 2) + local minY = y - difY + local maxY = y + difY + + local difZ = (sizeChar.Z / 2) + local minZ = z - difZ + local maxZ = z + difZ + + for offsetX = minX,maxX,1 do + for offsetY = minY,maxY,1 do + for offsetZ = minZ,maxZ,1 do + print(offsetX,offsetY,offsetZ) + self.Cells[offsetX][offsetY][offsetZ].player = player + self:Visualize(offsetX,offsetY,offsetZ) + end + end + end + + + root.CFrame = CFrame.new(snappedPos) + root.Anchored = true +end + +function Grid:Visualize(x,y,z) + local part = Instance.new("Part") + part.Anchored = true + part.CanCollide = false + part.CanTouch = false + part.Size = Vector3.new(self.CellSize,self.CellSize,self.CellSize) + local translatedPos = self:GetWorldPositionFromCell(x,y,z) + part.Position = translatedPos + part.Parent = workspace +end + +function Grid:ClearPlayers() + for x = 1, self.SizeX do + for y = 1, self.SizeY do + for z = 1, self.SizeZ do + self.Cells[x][y][z].player = nil + end + end + end +end + +function Grid:SetValidCell(x, y, z, state) + if self.Cells[x] + and self.Cells[x][y] + and self.Cells[x][y][z] then + self.Cells[x][y][z].isValid = state + end +end + +function Grid:GetAccuracy() + local valid = 0 + local filled = 0 + + for x = 1, self.SizeX do + for y = 1, self.SizeY do + for z = 1, self.SizeZ do + local cell = self.Cells[x][y][z] + + if cell.isValid then + valid += 1 + if cell.player then + filled += 1 + end + end + end + end + end + + if valid == 0 then return 0 end + + return (filled / valid) * 100 +end + + + +return Grid diff --git a/src/server/Modules/Classes/Round/ModifierManager/Modifier/SkibidiModifier.luau b/src/server/Modules/Classes/Round/ModifierManager/Modifier/SkibidiModifier.luau new file mode 100644 index 0000000..5a69f66 --- /dev/null +++ b/src/server/Modules/Classes/Round/ModifierManager/Modifier/SkibidiModifier.luau @@ -0,0 +1,22 @@ +local SkibidiModifier = {} + +local Grid = require(script.Parent.Parent.Parent.Grid) + +local Modifier = require(script.Parent) + +SkibidiModifier.__index = SkibidiModifier + +function SkibidiModifier.new() + local self = setmetatable(Modifier.new(),SkibidiModifier) + self.Priority = 1 + self.Type = "Generic" + return self +end + +function SkibidiModifier:BeforePlace(grid : Grid.Grid) + print(grid) + return true +end + + +return SkibidiModifier diff --git a/src/server/Modules/Classes/Round/ModifierManager/Modifier/init.luau b/src/server/Modules/Classes/Round/ModifierManager/Modifier/init.luau new file mode 100644 index 0000000..5db3ca3 --- /dev/null +++ b/src/server/Modules/Classes/Round/ModifierManager/Modifier/init.luau @@ -0,0 +1,18 @@ +local Modifier = {} +Modifier.__index = Modifier + +function Modifier.new() + local self = setmetatable({}, Modifier) + + self.Priority = 0 + self.Type = "Generic" + + return self +end + +function Modifier:OnRoundStart(grid) end +function Modifier:BeforePlace(grid, player, x, y, z) return true end +function Modifier:AfterFreeze(grid) end +function Modifier:OnRoundEnd(grid) end + +return Modifier \ No newline at end of file diff --git a/src/server/Modules/Classes/Round/ModifierManager/init.luau b/src/server/Modules/Classes/Round/ModifierManager/init.luau new file mode 100644 index 0000000..24dea55 --- /dev/null +++ b/src/server/Modules/Classes/Round/ModifierManager/init.luau @@ -0,0 +1,39 @@ +local ModifierManager = {} +ModifierManager.__index = ModifierManager + +function ModifierManager.new(modifiers) + local self = setmetatable({}, ModifierManager) + + self.ActiveModifiers = {require(script.Modifier.SkibidiModifier).new()} + + return self +end + +function ModifierManager:AddModifier(mod) + table.insert(self.ActiveModifiers, mod) + + + table.sort(self.ActiveModifiers, function(a, b) + return a.Priority > b.Priority + end) +end + +function ModifierManager:RunHook(hookName, grid, ...) + for _, mod in ipairs(self.ActiveModifiers) do + local func = mod[hookName] + + if func then + local result = func(mod, grid, ...) + + if hookName == "BeforePlace" and result == false then + return false + end + end + end + + return true +end + + + +return ModifierManager \ No newline at end of file diff --git a/src/server/Modules/Classes/Round/Timer.luau b/src/server/Modules/Classes/Round/Timer.luau new file mode 100644 index 0000000..b0023c7 --- /dev/null +++ b/src/server/Modules/Classes/Round/Timer.luau @@ -0,0 +1,35 @@ +local Timer = {} + + +Timer.__index = Timer +type self = { + Time : number +} + +local function getPlayers() + return game.Players:GetPlayers() +end + +export type Timer = typeof(setmetatable({} :: self, Timer)) + + +function Timer.new() + local self = setmetatable({},Timer) + + return self +end + +function Timer.Tick(self : Timer,number) + self.Time += number +end + +function Timer.Set(self : Timer,number) + self.Time = number +end + +function Timer.ShowTimer(self : Timer) + return tostring(math.round(self.Time)) +end + + +return Timer diff --git a/src/server/Modules/Classes/Round/init.luau b/src/server/Modules/Classes/Round/init.luau new file mode 100644 index 0000000..3042eac --- /dev/null +++ b/src/server/Modules/Classes/Round/init.luau @@ -0,0 +1,101 @@ +local Round = {} + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Values = ReplicatedStorage.Values +local DisplayText = Values.DisplayText + +local ModifierManager = require(script.ModifierManager) +local Grid = require(script.Grid) +local Timer = require(script.Timer) + +Round.__index = Round + +--private + +local function getPlayers() + return game.Players:GetPlayers() +end +local function intermissiontext(t : Timer.Timer) + return "Intermission(" .. t.ShowTimer(t) .. ")" +end + +local RunService = game:GetService("RunService") +local MIN_PLAYERS = 3 + +local NOT_ENOUGH_PLAYERS = 1 +local INTERMISSION = 2 +local PLAYING_PLACING_PEOPLE = 3 +local PLAYING_WALL_CHECK = 4 + + +function Round.new() + local self = setmetatable({},Round) + + self.State = NOT_ENOUGH_PLAYERS + self.Timer = Timer.new() + self.ModifierManager = ModifierManager.new() + self.Grid = nil :: Grid.Grid + + return self +end + +function Round:RoundStart() + self.Grid = Grid.new(150,100,150,1) + + for _,plr in pairs(getPlayers()) do + local char = plr.Character + char:MoveTo(self.Grid.Center.Position) + plr:SetAttribute("Playing",true) + end +end + +function Round:Tick(dt) + local t = self.Timer + local players = getPlayers() + if #players < MIN_PLAYERS then + if not RunService:IsStudio() and (self.State == INTERMISSION or self.State == NOT_ENOUGH_PLAYERS) then + self.State = NOT_ENOUGH_PLAYERS + DisplayText.Value = "Not enough players!" + t.Set(t,10) + return + end + end + if self.State == NOT_ENOUGH_PLAYERS then + self.State = INTERMISSION + t.Set(t,10) + DisplayText.Value = intermissiontext(t) + return + end + + t.Tick(t,-dt) + + local ended = t.Time <= 0 + + if self.State == INTERMISSION then + DisplayText.Value = intermissiontext(t) + + if ended then + self.State = PLAYING_PLACING_PEOPLE + DisplayText.Value = "Place yourself to match the shape!" + self:RoundStart() + t.Set(t,30) + end + + return + end + if self.State == PLAYING_PLACING_PEOPLE then + if ended then + DisplayText.Value = "Here it comes!" + self.State = PLAYING_WALL_CHECK + t.Set(t,15) + end + + return + end + +end + + + + +return Round diff --git a/src/server/Modules/RoundManager.luau b/src/server/Modules/RoundManager.luau new file mode 100644 index 0000000..80102aa --- /dev/null +++ b/src/server/Modules/RoundManager.luau @@ -0,0 +1,28 @@ +local RoundManager = {} + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Remotes = ReplicatedStorage.Remote + +local rev_LockPlayer = Remotes.LockPlayer + +local RunService = game:GetService("RunService") + +local Round = require("./Classes/Round") + +local g = Round.new() + +function lockPlayer(plr) + local result = g.ModifierManager:RunHook("BeforePlace") + print(result) + g.Grid:SnapPlayer(plr) +end + +function RoundManager:Init() + RunService.Heartbeat:Connect(function(dt) + g.Tick(g,dt) + end) + + rev_LockPlayer.OnServerEvent:Connect(lockPlayer) +end + +return RoundManager diff --git a/src/server/Modules/RoundManager.meta.json b/src/server/Modules/RoundManager.meta.json new file mode 100644 index 0000000..4e6c32c --- /dev/null +++ b/src/server/Modules/RoundManager.meta.json @@ -0,0 +1,7 @@ +{ + "properties": { + "Tags": [ + "LOAD_MODULE" + ] + } +} \ No newline at end of file diff --git a/src/server/Start.server.luau b/src/server/Start.server.luau new file mode 100644 index 0000000..bc324f2 --- /dev/null +++ b/src/server/Start.server.luau @@ -0,0 +1,16 @@ +--!strict +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local ModuleLoader = require(ReplicatedStorage.Shared.ModuleLoader) + +-- you can optionally modify settings (also change it via the attributes on ModuleLoader) +ModuleLoader.ChangeSettings({ + FOLDER_SEARCH_DEPTH = 1, + YIELD_THRESHOLD = 10, + VERBOSE_LOADING = false, + WAIT_FOR_SERVER = true, +}) +-- pass any containers for your custom services to the Start() function +ModuleLoader.Start(script) + +local ServerScriptService = game:GetService("ServerScriptService") +local ServerModules = ServerScriptService.Server.Modules diff --git a/src/shared/ModuleLoader/ActorForClient.rbxm b/src/shared/ModuleLoader/ActorForClient.rbxm new file mode 100644 index 0000000000000000000000000000000000000000..8a639e9af334f482f5b882f5ad9598109f4de6f9 GIT binary patch literal 2181 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 literal 0 HcmV?d00001 diff --git a/src/shared/ModuleLoader/ParallelModuleLoader.luau b/src/shared/ModuleLoader/ParallelModuleLoader.luau new file mode 100644 index 0000000..71771e5 --- /dev/null +++ b/src/shared/ModuleLoader/ParallelModuleLoader.luau @@ -0,0 +1,83 @@ +--!strict +--@author: crusherfire +--@date: 5/8/24 +--[[@description: + General code for parallel scripts. +]] +-- Since this module will be required by scripts in separate actors, these variables won't be shared! +local require = require +local requiredModule +local function onRequireModule(script: BaseScript, module: ModuleScript) + local success, result = pcall(function() + return require(module) + end) + + if not success then + warn("Parallel module errored!\n", result) + script:SetAttribute("Errored", true) + script:SetAttribute("Required", true) + return + end + requiredModule = result + script:SetAttribute("Required", true) +end + +local function onInitModule(script: BaseScript) + if script:GetAttribute("Errored") then + warn("Unable to init errored module!") + script:SetAttribute("Initialized", true) + return + end + if not requiredModule then + warn("Told to load module that does not exist!") + script:SetAttribute("Initialized", true) + return + end + if not requiredModule.Init then + script:SetAttribute("Initialized", true) + return + end + + local success, result = pcall(function() + requiredModule:Init() + end) + + if not success then + warn("Parallel module errored!\n", result) + script:SetAttribute("Errored", true) + script:SetAttribute("Initialized", true) + return + end + script:SetAttribute("Initialized", true) +end + +local function onStartModule(script: BaseScript) + if script:GetAttribute("Errored") then + warn("Unable to start errored module!") + script:SetAttribute("Started", true) + return + end + if not requiredModule then + warn("Told to start module that does not exist!") + script:SetAttribute("Started", true) + return + end + if not requiredModule.Start then + script:SetAttribute("Started", true) + return + end + + local success, result = pcall(function() + requiredModule:Start() + end) + + if not success then + warn("Parallel module errored!\n", result) + script:SetAttribute("Errored", true) + script:SetAttribute("Started", true) + return + end + script:SetAttribute("Started", true) +end + +return { onRequireModule = onRequireModule, onInitModule = onInitModule, onStartModule = onStartModule } diff --git a/src/shared/ModuleLoader/RelocatedTemplate.luau b/src/shared/ModuleLoader/RelocatedTemplate.luau new file mode 100644 index 0000000..1f012b7 --- /dev/null +++ b/src/shared/ModuleLoader/RelocatedTemplate.luau @@ -0,0 +1,10 @@ +--!strict +local ServerScriptService = game:GetService("ServerScriptService") + +local RELOCATED_FOLDER = ServerScriptService:FindFirstChild("RELOCATED_MODULES") +assert(RELOCATED_FOLDER, "ServerScriptService missing 'RELOCATE_MODULES' folder") + +local module = RELOCATED_FOLDER:FindFirstChild(script.Name) +assert(module, `RELOCATED_MODULES folder missing module '{script.Name}'`) + +return require(module) :: any \ No newline at end of file diff --git a/src/shared/ModuleLoader/init.luau b/src/shared/ModuleLoader/init.luau new file mode 100644 index 0000000..a29658a --- /dev/null +++ b/src/shared/ModuleLoader/init.luau @@ -0,0 +1,759 @@ +--!strict +--[[ + ---- + crusherfire's Module Loader! + 08/05/2025 + ---- + + -- FEATURES -- + "LoaderPriority" number attribute: + Set this attribute on a ModuleScript to modify the loading priority. Larger number == higher priority. + + "RelocateToServerScriptService" boolean attribute: + Relocates a module to ServerScriptService and leave a pointer in its place to hide server code from clients. + This should only be performed on server-only modules that you want to organize within the same containers in Studio. + + "ClientOnly" or "ServerOnly" boolean attributes: + Allows you to restrict a module from being loaded on a specific run context. + + "Parallel" boolean attribute: + Requires the module from another script within its own actor for executing code in parallel. + + "IgnoreLoader" boolean attribute: + Allows you to prevent a module from being loaded. + + Supports CollectionService tags by placing the tag name (defined by LoaderTag attribute) on ModuleScript instances. + + -- LOADER ATTRIBUTE SETTINGS -- + ClientWaitForServer: Client avoids starting the module loading process until the server finishes. + FolderSearchDepth: How deep the loader will search for modules to load in a folder. '1' represents the direct children. + LoaderTag: Tag to be set on modules you want to load that are not within loaded containers. + VerboseLoading: If messages should be output that document the loading process + YieldThreshold: How long to wait (in seconds) before displaying warning messages when a particular module is yielidng for too long + UseCollectionService: If the loader should search for modules to load based on LoaderTag + + -- DEFAULT FILTERING BEHAVIOR -- + Respects "ClientOnly", "ServerOnly", and "IgnoreLoader" attributes. + + Modules that are not a direct child of a given container or whose ancestry are not folders + that lead back to a container will not be loaded when the FolderSearchDepth is a larger value. + + NOTE: + If you do not like this default filtering behavior, you can pass your own filtering predicate to the StartCustom() function + and define your own behavior. Otherwise, use the Start() function for the default behavior! + -------------- +]] + +----------------------------- +-- SERVICES -- +----------------------------- +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local RunService = game:GetService("RunService") +local CollectionService = game:GetService("CollectionService") +local ServerScriptService = game:GetService("ServerScriptService") +local Players = game:GetService("Players") + +----------------------------- +-- VARIABLES -- +----------------------------- +local parallelModuleLoader = script.ParallelModuleLoader +local actorForServer: Actor? = script:FindFirstChild("ActorForServer") :: any +local actorForClient: Actor? = script:FindFirstChild("ActorForClient") :: any +local isClient = RunService:IsClient() +local require = require +local loadedEvent: RemoteEvent +if isClient then + loadedEvent = script:WaitForChild("LoadedEvent") +else + loadedEvent = Instance.new("RemoteEvent") + loadedEvent.Name = "LoadedEvent" + loadedEvent.Parent = script +end + +local started = false + +local tracker = { + Load = {} :: { [ModuleScript]: any }, + Init = {} :: { [ModuleScript]: boolean }, + Start = {} :: { [ModuleScript]: boolean } +} + +local trackerForActors = { + Load = {} :: { [ModuleScript]: Actor }, + Init = {}, + Start = {} +} + +export type LoaderSettings = { + FOLDER_SEARCH_DEPTH: number?, + YIELD_THRESHOLD: number?, + VERBOSE_LOADING: boolean?, + WAIT_FOR_SERVER: boolean?, + USE_COLLECTION_SERVICE: boolean?, +} + +export type KeepModulePredicate = (container: Instance, module: ModuleScript) -> (boolean) + +-- CONSTANTS -- +local SETTINGS: LoaderSettings = { + FOLDER_SEARCH_DEPTH = script:GetAttribute("FolderSearchDepth"), + YIELD_THRESHOLD = script:GetAttribute("YieldThreshold"), -- how long until the module starts warning for a module that is taking too long + VERBOSE_LOADING = script:GetAttribute("VerboseLoading"), + WAIT_FOR_SERVER = script:GetAttribute("ClientWaitForServer"), + USE_COLLECTION_SERVICE = script:GetAttribute("UseCollectionService"), +} + +local PRINT_IDENTIFIER = if isClient then "[C]" else "[S]" +local LOADED_IDENTIFIER = if isClient then "Client" else "Server" +local ACTOR_PARENT = if isClient then Players.LocalPlayer.PlayerScripts else game:GetService("ServerScriptService") +local TAG = script:GetAttribute("LoaderTag") +local RELOCATED_MODULES do + if RunService:IsServer() then + RELOCATED_MODULES = Instance.new("Folder") + RELOCATED_MODULES.Name = "RELOCATED_MODULES" + RELOCATED_MODULES.Parent = ServerScriptService + end +end + +----------------------------- +-- PRIVATE FUNCTIONS -- +----------------------------- + +-- !YIELDS! +local function waitForEither(eventYes: RBXScriptSignal, eventNo: RBXScriptSignal): boolean + local thread = coroutine.running() + + local connection1: any = nil + local connection2: any = nil + + connection1 = eventYes:Once(function(...) + if connection1 == nil then + return + end + + connection1:Disconnect() + connection2:Disconnect() + connection1 = nil + connection2 = nil + + if coroutine.status(thread) == "suspended" then + task.spawn(thread, true, ...) + end + end) + + connection2 = eventNo:Once(function(...) + if connection2 == nil then + return + end + + connection1:Disconnect() + connection2:Disconnect() + connection1 = nil + connection2 = nil + + if coroutine.status(thread) == "suspended" then + task.spawn(thread, false, ...) + end + end) + + return coroutine.yield() +end + +local function copy(t: T, deep: boolean?): T + if not deep then + return (table.clone(t :: any) :: any) :: T + end + local function deepCopy(object: any) + assert(typeof(object) == "table", "Expected table for deepCopy!") + -- Returns a deep copy of the provided table. + local newObject = setmetatable({}, getmetatable(object)) -- Clone metaData + + for index: any, value: any in object do + if typeof(value) == "table" then + newObject[index] = deepCopy(value) + continue + end + + newObject[index] = value + end + + return newObject + end + return deepCopy(t :: any) :: T +end + +local function reconcile(src: S, template: T): S & T + assert(type(src) == "table", "First argument must be a table") + assert(type(template) == "table", "Second argument must be a table") + + local tbl = table.clone(src) + + for k, v in template do + local sv = src[k] + if sv == nil then + if type(v) == "table" then + tbl[k] = copy(v, true) + else + tbl[k] = v + end + elseif type(sv) == "table" then + if type(v) == "table" then + tbl[k] = reconcile(sv, v) + else + tbl[k] = copy(sv, true) + end + end + end + + return (tbl :: any) :: S & T +end + +-- Returns a new array that is the result of array1 and array2 +local function mergeArrays(array1: {[number]: any}, array2: {[number]: any}) + local length = #array2 + local newArray = table.clone(array2) + for i, v in ipairs(array1) do + newArray[length + i] = v + end + return newArray +end + +local function filter(t: { T }, predicate: (T, any, { T }) -> boolean): { T } + assert(type(t) == "table", "First argument must be a table") + assert(type(predicate) == "function", "Second argument must be a function") + local newT = table.create(#t) + if #t > 0 then + local n = 0 + for i, v in t do + if predicate(v, i, t) then + n += 1 + newT[n] = v + end + end + else + for k, v in t do + if predicate(v, k, t) then + newT[k] = v + end + end + end + return newT +end + +-- Returns the 'depth' of descendant in the child hierarchy of root. +-- If the descendant is not found in root, then this function will return 0. +local function getDepthInHierarchy(descendant: Instance, root: Instance): number + local depth = 0 + local current: Instance? = descendant + while current and current ~= root do + current = current.Parent + depth += 1 + end + if not current then + depth = 0 + end + return depth +end + +local function findAllFromClass(class: string, searchIn: Instance, searchDepth: number?): { any } + assert(class and typeof(class) == "string", "class is invalid or nil") + assert(searchIn and typeof(searchIn) == "Instance", "searchIn is invalid or nil") + + local foundObjects = {} + + if searchDepth then + for _, object in pairs(searchIn:GetDescendants()) do + if object:IsA(class) and getDepthInHierarchy(object, searchIn) <= searchDepth then + table.insert(foundObjects, object) + end + end + else + for _, object in pairs(searchIn:GetDescendants()) do + if object:IsA(class) then + table.insert(foundObjects, object) + end + end + end + + return foundObjects +end + +local function keepModule(container: Instance, module: ModuleScript): boolean + if module:GetAttribute("ClientOnly") and RunService:IsServer() then + return false + elseif module:GetAttribute("ServerOnly") and RunService:IsClient() then + return false + elseif module:GetAttribute("IgnoreLoader") then + return false + end + local ancestor = module.Parent + while ancestor do + if ancestor == container then + -- The ancestry should eventually lead to the container (if ancestors were always folders) + return true + elseif not ancestor:IsA("Folder") then + return false + end + ancestor = ancestor.Parent + end + return false +end + +local function newPrint(...) + print(PRINT_IDENTIFIER, ...) +end + +local function newWarn(...) + warn(PRINT_IDENTIFIER, ...) +end + +local function loadModule(module: ModuleScript) + -- attempts to relocate the module, if eligible + local function attemptRelocate(module: ModuleScript) + if RunService:IsClient() then + return + end + if not module:GetAttribute("RelocateToServerScriptService") then + return + end + if module:IsDescendantOf(ServerScriptService) then + warn(`RelocateToServerScriptService attribute is enabled on module '{module:GetFullName()}' that's already in ServerScriptService`) + return + end + local clone = script.RelocatedTemplate:Clone() + clone.Name = module.Name + clone:SetAttribute("ServerOnly", true) + clone.Parent = module.Parent + module.Parent = RELOCATED_MODULES + end + + if module:GetAttribute("Parallel") then + local actorTemplate = if isClient then actorForClient else actorForServer + + if actorTemplate == nil then + newWarn(`Parallel module {module.Name} requested but no Actor template is configured - loading normally`) + else + -- This module needs to be run in parallel, so create new actor and script. + local newActorSystem = actorTemplate:Clone() + local loaderClone = parallelModuleLoader:Clone() + loaderClone.Parent = newActorSystem + local actorScript: BaseScript = newActorSystem:FindFirstChildWhichIsA("BaseScript") :: any + + actorScript.Enabled = true + actorScript.Name = `Required{module.Name}` + newActorSystem.Parent = ACTOR_PARENT + + if not actorScript:GetAttribute("Loaded") then + actorScript:GetAttributeChangedSignal("Loaded"):Wait() + end + + newActorSystem:SendMessage("RequireModule", module) + + if SETTINGS.VERBOSE_LOADING then + newPrint(("Loading PARALLEL module '%s'"):format(module.Name)) + end + + local startTime = tick() + if not actorScript:GetAttribute("Required") then + actorScript:GetAttributeChangedSignal("Required"):Wait() + end + local endTime = tick() + + if SETTINGS.VERBOSE_LOADING and not actorScript:GetAttribute("Errored") then + newPrint(`>> Loaded PARALLEL module {module.Name}`, ("(took %.3f seconds)"):format(endTime - startTime)) + elseif actorScript:GetAttribute("Errored") then + newWarn( + `>> Failed to load PARALLEL module {module.Name}`, + ("(took %.3f seconds)"):format(endTime - startTime) + ) + end + + -- relocate after loading to maintain relative paths within modules + attemptRelocate(module) + + trackerForActors.Load[module] = newActorSystem + tracker.Load[module] = true + tracker.Init[module] = true + tracker.Start[module] = true + return + end + end + + if SETTINGS.VERBOSE_LOADING then + newPrint(("Loading module '%s'"):format(module.Name)) + end + local mainThread = coroutine.running() + local startTime = tick() + local endTime + local executionSuccess, errMsg = false, "" + local thread: thread = task.spawn(function() + debug.setmemorycategory(`Module::{module.Name}`) + local success, result = xpcall(function() + return require(module) + end, debug.traceback) + debug.resetmemorycategory() + if success then + tracker.Load[module] = result + if result.Init then + tracker.Init[module] = false + end + if result.Start then + tracker.Start[module] = false + end + executionSuccess = true + + -- relocate after loading to maintain relative paths within modules + attemptRelocate(module) + else + errMsg = result + end + endTime = tick() + if coroutine.status(mainThread) == "suspended" then + task.spawn(mainThread) + end + end) + if not endTime then + endTime = tick() + end + if coroutine.status(thread) == "suspended" then + local loopThread = task.spawn(function() + task.wait(SETTINGS.YIELD_THRESHOLD) + while true do + if coroutine.status(thread) == "suspended" then + newWarn(`>> Loading Module '{module.Name}' is taking a while!`, ("(%.3f seconds elapsed)"):format(tick() - startTime)) + end + task.wait(5) + end + end) + coroutine.yield() + if coroutine.status(loopThread) ~= "dead" then + task.cancel(loopThread) + end + end + + if SETTINGS.VERBOSE_LOADING and executionSuccess then + newPrint(`>> Loaded module {module.Name}`, ("(took %.3f seconds)"):format(endTime - startTime)) + elseif not executionSuccess then + newWarn(`>> Failed to load module {module.Name}`, ("(took %.3f seconds)\n%s"):format(endTime - startTime, errMsg)) + end +end + +local function initializeModule(loadedModule, module: ModuleScript) + if trackerForActors.Load[module] then + local actorScript: BaseScript = trackerForActors.Load[module]:FindFirstChildWhichIsA("BaseScript") :: any + trackerForActors.Load[module]:SendMessage("InitModule") + + if SETTINGS.VERBOSE_LOADING then + newPrint(("Initializing PARALLEL module '%s'"):format(actorScript.Name)) + end + + local startTime = tick() + if not actorScript:GetAttribute("Initialized") then + actorScript:GetAttributeChangedSignal("Initialized"):Wait() + end + local endTime = tick() + + if SETTINGS.VERBOSE_LOADING and not actorScript:GetAttribute("Errored") then + newPrint(`>> Initialized PARALLEL module {actorScript.Name}`, ("(took %.3f seconds)"):format(endTime - startTime)) + elseif actorScript:GetAttribute("Errored") then + newWarn(`>> Failed to init PARALLEL module {actorScript.Name}`, ("(took %.3f seconds)"):format(endTime - startTime)) + end + return + end + + if not loadedModule.Init then + return + end + + if SETTINGS.VERBOSE_LOADING then + newPrint(("Initializing module '%s'"):format(module.Name)) + end + local mainThread = coroutine.running() + local startTime = tick() + local endTime + local executionSuccess, errMsg = false, "" + local thread: thread = task.spawn(function() + local success, err = xpcall(function() + loadedModule:Init() + end, function(err) + return `{err}\n{debug.traceback()}` + end) + executionSuccess = success + if success then + tracker.Init[module] = true + else + errMsg = err + end + endTime = tick() + if coroutine.status(mainThread) == "suspended" then + task.spawn(mainThread) + end + end) + if not endTime then + endTime = tick() + end + if coroutine.status(thread) == "suspended" then + local loopThread = task.spawn(function() + task.wait(SETTINGS.YIELD_THRESHOLD) + while true do + if coroutine.status(thread) == "suspended" then + newWarn(`>> :Init() for Module '{module.Name}' is taking a while!`, ("(%.3f seconds elapsed)"):format(tick() - startTime)) + end + task.wait(5) + end + end) + coroutine.yield() + if coroutine.status(loopThread) ~= "dead" then + task.cancel(loopThread) + end + end + + if SETTINGS.VERBOSE_LOADING and executionSuccess then + newPrint(`>> Initialized module {module.Name}`, ("(took %.3f seconds)"):format(endTime - startTime)) + elseif not executionSuccess then + newWarn(`>> Failed to init module {module.Name}`, ("(took %.3f seconds)\n%s"):format(endTime - startTime, errMsg)) + end +end + +local function startModule(loadedModule, module: ModuleScript) + if trackerForActors.Load[module] then + local actorScript: BaseScript = trackerForActors.Load[module]:FindFirstChildWhichIsA("BaseScript") :: any + trackerForActors.Load[module]:SendMessage("StartModule") + + if SETTINGS.VERBOSE_LOADING then + newPrint(("Starting PARALLEL module '%s'"):format(actorScript.Name)) + end + + local startTime = tick() + if not actorScript:GetAttribute("Started") then + actorScript:GetAttributeChangedSignal("Started"):Wait() + end + local endTime = tick() + + if SETTINGS.VERBOSE_LOADING and not actorScript:GetAttribute("Errored") then + newPrint(`>> Started PARALLEL module {actorScript.Name}`, ("(took %.3f seconds)"):format(endTime - startTime)) + elseif actorScript:GetAttribute("Errored") then + newWarn(`>> Failed to start PARALLEL module {actorScript.Name}`, ("(took %.3f seconds)"):format(endTime - startTime)) + end + return + end + + if not loadedModule.Start then + return + end + + if SETTINGS.VERBOSE_LOADING then + newPrint(("Starting module '%s'"):format(module.Name)) + end + local mainThread = coroutine.running() + local startTime = tick() + local endTime + local executionSuccess, errMsg = false, "" + local thread: thread = task.spawn(function() + local success, err = xpcall(function() + loadedModule:Start() + end, function(err) + return `{err}\n{debug.traceback()}` + end) + executionSuccess = success + if success then + tracker.Start[module] = true + else + errMsg = err + end + endTime = tick() + if coroutine.status(mainThread) == "suspended" then + task.spawn(mainThread) + end + end) + if not endTime then + endTime = tick() + end + if coroutine.status(thread) == "suspended" then + local loopThread = task.spawn(function() + task.wait(SETTINGS.YIELD_THRESHOLD) + while true do + if coroutine.status(thread) == "suspended" then + newWarn(`>> :Start() for Module '{module.Name}' is taking a while!`, ("(%.3f seconds elapsed)"):format(tick() - startTime)) + end + task.wait(5) + end + end) + coroutine.yield() + if coroutine.status(loopThread) ~= "dead" then + task.cancel(loopThread) + end + end + + if SETTINGS.VERBOSE_LOADING and executionSuccess then + newPrint(`>> Started module {module.Name}`, ("(took %.3f seconds)"):format(endTime - startTime)) + elseif not executionSuccess then + newWarn(`>> Failed to start module {module.Name}`, ("(took %.3f seconds)\n%s"):format(endTime - startTime, errMsg)) + end +end + +-- Gets all modules to be loaded in order. +local function getModules(containers: { Instance }): { ModuleScript } + local totalModules = {} + for _, container in ipairs(containers) do + local modules = findAllFromClass("ModuleScript", container, SETTINGS.FOLDER_SEARCH_DEPTH) + modules = filter(modules, function(module) + return keepModule(container, module) + end) + totalModules = mergeArrays(totalModules, modules) + end + if SETTINGS.USE_COLLECTION_SERVICE and TAG ~= "" then + for _, module in CollectionService:GetTagged(TAG) do + if not module:IsA("ModuleScript") then + warn(`item: {module} with tag: {TAG} is not a module script!`) + continue + end + if not keepModule(module.Parent, module) then + continue + end + if table.find(totalModules, module) then + continue + end + table.insert(totalModules, module) + end + end + + table.sort(totalModules, function(a, b) + local aPriority = a:GetAttribute("LoaderPriority") or 0 + local bPriority = b:GetAttribute("LoaderPriority") or 0 + + return aPriority > bPriority + end) + return totalModules +end + +----------------------------- +-- MAIN -- +----------------------------- + +--[[ + Starts the loader with the default module filtering behavior. +]] +local function start(...: Instance) + assert(not started, "attempt to start module loader more than once") + started = true + local containers = {...} + if isClient and SETTINGS.WAIT_FOR_SERVER and not workspace:GetAttribute("ServerLoaded") then + workspace:GetAttributeChangedSignal("ServerLoaded"):Wait() + end + + if SETTINGS.VERBOSE_LOADING then + newWarn("=== LOADING MODULES ===") + local modules = getModules(containers) + for _, module in modules do + loadModule(module) + end + + newWarn("=== INITIALIZING MODULES ===") + for _, module in modules do + if not tracker.Load[module] then + continue + end + initializeModule(tracker.Load[module], module) + end + + newWarn("=== STARTING MODULES ===") + for _, module in modules do + if not tracker.Load[module] then + continue + end + startModule(tracker.Load[module], module) + end + + newWarn("=== LOADING FINISHED ===") + else + local modules = getModules(containers) + for _, module in modules do + loadModule(module) + end + for _, module in modules do + if not tracker.Load[module] then + continue + end + initializeModule(tracker.Load[module], module) + end + for _, module in modules do + if not tracker.Load[module] then + continue + end + startModule(tracker.Load[module], module) + end + end + + workspace:SetAttribute(`{LOADED_IDENTIFIER}LoadedTimestamp`, workspace:GetServerTimeNow()) + workspace:SetAttribute(`{LOADED_IDENTIFIER}Loaded`, true) + if RunService:IsClient() then + loadedEvent:FireServer() + end +end + +--[[ + Starts the loader with your own custom module filtering behavior for determining what modules should be loaded. +]] +local function startCustom(shouldKeep: KeepModulePredicate, ...: Instance) + keepModule = shouldKeep + start(...) +end + +--[[ + Returns if the client finished loading, initializing, and starting all modules. +]] +local function isClientLoaded(player: Player): boolean + return player:GetAttribute("_ModulesLoaded") == true +end + +--[[ + Returns if the server finished loading, initializing, and starting all modules. +]] +local function isServerLoaded(): boolean + return workspace:GetAttribute("ServerLoaded") == true +end + +--[[ + !YIELDS! + Yields until the client has loaded all their modules. + Returns true if loaded or returns false if player left. +]] +local function waitForLoadedClient(player: Player): boolean + if not player:GetAttribute("_ModulesLoaded") then + return waitForEither(player:GetAttributeChangedSignal("_ModulesLoaded"), player:GetPropertyChangedSignal("Parent")) + end + return true +end + +--[[ + Modify the default settings determined by the attributes on the module loader. + The given settings are reconciled with the current settings. +]] +local function changeSettings(settings: LoaderSettings) + SETTINGS = reconcile(settings, SETTINGS) +end + +--[[ + Errors if the server is not loaded yet. +]] +local function getServerLoadedTimestamp() + assert(isServerLoaded(), "server is not loaded yet!") + return workspace:GetAttribute("ServerLoadedTimestamp") +end + +if not isClient then + loadedEvent.OnServerEvent:Connect(function(player) + player:SetAttribute("_ModulesLoaded", true) + end) +end + +return { + Start = start, + StartCustom = startCustom, + ChangeSettings = changeSettings, + IsServerLoaded = isServerLoaded, + IsClientLoaded = isClientLoaded, + WaitForLoadedClient = waitForLoadedClient, + GetServerLoadedTimestamp = getServerLoadedTimestamp +} \ No newline at end of file diff --git a/src/shared/ModuleLoader/init.meta.json b/src/shared/ModuleLoader/init.meta.json new file mode 100644 index 0000000..ca35435 --- /dev/null +++ b/src/shared/ModuleLoader/init.meta.json @@ -0,0 +1,10 @@ +{ + "attributes": { + "ClientWaitForServer": true, + "FolderSearchDepth": 3.0, + "LoaderTag": "LOAD_MODULE", + "UseCollectionService": true, + "VerboseLoading": false, + "YieldThreshold": 10.0 + } +} \ No newline at end of file diff --git a/src/shared/Modules/Client/GetCharacter.luau b/src/shared/Modules/Client/GetCharacter.luau new file mode 100644 index 0000000..5a10d8a --- /dev/null +++ b/src/shared/Modules/Client/GetCharacter.luau @@ -0,0 +1,57 @@ +local GetCharacter = {} + +local localPlayer = game.Players.LocalPlayer + +local RunService = game:GetService("RunService") + + + +local childAddedEvent +local otherEvents = {} + +function CharacterAdded(char : Model) + + if childAddedEvent then + childAddedEvent:Disconnect() + childAddedEvent = nil + end + if otherEvents then + for i,v in pairs(otherEvents) do + v:Disconnect() + end + table.clear(otherEvents) + end + shared.Character = char + shared.Head = char:WaitForChild("Head") + shared.Humanoid = char:WaitForChild("Humanoid") + shared.HumanoidRootPart = char:WaitForChild("HumanoidRootPart") + + for i,v in pairs(char:GetDescendants()) do + --hidePart(v) + end + childAddedEvent = char.DescendantAdded:Connect(function(bodyPart) + --hidePart(bodyPart) + end) + + +end + +function hidePart(bodyPart) + if (bodyPart:IsA('BasePart')) then + local event = bodyPart:GetPropertyChangedSignal('LocalTransparencyModifier'):Connect(function() + bodyPart.LocalTransparencyModifier = 1 + end) + + bodyPart.LocalTransparencyModifier = 1 + + table.insert(otherEvents,event) + end +end + +function GetCharacter:Start() + CharacterAdded(localPlayer.Character or localPlayer.CharacterAdded:Wait()) + localPlayer.CharacterAdded:Connect(CharacterAdded) + +end + +return GetCharacter diff --git a/src/shared/Modules/Client/GetCharacter.meta.json b/src/shared/Modules/Client/GetCharacter.meta.json new file mode 100644 index 0000000..e2625d7 --- /dev/null +++ b/src/shared/Modules/Client/GetCharacter.meta.json @@ -0,0 +1,10 @@ +{ + "properties": { + "Tags": [ + "LOAD_MODULE" + ] + }, + "attributes": { + "ClientOnly": true + } +} \ No newline at end of file diff --git a/src/shared/Modules/Client/UI/Display.luau b/src/shared/Modules/Client/UI/Display.luau new file mode 100644 index 0000000..e970b9f --- /dev/null +++ b/src/shared/Modules/Client/UI/Display.luau @@ -0,0 +1,22 @@ +local Display = {} + +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local Values = ReplicatedStorage:WaitForChild("Values") +local DisplayValue = Values:WaitForChild("DisplayText") + +local localPlayer = game.Players.LocalPlayer +local PlayerGui = localPlayer.PlayerGui + +local MainGui = PlayerGui:WaitForChild("MainGui") +local DisplayTextLabel = MainGui:WaitForChild("DisplayText") + +--acabar despues + +function Display:Init() + DisplayValue.Changed:Connect(function(value) + DisplayTextLabel.Text = value + end) +end + +return Display diff --git a/src/shared/Modules/Client/UI/Display.meta.json b/src/shared/Modules/Client/UI/Display.meta.json new file mode 100644 index 0000000..e2625d7 --- /dev/null +++ b/src/shared/Modules/Client/UI/Display.meta.json @@ -0,0 +1,10 @@ +{ + "properties": { + "Tags": [ + "LOAD_MODULE" + ] + }, + "attributes": { + "ClientOnly": true + } +} \ No newline at end of file diff --git a/src/shared/Modules/Client/UI/Lock.luau b/src/shared/Modules/Client/UI/Lock.luau new file mode 100644 index 0000000..f25b9bc --- /dev/null +++ b/src/shared/Modules/Client/UI/Lock.luau @@ -0,0 +1,42 @@ +local Lock = {} + +local localPlayer = game.Players.LocalPlayer + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Remotes = ReplicatedStorage.Remote +local rev_LockPlayer = Remotes.LockPlayer + +local PlayerGui = localPlayer.PlayerGui +local MainGui = PlayerGui:WaitForChild("MainGui") + +local LockButton = MainGui:WaitForChild("Lock") + + + +function changeButtonState(value) + + if value then + LockButton.Text = "UNLOCK(E)" + LockButton.UnlockedGradient.Enabled = false + LockButton.LockedGradient.Enabled = true + + else + LockButton.Text = "LOCK(E)" + LockButton.UnlockedGradient.Enabled = true + LockButton.LockedGradient.Enabled = false + end +end + +function Pressed() + changeButtonState(not localPlayer:GetAttribute("Locked")) + rev_LockPlayer:FireServer() +end + + +function Lock:Init() + changeButtonState(false) + + LockButton.Activated:Connect(Pressed) +end + +return Lock diff --git a/src/shared/Modules/Client/UI/Lock.meta.json b/src/shared/Modules/Client/UI/Lock.meta.json new file mode 100644 index 0000000..e2625d7 --- /dev/null +++ b/src/shared/Modules/Client/UI/Lock.meta.json @@ -0,0 +1,10 @@ +{ + "properties": { + "Tags": [ + "LOAD_MODULE" + ] + }, + "attributes": { + "ClientOnly": true + } +} \ No newline at end of file diff --git a/src/shared/Modules/Data/.gitkeep b/src/shared/Modules/Data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/shared/Modules/Utilities/FerrUtils.luau b/src/shared/Modules/Utilities/FerrUtils.luau new file mode 100644 index 0000000..3a7ad3c --- /dev/null +++ b/src/shared/Modules/Utilities/FerrUtils.luau @@ -0,0 +1,198 @@ +local FerrUtils = {} + +local Players = game:GetService("Players") + +-- Get length of a dictionary since you can't do # like tbls +function FerrUtils.LenDict(dict) : number + local count = 0 + + for _,key in pairs(dict or {}) do + count += 1 + end + return count +end + + +local paddingXZ = 68 +local paddingY = 18.301 + +function FerrUtils.ConvertPosition(pos,modifierY) + local worldX = pos.X * paddingXZ + local worldZ = pos.Y * paddingXZ + local modifier = modifierY or 0 + return Vector3.new(worldX, paddingY + modifier, worldZ) +end +function FerrUtils.calculateDirection(pos, direction)-- CameraIndex: 0=North(-Z), 1=East(+X), 2=South(+Z), 3=West(-X) + if direction == 0 then return pos + Vector2.new(0, -1) end -- North + if direction == 1 then return pos + Vector2.new(1, 0) end -- East + if direction == 2 then return pos + Vector2.new(0, 1) end -- South + if direction == 3 then return pos + Vector2.new(-1, 0) end -- West + return pos +end + +function FerrUtils.DeConvertPosition(pos : Vector3) + local x = pos.X + local y = pos.Z + + if x == 0 then + x = 0.1 + end + if y == 0 then + y = 1 + end + return Vector2.new(math.round(x / paddingXZ),math.round(y / paddingXZ)) +end + +local verbose = false +function log(message) + if verbose then + print(message) + end +end + + + + +local function stepDir(a, b) + if a < b then return 1 end + if a > b then return -1 end + return 0 +end +function FerrUtils.CreatePath(start: Vector2, finish: Vector2) + local waypoints = {} + + local xStep = stepDir(start.X, finish.X) + local yStep = stepDir(start.Y, finish.Y) + + local x, y = start.X, start.Y + + -- Move along X first + while x ~= finish.X do + x += xStep + table.insert(waypoints, FerrUtils.ConvertPosition(Vector2.new(x, y), 5)) + end + + -- Then move along Y + while y ~= finish.Y do + y += yStep + table.insert(waypoints, FerrUtils.ConvertPosition(Vector2.new(x, y), 5)) + end + + return waypoints +end +function FerrUtils.GetNearestPlayer(pos : Vector3) + local nearestPlayer = nil + local nearestDistance = 1000000 + + for i,v in pairs(Players:GetPlayers()) do + local char = v.Character + if not char then + continue + end + local root : Part = char:FindFirstChild("HumanoidRootPart") + if not root then + continue + end + + local distance = (root.Position - pos).Magnitude + + if distance < nearestDistance then + nearestDistance = distance + nearestPlayer = v + end + end + return nearestPlayer,nearestDistance +end +function FerrUtils.GetClosest(values,x) + + local closest = values[1] + local minDist = math.abs(x - closest) + + for i = 2, #values do + local d = math.abs(x - values[i]) + if d < minDist then + minDist = d + closest = values[i] + end + end + + return closest + +end +function FerrUtils.SetCollisionGroup(Descendants,group) + for i,v in pairs(Descendants) do + if v.ClassName == "Part" or v.ClassName == "MeshPart" then + v.CollisionGroup = group + end + end +end + +function FerrUtils.GetParent(instance) + if not instance then + return nil + end + + local current = instance + while current do + if current:GetAttribute("PARENT") then + return current + end + current = current.Parent + end + + return nil +end +function FerrUtils.EffectDuration(object,property,value,duration) + object[property] += value + task.wait(duration) + object[property] -= value +end +function FerrUtils.AddMax(ogValue,sumValue,max) + local returnVal + if ogValue + sumValue > max then + returnVal = max + else + returnVal = ogValue + sumValue + end + return returnVal +end +function FerrUtils.SubMax(ogValue,subValue,min) + local returnVal + if ogValue - subValue < min then + returnVal = min + else + returnVal = ogValue - subValue + end + return returnVal +end + + +function FerrUtils.TakeDamage(humanoid,damage) + humanoid:TakeDamage(damage) + return humanoid.Health <= 0 +end + +function FerrUtils.UnMarkModel(Model,name) + Model:SetAttribute("PARENT",nil) + Model:SetAttribute(name,nil) + + for i,v : Instance in pairs(Model:GetDescendants()) do + if v.ClassName == "Part" or v.ClassName == "MeshPart" then + v:SetAttribute(name,nil) + + end + end +end +function FerrUtils.MarkModel(Model,name,value) + Model:SetAttribute("PARENT",true) + Model:SetAttribute(name,value) + + for i,v : Instance in pairs(Model:GetDescendants()) do + if v.ClassName == "Part" or v.ClassName == "MeshPart" or v.ClassName == "UnionOperation" then + v:SetAttribute(name,value) + + end + end +end + +return FerrUtils diff --git a/src/shared/Modules/Utilities/Observers/_observeAllAttributes.luau b/src/shared/Modules/Utilities/Observers/_observeAllAttributes.luau new file mode 100644 index 0000000..8925302 --- /dev/null +++ b/src/shared/Modules/Utilities/Observers/_observeAllAttributes.luau @@ -0,0 +1,90 @@ +--!strict + +type GuardPredicate = (attributeName: string, value: any) -> (boolean) + +local function defaultGuard(_attributeName: string, _value: any): boolean + return true +end + +--[=[ + Creates an observer that watches all attributes on a given instance. + Your callback is invoked for existing attributes on start and + for every subsequent change where guard(attributeName, value) returns true. + + -- Only observe numeric attributes + local stop = observeAllAttributes( + workspace.Part, + function(name, value) + print(name, "=", value) + return function() + print(name, "was removed or no longer passes guard") + end + end, + function(name, value) + return typeof(value) == "number" + end + ) + + Returns a function that stops observing and runs any outstanding cleanup callbacks. +]=] +local function observeAllAttributes( + instance: any, + callback: (attributeName: string, value: any) -> (() -> ())?, + guardPredicate: (GuardPredicate)? +): () -> () + local cleanupFunctionsPerAttribute: { [string]: () -> () } = {} + local attributeGuard: GuardPredicate = if guardPredicate ~= nil then guardPredicate else defaultGuard + local attributeChangedConnection: RBXScriptConnection + + local function onAttributeChanged(attributeName: string) + -- Tear down any prior callback for this attribute + local previousCleanup = cleanupFunctionsPerAttribute[attributeName] + if typeof(previousCleanup) == "function" then + task.spawn(previousCleanup) + cleanupFunctionsPerAttribute[attributeName] = nil + end + + -- Fire new callback if guard passes + local newValue = instance:GetAttribute(attributeName) + if newValue ~= nil and attributeGuard(attributeName, newValue) then + task.spawn(function() + local cleanup = callback(attributeName, newValue) + if typeof(cleanup) == "function" then + -- Only keep it if we're still connected and the value hasn't changed again + if attributeChangedConnection.Connected + and instance:GetAttribute(attributeName) == newValue then + cleanupFunctionsPerAttribute[attributeName] = cleanup + else + task.spawn(cleanup) + end + end + end) + end + end + + -- Connect the global AttributeChanged event + attributeChangedConnection = instance.AttributeChanged:Connect(onAttributeChanged) + + -- Seed with existing attributes + task.defer(function() + if not attributeChangedConnection.Connected then + return + end + for name, _value in instance:GetAttributes() do + onAttributeChanged(name) + end + end) + + -- Return a stopper that disconnects and cleans up everything + return function() + attributeChangedConnection:Disconnect() + for name, cleanup in pairs(cleanupFunctionsPerAttribute) do + cleanupFunctionsPerAttribute[name] = nil + if typeof(cleanup) == "function" then + task.spawn(cleanup) + end + end + end +end + +return observeAllAttributes \ No newline at end of file diff --git a/src/shared/Modules/Utilities/Observers/_observeAttribute.luau b/src/shared/Modules/Utilities/Observers/_observeAttribute.luau new file mode 100644 index 0000000..1d248fd --- /dev/null +++ b/src/shared/Modules/Utilities/Observers/_observeAttribute.luau @@ -0,0 +1,101 @@ +--!strict +local function defaultGuard(_value: any) + return true +end + +--[=[ + @within Observers + + Creates an observer around an attribute of a given instance. The callback will fire for any non-nil + attribute value. + + ```lua + observeAttribute(workspace.Model, "MyAttribute", function(value) + print("MyAttribute is now:", value) + + return function() + -- Cleanup + print("MyAttribute is no longer:", value) + end + end) + ``` + + An optional `guard` predicate function can be supplied to further narrow which values trigger the observer. + For instance, if only strings are wanted: + + ```lua + observeAttribute( + workspace.Model, + "MyAttribute", + function(value) print("value is a string", value) end, + function(value) return typeof(value) == "string" end + ) + ``` + + The observer also returns a function that can be called to clean up the observer: + ```lua + local stopObserving = observeAttribute(workspace.Model, "MyAttribute", function(value) ... end) + + task.wait(10) + stopObserving() + ``` +]=] +local function observeAttribute( + instance: any, + name: string, + callback: (value: any) -> () -> (), + guard: ((value: any) -> boolean)? +): () -> () + local cleanFn: (() -> ())? = nil + + local onAttrChangedConn: RBXScriptConnection + local changedId = 0 + + local valueGuard: (value: any) -> boolean = if guard ~= nil then guard else defaultGuard + + local function OnAttributeChanged() + if cleanFn ~= nil then + task.spawn(cleanFn) + cleanFn = nil + end + + changedId += 1 + local id = changedId + + local value = instance:GetAttribute(name) + + if value ~= nil and valueGuard(value) then + task.spawn(function() + local clean = callback(value) + if id == changedId and onAttrChangedConn.Connected then + cleanFn = clean + else + task.spawn(clean) + end + end) + end + end + + -- Get changed values: + onAttrChangedConn = instance:GetAttributeChangedSignal(name):Connect(OnAttributeChanged) + + -- Get initial value: + task.defer(function() + if not onAttrChangedConn.Connected then + return + end + + OnAttributeChanged() + end) + + -- Cleanup: + return function() + onAttrChangedConn:Disconnect() + if cleanFn ~= nil then + task.spawn(cleanFn) + cleanFn = nil + end + end +end + +return observeAttribute \ No newline at end of file diff --git a/src/shared/Modules/Utilities/Observers/_observeCharacter.luau b/src/shared/Modules/Utilities/Observers/_observeCharacter.luau new file mode 100644 index 0000000..e197545 --- /dev/null +++ b/src/shared/Modules/Utilities/Observers/_observeCharacter.luau @@ -0,0 +1,82 @@ +--!strict + +local observePlayer = require(script.Parent._observePlayer) + +--[=[ + @within Observers + + Creates an observer that captures each character in the game. + + ```lua + observeCharacter(function(player, character) + print("Character spawned for " .. player.Name) + + return function() + -- Cleanup + print("Character removed for " .. player.Name) + end + end) + ``` +]=] +local function observeCharacter(callback: (player: Player, character: Model) -> (() -> ())?): () -> () + return observePlayer(function(player) + local cleanupFn: (() -> ())? = nil + + local characterAddedConn: RBXScriptConnection + + local function OnCharacterAdded(character: Model) + local currentCharCleanup: (() -> ())? = nil + + -- Call the callback: + task.defer(function() + local cleanup = callback(player, character) + -- If a cleanup function is given, save it for later: + if typeof(cleanup) == "function" then + if characterAddedConn.Connected and character.Parent then + currentCharCleanup = cleanup + cleanupFn = cleanup + else + -- Character is already gone or observer has stopped; call cleanup immediately: + task.spawn(cleanup) + end + end + end) + + -- Watch for the character to be removed from the game hierarchy: + local ancestryChangedConn: RBXScriptConnection + ancestryChangedConn = character.AncestryChanged:Connect(function(_, newParent) + if newParent == nil and ancestryChangedConn.Connected then + ancestryChangedConn:Disconnect() + if currentCharCleanup ~= nil then + task.spawn(currentCharCleanup) + if cleanupFn == currentCharCleanup then + cleanupFn = nil + end + currentCharCleanup = nil + end + end + end) + end + + -- Handle character added: + characterAddedConn = player.CharacterAdded:Connect(OnCharacterAdded) + + -- Handle initial character: + task.defer(function() + if player.Character and characterAddedConn.Connected then + task.spawn(OnCharacterAdded, player.Character) + end + end) + + -- Cleanup: + return function() + characterAddedConn:Disconnect() + if cleanupFn ~= nil then + task.spawn(cleanupFn) + cleanupFn = nil + end + end + end) +end + +return observeCharacter \ No newline at end of file diff --git a/src/shared/Modules/Utilities/Observers/_observeChildren.luau b/src/shared/Modules/Utilities/Observers/_observeChildren.luau new file mode 100644 index 0000000..7ea84cc --- /dev/null +++ b/src/shared/Modules/Utilities/Observers/_observeChildren.luau @@ -0,0 +1,104 @@ +--!strict + +type GuardPredicate = (child: any) -> (boolean) + +local function defaultChildGuard(_child: any): boolean + return true +end + +--[=[ + Creates an observer that captures each child for the given instance. + An optional `guard` predicate can be supplied to filter which children trigger the observer. + + ```lua + -- Only observe Parts + observeChildren( + workspace, + function(child) + print("Part added:", child:GetFullName()) + return function() + print("Part removed (or observer stopped):", child:GetFullName()) + end + end, + function(child) + return child:IsA("Part") + end + ) + ``` +]=] +local function observeChildren( + instance: any, + callback: (child: any) -> (() -> ())?, + guard: ( GuardPredicate )? +): () -> () + local childAddedConn: RBXScriptConnection + local childRemovedConn: RBXScriptConnection + + -- Map each child to its cleanup function + local cleanupFunctionsPerChild: { [Instance]: () -> () } = {} + + -- Choose the guard (either the one passed in, or a default that always returns true) + local childGuard: GuardPredicate = if guard ~= nil then guard else defaultChildGuard + + -- Fires when a new child appears + local function OnChildAdded(child: Instance) + -- skip if the observer was already disconnected + if not childAddedConn.Connected then + return + end + + -- skip if guard rejects this child + if not childGuard(child) then + return + end + + task.spawn(function() + local cleanup = callback(child) + if typeof(cleanup) == "function" then + -- only keep the cleanup if child is still parented and we're still observing + if childAddedConn.Connected and child.Parent ~= nil then + cleanupFunctionsPerChild[child] = cleanup + else + -- otherwise run it immediately + task.spawn(cleanup) + end + end + end) + end + + -- Fires when a child is removed + local function OnChildRemoved(child: Instance) + local cleanup = cleanupFunctionsPerChild[child] + cleanupFunctionsPerChild[child] = nil + if typeof(cleanup) == "function" then + task.spawn(cleanup) + end + end + + -- Connect events + childAddedConn = instance.ChildAdded:Connect(OnChildAdded) + childRemovedConn = instance.ChildRemoved:Connect(OnChildRemoved) + + -- Fire for existing children + task.defer(function() + if not childAddedConn.Connected then + return + end + for _, child in instance:GetChildren() do + OnChildAdded(child) + end + end) + + -- Return a disconnect function + return function() + childAddedConn:Disconnect() + childRemovedConn:Disconnect() + + -- Clean up any remaining children + for child, _ in pairs(cleanupFunctionsPerChild) do + OnChildRemoved(child) + end + end +end + +return observeChildren \ No newline at end of file diff --git a/src/shared/Modules/Utilities/Observers/_observeDescendants.luau b/src/shared/Modules/Utilities/Observers/_observeDescendants.luau new file mode 100644 index 0000000..a877109 --- /dev/null +++ b/src/shared/Modules/Utilities/Observers/_observeDescendants.luau @@ -0,0 +1,99 @@ +--!strict + +type GuardPredicate = (descendant: any) -> (boolean) + +local function defaultDescendantGuard(_descendant: Instance): boolean + return true +end + +--[=[ + Creates an observer that captures every descendant of the given instance. + An optional guard predicate can filter which descendants trigger the observer. + + -- Only observe Parts anywhere under workspace.Model + local stop = observeDescendants( + workspace.Model, + function(part) + print("Part added:", part:GetFullName()) + return function() + print("Part removed (or observer stopped):", part:GetFullName()) + end + end, + function(desc) + return desc:IsA("BasePart") + end + ) +]=] +local function observeDescendants( + instance: any, + callback: (descendant: any) -> (() -> ())?, + guard: ( GuardPredicate )? +): () -> () + local descAddedConn: RBXScriptConnection + local descRemovingConn: RBXScriptConnection + + -- Map each descendant to its cleanup function + local cleanupPerDescendant: { [Instance]: () -> () } = {} + + -- Use provided guard or default + local descendantGuard: GuardPredicate = if guard ~= nil then guard else defaultDescendantGuard + + -- When a new descendant appears + local function OnDescendantAdded(descendant: Instance) + if not descAddedConn.Connected then + return + end + + if not descendantGuard(descendant) then + return + end + + task.spawn(function() + local cleanup = callback(descendant) + if typeof(cleanup) == "function" then + -- only keep cleanup if still valid + if descAddedConn.Connected and descendant:IsDescendantOf(instance) then + cleanupPerDescendant[descendant] = cleanup + else + task.spawn(cleanup) + end + end + end) + end + + -- When a descendant is removed + local function OnDescendantRemoving(descendant: Instance) + local cleanup = cleanupPerDescendant[descendant] + cleanupPerDescendant[descendant] = nil + if typeof(cleanup) == "function" then + task.spawn(cleanup) + end + end + + -- Connect the events + descAddedConn = instance.DescendantAdded:Connect(OnDescendantAdded) + descRemovingConn = instance.DescendantRemoving:Connect(OnDescendantRemoving) + + -- Initialize existing descendants + task.defer(function() + if not descAddedConn.Connected then + return + end + for _, descendant in ipairs(instance:GetDescendants()) do + OnDescendantAdded(descendant) + end + end) + + -- Return a stop function + return function() + descAddedConn:Disconnect() + descRemovingConn:Disconnect() + + -- Clean up any still-tracked descendants + for descendant in pairs(cleanupPerDescendant) do + OnDescendantRemoving(descendant) + end + end +end + +return observeDescendants \ No newline at end of file diff --git a/src/shared/Modules/Utilities/Observers/_observePlayer.luau b/src/shared/Modules/Utilities/Observers/_observePlayer.luau new file mode 100644 index 0000000..64f85ed --- /dev/null +++ b/src/shared/Modules/Utilities/Observers/_observePlayer.luau @@ -0,0 +1,80 @@ +--!strict + +local Players = game:GetService("Players") + +--[=[ + @within Observers + + Creates an observer that captures each player in the game. + + ```lua + observePlayer(function(player) + print("Player entered game", player.Name) + + return function() + -- Cleanup + print("Player left game (or observer stopped)", player.Name) + end + end) + ``` +]=] +local function observePlayer(callback: (player: Player) -> (() -> ())?): () -> () + local playerAddedConn: RBXScriptConnection + local playerRemovingConn: RBXScriptConnection + + local cleanupsPerPlayer: { [Player]: () -> () } = {} + + local function OnPlayerAdded(player: Player) + if not playerAddedConn.Connected then + return + end + + task.spawn(function() + local cleanup = callback(player) + if typeof(cleanup) == "function" then + if playerAddedConn.Connected and player.Parent then + cleanupsPerPlayer[player] = cleanup + else + task.spawn(cleanup) + end + end + end) + end + + local function OnPlayerRemoving(player: Player) + local cleanup = cleanupsPerPlayer[player] + cleanupsPerPlayer[player] = nil + if typeof(cleanup) == "function" then + task.spawn(cleanup) + end + end + + -- Listen for changes: + playerAddedConn = Players.PlayerAdded:Connect(OnPlayerAdded) + playerRemovingConn = Players.PlayerRemoving:Connect(OnPlayerRemoving) + + -- Initial: + task.defer(function() + if not playerAddedConn.Connected then + return + end + + for _, player in Players:GetPlayers() do + task.spawn(OnPlayerAdded, player) + end + end) + + -- Cleanup: + return function() + playerAddedConn:Disconnect() + playerRemovingConn:Disconnect() + + local player = next(cleanupsPerPlayer) + while player do + OnPlayerRemoving(player) + player = next(cleanupsPerPlayer) + end + end +end + +return observePlayer \ No newline at end of file diff --git a/src/shared/Modules/Utilities/Observers/_observeProperty.luau b/src/shared/Modules/Utilities/Observers/_observeProperty.luau new file mode 100644 index 0000000..a24a68a --- /dev/null +++ b/src/shared/Modules/Utilities/Observers/_observeProperty.luau @@ -0,0 +1,91 @@ +--!strict + +local function defaultValueGuard(_value: any): boolean + return true +end + +--[=[ + @within Observers + + Creates an observer around a property of a given instance. + An optional `guard` predicate can be supplied to filter which values trigger the observer. + + ```lua + -- Only observe Name changes when they’re non-empty strings + local stop = observeProperty( + workspace.Model, + "Name", + function(newName: string) + print("New name:", newName) + return function() + print("Name changed away from:", newName) + end + end, + function(value) + return typeof(value) == "string" and #value > 0 + end + ) + ``` + + Returns a function that stops observing and runs any outstanding cleanup. +]=] +local function observeProperty( + instance: Instance, + propertyName: string, + callback: (value: any) -> () -> (), + guard: ((value: any) -> boolean)? +): () -> () + local cleanFn: (() -> ())? + local propChangedConn: RBXScriptConnection + local changeCounter = 0 + + -- decide which guard to use + local valueGuard: (value: any) -> boolean = if guard ~= nil then guard else defaultValueGuard + + local function onPropertyChanged() + -- run previous cleanup (if any) + if cleanFn then + task.spawn(cleanFn) + cleanFn = nil + end + + changeCounter += 1 + local currentId = changeCounter + local newValue = (instance :: any)[propertyName] + + -- only proceed if guard passes + if valueGuard(newValue) then + task.spawn(function() + local cleanup = callback(newValue) + -- if nothing else has changed and we're still connected, keep it + if currentId == changeCounter and propChangedConn.Connected then + cleanFn = cleanup + else + -- otherwise run it immediately + task.spawn(cleanup) + end + end) + end + end + + -- connect to the property‑changed signal + propChangedConn = instance:GetPropertyChangedSignal(propertyName):Connect(onPropertyChanged) + + -- fire once on startup + task.defer(function() + if propChangedConn.Connected then + onPropertyChanged() + end + end) + + -- return stop function + return function() + propChangedConn:Disconnect() + if cleanFn then + task.spawn(cleanFn) + cleanFn = nil + end + end +end + +return observeProperty \ No newline at end of file diff --git a/src/shared/Modules/Utilities/Observers/_observeTag.luau b/src/shared/Modules/Utilities/Observers/_observeTag.luau new file mode 100644 index 0000000..7b783d7 --- /dev/null +++ b/src/shared/Modules/Utilities/Observers/_observeTag.luau @@ -0,0 +1,196 @@ +--!strict + +local CollectionService = game:GetService("CollectionService") + +type InstanceStatus = "__inflight__" | "__dead__" + +--[=[ + @within Observers + + Creates an observer around a CollectionService tag. The given callback will fire for each instance + that has the given tag. + + The callback should return a function, which will be called when the given instance's tag is either + destroyed, loses the given tag, or (if the `ancestors` table is provided) goes outside of the allowed + ancestors. + + The function itself returns a function that can be called to stop the observer. This will also call + any cleanup functions of currently-observed instances. + + ```lua + local stopObserver = Observers.observeTag("MyTag", function(instance: Instance) + print("Observing", instance) + + -- The "cleanup" function: + return function() + print("Stopped observing", instance) + end + end) + + -- Optionally, the `stopObserver` function can be called to completely stop the observer: + task.wait(10) + stopObserver() + ``` + + #### Ancestor Inclusion List + By default, the `observeTag` function will observe a tagged instance anywhere in the Roblox game + hierarchy. The `ancestors` table can optionally be used, which will restrict the observer to only + observe tagged instances that are descendants of instances within the `ancestors` table. + + For instance, if a tagged instance should only be observed when it is in the Workspace, the Workspace + can be added to the `ancestors` list. This might be useful if a tagged model prefab exist somewhere + such as ServerStorage, but shouldn't be observed until placed into the Workspace. + + ```lua + local allowedAncestors = { workspace } + + Observers.observeTag( + "MyTag", + function(instance: Instance) + ... + end, + allowedAncestors + ) + ``` +]=] +function observeTag(tag: string, callback: (instance: T) -> (() -> ())?, ancestors: { Instance }?): () -> () + local instances: { [Instance]: InstanceStatus | () -> () } = {} + local ancestryConn: { [Instance]: RBXScriptConnection } = {} + + local onInstAddedConn: RBXScriptConnection + local onInstRemovedConn: RBXScriptConnection + + local function IsGoodAncestor(instance: Instance) + if ancestors == nil then + return true + end + + for _, ancestor in ancestors do + if instance:IsDescendantOf(ancestor) then + return true + end + end + + return false + end + + local function AttemptStartup(instance: Instance) + -- Mark instance as starting up: + instances[instance] = "__inflight__" + + -- Attempt to run the callback: + task.defer(function() + if instances[instance] ~= "__inflight__" then + return + end + + -- Run the callback in protected mode: + local success, cleanup = xpcall(function(inst: T) + local clean = callback(inst) + if clean ~= nil then + assert(typeof(clean) == "function", "callback must return a function or nil") + end + return clean + end, debug.traceback, instance :: any) + + -- If callback errored, print out the traceback: + if not success then + local err = "" + local firstLine = string.split(cleanup :: any, "\n")[1] + local lastColon = string.find(firstLine, ": ") + if lastColon then + err = firstLine:sub(lastColon + 1) + end + warn(`error while calling observeTag("{tag}") callback:{err}\n{cleanup}`) + return + end + + if instances[instance] ~= "__inflight__" then + -- Instance lost its tag or was destroyed before callback completed; call cleanup immediately: + if cleanup ~= nil then + task.spawn(cleanup :: any) + end + else + -- Good startup; mark the instance with the associated cleanup function: + instances[instance] = cleanup :: any + end + end) + end + + local function AttemptCleanup(instance: Instance) + local cleanup = instances[instance] + instances[instance] = "__dead__" + + if typeof(cleanup) == "function" then + task.spawn(cleanup) + end + end + + local function OnAncestryChanged(instance: Instance) + if IsGoodAncestor(instance) then + if instances[instance] == "__dead__" then + AttemptStartup(instance) + end + else + AttemptCleanup(instance) + end + end + + local function OnInstanceAdded(instance: Instance) + if not onInstAddedConn.Connected then + return + end + if instances[instance] ~= nil then + return + end + + instances[instance] = "__dead__" + + ancestryConn[instance] = instance.AncestryChanged:Connect(function() + OnAncestryChanged(instance) + end) + OnAncestryChanged(instance) + end + + local function OnInstanceRemoved(instance: Instance) + AttemptCleanup(instance) + + local ancestry = ancestryConn[instance] + if ancestry then + ancestry:Disconnect() + ancestryConn[instance] = nil + end + + instances[instance] = nil + end + + -- Hook up added/removed listeners for the given tag: + onInstAddedConn = CollectionService:GetInstanceAddedSignal(tag):Connect(OnInstanceAdded) + onInstRemovedConn = CollectionService:GetInstanceRemovedSignal(tag):Connect(OnInstanceRemoved) + + -- Attempt to mark already-existing tagged instances right away: + task.defer(function() + if not onInstAddedConn.Connected then + return + end + + for _, instance in CollectionService:GetTagged(tag) do + task.spawn(OnInstanceAdded, instance) + end + end) + + -- Full observer cleanup function: + return function() + onInstAddedConn:Disconnect() + onInstRemovedConn:Disconnect() + + -- Clear all instances: + local instance = next(instances) + while instance do + OnInstanceRemoved(instance) + instance = next(instances) + end + end +end + +return observeTag \ No newline at end of file diff --git a/src/shared/Modules/Utilities/Observers/init.luau b/src/shared/Modules/Utilities/Observers/init.luau new file mode 100644 index 0000000..e8bf84e --- /dev/null +++ b/src/shared/Modules/Utilities/Observers/init.luau @@ -0,0 +1,11 @@ +--!strict +return { + observeAttribute = require(script._observeAttribute), + observeAllAttributes = require(script._observeAllAttributes), + observeCharacter = require(script._observeCharacter), + observePlayer = require(script._observePlayer), + observeProperty = require(script._observeProperty), + observeTag = require(script._observeTag), + observeChildren = require(script._observeChildren), + observeDescendants = require(script._observeDescendants) +} \ No newline at end of file diff --git a/src/shared/Modules/Utilities/Observers/init.meta.json b/src/shared/Modules/Utilities/Observers/init.meta.json new file mode 100644 index 0000000..fbd00b1 --- /dev/null +++ b/src/shared/Modules/Utilities/Observers/init.meta.json @@ -0,0 +1,5 @@ +{ + "properties": { + "SourceAssetId": 119664548067214.0 + } +} \ No newline at end of file diff --git a/src/shared/Modules/Utilities/Signal.luau b/src/shared/Modules/Utilities/Signal.luau new file mode 100644 index 0000000..e3a1129 --- /dev/null +++ b/src/shared/Modules/Utilities/Signal.luau @@ -0,0 +1,180 @@ +-------------------------------------------------------------------------------- +-- Batched Yield-Safe Signal Implementation -- +-- This is a Signal class which has effectively identical behavior to a -- +-- normal RBXScriptSignal, with the only difference being a couple extra -- +-- stack frames at the bottom of the stack trace when an error is thrown. -- +-- This implementation caches runner coroutines, so the ability to yield in -- +-- the signal handlers comes at minimal extra cost over a naive signal -- +-- implementation that either always or never spawns a thread. -- +-- -- +-- API: -- +-- local Signal = require(THIS MODULE) -- +-- local sig = Signal.new() -- +-- local connection = sig:Connect(function(arg1, arg2, ...) ... end) -- +-- sig:Fire(arg1, arg2, ...) -- +-- connection:Disconnect() -- +-- sig:DisconnectAll() -- +-- local arg1, arg2, ... = sig:Wait() -- +-- -- +-- Licence: -- +-- Licenced under the MIT licence. -- +-- -- +-- Authors: -- +-- stravant - July 31st, 2021 - Created the file. -- +-------------------------------------------------------------------------------- + +-- The currently idle thread to run the next handler on +local freeRunnerThread = nil + +-- Function which acquires the currently idle handler runner thread, runs the +-- function fn on it, and then releases the thread, returning it to being the +-- currently idle one. +-- If there was a currently idle runner thread already, that's okay, that old +-- one will just get thrown and eventually GCed. +local function acquireRunnerThreadAndCallEventHandler(fn, ...) + local acquiredRunnerThread = freeRunnerThread + freeRunnerThread = nil + fn(...) + -- The handler finished running, this runner thread is free again. + freeRunnerThread = acquiredRunnerThread +end + +-- Coroutine runner that we create coroutines of. The coroutine can be +-- repeatedly resumed with functions to run followed by the argument to run +-- them with. +local function runEventHandlerInFreeThread() + -- Note: We cannot use the initial set of arguments passed to + -- runEventHandlerInFreeThread for a call to the handler, because those + -- arguments would stay on the stack for the duration of the thread's + -- existence, temporarily leaking references. Without access to raw bytecode + -- there's no way for us to clear the "..." references from the stack. + while true do + acquireRunnerThreadAndCallEventHandler(coroutine.yield()) + end +end + +-- Connection class +local Connection = {} +Connection.__index = Connection + +function Connection.new(signal, fn) + return setmetatable({ + _connected = true, + _signal = signal, + _fn = fn, + _next = false, + }, Connection) +end + +function Connection:Disconnect() + self._connected = false + + -- Unhook the node, but DON'T clear it. That way any fire calls that are + -- currently sitting on this node will be able to iterate forwards off of + -- it, but any subsequent fire calls will not hit it, and it will be GCed + -- when no more fire calls are sitting on it. + if self._signal._handlerListHead == self then + self._signal._handlerListHead = self._next + else + local prev = self._signal._handlerListHead + while prev and prev._next ~= self do + prev = prev._next + end + if prev then + prev._next = self._next + end + end +end + +-- Make Connection strict +setmetatable(Connection, { + __index = function(tb, key) + error(("Attempt to get Connection::%s (not a valid member)"):format(tostring(key)), 2) + end, + __newindex = function(tb, key, value) + error(("Attempt to set Connection::%s (not a valid member)"):format(tostring(key)), 2) + end +}) + +-- Signal class +local Signal = {} +Signal.__index = Signal + +function Signal.new() + return setmetatable({ + _handlerListHead = false, + }, Signal) +end + +function Signal:Connect(fn) + local connection = Connection.new(self, fn) + if self._handlerListHead then + connection._next = self._handlerListHead + self._handlerListHead = connection + else + self._handlerListHead = connection + end + return connection +end + +-- Disconnect all handlers. Since we use a linked list it suffices to clear the +-- reference to the head handler. +function Signal:DisconnectAll() + self._handlerListHead = false +end + +-- Signal:Fire(...) implemented by running the handler functions on the +-- coRunnerThread, and any time the resulting thread yielded without returning +-- to us, that means that it yielded to the Roblox scheduler and has been taken +-- over by Roblox scheduling, meaning we have to make a new coroutine runner. +function Signal:Fire(...) + local item = self._handlerListHead + while item do + if item._connected then + if not freeRunnerThread then + freeRunnerThread = coroutine.create(runEventHandlerInFreeThread) + -- Get the freeRunnerThread to the first yield + coroutine.resume(freeRunnerThread) + end + task.spawn(freeRunnerThread, item._fn, ...) + end + item = item._next + end +end + +-- Implement Signal:Wait() in terms of a temporary connection using +-- a Signal:Connect() which disconnects itself. +function Signal:Wait() + local waitingCoroutine = coroutine.running() + local cn; + cn = self:Connect(function(...) + cn:Disconnect() + task.spawn(waitingCoroutine, ...) + end) + return coroutine.yield() +end + +-- Implement Signal:Once() in terms of a connection which disconnects +-- itself before running the handler. +function Signal:Once(fn) + local cn; + cn = self:Connect(function(...) + if cn._connected then + cn:Disconnect() + end + fn(...) + end) + return cn +end + +-- Make signal strict +setmetatable(Signal, { + __index = function(tb, key) + error(("Attempt to get Signal::%s (not a valid member)"):format(tostring(key)), 2) + end, + __newindex = function(tb, key, value) + error(("Attempt to set Signal::%s (not a valid member)"):format(tostring(key)), 2) + end +}) + +return Signal diff --git a/src/shared/Modules/Utilities/t.luau b/src/shared/Modules/Utilities/t.luau new file mode 100644 index 0000000..3744c86 --- /dev/null +++ b/src/shared/Modules/Utilities/t.luau @@ -0,0 +1,1350 @@ +-- t: a runtime typechecker for Roblox + +local t = {} + +function t.type(typeName) + return function(value) + local valueType = type(value) + if valueType == typeName then + return true + else + return false, string.format("%s expected, got %s", typeName, valueType) + end + end +end + +function t.typeof(typeName) + return function(value) + local valueType = typeof(value) + if valueType == typeName then + return true + else + return false, string.format("%s expected, got %s", typeName, valueType) + end + end +end + +--[[** + matches any type except nil + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +function t.any(value) + if value ~= nil then + return true + else + return false, "any expected, got nil" + end +end + +--Lua primitives + +--[[** + ensures Lua primitive boolean type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.boolean = t.typeof("boolean") + +--[[** + ensures Lua primitive buffer type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.buffer = t.typeof("buffer") + +--[[** + ensures Lua primitive thread type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.thread = t.typeof("thread") + +--[[** + ensures Lua primitive callback type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.callback = t.typeof("function") +t["function"] = t.callback + +--[[** + ensures Lua primitive none type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.none = t.typeof("nil") +t["nil"] = t.none + +--[[** + ensures Lua primitive string type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.string = t.typeof("string") + +--[[** + ensures Lua primitive table type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.table = t.typeof("table") + +--[[** + ensures Lua primitive userdata type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.userdata = t.type("userdata") + +--[[** + ensures Lua primitive vector type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.vector = t.type("vector") + +--[[** + ensures value is a number and non-NaN + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +function t.number(value) + local valueType = typeof(value) + if valueType == "number" then + if value == value then + return true + else + return false, "unexpected NaN value" + end + else + return false, string.format("number expected, got %s", valueType) + end +end + +--[[** + ensures value is NaN + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +function t.nan(value) + local valueType = typeof(value) + if valueType == "number" then + if value ~= value then + return true + else + return false, "unexpected non-NaN value" + end + else + return false, string.format("number expected, got %s", valueType) + end +end + +-- roblox types + +--[[** + ensures Roblox Axes type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Axes = t.typeof("Axes") + +--[[** + ensures Roblox BrickColor type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.BrickColor = t.typeof("BrickColor") + +--[[** + ensures Roblox CatalogSearchParams type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.CatalogSearchParams = t.typeof("CatalogSearchParams") + +--[[** + ensures Roblox CFrame type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.CFrame = t.typeof("CFrame") + +--[[** + ensures Roblox Content type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Content = t.typeof("Content") + +--[[** + ensures Roblox Color3 type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Color3 = t.typeof("Color3") + +--[[** + ensures Roblox ColorSequence type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.ColorSequence = t.typeof("ColorSequence") + +--[[** + ensures Roblox ColorSequenceKeypoint type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.ColorSequenceKeypoint = t.typeof("ColorSequenceKeypoint") + +--[[** + ensures Roblox DateTime type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.DateTime = t.typeof("DateTime") + +--[[** + ensures Roblox DockWidgetPluginGuiInfo type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.DockWidgetPluginGuiInfo = t.typeof("DockWidgetPluginGuiInfo") + +--[[** + ensures Roblox Enum type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Enum = t.typeof("Enum") + +--[[** + ensures Roblox EnumItem type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.EnumItem = t.typeof("EnumItem") + +--[[** + ensures Roblox Enums type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Enums = t.typeof("Enums") + +--[[** + ensures Roblox Faces type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Faces = t.typeof("Faces") + +--[[** + ensures Roblox FloatCurveKey type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.FloatCurveKey = t.typeof("FloatCurveKey") + +--[[** + ensures Roblox Font type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Font = t.typeof("Font") + +--[[** + ensures Roblox Instance type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Instance = t.typeof("Instance") + +--[[** + ensures Roblox NumberRange type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.NumberRange = t.typeof("NumberRange") + +--[[** + ensures Roblox NumberSequence type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.NumberSequence = t.typeof("NumberSequence") + +--[[** + ensures Roblox NumberSequenceKeypoint type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.NumberSequenceKeypoint = t.typeof("NumberSequenceKeypoint") + +--[[** + ensures Roblox OverlapParams type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.OverlapParams = t.typeof("OverlapParams") + +--[[** + ensures Roblox PathWaypoint type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.PathWaypoint = t.typeof("PathWaypoint") + +--[[** + ensures Roblox PhysicalProperties type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.PhysicalProperties = t.typeof("PhysicalProperties") + +--[[** + ensures Roblox Random type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Random = t.typeof("Random") + +--[[** + ensures Roblox Ray type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Ray = t.typeof("Ray") + +--[[** + ensures Roblox RaycastParams type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.RaycastParams = t.typeof("RaycastParams") + +--[[** + ensures Roblox RaycastResult type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.RaycastResult = t.typeof("RaycastResult") + +--[[** + ensures Roblox RBXScriptConnection type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.RBXScriptConnection = t.typeof("RBXScriptConnection") + +--[[** + ensures Roblox RBXScriptSignal type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.RBXScriptSignal = t.typeof("RBXScriptSignal") + +--[[** + ensures Roblox Rect type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Rect = t.typeof("Rect") + +--[[** + ensures Roblox Region3 type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Region3 = t.typeof("Region3") + +--[[** + ensures Roblox Region3int16 type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Region3int16 = t.typeof("Region3int16") + +--[[** + ensures Roblox TweenInfo type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.TweenInfo = t.typeof("TweenInfo") + +--[[** + ensures Roblox UDim type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.UDim = t.typeof("UDim") + +--[[** + ensures Roblox UDim2 type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.UDim2 = t.typeof("UDim2") + +--[[** + ensures Roblox Vector2 type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Vector2 = t.typeof("Vector2") + +--[[** + ensures Roblox Vector2int16 type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Vector2int16 = t.typeof("Vector2int16") + +--[[** + ensures Roblox Vector3 type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Vector3 = t.typeof("Vector3") + +--[[** + ensures Roblox Vector3int16 type + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +t.Vector3int16 = t.typeof("Vector3int16") + +--[[** + ensures value is any of the given literal values + + @param literals The literals to check against + + @returns A function that will return true if the condition is passed +**--]] +function t.literalList(literals) + -- optimization for primitive types + local set = {} + for _, literal in ipairs(literals) do + set[literal] = true + end + return function(value) + if set[value] then + return true + end + for _, literal in ipairs(literals) do + if literal == value then + return true + end + end + + return false, "bad type for literal list" + end +end + +--[[** + ensures value is a given literal value + + @param literal The literal to use + + @returns A function that will return true iff the condition is passed +**--]] +function t.literal(...) + local size = select("#", ...) + if size == 1 then + local literal = ... + return function(value) + if value ~= literal then + return false, string.format("expected %s, got %s", tostring(literal), tostring(value)) + end + + return true + end + else + local literals = {} + for i = 1, size do + local value = select(i, ...) + literals[i] = t.literal(value) + end + + return t.unionList(literals) + end +end + +--[[** + DEPRECATED + Please use t.literal +**--]] +t.exactly = t.literal + +--[[** + Returns a t.union of each key in the table as a t.literal + + @param keyTable The table to get keys from + + @returns True iff the condition is satisfied, false otherwise +**--]] +function t.keyOf(keyTable) + local keys = {} + local length = 0 + for key in pairs(keyTable) do + length = length + 1 + keys[length] = key + end + + return t.literal(table.unpack(keys, 1, length)) +end + +--[[** + Returns a t.union of each value in the table as a t.literal + + @param valueTable The table to get values from + + @returns True iff the condition is satisfied, false otherwise +**--]] +function t.valueOf(valueTable) + local values = {} + local length = 0 + for _, value in pairs(valueTable) do + length = length + 1 + values[length] = value + end + + return t.literal(table.unpack(values, 1, length)) +end + +--[[** + ensures value is an integer + + @param value The value to check against + + @returns True iff the condition is satisfied, false otherwise +**--]] +function t.integer(value) + local success, errMsg = t.number(value) + if not success then + return false, errMsg or "" + end + + if value % 1 == 0 then + return true + else + return false, string.format("integer expected, got %s", value) + end +end + +--[[** + ensures value is a number where min <= value + + @param min The minimum to use + + @returns A function that will return true iff the condition is passed +**--]] +function t.numberMin(min) + return function(value) + local success, errMsg = t.number(value) + if not success then + return false, errMsg or "" + end + + if value >= min then + return true + else + return false, string.format("number >= %s expected, got %s", min, value) + end + end +end + +--[[** + ensures value is a number where value <= max + + @param max The maximum to use + + @returns A function that will return true iff the condition is passed +**--]] +function t.numberMax(max) + return function(value) + local success, errMsg = t.number(value) + if not success then + return false, errMsg + end + + if value <= max then + return true + else + return false, string.format("number <= %s expected, got %s", max, value) + end + end +end + +--[[** + ensures value is a number where min < value + + @param min The minimum to use + + @returns A function that will return true iff the condition is passed +**--]] +function t.numberMinExclusive(min) + return function(value) + local success, errMsg = t.number(value) + if not success then + return false, errMsg or "" + end + + if min < value then + return true + else + return false, string.format("number > %s expected, got %s", min, value) + end + end +end + +--[[** + ensures value is a number where value < max + + @param max The maximum to use + + @returns A function that will return true iff the condition is passed +**--]] +function t.numberMaxExclusive(max) + return function(value) + local success, errMsg = t.number(value) + if not success then + return false, errMsg or "" + end + + if value < max then + return true + else + return false, string.format("number < %s expected, got %s", max, value) + end + end +end + +--[[** + ensures value is a number where value > 0 + + @returns A function that will return true iff the condition is passed +**--]] +t.numberPositive = t.numberMinExclusive(0) + +--[[** + ensures value is a number where value < 0 + + @returns A function that will return true iff the condition is passed +**--]] +t.numberNegative = t.numberMaxExclusive(0) + +--[[** + ensures value is a number where min <= value <= max + + @param min The minimum to use + @param max The maximum to use + + @returns A function that will return true iff the condition is passed +**--]] +function t.numberConstrained(min, max) + assert(t.number(min)) + assert(t.number(max)) + local minCheck = t.numberMin(min) + local maxCheck = t.numberMax(max) + + return function(value) + local minSuccess, minErrMsg = minCheck(value) + if not minSuccess then + return false, minErrMsg or "" + end + + local maxSuccess, maxErrMsg = maxCheck(value) + if not maxSuccess then + return false, maxErrMsg or "" + end + + return true + end +end + +--[[** + ensures value is a number where min < value < max + + @param min The minimum to use + @param max The maximum to use + + @returns A function that will return true iff the condition is passed +**--]] +function t.numberConstrainedExclusive(min, max) + assert(t.number(min)) + assert(t.number(max)) + local minCheck = t.numberMinExclusive(min) + local maxCheck = t.numberMaxExclusive(max) + + return function(value) + local minSuccess, minErrMsg = minCheck(value) + if not minSuccess then + return false, minErrMsg or "" + end + + local maxSuccess, maxErrMsg = maxCheck(value) + if not maxSuccess then + return false, maxErrMsg or "" + end + + return true + end +end + +--[[** + ensures value matches string pattern + + @param string pattern to check against + + @returns A function that will return true iff the condition is passed +**--]] +function t.match(pattern) + assert(t.string(pattern)) + return function(value) + local stringSuccess, stringErrMsg = t.string(value) + if not stringSuccess then + return false, stringErrMsg + end + + if string.match(value, pattern) == nil then + return false, string.format("%q failed to match pattern %q", value, pattern) + end + + return true + end +end + +--[[** + ensures value is either nil or passes check + + @param check The check to use + + @returns A function that will return true iff the condition is passed +**--]] +function t.optional(check) + assert(t.callback(check)) + return function(value) + if value == nil then + return true + end + + local success, errMsg = check(value) + if success then + return true + else + return false, string.format("(optional) %s", errMsg or "") + end + end +end + +--[[** + matches given tuple against tuple type definition + + @param ... The type definition for the tuples + + @returns A function that will return true iff the condition is passed +**--]] +function t.tuple(...) + local checks = { ... } + return function(...) + local args = { ... } + for i, check in ipairs(checks) do + local success, errMsg = check(args[i]) + if success == false then + return false, string.format("Bad tuple index #%s:\n\t%s", i, errMsg or "") + end + end + + return true + end +end + +--[[** + ensures all keys in given table pass check + + @param check The function to use to check the keys + + @returns A function that will return true iff the condition is passed +**--]] +function t.keys(check) + assert(t.callback(check)) + return function(value) + local tableSuccess, tableErrMsg = t.table(value) + if tableSuccess == false then + return false, tableErrMsg or "" + end + + for key in pairs(value) do + local success, errMsg = check(key) + if success == false then + return false, string.format("bad key %s:\n\t%s", tostring(key), errMsg or "") + end + end + + return true + end +end + +--[[** + ensures all values in given table pass check + + @param check The function to use to check the values + + @returns A function that will return true iff the condition is passed +**--]] +function t.values(check) + assert(t.callback(check)) + return function(value) + local tableSuccess, tableErrMsg = t.table(value) + if tableSuccess == false then + return false, tableErrMsg or "" + end + + for key, val in pairs(value) do + local success, errMsg = check(val) + if success == false then + return false, string.format("bad value for key %s:\n\t%s", tostring(key), errMsg or "") + end + end + + return true + end +end + +--[[** + ensures value is a table and all keys pass keyCheck and all values pass valueCheck + + @param keyCheck The function to use to check the keys + @param valueCheck The function to use to check the values + + @returns A function that will return true iff the condition is passed +**--]] +function t.map(keyCheck, valueCheck) + assert(t.callback(keyCheck)) + assert(t.callback(valueCheck)) + local keyChecker = t.keys(keyCheck) + local valueChecker = t.values(valueCheck) + + return function(value) + local keySuccess, keyErr = keyChecker(value) + if not keySuccess then + return false, keyErr or "" + end + + local valueSuccess, valueErr = valueChecker(value) + if not valueSuccess then + return false, valueErr or "" + end + + return true + end +end + +--[[** + ensures value is a table and all keys pass valueCheck and all values are true + + @param valueCheck The function to use to check the values + + @returns A function that will return true iff the condition is passed +**--]] +function t.set(valueCheck) + return t.map(valueCheck, t.literal(true)) +end + +do + local arrayKeysCheck = t.keys(t.integer) +--[[** + ensures value is an array and all values of the array match check + + @param check The check to compare all values with + + @returns A function that will return true iff the condition is passed + **--]] + function t.array(check) + assert(t.callback(check)) + local valuesCheck = t.values(check) + + return function(value) + local keySuccess, keyErrMsg = arrayKeysCheck(value) + if keySuccess == false then + return false, string.format("[array] %s", keyErrMsg or "") + end + + -- # is unreliable for sparse arrays + -- Count upwards using ipairs to avoid false positives from the behavior of # + local arraySize = 0 + + for _ in ipairs(value) do + arraySize = arraySize + 1 + end + + for key in pairs(value) do + if key < 1 or key > arraySize then + return false, string.format("[array] key %s must be sequential", tostring(key)) + end + end + + local valueSuccess, valueErrMsg = valuesCheck(value) + if not valueSuccess then + return false, string.format("[array] %s", valueErrMsg or "") + end + + return true + end + end + +--[[** + ensures value is an array of a strict makeup and size + + @param check The check to compare all values with + + @returns A function that will return true iff the condition is passed + **--]] + function t.strictArray(...) + local valueTypes = { ... } + assert(t.array(t.callback)(valueTypes)) + + return function(value) + local keySuccess, keyErrMsg = arrayKeysCheck(value) + if keySuccess == false then + return false, string.format("[strictArray] %s", keyErrMsg or "") + end + + -- If there's more than the set array size, disallow + if #valueTypes < #value then + return false, string.format("[strictArray] Array size exceeds limit of %d", #valueTypes) + end + + for idx, typeFn in pairs(valueTypes) do + local typeSuccess, typeErrMsg = typeFn(value[idx]) + if not typeSuccess then + return false, string.format("[strictArray] Array index #%d - %s", idx, typeErrMsg) + end + end + + return true + end + end +end + +do + local callbackArray = t.array(t.callback) +--[[** + creates a union type + + @param checks The checks to union + + @returns A function that will return true iff the condition is passed + **--]] + function t.unionList(checks) + assert(callbackArray(checks)) + + return function(value) + for _, check in ipairs(checks) do + if check(value) then + return true + end + end + + return false, "bad type for union" + end + end + +--[[** + creates a union type + + @param ... The checks to union + + @returns A function that will return true iff the condition is passed + **--]] + function t.union(...) + return t.unionList({ ... }) + end + +--[[** + Alias for t.union + **--]] + t.some = t.union + +--[[** + creates an intersection type + + @param checks The checks to intersect + + @returns A function that will return true iff the condition is passed + **--]] + function t.intersectionList(checks) + assert(callbackArray(checks)) + + return function(value) + for _, check in ipairs(checks) do + local success, errMsg = check(value) + if not success then + return false, errMsg or "" + end + end + + return true + end + end + +--[[** + creates an intersection type + + @param ... The checks to intersect + + @returns A function that will return true iff the condition is passed + **--]] + function t.intersection(...) + return t.intersectionList({ ... }) + end + +--[[** + Alias for t.intersection + **--]] + t.every = t.intersection +end + +do + local checkInterface = t.map(t.any, t.callback) +--[[** + ensures value matches given interface definition + + @param checkTable The interface definition + + @returns A function that will return true iff the condition is passed + **--]] + function t.interface(checkTable) + assert(checkInterface(checkTable)) + return function(value) + local tableSuccess, tableErrMsg = t.table(value) + if tableSuccess == false then + return false, tableErrMsg or "" + end + + for key, check in pairs(checkTable) do + local success, errMsg = check(value[key]) + if success == false then + return false, string.format("[interface] bad value for %s:\n\t%s", tostring(key), errMsg or "") + end + end + + return true + end + end + +--[[** + ensures value matches given interface definition strictly + + @param checkTable The interface definition + + @returns A function that will return true iff the condition is passed + **--]] + function t.strictInterface(checkTable) + assert(checkInterface(checkTable)) + return function(value) + local tableSuccess, tableErrMsg = t.table(value) + if tableSuccess == false then + return false, tableErrMsg or "" + end + + for key, check in pairs(checkTable) do + local success, errMsg = check(value[key]) + if success == false then + return false, string.format("[interface] bad value for %s:\n\t%s", tostring(key), errMsg or "") + end + end + + for key in pairs(value) do + if not checkTable[key] then + return false, string.format("[interface] unexpected field %q", tostring(key)) + end + end + + return true + end + end +end + +--[[** + ensure value is an Instance and it's ClassName matches the given ClassName + + @param className The class name to check for + + @returns A function that will return true iff the condition is passed +**--]] +function t.instanceOf(className, childTable) + assert(t.string(className)) + + local childrenCheck + if childTable ~= nil then + childrenCheck = t.children(childTable) + end + + return function(value) + local instanceSuccess, instanceErrMsg = t.Instance(value) + if not instanceSuccess then + return false, instanceErrMsg or "" + end + + if value.ClassName ~= className then + return false, string.format("%s expected, got %s", className, value.ClassName) + end + + if childrenCheck then + local childrenSuccess, childrenErrMsg = childrenCheck(value) + if not childrenSuccess then + return false, childrenErrMsg + end + end + + return true + end +end + +t.instance = t.instanceOf + +--[[** + ensure value is an Instance and it's ClassName matches the given ClassName by an IsA comparison + + @param className The class name to check for + + @returns A function that will return true iff the condition is passed +**--]] +function t.instanceIsA(className, childTable) + assert(t.string(className)) + + local childrenCheck + if childTable ~= nil then + childrenCheck = t.children(childTable) + end + + return function(value) + local instanceSuccess, instanceErrMsg = t.Instance(value) + if not instanceSuccess then + return false, instanceErrMsg or "" + end + + if not value:IsA(className) then + return false, string.format("%s expected, got %s", className, value.ClassName) + end + + if childrenCheck then + local childrenSuccess, childrenErrMsg = childrenCheck(value) + if not childrenSuccess then + return false, childrenErrMsg + end + end + + return true + end +end + +--[[** + ensures value is an enum of the correct type + + @param enum The enum to check + + @returns A function that will return true iff the condition is passed +**--]] +function t.enum(enum) + assert(t.Enum(enum)) + return function(value) + local enumItemSuccess, enumItemErrMsg = t.EnumItem(value) + if not enumItemSuccess then + return false, enumItemErrMsg + end + + if value.EnumType == enum then + return true + else + return false, string.format("enum of %s expected, got enum of %s", tostring(enum), tostring(value.EnumType)) + end + end +end + +do + local checkWrap = t.tuple(t.callback, t.callback) + +--[[** + wraps a callback in an assert with checkArgs + + @param callback The function to wrap + @param checkArgs The function to use to check arguments in the assert + + @returns A function that first asserts using checkArgs and then calls callback + **--]] + function t.wrap(callback, checkArgs) + assert(checkWrap(callback, checkArgs)) + return function(...) + assert(checkArgs(...)) + return callback(...) + end + end +end + +--[[** + asserts a given check + + @param check The function to wrap with an assert + + @returns A function that simply wraps the given check in an assert +**--]] +function t.strict(check) + return function(...) + assert(check(...)) + end +end + +do + local checkChildren = t.map(t.string, t.callback) + +--[[** + Takes a table where keys are child names and values are functions to check the children against. + Pass an instance tree into the function. + If at least one child passes each check, the overall check passes. + + Warning! If you pass in a tree with more than one child of the same name, this function will always return false + + @param checkTable The table to check against + + @returns A function that checks an instance tree + **--]] + function t.children(checkTable) + assert(checkChildren(checkTable)) + + return function(value) + local instanceSuccess, instanceErrMsg = t.Instance(value) + if not instanceSuccess then + return false, instanceErrMsg or "" + end + + local childrenByName = {} + for _, child in ipairs(value:GetChildren()) do + local name = child.Name + if checkTable[name] then + if childrenByName[name] then + return false, string.format("Cannot process multiple children with the same name %q", name) + end + + childrenByName[name] = child + end + end + + for name, check in pairs(checkTable) do + local success, errMsg = check(childrenByName[name]) + if not success then + return false, string.format("[%s.%s] %s", value:GetFullName(), name, errMsg or "") + end + end + + return true + end + end +end + +return t \ No newline at end of file diff --git a/src/shared/Modules/Utilities/throttle.luau b/src/shared/Modules/Utilities/throttle.luau new file mode 100644 index 0000000..075dab1 --- /dev/null +++ b/src/shared/Modules/Utilities/throttle.luau @@ -0,0 +1,27 @@ +--!strict +local timeThrottle: { [any]: number } = {} + +return function( + identifier: T, + delay: number, + func: (T, A...) -> (), + ...: A... +): boolean + local now = os.clock() + local last = timeThrottle[identifier] + if last and now - last < delay then + return false + end + + timeThrottle[identifier] = now + task.spawn(func, identifier, ...) + + task.delay(delay, function() + -- to avoid memory leaks + if timeThrottle[identifier] == now then + timeThrottle[identifier] = nil + end + end) + + return true +end \ No newline at end of file diff --git a/src/shared/Modules/Utilities/throttle.meta.json b/src/shared/Modules/Utilities/throttle.meta.json new file mode 100644 index 0000000..d3ac6f6 --- /dev/null +++ b/src/shared/Modules/Utilities/throttle.meta.json @@ -0,0 +1,5 @@ +{ + "properties": { + "SourceAssetId": 127265032895682.0 + } +} \ No newline at end of file diff --git a/src/shared/Modules/Utilities/waitWithTimeout.luau b/src/shared/Modules/Utilities/waitWithTimeout.luau new file mode 100644 index 0000000..a7de02e --- /dev/null +++ b/src/shared/Modules/Utilities/waitWithTimeout.luau @@ -0,0 +1,35 @@ +--!strict +return function(event: RBXScriptSignal, timeoutInSeconds: number): (boolean, ...any) + local thread = coroutine.running() + local connection: RBXScriptConnection? + + local function onEvent(...) + if not connection then + return + end + + connection:Disconnect() + connection = nil + + if coroutine.status(thread) == "suspended" then + task.spawn(thread, false, ...) + end + end + + connection = event:Once(onEvent) + + task.delay(timeoutInSeconds, function() + if not connection then + return + end + + connection:Disconnect() + connection = nil + + if coroutine.status(thread) == "suspended" then + task.spawn(thread, true) + end + end) + + return coroutine.yield() +end \ No newline at end of file diff --git a/src/shared/Modules/Utilities/waitWithTimeout.meta.json b/src/shared/Modules/Utilities/waitWithTimeout.meta.json new file mode 100644 index 0000000..1a402b2 --- /dev/null +++ b/src/shared/Modules/Utilities/waitWithTimeout.meta.json @@ -0,0 +1,5 @@ +{ + "properties": { + "SourceAssetId": 116382382498753.0 + } +} \ No newline at end of file diff --git a/wally.toml b/wally.toml new file mode 100644 index 0000000..1159eef --- /dev/null +++ b/wally.toml @@ -0,0 +1,7 @@ +[package] +name = "zaremate/coreography" +version = "0.1.0" +registry = "https://github.com/UpliftGames/wally-index" +realm = "shared" + +[dependencies]