init
This commit is contained in:
commit
e70d96a953
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
# Project place file
|
||||
/shared-reality.rbxlx
|
||||
|
||||
# Roblox Studio lock files
|
||||
/*.rbxlx.lock
|
||||
/*.rbxl.lock
|
||||
|
||||
Packages/
|
||||
ServerPackages/
|
||||
sourcemap.json
|
||||
wally.lock
|
||||
17
README.md
Normal file
17
README.md
Normal file
@ -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).
|
||||
7
aftman.toml
Normal file
7
aftman.toml
Normal file
@ -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"
|
||||
BIN
coreography.rbxl
Normal file
BIN
coreography.rbxl
Normal file
Binary file not shown.
27
default.project.json
Normal file
27
default.project.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "coreography",
|
||||
"tree": {
|
||||
"$className": "DataModel",
|
||||
"ReplicatedStorage": {
|
||||
"Shared": {
|
||||
"$path": "src/shared"
|
||||
}
|
||||
},
|
||||
"ServerScriptService": {
|
||||
"Server": {
|
||||
"$path": "src/server"
|
||||
}
|
||||
},
|
||||
"StarterPlayer": {
|
||||
"StarterPlayerScripts": {
|
||||
"Client": {
|
||||
"$path": "src/client"
|
||||
}
|
||||
},
|
||||
"$properties": {
|
||||
"CameraMaxZoomDistance": 128.0,
|
||||
"CharacterUseJumpPower": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
rokit.toml
Normal file
8
rokit.toml
Normal file
@ -0,0 +1,8 @@
|
||||
# This file lists tools managed by Rokit, a toolchain manager for Roblox projects.
|
||||
# For more information, see https://github.com/rojo-rbx/rokit
|
||||
|
||||
# New tools can be added by running `rokit add <tool>` in a terminal.
|
||||
|
||||
[tools]
|
||||
rojo = "rojo-rbx/rojo@7.7.0-rc.1"
|
||||
wally = "UpliftGames/wally@0.3.2"
|
||||
1
selene.toml
Normal file
1
selene.toml
Normal file
@ -0,0 +1 @@
|
||||
std = "roblox"
|
||||
15
src/client/Start.client.luau
Normal file
15
src/client/Start.client.luau
Normal file
@ -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)
|
||||
186
src/server/Modules/Classes/Round/Grid.luau
Normal file
186
src/server/Modules/Classes/Round/Grid.luau
Normal file
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
39
src/server/Modules/Classes/Round/ModifierManager/init.luau
Normal file
39
src/server/Modules/Classes/Round/ModifierManager/init.luau
Normal file
@ -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
|
||||
35
src/server/Modules/Classes/Round/Timer.luau
Normal file
35
src/server/Modules/Classes/Round/Timer.luau
Normal file
@ -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
|
||||
101
src/server/Modules/Classes/Round/init.luau
Normal file
101
src/server/Modules/Classes/Round/init.luau
Normal file
@ -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
|
||||
28
src/server/Modules/RoundManager.luau
Normal file
28
src/server/Modules/RoundManager.luau
Normal file
@ -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
|
||||
7
src/server/Modules/RoundManager.meta.json
Normal file
7
src/server/Modules/RoundManager.meta.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"Tags": [
|
||||
"LOAD_MODULE"
|
||||
]
|
||||
}
|
||||
}
|
||||
16
src/server/Start.server.luau
Normal file
16
src/server/Start.server.luau
Normal file
@ -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
|
||||
BIN
src/shared/ModuleLoader/ActorForClient.rbxm
Normal file
BIN
src/shared/ModuleLoader/ActorForClient.rbxm
Normal file
Binary file not shown.
BIN
src/shared/ModuleLoader/ActorForServer.rbxm
Normal file
BIN
src/shared/ModuleLoader/ActorForServer.rbxm
Normal file
Binary file not shown.
83
src/shared/ModuleLoader/ParallelModuleLoader.luau
Normal file
83
src/shared/ModuleLoader/ParallelModuleLoader.luau
Normal file
@ -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 }
|
||||
10
src/shared/ModuleLoader/RelocatedTemplate.luau
Normal file
10
src/shared/ModuleLoader/RelocatedTemplate.luau
Normal file
@ -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
|
||||
759
src/shared/ModuleLoader/init.luau
Normal file
759
src/shared/ModuleLoader/init.luau
Normal file
@ -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 --
|
||||
-----------------------------
|
||||
|
||||
-- <strong><code>!YIELDS!</code></strong>
|
||||
local function waitForEither<Func, T...>(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: 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<S, T>(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: { 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 <code>descendant</code> in the child hierarchy of <code>root</code>.
|
||||
-- If the descendant is not found in <code>root</code>, 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
|
||||
|
||||
--[[
|
||||
<strong><code>!YIELDS!</code></strong>
|
||||
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 <code>settings</code> 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
|
||||
}
|
||||
10
src/shared/ModuleLoader/init.meta.json
Normal file
10
src/shared/ModuleLoader/init.meta.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"attributes": {
|
||||
"ClientWaitForServer": true,
|
||||
"FolderSearchDepth": 3.0,
|
||||
"LoaderTag": "LOAD_MODULE",
|
||||
"UseCollectionService": true,
|
||||
"VerboseLoading": false,
|
||||
"YieldThreshold": 10.0
|
||||
}
|
||||
}
|
||||
57
src/shared/Modules/Client/GetCharacter.luau
Normal file
57
src/shared/Modules/Client/GetCharacter.luau
Normal file
@ -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
|
||||
10
src/shared/Modules/Client/GetCharacter.meta.json
Normal file
10
src/shared/Modules/Client/GetCharacter.meta.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"properties": {
|
||||
"Tags": [
|
||||
"LOAD_MODULE"
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"ClientOnly": true
|
||||
}
|
||||
}
|
||||
22
src/shared/Modules/Client/UI/Display.luau
Normal file
22
src/shared/Modules/Client/UI/Display.luau
Normal file
@ -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
|
||||
10
src/shared/Modules/Client/UI/Display.meta.json
Normal file
10
src/shared/Modules/Client/UI/Display.meta.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"properties": {
|
||||
"Tags": [
|
||||
"LOAD_MODULE"
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"ClientOnly": true
|
||||
}
|
||||
}
|
||||
42
src/shared/Modules/Client/UI/Lock.luau
Normal file
42
src/shared/Modules/Client/UI/Lock.luau
Normal file
@ -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
|
||||
10
src/shared/Modules/Client/UI/Lock.meta.json
Normal file
10
src/shared/Modules/Client/UI/Lock.meta.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"properties": {
|
||||
"Tags": [
|
||||
"LOAD_MODULE"
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"ClientOnly": true
|
||||
}
|
||||
}
|
||||
0
src/shared/Modules/Data/.gitkeep
Normal file
0
src/shared/Modules/Data/.gitkeep
Normal file
198
src/shared/Modules/Utilities/FerrUtils.luau
Normal file
198
src/shared/Modules/Utilities/FerrUtils.luau
Normal file
@ -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
|
||||
@ -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
|
||||
101
src/shared/Modules/Utilities/Observers/_observeAttribute.luau
Normal file
101
src/shared/Modules/Utilities/Observers/_observeAttribute.luau
Normal file
@ -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
|
||||
@ -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
|
||||
104
src/shared/Modules/Utilities/Observers/_observeChildren.luau
Normal file
104
src/shared/Modules/Utilities/Observers/_observeChildren.luau
Normal file
@ -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
|
||||
@ -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
|
||||
80
src/shared/Modules/Utilities/Observers/_observePlayer.luau
Normal file
80
src/shared/Modules/Utilities/Observers/_observePlayer.luau
Normal file
@ -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
|
||||
91
src/shared/Modules/Utilities/Observers/_observeProperty.luau
Normal file
91
src/shared/Modules/Utilities/Observers/_observeProperty.luau
Normal file
@ -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
|
||||
196
src/shared/Modules/Utilities/Observers/_observeTag.luau
Normal file
196
src/shared/Modules/Utilities/Observers/_observeTag.luau
Normal file
@ -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<T>(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
|
||||
11
src/shared/Modules/Utilities/Observers/init.luau
Normal file
11
src/shared/Modules/Utilities/Observers/init.luau
Normal file
@ -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)
|
||||
}
|
||||
5
src/shared/Modules/Utilities/Observers/init.meta.json
Normal file
5
src/shared/Modules/Utilities/Observers/init.meta.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"properties": {
|
||||
"SourceAssetId": 119664548067214.0
|
||||
}
|
||||
}
|
||||
180
src/shared/Modules/Utilities/Signal.luau
Normal file
180
src/shared/Modules/Utilities/Signal.luau
Normal file
@ -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
|
||||
1350
src/shared/Modules/Utilities/t.luau
Normal file
1350
src/shared/Modules/Utilities/t.luau
Normal file
File diff suppressed because it is too large
Load Diff
27
src/shared/Modules/Utilities/throttle.luau
Normal file
27
src/shared/Modules/Utilities/throttle.luau
Normal file
@ -0,0 +1,27 @@
|
||||
--!strict
|
||||
local timeThrottle: { [any]: number } = {}
|
||||
|
||||
return function<T, A...>(
|
||||
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
|
||||
5
src/shared/Modules/Utilities/throttle.meta.json
Normal file
5
src/shared/Modules/Utilities/throttle.meta.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"properties": {
|
||||
"SourceAssetId": 127265032895682.0
|
||||
}
|
||||
}
|
||||
35
src/shared/Modules/Utilities/waitWithTimeout.luau
Normal file
35
src/shared/Modules/Utilities/waitWithTimeout.luau
Normal file
@ -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
|
||||
5
src/shared/Modules/Utilities/waitWithTimeout.meta.json
Normal file
5
src/shared/Modules/Utilities/waitWithTimeout.meta.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"properties": {
|
||||
"SourceAssetId": 116382382498753.0
|
||||
}
|
||||
}
|
||||
7
wally.toml
Normal file
7
wally.toml
Normal file
@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "zaremate/coreography"
|
||||
version = "0.1.0"
|
||||
registry = "https://github.com/UpliftGames/wally-index"
|
||||
realm = "shared"
|
||||
|
||||
[dependencies]
|
||||
Loading…
x
Reference in New Issue
Block a user