Compile Red library from corescripts/Libraries, update types to make Red FULLY TYPE SAFE!

This commit is contained in:
Lewin Kelly 2024-01-28 17:10:52 +00:00
parent d0c2b9d636
commit 7c4ee737f1
16 changed files with 1726 additions and 1515 deletions

View File

@ -0,0 +1,411 @@
return function(IsServer: boolean)
local RunService = game:GetService "RunService"
local RedEvent = require "../RedEvent"(IsServer)
local Remote = RedEvent.Remote
local Serdes = require "./Serdes"(IsServer)
local Spawn = require "../Util/Spawn"
local Promise = require "../Util/Promise"
local Clock = require "../Util/Clock"
local Event = {}
local nil_symbol = { __nil = true }
Event.Callbacks = {} :: { [string]: ((...any) -> ...any)? }
Event.Outgoing = {} :: any
if not IsServer then
Event.ActiveCalls = {}
end
function Event.Listen()
-- debug.setmemorycategory "Red.Listen"
if not IsServer then
Remote.OnClientEvent:connect(
function(SingleFire, MultipleFire, IncomingCall)
-- debug.profilebegin "Red.Listen.Incoming"
-- replace nil symbols with nil
if SingleFire.__nil then
SingleFire = nil
end
if MultipleFire.__nil then
MultipleFire = nil
end
if IncomingCall.__nil then
IncomingCall = nil
end
if SingleFire then
-- debug.profilebegin "Red.Listen.Incoming.SingleFire"
for EventId, Call in pairs(SingleFire) do
local Callback = Event.Callbacks[EventId]
local c = 0
repeat -- bruh
RunService.Stepped:wait()
Callback = Event.Callbacks[EventId]
c += 1
until Callback or c > 500 -- random
if Callback then
if type(Call) == "table" then
Spawn(Callback, unpack(Call))
else
Spawn(Callback, Call)
end
else
print "[Red]: Callback not found!"
end
end
-- debug.profileend()
end
if MultipleFire then
-- debug.profilebegin "Red.Listen.Incoming.Fire"
for EventId, Calls in pairs(MultipleFire) do
local Callback = Event.Callbacks[EventId]
if Callback then
for _, Call in ipairs(Calls) do
if type(Call) == "table" then
Spawn(Callback, unpack(Call))
else
Spawn(Callback, Call)
end
end
end
end
-- debug.profileend()
end
if IncomingCall then
-- debug.profilebegin "Red.Listen.Incoming.Call"
for _, Call in pairs(IncomingCall) do
local CallId = table.remove(Call, 1)
local Success = table.remove(Call, 1)
if Event.ActiveCalls[CallId] then
if Success then
Event.ActiveCalls[CallId].Resolve(
unpack(Call)
)
else
Event.ActiveCalls[CallId].Reject(
unpack(Call)
)
end
Event.ActiveCalls[CallId] = nil
end
end
-- debug.profileend()
end
-- debug.profileend()
end
)
Clock.new(1 / 60, function()
-- debug.profilebegin "Red.Listen.Outgoing"
if not next(Event.Outgoing) then
return
end
local SingleFire = {}
local SendSingleFire = false
if Event.Outgoing[1] then
for EventId, Calls in pairs(Event.Outgoing[1]) do
if #Calls == 1 then
SingleFire[EventId] = Calls[1]
Event.Outgoing[1][EventId] = nil
SendSingleFire = true
end
end
end
-- nils cannot be sent properly across remoteevents in 2013,
-- so we have to use a symbol to represent nil
local sf = nil_symbol
if SendSingleFire then
sf = SingleFire
end
local eo1, eo2 = Event.Outgoing[1], Event.Outgoing[2]
if eo1 == nil then
eo1 = nil_symbol
end
if eo2 == nil then
eo2 = nil_symbol
end
Remote:FireServer(sf, eo1, eo2)
-- table.clear(Event.Outgoing)
for i, _ in pairs(Event.Outgoing) do
Event.Outgoing[i] = nil
end
-- debug.profileend()
end)
else
Remote.OnServerEvent:connect(
function(Player, SingleFire, MultipleFire, IncomingCall)
-- debug.profilebegin "Red.Listen.Incoming"
-- replace nil symbols with nil
if SingleFire.__nil then
SingleFire = nil
end
if MultipleFire.__nil then
MultipleFire = nil
end
if IncomingCall.__nil then
IncomingCall = nil
end
if SingleFire then
-- debug.profilebegin "Red.Listen.Incoming.SingleFire"
for EventId, Call in pairs(SingleFire) do
local Callback = Event.Callbacks[EventId]
if Callback then
if type(Call) == "table" then
Spawn(Callback, Player, unpack(Call))
else
Spawn(Callback, Player, Call)
end
end
end
-- debug.profileend()
end
if MultipleFire then
-- debug.profilebegin "Red.Listen.Incoming.MultipleFire"
for EventId, Calls in pairs(MultipleFire) do
local Callback = Event.Callbacks[EventId]
if Callback then
for _, Call in ipairs(Calls) do
if type(Call) == "table" then
Spawn(Callback, Player, unpack(Call))
else
Spawn(Callback, Player, Call)
end
end
end
end
-- debug.profileend()
end
if IncomingCall then
-- debug.profilebegin "Red.Listen.Incoming.Call"
for EventId, Calls in pairs(IncomingCall) do
if Event.Callbacks[EventId] then
for _, Call in ipairs(Calls) do
Spawn(function()
local CallId = table.remove(Call, 1)
local Result = {
CallId,
pcall(
Event.Callbacks[EventId],
Player,
unpack(Call)
),
}
if Event.Outgoing[Player] == nil then
Event.Outgoing[Player] = {}
end
if Event.Outgoing[Player][2] == nil then
Event.Outgoing[Player][2] = {}
end
table.insert(
Event.Outgoing[Player][2],
Result
)
end)
end
else
if Event.Outgoing[Player] == nil then
Event.Outgoing[Player] = {}
end
if Event.Outgoing[Player][2] == nil then
Event.Outgoing[Player][2] = {}
end
for _, Call in ipairs(Calls) do
table.insert(Event.Outgoing[Player][2], {
Call[1],
false,
"[Red]: Event not found",
})
end
end
end
-- debug.profileend()
end
-- debug.profileend()
end
)
RunService.Heartbeat:connect(function()
-- debug.profilebegin "Red.Listen.Outgoing"
for Player, Packets in pairs(Event.Outgoing) do
local SingleCall = {}
local SendSingleCall = false
if Packets[1] then
for EventId, Calls in pairs(Packets[1]) do
if #Calls == 1 then
SingleCall[EventId] = Calls[1]
Packets[1][EventId] = nil
SendSingleCall = true
end
end
end
-- nils cannot be sent properly across remoteevents in 2013,
-- so we have to use a symbol to represent nil
local sc = nil_symbol
if SendSingleCall then
sc = SingleCall
end
local p1, p2 = Packets[1], Packets[2]
if p1 == nil then
p1 = nil_symbol
end
if p2 == nil then
p2 = nil_symbol
end
Remote:FireClient(Player, sc, p1, p2)
end
-- table.clear(Event.Outgoing)
for i, _ in pairs(Event.Outgoing) do
Event.Outgoing[i] = nil
end
-- debug.profileend()
end)
end
end
function Event.AddQueue(Queue: { any }, Call: { any })
local Length = #Call
if Length == 1 then
local Type = type(Call[1])
if Type ~= "table" then
table.insert(Queue, Call[1])
else
table.insert(Queue, Call)
end
else
table.insert(Queue, Call)
end
end
function Event.FireClient(Player: Player, EventName: string, ...)
assert(IsServer, "Event.FireClient can only be called from the server")
local EventId = Serdes.IdentifierAsync(EventName)
if Event.Outgoing[Player] == nil then
Event.Outgoing[Player] = {}
end
if Event.Outgoing[Player][1] == nil then
Event.Outgoing[Player][1] = {}
end
if Event.Outgoing[Player][1][EventId] == nil then
Event.Outgoing[Player][1][EventId] = {}
end
Event.AddQueue(Event.Outgoing[Player][1][EventId], { ... })
end
function Event.FireServer(EventName: string, ...)
assert(
not IsServer,
"Event.FireServer can only be called on the client"
)
local Args = { ... }
return Serdes.Identifier(EventName):Then(function(EventId)
if Event.Outgoing[1] == nil then
Event.Outgoing[1] = {}
end
if Event.Outgoing[1][EventId] == nil then
Event.Outgoing[1][EventId] = {}
end
Event.AddQueue(Event.Outgoing[1][EventId], Args)
end)
end
function Event.Call(EventName: string, ...)
assert(not IsServer, "Event.Call can only be called on the client")
local Args = { ... }
return Promise.new(function(Resolve, Reject)
local CallId = Serdes.OneTime()
local EventId = Serdes.IdentifierAsync(EventName)
if Event.Outgoing[2] == nil then
Event.Outgoing[2] = {}
end
if Event.Outgoing[2][EventId] == nil then
Event.Outgoing[2][EventId] = {}
end
table.insert(Args, 1, CallId)
table.insert(Event.Outgoing[2][EventId], Args)
Event.ActiveCalls[CallId] = {
Resolve = Resolve,
Reject = Reject,
}
end)
end
function Event.SetCallback(EventName: string, Callback: ((...any) -> any)?)
return Serdes.Identifier(EventName):Then(function(EventId)
Event.Callbacks[EventId] = Callback
end)
end
return Event
end

