This commit is contained in:
ZareMate 2026-03-02 22:23:43 +01:00
commit e70d96a953
48 changed files with 4217 additions and 0 deletions

11
.gitignore vendored Normal file
View 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
View 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
View 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

Binary file not shown.

27
default.project.json Normal file
View 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
View 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
View File

@ -0,0 +1 @@
std = "roblox"

View 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)

View 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

View File

@ -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

View File

@ -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

View 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

View 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

View 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

View 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

View File

@ -0,0 +1,7 @@
{
"properties": {
"Tags": [
"LOAD_MODULE"
]
}
}

View 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

Binary file not shown.

Binary file not shown.

View 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 }

View 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

View 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
}

View File

@ -0,0 +1,10 @@
{
"attributes": {
"ClientWaitForServer": true,
"FolderSearchDepth": 3.0,
"LoaderTag": "LOAD_MODULE",
"UseCollectionService": true,
"VerboseLoading": false,
"YieldThreshold": 10.0
}
}

View 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

View File

@ -0,0 +1,10 @@
{
"properties": {
"Tags": [
"LOAD_MODULE"
]
},
"attributes": {
"ClientOnly": true
}
}

View 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

View File

@ -0,0 +1,10 @@
{
"properties": {
"Tags": [
"LOAD_MODULE"
]
},
"attributes": {
"ClientOnly": true
}
}

View 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

View File

@ -0,0 +1,10 @@
{
"properties": {
"Tags": [
"LOAD_MODULE"
]
},
"attributes": {
"ClientOnly": true
}
}

View File

View 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

View File

@ -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

View 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

View File

@ -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

View 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

View File

@ -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

View 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

View 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 theyre 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 propertychanged 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

View 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

View 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)
}

View File

@ -0,0 +1,5 @@
{
"properties": {
"SourceAssetId": 119664548067214.0
}
}

View 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

File diff suppressed because it is too large Load Diff

View 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

View File

@ -0,0 +1,5 @@
{
"properties": {
"SourceAssetId": 127265032895682.0
}
}

View 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

View File

@ -0,0 +1,5 @@
{
"properties": {
"SourceAssetId": 116382382498753.0
}
}

7
wally.toml Normal file
View File

@ -0,0 +1,7 @@
[package]
name = "zaremate/coreography"
version = "0.1.0"
registry = "https://github.com/UpliftGames/wally-index"
realm = "shared"
[dependencies]