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