View File

@ -0,0 +1,76 @@
return function(IsServer: boolean)
local RedEvent = require "../RedEvent"(IsServer)
local Event = RedEvent.Remote
local Promise = require "../Util/Promise"
local Serdes = {}
Serdes.NextId = 1 -- StringValues think null bytes are empty strings
Serdes.NextOT = 1
function Serdes.RegisterIdentifier(Name: string)
assert(IsServer, "RegisterIdentifier can only be called on the server")
local Id = string.char(Serdes.NextId)
Serdes.NextId += 1
local e = Event:FindFirstChild(Name)
if e then
e.Value = Id
else
e = Instance.new "StringValue"
e.Name = Name
e.Value = Id
e.Parent = Event
end
return Id
end
function Serdes.Identifier(Name: string)
if not IsServer then
return Promise.new(function(Resolve)
local e = Event:WaitForChild(Name)
if e.Value ~= nil then
Resolve(e.Value)
else
local Thread = Delay(5, function()
print(
"[Red.Serdes]: Retrieving identifier exceeded 5 seconds. Make sure '"
.. Name
.. "' is registered on the server."
)
end)
e.Changed:Once(function()
coroutine.yield(Thread :: thread)
Resolve(e.Value)
end)
end
end)
else
local e = Event:FindFirstChild(Name)
if e and e.Value then
return Promise.Resolve(e.Value)
end
return Promise.Resolve(Serdes.RegisterIdentifier(Name))
end
end
function Serdes.IdentifierAsync(Name: string)
return Serdes.Identifier(Name):Await()
end
function Serdes.OneTime()
Serdes.NextOT += 1
if Serdes.NextOT == 0xFFFF + 1 then
Serdes.NextOT = 0
end
return string.char(Serdes.NextOT)
end
return Serdes
end

198
Libraries/Red/Net/init.luau Normal file
View File

@ -0,0 +1,198 @@
return function(IsServer: boolean)
local Players = game:GetService "Players"
local RedEvent = require "../RedEvent"(IsServer)
local Remote = RedEvent.Remote
local ClientFolder = RedEvent.ClientFolder
local Serdes = require "./Serdes"(IsServer)
local Event = require "./Event"(IsServer)
local Server = {}
Server.__index = Server
function Server.Server(Name: string)
local self = setmetatable({}, Server)
self.Name = Name
self.FolderInstance = nil :: Folder?
return self
end
Server.new = Server.Server
function Server.Fire(self: Server, Player: Player, EventName: string, ...)
Event.FireClient(Player, self.Name .. "_" .. EventName, ...)
end
function Server.FireAll(self: Server, EventName: string, ...)
for _, Player in ipairs(Players:GetPlayers()) do
self:Fire(Player, EventName, ...)
end
end
function Server.FireAllExcept(
self: Server,
Player: Player,
EventName: string,
...
)
for _, OtherPlayer in ipairs(Players:GetPlayers()) do
if OtherPlayer ~= Player then
self:Fire(OtherPlayer, EventName, ...)
end
end
end
function Server.FireList(
self: Server,
PlayerList: { Player },
EventName: string,
...
)
for _, Player in ipairs(PlayerList) do
self:Fire(Player, EventName, ...)
end
end
function Server.FireWithFilter(
self: Server,
Filter: (Player) -> boolean,
EventName: string,
...
)
for _, Player in ipairs(Players:GetPlayers()) do
if Filter(Player) then
self:Fire(Player, EventName, ...)
end
end
end
function Server.On(
self: Server,
EventName: string,
Callback: ((Player, ...any) -> ...any)?
)
Event.SetCallback(self.Name .. "_" .. EventName, Callback)
end
function Server.Folder(self: Server, Player: Player?)
if Player then
ClientFolder = (
Player:FindFirstChild "PlayerGui" :: any
).Red :: ScreenGui
if ClientFolder:FindFirstChild(self.Name) then
return ClientFolder:FindFirstChild(self.Name) :: Model
else
local Folder = Instance.new "Model"
Folder.Name = self.Name
Folder.Parent = ClientFolder
return Folder :: Model
end
else
if not self.FolderInstance then
local Folder = Instance.new "Model"
Folder.Name = self.Name
Folder.Parent = Remote
self.FolderInstance = Folder
end
return self.FolderInstance :: Model
end
end
export type Server = typeof(Server.Server "")
local Client = {}
Client.__index = Client
function Client.Client(Name: string)
local self = setmetatable({}, Client)
self.Name = Name
self.FolderInstance = nil :: Folder?
self.LocalFolderInstance = nil :: Folder?
return self
end
Client.new = Client.Client
function Client.Fire(self: Client, EventName: string, ...)
return Event.FireServer(self.Name .. "_" .. EventName, ...)
end
function Client.Call(self: Client, EventName: string, ...)
return Event.Call(self.Name .. "_" .. EventName, ...)
end
function Client.On(
self: Client,
EventName: string,
Callback: ((...any) -> ())?
)
return Event.SetCallback(self.Name .. "_" .. EventName, Callback)
end
function Client.Folder(self: Client)
if not self.FolderInstance then
self.FolderInstance = Remote:WaitForChild(self.Name) :: Model
end
return self.FolderInstance :: Model
end
function Client.LocalFolder(self: Client)
if not self.LocalFolderInstance then
self.LocalFolderInstance =
ClientFolder:WaitForChild(self.Name) :: Model
end
return self.LocalFolderInstance :: Model
end
export type Client = typeof(Client.Client "")
local Net = {}
Net.ServerNamespaceList = {}
Net.ClientNamespaceList = {}
function Net.Server(Name: string, Definitions: { string }?): Server
assert(IsServer, "Net.Server can only be used on the server")
if not Net.ServerNamespaceList[Name] then
Net.ServerNamespaceList[Name] = Server.Server(Name)
end
if Definitions then
for _, Term in ipairs(Definitions) do
Serdes.Identifier((Name :: string) .. "_" .. Term)
end
end
return Net.ServerNamespaceList[Name]
end
function Net.Client(Name: string): Client
assert(not IsServer, "Net.Client can only be used on the client")
if Net.ClientNamespaceList[Name] == nil then
Net.ClientNamespaceList[Name] = Client.Client(Name)
end
return Net.ClientNamespaceList[Name]
end
function Net.Identifier(Name: string)
return Serdes.Identifier(Name)
end
Event.Listen()
return Net
end

View File

@ -0,0 +1,45 @@
return function(IsServer: boolean)
local ReplicatedStorage = game:GetService "ReplicatedStorage"
local Players = game:GetService "Players"
local RedEvent = {}
local Remote, ClientFolder
if IsServer then
Remote = ReplicatedStorage:FindFirstChild "RedEvent" :: RemoteEvent
if not Remote then -- prevent vile promise bugs
Remote = Instance.new "RemoteEvent"
Remote.Name = "RedEvent"
Remote.Parent = ReplicatedStorage
end
local function PlayerAdded(Player: Player)
ClientFolder = Instance.new "ScreenGui"
-- ClientFolder.Enabled = false
-- ClientFolder.ResetOnSpawn = false
ClientFolder.Name = "Red"
ClientFolder.Parent = Player:FindFirstChild "PlayerGui"
end
Players.PlayerAdded:connect(PlayerAdded)
for _, Player in ipairs(Players:GetPlayers()) do
PlayerAdded(Player)
end
else
Remote = ReplicatedStorage:WaitForChild "RedEvent" :: RemoteEvent
ClientFolder = (
Players.LocalPlayer:FindFirstChild "PlayerGui" :: PlayerGui
):WaitForChild "Red" :: ScreenGui
ClientFolder.Parent = nil
end
RedEvent.Remote = Remote
RedEvent.ClientFolder = ClientFolder
return RedEvent
end

View File

@ -0,0 +1,27 @@
local Spawn = require "./Spawn"
local typeof = require "../../../Modules/Polyfill/typeof"
type BinItem = Instance | RBXScriptConnection | () -> ...any
return function()
local Bin: { BinItem } = {}
return function(Item: BinItem)
table.insert(Bin, Item)
end, function()
for _, Item in ipairs(Bin) do
if typeof(Item) == "Instance" then
Item:Destroy()
elseif typeof(Item) == "RBXScriptConnection" then
Item:disconnect()
elseif typeof(Item) == "function" then
Spawn(Item)
end
end
-- table.clear(Bin)
for i, _ in ipairs(Bin) do
Bin[i] = nil
end
end
end

View File

@ -0,0 +1,67 @@
-- local ReplicatedStorage = game:GetService "ReplicatedStorage"
local RunService = game:GetService "RunService"
local function MakeHeartbeatFunction(Clock: Clock)
return function(Delta)
Clock:Advance(Delta)
end
end
local Clock = {}
Clock.__index = Clock
function Clock.Clock(Interval: number, Callback: () -> ())
local self = setmetatable({}, Clock)
self.Interval = Interval
self.Callback = Callback
self.Delta = 0
self.Connection = RunService.Heartbeat:connect(MakeHeartbeatFunction(self))
return self
end
Clock.new = Clock.Clock
function Clock.Pause(self: Clock)
if self.Connection then
self.Connection:Disconnect()
end
end
function Clock.Resume(self: Clock)
if self.Connection.Connected then
return
end
self.Connection = RunService.Heartbeat:connect(MakeHeartbeatFunction(self))
end
function Clock.Advance(self: Clock, Delta: number)
self.Delta += Delta
if self.Delta >= self.Interval * 10 then
local Skipped = math.floor(self.Delta / self.Interval)
self.Delta -= Skipped * self.Interval
-- if ReplicatedStorage:GetAttribute "RedDebug" then
-- warn(
-- "[Red.Clock]: Clock is falling behind! Skipped "
-- .. Skipped
-- .. " intervals."
-- )
-- end
return
end
if self.Delta >= self.Interval then
self.Delta -= self.Interval
self.Callback()
end
end
export type Clock = typeof(Clock.Clock(...))
return Clock

View File

@ -0,0 +1,41 @@
local CollectionService = game:GetService "CollectionService"
local Spawn = require "./Spawn"
return function<T...>(
Tag: string,
Start: (Instance) -> T...,
Stop: (T...) -> ()
): () -> ()
local InstanceMap = {}
for _, Instance in ipairs(CollectionService:GetTagged(Tag)) do
Spawn(function()
InstanceMap[Instance] = { Start(Instance) }
end)
end
local AddConnection = CollectionService:GetInstanceAddedSignal(Tag)
:connect(function(Instance)
InstanceMap[Instance] = { Start(Instance) }
end)
local RemoveConnection = CollectionService:GetInstanceRemovedSignal(Tag)
:connect(function(Instance)
local Value = InstanceMap[Instance]
if Value then
InstanceMap[Instance] = nil
Stop(unpack(Value))
end
end)
return function()
AddConnection:Disconnect()
RemoveConnection:Disconnect()
for Instance, Value in pairs(InstanceMap) do
Spawn(Stop, unpack(Value))
end
end
end

View File

@ -0,0 +1,206 @@
local RunService = game:GetService "RunService"
local Spawn = require "./Spawn"
local Promise = {}
Promise.__index = Promise
function Promise.Promise(Callback: (
Resolve: (...any) -> (),
Reject: (...any) -> ()
) -> ())
local self = setmetatable({}, Promise)
self.Status = "Pending"
self.OnResolve = {} :: { (...any) -> () }
self.OnReject = {} :: { (...any) -> () }
self.Value = {} :: { any }
self.Thread = nil :: thread?
self.Thread = coroutine.create(function()
local ok, err = ypcall(Callback, function(...)
self:_Resolve(...)
end, function(...)
self:_Reject(...)
end)
if not ok then
self:_Reject(err)
end
end)
coroutine.resume(self.Thread :: thread)
return self
end
Promise.new = Promise.Promise :: (
Callback: (Resolve: (...any) -> (), Reject: (...any) -> ()) -> ()
) -> Promise
function Promise.Resolve(...: any): Promise
local self = setmetatable({}, Promise)
self.Status = "Resolved"
self.OnResolve = {} :: { (...any) -> () }
self.OnReject = {} :: { (...any) -> () }
self.Value = { ... } :: { any }
self.Thread = nil :: thread?
return self
end
function Promise.Reject(...: any): Promise
local self = setmetatable({}, Promise)
self.Status = "Rejected"
self.OnResolve = {} :: { (...any) -> () }
self.OnReject = {} :: { (...any) -> () }
self.Value = { ... } :: { any }
self.Thread = nil :: thread?
return self
end
function Promise._Resolve(self: Promise, ...: any)
assert(
self.Status == "Pending",
"Cannot resolve a promise that is not pending."
)
self.Status = "Resolved"
self.Value = { ... }
for _, Callback in ipairs(self.OnResolve) do
Spawn(Callback, ...)
end
-- task.defer(task.cancel, self.Thread :: thread)
coroutine.resume(coroutine.create(function()
coroutine.yield(self.Thread :: thread)
end))
end
function Promise._Reject(self: Promise, ...: any)
assert(
self.Status == "Pending",
"Cannot reject a promise that is not pending."
)
self.Status = "Rejected"
self.Value = { ... }
for _, Callback in ipairs(self.OnReject) do
Spawn(Callback, ...)
end
-- task.defer(task.cancel, self.Thread :: thread)
coroutine.resume(coroutine.create(function()
coroutine.yield(self.Thread :: thread)
end))
end
function Promise.Then(
self: Promise,
OnResolve: ((...any) -> ...any)?,
OnReject: ((...any) -> ...any)?
): Promise
return Promise.Promise(function(Resolve, Reject)
local function PromiseResolutionProcedure(
Value: Promise | any,
...: any
)
if type(Value) == "table" and getmetatable(Value) == Promise then
if Value.Status == "Pending" then
table.insert(Value.OnResolve, Resolve)
table.insert(Value.OnReject, Reject)
elseif Value.Status == "Resolved" then
Resolve(Value.Value)
elseif Value.Status == "Rejected" then
Reject(Value.Value)
end
else
Resolve(Value, ...)
end
end
if self.Status == "Pending" then
if OnResolve then
table.insert(self.OnResolve, function(...)
PromiseResolutionProcedure(OnResolve(...))
end)
else
table.insert(self.OnResolve, PromiseResolutionProcedure)
end
if OnReject then
table.insert(self.OnReject, function(...)
PromiseResolutionProcedure(OnReject(...))
end)
else
table.insert(self.OnReject, Reject)
end
elseif self.Status == "Resolved" then
if OnResolve then
PromiseResolutionProcedure(OnResolve(unpack(self.Value)))
else
Resolve(unpack(self.Value))
end
elseif self.Status == "Rejected" then
if OnReject then
PromiseResolutionProcedure(OnReject(unpack(self.Value)))
else
Reject(unpack(self.Value))
end
end
end)
end
function Promise.Catch(self: Promise, OnReject: (...any) -> ())
return self:Then(nil, OnReject)
end
function Promise.Finally(self: Promise, Finally: () -> ())
return self:Then(function(...)
Finally()
return ...
end, function(Error)
Finally()
error(Error)
end)
end
function Promise.Await(self: Promise): ...any
if self.Status == "Resolved" then
return unpack(self.Value)
elseif self.Status == "Rejected" then
return error(unpack(self.Value))
end
local c = 0
repeat -- bruh
RunService.Stepped:wait()
c += 1
until self.Status ~= "Pending" or c > 500 -- random
local Current = coroutine.running()
local function Resume()
coroutine.resume(Current)
end
table.insert(self.OnResolve, Resume)
table.insert(self.OnReject, Resume)
coroutine.yield()
if self.Status == "Resolved" then
return unpack(self.Value)
end
return error(unpack(self.Value))
end
export type Promise = typeof(Promise.Promise(...))
return Promise

View File

@ -0,0 +1,40 @@
return function<T>(Limit: number, Interval: number)
assert(Limit > 0, "Limit must be greater than 0")
local CountMap = {} :: { [T]: number }
local CountKeyless = 0
return function(Key: T?)
if Key then
local Count = CountMap[Key]
if Count == nil then
Count = 0
Delay(Interval, function()
CountMap[Key] = nil
end)
end
if Count >= Limit then
return false
end
CountMap[Key] = Count + 1
else
if CountKeyless == 0 then
Delay(Interval, function()
CountKeyless = 0
end)
end
if CountKeyless >= Limit then
return false
end
CountKeyless += 1
end
return true
end
end

View File

@ -0,0 +1,74 @@
local Promise = require "./Promise"
local Spawn = require "./Spawn"
type SignalNode<T...> = {
Next: SignalNode<T...>?,
Callback: (T...) -> (),
}
export type Signal<T...> = {
Root: SignalNode<T...>?,
Connect: (self: Signal<T...>, Callback: (T...) -> ()) -> () -> (),
Wait: (self: Signal<T...>) -> Promise.Promise,
Fire: (self: Signal<T...>, T...) -> (),
DisconnectAll: (self: Signal<T...>) -> (),
}
local Signal = {}
Signal.__index = Signal
function Signal.new<T...>(): Signal<T...>
return setmetatable({
Root = nil,
}, Signal) :: any
end
function Signal.Connect<T...>(self: Signal<T...>, Callback: (T...) -> ()): () -> ()
local Node = {
Next = self.Root,
Callback = Callback,
}
self.Root = Node
return function()
if self.Root == Node then
self.Root = Node.Next
else
local Current = self.Root
while Current do
if Current.Next == Node then
Current.Next = Node.Next
break
end
Current = Current.Next
end
end
end
end
function Signal.Wait<T...>(self: Signal<T...>): Promise.Promise
return Promise.new(function(Resolve)
local Disconnect
Disconnect = self:Connect(function(...)
Disconnect()
Resolve(... :: any)
end)
end)
end
function Signal.Fire<T...>(self: Signal<T...>, ...: T...)
local Current = self.Root
while Current do
Spawn(Current.Callback, ...)
Current = Current.Next
end
end
function Signal.DisconnectAll<T...>(self: Signal<T...>)
self.Root = nil
end
return Signal

View File

@ -0,0 +1,23 @@
local FreeThread: thread? = nil
local function FunctionPasser(fn, ...)
local AquiredThread = FreeThread
FreeThread = nil
fn(...)
FreeThread = AquiredThread
end
local function Yielder()
while true do
FunctionPasser(coroutine.yield())
end
end
return function<T...>(fn: (T...) -> (), ...: T...)
if not FreeThread then
FreeThread = coroutine.create(Yielder)
coroutine.resume(FreeThread :: any)
end
coroutine.resume(FreeThread :: thread, fn, ...)
end

33
Libraries/Red/init.luau Normal file
View File

@ -0,0 +1,33 @@
local function Red(_, Script: LuaSourceContainer)
local _SERVER
if Script:IsA "LocalScript" then
_SERVER = false
elseif Script:IsA "Script" then
_SERVER = true
else
error("Argument must be the script itself", 2)
end
local Net = require "./Net"(_SERVER)
return {
Server = Net.Server,
Client = Net.Client,
Collection = require "./Util/Collection",
Ratelimit = require "./Util/Ratelimit",
Promise = require "./Util/Promise",
Signal = require "./Util/Signal",
Clock = require "./Util/Clock",
Spawn = require "./Util/Spawn",
Bin = require "./Util/Bin",
}
end
return {
Help = function()
return "See https://redblox.dev/ for more information."
end,
Load = Red,
}

181
Libraries/Red/typeof.luau Normal file
View File

@ -0,0 +1,181 @@
-- A basic polyfill for the typeof function
return function(value)
local basicType = type(value)
if
basicType == "nil"
or basicType == "boolean"
or basicType == "number"
or basicType == "string"
or basicType == "function"
or basicType == "thread"
or basicType == "table"
then
return basicType
end
-- Will short-circuit
--[[
{
name of type to check,
{ list of required properties },
}
]]
local tests = {
{
"Instance",
{ "ClassName" },
},
{
"EnumItem",
{ "EnumType", "Name", "Value" },
},
{
"Enum",
{ "GetEnumItems" },
},
{
"Enums",
{ "MembershipType" }, -- lmao
},
{
"RBXScriptSignal",
{
"connect",
-- "connected",
-- "connectFirst",
-- "connectLast",
"wait",
},
},
{
"RBXScriptConnection",
{
"connected",
"disconnect",
},
},
{
"TweenInfo",
{
"EasingDirection",
-- "Time",
-- "DelayTime",
"RepeatCount",
"EasingStyle",
-- "Reverses",
},
},
{
"CFrame",
{
"p",
"x",
"y",
"z",
"lookVector",
},
},
{
"Vector3",
{
"Lerp",
-- "Cross",
-- "Dot",
"unit",
"magnitude",
"x",
"y",
"z",
},
},
{
"Vector3int16",
{ "z", "x", "y" },
},
{
"Vector2",
{ "unit", "magnitude", "x", "y" },
},
{
"Vector2int16",
{ "x", "y" },
},
{
"Region3",
{ "CFrame", "Size" },
},
{
"Region3int16",
{ "Min", "Max" },
},
{
"Ray",
{
"Origin",
"Direction",
"Unit",
"ClosestPoint",
"Distance",
},
},
{
"UDim",
{ "Scale", "Offset" },
},
{
"Axes",
{ "Z", "X", "Y" },
},
{
"UDim2",
{ "X", "Y" },
},
{
"BrickColor",
{
"Number",
"Name",
"Color",
"r",
"g",
"b",
},
},
{
"Color3",
{ "r", "g", "b" },
},
{
"Faces",
{
"Right",
"Top",
"Back",
-- "Left",
-- "Bottom",
-- "Front",
},
},
}
for _, v in ipairs(tests) do
local t, test = v[1], v[2]
local ok, result = pcall(function()
for _, prop in ipairs(test) do
if value[prop] == nil then
return false
end
-- Cannot throw if the property does not exist,
-- as userdatas may allow nil indexing
end
return true
end)
if ok and result then
return t
end
end
end

View File

@ -5,6 +5,7 @@ done
echo "Processing libraries..."
darklua process -c dense.json5 ./corescripts/Libraries/Fusion/init.luau ./corescripts/processed/10000001.lua
darklua process -c bundle.json5 ./corescripts/Libraries/Red/init.luau ./corescripts/processed/10000002.lua
echo "Processing other corescripts..."
for file in ./corescripts/luau/[a-z]*.luau; do

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff