Add Jabby module

This commit is contained in:
Ukendio 2026-02-18 01:29:34 +01:00
parent 22dd91b111
commit aeedea2fcb
123 changed files with 18769 additions and 0 deletions

View file

@ -0,0 +1,106 @@
local ui = require(script.Parent.Parent.Parent.Parent.ui)
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local query_parser = require(script.Parent.Parent.Parent.Parent.server.query_parser)
local create = vide.create
local source = vide.source
local show = vide.show
type props = {
changes: vide.Source<{[string]: string}>,
editing: vide.Source<false | string>,
adding: vide.Source<false | string>,
text: vide.Source<string>
}
return function(props: props)
local component_edit_text = source("")
local adding = props.adding
local editing = props.editing
local text = props.text
return create "Folder" {
Name = "Add Component",
show(adding, function()
return create "Frame" {
ZIndex = 1000,
Size = UDim2.new(1,16, 1, 16),
Position = UDim2.fromScale(0.5, 0.5),
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundColor3 = Color3.new(0, 0, 0),
BackgroundTransparency = 0.5,
Active = true,
create "UIListLayout" {
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
Padding = UDim.new(0, 8)
},
ui.padding {
padding = UDim.new(0, 32)
},
ui.textfield {
size = UDim2.fromOffset(200, 30),
placeholder = "Entity",
oninput = component_edit_text,
},
create "Frame" {
Size = UDim2.new(1, 0, 0, 30),
BackgroundTransparency = 1,
AutomaticSize = Enum.AutomaticSize.Y,
create "UIListLayout" {
HorizontalFlex = Enum.UIFlexAlignment.Fill,
FillDirection = Enum.FillDirection.Horizontal,
Padding = UDim.new(0, 8)
},
ui.button {
size = UDim2.fromOffset(150, 30),
text = "Edit",
activated = function()
adding(false)
editing(component_edit_text())
end,
disabled = function()
local ok, node = pcall(query_parser, component_edit_text())
if not ok then return true end
if not node[1] then return true end
if node[2] then return true end
local n = node[1]
if n.type == "Relationship" then
if n.left.type == "Wildcard" then return true end
if n.right.type == "Wildcard" then return true end
end
return false
end,
accent = true
},
ui.button {
size = UDim2.fromOffset(150, 30),
text = "Cancel",
activated = function()
adding(false)
end
},
}
}
end)
}
end

View file

@ -0,0 +1,118 @@
local ui = require(script.Parent.Parent.Parent.Parent.ui)
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local create = vide.create
local show = vide.show
type props = {
components: () -> {[string]: string},
changes: vide.Source<{[string]: string}>,
editing: vide.Source<false | string>,
text: vide.Source<string>
}
return function(props: props)
local editing = props.editing
local text = props.text
local changes = props.changes
return create "Folder" {
Name = "Text Editor",
show(function()
return editing()
end, function()
return create "Frame" {
ZIndex = 1000,
Size = UDim2.new(1,16, 1, 16),
Position = UDim2.fromScale(0.5, 0.5),
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundColor3 = Color3.new(0, 0, 0),
BackgroundTransparency = 0.5,
Active = true,
create "UIListLayout" {
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
VerticalFlex = Enum.UIFlexAlignment.SpaceEvenly,
Padding = UDim.new(0, 8)
},
ui.padding {
padding = UDim.new(0, 32)
},
ui.typography {
text = function()
return `Editing {editing()}`
end
},
create "Frame" {
Size = UDim2.fromScale(1, 0),
create "UIFlexItem" {
FlexMode = Enum.UIFlexMode.Fill
},
BackgroundTransparency = 1,
ui.textfield {
size = UDim2.fromScale(1, 1),
position = UDim2.fromScale(0.5, 0.5),
anchorpoint = Vector2.new(0.5, 0.5),
multiline = true,
code = true,
text = text,
oninput = text,
},
},
create "Frame" {
Size = UDim2.new(1, 0, 0, 30),
BackgroundTransparency = 1,
AutomaticSize = Enum.AutomaticSize.Y,
create "UIListLayout" {
HorizontalFlex = Enum.UIFlexAlignment.Fill,
FillDirection = Enum.FillDirection.Horizontal,
Padding = UDim.new(0, 8)
},
ui.button {
size = UDim2.fromOffset(150, 30),
text = "Save Changes",
activated = function()
local key = editing()
changes()[key] = text()
editing(false)
changes(changes())
if props.components()[key] ~= nil then return end
props.components()[key] = text()
end,
accent = true
},
ui.button {
size = UDim2.fromOffset(150, 30),
text = "Cancel Changes",
activated = function()
editing(false)
end
},
}
}
end)
}
end

View file

@ -0,0 +1,81 @@
local RunService = game:GetService("RunService")
local vide = require(script.Parent.Parent.Parent.vide)
local loop = require(script.Parent.Parent.Parent.modules.loop)
local widget = require(script.widget)
local source = vide.source
local cleanup = vide.cleanup
local overview_entity = {
class_name = "app" :: "app",
name = "Entity"
}
type props = {
host: Player | "server",
vm: number,
id: number,
entity: number,
}
local function generate_random_query_id()
return math.random(2 ^ 31 - 1)
end
function overview_entity.mount(props: props, destroy: () -> ())
local keys = source({})
local changes = source({})
local enable_live_updates = source(true)
local apply_changes = source(false)
local deleting = source(false)
local inspect_id = generate_random_query_id()
-- check if the query and columns are properly
local app_loop = loop (
"app-client-entity",
{
host = props.host,
vm = props.vm,
id = props.id,
inspect_id = inspect_id,
entity = tonumber(props.entity),
keys = keys,
live_updates = enable_live_updates,
changes = changes,
apply_changes = apply_changes,
deleting = deleting
},
{i = 1},
script.systems.obtain_entity_data
)
cleanup(
RunService.Heartbeat:Connect(app_loop)
)
return widget {
host = props.host,
vm = props.vm,
id = props.id,
inspect_id = inspect_id,
entity = props.entity,
components = keys,
live_updates = enable_live_updates,
changes = changes,
apply_changes = apply_changes,
delete = function()
deleting(true)
end,
destroy = destroy
}
end
return overview_entity

View file

@ -0,0 +1,85 @@
local vide = require(script.Parent.Parent.Parent.Parent.Parent.vide)
local queue = require(script.Parent.Parent.Parent.Parent.Parent.modules.queue)
local remotes = require(script.Parent.Parent.Parent.Parent.Parent.modules.remotes)
local effect = vide.effect
local cleanup = vide.cleanup
local batch = vide.batch
type Context = {
host: Player | "server",
vm: number,
id: number,
inspect_id: number,
entity: number,
keys: vide.Source<{[string]: string}>,
changes: vide.Source<{[string]: string}>,
apply_changes: vide.Source<boolean>,
live_updates: () -> boolean,
deleting: () -> boolean,
}
return function(context: Context)
local inspect_entity_update = queue(remotes.inspect_entity_update)
local current_inspectid = context.inspect_id
local outgoing = {
host = context.host,
to_vm = context.vm,
}
remotes.inspect_entity:fire(
outgoing,
context.id,
context.entity,
current_inspectid
)
local settings_changed = false
effect(function()
context.live_updates()
settings_changed = true
end)
cleanup(function()
remotes.stop_inspect_entity:fire(
outgoing,
current_inspectid
)
end)
return function()
if context.apply_changes() then
remotes.update_entity:fire(outgoing, current_inspectid, context.changes())
context.apply_changes(false)
context.changes({})
end
if context.deleting() then
remotes.delete_entity:fire(outgoing, current_inspectid)
end
if settings_changed then
remotes.update_inspect_settings:fire(
outgoing,
current_inspectid,
{paused = not context.live_updates()}
)
settings_changed = false
end
batch(function()
for incoming, inspectid, key, value in inspect_entity_update:iter() do
if inspectid ~= current_inspectid then continue end
context.keys()[key] = value
context.keys(context.keys())
end
end)
end
end

View file

@ -0,0 +1,414 @@
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local ui = require(script.Parent.Parent.Parent.Parent.ui)
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local remotes = require(script.Parent.Parent.Parent.Parent.modules.remotes)
local add_component = require(script.Parent.add_component)
local editor = require(script.Parent.editor)
local create = vide.create
local indexes = vide.indexes
local source = vide.source
local show = vide.show
type SystemId = number
type props = {
host: Player | "server",
vm: number,
id: number,
entity: number,
inspect_id: number,
components: vide.Source<{[string]: string}>,
changes: vide.Source<{[string]: string}>,
live_updates: vide.Source<boolean>,
apply_changes: vide.Source<boolean>,
destroy: () -> (),
delete: () -> ()
}
local mouse_location = source(Vector2.zero)
RunService.PreRender:Connect(function()
mouse_location(UserInputService:GetMouseLocation())
end)
return function(props: props)
local outgoing = {
host = props.host,
to_vm = props.vm,
}
local live_updates = props.live_updates
local changes = props.changes
local text = source("")
local adding = source(false)
local editing = source(false :: false | string)
local function components()
local components = {}
for key, value in props.components() do
if value == "tag" then continue end
components[key] = value
end
return components
end
local function tags()
local tags = {}
for key, value in props.components() do
if value ~= "tag" then continue end
tags[key] = value
end
return tags
end
local function is_removed(value: string)
return value == "nil"
end
local function edit_component(component: string)
remotes.get_component:fire(outgoing, props.inspect_id, component)
editing(component)
text("waiting...")
end
vide.cleanup(remotes.return_component:connect(function(from, inspect, component, value)
if props.host ~= from.host then return end
if props.inspect_id ~= inspect then return end
local current_value = props.changes()[component]
if current_value then
text(current_value)
else
text(value)
end
end))
return ui.widget {
title = `Entity #{props.entity}`,
subtitle = `host: {props.host} vm: {props.vm} id: {props.id}`,
min_size = Vector2.new(300, 300),
bind_to_close = props.destroy,
create "Frame" {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
create "UIListLayout" {
VerticalFlex = Enum.UIFlexAlignment.SpaceEvenly,
Padding = UDim.new(0, 8)
},
editor {
components = props.components,
editing = editing,
text = text,
changes = changes
},
add_component {
editing = edit_component,
text = text,
changes = changes,
adding = adding
},
ui.row {
justifycontent = Enum.UIFlexAlignment.Fill,
ui.button {
text = "Live Updates",
activated = function()
live_updates(not live_updates())
end,
create "UIListLayout" {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
HorizontalFlex = Enum.UIFlexAlignment.SpaceBetween,
Padding = UDim.new(0, 4)
},
ui.checkbox {
size = UDim2.fromOffset(16, 16),
layoutorder = -1,
checked = live_updates,
create "UIFlexItem" {
FlexMode = Enum.UIFlexMode.None,
}
}
},
ui.button {
size = UDim2.fromOffset(130, 30),
text = function()
local total = 0
for _, change in changes() do
total += 1
end
return `Apply {total} Edits`
end,
disabled = function()
return next(changes()) == nil
end,
activated = function()
props.apply_changes(true)
end
},
ui.button {
create "UIFlexItem" {
ItemLineAlignment = Enum.ItemLineAlignment.End,
},
size = UDim2.fromOffset(130, 30),
text = "Cancel changes",
disabled = function()
return next(changes()) == nil
end,
activated = function()
changes({})
end
}
},
create "ScrollingFrame" {
Size = UDim2.fromScale(1, 0),
CanvasSize = UDim2.new(),
AutomaticCanvasSize = Enum.AutomaticSize.Y,
BackgroundColor3 = ui.theme.bg[-1],
ScrollBarThickness = 6,
VerticalScrollBarInset = Enum.ScrollBarInset.Always,
create "UIFlexItem" {
FlexMode = Enum.UIFlexMode.Fill
},
create "UIListLayout" {
Padding = UDim.new(0, 4)
},
ui.typography {text = "Components"},
ui.container {
Size = UDim2.fromScale(1, 0),
AutomaticSize = Enum.AutomaticSize.Y,
create "UIListLayout" {
SortOrder = Enum.SortOrder.Name,
},
indexes(components, function(value, key)
return ui.button {
{ Name = if not string.match(key, "^%a") then "zzzz" .. key else key },
size = UDim2.new(1, 0, 0, 32),
automaticsize = Enum.AutomaticSize.Y,
text = "",
corner = false,
activated = function()
edit_component(key)
end,
create "UIListLayout" {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalFlex = Enum.UIFlexAlignment.SpaceEvenly,
VerticalAlignment = Enum.VerticalAlignment.Center,
Padding = UDim.new(0, 8)
},
ui.padding {
y = UDim.new(0, 4)
},
ui.typography {
size = UDim2.new(0, 100, 0, 18),
automaticsize = Enum.AutomaticSize.Y,
text = key,
code = true,
wrapped = true,
truncate = Enum.TextTruncate.SplitWord,
xalignment = Enum.TextXAlignment.Left,
},
ui.typography {
size = UDim2.fromOffset(0, 18),
automaticsize = Enum.AutomaticSize.Y,
yalignment = Enum.TextYAlignment.Top,
xalignment = Enum.TextXAlignment.Left,
text = value,
wrapped = true,
truncate = Enum.TextTruncate.AtEnd,
code = true,
create "UIFlexItem" {
FlexMode = Enum.UIFlexMode.Fill
},
},
show(function()
return props.changes()[key] ~= nil
end, function()
return ui.typography {
text = function()
local old = value()
local change = props.changes()[key]
return if old == nil then "(added)"
elseif is_removed(change) then "(removed)"
else "(changed)"
end,
disabled = true,
textsize = 14,
}
end),
create "UISizeConstraint" {
MaxSize = Vector2.new(math.huge, 300)
}
}
end),
},
ui.typography {text = "Tags"},
ui.container {
Size = UDim2.fromScale(1, 0),
AutomaticSize = Enum.AutomaticSize.Y,
create "UIListLayout" {
SortOrder = Enum.SortOrder.Name,
},
indexes(tags, function(value, key)
local function did_change()
return changes()[key] ~= nil
end
return ui.button {
{ Name = if not string.match(key, "^%a") then "zzzz" .. key else key },
size = UDim2.new(1, 0, 0, 24),
text = "",
corner = false,
activated = function()
if changes()[key] == "tag" then
changes()[key] = nil
props.components()[key] = nil
-- notify about the change
changes(changes())
props.components(props.components())
elseif changes()[key] then
changes()[key] = nil
-- notify about the change
changes(changes())
else
changes()[key] = "nil"
-- notify about the change
changes(changes())
end
end,
create "UIListLayout" {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalFlex = Enum.UIFlexAlignment.SpaceEvenly,
VerticalAlignment = Enum.VerticalAlignment.Center,
Padding = UDim.new(0, 8)
},
ui.padding {
y = UDim.new(0, 4)
},
ui.typography {
size = UDim2.fromScale(0, 1),
text = key,
code = true,
wrapped = true,
truncate = Enum.TextTruncate.SplitWord,
xalignment = Enum.TextXAlignment.Left,
create "UIFlexItem" {
FlexMode = Enum.UIFlexMode.Fill
}
},
show(did_change, function()
return ui.typography {
text = function()
local old = value()
local change = changes()[key]
return if old == nil then "(added)"
elseif is_removed(change) then "(removed)"
else "(changed)"
end,
disabled = true,
textsize = 14,
}
end)
}
end),
}
},
ui.row {
justifycontent = Enum.UIFlexAlignment.Fill,
ui.button {
size = UDim2.fromOffset(100, 30),
text = "Delete Id",
activated = props.delete
},
ui.button {
size = UDim2.fromOffset(200, 30),
text = "Add Component",
activated = function()
adding(true)
end
},
}
}
}
end

View file

@ -0,0 +1,36 @@
local RunService = game:GetService("RunService")
local vide = require(script.Parent.Parent.Parent.vide)
local loop = require(script.Parent.Parent.Parent.modules.loop)
local widget = require(script.widget)
local cleanup = vide.cleanup
local home = {
class_name = "app" :: "app",
name = "Home"
}
function home.mount(_: nil, destroy: () -> ())
local servers = vide.source {} :: any
local app_loop = loop (
"app-client-home",
servers,
{i = 1},
script.systems.get_core_data
)
cleanup(
RunService.Heartbeat:Connect(app_loop)
)
return widget {
servers = servers,
destroy = destroy
}
end
return home

View file

@ -0,0 +1,93 @@
local Players = game:GetService("Players")
local vide = require(script.Parent.Parent.Parent.Parent.Parent.vide)
local queue = require(script.Parent.Parent.Parent.Parent.Parent.modules.queue)
local remotes = require(script.Parent.Parent.Parent.Parent.Parent.modules.remotes)
local reverse_connector = require(script.Parent.Parent.Parent.Parent.Parent.modules.reverse_connector)
return function(data)
for _, player in Players:GetPlayers() do
remotes.ping:fire({
host = player,
})
end
remotes.ping:fire({
host = "server"
})
local servers_responding = queue(remotes.new_server_registered)
local server_update = queue(remotes.update_server_data)
local player_removing = queue(Players.PlayerRemoving)
local n = 0
local servers = data :: any
local map_to_idx = {
server = {}
}
return function()
for connector in servers_responding:iter() do
local outgoing = reverse_connector(connector)
remotes.bind_to_server_core:fire(outgoing)
end
for player in player_removing:iter() do
local indexes = map_to_idx[player]
if not indexes then continue end
for _, idx in indexes do
servers()[idx] = nil
end
servers(servers())
end
for connector, packet in server_update:iter() do
local outgoing = reverse_connector(connector)
map_to_idx[outgoing.host] = map_to_idx[outgoing.host] or {}
local idx = map_to_idx[outgoing.host][outgoing.to_vm]
if not idx then
-- print("new server")
idx = n + 1; n += 1
map_to_idx[outgoing.host][outgoing.to_vm] = idx
servers()[idx] = {
host = outgoing.host,
vm = outgoing.to_vm,
schedulers = vide.source {},
worlds = vide.source {}
}
servers(servers())
-- print("set worlds")
-- print(servers())
end
local server = servers()[idx]
local schedulers = server.schedulers()
local worlds = server.worlds()
table.clear(schedulers)
table.clear(worlds)
for index, data in packet.schedulers do
local at = schedulers[index]
if at and at.name == data.name and at.id == data.id then continue end
schedulers[index] = data
end
for index, data in packet.worlds do
local at = worlds[index]
if at and at.name == data.name and at.id == data.id then continue end
worlds[index] = data
end
server.schedulers(schedulers)
server.worlds(worlds)
end
end
end

View file

@ -0,0 +1,183 @@
local Players = game:GetService("Players")
local ui = require(script.Parent.Parent.Parent.Parent.ui)
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local spawn_app = require(script.Parent.Parent.Parent.spawn_app)
local overview_scheduler = require(script.Parent.Parent.overview_scheduler)
local registry = require(script.Parent.Parent.registry)
local create = vide.create
local derive = vide.derive
local source = vide.source
local values = vide.values
local show = vide.show
type props = {
servers: () -> {
{
host: "server" | Player,
vm: number,
schedulers: () -> {
{id: number, name: string}
},
worlds: () -> {
{id: number, name: string}
}
}
},
destroy: () -> ()
}
return function(props: props)
local selected = source(Players.LocalPlayer)
local hosts = derive(function()
local hosts = {}
for _, server in props.servers() do
local host = server.host
hosts[host] = hosts[host] or {}
table.insert(hosts[host], server)
end
hosts["all"] = props.servers()
return hosts
end)
local options = derive(function()
local options = {}
for host, servers in hosts() do
options[host] = if type(host) == "string" then host else `@{host.Name}`
end
options[Players.LocalPlayer] = "localplayer"
options["all"] = "all"
return options
end)
local function objects()
return hosts()[selected()] or {} :: never
end
local function is_empty()
local no_objects = next(objects()) == nil
if no_objects then return "No objects found. You may not have permissions to use this." end
-- for _, object in objects() do
-- if #object.worlds() == 0 then return "No worlds found. Did you forget to set updated?" end
-- if #object.schedulers() == 0 then return "No schedulers found. Did you forget to set updated?" end
-- end
return false
end
return ui.widget {
title = "Home",
min_size = Vector2.new(230, 200),
bind_to_close = props.destroy,
ui.container {
create "UIListLayout" {
Padding = UDim.new(0, 2),
VerticalFlex = Enum.UIFlexAlignment.SpaceEvenly,
HorizontalFlex = Enum.UIFlexAlignment.Fill
},
ui.select {
size = UDim2.new(1, 0, 0, 32),
options = options :: any,
selected = selected :: any,
update_selected = selected
},
create "ScrollingFrame" {
-- Size = UDim2.fromScale(1, 1),
CanvasSize = UDim2.new(),
AutomaticCanvasSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
ScrollBarThickness = 6,
HorizontalScrollBarInset = Enum.ScrollBarInset.Always,
create "UIFlexItem" {
FlexMode = Enum.UIFlexMode.Fill
},
ui.padding {
x = UDim.new(0, 1),
right = UDim.new(0, 8)
},
create "UIListLayout" {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalFlex = Enum.UIFlexAlignment.Fill,
Padding = UDim.new(0, 8),
Wraps = true
},
show(is_empty, function()
return ui.typography {
text = is_empty(),
xalignment = Enum.TextXAlignment.Left,
wrapped = true
}
end),
values(objects, function(value, key)
return ui.pane{
name = "",
size = UDim2.fromOffset(200, 0),
automaticsize = Enum.AutomaticSize.Y,
create "UIListLayout" {
Padding = UDim.new(0, 8)
},
ui.typography {
text = `host: {value.host}\tvm id: {value.vm}`,
wrapped = true
},
values(value.worlds, function(world)
return ui.button {
size = UDim2.new(1, 0, 0, 30),
text = `World: {world.name}`,
activated = function()
spawn_app.spawn_app(registry, {
host = value.host,
vm = value.vm,
id = world.id
})
end
}
end),
values(value.schedulers, function(scheduler)
return ui.button {
size = UDim2.new(1, 0, 0, 30),
text = `Scheduler: {scheduler.name}`,
activated = function()
spawn_app.spawn_app(overview_scheduler, {
host = value.host,
vm = value.vm,
id = scheduler.id
})
end
}
end)
}
end)
}
}
}
end

View file

@ -0,0 +1,68 @@
local RunService = game:GetService("RunService")
local vide = require(script.Parent.Parent.Parent.vide)
local loop = require(script.Parent.Parent.Parent.modules.loop)
local remotes = require(script.Parent.Parent.Parent.modules.remotes)
local widget = require(script.widget)
local source = vide.source
local cleanup = vide.cleanup
local overview_scheduler = {
class_name = "app" :: "app",
name = "Scheduler"
}
type props = {
host: Player | "server",
vm: number,
id: number
}
function overview_scheduler.mount(props: props, destroy: () -> ())
local system_data = source {}
local system_frames = source {}
local system_ids = source {}
local app_loop = loop (
"app-client-scheduler",
{
host = props.host,
vm = props.vm,
id = props.id,
system_ids = system_ids,
system_data = system_data,
system_frames = system_frames,
},
{i = 1},
script.systems.get_scheduler_data
)
cleanup(
RunService.Heartbeat:Connect(app_loop)
)
return widget {
host = props.host,
vm = props.vm,
id = props.id,
system_ids = system_ids,
system_data = system_data,
system_frames = system_frames,
pause_system = function(system: number)
remotes.scheduler_system_pause:fire({
host = props.host,
to_vm = props.vm
}, props.id, system, not system_data()[system].paused)
end,
destroy = destroy
}
end
return overview_scheduler

View file

@ -0,0 +1,46 @@
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local create = vide.create
local indexes = vide.indexes
local derive = vide.derive
type stack_bar = {
values: () -> {{value: number, color: Color3}},
selected: (number) -> ()
}
return function(props: stack_bar)
local total = derive(function()
local total = 0
for _, value in props.values() do
total += value.value
end
return total
end)
return create "Frame" {
Name = "Graph",
Size = UDim2.new(1, 0, 0, 32),
indexes(props.values, function(value, index)
return create "Frame" {
Size = function()
return UDim2.fromScale(value().value / total(), 1)
end,
BackgroundColor3 = function() return value().color end,
MouseEnter = function()
props.selected(index)
end,
}
end),
create "UIListLayout" {
FillDirection = Enum.FillDirection.Horizontal,
},
}
end

View file

@ -0,0 +1,98 @@
local vide = require(script.Parent.Parent.Parent.Parent.Parent.vide)
local queue = require(script.Parent.Parent.Parent.Parent.Parent.modules.queue)
local remotes = require(script.Parent.Parent.Parent.Parent.Parent.modules.remotes)
local types = require(script.Parent.Parent.Parent.Parent.Parent.modules.types)
local batch = vide.batch
local cleanup = vide.cleanup
type SystemId = types.SystemId
type SystemData = types.SystemData
type SystemFrame = types.SystemFrame
type context = {
host: Player | "server",
vm: number,
id: number,
system_ids: vide.Source<{[SystemId]: true}>,
system_data: vide.Source<{[SystemId]: types.SystemData}>,
system_frames: vide.Source<{[SystemId]: {types.SystemFrame}}>,
}
local MAX_BUFFER_SIZE = 50
return function(context: context)
local outgoing = {
host = context.host,
to_vm = context.vm
}
remotes.request_scheduler:fire(outgoing, context.id)
local scheduler_static_data_updated = queue(remotes.scheduler_system_static_update)
local scheduler_frame_data_updated = queue(remotes.scheduler_system_update)
cleanup(function()
remotes.disconnect_scheduler:fire(outgoing, context.id)
end)
return function()
batch(function()
for incoming, scheduler, id, new_data in scheduler_static_data_updated:iter() do
if incoming.host ~= context.host then continue end
if incoming.from_vm ~= context.vm then continue end
if scheduler ~= context.id then continue end
if new_data == nil then
context.system_ids()[id] = nil
context.system_data()[id] = nil
context.system_frames()[id] = nil
context.system_ids(context.system_ids())
context.system_data(context.system_data())
context.system_frames(context.system_frames())
else
context.system_ids()[id] = true
context.system_data()[id] = new_data
context.system_frames()[id] = context.system_frames()[id] or {}
context.system_ids(context.system_ids())
context.system_data(context.system_data())
context.system_frames(context.system_frames())
end
end
for incoming, scheduler, id, f, s in scheduler_frame_data_updated:iter() do
if incoming.host ~= context.host then continue end
if incoming.from_vm ~= context.vm then continue end
if scheduler ~= context.id then continue end
if context.system_frames()[id] == nil then continue end
-- look where to append the frame to
local frames = context.system_frames()[id]
local f_data = {i = f, s = s}
local added = false
--- since it's unreliable we have to constantly check if we arent out of order
for i, frame in frames do
if frame.i == f then frames[i] = f_data; continue end
if frame.i > f then continue end
table.insert(frames, i, f_data)
table.remove(frames, MAX_BUFFER_SIZE + 1)
added = true
break
end
if #frames <= MAX_BUFFER_SIZE and added == false then
table.insert(frames, f_data)
end
context.system_frames(context.system_frames())
end
end)
end
end

View file

@ -0,0 +1,412 @@
local ui = require(script.Parent.Parent.Parent.Parent.ui)
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local convert_units = require(script.Parent.Parent.Parent.Parent.modules.convert_units)
local types = require(script.Parent.Parent.Parent.Parent.modules.types)
local spawn_app = require(script.Parent.Parent.Parent.spawn_app)
local system_widget = require(script.Parent.Parent.system)
local stack_bar = require(script.Parent.stack_bar)
local create = vide.create
local indexes = vide.indexes
local values = vide.values
local changed = vide.changed
local source = vide.source
local derive = vide.derive
type SystemId = number
type props = {
host: Player | "server",
vm: number,
id: number,
system_ids: () -> {[SystemId]: true},
system_data: () -> {[SystemId]: types.SystemData},
system_frames: () -> {[SystemId]: {types.SystemFrame}},
pause_system: (SystemId) -> (),
destroy: () -> ()
}
local function color(n: number)
return Color3.fromHSV((0.15 * (n-1)) % 1, 1, 1)
end
local sort_by_options = {
"Name",
"Id",
"Frame Time"
}
return function(props: props)
local selected = source(0)
local systems_query = source("")
local sort_by = source(2)
local max_frametime = derive(function()
local max = 0
for _, frames in props.system_frames() do
local sum = 0
for _, frame in frames do
sum += frame.s
end
max = math.max(max, sum / #frames)
end
return max
end)
local map_phases_to_systems = derive(function()
local phases: {[false | string]: {number}} = {[false] = {}}
for id, data in props.system_data() do
if phases[data.phase or false] == nil then phases[data.phase or false] = {} end
table.insert(phases[data.phase or false], id)
end
return phases
end)
local function system(id: number)
local gui_state = source(Enum.GuiState.Idle)
local function frame_time()
local sum = 0
local frames = props.system_frames()[id]
for _, frame in frames do
sum += frame.s
end
return sum / #frames
end
local b = create "ImageButton" {
Name = function()
return props.system_data()[id].name
end,
Size = UDim2.new(1, 0, 0, 32),
LayoutOrder = function()
return if sort_by() == 3 then 1e9 - frame_time() * 1e8 else id
end,
BackgroundColor3 = function()
return if gui_state() == Enum.GuiState.Press then
ui.theme.bg[-1]()
elseif gui_state() == Enum.GuiState.Hover then
ui.theme.bg[6]()
else
ui.theme.bg[3]()
end,
Visible = function()
return not not string.match(props.system_data()[id].name, systems_query())
end,
changed("GuiState", gui_state),
Activated = function()
spawn_app.spawn_app(system_widget, {
host = props.host,
vm = props.vm,
scheduler = props.id,
system = id,
name = props.system_data()[id].name
})
end,
MouseButton2Click = function()
props.pause_system(id)
end,
-- create a frame that ignores all rules!
create "Folder" {
create "Frame" {
Position = UDim2.new(0, 0, 1, 4),
AnchorPoint = Vector2.new(0, 1),
Size = function()
return UDim2.new(frame_time() / max_frametime(), 0, 0, 1)
end,
BackgroundColor3 = ui.theme.fg_on_bg_high[0]
}
},
create "UIStroke" {
Color = ui.theme.bg[-3]
},
create "UICorner" {
CornerRadius = UDim.new(0, 4)
},
create "UIListLayout" {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
HorizontalFlex = Enum.UIFlexAlignment.SpaceEvenly,
Padding = UDim.new(0, 8)
},
ui.padding {
x = UDim.new(0, 8)
},
create "Frame" {
Size = UDim2.fromOffset(16, 16),
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundColor3 = color(id),
create "UICorner" {
CornerRadius = UDim.new(1, 0)
},
},
ui.typography {
automaticsize = Enum.AutomaticSize.None,
text = function()
return props.system_data()[id].name
end,
truncate = Enum.TextTruncate.SplitWord,
xalignment = Enum.TextXAlignment.Left,
disabled = function()
return props.system_data()[id].paused
end,
create "UIFlexItem" {
FlexMode = Enum.UIFlexMode.Fill,
GrowRatio = 1,
ShrinkRatio = 1
}
},
ui.typography {
automaticsize = Enum.AutomaticSize.XY,
text = function()
local sum = 0
local frames = props.system_frames()[id]
for _, frame in frames do
sum += frame.s
end
return `{convert_units("s", sum / #frames)}`
end,
xalignment = Enum.TextXAlignment.Right,
disabled = true,
},
}
return b
end
return ui.widget {
title = "Scheduler",
subtitle = `host: {props.host} vm: {props.vm} id: {props.id}`,
min_size = Vector2.new(200, 300),
bind_to_close = props.destroy,
create "Frame" {
Name = "Elements",
Size = UDim2.fromScale(1, 1),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
create "UIListLayout" {
VerticalAlignment = Enum.VerticalAlignment.Bottom,
FillDirection = Enum.FillDirection.Vertical,
VerticalFlex = Enum.UIFlexAlignment.SpaceBetween,
Padding = UDim.new(0, 8)
},
create "Frame" {
Size = UDim2.fromScale(1, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
-- create "UIFlexItem" {
-- FlexMode = Enum.UIFlexMode.Custom,
-- GrowRatio = 0,
-- ShrinkRatio = 0
-- },
ui.pane {
name = "Overview",
size = UDim2.fromScale(1, 0),
create "UIListLayout" {
FillDirection = Enum.FillDirection.Vertical
},
ui.typography {
text = function()
local run_time = 0
for id, frames in props.system_frames() do
if props.system_data()[id].paused then continue end
local sum = 0
for _, frame in frames do
sum += frame.s
end
run_time += sum / #frames
end
return `Run time: {convert_units("s", run_time)}`
end
},
stack_bar {
values = function()
local v = {}
local system_ids = props.system_ids()
local system_frames = props.system_frames()
for i = 1, table.maxn(system_ids) do
if system_ids[i] == nil then continue end
if props.system_data()[i].paused then continue end
local sum = 0
local frames = system_frames[i]
for _, frame in frames do
sum += frame.s
end
table.insert(v, {value = sum / #frames, color = color(i)})
end
return v
end,
selected = selected
},
ui.row {
justifycontent = Enum.UIFlexAlignment.Fill,
ui.button {
text = "Pause all",
activated = function()
for system, data in props.system_data() do
if data.paused then continue end
props.pause_system(system)
end
end
},
ui.button {
text = "Resume all",
activated = function()
for system, data in props.system_data() do
if not data.paused then continue end
props.pause_system(system)
end
end
}
}
},
},
ui.select {
size = UDim2.new(1, 0, 0, 30),
options = sort_by_options,
selected = sort_by,
update_selected = function(new)
-- print(new)
sort_by(new)
end
},
ui.textfield {
size = UDim2.new(1, 0, 0, 36),
placeholder = "System Match",
oninput = systems_query,
},
create "ScrollingFrame" {
Name = "Systems",
Size = UDim2.fromScale(1, 0),
CanvasSize = UDim2.new(),
AutomaticCanvasSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
ScrollBarThickness = 6,
VerticalScrollBarInset = Enum.ScrollBarInset.Always,
ScrollBarImageColor3 = ui.theme.fg_on_bg_low[3],
create "UIFlexItem" {
FlexMode = Enum.UIFlexMode.Fill
},
create "UIListLayout" {
FillDirection = Enum.FillDirection.Vertical,
Padding = UDim.new(0, 8),
SortOrder = function()
return if sort_by() == 1 then Enum.SortOrder.Name else Enum.SortOrder.LayoutOrder
end
},
ui.padding {
y = UDim.new(0, 1),
x = UDim.new(0, 1)
},
values(function()
return map_phases_to_systems()[false]
end, system),
indexes(map_phases_to_systems, function(systems, phase)
if phase == false then return {} end
local expanded = source(true)
-- print(systems())
return ui.accordion {
expanded = expanded,
set_expanded = expanded,
text = phase,
ui.container {
Size = UDim2.fromScale(1, 0),
create "UIListLayout" {
FillDirection = Enum.FillDirection.Vertical,
Padding = UDim.new(0, 8),
SortOrder = function()
return if sort_by() == 1 then Enum.SortOrder.Name else Enum.SortOrder.LayoutOrder
end
},
values(systems, system)
}
}
end),
}
}
}
end

View file

@ -0,0 +1,154 @@
local ContextActionService = game:GetService("ContextActionService")
local RunService = game:GetService("RunService")
local vide = require(script.Parent.Parent.Parent.vide)
local loop = require(script.Parent.Parent.Parent.modules.loop)
local spawn_app = require(script.Parent.Parent.spawn_app)
local entity_widget = require(script.Parent.entity)
local widget = require(script.widget)
local source = vide.source
local effect = vide.effect
local cleanup = vide.cleanup
local overview_query = {
class_name = "app" :: "app",
name = "Query"
}
type props = {
host: Player | "server",
vm: number,
id: number
}
function overview_query.mount(props: props, destroy: () -> ())
-- the entity id
local current_entity = source(nil)
-- enables picking
local enable_pick = source(false)
-- the entity data as a string
local entity_hovering_over = source()
-- the part the player is hovering over
local hovering_over = source()
local validate_query = source("")
local ok = source(false)
local msg = source("")
local query = source("")
local primary_entity = source()
local columns = source({})
local from = source(1)
local upto = source(25)
local total_entities = source(0)
local paused = source(false)
local refresh = source(false)
-- check if the query and columns are properly
local app_loop = loop (
"app-client-registry",
{
host = props.host,
vm = props.vm,
id = props.id,
enable_pick = enable_pick,
entity_hovering_over = entity_hovering_over,
hovering_over = hovering_over,
set_entity = current_entity,
columns = columns,
query = query,
primary_entity = primary_entity,
paused = paused,
refresh = refresh,
total_entities = total_entities,
from = from,
upto = upto,
validate_query = validate_query,
ok = ok,
msg = msg
},
{i = 1},
script.systems.validate_query,
script.systems.obtain_query_data,
script.systems.send_workspace_entity,
script.systems.highlight_workspace_entity
)
cleanup(RunService.Heartbeat:Connect(app_loop))
local function open_entity_widget(_, state: Enum.UserInputState)
local entity = current_entity()
if state ~= Enum.UserInputState.Begin then return end
if entity == nil then return end
enable_pick(false)
entity_hovering_over(nil)
hovering_over(nil)
spawn_app.spawn_app(entity_widget, {
host = props.host,
vm = props.vm,
id = props.id,
entity = entity
})
end
effect(function()
local picking = enable_pick()
local key = `select entity:{props.host} {props.vm} {props.id}`
if picking then
ContextActionService:BindAction(key, open_entity_widget, false, Enum.UserInputType.MouseButton1)
end
cleanup(function()
ContextActionService:UnbindAction(key)
end)
end)
return widget {
host = props.host,
vm = props.vm,
id = props.id,
validate_query = validate_query,
update_system_query = query,
current_query = query,
total_rows_per_page = source(25),
set_rows_per_page = source(25),
primary_entity = primary_entity,
from = from,
upto = upto,
total_entities = total_entities,
paused = paused,
refresh = refresh,
enable_pick = enable_pick,
entity_hovering_over = entity_hovering_over,
hovering_over = hovering_over,
ok = ok,
msg = msg,
columns = columns,
destroy = destroy
}
end
return overview_query

View file

@ -0,0 +1,34 @@
local queue = require(script.Parent.Parent.Parent.Parent.Parent.modules.queue)
local remotes = require(script.Parent.Parent.Parent.Parent.Parent.modules.remotes)
type Context = {
host: Player | "server",
vm: number,
id: number,
enable_pick: () -> boolean,
entity_hovering_over: (string) -> (),
set_entity: (number) -> (),
hovering_over: (BasePart) -> ()
}
return function(context: Context)
local send_mouse_entity = queue(remotes.send_mouse_entity)
return function()
for incoming, id, to_highlight, entity, components in send_mouse_entity:iter() do
if incoming.host ~= context.host then continue end
if incoming.from_vm ~= context.vm then continue end
if id ~= context.id then continue end
if context.enable_pick() == false then continue end
context.hovering_over(to_highlight)
context.entity_hovering_over(components)
context.set_entity(entity)
end
end
end

View file

@ -0,0 +1,128 @@
local vide = require(script.Parent.Parent.Parent.Parent.Parent.vide)
local queue = require(script.Parent.Parent.Parent.Parent.Parent.modules.queue)
local remotes = require(script.Parent.Parent.Parent.Parent.Parent.modules.remotes)
local effect = vide.effect
local batch = vide.batch
local cleanup = vide.cleanup
type Context = {
host: Player | "server",
vm: number,
id: number,
columns: vide.Source<{ { any } }>,
query: () -> string,
paused: () -> boolean,
refresh: vide.Source<boolean>,
total_entities: (number) -> (),
from: () -> number,
upto: () -> number,
}
local function generate_random_query_id()
return math.random(2 ^ 31 - 1)
end
return function(context: Context)
local query_changed = false
local page_changed = false
effect(function()
if #context.query() > 0 then
query_changed = true
end
end)
effect(function()
context.from()
context.upto()
page_changed = true
end)
local current_query_id = -1
local query_last_frame = 0
local update_query_result = queue(remotes.update_query_result)
local count_updated = queue(remotes.count_total_entities)
local columns = context.columns
local outgoing = {
host = context.host,
to_vm = context.vm
}
cleanup(function()
remotes.disconnect_query:fire(outgoing, current_query_id)
end)
local should_refresh = false
effect(function()
if context.refresh() ~= true then return end
context.refresh(false)
should_refresh = true
end)
local paused_state = context.paused()
local paused_updated = false
effect(function()
paused_updated = true
paused_state = context.paused()
end)
return function()
if query_changed then
columns({})
remotes.disconnect_query:fire(outgoing, current_query_id)
current_query_id = generate_random_query_id()
-- print("requesting new query", current_query_id)
remotes.request_query:fire(outgoing, context.id, current_query_id, context.query())
remotes.advance_query_page:fire(outgoing, current_query_id, context.from(), context.upto())
query_last_frame = 0
query_changed = false
remotes.pause_query:fire(outgoing, current_query_id, paused_state)
end
if page_changed then
remotes.advance_query_page:fire(outgoing, current_query_id, context.from(), context.upto())
page_changed = false
end
for incoming, query, value in count_updated:iter() do
if query ~= current_query_id then continue end
context.total_entities(value)
end
if paused_updated then
remotes.pause_query:fire(outgoing, current_query_id, paused_state)
paused_updated = false
end
if should_refresh then
remotes.refresh_results:fire(outgoing, current_query_id)
should_refresh = false
end
batch(function()
for incoming, query, frame, column, row, value in update_query_result:iter() do
if query ~= current_query_id then continue end
if frame < query_last_frame - 10 then continue end
query_last_frame = math.max(query_last_frame, frame)
if columns()[column] == nil then
columns()[column] = {}
end
columns()[column][row] = value
columns(columns())
end
end)
end
end

View file

@ -0,0 +1,33 @@
local UserInputService = game:GetService("UserInputService")
local remotes = require(script.Parent.Parent.Parent.Parent.Parent.modules.remotes)
type Context = {
host: Player | "server",
vm: number,
id: number,
enable_pick: () -> boolean,
}
return function(context: Context)
if context.enable_pick() == false then return end
local location = UserInputService:GetMouseLocation()
local camera = workspace.CurrentCamera
local ray = camera:ViewportPointToRay(location.X, location.Y)
remotes.send_mouse_pointer:fire(
{
host = context.host,
to_vm = context.vm
},
context.id,
ray.Origin,
ray.Direction
)
end

View file

@ -0,0 +1,98 @@
local Players = game:GetService("Players")
local jecs = require(script.Parent.Parent.Parent.Parent.Parent.jecs)
local vide = require(script.Parent.Parent.Parent.Parent.Parent.vide)
local queue = require(script.Parent.Parent.Parent.Parent.Parent.modules.queue)
local remotes = require(script.Parent.Parent.Parent.Parent.Parent.modules.remotes)
local effect = vide.effect
type Context = {
host: Player | "server",
vm: number,
id: number,
validate_query: () -> string,
msg: (string) -> (),
ok: (boolean) -> (),
primary_entity: (any?) -> ()
}
return function(context: Context)
local query_changed = false
effect(function()
context.validate_query()
query_changed = true
end)
local n = 0
local already_validated = false
local MIN_DELAY_UNTIL_VALIDATE = 0
local validate_result = queue(remotes.validate_result)
if context.host == Players.LocalPlayer then
MIN_DELAY_UNTIL_VALIDATE = 0.3
elseif context.host == "server" then
MIN_DELAY_UNTIL_VALIDATE = 0.5
else
MIN_DELAY_UNTIL_VALIDATE = 0.5
end
return function(dt)
if query_changed then
n = 0
already_validated = false
query_changed = false
context.ok(false)
context.msg("")
end
for incoming, world, query, terms, ok, msg in validate_result:iter() do
if incoming.host ~= context.host then continue end
if incoming.from_vm ~= context.vm then continue end
if world ~= context.id then continue end
if query ~= context.validate_query() then continue end
context.ok(ok)
context.msg(msg or "")
context.primary_entity(nil)
if not terms then continue end
if #terms.include + #terms.exclude + #terms.with > 1 then continue end
local first = terms.include[1]
if first == nil then continue end
if jecs.IS_PAIR(first) then continue end
context.primary_entity(terms.include[1])
end
n += dt
if n < MIN_DELAY_UNTIL_VALIDATE then return end
if already_validated then return end
if context.validate_query() == "" then
context.ok(false)
context.msg("empty query")
return
end
already_validated = true
remotes.validate_query:fire(
{
host = context.host,
to_vm = context.vm
},
context.id,
context.validate_query()
)
end
end

View file

@ -0,0 +1,503 @@
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local ui = require(script.Parent.Parent.Parent.Parent.ui)
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local tooltip = require(script.Parent.Parent.Parent.components.tooltip)
local spawn_app = require(script.Parent.Parent.Parent.spawn_app)
local entity = require(script.Parent.Parent.entity)
local create = vide.create
local effect = vide.effect
local source = vide.source
local show = vide.show
type SystemId = number
type props = {
host: Player | "server",
vm: number,
id: number,
validate_query: (string) -> (),
ok: () -> boolean,
msg: () -> string,
paused: vide.Source<boolean>,
refresh: (boolean) -> (),
from: vide.Source<number>,
upto: vide.Source<number>,
primary_entity: () -> number?,
update_system_query: (query: string) -> (),
current_query: () -> string,
total_entities: () -> number,
enable_pick: vide.Source<boolean>,
entity_hovering_over: () -> string,
hovering_over: () -> BasePart,
columns: () -> {{any}},
destroy: () -> ()
}
local mouse_location = source(Vector2.zero)
RunService.PreRender:Connect(function()
mouse_location(UserInputService:GetMouseLocation())
end)
return function(props: props)
local page_input = source("1")
local rows_input = source("25")
local page = source(1)
local rows = source(20)
effect(function()
page_input(tostring(page()))
end)
effect(function()
rows_input(tostring(rows()))
end)
effect(function()
local page = page()
local rows_per_page = rows()
local from = (page - 1) * rows_per_page + 1
local upto = from + rows_per_page - 1
props.from(from)
props.upto(upto)
end)
local row_template = {
Size = UDim2.new(0, 0, 0, 26),
AutomaticSize = Enum.AutomaticSize.X
} :: any
return ui.widget {
title = "Querying",
subtitle = `host: {props.host} vm: {props.vm} id: {props.id}`,
min_size = Vector2.new(300, 300),
bind_to_close = props.destroy,
create "Frame" {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
tooltip {
transparency = 0.3,
visible = function()
return props.entity_hovering_over() and #props.entity_hovering_over() > 0 or false
end,
ui.typography {
automaticsize = Enum.AutomaticSize.XY,
text = function()
return props.entity_hovering_over() or ""
end,
xalignment = Enum.TextXAlignment.Left,
wrapped = true,
code = true,
{ RichText = true },
create "UIStroke" {
Thickness = 1,
Color = ui.theme.bg[-5]
}
}
},
create "Highlight" {
DepthMode = Enum.HighlightDepthMode.AlwaysOnTop,
OutlineColor = Color3.new(1, 1, 1),
FillColor = ui.theme.acc[0],
FillTransparency = 0.5,
Adornee = props.hovering_over
},
create "UIListLayout" {
VerticalFlex = Enum.UIFlexAlignment.SpaceAround,
Padding = UDim.new(0, 8)
},
create "Frame" {
Name = "Query + Pick",
Size = UDim2.new(1, 0, 0, 30),
BackgroundTransparency = 1,
create "UIListLayout" {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalFlex = Enum.UIFlexAlignment.SpaceAround,
VerticalFlex = Enum.UIFlexAlignment.SpaceAround,
Padding = UDim.new(0, 8)
},
create "Frame" {
Name = "Query",
Size = UDim2.fromScale(0, 1),
BackgroundTransparency = 1,
create "UIFlexItem" {
FlexMode = Enum.UIFlexMode.Fill
},
ui.textfield {
size = UDim2.new(1, 0, 0, 30),
placeholder = "Query",
code = true,
oninput = function(text)
props.validate_query(text)
end,
enter = function(text)
props.update_system_query(text)
end
},
},
vide.show(function()
return props.primary_entity()
end, function()
return ui.button {
size = UDim2.fromOffset(30, 30),
text = "",
activated = function()
spawn_app.spawn_app(entity, {
host = props.host,
vm = props.vm,
id = props.id,
entity = props.primary_entity()
})
end,
create "ImageLabel" {
Size = UDim2.fromOffset(24, 24),
Position = UDim2.fromScale(0.5, 0.5),
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
ImageColor3 = ui.theme.fg_on_bg_high[3],
Image = "rbxassetid://10723415903"
},
}
end),
ui.button {
size = UDim2.fromOffset(30, 30),
text = "",
accent = props.enable_pick,
activated = function()
props.enable_pick(not props.enable_pick())
end,
create "ImageLabel" {
Size = UDim2.fromOffset(24, 24),
Position = UDim2.fromScale(0.5, 0.5),
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
ImageColor3 = ui.theme.fg_on_bg_high[3],
Image = "rbxassetid://10734898355"
},
},
},
create "Frame" {
Size = UDim2.new(1, 0, 0, 24),
BackgroundTransparency = 1,
Visible = function()
return not props.ok() and #props.msg() > 0
end,
ui.typography {
text = props.msg
},
},
create "Frame" {
Size = UDim2.new(1, 0, 0, 32),
BackgroundColor3 = ui.theme.bg[2],
AutomaticSize = Enum.AutomaticSize.Y,
create "UICorner" {
CornerRadius = UDim.new(0, 8)
},
create "UIListLayout" {
FillDirection = Enum.FillDirection.Horizontal,
Padding = UDim.new(0, 8),
HorizontalFlex = Enum.UIFlexAlignment.SpaceAround,
VerticalAlignment = Enum.VerticalAlignment.Center,
Wraps = true
},
ui.row {
spacing = UDim.new(0, 8),
alignitems = Enum.ItemLineAlignment.Center,
row_template,
ui.typography {
text = "Page:"
},
ui.textfield {
size = UDim2.fromOffset(40, 26),
placeholder = "1",
text = function()
return page_input()
end,
oninput = page_input,
enter = function(text)
local n = tonumber(text)
if n == nil then
page_input(tostring(page()))
else
page(n)
end
end
},
ui.typography {
text = function()
return `/ {math.ceil(props.total_entities() / rows())}`
end
},
},
ui.row {
spacing = UDim.new(0, 8),
alignitems = Enum.ItemLineAlignment.Center,
row_template,
ui.typography {
text = "Rows:"
},
ui.textfield {
size = UDim2.fromOffset(40, 26),
placeholder = "Rows",
text = function()
return rows_input()
end,
oninput = rows_input,
enter = function(text)
local n = tonumber(text)
if n == nil then
rows_input(tostring(rows()))
else
rows(n)
end
end
},
},
ui.row {
spacing = UDim.new(0, 4),
row_template,
ui.button {
size = UDim2.fromOffset(26, 26),
text = "",
accent = function()
return not props.paused()
end,
activated = function()
props.paused(not props.paused())
end,
{LayoutOrder = 10},
create "ImageLabel" {
Size = UDim2.fromOffset(24, 24),
Position = UDim2.fromScale(0.5, 0.5),
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
ImageColor3 = ui.theme.fg_on_bg_high[3],
Image = function()
return if props.paused() then "rbxassetid://10735024209"
else "rbxassetid://10734923214"
end
},
},
show(props.paused, function()
return ui.button {
size = UDim2.fromOffset(26, 26),
text = "",
activated = function()
props.refresh(true)
end,
create "ImageLabel" {
Size = UDim2.fromOffset(24, 24),
Position = UDim2.fromScale(0.5, 0.5),
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
ImageColor3 = ui.theme.fg_on_bg_high[3],
Image = "rbxassetid://10734933222"
},
}
end)
}
},
ui.background {
size = UDim2.fromScale(1, 0),
automaticsize = Enum.AutomaticSize.Y,
create "UICorner" {
CornerRadius = UDim.new(0, 8)
},
create "UIFlexItem" {
FlexMode = Enum.UIFlexMode.Fill
},
ui.tablesheet {
size = UDim2.fromScale(1, 1),
suggested_column_sizes = { 0.1 },
column_sizes = function()
local t = {}
for i in props.columns() do
t[i] = 200
end
t[1] = 50
return t
end,
columns = props.columns,
read_value = function(c, r)
local column = props.columns()[c]
if not column then return "" end
return column[r] or ""
end,
on_click = function(c, r)
if not props.columns()[1][r-1] then return end
spawn_app.spawn_app(entity, {
host = props.host,
vm = props.vm,
id = props.id,
entity = props.columns()[1][r-1]
})
end,
on_click2 = function() end,
below = {
ui.padding {
x = UDim.new(0, 4),
y = UDim.new(0, 2)
},
create "UIListLayout" {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
Padding = UDim.new(0, 8)
},
ui.button {
size = UDim2.fromOffset(70, 26),
text = "Previous",
activated = function()
page(page() - 1)
end,
disabled = function()
return page() == 1 or props.ok() == false
end
} :: Instance,
ui.button {
size = UDim2.fromOffset(70, 26),
text = "Next",
activated = function()
page(page() + 1)
end,
disabled = function()
local max_pages = math.max(1, math.ceil(props.total_entities() / rows()))
return page() == max_pages or props.ok() == false
end
} :: Instance,
ui.typography {
position = UDim2.new(0, 4, 0.5, 0),
anchorpoint = Vector2.new(0, 0.5),
text = function()
return `total entities: {props.total_entities()}\tfrom: {props.from()}\tuntil: {props.upto()}`
end,
} :: Instance,
}
}
}
}
}
end

View file

@ -0,0 +1,81 @@
local RunService = game:GetService("RunService")
local vide = require(script.Parent.Parent.Parent.vide)
local loop = require(script.Parent.Parent.Parent.modules.loop)
local remotes = require(script.Parent.Parent.Parent.modules.remotes)
local types = require(script.Parent.Parent.Parent.modules.types)
local widget = require(script.widget)
local cleanup = vide.cleanup
local system = {
class_name = "app" :: "app",
name = "System"
}
type props = {
host: number,
vm: number,
scheduler: number,
system: number,
name: string
}
function system.mount(props: props, destroy: () -> ())
local watch_id = math.random(2^31 - 1)
local recording = vide.source(false)
local watching_frame = vide.source(0)
local per_frame_data = vide.source({} :: {[number]: number})
local changes = vide.source({
component = {},
entities = {},
types = {},
values = {}
} :: types.WatchLoggedChanges)
local system_props_data = {
watch_id = watch_id,
host = props.host,
vm = props.vm,
scheduler = props.scheduler,
system = props.system,
name = props.name,
changes = changes,
recording = recording,
per_frame_data = per_frame_data,
watching_frame = watching_frame,
destroy = destroy,
}
local app_loop = loop (
"app-client-system",
system_props_data,
{i = 1},
script.systems.replicate
)
local outgoing: types.OutgoingConnector = {
host = system_props_data.host,
to_vm = system_props_data.vm
}
remotes.create_watch:fire(outgoing, props.scheduler, props.system, watch_id)
remotes.connect_watch:fire(outgoing, watch_id)
cleanup(RunService.Heartbeat:Connect(app_loop))
cleanup(function()
remotes.disconnect_watch:fire(outgoing, watch_id)
remotes.stop_watch:fire(outgoing, watch_id)
remotes.remove_watch:fire(outgoing, watch_id)
end)
return widget(system_props_data)
end
return system

View file

@ -0,0 +1,97 @@
local vide = require(script.Parent.Parent.Parent.Parent.Parent.vide)
local queue = require(script.Parent.Parent.Parent.Parent.Parent.modules.queue)
local remotes = require(script.Parent.Parent.Parent.Parent.Parent.modules.remotes)
local types = require(script.Parent.Parent.Parent.Parent.Parent.modules.types)
local batch = vide.batch
type Context = {
host: "server" | Player,
vm: number,
watch_id: number,
scheduler: number,
system: number,
name: string,
recording: vide.Source<boolean>,
watching_frame: vide.Source<number>,
per_frame_data: vide.Source<{[number]: number}>,
changes: (types.WatchLoggedChanges) -> (),
}
return function(context: Context)
local watch_id = context.watch_id
local outgoing: types.OutgoingConnector = {
host = context.host,
to_vm = context.vm
}
local recording_state_changed = false
local recording = false
vide.effect(function()
recording_state_changed = true
recording = context.recording()
end)
local watching_frame_changed = false
local watching_frame = 1
vide.effect(function()
watching_frame_changed = true
watching_frame = context.watching_frame()
end)
local receive_update_data = queue(remotes.update_watch_data)
local receive_overview = queue(remotes.update_overview)
return function()
if recording_state_changed and recording then
remotes.start_record_watch:fire(outgoing, watch_id)
recording_state_changed = false
elseif recording_state_changed and not recording then
remotes.stop_watch:fire(outgoing, watch_id)
recording_state_changed = false
end
if watching_frame_changed then
remotes.request_watch_data:fire(outgoing, watch_id, watching_frame)
watching_frame_changed = false
end
debug.profilebegin("receive update data")
batch(function()
for from, watch, frame, changes in receive_update_data:iter() do
if watch ~= watch_id then continue end
if frame ~= watching_frame then continue end
if changes == nil then
context.changes({
types = {},
entities = {},
component = {},
values = {},
worlds = {}
})
else
context.changes(changes)
end
end
end)
debug.profileend()
debug.profilebegin("receive overview")
batch(function()
for from, watch, frame, value in receive_overview:iter() do
if watch ~= watch_id then continue end
local data = context.per_frame_data()
if data[frame] == value then continue end
data[frame] = value
context.per_frame_data(data)
end
end)
debug.profileend()
end
end

View file

@ -0,0 +1,235 @@
local ui = require(script.Parent.Parent.Parent.Parent.ui)
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local types = require(script.Parent.Parent.Parent.Parent.modules.types)
local tooltip = require(script.Parent.Parent.Parent.components.tooltip)
local virtualscroller_horizontal = require(script.Parent.Parent.Parent.components.virtualscroller_horizontal)
local create = vide.create
local source = vide.source
type props = {
host: "server" | Player,
vm: number,
scheduler: number,
system: number,
name: string,
recording: vide.Source<boolean>,
watching_frame: vide.Source<number>,
per_frame_data: () -> {[number]: number},
changes: () -> types.WatchLoggedChanges,
}
local PROFILER_THICKNESS = 6
return function(props: props)
local is_recording = props.recording
local watching_frame = props.watching_frame
local per_frame_data = props.per_frame_data
local changes = props.changes
local function sheet_changes()
local changes = changes()
return {
{"type", unpack(changes.types)},
{"entity", unpack(changes.entities)},
{"component", unpack(changes.component)},
{"value", unpack(changes.values)}
}
end
local function max()
return math.max(1, unpack(per_frame_data()))
end
local function total_changes()
local sum = 0
for _, value in per_frame_data() do
sum += value
end
return sum
end
local hovering_over = source(false)
return ui.list {
justifycontent = Enum.UIFlexAlignment.SpaceEvenly,
spacing = UDim.new(0, 4),
ui.pane {
virtualscroller_horizontal {
item_size = PROFILER_THICKNESS,
item = function(index)
local function value()
return per_frame_data()[index()] or 0
end
return create "TextButton" {
Size = function()
return UDim2.new(0, PROFILER_THICKNESS, 1, 0)
end,
BackgroundTransparency = function()
return if hovering_over() == index() then 0.5 else 1
end,
BackgroundColor3 = ui.theme.bg[10],
MouseEnter = function()
hovering_over(index())
end,
MouseLeave = function()
if hovering_over() ~= index() then return end
hovering_over(false)
end,
Activated = function()
watching_frame(index())
end,
AutoLocalize = false,
create "Frame" {
Size = function()
return UDim2.fromScale(1, value() / max())
end,
Position = UDim2.fromScale(1, 1),
AnchorPoint = Vector2.new(1, 1),
BackgroundColor3 = function()
return if watching_frame() == index() then
ui.theme.acc[20]()
elseif hovering_over() == index() then
ui.theme.acc[5]()
else ui.theme.acc[0]()
end,
}
}
end,
max_items = function()
return #per_frame_data()
end,
create "UIStroke" {Color = ui.theme.bg[-3]},
{
Size = UDim2.new(1, 0, 0, 56),
HorizontalScrollBarInset = Enum.ScrollBarInset.ScrollBar,
BackgroundColor3 = ui.theme.bg[-1],
ScrollBarThickness = 6,
CanvasPosition = function()
per_frame_data()
return Vector2.new(table.maxn(per_frame_data()) * PROFILER_THICKNESS)
end,
}
},
ui.typography {
text = function()
return `Recorded {#per_frame_data()} frames and tracked {total_changes()} changes`
end
},
ui.typography {
text = function()
return `Currently viewing frame {watching_frame()}`
end
}
},
tooltip {
transparency = 0,
visible = function()
return hovering_over() ~= false
end,
ui.typography {
automaticsize = Enum.AutomaticSize.XY,
text = function()
return `\z
Frame: #{hovering_over()}\n\z
Changes: {per_frame_data()[hovering_over()] or 0}`
end,
xalignment = Enum.TextXAlignment.Left,
wrapped = true
}
},
ui.container {
Size = UDim2.fromScale(1, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundColor3 = ui.theme.bg[1],
BackgroundTransparency = 0,
create "UIListLayout" {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalFlex = Enum.UIFlexAlignment.SpaceEvenly,
VerticalFlex = Enum.UIFlexAlignment.Fill,
Padding = UDim.new(0, 8)
},
ui.padding {
x = UDim.new(0, 4),
y = UDim.new(0, 4)
},
ui.button {
size = UDim2.new(0, 80, 0, 30),
automaticsize = Enum.AutomaticSize.X,
text = function()
return if is_recording() then "Pause" else "Record"
end,
activated = function()
is_recording(not is_recording())
end
},
ui.container {
Size = UDim2.fromScale(0, 0),
create "UIFlexItem" {
FlexMode = Enum.UIFlexMode.Fill
},
ui.textfield {
size = UDim2.new(1, 0, 1, 0),
text = tostring(watching_frame()),
placeholder = "frame",
enter = function(text: string)
if tonumber(text) == nil then return end
watching_frame(tonumber(text))
end
}
},
},
ui.container {
Size = UDim2.fromScale(1, 1),
create "UIFlexItem" {
FlexMode = Enum.UIFlexMode.Fill
},
ui.tablesheet {
size = UDim2.fromScale(1, 1),
column_sizes = source {100, 80, 100, 200},
read_value = function(column, row)
local v = sheet_changes()[column][row]
return if v == false then "" else v
end,
on_click = function() end,
on_click2 = function() end,
columns = sheet_changes
}
}
}
end

View file

@ -0,0 +1,34 @@
local ui = require(script.Parent.Parent.Parent.Parent.ui)
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local types = require(script.Parent.Parent.Parent.Parent.modules.types)
local watch_tracker = require(script.Parent.watch_tracker)
type props = {
host: "server" | Player,
vm: number,
scheduler: number,
system: number,
name: string,
destroy: () -> (),
recording: vide.Source<boolean>,
watching_frame: vide.Source<number>,
per_frame_data: () -> {[number]: number},
changes: () -> types.WatchLoggedChanges,
}
return function(props: props)
return ui.widget {
title = `system - {props.name}`,
subtitle = `host: {props.host} vm: {props.vm} scheduler: {props.scheduler} system: {props.system}`,
bind_to_close = props.destroy,
size = Vector2.new(350, 400),
min_size = Vector2.new(300, 300),
watch_tracker(props)
}
end

View file

@ -0,0 +1,60 @@
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local ui = require(script.Parent.Parent.Parent.ui)
local vide = require(script.Parent.Parent.Parent.vide)
local create = vide.create
local source = vide.source
local cleanup = vide.cleanup
type props = {
visible: boolean | () -> boolean,
transparency: number? | () -> number,
[number]: any
}
return function(props: props)
local mouse_location = source(Vector2.zero)
cleanup(RunService.PreRender:Connect(function()
mouse_location(UserInputService:GetMouseLocation())
end))
return create "ScreenGui" {
Name = "Mouse Hover",
IgnoreGuiInset = true,
DisplayOrder = 1e9,
Enabled = props.visible,
create "Frame" {
Position = function()
return UDim2.fromOffset(
mouse_location().X + 24,
mouse_location().Y + 24
)
end,
Size = UDim2.fromOffset(400, 0),
AutomaticSize = Enum.AutomaticSize.XY,
BackgroundColor3 = ui.theme.bg[0],
BackgroundTransparency = props.transparency or 0.5,
ui.padding {},
create "UICorner" {
CornerRadius = UDim.new(0, 8)
},
create "UIStroke" {
Color = ui.theme.bg[-10],
Thickness = 2,
Transparency = 0.8
},
unpack(props)
}
}
end

View file

@ -0,0 +1,185 @@
local ui = require(script.Parent.Parent.Parent.ui)
local vide = require(script.Parent.Parent.Parent.vide)
local create = vide.create
local source = vide.source
local values = vide.values
local changed = vide.changed
local effect = vide.effect
local untrack = vide.untrack
type can<T> = T | () -> T
type props = {
size: can<UDim2>?,
position: can<UDim2>?,
anchorpoint: can<UDim2>?,
--- streams in items. when index is -1, should expect to be unused
item: (index: () -> number) -> Instance,
--- streams in separators. when index is -1, should expect to be unused
separator: ((index: () -> number) -> Instance)?,
item_size: number,
separator_size: number?,
max_items: (() -> number)?,
[number]: any
}
return function(props: props)
local items = source({} :: {vide.Source<number>})
local absolute_size = source(Vector2.zero)
local canvas_position = source(Vector2.zero)
local item_size = props.item_size
local separator_size = props.separator_size or 0
local item = props.item
local separator = props.separator
local OVERFLOW_ONE_SIDE = 4
effect(function()
local absolute_size = absolute_size()
local canvas_position = canvas_position()
local child_size = item_size + separator_size
local total_required = math.ceil(absolute_size.X / child_size) + OVERFLOW_ONE_SIDE * 2
local sources = untrack(items)
local min_index = math.floor(canvas_position.X / child_size)
local max_index = math.ceil((canvas_position.X + absolute_size.X) / child_size)
local max_items = math.huge
if props.max_items then
max_items = props.max_items()
end
untrack(function()
-- mark any sources out of range as unused
local unused = {}
for i, s in sources do
local index = s()
if
index >= math.max(min_index, 1)
and index <= math.min(max_index, max_items)
then continue end
unused[i] = true
s(-1)
end
-- add sources necessary
if #sources < total_required then
for i = #sources + 1, total_required do
sources[i] = source(-1)
unused[i] = true
end
items(sources)
end
-- update indexes of any sources that went unused
local did_not_render = {}
for i = math.max(min_index, 1), math.min(max_index, max_items) do
did_not_render[i] = true
end
for _, s in sources do
did_not_render[s()] = nil
end
for index in unused do
local s = sources[index]
local key = next(did_not_render)
if not key then break end
s(key)
did_not_render[key] = nil
unused[index] = nil
end
if next(did_not_render) then warn("missing source!", next(did_not_render)) end
-- remove unnecessary sources
if #sources > total_required then
for i = #sources, 1, -1 do
if unused[i] then
table.remove(sources, i)
end
unused[i] = nil
if #sources < total_required then break end
end
items(sources)
end
end)
end)
return create "ScrollingFrame" {
Size = props.size or UDim2.fromScale(1, 1),
Position = props.position,
AnchorPoint = props.anchorpoint,
BackgroundTransparency = 1,
CanvasSize = function()
if props.max_items then
return UDim2.fromOffset(props.max_items() * (item_size + separator_size), 0)
else
local absolute_size = absolute_size()
local canvas_position = canvas_position()
local child_size = item_size + separator_size
local max_index = math.ceil((canvas_position.X + absolute_size.X) / child_size) + OVERFLOW_ONE_SIDE
return UDim2.fromOffset(max_index * child_size, 0)
end
end,
values(items, function(index)
return create "Frame" {
Name = index,
Position = function()
if index() == -1 then UDim2.fromOffset(0, -1000) end
return UDim2.fromOffset(
(item_size + separator_size) * (index() - 1),
0
)
end,
Size = UDim2.new(0, item_size + separator_size, 1, 0),
BackgroundTransparency = 1,
ui.container {
Name = "Item",
item(index),
},
if separator then
ui.container {
Name = "Separator",
separator(index)
}
else nil,
}
end),
changed("AbsoluteSize", absolute_size),
changed("CanvasPosition", canvas_position),
unpack(props),
}
end

View file

@ -0,0 +1,19 @@
local entity = require(script.apps.entity)
local home = require(script.apps.home)
local overview_scheduler = require(script.apps.overview_scheduler)
local registry = require(script.apps.registry)
local spawn_app = require(script.spawn_app)
return {
apps = {
home = home,
entity = entity,
scheduler = overview_scheduler,
registry = registry,
},
spawn_app = spawn_app.spawn_app,
unmount_all = spawn_app.unmount_all
}

View file

@ -0,0 +1,36 @@
local Players = game:GetService("Players")
local vide = require(script.Parent.Parent.vide)
local types = require(script.Parent.Parent.modules.types)
local destroy_fn = {}
local function unmount_all()
for destroy in destroy_fn do
destroy()
end
end
local function spawn_app<T>(app: types.Application<T>, props: T): () -> ()
return vide.root(function(destroy)
local destroy = function()
destroy_fn[destroy] = nil
destroy()
end
local application = app.mount(props, destroy)
application.Parent = Players.LocalPlayer.PlayerGui
vide.cleanup(application)
destroy_fn[destroy] = true
return destroy
end)
end
return {
unmount_all = unmount_all,
spawn_app = spawn_app
}

View file

@ -0,0 +1,46 @@
local RunService = game:GetService("RunService")
local jecs = require("@jecs")
local jabby = require("@modules/jabby")
local world = jecs.world()
jabby.set_check_function(function()
return true
end)
local scheduler = jabby.scheduler.create()
jabby.register({
applet = jabby.applets.scheduler,
name = "Example Scheduler",
configuration = {
scheduler = scheduler,
},
})
jabby.register({
applet = jabby.applets.world,
name = "Example World",
configuration = {
world = world,
},
})
local system_id = scheduler:register_system({
name = "example_system",
module = script,
})
local function example_system(_world: jecs.World, dt: number)
return dt
end
if RunService:IsClient() then
local client = jabby.obtain_client()
client.spawn_app(client.apps.home, nil)
end
RunService.Heartbeat:Connect(function(dt)
scheduler:run(system_id, example_system, world, dt)
end)

4115
modules/Jabby/jecs.luau Normal file

File diff suppressed because it is too large Load diff

60
modules/Jabby/module.luau Normal file
View file

@ -0,0 +1,60 @@
local jecs = require(script.Parent.jecs)
local traffic_check = require(script.Parent.modules.traffic_check)
local types = require(script.Parent.modules.types)
local vm_id = require(script.Parent.modules.vm_id)
local server = require(script.Parent.server)
local public = require(script.Parent.server.public)
local scheduler = require(script.Parent.server.scheduler)
type Applet<T> = {
add_to_public: (name: string, config: T) -> ()
}
local world_applet = {
add_to_public = function(
name: string, config: { world: jecs.World, entities: {[Instance]: jecs.Entity<any>}?, get_entity_from_part: ((part: BasePart) -> (jecs.Entity<any>, Part?))? }
)
public.updated = true
table.insert(public, {
class_name = "World",
name = name,
world = config.world,
entities = config.entities,
get_entity_from_part = config.get_entity_from_part
})
end
}
local scheduler_applet = {
add_to_public = function(
name: string, config: { scheduler: types.Scheduler }
)
public.updated = true
config.scheduler.name = name
table.insert(public, config.scheduler)
end
}
return {
set_check_function = function(callback: (Player) -> boolean)
traffic_check.can_use_jabby = callback
end,
obtain_client = function()
return require(script.Parent.client)
end,
vm_id = vm_id,
scheduler = scheduler,
broadcast_server = server.broadcast,
applets = {
world = world_applet,
scheduler = scheduler_applet
},
register = function<T>(info: { name: string, applet: Applet<T>, configuration: T })
info.applet.add_to_public(info.name, info.configuration)
end,
}

View file

@ -0,0 +1,11 @@
local function average(n: {number})
local sum = 0
for i, v in n do
sum += v
end
return sum / #n
end
return average

View file

@ -0,0 +1,39 @@
local function convert_units(unit: string, value: number): (string)
local s = math.sign(value)
value = math.abs(value)
local prefixes = {
[4] = "T",
[3] ="G",
[2] ="M",
[1] = "k",
[0] = " ",
[-1] = "m",
[-2] = "u",
[-3] = "n",
[-4] = "p"
}
local order = 0
while value >= 1000 do
order += 1
value /= 1000
end
while value ~= 0 and value < 1 do
order -= 1
value *= 1000
end
if value >= 100 then
value = math.floor(value)
elseif value >= 10 then
value = math.floor(value * 1e1) / 1e1
elseif value >= 1 then
value = math.floor(value * 1e2) / 1e2
end
return value * s .. prefixes[order] .. unit
end
return convert_units

View file

@ -0,0 +1,5 @@
local types = require(script.Parent.types)
return function(connector: types.IncomingConnector | types.OutgoingConnector)
return `{connector.host}\0{connector.from_vm or connector.to_vm}`
end

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,107 @@
local scheduler = require(script.Parent.Parent.server.scheduler)
type Array<T> = { T }
export type System = (any, number) -> ...(any, number) -> ()
type GroupInfo = { i: number?, o: number? }
type SystemGroup = {
interval: number,
offset: number,
dt: number,
[number]: {
id: number,
name: string,
type: number,
fn: (...any) -> ...any
}
}
local function loop_create(name: string, data: any, ...: ModuleScript | () -> () | GroupInfo)
local jabby_scheduler = scheduler.create(name)
local groups = {} :: Array<SystemGroup>
local current_group: SystemGroup?
local function process_systems(array: Array<any>)
for i, v in array do
if type(v) == "table" then
if v.i then
if current_group then
table.insert(groups, current_group)
end
current_group = {
interval = v.i or 1,
offset = v.o or 0,
dt = 0
}
else
process_systems(v)
end
elseif type(v) == "function" then
assert(current_group)
table.insert(current_group, {
id = jabby_scheduler:register_system(),
name = "UNNAMED",
type = 0,
fn = v
})
else
assert(current_group)
local fn = (require :: any)(v) :: System
local fn2 = fn(data, 0)
table.insert(current_group, {
id = jabby_scheduler:register_system({name = `{v.Name}`}),
name = v.Name,
type = fn2 and 1 or 0,
fn = fn2 or fn
})
end
end
end
process_systems { ... }
assert(current_group)
table.insert(groups, current_group)
current_group = nil
local frame_count = 0
return function(dt)
frame_count += 1
debug.profilebegin("ECS LOOP")
for _, group in groups do
group.dt += dt
if frame_count % group.interval == group.offset then
for _, system in ipairs(group) do
debug.setmemorycategory(system.name)
debug.profilebegin(system.name)
if system.type == 0 then
jabby_scheduler:run(system.id, system.fn, data, group.dt)
else
jabby_scheduler:run(system.id, system.fn, group.dt)
end
debug.profileend()
end
group.dt = 0
end
end
debug.resetmemorycategory()
debug.profileend()
end, jabby_scheduler
end
return loop_create

View file

@ -0,0 +1,202 @@
--[[
net is a utility library designed to handle connections to other actors and
the server for me.
]]
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local signal = require(script.Parent.signal)
local vm_id = require(script.Parent.vm_id)
local traffic_check = require(script.Parent.traffic_check)
local types = require(script.Parent.types)
local local_host: "server" | Player
local MANAGER_VM = 0
if RunService:IsServer() then
local_host = "server"
else
local_host = Players.LocalPlayer
end
local function tincoming_connector(t: any): boolean
if typeof(t) ~= "table" then return false end
if not (t.host == "server" or (typeof(t.host) == "Instance" and t.host:IsA("Player"))) then return false end
if typeof(t.from_vm) ~= "number" then return false end
if t.to_vm ~= nil and typeof(t.to_vm) ~= "number" then return false end
return true
end
local NAME = "JABBY_REMOTES"
local folder: Instance
local created_folder = false
if RunService:IsServer() then
local existing = ReplicatedStorage:FindFirstChild(NAME)
if existing then
folder = existing
else
created_folder = true
warn("\z
There's a bug with jabby that sometimes causes the JABBY_REMOTES folder to not replicate very rarely. \z
Unfortunately, I still haven't thought of a fix yet -,- so please instead clone the JABBY_REMOTES \z
folder into your game which should stop it. make sure to set archivable to true!")
folder = Instance.new("Folder")
folder.Name = NAME
folder.Archivable = false
folder.Parent = ReplicatedStorage
end
else
folder = ReplicatedStorage:WaitForChild(NAME)
end
local function get_remote_event(name: string, unreliable: boolean?): RemoteEvent & { actor: BindableEvent, peer: RemoteEvent }
if RunService:IsServer() then
return folder:FindFirstChild(name) :: RemoteEvent & { actor: BindableEvent }
or (function()
if not created_folder then
warn(`btw, you are missing {name} from the JABBY_REMOTES folder`)
end
local remote = Instance.new(if unreliable then "UnreliableRemoteEvent" else "RemoteEvent")
remote.Name = name
remote.Parent = folder
local fire_actor = Instance.new("BindableEvent")
fire_actor.Name = "actor"
fire_actor.Parent = remote
local peer = Instance.new("RemoteEvent")
peer.Name = "peer"
peer.Parent = remote
return remote :: RemoteEvent & { actor: BindableEvent, peer: RemoteEvent }
end)()
else
return folder:WaitForChild(name) :: RemoteEvent & { actor: BindableEvent }
end
end
local function create_event<T...>(name: string, unreliable: boolean?, do_not_block_traffic: boolean?)
local remote = get_remote_event(name, unreliable)
local on_event_fire, fire = signal()
local event = {
type = "event",
fire = function(_, connector: types.OutgoingConnector, ...)
--- if the host is within this vm, we can fire it straight to
if not traffic_check.check(local_host, connector.host, true) then return end
-- same host, same vm.
if
connector.host == local_host
and connector.to_vm == vm_id
then
local incoming = {
host = local_host,
from_vm = vm_id,
to_vm = connector.to_vm
}
fire(incoming, ...)
--- if the host is the same, but in a separate actor
--- we have to fire the actor
elseif
connector.host == local_host
and connector.to_vm ~= vm_id
then
local incoming = {
host = local_host,
from_vm = vm_id,
to_vm = connector.to_vm
}
remote.actor:Fire(incoming, ...)
--- we need to fire the server
elseif connector.host == "server" then
local incoming = {
host = "server",
from_vm = vm_id,
to_vm = connector.to_vm
}
remote:FireServer(incoming, ...)
--- we need to fire the client
elseif local_host == "server" then
local incoming = {
host = "server",
from_vm = vm_id,
to_vm = connector.to_vm
}
remote:FireClient(connector.host, incoming, ...)
--- we need to tell the server to redirect this to the client
else
local incoming = {
host = connector.host,
from_vm = vm_id,
to_vm = connector.to_vm
}
remote:FireServer(incoming, ...)
end
end,
connect = function(_, callback: (types.IncomingConnector, T...) -> ())
return on_event_fire:connect(callback :: any)
end
}
if RunService:IsServer() then
remote.OnServerEvent:Connect(function(player, target: types.IncomingConnector, ...)
--- check if the player is allowed to send this
if not do_not_block_traffic and not traffic_check.check(player, target.host) then
return
end
--- check if its a proper connector
if not tincoming_connector(target) then return end
if target.host == "server" and (target.to_vm == vm_id or target.to_vm == nil) then
target.host = player
fire(target, ...)
elseif target.host ~= "server" and vm_id == MANAGER_VM then
local to = target.host
target.host = player
remote:FireClient(
to,
target,
...
)
end
end)
else
remote.OnClientEvent:Connect(function(incoming: types.IncomingConnector, ...)
-- print("receive", remote.Name, "from", incoming.host)
if tincoming_connector(incoming) == false then return end
if incoming.to_vm ~= vm_id and incoming.to_vm ~= nil then return end
traffic_check._whitelist(local_host, incoming.host)
fire(incoming, ...)
end)
end
remote:WaitForChild("actor").Event:Connect(function(incoming: types.IncomingConnector, ...)
if incoming.to_vm ~= vm_id and incoming.to_vm ~= nil then return end
fire(incoming, ...)
end)
return (event :: any) :: types.NetEvent<T...>
end
return {
create_event = create_event,
local_host = local_host
}

View file

@ -0,0 +1,115 @@
--- Licensed under MIT from centau_ri
export type Queue<T...> = typeof(setmetatable(
{} :: {
add: (self: Queue<T...>, T...) -> (),
clear: (self: Queue<T...>) -> (),
iter: (self: Queue<T...>) -> () -> T...,
},
{} :: {
__len: (self: Queue<T...>) -> number,
__iter: (self: Queue<T...>) -> () -> T...,
}
))
type Array<T> = { T }
local Queue = {}
do
Queue.__index = Queue
type _Queue = Queue<...any> & {
size: number,
columns: Array<Array<unknown>>,
}
function Queue.new<T...>(): Queue<T...>
local self: _Queue = setmetatable({
size = 0,
columns = {},
}, Queue) :: any
setmetatable(self.columns, {
__index = function(columns: Array<Array<unknown>>, idx: number)
columns[idx] = {}
return columns[idx]
end,
})
return self :: Queue<T...>
end
function Queue.add(self: _Queue, ...: unknown)
-- iteration will stop if first value is `nil`
assert((...) ~= nil, "first argument cannot be nil")
local columns = self.columns
local n = self.size + 1
self.size = n
for i = 1, select("#", ...) do
columns[i][n] = select(i, ...)
end
end
function Queue.clear(self: _Queue)
self.size = 0
for _, column in next, self.columns do
table.clear(column)
end
end
local function iter(self: _Queue)
local columns = self.columns
local n = self.size
local i = 0
if #columns <= 1 then
local column = columns[1]
return function()
i += 1
local value = column[i]
if i == n then self:clear() end
return value
end
else
local tuple = table.create(#columns)
return function()
i += 1
for ci, column in next, columns do
tuple[ci] = column[i]
end
if i == n then self:clear() end
return unpack(tuple)
end
end
end
Queue.iter = iter
Queue.__iter = iter
function Queue.__len(self: _Queue)
return self.size
end
end
type ISignal<T...> = {
connect: (self: any, listener: (T...) -> ()) -> (),
} | {
Connect: (self: any, listener: (T...) -> ()) -> (),
}
local queue_create = function<T...>(signal: ISignal<T...>?): Queue<T...>
local queue = Queue.new()
if signal then
local connector = (signal :: any).connect or (signal :: any).Connect
assert(connector, "signal has no member `connect()`")
connector(signal, function(...)
queue:add(...)
end)
end
return queue
end :: (<T...>() -> Queue<T...>) & (<T...>(signal: ISignal<T...>) -> Queue<T...>)
return queue_create

View file

@ -0,0 +1,359 @@
local jecs = require(script.Parent.Parent.jecs)
local net = require(script.Parent.net)
local types = require(script.Parent.types)
--todo: redo this file
type Query = {
include: { jecs.Entity<any> },
exclude: { jecs.Entity<any> },
with: { jecs.Entity<any> },
}
return {
--[=[
Broadcasts to clients that a new server has been registered.
Accepts no params.
]=]
new_server_registered = net.create_event("server_registered", false, true)
:: types.NetEvent<>,
--[=[
Pings all servers and make them respond with new_server_registered
]=]
ping = net.create_event("ping", false, true)
:: types.NetEvent<>,
--[=[
Broadcasts to servers that a new client has been registered
Accepts no params.
]=]
bind_to_server_core = net.create_event("client_registered")
:: types.NetEvent<>,
--[=[
Sends a update to a client about a new server
]=]
update_server_data =
net.create_event("update_server_data")
:: types.NetEvent<{
worlds: {{id: number, name: string}},
schedulers: {{id: number, name: string}}
}>,
--[=[
The client will use this to send the mouse pointer to the server
]=]
send_mouse_pointer =
net.create_event("send_mouse_pointer")
:: types.NetEvent<number, Vector3, Vector3>,
--[[
]]
send_mouse_entity =
net.create_event("send_mouse_entity", true)
:: types.NetEvent<number, Part?, number?, string?>,
--[=[
Requests the server to validate a query
world: number
query: string
]=]
validate_query =
net.create_event("validate_query")
:: types.NetEvent<number, string>,
--[=[
Result of the validation
world: number query: string, terms: {}, ok: boolean, message: string?
]=]
validate_result =
net.create_event("validate_result")
:: types.NetEvent<number, string, Query?, boolean, string?>,
--[=[
Requests a server to initiate replication of a query.
world: number
id: number
query_id: number
query: string
]=]
request_query =
net.create_event("replicate_query")
:: types.NetEvent<number, number, string>,
--[=[
Disconnects query
query_id: number
]=]
disconnect_query =
net.create_event("disconnect_query")
:: types.NetEvent<number>,
--[=[
Changes the offsets to query for
query_id: number
from: number
to: number
]=]
advance_query_page =
net.create_event("advance_query_page")
:: types.NetEvent<number, number, number>,
--- pause the query
--- query id
--- should pause
pause_query =
net.create_event("pause_query")
:: types.NetEvent<number, boolean>,
--- refreshes query results
--- query_id
refresh_results =
net.create_event("refresh_query")
:: types.NetEvent<number>,
--[=[
Updates a single result
query_id: number
frame: number
column: number
row: number
value: any
]=]
update_query_result =
net.create_event("update_query_result", true)
:: types.NetEvent<(number, number, number, number, any)>,
--[=[
Counts the total number of entities
query id: number
count: number
]=]
count_total_entities =
net.create_event("count_total_entities", true)
:: types.NetEvent<number, number>,
--[=[
Requests a server to initiate replication of a scheduler
]=]
request_scheduler =
net.create_event("initiate_replicate_scheduler") ::
types.NetEvent<number>,
--[=[
Requests the server to stop replicating a scheduler
]=]
disconnect_scheduler =
net.create_event("disconnect_replicate_scheduler") ::
types.NetEvent<number>,
--[=[
Updates any static data about systems (like new systems)
systemid: number
static_data: {}
]=]
scheduler_system_static_update =
net.create_event("scheduler_system_update_static") ::
types.NetEvent<number, number, types.SystemData?>,
--[=[
Appends a frame to a system
systemid: number
frame_count: number
time_took: number
]=]
scheduler_system_update =
net.create_event("append_frame_system", true) ::
types.NetEvent<number, number, number, number>,
--[=[
Pauses a system
scheduler: number
systemid: number
paused: boolean
]=]
scheduler_system_pause =
net.create_event("scheduler_pause") ::
types.NetEvent<number, number, boolean>,
--[[
Validates a component
world: number
component: string
]]
validate_entity_component =
net.create_event("validate_entity_component") ::
types.NetEvent<number, string>,
--[[
entity component reslt
world: number
component: string
ok: boolean
reason: string
]]
validate_entity_component_result =
net.create_event("validate_entity_component_result") ::
types.NetEvent<number, string, boolean, string>,
--[[
Inspect a entity's components
world: number
entity: number,
inspectid: number
]]
inspect_entity =
net.create_event("inspect_entity") ::
types.NetEvent<number, number, number>,
--[=[
Gets the component of an entity
inspect: number
component: string
]=]
get_component =
net.create_event("get_entity_component") ::
types.NetEvent<number, string>,
--[=[
Returns the component of an entity
inspect: number
component: string
value: string
]=]
return_component =
net.create_event("return_entity_component") ::
types.NetEvent<number, string, string>,
--[[
Delete entity
inspectid: number
]]
delete_entity =
net.create_event("delete_entity") ::
types.NetEvent<number>,
--[[
Stops inspecting a entity
inspectid: number
]]
stop_inspect_entity =
net.create_event("stop_inspect_entity") ::
types.NetEvent<number>,
--[[
Updates a entity
inspectid: number
changes: {[component]: string}
]]
update_entity =
net.create_event("update_entity") ::
types.NetEvent<number, {[string]: string}>,
--[[
Update the settings when dealing with inspecting
inspectid: nuimber,
settings: {}
]]
update_inspect_settings =
net.create_event("inspect_entity_settings_update") ::
types.NetEvent<number, {paused: boolean}>,
--[[
Inspector update
inspectid: number
key: string
value: string
]]
inspect_entity_update =
net.create_event("inspect_entity_update") ::
types.NetEvent<number, string, string?>,
--[[
Creates a watch on a system
scheduler: number,
system: number
watchid: number
]]
create_watch =
net.create_event("create_watch") ::
types.NetEvent<number, number, number>,
--[[
Removes a watch on a system
watchid: number
]]
remove_watch =
net.create_event("remove_watch") ::
types.NetEvent<number>,
--[[
Retrieves data about a frame for a watch
watchid: number
frame: number
]]
request_watch_data =
net.create_event("request_watch_data") ::
types.NetEvent<number, number>,
--[[
Updates watch data for a frame
watchid: number
frame: number
changes: types.WatchLoggedChanges
]]
update_watch_data =
net.create_event("update_watch_data") ::
types.NetEvent<number, number, types.WatchLoggedChanges?>,
start_record_watch =
net.create_event("start_record_watch") ::
types.NetEvent<number>,
stop_watch =
net.create_event("stop_watch") ::
types.NetEvent<number>,
clear_watch =
net.create_event("clear_watch") ::
types.NetEvent<number>,
connect_watch =
net.create_event("connect_to_watch") ::
types.NetEvent<number>,
disconnect_watch =
net.create_event("disconnect_watch") ::
types.NetEvent<number>,
update_overview =
net.create_event("update_watch_overview", true) ::
types.NetEvent<number, number, number>
}

View file

@ -0,0 +1,11 @@
--!nocheck
local types = require(script.Parent.types)
local function reverse(connector: types.IncomingConnector): types.OutgoingConnector
return {
host = connector.host,
to_vm = connector.from_vm,
}
end
return reverse

View file

@ -0,0 +1,75 @@
--[[
A rudimentary signal class. Yielding may cause bugs.
]]
local signal = {}
signal.__index = signal
type Connection = { disconnect: (any?) -> (), reconnect: (any?) -> () }
export type Signal<T... = ...unknown> = {
class_name: "Signal",
connect: (Signal<T...>, callback: (T...) -> ()) -> Connection,
wait: (Signal<T...>) -> T...,
once: (Signal<T...>, callback: (T...) -> ()) -> Connection,
callbacks: { [(T...) -> ()]: true },
}
export type SignalInternal<T... = ...unknown> = Signal<T...> & {
fire: (SignalInternal<T...>, T...) -> (),
}
function signal.connect<T...>(self: Signal<T...>, callback: (T...) -> ())
assert(type(callback) == "function")
self.callbacks[callback] = true
return {
disconnect = function() self.callbacks[callback] = nil end,
reconnect = function() self.callbacks[callback] = true end,
}
end
function signal.fire<T...>(self: Signal<T...>, ...: T...)
for callback in self.callbacks do
callback(...)
end
end
function signal.once<T...>(self: Signal<T...>, callback: (T...) -> ())
local connection
connection = self:connect(function(...)
connection:disconnect()
callback(...)
end)
return connection
end
function signal.wait<T...>(self: Signal<T...>)
local thread = coroutine.running()
local connection = self:connect(function(...) coroutine.resume(thread, ...) end)
local packed = { coroutine.yield() }
connection:disconnect()
return unpack(packed)
end
local function new_signal<T...>(): (Signal<T...>, (T...) -> ())
local self = setmetatable({
class_name = "Signal",
callbacks = {},
}, signal)
local function fire(...)
for callback in self.callbacks :: any do
callback(...)
end
end
return self :: any, fire
end
return new_signal

View file

@ -0,0 +1,82 @@
local Players = game:GetService("Players")
--[[
a utility library to handle checking traffic and determining if the sender is
permitted to send the given data.
]]
local signal = require(script.Parent.signal)
local traffic_check = {}
local whitelist_player_to = {}
local on_fail, fire = signal()
--- A function that needs to be overwritten by the user.
--- This function is used to find out what permissions a user may have.
traffic_check.can_use_jabby = function(player: Player)
local is_studio = game:GetService("RunService"):IsStudio()
return is_studio --is_owner or is_studio
end
--- Runs a callback defined by the developer to determine if a player is allowed
--- to use a given function
local function communication_is_allowed(from: "server" | Player, to: "server" | Player, dont_whitelist: boolean?)
if from == "server" then return true end
whitelist_player_to[from] = whitelist_player_to[from] or {}
whitelist_player_to[to] = whitelist_player_to[to] or {}
if traffic_check.can_use_jabby(from) or whitelist_player_to[from][to] then
if dont_whitelist then return true end
whitelist_player_to[to][from] = from
return true
else
return false
end
end
--- Runs the given check and fires the on_fail signal if the player fails the
--- check.
local function check(from: "server" | Player, to: "server" | Player, dont_whitelist: boolean?)
if communication_is_allowed(from, to, dont_whitelist) then
return true
else
fire(from)
return false
end
end
local function check_no_wl(from: "server" | Player)
if from == "server" then return true end
if traffic_check.can_use_jabby(from) then
return true
else
-- print(from, "cant use jabby")
fire(from)
return false
end
end
local function _whitelist(from: "server" | Player, to: "server" | Player)
whitelist_player_to[from] = whitelist_player_to[from] or {}
whitelist_player_to[to] = whitelist_player_to[to] or {}
whitelist_player_to[from][to] = from
end
traffic_check.communication_is_allowed = communication_is_allowed
traffic_check.check_no_wl = check_no_wl
traffic_check.check = check
traffic_check._whitelist = _whitelist
traffic_check.on_fail = on_fail
Players.PlayerRemoving:Connect(function(player)
whitelist_player_to[player] = nil
end)
return traffic_check

View file

@ -0,0 +1,117 @@
local jecs = require(script.Parent.Parent.jecs)
type host = "client" | "server"
export type IncomingConnector = {
host: Player | "server",
from_vm: number,
to_vm: number
}
export type OutgoingConnector = {
host: Player | "server",
to_vm: number?, -- not specifying a vm makes it received by all
from_vm: nil
}
export type NetEvent<T...> = {
type: "event",
fire: (any, connector: OutgoingConnector, T...) -> (),
connect: (any, callback: (connector: IncomingConnector, T...) -> ()) -> RBXScriptConnection,
}
export type NetCallback<T..., U...> = {
type: "callback",
invoke: (any, connector: OutgoingConnector, T...) -> U...,
set_callback: (any, callback: (connector: IncomingConnector, T...) -> U...) -> (),
}
export type SystemId = number
export type SystemTag = "processing" | "finished" | "paused"
export type SystemSettingData = {
name: string?,
phase: string?,
layout_order: number?,
paused: boolean?
}
export type SystemData = {
name: string,
phase: string?,
layout_order: number,
paused: boolean
}
type ChangeTypes = "remove" | "clear" | "delete" | "add" | "set" | "entity" | "component"
export type WatchLoggedChanges = {
types: {ChangeTypes},
entities: {jecs.Entity<any>},
component: {jecs.Entity<any>},
values: {string},
worlds: {jecs.World}
}
export type SystemWatch = {
--- enables Lua Object Notation.
--- incurs a significant performance penalty.
enable_lon: boolean,
--- the current frame to process
frame: number,
frames: {[number]: WatchLoggedChanges}
}
export type SystemLabel = {}
export type SystemFrame = {
i: number,
s: number
}
type WatchData = {active: boolean, watch: SystemWatch, untrack: () -> ()}
export type Scheduler = {
class_name: "Scheduler",
name: string,
valid_system_ids: {[SystemId]: true},
system_data: {[SystemId]: SystemData},
system_data_updated: {[SystemId]: true},
system_frames: {[SystemId]: SystemFrame},
system_frames_updated: {[SystemId]: {[SystemFrame]: true}},
system_watches: {[SystemId]: {WatchData}},
register_system: (Scheduler, settings: SystemSettingData?) -> SystemId,
set_system_data: (Scheduler, system: SystemId, settings: SystemSettingData) -> (),
get_system_data: (Scheduler, system: SystemId) -> SystemSettingData,
create_watch_for_system: (Scheduler, system: SystemId) -> WatchData,
remove_system: (Scheduler, system: SystemId) -> (),
-- mark_system_frame_start: (Scheduler, system: SystemId) -> (),
-- mark_system_frame_end: (Scheduler, system: SystemId, s: number?) -> (),
-- append_extra_frame_data: (Scheduler, system: SystemId, label: SystemLabel) -> (),
--- this should call mark_system_frame_start and mark_system_frame_end for you
run: <T...>(Scheduler, system: SystemId, system: () -> (), T...) -> (),
}
export type World = {
class_name: "World",
name: string,
world: jecs.World,
entities: {[Instance]: jecs.Entity<any>}?,
get_entity_from_part: ((part: BasePart) -> (jecs.Entity<any>?, Part?))?
}
export type Application<T> = {
class_name: "app",
name: string,
mount: (props: T, destroy: () -> ()) -> Instance
}
return nil

View file

@ -0,0 +1,71 @@
--------------------------------------------------------------------------------
-- videx/store.luau
--------------------------------------------------------------------------------
local vide = require(script.Parent.Parent.vide)
local source = vide.source
local NULL = newproxy()
local Store = {}
--[=[
Creates a new store object that receives some initial state and then returns
a table with the same structure, but all keys of the given table will be reactive.
When accessed inside a reactive scope, the reactive scope will update whenever
the key that is accessed is changed.
@param initial_state `T : {[string]: any}` The initial state the store will start in.
@param mutations `() -> {[string]: (T, ...any) -> ...any}?` A list of functions that mutate the data.
@return `T & U` A resulting table that
]=]
function Store.new<T, U>(
initial_state: T & {},
mutations: (T & U) -> U
): T & U
local sources = {}
for i, v in initial_state :: any do
local src = source(v ~= NULL and v or nil)
sources[i] = src
end
local internal_proxy = {}
setmetatable(internal_proxy, {
__index = function(_, index)
return sources[index]()
end,
__newindex = function(_, index, value)
sources[index](value)
end
})
local external_proxy = {}
setmetatable(external_proxy :: any, {
__index = function(_, index)
local src = sources[index]
if src == nil then error(`invalid index {index}`, 2) end
return src()
end,
__newindex = function(_, index, value)
sources[index](value)
end
})
for i, v in next, mutations(internal_proxy :: any) :: any do
if rawget(external_proxy, i) then
error(`duplicate field "{i}"`, 2)
end
rawset(external_proxy, i, v)
end
return external_proxy :: T & U & {}
end
--- A special symbol used to indicate that a value should be nil within a Store.
Store.null = NULL :: nil
return Store

View file

@ -0,0 +1,14 @@
--[[
Provides a unique identifier for a VM.
This currently cannot be tested unless there is some parallel system for jest.
]]
local SharedTableRegistry = game:GetService("SharedTableRegistry")
local shared_table = SharedTableRegistry:GetSharedTable("_gorp_common_vm_count")
shared_table.id = shared_table.id or 0
return SharedTable.increment(shared_table, "id", 1)

View file

@ -0,0 +1,42 @@
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local loop = require(script.Parent.modules.loop)
local remotes = require(script.Parent.modules.remotes)
local traffic_check = require(script.Parent.modules.traffic_check)
local vm_id = require(script.Parent.modules.vm_id)
local function broadcast()
for _, player in Players:GetPlayers() do
if not traffic_check.can_use_jabby(player) then continue end
remotes.new_server_registered:fire({
host = player,
})
end
end
task.delay(0, broadcast)
local systems = script.systems
local loop = loop (
`jabby-host:{
if RunService:IsServer() then "server" else "client"
}-vm:{vm_id}`,
nil,
{i = 1},
systems.ping,
systems.replicate_core,
systems.replicate_scheduler,
systems.replicate_registry,
systems.replicate_system_watch,
systems.mouse_pointer,
systems.entity
)
RunService.PostSimulation:Connect(loop)
return {
broadcast = broadcast
}

View file

@ -0,0 +1,16 @@
--[[
anything in here is considered "public" and will be visible to jabby clients
]]
local self = {
updated = false,
} :: {
updated: boolean,
[number]: any
}
return self

View file

@ -0,0 +1,402 @@
--!strict
local escape_chars = {
a = "\a",
b = "\b",
f = "\f",
n = "\n",
r = "\r",
t = "\t",
v = "\v",
["\\"] = "\\",
["\""] = "\"",
["\'"] = "\'"
}
export type Position = {
line: number,
pos: number,
col: number
}
export type String = {type: "string", s: string}
export type Number = {type: "number", s: number}
export type Identifier = {type: "identifier", s: string}
export type Operator = {type: "operator", s: "#" | "!" | "*" | "$"}
export type Symbol = {type: "symbol", s: "(" | ")" | ";" | ","}
export type EOF = {type: "eof", s: "eof"}
export type Token =
String
| Number
| Identifier
| Symbol
| Operator
| EOF
export type Stream = {
next: () -> Token,
peek: (n: number?) -> Token,
eof: () -> boolean,
croak: (msg: string) -> (),
pos: () -> Position
}
local function stream(input: string)
local pos = 0
local line = 1
local col = 1
local function peek(): string
return string.sub(input, pos+1, pos+1)
end
local function next(): string
local char = peek()
pos += 1
if char == "\n" then line += 1; col = 1
else col += 1 end
return char
end
local function eof(): boolean
return peek() == ""
end
local function position()
return {
pos = pos,
line = line,
col = col
}
end
local function croak(msg)
error(`{msg} ({line}:{col})`, 0)
end
return {
peek = peek,
next = next,
eof = eof,
croak = croak,
pos = position
}
end
local function lex(source: string): Stream
local input = stream(source)
local function is_whitespace(char: string)
return not not string.match(char, "[\t ]")
end
local function is_digit(char: string)
return not not (string.match(char, "%d"))
end
local function is_start_identifier(char: string)
return not not string.match(char, "[%a_]")
end
local function is_identifier(char: string)
return not not string.match(char, "[%a_:%.]")
end
local function is_op_char(char: string)
return char == "#" or char == "!" or char == "*" or char == "$"
end
local function is_punc(char: string)
return not not string.match(char, "[%(%);,]")
end
local function read_while(predicate: (char: string) -> boolean)
local str = ""
while input.eof() == false and predicate(input.peek()) do
str ..= input.next()
end
return str
end
local function skip_whitespace()
read_while(is_whitespace)
end
local function read_string(): String
local escaped = false
local token = ""
local eliminator = input.next()
local from = input.pos()
while input.eof() == false and (input.peek() ~= eliminator or escaped) do
local char = input.next()
if char == "\\" then
escaped = true
end
if escaped then
token ..= escape_chars[char] or input.croak(`cannot escape {char}`)
escaped = false
else
token ..= char
end
end
local to = input.pos()
-- print("t", token, eliminator, input.peek())
if input.peek() ~= eliminator then input.croak("unterminated string") end
input.next()
return {type = "string", s = token, from = from, to = to}
end
local function read_number(): Number
local decimal_pointer = false
local from = input.pos()
local token = read_while(function(char)
if decimal_pointer and char == "." then return false end
if char == "." then decimal_pointer = true end
return is_digit(char)
end)
local to = input.pos()
local n = tonumber(token)
if not n then
input.croak(`could not read {token} as number`)
end
return {type = "number", s = assert(n), from = from, to = to}
end
local function read_identifier(): Identifier
local from = input.pos()
local token = read_while(is_identifier)
local to = input.pos()
-- if table.find(keywords, token) then
-- return {type = "keyword", s = token :: any, from = from, to = to}
-- else
return {type = "identifier", s = token, from = from, to = to}
-- end
end
local function read_next(): Token
skip_whitespace()
if input.eof() then return {type = "eof", s = "eof"} end
local char = input.peek()
if char == "\"" or char == "'" then return read_string() end
if is_digit(char) then return read_number() end
if is_start_identifier(char) then return read_identifier() end
if is_op_char(char) then return {type = "operator", s = input.next() :: any} end
if is_punc(char) then return {type = "symbol", s = input.next() :: any} end
input.croak(`cannot lex {char}`)
error("fail")
end
local current: {Token} = {}
local function next()
local token = table.remove(current, 1)
return if token == nil then read_next() else token
end
local function peek(n: number?)
local n = n or 1
while #current < n do
table.insert(current, read_next())
end
return current[n]
end
local function eof()
return peek().type == "eof"
end
return {
peek = peek,
next = next,
eof = eof,
croak = input.croak,
pos = input.pos
}
end
type Wildcard = {
type: "Wildcard",
name: "*"
}
export type Value = {
type: "Name",
name: string
} | {
type: "Entity",
entity: number
}
export type PureComponent = {
type: "Component",
query: boolean,
exclude: boolean,
value: Value,
}
export type Relationship = {
type: "Relationship",
query: boolean,
exclude: boolean,
left: PureComponent | Wildcard,
right: PureComponent | Wildcard
}
type Component = Relationship | PureComponent | Wildcard
local function parse(input: string): {PureComponent | Relationship}
local lexer = lex(input)
local result: {PureComponent | Relationship} = {}
local should_query = true
local should_exclude = false
local should_relationship = false
local interpret_pointer = false
local components: {Component} = {}
while true do
local symbol = lexer.peek()
-- print2(symbol)
if symbol.type == "eof" then
break
elseif interpret_pointer or symbol.type == "number" then
if not interpret_pointer then
lexer.croak("expected $")
elseif symbol.type ~= "number" then
lexer.croak("expected number")
error("")
end
table.insert(components, {
type = "Component",
query = should_query,
exclude = should_exclude,
value = {type = "Entity", entity = tonumber(lexer.next().s) :: number}
})
should_query = if should_relationship then should_query else true
should_exclude = if should_relationship then should_exclude else false
interpret_pointer = false
if lexer.peek().type ~= "symbol" and lexer.peek().type ~= "eof" then lexer.croak("expected symbol or eof after identifier") end
elseif symbol.type == "operator" then
if symbol.s == "#" then
if should_relationship then lexer.croak("cannot tag inside relationship") end
should_query = false
lexer.next()
elseif symbol.s == "!" then
if should_relationship then lexer.croak("cannot exclude in relationship") end
should_exclude = true
should_query = false
lexer.next()
elseif symbol.s == "$" then
interpret_pointer = true
lexer.next()
elseif symbol.s == "*" then
if not should_relationship then lexer.croak("cannot use wildcards outside relationship") end
table.insert(components, {
type = "Wildcard",
name = "*"
})
lexer.next()
end
elseif symbol.type == "symbol" then
if symbol.s == "(" then
if should_relationship == true then lexer.croak("relationship within relationship") end
should_relationship = true
lexer.next()
elseif symbol.s == ")" then
if should_relationship == false then lexer.croak("missing (") end
if #components == 2 then
local right = table.remove(components) :: Component
local left = table.remove(components) :: Component
if left.type == "Wildcard" and right.type == "Wildcard" then
lexer.croak("both components are wildcards")
end
components = {{
type = "Relationship",
query = should_query,
exclude = should_exclude,
left = left :: any,
right = right :: any
}}
should_query = true
should_exclude = false
should_relationship = false
lexer.next()
else
lexer.croak(`expected 2 components, got {#components}`)
end
elseif symbol.s == "," or symbol.s == ";" then
if should_relationship then
lexer.next()
continue
end
local ctype = table.remove(components)
if ctype == nil then
lexer.croak("no component provided")
error("")
end
table.insert(result, ctype :: any)
should_query = true
should_exclude = false
lexer.next()
end
elseif symbol.type == "identifier" then
table.insert(components, {
type = "Component",
query = should_query,
exclude = should_exclude,
value = {type = "Name", name = lexer.next().s :: string}
})
should_query = if should_relationship then should_query else true
should_exclude = if should_relationship then should_exclude else false
if lexer.peek().type ~= "symbol" and lexer.peek().type ~= "eof" then lexer.croak("expected symbol or eof after identifier") end
elseif symbol.type == "string" then
table.insert(components, {
type = "Component",
query = should_query,
exclude = should_exclude,
value = {type = "Name", name = lexer.next().s :: string}
})
should_query = if should_relationship then should_query else true
should_exclude = if should_relationship then should_exclude else false
if lexer.peek().type ~= "symbol" and lexer.peek().type ~= "eof" then lexer.croak("expected symbol or eof after string") end
end
end
table.insert(result, table.remove(components) :: any)
return result
end
return parse

View file

@ -0,0 +1,197 @@
local types = require(script.Parent.Parent.modules.types)
local watch = require(script.Parent.watch)
type SystemId = types.SystemId
type SystemSettingData = types.SystemSettingData
type SystemTag = types.SystemTag
type SystemData = types.SystemData
type ProcessingFrame = {
started_at: number
}
type SystemFrame = types.SystemFrame
local MAX_BUFFER_SIZE = 50
local n = 0
local schedulers = {}
local function unit() end
local function create_scheduler()
local count = 1
local frames = 0
local scheduler = {
class_name = "Scheduler",
name = "Scheduler",
--- contains a map of valid system ids
valid_system_ids = {} :: {[SystemId]: true},
--- contains a list of static system data that is updated infrequently
system_data = {} :: {[SystemId]: SystemData},
--- list of system data that has updated
system_data_updated = {} :: {[SystemId]: true},
--- contains a buffer of the last couple frames of system data that is
--- refreshed constantly
system_frames = {} :: {[SystemId]: {SystemFrame}},
--- stores the frames that have been updated
system_frames_updated = {} :: {[SystemId]: {[SystemFrame]: true}},
--- contains the current frame that a system is processing
processing_frame = {} :: {[SystemId]: ProcessingFrame},
--- contains a list of watches for each system
system_watches = {} :: {[SystemId]: {{active: boolean, watch: types.SystemWatch}}}
}
local function ENABLE_WATCHES(id: SystemId)
local watches = scheduler.system_watches[id]
local cleanup = {}
for i, system_watch in watches do
if system_watch.active == false then continue end
watch.step_watch(system_watch.watch)
cleanup[i] = watch.track_watch(system_watch.watch)
end
return function()
for _, stop in cleanup do
stop()
end
end
end
local function ASSERT_SYSTEM_VALID(id: SystemId)
assert(scheduler.valid_system_ids[id], `attempt to use unknown system with id #{id}`)
end
function scheduler:register_system(settings: types.SystemSettingData?)
local id = count; count += 1
scheduler.valid_system_ids[id] = true
scheduler.system_data[id] = {
name = "UNNAMED",
phase = nil,
layout_order = 0,
paused = false
}
scheduler.system_frames[id] = {}
scheduler.system_frames_updated[id] = {}
if settings then
scheduler:set_system_data(id, settings)
end
return id
end
function scheduler:set_system_data(id: SystemId, settings: types.SystemSettingData)
ASSERT_SYSTEM_VALID(id)
for key, value in settings do
scheduler.system_data[id][key] = value
end
scheduler.system_data_updated[id] = true
end
function scheduler:get_system_data(id: SystemId)
ASSERT_SYSTEM_VALID(id)
return scheduler.system_data[id]
end
function scheduler:remove_system(id: SystemId)
scheduler.valid_system_ids[id] = nil
scheduler.system_data[id] = nil
scheduler.system_frames[id] = nil
scheduler.system_frames_updated[id] = nil
scheduler.system_data_updated[id] = true
scheduler.system_watches[id] = nil
end
function scheduler:_mark_system_frame_start(id: SystemId)
ASSERT_SYSTEM_VALID(id)
scheduler.processing_frame[id] = {
started_at = os.clock()
}
end
function scheduler:_mark_system_frame_end(id: SystemId, s: number?)
ASSERT_SYSTEM_VALID(id)
local now = os.clock()
local pending_frame_data = scheduler.processing_frame[id]
assert(pending_frame_data ~= nil, "no processing frame")
local frame = {
i = frames,
s = now - pending_frame_data.started_at
}
frames += 1
scheduler.processing_frame[id] = nil
scheduler.system_frames_updated[id][frame] = true
local last_frame = scheduler.system_frames[id][MAX_BUFFER_SIZE]
if last_frame then
scheduler.system_frames_updated[id][last_frame] = nil
end
table.insert(scheduler.system_frames[id], 1, frame)
table.remove(scheduler.system_frames[id], MAX_BUFFER_SIZE + 1)
end
function scheduler:append_extra_frame_data(id: SystemId, label: {})
--todo
error("todo")
end
function scheduler:run<T...>(id: SystemId, system: (T...) -> (), ...: T...)
ASSERT_SYSTEM_VALID(id)
local system_data = scheduler.system_data[id]
if system_data.paused then return end
local watches = scheduler.system_watches[id]
local cleanup_watches = unit
if watches then
cleanup_watches = ENABLE_WATCHES(id)
end
scheduler:_mark_system_frame_start(id)
system(...)
scheduler:_mark_system_frame_end(id)
cleanup_watches()
end
function scheduler:create_watch_for_system(id: SystemId)
ASSERT_SYSTEM_VALID(id)
local new_watch = watch.create_watch()
local watch_data
scheduler.system_watches[id] = scheduler.system_watches[id] or {} :: never
local function untrack()
local idx = table.find(scheduler.system_watches[id], watch_data)
table.remove(scheduler.system_watches[id], idx)
end
watch_data = {active = false, watch = new_watch, untrack = untrack}
table.insert(scheduler.system_watches[id], watch_data)
return watch_data
end
schedulers[n + 1] = scheduler
n = n + 1
return scheduler
end
return {
create = create_scheduler,
schedulers = schedulers
}

View file

@ -0,0 +1,294 @@
local jecs = require(script.Parent.Parent.Parent.jecs)
local lon = require(script.Parent.Parent.Parent.modules.lon)
local queue = require(script.Parent.Parent.Parent.modules.queue)
local remotes = require(script.Parent.Parent.Parent.modules.remotes)
local reverse_connector = require(script.Parent.Parent.Parent.modules.reverse_connector)
local traffic_check = require(script.Parent.Parent.Parent.modules.traffic_check)
local types = require(script.Parent.Parent.Parent.modules.types)
local public = require(script.Parent.Parent.public)
local query_parser = require(script.Parent.Parent.query_parser)
local entity_index_try_get = jecs.entity_index_try_get
local IS_PAIR = jecs.IS_PAIR
local pair = jecs.pair
local pair_first = jecs.pair_first
local pair_second = jecs.pair_second
local empty_table = {}
local function get_all_components(world, entity): {}
local record = entity_index_try_get(world.entity_index, entity)
if not record then return empty_table end
local archetype = record.archetype
if not archetype then return empty_table end
local components = {}
for _, ty in archetype.types do
table.insert(components, ty)
end
return components
end
local function convert_component(world, debug, entity): string
if IS_PAIR(entity) then
local left = convert_component(world, debug, pair_first(world, entity))
local right = convert_component(world, debug, pair_second(world, entity))
return `({left}, {right})`
else
return world:get(entity, debug) or `${tostring(entity)}`
end
end
--- Indicates a value is a tag
local TAG = newproxy()
--- Indicates a value is set to nil; this is not allowed in 0.3.0
local BAD_VALUE = newproxy()
local function get_component(ctype_name: string, map_components: {[string]: number})
local function get_entity(ctype: query_parser.PureComponent)
local value = ctype.value
if value.type == "Entity" then
return value.entity
elseif value.type == "Name" then
return map_components[value.name]
end
error("bad")
end
local entity_to_set
local parse = query_parser(ctype_name)[1]
if parse.type == "Component" then
entity_to_set = get_entity(parse)
elseif parse.type == "Relationship" then
local left, right = jecs.Wildcard, jecs.Wildcard
if parse.left.type == "Component" then
left = get_entity(parse.left)
end
if parse.right.type == "Component" then
right = get_entity(parse.right)
end
entity_to_set = pair(left, right)
end
return entity_to_set
end
return function()
local inspect_entity = queue(remotes.inspect_entity)
local update_inspect_settings = queue(remotes.update_inspect_settings)
local stop_inspect_entity = queue(remotes.stop_inspect_entity)
local update_entity = queue(remotes.update_entity)
local delete_entity = queue(remotes.delete_entity)
local get_entity_component = queue(remotes.get_component)
local validate_entity = queue(remotes.validate_entity_component)
local inspectors = {}
return function()
for incoming, world_id, ctype_name in validate_entity:iter() do
local world: types.World = public[world_id]
local outgoing = reverse_connector(incoming)
if not traffic_check.check_no_wl(incoming.host) then continue end
if not world or world.class_name ~= "World" then continue end
local map_components = {}
for id, name in world.world:query(jecs.Name):iter() do
map_components[name] = id
end
local ok, reason = pcall(get_component, ctype_name, map_components)
remotes.validate_entity_component_result:fire(
outgoing, world_id, ctype_name, ok, not ok and reason or nil
)
end
for incoming, world_id, entity, inspect_id in inspect_entity:iter() do
local world: types.World = public[world_id]
local outgoing = reverse_connector(incoming)
if not traffic_check.check_no_wl(incoming.host) then continue end
if not world or world.class_name ~= "World" then continue end
inspectors[inspect_id] = {
outgoing = outgoing,
world = world,
entity = entity,
paused = false,
new_values = {},
old_values = {}
}
end
for incoming, inspect_id in stop_inspect_entity:iter() do
if not traffic_check.check_no_wl(incoming.host) then continue end
inspectors[inspect_id] = nil
end
for incoming, inspect_id in delete_entity:iter() do
if not traffic_check.check_no_wl(incoming.host) then continue end
local inspect_data = inspectors[inspect_id]
local world_data = inspect_data.world
local world = world_data.world
local entity = inspect_data.entity
world:delete(entity)
end
for incoming, inspect_id, settings in update_inspect_settings:iter() do
if not traffic_check.check_no_wl(incoming.host) then continue end
local inspect_data = inspectors[inspect_id]
if not inspect_data then continue end
inspect_data.paused = settings.paused
end
for incoming, inspect_id, component in get_entity_component:iter() do
if not traffic_check.check_no_wl(incoming.host) then continue end
local inspect_data = inspectors[inspect_id]
local world_data = inspect_data.world
local world = world_data.world
local entity = inspect_data.entity
local to = reverse_connector(incoming)
local map_components = {}
for id, name in world:query(jecs.Name):iter() do
map_components[name] = id
end
local ok, component = pcall(get_component, component, map_components)
if component and ok then
remotes.return_component:fire(to, inspect_id, component, lon.output(world:get(entity, component), true, false))
else
remotes.return_component:fire(to, inspect_id, component, "nil")
end
end
for incoming, inspect_id, changes in update_entity:iter() do
if not traffic_check.check_no_wl(incoming.host) then continue end
local inspect_data = inspectors[inspect_id]
local world_data = inspect_data.world
local world = world_data.world
local entity = inspect_data.entity
local map_components = {}
for id, name in world:query(jecs.Name):iter() do
map_components[name] = id
end
for ctype_name, value in changes do
-- get the component we need to set
local ok, entity_to_set = pcall(get_component, ctype_name, map_components)
local old = world:get(entity, entity_to_set)
if not ok then
warn("attempted to set", ctype_name, "to", value)
warn(entity_to_set)
continue
end
local ok, result = pcall(lon.parse, value)
if not ok then
warn("attempted to set", ctype_name, "to", value)
warn(result)
continue
end
local ok, data = pcall(
lon.compile,
result,
{ tag = TAG, old = old }
)
if not ok then
warn("attempted to set", ctype_name, "to", value)
warn(data)
continue
end
if data == nil then
world:remove(entity, entity_to_set)
elseif data == TAG then
-- trying to use world:set with a tag will result in an error,
-- even if the data is nil, so instead we use world:add
world:add(entity, entity_to_set)
else
world:set(entity, entity_to_set, data)
end
end
end
for inspect_id, inspector_data in inspectors do
local world = inspector_data.world.world
local entity = inspector_data.entity
if inspector_data.paused then continue end
if world:contains(entity) == false then continue end
local new_values = inspector_data.new_values
local old_values = inspector_data.old_values
local components = get_all_components(world, entity)
local function is_tag(id: jecs.Entity<any>)
return jecs.is_tag(world, id)
end
for _, component in components do
local name = convert_component(world, jecs.Name, component)
local is_tag = is_tag(component)
if is_tag then
new_values[name] = TAG
else
local value = world:get(entity, component)
new_values[name] = if value == nil then BAD_VALUE else value
end
end
for name, new_value in new_values do
local old_value = old_values[name]
if old_value == new_value and typeof(new_value) ~= "table" then continue end
remotes.inspect_entity_update:fire(
inspector_data.outgoing,
inspect_id,
name,
if new_value == TAG then "tag"
--todo: figure out a better way to say that you are not allowed to store nil in a component
elseif new_value == BAD_VALUE then "nil (not allowed)"
else lon.output(new_value, true, true)
)
end
for name, value in old_values do
local new_value = new_values[name]
if new_value ~= nil then continue end
remotes.inspect_entity_update:fire(
inspector_data.outgoing,
inspect_id,
name,
nil
)
end
table.clear(old_values)
inspector_data.new_values = old_values
inspector_data.old_values = new_values
end
end
end

View file

@ -0,0 +1,209 @@
local jecs = require(script.Parent.Parent.Parent.jecs)
local queue = require(script.Parent.Parent.Parent.modules.queue)
local remotes = require(script.Parent.Parent.Parent.modules.remotes)
local reverse_connector = require(script.Parent.Parent.Parent.modules.reverse_connector)
local traffic_check = require(script.Parent.Parent.Parent.modules.traffic_check)
local types = require(script.Parent.Parent.Parent.modules.types)
local public = require(script.Parent.Parent.public)
local entity_index_try_get = jecs.entity_index_try_get
local IS_PAIR = jecs.IS_PAIR
local pair_first = jecs.pair_first
local pair_second = jecs.pair_second
local empty_table = {}
local function convert_component(world, debug, entity): string
if IS_PAIR(entity) then
local left = convert_component(world, debug, pair_first(world, entity))
local right = convert_component(world, debug, pair_second(world, entity))
return `({left}, {right})`
else
return world:get(entity, debug) or `${tostring(entity)}`
end
end
local function get_type(t: { [any]: any }): string
local key, value = next(t)
if key == nil then return "" end
return `[{typeof(key)}]: {typeof(value)}`
end
local function get_string_keys(t: { [any]: any }): ({ string }, boolean)
local keys = {}
local i = 0
for key in t do
if i > 3 then return keys, true end
if typeof(key) ~= "string" then continue end
table.insert(keys, key)
i += 1
end
return keys, false
end
local function is_tag(world: jecs.World, id: jecs.Entity<any>)
return jecs.is_tag(world, id)
end
local function get_all_components(world, entity): {}
local record = entity_index_try_get(world.entity_index, entity)
if not record then return empty_table end
local archetype = record.archetype
if not archetype then return empty_table end
local components = {}
for _, ty in archetype.types do
table.insert(components, ty)
end
table.sort(components, function(a, b)
return if is_tag(world, a) and is_tag(world, b) then a < b
elseif is_tag(world, a) then true
elseif is_tag(world, b) then false
else a < b
end)
return components
end
local function obtain_string(entity: jecs.Entity<any>, world: jecs.World)
local MAX_SIZE = 840
local has_more = false
local entity_name = world:get(entity, jecs.Name)
local strings = {`<b>{if entity_name then `{entity_name} #` else "#"}{entity}</b>\n`}
local n = #strings[1]
for _, id in get_all_components(world, entity) do
if id == jecs.Name then continue end
local name = convert_component(world, jecs.Name, id)
local value = if is_tag(world, id) then nil else world:get(entity, id)
local to_append
if typeof(value) == "table" then
local string_keys = get_string_keys(value)
if #string_keys > 0 then
local temp_b = {`<b>{name}</b>:`}
local temp_n = #temp_b[1]
for key, value in pairs(value) do
if #temp_b > 0 then
table.insert(temp_b, "\n")
end
local str_of_v = if type(value) == "string" then `{value}`
elseif typeof(value) == "table" then get_type(value)
else tostring(value)
if #str_of_v > 32 then
str_of_v = `{string.sub(str_of_v, 1, 30)}..`
end
local str = `{key}: {str_of_v}`
if temp_n + #str + 2 > 32 then
table.insert(temp_b, "...")
break
else
table.insert(temp_b, str)
end
end
to_append = `{table.concat(temp_b)}`
else
to_append = `<b>{name}</b>: {get_type(value)}`
end
elseif is_tag(world, id) then
to_append = `<b>{name}</b>`
else
local value = tostring(value)
if #value > 32 then
to_append = `<b>{name}</b>: {string.sub(value, 1, 30)}..`
else
to_append = `<b>{name}</b>: {value}`
end
end
if MAX_SIZE > n + #to_append then
n += #to_append
table.insert(strings, to_append)
else
has_more = true
end
end
local str = table.concat(strings, "\n")
if has_more then str = str .. "..." end
return str
end
return function()
local send_mouse_pointer = queue(remotes.send_mouse_pointer)
return function()
for incoming, world_id, pos, dir in send_mouse_pointer:iter() do
if not traffic_check.check_no_wl(incoming.host) then continue end
local world_data: types.World = public[world_id]
local world = world_data.world
local outgoing = reverse_connector(incoming)
if world_data.entities == nil and world_data.get_entity_from_part == nil then continue end
if not world_data or world_data.class_name ~= "World" then continue end
local result = workspace:Raycast(pos, dir * 1000)
if not result then
remotes.send_mouse_entity:fire(
outgoing,
world_id
)
continue
end
local part = result.Instance
local entity
-- no way to obtain the entity
if world_data.get_entity_from_part == nil and world_data.entities == nil then
remotes.send_mouse_entity:fire(
outgoing,
world_id
)
continue
end
if world_data.get_entity_from_part == nil then
entity = world_data.entities[part]
while entity == nil and part.Parent ~= game do
part, entity = part.Parent, world_data.entities[part]
end
else
entity, part = world_data.get_entity_from_part(part)
end
if not entity then
remotes.send_mouse_entity:fire(
outgoing,
world_id
)
continue
end
local str = obtain_string(entity, world, jecs.Name)
remotes.send_mouse_entity:fire(
outgoing,
world_id,
part,
entity,
str
)
end
end
end

View file

@ -0,0 +1,28 @@
local Players = game:GetService("Players")
local net = require(script.Parent.Parent.Parent.modules.net)
local queue = require(script.Parent.Parent.Parent.modules.queue)
local remotes = require(script.Parent.Parent.Parent.modules.remotes)
local reverse_connector = require(script.Parent.Parent.Parent.modules.reverse_connector)
local traffic_check = require(script.Parent.Parent.Parent.modules.traffic_check)
return function()
local ping = queue(remotes.ping)
for _, player in Players:GetPlayers() do
if traffic_check.communication_is_allowed(net.local_host, player, true) then
remotes.new_server_registered:fire({
host = player,
})
end
end
return function()
for connector in ping:iter() do
local outgoing = reverse_connector(connector)
remotes.new_server_registered:fire(outgoing)
end
end
end

View file

@ -0,0 +1,80 @@
local queue = require(script.Parent.Parent.Parent.modules.queue)
local remotes = require(script.Parent.Parent.Parent.modules.remotes)
local reverse_connector = require(script.Parent.Parent.Parent.modules.reverse_connector)
local traffic_check = require(script.Parent.Parent.Parent.modules.traffic_check)
local public = require(script.Parent.Parent.public)
return function()
local connected = {}
local bind_to_core = queue(remotes.bind_to_server_core)
return function()
for connector in bind_to_core:iter() do
local outgoing = reverse_connector(connector)
if not traffic_check.check_no_wl(connector.host) then continue end
-- print("help")
table.insert(connected, outgoing)
local schedulers = {}
local worlds = {}
for idx, data in ipairs(public) do
if data.class_name == "Scheduler" then
table.insert(schedulers, {
name = data.name :: string,
id = idx
})
elseif data.class_name == "World" then
table.insert(worlds, {
name = data.name :: string,
id = idx
})
end
end
remotes.update_server_data:fire(outgoing, {
schedulers = schedulers,
worlds = worlds
})
end
if public.updated == false then return end
public.updated = false
local schedulers = {}
local worlds = {}
for idx, data in ipairs(public) do
if data.class_name == "Scheduler" then
table.insert(schedulers, {
name = data.name :: string,
id = idx
})
elseif data.class_name == "World" then
table.insert(worlds, {
name = data.name :: string,
id = idx
})
end
end
local fired_to = {}
for _, connector in connected do
if fired_to[connector] then return end
fired_to[connector] = true
remotes.update_server_data:fire(connector, {
schedulers = schedulers,
worlds = worlds
})
end
end
end

View file

@ -0,0 +1,503 @@
local jecs = require(script.Parent.Parent.Parent.jecs)
local queue = require(script.Parent.Parent.Parent.modules.queue)
local remotes = require(script.Parent.Parent.Parent.modules.remotes)
local reverse_connector = require(script.Parent.Parent.Parent.modules.reverse_connector)
local traffic_check = require(script.Parent.Parent.Parent.modules.traffic_check)
local types = require(script.Parent.Parent.Parent.modules.types)
local public = require(script.Parent.Parent.public)
local query_parser = require(script.Parent.Parent.query_parser)
type Connection = {
outgoing: types.OutgoingConnector,
query_id: number,
frame: number,
paused: boolean,
refresh: boolean,
world: types.World,
include: {jecs.Entity<any>},
exclude: {jecs.Entity<any>},
with: {jecs.Entity<any>},
new_columns: {{any}},
old_columns: {{any}},
from: number,
upto: number
}
local NIL = newproxy() -- NULL is displayed if the value exists, buth as no value
local function clear_columns(columns: {{any}})
for _, column in columns do
local name = column[1]
table.clear(column)
column[1] = name
assert(column[1] == name)
end
return columns
end
local function reverse_columns(columns: {{any}}, size: number)
for _, column in columns do
for i = 0, size // 2 - 1 do
column[i + 2], column[(size + 1) - i] = column[(size + 1) - i], column[i + 2]
end
end
return columns
end
return function()
local processing_queries: {[number]: Connection} = {}
local validate_query = queue(remotes.validate_query)
local request_query = queue(remotes.request_query)
local disconnect_query = queue(remotes.disconnect_query)
local advance_query_page = queue(remotes.advance_query_page)
local pause_query = queue(remotes.pause_query)
local refresh_query = queue(remotes.refresh_results)
local function check_if_query_valid(world: types.World, query: string): (boolean, string)
local map_components = {}
local ok, result = pcall(query_parser, query)
local msg = nil
if not ok then
return ok, result :: any
end
for id, name in world.world:query(jecs.Name):iter() do
map_components[name] = id
end
local total_to_query = 0
for _, ctype in result do
if not ok then break end
if ctype.query and not ctype.exclude then
total_to_query += 1
end
if ctype.type == "Component" then
if ctype.value.type == "Entity" then
if world.world:contains(ctype.value.entity) then continue end
return false, "entity does not exist"
elseif ctype.value.type == "Name" then
if map_components[ctype.value.name] then continue end
return false, `unknown component called {ctype.value.name}`
else
return false, "what"
end
elseif ctype.type == "Relationship" then
local both_wildcard = ctype.left.type == "Wildcard" and ctype.right.type == "Wildcard"
if both_wildcard then
return false, `(*, *) is not a valid relationship`
end
local left = ctype.left
local right = ctype.right
if left.type == "Component" then
if left.value.type == "Entity" then
if world.world:contains(left.value.entity) then continue end
return false, "entity does not exist"
elseif left.value.type == "Name" then
if map_components[left.value.name] then continue end
return false, `unknown component called {left.value.name}`
else
return false, "what"
end
end
if right.type == "Component" then
if right.value.type == "Entity" then
if world.world:contains(right.value.entity) then continue end
return false, "entity does not exist"
elseif right.value.type == "Name" then
if map_components[right.value.name] then continue end
return false, `unknown component called {right.value.name}`
else
return false, "what"
end
end
end
end
if total_to_query > 26 then
warn("attempting to observe too many values")
return false, "attempting to observe too many entities"
end
return ok, msg
end
--fixme: contains is missing from types
local function check_if_still_valid(world: any, entities: {any})
for _, id in entities do
if jecs.IS_PAIR(id) then
if not (world:contains(jecs.pair_first(world, id) and jecs.pair_second(world, id))) then
return false
end
elseif not world:contains(id) then
return false
end
end
return true
end
local function get_terms(query: string, world: jecs.World)
local result = query_parser(query)
local include = {}
local exclude = {}
local with = {}
local map_components = {}
local map_entity: {[any]: any} = {}
for id, name in world:query(jecs.Name):iter() do
map_components[name] = id
end
local function get_entity(ctype: query_parser.PureComponent)
local value = ctype.value
if value.type == "Entity" then
return value.entity
elseif value.type == "Name" then
return map_components[value.name]
end
error("bad")
end
for _, ctype in result do
if ctype.type == "Component" then
map_entity[ctype] = get_entity(ctype)
elseif ctype.type == "Relationship" then
local left, right = jecs.Wildcard, jecs.Wildcard
if ctype.left.type == "Component" then
left = get_entity(ctype.left)
end
if ctype.right.type == "Component" then
right = get_entity(ctype.right)
end
local pair = jecs.pair(left, right)
map_entity[ctype] = pair
end
end
for _, ctype in result do
local entity = map_entity[ctype]
if ctype.exclude then
table.insert(exclude, entity)
elseif ctype.query then
-- local name = if ctype.type == "Component" then ctype.name else `({ctype.left.name}, {ctype.right.name})`
table.insert(include, entity)
else
table.insert(with, entity)
end
end
return include, exclude, with
end
return function()
for incoming, world_id, query in validate_query:iter() do
if not traffic_check.check_no_wl(incoming.host) then continue end
local world: types.World = public[world_id]
local outgoing = reverse_connector(incoming)
if not world or world.class_name ~= "World" then
remotes.validate_result:fire(outgoing, world_id, query, nil, false, "world does not exist")
continue
end
local ok, message = check_if_query_valid(world, query)
local include, exclude, with
if ok then include, exclude, with = get_terms(query, world.world) end
remotes.validate_result:fire(outgoing, world_id, query, ok and {
include = include,
exclude = exclude,
with = with
}, ok, message)
end
for incoming, query_id in disconnect_query:iter() do
processing_queries[query_id] = nil
end
for incoming, world_id, query_id, query in request_query:iter() do
if not traffic_check.check_no_wl(incoming.host) then continue end
local world: types.World = public[world_id]
local outgoing = reverse_connector(incoming)
if not world or world.class_name ~= "World" then continue end
local ok = check_if_query_valid(world, query)
if not ok then continue end
local include, exclude, with = get_terms(query, world.world)
local new_columns = {}
local old_columns = {}
table.insert(new_columns, {})
table.insert(old_columns, {})
for _, ctype in include do
table.insert(new_columns, {})
table.insert(old_columns, {})
end
if processing_queries[query_id] then
local connection = processing_queries[query_id]
connection.outgoing = outgoing
connection.query_id = query_id
connection.world = world
connection.refresh = true
connection.include = include
connection.exclude = exclude
connection.with = with
connection.new_columns = new_columns
connection.old_columns = old_columns
connection.from = 1
connection.upto = 25
else
local connection: Connection = {
outgoing = outgoing,
query_id = query_id,
frame = 0,
world = world,
paused = false,
refresh = false,
include = include,
exclude = exclude,
with = with,
new_columns = new_columns,
old_columns = old_columns,
from = 1,
upto = 25
}
processing_queries[query_id] = connection
end
end
for incoming, query_id in refresh_query:iter() do
if not traffic_check.check_no_wl(incoming.host) then continue end
local query = processing_queries[query_id]
if not query then continue end
query.refresh = true
end
for incoming, query_id, state in pause_query:iter() do
if not traffic_check.check_no_wl(incoming.host) then continue end
local query = processing_queries[query_id]
if not query then continue end
query.paused = state
end
for incoming, query_id, from, to in advance_query_page:iter() do
if not traffic_check.check_no_wl(incoming.host) then continue end
local query = processing_queries[query_id]
if not query then continue end
query.refresh = true
query.from = from
query.upto = to
end
for query_id, query_data in processing_queries do
if query_data.paused and query_data.refresh ~= true then continue end
debug.profilebegin("process query")
query_data.refresh = false
local world_data = query_data.world
local world = world_data.world
local debug_trait = jecs.Name
if
not (check_if_still_valid(world, query_data.include)
and check_if_still_valid(world, query_data.exclude)
and check_if_still_valid(world, query_data.with))
then
-- query is no longer valid!
--todo: query is invalid, notify the client about this
debug.profileend()
continue
end
local query = world:query(unpack(query_data.include))
if #query_data.exclude > 0 then
query = query:without(unpack(query_data.exclude))
end
if #query_data.with > 0 then
query = query:with(unpack(query_data.with))
end
local from = query_data.from
local upto = query_data.upto
local new_columns = query_data.new_columns
local old_columns = query_data.old_columns
-- set the names of each column
--todo: fix type
local function get_name(entity: any)
if jecs.IS_PAIR(entity) then
local left = jecs.pair_first(world, entity)
local right = jecs.pair_second(world, entity)
return `({get_name(left)}, {get_name(right)})`
elseif entity == jecs.Wildcard :: any then
return "*"
elseif world:has(entity, debug_trait) then
return world:get(entity, debug_trait)
else
return `${entity}`
end
end
-- set column names
for index, column in new_columns do
local e = query_data.include[index - 1]
if e then
column[1] = get_name(e)
else
column[1] = "id"
end
end
-- process the data into columns
-- we inline the query here, as jecs queries are in reverse to prevent iterator invalidation
-- this is usually fine, but it's annoying, as now entities are added to the first page.
--todo: pause button
local total_entities = 0
local archetypes = query:archetypes()
for _, archetype: jecs.Archetype in archetypes do
total_entities += #archetype.entities
end
local entities = table.create(total_entities)
local at = total_entities
local row_entity = 1
for _, archetype: jecs.Archetype in archetypes do
for row = #archetype.entities, 1, -1 do
local entity = archetype.entities[row]
table.insert(entities, entity)
end
end
table.sort(entities)
for i = from, upto do
row_entity += 1
local entity = entities[i]
if not entity then continue end
new_columns[1][row_entity] = entity
for idx, ctype in query_data.include do
local value = world:get(entity, ctype)
new_columns[idx + 1][row_entity] = if value == nil then NIL else value
end
end
--- reverse the order of each array
remotes.count_total_entities:fire(
query_data.outgoing,
query_id,
total_entities
)
-- diff the columns and replicate any new values
for column = 1, math.max(#new_columns, #old_columns) do
for row = 1, upto do
local new_value = new_columns[column][row]
local old_value = old_columns[column][row]
if new_value ~= old_value or typeof(new_value) == "table" then
-- todo: improve replication of the new value
-- ideally, we would figure out if the value is a certain type and needs special replication
-- if we for example determine a value is a string, or table, we cap it at MAX_CHARACTERS
-- or we tostring a couple keys of the table until we reach MAX_CHARACTERS.
-- we wanna be able to replicate every single. value
local MAX_CHARS = 750
local str
if typeof(new_value) == "string" then
str = `"{string.sub(new_value, 1, MAX_CHARS-2)}"`
elseif typeof(new_value) == "table" then
local temp_n = 0
local temp_b = {}
for key, value in new_value do
if #temp_b > 0 then
table.insert(temp_b, "; ")
end
local str_of_v = if type(value) == "string" then `"{value}"` else tostring(value)
local str = `{key}: {str_of_v}`
if temp_n + #str + 2 > MAX_CHARS then
table.insert(temp_b, "...")
break
else
table.insert(temp_b, str)
end
end
str = `\{{table.concat(temp_b)}\}`
elseif new_value == NIL then
str = "" -- important distinction, this is still a valid component
elseif new_value == nil then
str = nil -- but this isnt
else
str = string.sub(tostring(new_value), 1, MAX_CHARS-2)
end
if row == 1 then str = new_value end
remotes.update_query_result:fire(
query_data.outgoing,
query_id,
query_data.frame,
column,
row,
str
)
end
end
end
query_data.new_columns = clear_columns(old_columns)
query_data.old_columns = new_columns
query_data.frame += 1
debug.profileend()
end
end
end

View file

@ -0,0 +1,104 @@
--!nolint LocalShadow
local hash = require(script.Parent.Parent.Parent.modules.hash_connector)
local queue = require(script.Parent.Parent.Parent.modules.queue)
local remotes = require(script.Parent.Parent.Parent.modules.remotes)
local reverse_connector = require(script.Parent.Parent.Parent.modules.reverse_connector)
local traffic_check = require(script.Parent.Parent.Parent.modules.traffic_check)
local types = require(script.Parent.Parent.Parent.modules.types)
local public = require(script.Parent.Parent.public)
return function()
local connected = {}
local request_scheduler = queue(remotes.request_scheduler)
local disconnect_scheduler = queue(remotes.disconnect_scheduler)
local schedule_pause = queue(remotes.scheduler_system_pause)
return function()
for incoming, id in request_scheduler:iter() do
if not traffic_check.check_no_wl(incoming.host) then continue end
local scheduler = public[id]
if scheduler.class_name ~= "Scheduler" then continue end
if scheduler == nil then continue end
local outgoing = reverse_connector(incoming)
connected[id] = connected[id] or {}
table.insert(connected[id], outgoing)
-- print("connected")
for system_id, data in scheduler.system_data do
remotes.scheduler_system_static_update:fire(outgoing, id, system_id, data)
end
for system_id, frames in scheduler.system_frames do
local frame = frames[1]
if not frame then continue end
remotes.scheduler_system_update:fire(outgoing, id, system_id, frame.i, frame.s)
end
end
for incoming, id in disconnect_scheduler:iter() do
if not traffic_check.check_no_wl(incoming.host) then continue end
if not connected[id] then continue end
local scheduler_connected = connected[id]
for i = #scheduler_connected, 1, -1 do
local connector = scheduler_connected[i]
if connector.host ~= incoming.host then continue end
if connector.to_vm ~= incoming.from_vm then continue end
scheduler_connected[i] = scheduler_connected[#scheduler_connected]
scheduler_connected[#scheduler_connected] = nil
break
end
end
for incoming, id, system, paused in schedule_pause:iter() do
if not traffic_check.check_no_wl(incoming.host) then continue end
local scheduler: types.Scheduler = public[id]
if not scheduler then return end
scheduler:set_system_data(system, {
paused = paused
})
end
for id, connected in connected do
local scheduler: types.Scheduler = public[id]
if #connected == 0 then continue end
for system_id in scheduler.system_data_updated do
local map = {}
local data = scheduler.system_data[system_id]
for _, connector in connected do
if map[hash(connector)] then continue end
map[hash(connector)] = true
remotes.scheduler_system_static_update:fire(connector, id, system_id, data)
end
scheduler.system_data_updated[system_id] = nil
end
for system_id, frames in scheduler.system_frames_updated do
local map = {}
for frame in frames do
for _, connector in connected do
if map[hash(connector)] then continue end
map[hash(connector)] = true
remotes.scheduler_system_update:fire(connector, id, system_id, frame.i, frame.s)
end
end
table.clear(frames)
end
end
end
end

View file

@ -0,0 +1,175 @@
--[[
Handles the API for the system watch.
Users will be able to add watches to their systems to track changes.
Users will be able to learn about what actions a system performs on a jecs world
through this.
Hooked API's:
component()
entity()
remove()
clear()
delete()
add()
set()
]]
local jecs = require(script.Parent.Parent.Parent.jecs)
local hash_connector = require(script.Parent.Parent.Parent.modules.hash_connector)
local lon = require(script.Parent.Parent.Parent.modules.lon)
local queue = require(script.Parent.Parent.Parent.modules.queue)
local remotes = require(script.Parent.Parent.Parent.modules.remotes)
local reverse_connector = require(script.Parent.Parent.Parent.modules.reverse_connector)
local types = require(script.Parent.Parent.Parent.modules.types)
local public = require(script.Parent.Parent.public)
local watch = require(script.Parent.Parent.watch)
local NIL = watch.NIL
return function()
local stored_watches = {}
local connected_watches = {}
local function create_watch_for_id(
scheduler: types.Scheduler,
system: types.SystemId,
watch_id: number
)
local watch = scheduler:create_watch_for_system(system)
stored_watches[watch_id] = watch
end
local function send_watch_data_to(host: types.OutgoingConnector, watch_id: number, frame: number)
local map_worlds_to_name = {}
local watch = stored_watches[watch_id]
local frames = watch.watch.frames
local data = frames[frame]
if not data then
remotes.update_watch_data:fire(host, watch_id, frame, nil)
return
end
for _, world in ipairs(public) do
if world.world == nil then continue end
map_worlds_to_name[world.world] = jecs.Name
end
local to_send = {
types = data.types,
entities = data.entities,
component = table.clone(data.component),
values = table.clone(data.values)
}
for idx, ctype in to_send.component do
local world = data.worlds[idx]
to_send.component[idx] = world:get(ctype, map_worlds_to_name[world]) or ctype
end
for idx, value in to_send.values do
if value == NIL then to_send.values[idx] = "" end
to_send.values[idx] = lon.output(value, false)
end
remotes.update_watch_data:fire(host, watch_id, frame, to_send :: any)
end
local function remove_watch_id(watch_id: number)
if not stored_watches[watch_id] then return end
stored_watches[watch_id].untrack()
stored_watches[watch_id] = nil
connected_watches[watch_id] = nil
end
local function start_record_watch(watch_id: number)
local watch = stored_watches[watch_id]
watch.active = true
end
local function stop_record_watch(watch_id: number)
local watch = stored_watches[watch_id]
watch.active = false
end
local function connect_watch(host: types.OutgoingConnector, watch_id: number)
connected_watches[watch_id] = connected_watches[watch_id] or {}
connected_watches[watch_id][hash_connector(host)] = host
local watch = stored_watches[watch_id]
for i, frame in watch.watch.frames do
remotes.update_overview:fire(host, watch_id, i, #frame.types)
end
end
local function disconnect_watch(host: types.OutgoingConnector, watch_id: number)
if not connected_watches[watch_id] then return end
connected_watches[watch_id][hash_connector(host)] = nil
end
local request_create_watch = queue(remotes.create_watch)
local request_remove_watch = queue(remotes.remove_watch)
local request_watch_data = queue(remotes.request_watch_data)
local request_stop_watch = queue(remotes.stop_watch)
local request_record_watch = queue(remotes.start_record_watch)
local request_connect_watch = queue(remotes.connect_watch)
local request_disconnect_watch = queue(remotes.disconnect_watch)
-- local set_lon_enabled = queue(remotes.set_lon_enabled)
return function()
for from, scheduler_id, system, watch_id in request_create_watch:iter() do
local scheduler = public[scheduler_id]
if scheduler.class_name ~= "Scheduler" then continue end
if scheduler == nil then continue end
create_watch_for_id(scheduler, system, watch_id)
end
for from, watch_id in request_stop_watch:iter() do
stop_record_watch(watch_id)
end
for from, watch_id in request_remove_watch:iter() do
remove_watch_id(watch_id)
end
for from, watch_id, frame in request_watch_data:iter() do
send_watch_data_to(
reverse_connector(from),
watch_id,
frame
)
end
for from, watch_id in request_record_watch:iter() do
start_record_watch(watch_id)
end
for from, watch_id in request_connect_watch:iter() do
connect_watch(reverse_connector(from), watch_id)
end
for from, watch_id in request_disconnect_watch:iter() do
disconnect_watch(from, watch_id)
end
for watch_id, hosts in connected_watches do
local watch = stored_watches[watch_id]
local current_frame = watch.watch.frame
local frame_data = watch.watch.frames[current_frame] or {types = {}}
local changes = #frame_data.types
for _, host in hosts do
remotes.update_overview:fire(host, watch_id, current_frame, changes)
end
end
end
end

View file

@ -0,0 +1,129 @@
local types = require(script.Parent.Parent.modules.types)
local world_hook = require(script.Parent.world_hook)
local NIL = newproxy()
type ChangeTypes = "remove" | "clear" | "delete" | "add" | "set" | "entity" | "component"
type Changes = types.WatchLoggedChanges
export type SystemWatch = {
--- enables Lua Object Notation.
--- incurs a significant performance penalty.
enable_lon: boolean,
--- the current frame to process
frame: number,
frames: {[number]: Changes}
}
local function create_changes()
return {
types = {},
entities = {},
component = {},
values = {},
worlds = {}
}
end
local function step_watch(watch: SystemWatch)
watch.frame += 1
watch.frames[watch.frame] = create_changes()
end
local function track_watch(watch: SystemWatch)
local hooks = {
world_hook.hook_onto("remove", function(self, id, component)
local frame = watch.frames[watch.frame]
table.insert(frame.types, "remove")
table.insert(frame.entities, id)
table.insert(frame.component, component)
table.insert(frame.values, NIL)
table.insert(frame.worlds, self)
end),
world_hook.hook_onto("clear", function(self, id)
local frame = watch.frames[watch.frame]
table.insert(frame.types, "clear")
table.insert(frame.entities, id)
table.insert(frame.component, NIL)
table.insert(frame.values, NIL)
table.insert(frame.worlds, self)
end),
world_hook.hook_onto("delete", function(self, id)
local frame = watch.frames[watch.frame]
table.insert(frame.types, "delete")
table.insert(frame.entities, id)
table.insert(frame.component, NIL)
table.insert(frame.values, NIL)
table.insert(frame.worlds, self)
end),
world_hook.hook_onto("add", function(self, id, component)
local frame = watch.frames[watch.frame]
table.insert(frame.types, "add")
table.insert(frame.entities, id)
table.insert(frame.component, component)
table.insert(frame.values, NIL)
table.insert(frame.worlds, self)
end),
world_hook.hook_onto("set", function(self, entity, component, value)
if self:has(entity, component) then
local frame = watch.frames[watch.frame]
table.insert(frame.types, "change")
table.insert(frame.entities, entity)
table.insert(frame.component, component)
table.insert(frame.values, value)
table.insert(frame.worlds, self)
else
local frame = watch.frames[watch.frame]
table.insert(frame.types, "move")
table.insert(frame.entities, entity)
table.insert(frame.component, component)
table.insert(frame.values, value)
table.insert(frame.worlds, self)
end
end)
}
--- stops all hooks
local function stop_hook()
for _, destroy in hooks do
destroy()
end
end
return stop_hook
end
local function create_watch()
local watch: SystemWatch = {
enable_lon = false,
frame = 0,
frames = {}
}
return watch
end
return {
create_watch = create_watch,
track_watch = track_watch,
step_watch = step_watch,
NIL = NIL
}

View file

@ -0,0 +1,75 @@
local public = require(script.Parent.public)
local function i_hook_onto(key: string, hooks: {(...any) -> ()})
local to_unhook = {}
for _, world_data in ipairs(public) do
local world = world_data.world
if not world then continue end
local method: any = world[key]
assert(typeof(method) == "function", "can only hook onto functions")
-- create a new wrapper function
local function run_hook(...)
for _, hook in hooks do
hook(...)
end
return method(...)
end
-- print(debug.info(world[key], "s"))
world[key] = run_hook
to_unhook[world] = method
end
return function()
for world, method in to_unhook do
world[key] = method
end
end
end
local hooks = {}
local function find_swap_pop<T>(list: {T}, value: T)
local idx = table.find(list, value)
if not idx then return end
list[idx] = list[#list]
list[#list] = nil
end
local function hook_onto(key: string, hook: (...any) -> ())
if hooks[key] == nil then
local callbacks = {}
local cleanup = i_hook_onto(key, callbacks)
hooks[key] = {
cleanup = cleanup,
callbacks = callbacks
}
end
local hook_info = hooks[key]
local dead = false
table.insert(hook_info.callbacks, hook)
local function unhook()
if dead then return end
dead = true
find_swap_pop(hook_info.callbacks, hook)
if hook_info.callbacks[1] == nil then
hook_info.cleanup()
hooks[key] = nil
end
end
return unhook
end
return {
hook_onto = hook_onto
}

View file

@ -0,0 +1,121 @@
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local theme = require(script.Parent.Parent.Parent.util.theme)
local container = require(script.Parent.Parent.util.container)
local padding = require(script.Parent.Parent.util.padding)
local typography = require(script.Parent.typography)
local create = vide.create
local source = vide.source
local changed = vide.changed
local spring = vide.spring
local CHEVRON_DOWN = "rbxassetid://10709790948"
local CHEVRON_UP = "rbxassetid://10709791523"
type can<T> = T | () -> T
type props = {
text: can<string>,
expanded: () -> boolean,
set_expanded: (boolean) -> (),
[any]: any
}
return function(props: props)
local gui_state = source(Enum.GuiState.Idle)
local container_size = source(Vector2.zero)
return container {
Name = "Accordion",
Size = spring(function()
if props.expanded() == false then return UDim2.new(1, 0, 0, 32) end
return UDim2.new(1, 0, 0, 40 + container_size().Y)
end, 0.1),
ClipsDescendants = true,
create "ImageButton" {
Name = "Accordion",
AutoLocalize = false,
Size = UDim2.new(1, 0, 0, 32),
BackgroundColor3 = spring(function()
return if gui_state() == Enum.GuiState.Press then
theme.bg[-1]()
elseif gui_state() == Enum.GuiState.Hover then
theme.bg[3]()
else
theme.bg[0]()
end, 0.1),
padding {x = UDim.new(0, 8)},
create "UIListLayout" {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalFlex = Enum.UIFlexAlignment.SpaceBetween,
VerticalAlignment = Enum.VerticalAlignment.Center,
Padding = UDim.new(0, 8)
},
create "UICorner" {
CornerRadius = UDim.new(0, 8)
},
container {
Size = UDim2.fromOffset(16, 16),
create "ImageLabel" {
Size = UDim2.fromOffset(16, 16),
BackgroundTransparency = 1,
AutoLocalize = false,
Image = CHEVRON_DOWN,
Rotation = spring(function()
return if props.expanded() then 180 else 0
end, 0.1)
},
},
typography {
size = UDim2.fromScale(0, 1),
text = props.text,
truncate = Enum.TextTruncate.SplitWord,
xalignment = Enum.TextXAlignment.Left,
create "UIFlexItem" {
FlexMode = Enum.UIFlexMode.Fill
}
},
Activated = function()
props.set_expanded(not props.expanded())
end,
changed("GuiState", gui_state)
},
container {
Name = "Children",
Position = UDim2.fromOffset(0, 40),
AutomaticSize = Enum.AutomaticSize.None,
Size = function()
return UDim2.new(1, 0, 0, container_size().Y)
end,
BackgroundColor3 = theme.bg[3],
ClipsDescendants = true,
container {
Size = UDim2.fromScale(1, 0),
AutomaticSize = Enum.AutomaticSize.Y,
unpack(props),
changed("AbsoluteSize", container_size)
}
}
}
end

View file

@ -0,0 +1,46 @@
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local theme = require(script.Parent.Parent.Parent.util.theme)
local create = vide.create
local read = vide.read
type can<T> = (() -> T) | T
type Background = {
position: can<UDim2>?,
size: can<UDim2>?,
anchorpoint: can<UDim2>?,
automaticsize: can<Enum.AutomaticSize>?,
layoutorder: can<number>?,
zindex: can<number>?,
depth: can<number>?,
accent: can<boolean>?,
[number]: any
}
return function(props: Background)
return create "Frame" {
Position = props.position,
Size = props.size or UDim2.fromScale(1, 1),
AnchorPoint = props.anchorpoint,
AutomaticSize = props.automaticsize,
AutoLocalize = false,
LayoutOrder = props.layoutorder,
ZIndex = props.zindex,
BackgroundColor3 = function()
return
if read(props.accent) then theme.acc[read(props.depth) or 0]()
else theme.bg[read(props.depth) or 0]()
end,
unpack(props)
}
end

View file

@ -0,0 +1,64 @@
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local anim = require(script.Parent.Parent.Parent.util.anim)
local theme = require(script.Parent.Parent.Parent.util.theme)
local create = vide.create
local read = vide.read
type can<T> = (() -> T) | T
type Background = {
position: can<UDim2>?,
size: can<UDim2>?,
anchorpoint: can<UDim2>?,
automaticsize: can<Enum.AutomaticSize>?,
layoutorder: can<number>?,
zindex: can<number>?,
checked: can<boolean>,
[number]: any
}
return function(props: Background)
return create "Frame" {
Position = props.position,
Size = props.size or UDim2.fromOffset(24, 24),
AnchorPoint = props.anchorpoint or Vector2.new(0.5, 0.5),
AutomaticSize = props.automaticsize,
AutoLocalize = false,
LayoutOrder = props.layoutorder,
ZIndex = props.zindex,
BackgroundColor3 = anim(function()
return if read(props.checked) then theme.acc[3]() else theme.bg[1]()
end),
create "UIStroke" {
Color = function()
return if read(props.checked) then theme.acc[0]() else theme.bg[-3]()
end
},
create "UICorner" {
CornerRadius = UDim.new(0, 4)
},
create "ImageLabel" {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
Image = "rbxassetid://100188624502987",
ImageTransparency = anim(function()
return if read(props.checked) then 0 else 1
end)
},
unpack(props)
}
end

View file

@ -0,0 +1,23 @@
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local theme = require(script.Parent.Parent.Parent.util.theme)
local create = vide.create
local read = vide.read
type can<T> = T | () -> T
type props = {
thickness: can<number>?,
position: can<UDim2>?,
}
return function(props: props)
return create "Frame" {
BackgroundColor3 = theme.bg[-2],
Position = props.position,
AutoLocalize = false,
Size = function()
return UDim2.new(1, 0, 0, read(props.thickness) or 1)
end,
}
end

View file

@ -0,0 +1,118 @@
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local anim = require(script.Parent.Parent.Parent.util.anim)
local theme = require(script.Parent.Parent.Parent.util.theme)
local button = require(script.Parent.Parent.interactable.button)
local container = require(script.Parent.Parent.util.container)
local list = require(script.Parent.Parent.util.list)
local padding = require(script.Parent.Parent.util.padding)
local rounded_frame = require(script.Parent.Parent.util.rounded_frame)
local divider = require(script.Parent.divider)
local typography = require(script.Parent.typography)
local create = vide.create
local source = vide.source
local changed = vide.changed
local indexes = vide.indexes
local untrack = vide.untrack
local cleanup = vide.cleanup
type can<T> = T | () -> T
type props = {
labels: () -> {
{
title: string,
ui: () -> Instance | {Instance}
}
}
}
return function(props: props)
local selected = source(1)
return list {
justifycontent = Enum.UIFlexAlignment.Fill,
spacing = UDim.new(),
create "Frame" {
Size = UDim2.new(1, 0, 0, 32),
AutoLocalize = false,
BackgroundColor3 = theme.bg[3],
divider {
position = UDim2.fromScale(0, 1),
},
container {
create "UIListLayout" {
FillDirection = Enum.FillDirection.Horizontal
},
indexes(props.labels, function(value, key)
local guistate = source(Enum.GuiState.Idle)
return rounded_frame {
name = key,
size = UDim2.fromOffset(50, 30),
automaticsize = Enum.AutomaticSize.X,
topleft = UDim.new(0, 4),
topright = UDim.new(0, 4),
color = function()
return if selected() == key then theme.bg[0]()
elseif guistate() == Enum.GuiState.Idle then theme.bg[3]()
else theme.bg[1]()
end,
create "TextButton" {
Size = UDim2.fromScale(1, 1),
AutoLocalize = false,
BackgroundTransparency = 1,
Activated = function()
selected(key)
end,
typography {
position = UDim2.fromScale(0.5, 0.5),
anchorpoint = Vector2.new(0.5, 0.5),
text = function()
return value().title
end,
textsize = 16
},
padding {
x = UDim.new(0, 24),
y = UDim.new(0, 2)
},
changed("GuiState", guistate)
}
}
end),
},
ZIndex = 100,
},
create "Frame" {
Size = UDim2.new(1, 0, 1, 0),
AutoLocalize = false,
BackgroundColor3 = theme.bg[0],
function()
return untrack(props.labels()[selected()].ui)
end
},
}
end

View file

@ -0,0 +1,70 @@
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local theme = require(script.Parent.Parent.Parent.util.theme)
local list = require(script.Parent.Parent.util.list)
local padding = require(script.Parent.Parent.util.padding)
local background = require(script.Parent.background)
local typography = require(script.Parent.typography)
local create = vide.create
local read = vide.read
local show = vide.show
type can<T> = T | () -> T
type props = {
size: can<UDim2>?,
position: can<UDim2>?,
anchorpoint: can<UDim2>?,
layoutorder: can<number>?,
automaticsize: can<Enum.AutomaticSize>?,
name: can<string>?,
[number]: any
}
return function(props: props)
return create "Frame" {
Name = props.name,
Size = props.size or UDim2.fromScale(1, 0),
Position = props.position,
AnchorPoint = props.anchorpoint,
LayoutOrder = props.layoutorder,
AutomaticSize = props.automaticsize or Enum.AutomaticSize.Y,
BackgroundColor3 = theme.bg[0],
AutoLocalize = false,
create "UICorner" {
CornerRadius = UDim.new(0, 8)
},
create "UIStroke" {
Color = theme.bg[-3]
},
show(function()
return if read(props.name) then #read(props.name) > 0 else false
end, function()
return background {
size = UDim2.new(),
position = UDim2.fromOffset(4, -16),
automaticsize = Enum.AutomaticSize.XY,
typography {
text = props.name,
disabled = true,
textsize = 14
},
padding {x = UDim.new(0, 2), y = UDim.new()}
}
end),
padding {},
list {
unpack(props)
}
}
end

View file

@ -0,0 +1,207 @@
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local theme = require(script.Parent.Parent.Parent.util.theme)
local padding = require(script.Parent.Parent.util.padding)
local rounded_frame = require(script.Parent.Parent.util.rounded_frame)
local typography = require(script.Parent.typography)
local create = vide.create
local source = vide.source
local derive = vide.derive
local effect = vide.effect
local cleanup = vide.cleanup
local indexes = vide.indexes
local changed = vide.changed
local untrack = vide.untrack
local MAX_PIXELS_OFFSET = 32
local BEFORE = source(0)
local AFTER = source(1)
type ResizeableBar = {
meaning: () -> { string },
min_sizes: (() -> { vide.source<number>? })?,
sizes: vide.source<{ vide.source<number> }>,
suggested_sizes: { number }?,
splits: (vide.source<{ vide.source<number> }>)?,
base_splits: { number }?
}
return function(props: ResizeableBar)
local meaning = props.meaning
local sizes = props.sizes
local min_sizes = props.min_sizes or source({}) :: never
local suggested_sizes = props.suggested_sizes or {}
local absolute_size = source(Vector2.one)
local absolute_position = source(Vector2.one)
local total = derive(function()
return #meaning()
end)
local splits = props.splits or source {}
local total_columns = derive(function()
return #props.meaning()
end)
effect(function(previous)
local new = {}
for i = 1, total_columns() - 1 do
local old_split = vide.read(previous and previous[i] or nil)
new[i] = source(math.min(if old_split and old_split ~= 1 then old_split else suggested_sizes[i] or 1, i / total_columns()))
end
splits(new)
return new
end)
for i, split in (props.base_splits :: never) or {} do
splits()[i](split)
end
local function get_size(index: number)
local split_before = splits()[index - 1] or BEFORE :: never
local split_after = splits()[index] or AFTER :: never
local size = split_after() - split_before()
return size
end
local function get_min_size(i: number)
local min_size = min_sizes()[i]
return min_size and min_size() or 0.025
end
effect(function()
local new = setmetatable({}, {
__index = function()
return function() return 0 end
end,
})
for i = 1, total() do
min_sizes()[i] = min_sizes()[i] or source(0.025)
untrack(function()
new[i] = derive(function()
return get_size(i)
end)
end)
end
sizes(new :: any)
end)
local down = false
local updating = 0
return rounded_frame {
size = function()
return UDim2.new(1, 0, 0, 32)
end,
topleft = UDim.new(0, 8),
topright = UDim.new(0, 8),
color = theme.bg[1],
create "TextButton" {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
AutoLocalize = false,
Text = "",
create "UIListLayout" {
FillDirection = Enum.FillDirection.Horizontal,
Padding = UDim.new(0, 0)
},
indexes(meaning, function(column, i)
return typography {
size = function()
return UDim2.fromScale(get_size(i), 1)
end,
automaticsize = Enum.AutomaticSize.None,
text = function()
return column() or ""
end,
xalignment = Enum.TextXAlignment.Left,
truncate = Enum.TextTruncate.AtEnd,
header = true,
textsize = 18,
padding {x = UDim.new(0, 8)}
}
end),
changed("AbsoluteSize", absolute_size),
changed("AbsolutePosition", absolute_position),
MouseButton1Down = function(x: number)
-- find the nearest split
x -= absolute_position().X
local absolute_size = absolute_size()
local nearest = -1
for i, location in splits() do
local absolute_x = absolute_size.X * location()
if math.abs(x - absolute_x) > MAX_PIXELS_OFFSET then continue end
nearest = i
end
down = nearest ~= -1
updating = nearest
end,
MouseButton1Up = function()
down = false
end,
cleanup(RunService.Heartbeat:Connect(function()
local x = UserInputService:GetMouseLocation().X
x -= absolute_position().X
if down == false then return end
down = UserInputService:IsMouseButtonPressed(Enum.UserInputType.MouseButton1) == true
local relative = x / absolute_size().X
local current = splits()[updating]()
local left_to_move = relative - current
if left_to_move > 0 then
for i = updating, total() - 1, 1 do
local min_size = get_min_size(i + 1)
local size = get_size(i + 1)
local new_size = math.max(size - left_to_move, min_size)
local difference = size - new_size
splits()[i](splits()[i]() + difference)
left_to_move -= difference
if left_to_move == 0 then break end
end
else
for i = updating, 1, -1 do
local min_size = get_min_size(i)
local size = math.max(get_size(i), min_size) -- this is changing, which it isnt supposed to do
local new_size = math.max(size + left_to_move, min_size)
local difference = new_size - size
splits()[i](splits()[i]() + difference)
--assert((new_size + difference) == get_size(i - 1))
left_to_move -= difference
if left_to_move == 0 then break end
end
end
end)),
}
}
end

View file

@ -0,0 +1,22 @@
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local theme = require(script.Parent.Parent.Parent.util.theme)
local create = vide.create
type props = vide.vScrollingFrame
return function(props: props)
return create "ScrollingFrame" {
AutoLocalize = false,
ScrollBarThickness = 6,
ScrollBarImageColor3 = theme.fg_on_bg_low[0],
CanvasSize = UDim2.new(),
BackgroundTransparency = 1,
props
}
end

View file

@ -0,0 +1,123 @@
local UserInputService = game:GetService("UserInputService")
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local create = vide.create
local source = vide.source
local cleanup = vide.cleanup
local effect = vide.effect
local action = vide.action
local changed = vide.changed
local untrack = vide.untrack
local read = vide.read
type can<T> = T | () -> T
type snap_area = {
zindex: can<number>?,
snapped: (boolean) -> ()
}
type snappable = {
--- tells the object that you are dragging it
dragging: () -> boolean,
--- allows making the widget float by itself without being anchored to anything
allow_floating: can<boolean>,
--- callbacks that update the position and size
snapped: (boolean) -> (),
position: (UDim2) -> (),
size: (UDim2) -> ()
}
local function in_bounds(mpos: Vector2, pos: Vector2, size: Vector2)
return mpos.X >= pos.X and mpos.X <= pos.X + size.X and mpos.Y >= pos.Y and mpos.Y <= pos.Y + size.Y
end
return function()
local snap_areas = {}
local mouse_position = source(Vector2.zero)
local function snap_area(props: snap_area)
local position = source(Vector2.zero)
local size = source(Vector2.zero)
local docked = source(false)
return create "Frame" {
Name = "SnapArea",
AutoLocalize = false,
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
changed("AbsoluteSize", size),
changed("AbsolutePosition", position),
action(function(ref)
snap_areas[ref] = {
position = position,
docked = docked,
size = size,
zindex = props.zindex or 0
}
cleanup(function()
snap_areas[ref] = nil
end)
end)
}
end
local function snappable(props: snappable)
local snapped_to = source()
effect(function()
if props.dragging() == false then return end
local mpos = mouse_position()
untrack(function()
if snapped_to() then snapped_to().docked(false) end
local snap_to
for _, data in snap_areas do
if not in_bounds(mpos, data.position(), data.size()) then continue end
if snap_to and read(data.zindex) <= read(snap_to.zindex) then continue end
snap_to = data
end
if not snap_to and read(props.allow_floating) == false then return end
if snap_to and snap_to.docked() then return end
if snap_to then snap_to.docked(true) end
snapped_to(snap_to)
end)
end)
effect(function()
props.snapped(if snapped_to() then true else false)
end)
effect(function()
if not snapped_to() then return end
local data = snapped_to()
local pos = data.position()
local size = data.size()
props.position(UDim2.fromOffset(pos.X, pos.Y))
props.size(UDim2.fromOffset(size.X, size.Y))
end)
end
cleanup(UserInputService.InputChanged:Connect(function(input)
if input.UserInputType ~= Enum.UserInputType.MouseMovement then return end
mouse_position(Vector2.new(input.Position.X, input.Position.Y))
end))
return {
snap_area = snap_area,
snappable = snappable
}
end

View file

@ -0,0 +1,179 @@
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local theme = require(script.Parent.Parent.Parent.util.theme)
local button = require(script.Parent.Parent.interactable.button)
local rounded_frame = require(script.Parent.Parent.util.rounded_frame)
local virtualscroller = require(script.Parent.Parent.util.virtualscroller)
local resizeable_bar = require(script.Parent.resizeable_bar)
local scroll_frame = require(script.Parent.scroll_frame)
local create = vide.create
local source = vide.source
local derive = vide.derive
local indexes = vide.indexes
type can<T> = T | () -> T
type table = {
size: can<UDim2>?,
suggested_column_sizes: { number }?,
base_splits: { number }?,
columns: () -> {{any}},
on_click: (column: number, row: number) -> (),
on_click2: (column: number, row: number) -> (),
read_value: (column: number, row: number) -> string,
below: {[number]: any}?,
[number]: any
}
return function(props: table)
local sizes = source({})
local splits = source({})
local meaning = derive(function()
local t = {}
for i, column in props.columns() do
t[i] = column[1]
end
return t
end)
local function get_size(index: number)
local split_before = splits()[index - 1] or source(0) :: never
local split_after = splits()[index] or source(1) :: never
local size = split_after() - split_before()
return size
end
return scroll_frame {
Size = props.size or UDim2.new(1, 0, 0, 8 * 32),
CanvasSize = function()
return UDim2.new(1, 0)
end,
create "UIListLayout" {
VerticalFlex = Enum.UIFlexAlignment.SpaceEvenly
},
resizeable_bar {
meaning = meaning,
sizes = sizes,
splits = splits,
base_splits = props.base_splits,
suggested_sizes = props.suggested_column_sizes
},
create "Folder" {
indexes(meaning, function(_, i)
return create "Frame" {
Size = UDim2.new(0, 1, 1, -32),
AutoLocalize = false,
Position = function()
local pos = splits()[i]
return if not pos then UDim2.fromScale(0, 0) else UDim2.fromScale(pos(), 0)
end,
BackgroundColor3 = theme.bg[-1],
ZIndex = 100
}
end)
},
virtualscroller {
size = UDim2.fromScale(1, 0),
create "UIFlexItem" {
FlexMode = Enum.UIFlexMode.Grow,
},
{
BackgroundColor3 = theme.bg[0],
VerticalScrollBarInset = Enum.ScrollBarInset.None,
BackgroundTransparency = 0,
},
item_size = 32,
item = function(index)
return create "Frame" {
Size = UDim2.new(1, 0, 0, 32),
AutoLocalize = false,
BackgroundColor3 = theme.bg[2],
create "UIListLayout" {
FillDirection = Enum.FillDirection.Horizontal,
Padding = UDim.new(0, 0)
},
create "UIStroke" {
Color = theme.bg[-1],
},
indexes(props.columns, function(column, i)
return button {
size = function()
local column_size = get_size(i)
return UDim2.new(column_size, 0, 0, 32)
end,
text = function()
return props.read_value(i, index() + 1) or ""
end,
create "UIListLayout" {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center
},
xalignment = Enum.TextXAlignment.Left,
corner = false,
stroke = false,
code = true,
activated = function()
props.on_click(i, index() + 2)
end,
mouse2 = function()
props.on_click2(i, index() + 2)
end
} :: Instance
end)
}
end,
max_items = function()
local value = (props.columns()[1] ~= nil and #props.columns()[1] or 0)
return value - 1
end,
},
rounded_frame {
size = function()
return UDim2.new(1, 0, 0, 32)
end,
color = theme.bg[1],
props.below,
bottomleft = UDim.new(0, 8),
bottomright = UDim.new(0, 8),
},
unpack(props)
}
end

View file

@ -0,0 +1,86 @@
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local anim = require(script.Parent.Parent.Parent.util.anim)
local theme = require(script.Parent.Parent.Parent.util.theme)
local create = vide.create
local read = vide.read
type can<T> = T | () -> T
type props = {
size: can<UDim2>?,
position: can<UDim2>?,
anchorpoint: can<Vector2>?,
automaticsize: can<Enum.AutomaticSize>?,
accent: can<boolean>?,
xalignment: can<Enum.TextXAlignment>?,
yalignment: can<Enum.TextYAlignment>?,
truncate: can<Enum.TextTruncate>?,
wrapped: can<boolean>?,
header: can<boolean>?,
code: can<boolean>?,
disabled: can<boolean>?,
text: can<string>,
textsize: can<number>?,
visible: can<boolean>?,
[number]: any
}
return function(props: props)
local function font()
return if read(props.code) then theme.code else theme.font
end
local function fg()
local accent = read(props.accent)
local disabled = read(props.disabled)
return if accent then
if disabled then theme.fg_on_acc_low[0]()
else theme.fg_on_acc_high[0]()
else
if disabled then theme.fg_on_bg_low[0]()
else theme.fg_on_bg_high[0]()
end
return create "TextLabel" {
Size = props.size,
Position = props.position,
AnchorPoint = props.anchorpoint,
AutomaticSize = props.automaticsize or Enum.AutomaticSize.XY,
AutoLocalize = false,
TextXAlignment = props.xalignment,
TextYAlignment = props.yalignment,
TextTruncate = props.truncate,
BackgroundTransparency = 1,
Text = props.text,
TextSize = props.textsize or function()
return if read(props.header) then theme.header else theme.body
end,
TextWrapped = props.wrapped,
FontFace = function()
return if read(props.header) then
Font.new(font().Family, Enum.FontWeight.Bold)
else font()
end,
TextColor3 = anim(fg),
Visible = props.visible,
unpack(props)
}
end

View file

@ -0,0 +1,229 @@
local GuiService = game:GetService("GuiService")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local vide = require(script.Parent.Parent.Parent.Parent.Parent.vide)
local theme = require(script.Parent.Parent.Parent.Parent.util.theme)
local container = require(script.Parent.Parent.Parent.util.container)
local create = vide.create
local source = vide.source
local spring = vide.spring
local changed = vide.changed
local cleanup = vide.cleanup
type Source<T> = vide.Source<T>
type props = {
resize_range: number,
min_size: Vector2,
can_resize_left: Source<boolean>,
can_resize_right: Source<boolean>,
can_resize_bottom: Source<boolean>,
can_resize_top: Source<boolean>,
resizing: () -> boolean,
}
local function xpos(s: () -> number)
return function()
return Vector2.new(s(), 0)
end
end
local function ypos(s: () -> number)
return function()
return Vector2.new(0, s())
end
end
return function(props: props)
local RESIZE_RANGE = props.resize_range
local MIN_SIZE = props.min_size
local can_resize_left = props.can_resize_left
local can_resize_right = props.can_resize_right
local can_resize_bottom = props.can_resize_bottom
local can_resize_top = props.can_resize_top
local resizing = props.resizing
local absolute_size = source(Vector2.new(1, 1))
local absolute_position = source(Vector2.zero)
local thickness = 4
local border_selected = theme.acc[8]
local gradient = NumberSequence.new({
NumberSequenceKeypoint.new(0, 1),
NumberSequenceKeypoint.new(0.25, 1),
NumberSequenceKeypoint.new(0.5, 0),
NumberSequenceKeypoint.new(0.75, 1),
NumberSequenceKeypoint.new(1, 1),
})
local x = source(0)
local y = source(0)
cleanup(RunService.Heartbeat:Connect(function()
local mposition = UserInputService:GetMouseLocation()
local top_inset, bottom_inset = GuiService:GetGuiInset()
mposition += - top_inset - bottom_inset
if MIN_SIZE.X ~= absolute_size().X or not resizing() then x(mposition.X) end
if MIN_SIZE.Y ~= absolute_size().Y or not resizing() then y(mposition.Y) end
end))
cleanup(RunService.RenderStepped:Connect(function()
local mposition = UserInputService:GetMouseLocation()
local top_inset, bottom_inset = GuiService:GetGuiInset()
mposition += - top_inset - bottom_inset
local x, y = mposition.X, mposition.Y
if resizing() then return end
local left = absolute_position().X
local top = absolute_position().Y
local right = left + absolute_size().X
local bottom = top + absolute_size().Y
local topleft = absolute_position() - Vector2.new(RESIZE_RANGE, RESIZE_RANGE)
local bottomright = absolute_position() + absolute_size() + Vector2.new(RESIZE_RANGE, RESIZE_RANGE)
-- perform AABB to check if the cursor is in range
local within_bounds = x > topleft.X and y > topleft.Y and x < bottomright.X and y < bottomright.Y
can_resize_top(y > top - RESIZE_RANGE and y < top and within_bounds)
can_resize_left(x < left + RESIZE_RANGE and x > left - RESIZE_RANGE and within_bounds)
can_resize_bottom(y < bottom + RESIZE_RANGE and y > bottom - RESIZE_RANGE and within_bounds)
can_resize_right(x < right + RESIZE_RANGE and x > right - RESIZE_RANGE and within_bounds)
end))
return {
changed("AbsoluteSize", function(value: Vector2)
if value.Magnitude == 0 then return end
absolute_size(value)
end),
changed("AbsolutePosition", absolute_position),
container {
Name = "Left",
Position = UDim2.fromScale(0, 0.5),
Size = UDim2.new(0, thickness, 1, thickness * 2),
AnchorPoint = Vector2.new(1, 0.5),
BackgroundColor3 = border_selected,
BackgroundTransparency = spring(function()
return can_resize_left() and 0 or 1
end, 0.2),
ZIndex = 1000,
create "UIGradient" {
Rotation = 90,
Transparency = gradient,
Offset = ypos(spring(function()
return (y() - absolute_position().Y - absolute_size().Y / 2) / absolute_size().Y
end, 0.1)),
},
create "UICorner" {
CornerRadius = UDim.new(0, 4)
},
},
container {
Name = "Right",
Position = UDim2.fromScale(1, 0.5),
Size = UDim2.new(0, thickness, 1, thickness * 2),
AnchorPoint = Vector2.new(0, 0.5),
BackgroundColor3 = border_selected,
BackgroundTransparency = spring(function()
return can_resize_right() and 0 or 1
end, 0.2),
ZIndex = 1000,
create "UIGradient" {
Rotation = 90,
Transparency = gradient,
Offset = ypos(spring(function()
return (y() - absolute_position().Y - absolute_size().Y / 2) / absolute_size().Y
end, 0.1)),
},
create "UICorner" {
CornerRadius = UDim.new(0, 4)
},
},
container {
Name = "Bottom",
Position = UDim2.fromScale(0.5, 1),
Size = UDim2.new(1, thickness * 2, 0, thickness),
AnchorPoint = Vector2.new(0.5, 0),
BackgroundColor3 = border_selected,
BackgroundTransparency = spring(function()
return can_resize_bottom() and 0 or 1
end, 0.2),
ZIndex = 1000,
create "UIGradient" {
Transparency = gradient,
Offset = xpos(spring(function()
return (x() - absolute_position().X - absolute_size().X / 2) / absolute_size().X
end, 0.1)),
},
create "UICorner" {
CornerRadius = UDim.new(0, 4)
},
},
container {
Name = "Top",
Position = UDim2.fromScale(0.5, 0),
Size = UDim2.new(1, thickness * 2, 0, thickness),
AnchorPoint = Vector2.new(0.5, 1),
BackgroundColor3 = border_selected,
BackgroundTransparency = spring(function()
return can_resize_top() and 0 or 1
end, 0.2),
ZIndex = 1000,
create "UIGradient" {
Transparency = gradient,
Offset = xpos(spring(function()
return (x() - absolute_position().X - absolute_size().X / 2) / absolute_size().X
end, 0.1)),
},
create "UICorner" {
CornerRadius = UDim.new(0, 4)
},
},
} :: { any }
end

View file

@ -0,0 +1,299 @@
local GuiService = game:GetService("GuiService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local theme = require(script.Parent.Parent.Parent.util.theme)
local container = require(script.Parent.Parent.util.container)
local padding = require(script.Parent.Parent.util.padding)
local shadow = require(script.Parent.Parent.util.shadow)
local divider = require(script.Parent.divider)
local snapping = require(script.Parent.snapping)
local borders = require(script.borders)
local topbar = require(script.topbar)
local create = vide.create
local source = vide.source
local cleanup = vide.cleanup
local changed = vide.changed
local reference = vide.action
local spring = vide.spring
type can<T> = T | () -> T
type props = {
title: can<string>,
subtitle: can<string>?,
min_size: Vector2?,
position: Vector2?,
size: Vector2?,
bind_to_close: (() -> ())?,
[number]: any
}
local HIGHEST_DISPLAY_ORDER = 100000
local RESIZE_RANGE = 6
local docks
vide.mount(function()
docks = snapping()
return create "ScreenGui" {
Name = "docks",
AutoLocalize = false,
-- create "Frame" {
-- Size = UDim2.new(1, 0, 0, 200),
-- BackgroundTransparency = 1,
-- docks.snap_area {}
-- },
-- create "Frame" {
-- Size = UDim2.new(1, 0, 0, 200),
-- Position = UDim2.fromScale(0, 1),
-- AnchorPoint = Vector2.new(0, 1),
-- BackgroundTransparency = 1,
-- docks.snap_area {}
-- },
create "Frame" {
Size = UDim2.new(0, 16, 1, 0),
BackgroundTransparency = 1,
AutoLocalize = false,
docks.snap_area {}
},
}
end, Players.LocalPlayer.PlayerGui)
return function(props: props)
local min_size = Vector2.new(100, 100):Max(props.min_size or Vector2.zero)
local position = props.position or Vector2.new(32, 32)
local base_size = props.size or min_size * 1.5
local x_size = source(math.max(min_size.X, base_size.X))
local y_size = source(math.max(min_size.Y, base_size.Y))
local x_position = source(position.X)
local y_position = source(position.Y)
local offset = source(Vector2.zero)
local dragging = source(false)
local absolute_position = source(Vector2.zero)
local absolute_size = source(Vector2.zero)
local can_resize_top = source(false)
local can_resize_bottom = source(false)
local can_resize_right = source(false)
local can_resize_left = source(false)
local resizing = source(false)
local ref = source()
local display_order = source(HIGHEST_DISPLAY_ORDER + 1)
HIGHEST_DISPLAY_ORDER += 1
local mouse_inside = source(false)
local top: Vector2
local bottom: Vector2
cleanup(UserInputService.InputEnded:Connect(function(input)
if
input.UserInputType ~= Enum.UserInputType.MouseButton1
and input.UserInputType ~= Enum.UserInputType.Touch
then
return
end
resizing(false)
dragging(false)
end))
cleanup(UserInputService.InputChanged:Connect(function(input: InputObject)
if input.UserInputType ~= Enum.UserInputType.MouseMovement then return end
if not resizing() then return end
local mposition = UserInputService:GetMouseLocation()
local top_inset, bottom_inset = GuiService:GetGuiInset()
mposition += - top_inset - bottom_inset
local x, y = mposition.X, mposition.Y
if can_resize_bottom() then y_size(math.max(y - top.Y, min_size.Y)) end
if can_resize_right() then x_size(math.max(x - top.X, min_size.X)) end
if can_resize_top() then
y_size(math.max(bottom.Y - y, min_size.Y))
y_position(math.min(y, bottom.Y - min_size.Y))
end
if can_resize_left() then
x_size(math.max(bottom.X - x, min_size.X))
x_position(math.min(x, bottom.X - min_size.X))
end
end))
cleanup(UserInputService.InputBegan:Connect(function(input)
if input.UserInputType ~= Enum.UserInputType.MouseButton1 then return end
if not dragging() then resizing(true) end
top = absolute_position()
bottom = absolute_position() + absolute_size()
local player_gui
if Players.LocalPlayer and RunService:IsRunning() then
player_gui = Players.LocalPlayer:WaitForChild("PlayerGui") :: PlayerGui
elseif RunService:IsStudio() and RunService:IsRunning() then
player_gui = game:GetService("CoreGui") :: any
else
return
end
local objects = player_gui:GetGuiObjectsAtPosition(input.Position.X, input.Position.Y)
if #objects == 0 then return end
if not objects[1]:IsDescendantOf(ref()) then return end
display_order(HIGHEST_DISPLAY_ORDER + 1)
HIGHEST_DISPLAY_ORDER += 1
end))
cleanup(UserInputService.InputChanged:Connect(function(input: InputObject)
if dragging() == false then return end
if not UserInputService:IsMouseButtonPressed(Enum.UserInputType.MouseButton1) then
dragging(false)
return
end
local position = UserInputService:GetMouseLocation()
-- local top_inset, bottom_inset = GuiService:GetGuiInset()
-- position += - top_inset - bottom_inset
x_position(position.X + offset().X)
y_position(position.Y + offset().Y)
end))
local snapped = source(false)
local snap_size = source(UDim2.new())
local snap_pos = source(UDim2.new())
local function radius()
return if snapped() then UDim.new() else UDim.new(0, 6)
end
return create "ScreenGui" {
Name = props.title,
AutoLocalize = false,
DisplayOrder = display_order,
reference(ref),
create "Frame" {
AutoLocalize = false,
Position = function()
return if snapped() then snap_pos() else UDim2.fromOffset(x_position(), y_position())
end,
Size = function()
return if snapped() then UDim2.fromOffset(x_size() + 6, snap_size().Y.Offset) else UDim2.fromOffset(x_size() + 6, y_size() + 6)
end,
Active = true,
BackgroundColor3 = theme.bg[0],
MouseMoved = function()
if resizing() then return end
local mposition = UserInputService:GetMouseLocation()
local top_inset, bottom_inset = GuiService:GetGuiInset()
position += - top_inset - bottom_inset
local x, y = mposition.X, mposition.Y
x -= absolute_position().X
y -= absolute_position().Y
can_resize_top(y < RESIZE_RANGE)
can_resize_left(x < RESIZE_RANGE)
can_resize_bottom(y > (absolute_size().Y - RESIZE_RANGE))
can_resize_right(x > (absolute_size().X - RESIZE_RANGE))
end,
MouseEnter = function()
mouse_inside(true)
end,
MouseLeave = function()
if resizing() then return end
if RunService:IsRunning() == false then return end
mouse_inside(false)
end,
changed("AbsolutePosition", absolute_position),
changed("AbsoluteSize", absolute_size),
create "UICorner" {
CornerRadius = radius
},
shadow {},
borders {
resize_range = RESIZE_RANGE,
min_size = min_size,
can_resize_top = can_resize_top,
can_resize_bottom = can_resize_bottom,
can_resize_left = can_resize_left,
can_resize_right = can_resize_right,
resizing = resizing,
},
container {
Size = UDim2.fromScale(1, 1),
create "UIListLayout" {},
topbar {
title = props.title,
subtitle = props.subtitle,
dragging = dragging,
offset = offset,
bind_to_close = props.bind_to_close,
radius = radius
},
divider {},
container {
Size = UDim2.fromScale(1, 0),
padding {
x = UDim.new(0, 8),
y = UDim.new(0, 8)
},
unpack(props),
create "UIFlexItem" {
FlexMode = Enum.UIFlexMode.Grow
},
}
},
docks.snappable {
dragging = dragging,
snapped = snapped,
position = snap_pos,
size = snap_size
}
},
}
end

View file

@ -0,0 +1,150 @@
local vide = require(script.Parent.Parent.Parent.Parent.Parent.vide)
local theme = require(script.Parent.Parent.Parent.Parent.util.theme)
local list = require(script.Parent.Parent.Parent.util.list)
local padding = require(script.Parent.Parent.Parent.util.padding)
local rounded_frame = require(script.Parent.Parent.Parent.util.rounded_frame)
local typography = require(script.Parent.Parent.typography)
local create = vide.create
local source = vide.source
local changed = vide.changed
local spring = vide.spring
local show = vide.show
type Source<T> = vide.Source<T>
type props = {
title: (string | () -> string)?,
subtitle: (string | () -> string)?,
bind_to_close: (() -> ())?,
radius: () -> UDim,
dragging: Source<boolean>,
offset: (new: Vector2) -> (),
[any]: any,
}
return function(props: props)
local bind_to_close = props.bind_to_close
local dragging = props.dragging
local offset = props.offset
local closeable = not not bind_to_close
local absolute_position = source(Vector2.zero)
local gui_state = source(Enum.GuiState.Idle)
return rounded_frame {
name = "Topbar",
size = UDim2.new(1, 0, 0, 48),
color = theme.bg[3],
topleft = props.radius,
topright = props.radius,
create "ImageButton" {
Size = UDim2.fromScale(1, 1),
AutoLocalize = false,
BackgroundTransparency = 1,
ZIndex = 1000,
changed("AbsolutePosition", absolute_position),
MouseButton1Down = function(x, y)
offset(absolute_position() - Vector2.new(x, y))
dragging(true)
end,
create "UIListLayout" {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
HorizontalFlex = Enum.UIFlexAlignment.Fill,
},
padding {
x = UDim.new(0, 16)
},
list {
spacing = UDim.new(),
typography {
size = UDim2.fromScale(1, 0),
text = props.title,
xalignment = Enum.TextXAlignment.Left,
truncate = Enum.TextTruncate.SplitWord,
textsize = 20,
header = true,
},
show(function()
return props.subtitle ~= nil
end, function()
return typography {
size = UDim2.fromScale(1, 0),
text = props.subtitle,
bold = true,
xalignment = Enum.TextXAlignment.Left,
truncate = Enum.TextTruncate.SplitWord,
textsize = 16,
}
end)
},
show(source(closeable), function()
return create "ImageButton" {
Size = UDim2.fromOffset(32, 32),
BackgroundColor3 = spring(function()
return if gui_state() == Enum.GuiState.Hover then
theme.bg[5]()
elseif gui_state() == Enum.GuiState.Press then
theme.bg[0]()
else
theme.bg[3]()
end, 0.1),
changed("GuiState", gui_state),
Activated = props.bind_to_close,
create "UICorner" {
CornerRadius = UDim.new(1, 0)
},
create "ImageLabel" {
Size = UDim2.fromOffset(24, 24),
Position = UDim2.fromScale(0.5, 0.5),
AnchorPoint = Vector2.new(0.5, 0.5),
AutoLocalize = false,
BackgroundTransparency = 1,
ImageColor3 = theme.fg_on_bg_high[3],
Image = "rbxassetid://10747384394",
},
create "UIFlexItem" {
FlexMode = Enum.UIFlexMode.Custom,
ShrinkRatio = 0
}
}
end)
}
}
end

View file

@ -0,0 +1,77 @@
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local theme = require(script.Parent.Parent.Parent.util.theme)
local container = require(script.Parent.Parent.util.container)
local create = vide.create
local indexes = vide.indexes
local read = vide.read
type can<T> = T | () -> T
type props = {
position: can<UDim2>?,
size: can<UDim2>?,
anchorpoint: can<UDim2>?,
values: () -> {number},
max: can<number>?,
min: can<number>?,
[number]: Instance
}
return function(props: props)
local max = props.max or function()
return math.max(unpack(props.values()))
end
local function total()
return #props.values()
end
return container {
Position = props.position,
Size = props.size,
AnchorPoint = props.anchorpoint,
ClipsDescendants = true,
indexes(props.values, function(value, index)
return create "Frame" {
AutoLocalize = false,
Position = function()
return UDim2.fromScale((index - 1) / total(), 1)
end,
Size = function()
return UDim2.fromScale(
1/total(),
value() / read(max)
)
end,
AnchorPoint = Vector2.new(0, 1),
create "UIGradient" {
Color = function()
return ColorSequence.new(
theme.acc[10](),
theme.acc[-3]()
)
end,
Rotation = 90
}
}
end),
unpack(props)
}
end

View file

@ -0,0 +1,53 @@
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local theme = require(script.Parent.Parent.Parent.util.theme)
local container = require(script.Parent.Parent.util.container)
local create = vide.create
local source = vide.source
local action = vide.action
local effect = vide.effect
type can<T> = T | () -> T
type props = {
position: can<UDim2>?,
size: can<UDim2>?,
anchorpoint: can<UDim2>?,
values: () -> {Path2DControlPoint},
[number]: Instance
}
return function(props: props)
local path2d: vide.Source<Path2D> = source()
effect(function()
if not path2d() then return end
local path = path2d()
path:SetControlPoints(props.values())
end)
return container {
Position = props.position,
Size = props.size,
AnchorPoint = props.anchorpoint,
create "Path2D" {
Thickness = 2,
Color3 = theme.acc[3],
action(path2d)
},
unpack(props)
}
end

View file

@ -0,0 +1,74 @@
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local theme = require(script.Parent.Parent.Parent.util.theme)
local container = require(script.Parent.Parent.util.container)
local create = vide.create
local source = vide.source
local action = vide.action
local effect = vide.effect
local read = vide.read
type can<T> = T | () -> T
type props = {
position: can<UDim2>?,
size: can<UDim2>?,
anchorpoint: can<UDim2>?,
values: () -> {number},
max: can<number>?,
min: can<number>?,
[number]: Instance
}
return function(props: props)
local path2d: vide.Source<Path2D> = source()
effect(function()
if not path2d() then return end
local path = path2d()
local points = table.create(50)
local total = #props.values()
local max = read(props.max) or 100
local min = read(props.min) or 0
local diff = math.abs(max - min)
for index, value in props.values() do
table.insert(
points,
Path2DControlPoint.new(
UDim2.fromScale(
(index - 1) / (total - 1), 1 - (value - min) / diff
)
)
)
end
path:SetControlPoints(points)
end)
return container {
Position = props.position,
Size = props.size,
AnchorPoint = props.anchorpoint,
ClipsDescendants = true,
create "Path2D" {
Thickness = 2,
Color3 = theme.acc[3],
action(path2d)
},
unpack(props)
}
end

View file

@ -0,0 +1,160 @@
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local anim = require(script.Parent.Parent.Parent.util.anim)
local theme = require(script.Parent.Parent.Parent.util.theme)
local typography = require(script.Parent.Parent.display.typography)
local padding = require(script.Parent.Parent.util.padding)
local create = vide.create
local source = vide.source
local changed = vide.changed
local show = vide.show
local read = vide.read
type can<T> = T | () -> T
type props = {
size: can<UDim2>?,
position: can<UDim2>?,
anchorpoint: can<Vector2>?,
automaticsize: can<Enum.AutomaticSize>?,
text: can<string>?,
disabled: can<boolean>?,
activated: () -> ()?,
mouse2: () -> ()?,
down: () -> ()?,
up: () -> ()?,
--- enables the stroke (enabled by default)
stroke: can<boolean>?,
--- enables the corner (enabled by default)
corner: can<boolean>?,
accent: can<boolean>?,
xalignment: can<Enum.TextXAlignment>?,
code: can<boolean>?,
[number]: any
}
return function(props: props)
local guistate = source(Enum.GuiState.Idle)
local function bg()
local accent = read(props.accent)
local guistate = guistate()
return if accent then
if guistate == Enum.GuiState.NonInteractable then theme.acc[-5]()
elseif guistate == Enum.GuiState.Idle then theme.acc[0]()
elseif guistate == Enum.GuiState.Hover then theme.acc[3]()
elseif guistate == Enum.GuiState.Press then theme.acc[-8]()
else theme.acc[0]()
else
if guistate == Enum.GuiState.NonInteractable then theme.bg[-2]()
elseif guistate == Enum.GuiState.Idle then theme.bg[3]()
elseif guistate == Enum.GuiState.Hover then theme.bg[6]()
elseif guistate == Enum.GuiState.Press then theme.bg[0]()
else theme.acc[0]()
end
local function stroke()
local accent = read(props.accent)
local guistate = guistate()
return if accent then
if guistate == Enum.GuiState.NonInteractable then theme.acc[-7]()
else theme.acc[-7]()
else
if guistate == Enum.GuiState.NonInteractable then theme.bg[-3]()
else theme.bg[-3]()
end
return create "TextButton" {
Name = props.text,
AutoLocalize = false,
Size = props.size or UDim2.fromOffset(100, 30),
Position = props.position,
AnchorPoint = props.anchorpoint,
AutomaticSize = props.automaticsize,
Interactable = function()
return not read(props.disabled)
end,
BackgroundColor3 = anim(bg),
Activated = props.activated,
MouseButton2Click = props.mouse2,
MouseButton1Down = props.down,
MouseButton1Up = props.up,
typography {
position = UDim2.fromScale(0.5, 0.5),
anchorpoint = Vector2.new(0.5, 0.5),
size = UDim2.fromScale(1, 1),
automaticsize = Enum.AutomaticSize.Y,
text = props.text,
truncate = Enum.TextTruncate.SplitWord,
xalignment = props.xalignment,
accent = props.accent,
disabled = props.disabled,
visible = function()
return read(props.text) ~= ""
end,
code = props.code,
create "UIFlexItem" {
FlexMode = Enum.UIFlexMode.Fill
}
},
show(
function()
return read(props.stroke) ~= false
end,
source(
create "UIStroke" {
ApplyStrokeMode = Enum.ApplyStrokeMode.Border,
Color = anim(stroke),
Thickness = 1,
Enabled = props.stroke
}
)
),
show(
function()
return read(props.corner) ~= false
end,
source(
create "UICorner" {
CornerRadius = UDim.new(0,4)
}
)
),
padding {
x = UDim.new(0, 8),
y = UDim.new(0, 2)
},
changed("GuiState", guistate),
unpack(props),
}
end

View file

@ -0,0 +1,166 @@
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local theme = require(script.Parent.Parent.Parent.util.theme)
local scroll_frame = require(script.Parent.Parent.display.scroll_frame)
local container = require(script.Parent.Parent.util.container)
local list = require(script.Parent.Parent.util.list)
local padding = require(script.Parent.Parent.util.padding)
local portal = require(script.Parent.Parent.util.portal)
local button = require(script.Parent.button)
local create = vide.create
local source = vide.source
local changed = vide.changed
local indexes = vide.indexes
local spring = vide.spring
local read = vide.read
local MAX_SIZE = 100
type can<T> = T | () -> T
type dropdown = {
size: can<UDim2>?,
position: can<UDim2>?,
anchorpoint: can<Vector2>?,
selected: can<number>,
update_selected: (() -> number)?,
options: can<{string}>
}
local function dropdown(props: dropdown)
local selected = props.selected
local update_selected = props.update_selected or function() end
local options = props.options
local enabled = source(false)
local absolute_size = source(Vector2.zero)
local size = spring(function()
if not enabled() then return UDim2.fromScale(1, 0) end
return UDim2.new(1, 0, 0, math.min(MAX_SIZE, absolute_size().Y))
end, 0.1)
return button {
size = props.size or UDim2.fromOffset(200, 32),
position = props.position or UDim2.fromScale(0.5, 0.5),
anchorpoint = props.anchorpoint or Vector2.new(0.5, 0.5),
xalignment = Enum.TextXAlignment.Left,
text = function()
return read(options)[read(selected)]
end,
activated = function()
enabled(not enabled())
end,
create "UIListLayout" {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
Padding = UDim.new(0, 8),
},
container {
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.fromScale(1, 0),
Size = UDim2.new(0, 18, 0, 16),
LayoutOrder = -1,
create "ImageLabel" {
Name = "arrow",
AutoLocalize = false,
Size = UDim2.new(0, 8, 0, 4),
Position = UDim2.fromScale(0.5, 0.5),
AnchorPoint = Vector2.new(0.5, 0.5),
Rotation = spring(function()
return if enabled() then -180 else 0
end, 0.1),
BackgroundTransparency = 1,
BackgroundColor3 = theme.fg_on_bg_high[3],
Image = "rbxassetid://7260137654",
ImageColor3 = theme.fg_on_bg_low[3],
ScaleType = Enum.ScaleType.Stretch
}
},
portal {
inherit_layout = true,
container {
Position = UDim2.new(0, 1, 1, 4),
Size = size,
BackgroundTransparency = 0,
BackgroundColor3 = theme.bg[3],
ClipsDescendants = true,
Visible = function()
return size().Y.Offset > 1
end,
padding {
padding = UDim.new(0, 2)
},
scroll_frame {
Size = UDim2.fromScale(1, 1),
ScrollBarThickness = 4,
AutomaticCanvasSize = Enum.AutomaticSize.Y,
list {
spacing = UDim.new(0, 1),
changed("AbsoluteSize", absolute_size),
indexes(function()
return read(options)
end, function(value, key)
return button {
size = UDim2.new(1, 0, 0, 30),
text = value,
stroke = false,
activated = function()
enabled(false)
update_selected(key)
end,
create "UIListLayout" {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
Padding = UDim.new(0, 8),
},
}
end)
}
},
create "UIStroke" {
Color = theme.bg[-3]
},
create "UICorner" {
CornerRadius = UDim.new(0, 3)
}
}
}
}
end
return dropdown

View file

@ -0,0 +1,175 @@
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local anim = require(script.Parent.Parent.Parent.util.anim)
local theme = require(script.Parent.Parent.Parent.util.theme)
local typography = require(script.Parent.Parent.display.typography)
local padding = require(script.Parent.Parent.util.padding)
local create = vide.create
local source = vide.source
local changed = vide.changed
local effect = vide.effect
local action = vide.action
local read = vide.read
type can<T> = T | () -> T
type props = {
size: can<UDim2>?,
position: can<UDim2>?,
anchorpoint: can<UDim2>?,
text: can<string>?,
placeholder: can<string>?,
multiline: can<boolean>?,
code: can<boolean>?,
disabled: can<boolean>?,
stroke: can<boolean>?,
corner: can<boolean>?,
--- called whenever a character is added / removed
oninput: ((new: string) -> ())?,
--- called whenever focus is lost
focuslost: ((text: string, enter: boolean?) -> ())?,
--- called whenever focus is lost by pressing enter
enter: ((text: string) -> ())?,
}
return function(props: props)
local guistate = source(Enum.GuiState.Idle)
local focused = source(false)
local textbox = source() :: vide.Source<TextBox>
local text = source("")
effect(function()
text(read(props.text) or "")
end)
local function bg()
local guistate = guistate()
return if guistate == Enum.GuiState.NonInteractable then theme.bg[0]()
elseif focused() then theme.bg[-3]()
else theme.bg[-2]()
end
local function fg()
local disabled = read(props.disabled)
return if disabled then theme.fg_on_bg_low[0]()
else theme.fg_on_bg_high[0]()
end
local function stroke()
local guistate = guistate()
return if guistate == Enum.GuiState.NonInteractable then theme.bg[-3]()
elseif focused() then theme.acc[5]()
elseif guistate == Enum.GuiState.Idle then theme.bg[-3]()
elseif guistate == Enum.GuiState.Hover then theme.bg[3]()
else theme.bg[-3]()
end
-- this effect will automatically focus the textbox if focused is true
effect(function()
if focused() == true and textbox() then
textbox():CaptureFocus()
end
end)
return create "TextButton" {
Name = props.placeholder or "Textbox",
AutoLocalize = false,
Size = props.size or UDim2.fromOffset(300, 30),
Position = props.position,
AnchorPoint = props.anchorpoint,
Activated = function()
focused(true)
end,
Interactable = function()
return not props.disabled
end,
BackgroundColor3 = anim(bg),
ClipsDescendants = true,
create "TextBox" {
Size = UDim2.fromScale(1, 1),
AutoLocalize = false,
MultiLine = props.multiline,
BackgroundTransparency = 1,
Focused = function()
focused(true)
end,
FocusLost = function(enter)
focused(false)
if props.focuslost then
props.focuslost(text(), enter)
end
if props.enter then
props.enter(text())
end
end,
TextSize = theme.body,
FontFace = function()
return if read(props.code) then theme.code else theme.font
end,
TextColor3 = anim(fg),
PlaceholderColor3 = theme.fg_on_bg_low[0],
PlaceholderText = props.placeholder,
Text = props.text,
ClipsDescendants = true,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = function()
return if read(props.multiline) then Enum.TextYAlignment.Top else Enum.TextYAlignment.Center
end,
action(textbox),
changed("Text", text),
if props.oninput then changed("Text", props.oninput) else nil
},
create "UIStroke" {
ApplyStrokeMode = Enum.ApplyStrokeMode.Border,
Color = anim(stroke),
Thickness = 1,
Enabled = props.stroke
},
create "UICorner" {
CornerRadius = function()
return if read(props.corner) == false then UDim.new() else UDim.new(0, 4)
end
},
padding {
x = UDim.new(0, 8),
y = UDim.new(0, 2)
},
changed("GuiState", guistate)
}
end

View file

@ -0,0 +1,25 @@
--[[
container is a basic transparent frame that covers the entire frame.
]]
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local create = vide.create
local function container(props: vide.vFrame)
return create "Frame" {
Name = "Container",
AutoLocalize = false,
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
props,
}
end
return container

View file

@ -0,0 +1,31 @@
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local container = require(script.Parent.container)
local read = vide.read
type can<T> = T | () -> T
type props = {
gap: can<number>,
direction: can<"x" | "y">?,
}
return function(props: props)
local function direction()
return read(props.direction) or "x"
end
return container {
Size = function()
return if direction() == "x" then
UDim2.new(0, read(props.gap), 1, 0)
else
UDim2.new(1, 0, 0, read(props.gap))
end
}
end

View file

@ -0,0 +1,55 @@
--[[
Creates a container for a list of elements.
]]
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local container = require(script.Parent.container)
local create = vide.create
local read = vide.read
type can<T> = (() -> T) | T
type layout = {
justifycontent: can<Enum.UIFlexAlignment>?,
alignitems: can<Enum.ItemLineAlignment>?,
spacing: can<number | UDim>?,
wraps: can<boolean>?,
[number]: Instance
}
local function layout(props: layout)
return container {
Size = UDim2.fromScale(1, 0),
AutomaticSize = Enum.AutomaticSize.Y,
create "UIListLayout" {
Padding = function()
local spacing: number | UDim? = read(props.spacing)
return if typeof(spacing) == "number" then
UDim.new(0, spacing)
elseif typeof(spacing) == "UDim" then
spacing
elseif typeof(spacing) == "nil" then
UDim.new(0, 8)
else
error("incorrect spacing type")
end,
VerticalFlex = props.justifycontent,
ItemLineAlignment = props.alignitems,
Wraps = props.wraps
},
unpack(props)
}
end
return layout

View file

@ -0,0 +1,41 @@
--[[
Fast and easy to use padding utility to make controlling padding quick and simple.
]]
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local create = vide.create
type can<T> = T | () -> T
type padding = {
padding: can<UDim>?,
x: can<UDim>?,
y: can<UDim>?,
left: can<UDim>?,
right: can<UDim>?,
top: can<UDim>?,
bottom: can<UDim>?
}
local function padding(props: padding)
local padding = props.padding or UDim.new(0, 8)
local x = props.x or padding
local y = props.y or padding
local left = props.left or x
local right = props.right or x
local top = props.top or y
local bottom = props.bottom or y
return create "UIPadding" {
PaddingLeft = left,
PaddingRight = right,
PaddingTop = top,
PaddingBottom = bottom,
}
end
return padding

View file

@ -0,0 +1,102 @@
--[[
portal is used to render a component over other components.
it will find the nearest layer collector to parent itself and it's descendants
onto, and if inherit_layout is enabled, inherits the nearest guibase2d's size and
position properties.
]]
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local create = vide.create
local source = vide.source
local effect = vide.effect
local cleanup = vide.cleanup
local ref = vide.action
local read = vide.read
type can<T> = T | () -> T
type portal = {
--- controls if the portal should inherit the layout of the frame it's under
inherit_layout: can<boolean>?,
[number]: Instance,
}
local layout = 100_000
local function portal(props: portal)
local inherit_layout = props.inherit_layout
local nearest_gui_base = source(nil :: GuiBase2d?)
local nearest_layer_collector = source(nil :: LayerCollector?)
local size = source(UDim2.fromScale(1, 1))
local position = source(UDim2.fromScale(0, 0))
local reference = source(nil :: Configuration?)
-- this will create connections to update the size and position sources
effect(function()
local object = nearest_gui_base()
if not object then return end
local function update()
size(UDim2.fromOffset(object.AbsoluteSize.X, object.AbsoluteSize.Y))
position(UDim2.fromOffset(object.AbsolutePosition.X, object.AbsolutePosition.Y))
end
cleanup(object:GetPropertyChangedSignal("AbsoluteSize"):Connect(update))
cleanup(object:GetPropertyChangedSignal("AbsolutePosition"):Connect(update))
end)
-- creates a container that is mounted to somewhere.
cleanup(vide.mount(function()
cleanup(create "Frame" {
Name = `Portal:{layout}`,
Parent = nearest_layer_collector,
AutoLocalize = false,
ZIndex = layout,
Size = function()
return if read(inherit_layout) == true then size()
else UDim2.fromScale(1, 1)
end,
Position = function()
return if read(inherit_layout) == true then position()
else UDim2.fromScale(0, 0)
end,
BackgroundTransparency = 1,
unpack(props)
})
end))
-- this is an anchor used to reference what gui base and layer collector to use.
return create "Configuration" {
Name = `PortalAnchor:{layout}`,
AncestryChanged = function()
local reference = reference()
if not reference then
nearest_gui_base(nil)
return
end
nearest_gui_base(reference:FindFirstAncestorWhichIsA("GuiBase2d"))
nearest_layer_collector(reference:FindFirstAncestorWhichIsA("LayerCollector"))
end,
ref(function(instance)
layout += 1
reference(instance)
nearest_gui_base(instance:FindFirstAncestorWhichIsA("GuiBase2d"))
nearest_layer_collector(instance:FindFirstAncestorWhichIsA("LayerCollector"))
end)
}
end
return portal

View file

@ -0,0 +1,201 @@
--[[
rounded_frame is a special kind of frame with UICorner controls for every
single corner.
]]
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local container = require(script.Parent.container)
local create = vide.create
local read = vide.read
type can<T> = T | () -> T
type rounded_frame = {
name: can<string>?,
size: can<UDim2>?,
position: can<UDim2>?,
anchor_point: can<Vector2>?,
topleft: can<UDim>?,
topright: can<UDim>?,
bottomleft: can<UDim>?,
bottomright: can<UDim>?,
color: can<Color3>?,
layout: vide.vFrame?,
[number]: any,
}
local function rounded_frame(props: rounded_frame)
local topleft = props.topleft or UDim.new()
local topright = props.topright or UDim.new()
local bottomleft = props.bottomleft or UDim.new()
local bottomright = props.bottomright or UDim.new()
local function corner(name: string, position: UDim2, anchor_point: Vector2, udim: can<UDim>)
return create "Frame" {
Name = name,
AutoLocalize = false,
Size = function()
return UDim2.new(read(udim), read(udim))
end,
Position = position,
AnchorPoint = anchor_point,
BackgroundTransparency = 1,
ClipsDescendants = true,
create "Frame" {
Name = "TopLeft",
AutoLocalize = false,
Size = UDim2.fromScale(2, 2),
Position = UDim2.fromScale(-anchor_point.X, -anchor_point.Y),
BackgroundColor3 = props.color,
ClipsDescendants = true,
create "UICorner" {
CornerRadius = udim
}
}
}
end
return create "Frame" {
Name = props.name or "RoundedFrame",
Size = props.size,
Position = props.position,
AnchorPoint = props.anchor_point,
BackgroundColor3 = props.color,
BackgroundTransparency = 1,
create "Folder" {
Name = "Corner",
corner("TopLeft", UDim2.fromScale(0, 0), Vector2.new(0, 0), topleft),
corner("TopRight", UDim2.fromScale(1, 0), Vector2.new(1, 0), topright),
corner("BottomLeft", UDim2.fromScale(0, 1), Vector2.new(0, 1), bottomleft),
corner("BottomRight", UDim2.fromScale(1, 1), Vector2.new(1, 1), bottomright),
create "Frame" {
AutoLocalize = false,
Name = "FrameLeft",
Size = function()
return UDim2.new(
0.5,
0,
1 - read(topleft).Scale - read(bottomleft).Scale,
- (read(topleft).Offset + read(bottomleft).Offset)
)
end,
Position = function()
return UDim2.new(
0, 0,
0.5 + read(topleft).Scale / 2 - read(bottomleft).Scale / 2,
0 + read(topleft).Offset / 2 - read(bottomleft).Offset / 2
)
end,
AnchorPoint = Vector2.new(0, 0.5),
BackgroundColor3 = props.color,
},
create "Frame" {
Name = "FrameRight",
AutoLocalize = false,
Size = function()
return UDim2.new(
0.5,
0,
1 - read(topright).Scale - read(bottomright).Scale,
- (read(topright).Offset + read(bottomright).Offset)
)
end,
Position = function()
return UDim2.new(
1, 0,
0.5 + read(topright).Scale / 2 - read(bottomright).Scale / 2,
0 + read(topright).Offset / 2 - read(bottomright).Offset / 2
)
end,
AnchorPoint = Vector2.new(1, 0.5),
BackgroundColor3 = props.color,
},
create "Frame" {
Name = "FrameTop",
AutoLocalize = false,
Size = function()
return UDim2.new(
1 - read(topleft).Scale - read(topright).Scale,
- (read(topleft).Offset + read(topright).Offset),
0.5,
0
)
end,
Position = function()
return UDim2.new(
0.5 + read(topleft).Scale / 2 - read(topright).Scale / 2,
0 + read(topleft).Offset / 2 - read(topright).Offset / 2,
0, 0
)
end,
AnchorPoint = Vector2.new(0.5, 0),
BackgroundColor3 = props.color,
},
create "Frame" {
Name = "FrameBottom",
AutoLocalize = false,
Size = function()
return UDim2.new(
1 - read(bottomleft).Scale - read(bottomright).Scale,
- (read(bottomleft).Offset + read(bottomright).Offset),
0.5,
0
)
end,
Position = function()
return UDim2.new(
0.5 + read(bottomleft).Scale / 2 - read(bottomright).Scale / 2,
0 + read(bottomleft).Offset / 2 - read(bottomright).Offset / 2,
1, 0
)
end,
AnchorPoint = Vector2.new(0.5, 1),
BackgroundColor3 = props.color,
},
},
container {
unpack(props)
},
props.layout
}
end
return rounded_frame

View file

@ -0,0 +1,56 @@
--[[
Creates a container for a list of elements.
]]
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local container = require(script.Parent.container)
local create = vide.create
local read = vide.read
type can<T> = (() -> T) | T
type layout = {
justifycontent: can<Enum.UIFlexAlignment>?,
alignitems: can<Enum.ItemLineAlignment>?,
spacing: can<number | UDim>?,
wraps: can<boolean>?,
[number]: Instance
}
local function layout(props: layout)
return container {
Size = UDim2.fromScale(1, 0),
AutomaticSize = Enum.AutomaticSize.Y,
create "UIListLayout" {
Padding = function()
local spacing: number | UDim? = read(props.spacing)
return if typeof(spacing) == "number" then
UDim.new(0, spacing)
elseif typeof(spacing) == "UDim" then
spacing
elseif typeof(spacing) == "nil" then
UDim.new(0, 8)
else
error("incorrect spacing type")
end,
FillDirection = Enum.FillDirection.Horizontal,
HorizontalFlex = props.justifycontent,
ItemLineAlignment = props.alignitems,
Wraps = props.wraps
},
unpack(props)
}
end
return layout

View file

@ -0,0 +1,20 @@
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local create = vide.create
type can<T> = T | () -> T
type props = {
zindex: can<number>?,
transparency: can<number>?
}
return function(props: props)
return create "UIStroke" {
Thickness = 2,
Color = Color3.new(0, 0, 0),
Transparency = 0.8
}
end

View file

@ -0,0 +1,189 @@
local vide = require(script.Parent.Parent.Parent.Parent.vide)
local scroll_frame = require(script.Parent.Parent.display.scroll_frame)
local container = require(script.Parent.container)
local create = vide.create
local source = vide.source
local values = vide.values
local changed = vide.changed
local effect = vide.effect
local untrack = vide.untrack
local batch = vide.batch
type can<T> = T | () -> T
type props = {
size: can<UDim2>?,
position: can<UDim2>?,
anchorpoint: can<UDim2>?,
--- streams in items. when index is -1, should expect to be unused
item: (index: () -> number) -> Instance,
--- streams in separators. when index is -1, should expect to be unused
separator: ((index: () -> number) -> Instance)?,
item_size: number,
separator_size: number?,
max_items: (() -> number)?,
[number]: any
}
return function(props: props)
local items = source({} :: {vide.Source<number>})
local absolute_size = source(Vector2.zero)
local canvas_position = source(Vector2.zero)
local item_size = props.item_size
local separator_size = props.separator_size or 0
local item = props.item
local separator = props.separator
local OVERFLOW = 4
effect(function()
local absolute_size = absolute_size()
local canvas_position = canvas_position()
local child_size = item_size + separator_size
local total_required = math.ceil(absolute_size.Y / child_size) + OVERFLOW
local sources = untrack(items)
local min_index = math.floor(canvas_position.Y / child_size)
local max_index = math.ceil((canvas_position.Y + absolute_size.Y) / child_size)
local max_items = math.huge
if props.max_items then
max_items = props.max_items()
end
batch(function()
untrack(function()
-- mark any sources out of range as unused
local unused = {}
for i, s in sources do
local index = s()
if
index >= math.max(min_index, 1)
and index <= math.min(max_index, max_items)
then continue end
unused[i] = true
s(-1)
end
-- add sources necessary
if #sources < total_required then
for i = #sources + 1, total_required do
sources[i] = source(-1)
unused[i] = true
end
items(sources)
end
-- update indexes of any sources that went unused
local did_not_render = {}
for i = math.max(min_index, 1), math.min(max_index, max_items) do
did_not_render[i] = true
end
for _, s in sources do
did_not_render[s()] = nil
end
for index in unused do
local s = sources[index]
local key = next(did_not_render)
if not key then break end
s(key)
did_not_render[key] = nil
unused[index] = nil
end
-- remove unnecessary sources
if #sources > total_required then
for i = #sources, 1, -1 do
if unused[i] then
table.remove(sources, i)
end
unused[i] = nil
if #sources < total_required then break end
end
items(sources)
end
end)
end)
end)
return scroll_frame {
unpack(props),
Size = props.size or UDim2.fromScale(1, 1),
Position = props.position,
AnchorPoint = props.anchorpoint,
BackgroundTransparency = 1,
CanvasSize = function()
if props.max_items then
return UDim2.fromOffset(0, props.max_items() * (item_size + separator_size))
else
local absolute_size = absolute_size()
local canvas_position = canvas_position()
local child_size = item_size + separator_size
local max_index = math.ceil((canvas_position.Y + absolute_size.Y) / child_size) + OVERFLOW
return UDim2.fromOffset(0, max_index * child_size)
end
end,
values(items, function(index)
return create "Frame" {
Name = index,
AutoLocalize = false,
Position = function()
if index() == -1 then UDim2.fromOffset(0, -1000) end
return UDim2.fromOffset(
0,
(item_size + separator_size) * (index() - 1)
)
end,
Size = UDim2.new(1, 0, 0, item_size + separator_size),
BackgroundTransparency = 1,
container {
Name = "Item",
item(index),
},
if separator then
container {
Name = "Separator",
separator(index)
}
else nil,
}
end),
changed("AbsoluteSize", absolute_size),
changed("CanvasPosition", canvas_position),
}
end

View file

@ -0,0 +1,67 @@
local accordion = require(script.components.display.accordion)
local background = require(script.components.display.background)
local checkbox = require(script.components.display.checkbox)
local divider = require(script.components.display.divider)
local pages = require(script.components.display.pages)
local pane = require(script.components.display.pane)
local snapping = require(script.components.display.snapping)
local tablesheet = require(script.components.display.tablesheet)
local typography = require(script.components.display.typography)
local widget = require(script.components.display.widget)
local bargraph = require(script.components.graph.bargraph)
local graph = require(script.components.graph.graph)
local linegraph = require(script.components.graph.linegraph)
local button = require(script.components.interactable.button)
local select = require(script.components.interactable.select)
local textfield = require(script.components.interactable.textfield)
local container = require(script.components.util.container)
local gap = require(script.components.util.gap)
local list = require(script.components.util.list)
local padding = require(script.components.util.padding)
local rounded_frame = require(script.components.util.rounded_frame)
local row = require(script.components.util.row)
local shadow = require(script.components.util.shadow)
local virtualscroller = require(script.components.util.virtualscroller)
local anim = require(script.util.anim)
local oklch = require(script.util.oklch)
local theme = require(script.util.theme)
return {
widget = widget,
background = background,
pane = pane,
snapping = snapping,
typography = typography,
tablesheet = tablesheet,
accordion = accordion,
divider = divider,
checkbox = checkbox,
bargraph = bargraph,
graph = graph,
linegraph = linegraph,
button = button,
textfield = textfield,
select = select,
pages = pages,
theme = theme,
anim = anim,
container = container,
rounded_frame = rounded_frame,
list = list,
row = row,
shadow = shadow,
padding = padding,
virtualscroller = virtualscroller,
gap = gap,
oklch = oklch,
}

View file

@ -0,0 +1,350 @@
local SA98G = {
mainTRC = 2.4,
mainTRCencode = 1 / 2.4,
sRco = 0.2126729,
sGco = 0.7151522,
sBco = 0.0721750,
normBG = 0.56,
normTXT = 0.57,
revTXT = 0.62,
revBG = 0.65,
blkThrs = 0.022,
blkClmp = 1.414,
scaleBoW = 1.14,
scaleWoB = 1.14,
loBoWoffset = 0.027,
loWoBoffset = 0.027,
deltaYmin = 0.0005,
loClip = 0.1,
mFactor = 1.94685544331710,
mFactInv = 1 / 1.94685544331710,
mOffsetIn = 0.03873938165714010,
mExpAdj = 0.2833433964208690,
mExp = 0.2833433964208690 / 1.414,
mOffsetOut = 0.3128657958707580,
}
local function isNaN(n)
return n ~= n
end
local function reverseAPCA(contrast, knownY, knownType, returnAs)
if contrast == nil then
contrast = 0
end
if knownY == nil then
knownY = 1.0
end
if knownType == nil then
knownType = "bg"
end
if returnAs == nil then
returnAs = "hex"
end
if math.abs(contrast) < 9 then
return false
end
local unknownY = knownY
local knownExp
local unknownExp
--/// APCA 0.0.98G - 4g - W3 Compatible Constants ////////////////////
local scale = if contrast > 0 then SA98G.scaleBoW else SA98G.scaleWoB
local offset = if contrast > 0 then SA98G.loBoWoffset else -SA98G.loWoBoffset
contrast = (assert(tonumber(contrast)) * 0.01 + offset) / scale
-- Soft clamps Y if it is near black.
knownY = if (knownY > SA98G.blkThrs) then knownY else knownY + math.pow(SA98G.blkThrs - knownY, SA98G.blkClmp)
-- set the known and unknown exponents
if knownType == "bg" or knownType == "background" then
knownExp = if contrast > 0 then SA98G.normBG else SA98G.revBG
unknownExp = if contrast > 0 then SA98G.normTXT else SA98G.revTXT
unknownY = math.pow(math.pow(knownY, knownExp) - contrast, 1 / unknownExp)
if isNaN(unknownY) then
return false
end
elseif knownType == "txt" or knownType == "text" then
knownExp = if contrast > 0 then SA98G.normTXT else SA98G.revTXT
unknownExp = if contrast > 0 then SA98G.normBG else SA98G.revBG
unknownY = math.pow(contrast + math.pow(knownY, knownExp), 1 / unknownExp)
if isNaN(unknownY) then
return false
end
else
return false
end
--return contrast +'----'+unknownY;
if unknownY > 1.06 or unknownY < 0 then
return false
end
-- if (unknownY < 0) { return false } // return false on underflow
--unknownY = math.max(unknownY,0.0);
-- unclamp
unknownY = if (unknownY > SA98G.blkThrs) then unknownY else (math.pow(((unknownY + SA98G.mOffsetIn) * SA98G.mFactor), SA98G.mExp) * SA98G.mFactInv) - SA98G.mOffsetOut
-- unknownY - 0.22 * math.pow(unknownY*0.5, 1/blkClmp);
unknownY = math.max(math.min(unknownY, 1.0), 0.0)
if returnAs == "color" then
local colorB = math.round(math.pow(unknownY, SA98G.mainTRCencode) * 255)
local retUse = if (knownType == "bg") then "txtColor" else "bgColor"
return { colorB, colorB, colorB, 1, retUse }
elseif returnAs == "Y" or returnAs == "y" then
return math.max(0.0, unknownY)
else
return false
end
end
local function sRGBtoY(sRGBcolor: Color3)
local r = sRGBcolor.R
local g = sRGBcolor.G
local b = sRGBcolor.B
local function simpleExp(chan)
return math.pow(chan, SA98G.mainTRC)
end
return SA98G.sRco * simpleExp(r) + SA98G.sGco * simpleExp(g) + SA98G.sBco * simpleExp(b)
end
local function displayP3toY(rgb: Color3)
local mainTRC = 2.4
local sRco, sGco, sBco = 0.2289829594805780, 0.6917492625852380, 0.0792677779341829
local function simpleExp(chan)
return math.pow(chan, mainTRC)
end
return sRco * simpleExp(rgb.R) + sGco * simpleExp(rgb.G) + sBco * simpleExp(rgb.B)
end
local function adobeRGBtoY(rgb: Color3)
local mainTRC = 2.35
local sRco = 0.2973550227113810
local sGco = 0.6273727497145280
local sBco = 0.0752722275740913
local function simpleExp(chan)
return math.pow(chan / 255.0, mainTRC)
end
return sRco * simpleExp(rgb.R) + sGco * simpleExp(rgb.G) + sBco * simpleExp(rgb.B)
end
local function APCAcontrast(txtY, bgY, places)
places = places or -1
local icp = { 0, 1.1 }
if math.min(txtY, bgY) < icp[1] or math.max(txtY, bgY) > icp[2] then
return 0
end
local SAPC = 0
local outputContrast = 0
local polCat = "BoW"
txtY = (txtY > SA98G.blkThrs) and txtY or txtY + math.pow(SA98G.blkThrs - txtY, SA98G.blkClmp)
bgY = (bgY > SA98G.blkThrs) and bgY or bgY + math.pow(SA98G.blkThrs - bgY, SA98G.blkClmp)
if math.abs(bgY - txtY) < SA98G.deltaYmin then
return 0
end
if bgY > txtY then -- black text on white
SAPC = (math.pow(bgY, SA98G.normBG) - math.pow(txtY, SA98G.normTXT)) * SA98G.scaleBoW
outputContrast = (SAPC < SA98G.loClip) and 0.0 or SAPC - SA98G.loBoWoffset
else
-- should always return negative
polCat = "WoB" -- white on black
SAPC = (math.pow(bgY, SA98G.revBG) - math.pow(txtY, SA98G.revTXT)) * SA98G.scaleWoB
outputContrast = (SAPC > -SA98G.loClip) and 0.0 or SAPC + SA98G.loWoBoffset
end
if places < 0 then
return outputContrast * 100.0
elseif places == 0 then
return math.round(math.abs(outputContrast) * 100.0) --+ "<sub>" + polCat + "</sub>" -- why is there html
elseif places // 1 == places then
return (outputContrast * 100.0) * places // 1 / places
else
return 0.0
end
end
local function alphaBlend(rgbFG: Color3, aFG: number, rgbBG: Color3, round: boolean?)
round = if round == nil then true else round
aFG = aFG or 1
local compBlend = 1 - aFG
local rgbOut = {0, 0, 0}
rgbOut[1] = rgbBG.R * compBlend + rgbFG.R * aFG
if round then rgbOut[1] = math.min(math.round(rgbOut[1]), 255) end
rgbOut[2] = rgbBG.G * compBlend + rgbFG.G * aFG
if round then rgbOut[2] = math.min(math.round(rgbOut[2]), 255) end
rgbOut[3] = rgbBG.B * compBlend + rgbFG.B * aFG
if round then rgbOut[3] = math.min(math.round(rgbOut[3]), 255) end
return Color3.new(rgbOut[1], rgbOut[2], rgbOut[3])
end
local function calcAPCA(textcolor: Color3, bgColor: Color3, textalpha: number?, places: number?, round: boolean?)
places = -1
--todo: alpha blending
if textalpha then textcolor = alphaBlend(textcolor, textalpha, bgColor, round) end
return APCAcontrast(sRGBtoY(textcolor), sRGBtoY(bgColor), places)
end
local function fontLookupAPCA(contrast, places: number?)
places = places or 2
-- Font size interpolations. Here the chart was re-ordered to put
-- the main contrast levels each on one line, instead of font size per line.
-- First column is LC value, then each following column is font size by weight
-- G G G G G G Public Beta 0.1.7 (G) • MAY 28 2022
-- Lc values under 70 should have Lc 15 ADDED if used for body text
-- All font sizes are in px and reference font is Barlow
-- 999: prohibited - too low contrast
-- 777: NON TEXT at this minimum weight stroke
-- 666 - this is for spot text, not fluent-Things like copyright or placeholder.
-- 5xx - minimum font at this weight for content, 5xx % 500 for font-size
-- 4xx - minimum font at this weight for any purpose], 4xx % 400 for font-size
-- MAIN FONT SIZE LOOKUP
---- ASCENDING SORTED Public Beta 0.1.7 (G) • MAY 28 2022 ////
---- Lc 45 * 0.2 = 9 which is the index for the row for Lc 45
-- MAIN FONT LOOKUP May 28 2022 EXPANDED
-- Sorted by Lc Value
-- First row is standard weights 100-900
-- First column is font size in px
-- All other values are the Lc contrast
-- 999 = too low. 777 = non-text and spot text only
local fontMatrixAscend = {
{'Lc',100,200,300,400,500,600,700,800,900},
{0,999,999,999,999,999,999,999,999,999},
{10,999,999,999,999,999,999,999,999,999},
{15,777,777,777,777,777,777,777,777,777},
{20,777,777,777,777,777,777,777,777,777},
{25,777,777,777,120,120,108,96,96,96},
{30,777,777,120,108,108,96,72,72,72},
{35,777,120,108,96,72,60,48,48,48},
{40,120,108,96,60,48,42,32,32,32},
{45,108,96,72,42,32,28,24,24,24},
{50,96,72,60,32,28,24,21,21,21},
{55,80,60,48,28,24,21,18,18,18},
{60,72,48,42,24,21,18,16,16,18},
{65,68,46,32,21.75,19,17,15,16,18},
{70,64,44,28,19.5,18,16,14.5,16,18},
{75,60,42,24,18,16,15,14,16,18},
{80,56,38.25,23,17.25,15.81,14.81,14,16,18},
{85,52,34.5,22,16.5,15.625,14.625,14,16,18},
{90,48,32,21,16,15.5,14.5,14,16,18},
{95,45,28,19.5,15.5,15,14,13.5,16,18},
{100,42,26.5,18.5,15,14.5,13.5,13,16,18},
{105,39,25,18,14.5,14,13,12,16,18},
{110,36,24,18,14,13,12,11,16,18},
{115,34.5,22.5,17.25,12.5,11.875,11.25,10.625,14.5,16.5},
{120,33,21,16.5,11,10.75,10.5,10.25,13,15},
{125,32,20,16,10,10,10,10,12,14},
}
local fontDeltaAscend = {
{'∆Lc',100,200,300,400,500,600,700,800,900},
{0,0,0,0,0,0,0,0,0,0},
{10,0,0,0,0,0,0,0,0,0},
{15,0,0,0,0,0,0,0,0,0},
{20,0,0,0,0,0,0,0,0,0},
{25,0,0,0,12,12,12,24,24,24},
{30,0,0,12,12,36,36,24,24,24},
{35,0,12,12,36,24,18,16,16,16},
{40,12,12,24,18,16,14,8,8,8},
{45,12,24,12,10,4,4,3,3,3},
{50,16,12,12,4,4,3,3,3,3},
{55,8,12,6,4,3,3,2,2,0},
{60,4,2,10,2.25,2,1,1,0,0},
{65,4,2,4,2.25,1,1,0.5,0,0},
{70,4,2,4,1.5,2,1,0.5,0,0},
{75,4,3.75,1,0.75,0.188,0.188,0,0,0},
{80,4,3.75,1,0.75,0.188,0.188,0,0,0},
{85,4,2.5,1,0.5,0.125,0.125,0,0,0},
{90,3,4,1.5,0.5,0.5,0.5,0.5,0,0},
{95,3,1.5,1,0.5,0.5,0.5,0.5,0,0},
{100,3,1.5,0.5,0.5,0.5,0.5,1,0,0},
{105,3,1,0,0.5,1,1,1,0,0},
{110,1.5,1.5,0.75,1.5,1.125,0.75,0.375,1.5,1.5},
{115,1.5,1.5,0.75,1.5,1.125,0.75,0.375,1.5,1.5},
{120,1,1,0.5,1,0.75,0.5,0.25,1,1},
{125,0,0,0,0,0,0,0,0,0},
};
local weightArray = {0, 100, 200, 300, 400, 500, 600, 700, 800, 900}
local weightArrayLen = #weightArray
local returnArray = {tostring(contrast * places // 1 / places), 0, 0, 0, 0, 0, 0, 0, 0, 0}
local returnArrayLen = #returnArray
local contrastArrayAscend = {'lc',0,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100,105,110,115,120,125,}
local contrastArrayLenAsc = #contrastArrayAscend
-- Lc 45 * 0.2 = 9 and 9 is the index for the row for lc 45
local tempFont = 777
local contrast = math.abs(contrast)
local factor = 0.2
local index = contrast == 0 and 1 or bit32.bor(contrast * factor, 0)
local w = 0
local scoreAdj = (contrast - fontMatrixAscend[index + 1][w + 1]) * factor
w += 1
while w < weightArrayLen do
w += 1
tempFont = fontMatrixAscend[index+1][w+1]
if tempFont > 400 then
returnArray[w + 1] = tempFont
elseif contrast < 14.5 then
returnArray[w + 1] = 999
elseif contrast < 29.5 then
returnArray[w + 1] = 777
else
--- interpolation of font size
if tempFont > 24 then
returnArray[w + 1] = math.round(tempFont - (fontDeltaAscend[index + 1][w + 1] * scoreAdj))
else
returnArray[w + 1] = tempFont - ((2 * fontDeltaAscend[index + 1][w + 1] * scoreAdj // 1) * 0.5)
end
end
end
return returnArray
end
return {
APCAcontrast = APCAcontrast,
reverseAPCA = reverseAPCA,
calcAPCA = calcAPCA,
fontLookupAPCA = fontLookupAPCA,
sRGBtoY = sRGBtoY,
displayP3toY = displayP3toY,
adobeRGBtoY = adobeRGBtoY,
alphaBlend = alphaBlend
}

View file

@ -0,0 +1,62 @@
local vide = require(script.Parent.Parent.Parent.vide)
local action = vide.action
local cleanup = vide.cleanup
--[[
Cascades are applied and passed through an instance.
Use a context if you need to pass this through function scopes.
This is primarily useful for passing down theme information.
]]
local function cascade<T>(default_value: T)
local self = {}
local senders: {[Instance]: T} = {}
local function get_cascaded_value(from: Instance?): T
while from ~= nil do
local sender = senders[from]
if sender ~= nil then
return sender
else
from = from.Parent
end
end
return default_value
end
function self.send(value: T)
return action(function(instance)
cleanup(function()
senders[instance] = nil
end)
senders[instance] = value
end)
end
function self.receive(output_to: (T) -> any)
return action(function(instance)
local function recalculate()
output_to(
get_cascaded_value(instance.Parent)
)
end
recalculate()
cleanup(instance.AncestryChanged:Connect(recalculate))
cleanup(function()
output_to(default_value)
end)
end)
end
return self
end
return cascade

View file

@ -0,0 +1,57 @@
--[[
Implements a form of dependency injection to save the need from passing data
as props through intermediate components.
]]
export type Context<T = nil> = {
default_value: T,
_values: {[thread]: T},
provide: <U>(Context<T>, callback: () -> U) -> (new: T) -> U,
consume: (Context<T>) -> T
}
type ContextNoDefault<T> = {
_values: {[thread]: T},
provide: <U>(Context<T>, callback: () -> U) -> (new: T) -> U,
consume: (Context<T>) -> T?
}
local function provide<T, U>(context: Context<T>, callback: () -> U)
return function(new: T): U
local thread = coroutine.running()
local old = context._values[thread]
context._values[thread] = new
local ok, value = pcall(callback)
context._values[thread] = old
if not ok then
error(`provided callback errored with "{value}"`, 2)
end
return value
end :: (new: T) -> U
end
local function consume<T>(context: Context<T>): T
local thread = coroutine.running()
return context._values[thread] or context.default_value
end
local function create_context<T>(default_value: T?): Context<T>
return {
default_value = default_value,
_values = {},
provide = provide :: any,
consume = consume
}
end
return create_context :: (<T>(default_value: T) -> Context<T>) & (<T>() -> ContextNoDefault<T>)

View file

@ -0,0 +1,26 @@
local RunService = game:GetService("RunService")
local vide = require(script.Parent.Parent.Parent.vide)
local source = vide.source
local effect = vide.effect
local cleanup = vide.cleanup
return function<T>(delay: number, input: () -> T): () -> T
local output = source(input())
effect(function()
local v = input()
local t = delay
cleanup(RunService.Heartbeat:Connect(function(dt)
t -= dt
if t > 0 then return end
output(v)
end))
end)
return output
end

View file

@ -0,0 +1,434 @@
--!nolint
--!strict
-- Oklab C implementation provided by Björn Ottosson:
-- https://bottosson.github.io/posts/gamutclipping/
-- Luau port and Roblox/Lch extensions by Elttob:
-- https://elttob.uk/
-- Licensed under MIT
local TAU = 2 * math.pi
local function cbrt(x: number)
return math.sign(x) * math.abs(x) ^ (1/3)
end
local Oklab = {}
function Oklab.linear_srgb_to_oklab(
c: Vector3
): Vector3
local l = 0.4122214708 * c.X + 0.5363325363 * c.Y + 0.0514459929 * c.Z
local m = 0.2119034982 * c.X + 0.6806995451 * c.Y + 0.1073969566 * c.Z
local s = 0.0883024619 * c.X + 0.2817188376 * c.Y + 0.6299787005 * c.Z
local l_ = cbrt(l)
local m_ = cbrt(m)
local s_ = cbrt(s)
return Vector3.new(
0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_
)
end
function Oklab.oklab_to_linear_srgb(
c: Vector3
): Vector3
local l_ = c.X + 0.3963377774 * c.Y + 0.2158037573 * c.Z
local m_ = c.X - 0.1055613458 * c.Y - 0.0638541728 * c.Z
local s_ = c.X - 0.0894841775 * c.Y - 1.2914855480 * c.Z
local l = l_ * l_ * l_
local m = m_ * m_ * m_
local s = s_ * s_ * s_
return Vector3.new(
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
)
end
-- Finds the maximum saturation possible for a given hue that fits in sRGB.
-- Saturation here is defined as S = C/L
-- a and b must be normalised so a^2 + b^2 == 1
function Oklab.compute_max_saturation(
a: number,
b: number
): number
-- Max saturation will be when one of r, g or b goes below zero.
-- Select different coefficients depending on which component goes below zero first
local k0, k1, k2, k3, k4, wl, wm, ws
if -1.88170328 * a - 0.80936493 * b > 1 then
-- Red component
k0, k1, k2, k3, k4 = 1.19086277, 1.76576728, 0.59662641, 0.75515197, 0.56771245
wl, wm, ws = 4.0767416621, -3.3077115913, 0.2309699292
elseif 1.81444104 * a - 1.19445276 * b > 1 then
-- Green component
k0, k1, k2, k3, k4 = 0.73956515, -0.45954404, 0.08285427, 0.12541070, 0.14503204
wl, wm, ws = -1.2684380046, 2.6097574011, -0.3413193965
else
-- Blue component
k0, k1, k2, k3, k4 = 1.35733652, -0.00915799, -1.15130210, -0.50559606, 0.00692167
wl, wm, ws = -0.0041960863, -0.7034186147, 1.7076147010
end
-- Approximate max saturation using a polynomial
local S = k0 + k1 * a + k2 * b + k3 * a * a + k4 * a * b
-- Do one step Halley's method to get closer
-- this gives an error less than 10e6, except for some blue hues where the dS/dh is close to infinite
-- this should be sufficient for most applications, otherwise do two/three steps
local k_l = 0.3963377774 * a + 0.2158037573 * b
local k_m = -0.1055613458 * a - 0.0638541728 * b
local k_s = -0.0894841775 * a - 1.2914855480 * b
do
local l_ = 1 + S * k_l
local m_ = 1 + S * k_m
local s_ = 1 + S * k_s
local l = l_ * l_ * l_
local m = m_ * m_ * m_
local s = s_ * s_ * s_
local l_dS = 3 * k_l * l_ * l_
local m_dS = 3 * k_m * m_ * m_
local s_dS = 3 * k_s * s_ * s_
local l_dS2 = 6 * k_l * k_l * l_
local m_dS2 = 6 * k_m * k_m * m_
local s_dS2 = 6 * k_s * k_s * s_
local f = wl * l + wm * m + ws * s
local f1 = wl * l_dS + wm * m_dS + ws * s_dS
local f2 = wl * l_dS2 + wm * m_dS2 + ws * s_dS2
S = S - f * f1 / (f1*f1 - 0.5 * f * f2)
end
return S
end
-- finds L_cusp and C_cusp for a given hue
-- a and b must be normalised so a^2 + b^2 == 1
function Oklab.find_cusp(
a: number,
b: number
): (number, number)
-- First, find the maximum saturation (saturation S = C/L)
local S_cusp = Oklab.compute_max_saturation(a, b)
-- Convert to linear sRGB to find the first point where at least one of r,g or b >= 1:
local rgb_at_max = Oklab.oklab_to_linear_srgb(Vector3.new(1, S_cusp * a, S_cusp * b))
local L_cusp = cbrt(1 / math.max(rgb_at_max.X, rgb_at_max.Y, rgb_at_max.Z))
local C_cusp = L_cusp * S_cusp
return L_cusp, C_cusp
end
-- Finds intersection of the line defined by
-- L = L0 * (1 - t) + t * L1;
-- C = t * C1;
-- a and b must be normalized so a^2 + b^2 == 1
function Oklab.find_gamut_intersection(
a: number,
b: number,
L1: number,
C1: number,
L0: number
): number
-- Find the cusp of the gamut triangle
local L_cusp, C_cusp = Oklab.find_cusp(a, b)
-- Find the intersection for upper and lower half seprately
local t
if ((L1 - L0) * C_cusp - (L_cusp - L0) * C1) <= 0 then
-- Lower half
t = C_cusp * L0 / (C1 * L_cusp + C_cusp * (L0 - L1))
else
-- Upper half
-- First intersect with triangle
t = C_cusp * (L0 - 1) / (C1 * (L_cusp - 1) + C_cusp * (L0 - L1))
-- Then one step Halley's method
do
local dL = L1 - L0
local dC = C1
local k_l = 0.3963377774 * a + 0.2158037573 * b
local k_m = -0.1055613458 * a - 0.0638541728 * b
local k_s = -0.0894841775 * a - 1.2914855480 * b
local l_dt = dL + dC * k_l
local m_dt = dL + dC * k_m
local s_dt = dL + dC * k_s
-- If higher accuracy is required, 2 or 3 iterations of the following block can be used:
do
local L = L0 * (1 - t) + t * L1
local C = t * C1
local l_ = L + C * k_l
local m_ = L + C * k_m
local s_ = L + C * k_s
local l = l_ * l_ * l_
local m = m_ * m_ * m_
local s = s_ * s_ * s_
local ldt = 3 * l_dt * l_ * l_
local mdt = 3 * m_dt * m_ * m_
local sdt = 3 * s_dt * s_ * s_
local ldt2 = 6 * l_dt * l_dt * l_
local mdt2 = 6 * m_dt * m_dt * m_
local sdt2 = 6 * s_dt * s_dt * s_
local r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s - 1
local r1 = 4.0767416621 * ldt - 3.3077115913 * mdt + 0.2309699292 * sdt
local r2 = 4.0767416621 * ldt2 - 3.3077115913 * mdt2 + 0.2309699292 * sdt2
local u_r = r1 / (r1 * r1 - 0.5 * r * r2)
local t_r = -r * u_r
local g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s - 1
local g1 = -1.2684380046 * ldt + 2.6097574011 * mdt - 0.3413193965 * sdt
local g2 = -1.2684380046 * ldt2 + 2.6097574011 * mdt2 - 0.3413193965 * sdt2
local u_g = g1 / (g1 * g1 - 0.5 * g * g2)
local t_g = -g * u_g
local b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s - 1
local b1 = -0.0041960863 * ldt - 0.7034186147 * mdt + 1.7076147010 * sdt
local b2 = -0.0041960863 * ldt2 - 0.7034186147 * mdt2 + 1.7076147010 * sdt2
local u_b = b1 / (b1 * b1 - 0.5 * b * b2)
local t_b = -b * u_b
t_r = if u_r >= 0 then t_r else math.huge
t_g = if u_g >= 0 then t_g else math.huge
t_b = if u_b >= 0 then t_b else math.huge
t += math.min(t_r, t_g, t_b)
end
end
end
return t
end
function Oklab.gamut_clip_preserve_chroma(
rgb: Vector3
): Vector3
if rgb.X <= 1 and rgb.Y <= 1 and rgb.Z <= 1 and rgb.X >= 0 and rgb.Y >= 0 and rgb.Z >= 0 then
return rgb
end
local lab = Oklab.linear_srgb_to_oklab(rgb)
local L = lab.X
local eps = 0.00001
local C = math.max(eps, math.sqrt(lab.Y * lab.Y + lab.Z * lab.Z))
local a_ = if C == 0 then 0 else lab.Y / C
local b_ = if C == 0 then 0 else lab.Z / C
local L0 = math.clamp(L, 0, 1)
local t = Oklab.find_gamut_intersection(a_, b_, L, C, L0)
local L_clipped = L0 * (1 - t) + t * L
local C_clipped = t * C
return Oklab.oklab_to_linear_srgb(Vector3.new(L_clipped, C_clipped * a_, C_clipped * b_))
end
function Oklab.gamut_clip_project_to_0_5(
rgb: Vector3
): Vector3
if rgb.X <= 1 and rgb.Y <= 1 and rgb.Z <= 1 and rgb.X >= 0 and rgb.Y >= 0 and rgb.Z >= 0 then
return rgb
end
local lab = Oklab.linear_srgb_to_oklab(rgb)
local L = lab.X
local eps = 0.00001
local C = math.max(eps, math.sqrt(lab.Y * lab.Y + lab.Z * lab.Z))
local a_ = lab.Y / C
local b_ = lab.Z / C
local L0 = 0.5
local t = Oklab.find_gamut_intersection(a_, b_, L, C, L0)
local L_clipped = L0 * (1 - t) + t * L
local C_clipped = t * C
return Oklab.oklab_to_linear_srgb(Vector3.new(L_clipped, C_clipped * a_, C_clipped * b_))
end
function Oklab.gamut_clip_project_to_L_cusp(
rgb: Vector3
): Vector3
if rgb.X <= 1 and rgb.Y <= 1 and rgb.Z <= 1 and rgb.X >= 0 and rgb.Y >= 0 and rgb.Z >= 0 then
return rgb
end
local lab = Oklab.linear_srgb_to_oklab(rgb)
local L = lab.X
local eps = 0.00001
local C = math.max(eps, math.sqrt(lab.Y * lab.Y + lab.Z * lab.Z))
local a_ = lab.Y / C
local b_ = lab.Z / C
-- The cusp is computed here and in find_gamut_intersection, an optimised solution would only compute it once.
local L_cusp, C_cusp = Oklab.find_cusp(a_, b_)
local L0 = L_cusp
local t = Oklab.find_gamut_intersection(a_, b_, L, C, L0)
local L_clipped = L0 * (1 - t) + t * L
local C_clipped = t * C
return Oklab.oklab_to_linear_srgb(Vector3.new(L_clipped, C_clipped * a_, C_clipped * b_))
end
function Oklab.gamut_clip_adaptive_L0_0_5(
rgb: Vector3,
alpha: number?
): Vector3
if rgb.X <= 1 and rgb.Y <= 1 and rgb.Z <= 1 and rgb.X >= 0 and rgb.Y >= 0 and rgb.Z >= 0 then
return rgb
end
local alpha = alpha or 0.05
local lab = Oklab.linear_srgb_to_oklab(rgb)
local L = lab.X
local eps = 0.00001
local C = math.max(eps, math.sqrt(lab.Y * lab.Y + lab.Z * lab.Z))
local a_ = lab.Y / C
local b_ = lab.Z / C
local Ld = L - 0.5
local e1 = 0.5 + math.abs(Ld) + alpha * C
local L0 = 0.5 * (1 + math.sign(Ld) * (e1 - math.sqrt(e1*e1 - 2 * math.abs(Ld))))
local t = Oklab.find_gamut_intersection(a_, b_, L, C, L0)
local L_clipped = L0 * (1 - t) + t * L
local C_clipped = t * C
return Oklab.oklab_to_linear_srgb(Vector3.new(L_clipped, C_clipped * a_, C_clipped * b_))
end
function Oklab.gamut_clip_adaptive_L0_L_cusp(
rgb: Vector3,
alpha: number?
): Vector3
if rgb.X < 1 and rgb.Y < 1 and rgb.Z < 1 and rgb.X > 0 and rgb.Y > 0 and rgb.Z > 0 then
return rgb
end
local alpha = alpha or 0.05
local lab = Oklab.linear_srgb_to_oklab(rgb)
local L = lab.X
local eps = 0.00001
local C = math.max(eps, math.sqrt(lab.Y * lab.Y + lab.Z * lab.Z))
local a_ = lab.Y / C
local b_ = lab.Z / C
-- The cusp is computed here and in find_gamut_intersection, an optimized solution would only compute it once.
local L_cusp, C_cusp = Oklab.find_cusp(a_, b_)
local Ld = L - L_cusp
local k = 2 * (if Ld > 0 then 1 - L_cusp else L_cusp)
local e1 = 0.5*k + math.abs(Ld) + alpha * C/k
local L0 = L_cusp + 0.5 * (math.sign(Ld) * (e1 - math.sqrt(e1 * e1 - 2 * k * math.abs(Ld))))
local t = Oklab.find_gamut_intersection(a_, b_, L, C, L0)
local L_clipped = L0 * (1 - t) + t * L
local C_clipped = t * C
return Oklab.oklab_to_linear_srgb(Vector3.new(L_clipped, C_clipped * a_, C_clipped * b_))
end
--[[
ROBLOX EXTENSIONS
]]
Oklab.default_gamut_clip = Oklab.gamut_clip_adaptive_L0_0_5
local function component_to_gamma(x: number): number
if x >= 0.0031308 then
return (1.055) * x^(1.0/2.4) - 0.055
else
return 12.92 * x
end
end
local function component_to_linear(x: number): number
if x >= 0.04045 then
return ((x + 0.055)/(1 + 0.055))^2.4
else
return x / 12.92
end
end
function Oklab.color3_to_linear_srgb(
c: Color3
): Vector3
return Vector3.new(
component_to_linear(c.R),
component_to_linear(c.G),
component_to_linear(c.B)
)
end
function Oklab.linear_srgb_to_color3(
c: Vector3,
use_default_gamut_clip: boolean?
): Color3
if use_default_gamut_clip == false then
return Color3.new(
component_to_gamma(c.X),
component_to_gamma(c.Y),
component_to_gamma(c.Z)
)
else
local c = Oklab.default_gamut_clip(c)
return Color3.new(
math.clamp(component_to_gamma(c.X), 0, 1),
math.clamp(component_to_gamma(c.Y), 0, 1),
math.clamp(component_to_gamma(c.Z), 0, 1)
)
end
end
--[[
LCH EXTENSIONS
]]
function Oklab.oklch_to_oklab(oklch: Vector3): Vector3
return Vector3.new(
oklch.X,
oklch.Y * math.cos(oklch.Z * TAU),
oklch.Y * math.sin(oklch.Z * TAU)
)
end
function Oklab.oklab_to_oklch(oklab: Vector3): Vector3
return Vector3.new(
oklab.X,
math.sqrt(oklab.Y^2 + oklab.Z^2),
(math.atan2(oklab.Z, oklab.Y) / TAU) % 1
)
end
return Oklab

View file

@ -0,0 +1,101 @@
--------------------------------------------------------------------------------
-- videx/store.luau
--------------------------------------------------------------------------------
local vide = require(script.Parent.Parent.Parent.vide)
local source = vide.source
local NULL = newproxy()
local Store = {}
--[=[
Creates a new store object that receives some initial state and then returns
a table with the same structure, but all keys of the given table will be reactive.
When accessed inside a reactive scope, the reactive scope will update whenever
the key that is accessed is changed.
@param initial_state `T : {[string]: any}` The initial state the store will start in.
@param mutations `() -> {[string]: (T, ...any) -> ...any}?` A list of functions that mutate the data.
@return `T & U` A resulting table that
]=]
function Store.new<T, U>(
initial_state: T & {},
mutations: (T & U) -> U
): T & U
local sources = {}
for i, v in initial_state :: any do
local src = source(v ~= NULL and v or nil)
sources[i] = src
end
local internal_proxy = {}
setmetatable(internal_proxy, {
__index = function(_, index)
return sources[index]()
end,
__newindex = function(_, index, value)
sources[index](value)
end
})
local external_proxy = {}
setmetatable(external_proxy :: any, {
__index = function(_, index)
local src = sources[index]
if src == nil then error(`invalid index {index}`, 2) end
return src()
end,
__newindex = function(_, index, value)
sources[index](value)
end
})
for i, v in next, mutations(internal_proxy :: any) :: any do
if rawget(external_proxy, i) then
error(`duplicate field "{i}"`, 2)
end
rawset(external_proxy, i, v)
end
return external_proxy :: T & U & {}
end
--[=[
Creates a new store object that receives some initial state and then returns
a table with the same structure, but all keys of the given table will be reactive.
When accessed inside a reactive scope, the reactive scope will update whenever
the key that is accessed is changed.
@param initial_state `T : {[string]: any}` The initial state the store will start in.
@param mutations `() -> {[string]: (T, ...any) -> ...any}?` A list of functions that mutate the data.
@return `T & U` A resulting table that
]=]
function Store.new_deep<T, U>(
initial_state: T & {},
mutations: (T & U) -> U
): T & U
local main = Store.new(initial_state, mutations)
for key, value in initial_state :: any do
if type(value) == "table" then
main[key] = Store.new_deep(value, mutations :: any)
end
end
return main
end
--- A special symbol used to indicate that a value should be nil within a Store.
Store.null = NULL :: nil
return Store

View file

@ -0,0 +1,27 @@
local vide = require(script.Parent.Parent.Parent.vide)
local reduced_motion = require(script.Parent.reduced_motion)
local source = vide.source
local spring = vide.spring
local untrack = vide.untrack
type Animation = "move" | ""
return function<T>(s: () -> T)
local type = typeof(untrack(s))
local is_movement = false
local reduce = reduced_motion:consume()
if type == "UDim" or type == "UDim2" or type == "Vector2" then
is_movement = true
end
local spr = spring(s, 0.1)
return function()
if is_movement and reduce then
return s()
else
return spr()
end
end
end

View file

View file

@ -0,0 +1,10 @@
--[[
Consumes a property from a table and removes it
]]
return function(t: {}, key: string)
local v = t[key]; t[key] = nil
return v
end

View file

@ -0,0 +1,155 @@
local vide = require(script.Parent.Parent.Parent.vide)
local apcaw3 = require(script.Parent.Parent.libraries.apcaw3)
local oklch = require(script.Parent.oklch)
local derive = vide.derive
local read = vide.read
local function min_contrast(options: {
size: can<number>,
weight: can<number | Font>?,
body: can<boolean>?
})
local size = options.size
local weight = options.weight or 400
local needs_body = options.body
local NO = math.huge
if not size and not weight then return 0 end
local MIN_CONTRAST = {
[12] = {NO, NO, NO, NO, NO, NO, NO, NO, NO},
[14] = {NO, NO, NO, 100, 100, 90, 75, NO, NO},
[15] = {NO, NO, NO, 100, 90, 75, 70, NO, NO},
[16] = {NO, NO, NO, 90, 75, 70, 60, 60, NO},
[18] = {NO, NO, 100, 75, 70, 60, 55, 55, 55},
[21] = {NO, NO, 90, 70, 60, 55, 50, 50, 50},
[24] = {NO, NO, 75, 60, 55, 50, 45, 45, 55},
[28] = {NO, 100, 70, 55, 50, 45, 43, 43, 43},
[32] = {NO, 90, 65, 50, 45, 43, 40, 40, 40},
[36] = {NO, 75, 60, 45, 43, 40, 38, 38, 38},
[42] = {100, 70, 55, 43, 40, 38, 35, 35, 35},
[48] = {90, 60, 50, 40, 38, 35, 33, 33, 33},
[60] = {75, 55, 45, 37, 35, 33, 30 ,30, 30},
[72] = {60, 50, 40, 35, 33, 30, 30, 30, 30},
[96] = {50, 45, 35, 33, 30, 30, 30, 30, 30}
}
local MIN_CONTRAST_BODY = {
[12] = {NO, NO, NO, NO, NO, NO, NO, NO, NO},
[14] = {NO, NO, NO, 100, 100, 90, 75, NO, NO},
[15] = {NO, NO, NO, 100, 90, 90, 85, NO, NO},
[16] = {NO, NO, NO, 90, 75, 85, 75, NO, NO},
[18] = {NO, NO, 100, 75, 85, 75, 70, NO, NO},
[21] = {NO, NO, 90, 70, 75, 70, 65, NO, NO},
[24] = {NO, NO, 75, 75, 70, 65, 60, NO, NO},
[28] = {NO, NO, 85, 70, 65, 60, 58, NO, NO},
[32] = {NO, NO, 80, 65, 60, 58, 55, NO, NO},
[36] = {NO, NO, 75, 60, 58, 55, 52, NO, NO},
[42] = {NO, NO, NO, NO, NO, NO, NO, NO, NO},
[48] = {NO, NO, NO, NO, NO, NO, NO, NO, NO},
[60] = {NO, NO, NO, NO, NO, NO, NO, NO, NO},
[72] = {NO, NO, NO, NO, NO, NO, NO, NO, NO},
[96] = {NO, NO, NO, NO, NO, NO, NO, NO, NO}
}
return derive(function()
local matrix_to_use =
if read(needs_body) then MIN_CONTRAST_BODY
else MIN_CONTRAST
local row_to_use = matrix_to_use[read(size)]
if not row_to_use then return NO end
local weight: Font | number = read(weight)
local font_weight: number = 400
if type(weight) == "number" then
font_weight = weight
elseif typeof(weight) == "Font" then
font_weight = weight.Weight.Value
end
return row_to_use[font_weight // 100] or NO
end)
end
--- collapses a state to a single value
type recursive<T> = (() -> recursive<T>) | T
type can<T> = (() -> T) | T
local function unwrap<T>(source: recursive<T>): T
local value: recursive<T>
while type(source) == "function" do
source = source()
end
value = source
return value :: T
end
local function get_appropriate_color(options: {
background: recursive<Color3>?,
foreground: recursive<{{number}}>,
elevation: recursive<number>?,
min_contrast: recursive<number>,
}): () -> Color3
return function()
local min_contrast = unwrap(options.min_contrast)
local elevation = unwrap(options.elevation) or 0
local bg = unwrap(options.background)
if bg == nil then
local foreground = unwrap(options.foreground)[1]
local l, c, h = unpack(foreground, 1, 3)
local l_a, l_c, l_h = unpack(foreground, 4, 6)
local color = oklch(
l + elevation * (l_a or 0),
c + elevation * (l_c or 0),
h + elevation * (l_h or 0)
)
return color
end
if min_contrast == math.huge then
warn("min contrast is invalid")
end
local max = -1
local furthest_away = Color3.new()
for _, foreground: {number} in unwrap(options.foreground) do
local l, c, h = unpack(foreground, 1, 3)
local l_a, l_c, l_h = unpack(foreground, 4, 6)
local color = oklch(
l + elevation * (l_a or 0),
c + elevation * (l_c or 0),
h + elevation * (l_h or 0)
)
local contrast = math.abs(apcaw3.calcAPCA(color, bg, nil, 1, true))
if max < contrast then
max = contrast
furthest_away = color
end
if contrast >= min_contrast then
return color
end
end
-- warn(`unable to find a color, max contrast found is {max // 1} but needs at least {min_contrast}`)
return furthest_away
end
end
return {
min_contrast = min_contrast,
get_appropriate_color = get_appropriate_color
}

View file

@ -0,0 +1,28 @@
local oklab = require(script.Parent.Parent.libraries.oklab)
--[=[
Converts OkLCh into a Color3.
lightness is a value between 0-1, determining how "light" a color is.
chroma is a value between 0 to infinity, determining how colorful something is.
current displays can only display a chroma up to around 0.34, and srgb can only
go up to 0.245.
hue is a hue circle from 0-360
]=]
local function oklch(lightness: number, chroma: number, hue: number)
return oklab.linear_srgb_to_color3(
oklab.oklab_to_linear_srgb(
oklab.oklch_to_oklab(
Vector3.new(math.clamp(lightness, 0, 1), chroma, hue)
)
),
true
)
end
return oklch

View file

@ -0,0 +1,3 @@
local context = require(script.Parent.Parent.libraries.context)
return context(false)

View file

@ -0,0 +1,253 @@
--[[
simple theme engine for computing colors
]]
local vide = require(script.Parent.Parent.Parent.vide)
local apcaw3 = require(script.Parent.Parent.libraries.apcaw3)
local contrast = require(script.Parent.contrast)
local oklch_to_color3 = require(script.Parent.oklch)
local source = vide.source
local function oklch(t: {number})
return table.freeze {
l = t[1],
c = t[2],
h = t[3],
color = oklch_to_color3(unpack(t))
}
end
local background = source(oklch {0.2012, 0.01, 0.6}) -- note: for light mode change this to 0.9012 from 0.3012
local accent = source(oklch {0.52, 0.21, 0.71}) -- note: for light mode change this to 0.62 from 0.52
local BODY_SIZE = 18
local HEADER_SIZE = 24
local FONT = Font.fromName(
"BuilderSans",
Enum.FontWeight.Regular,
Enum.FontStyle.Normal
)
local FONT_CODE = Font.new(
"rbxassetid://16658246179",
Enum.FontWeight.Regular,
Enum.FontStyle.Normal
)
--- determines when it should be decreasing instead of increasing
local function should_flip(lightness: number)
return lightness > 0.65
end
local CONTRAST_LOW = 0.4
local CONTRAST_HIGH = 0.7
type Depth = number
type ColorPalette = {
bg: {[Depth]: Color3},
fg_on_bg_high: {[Depth]: Color3},
fg_on_bg_low: {[Depth]: Color3},
fg_on_accent: {[Depth]: Color3},
accent: Color3,
}
local function compute_bg(depth: Depth)
local function get()
local bg = background()
-- print(bg.l + 0.01 * depth, depth, bg.c, bg.h)
return oklch {
math.clamp(bg.l + 0.02 * depth, 0, 1),
bg.c,
bg.h
}
-- if should_flip(bg.l) then
-- return oklch {
-- math.clamp(bg.l - 0.02 * depth, 0, 1),
-- bg.c,
-- bg.h
-- }
-- else
-- return oklch {
-- math.clamp(bg.l + 0.02 * depth, 0, 1),
-- bg.c,
-- bg.h
-- }
-- end
end
return function(raw: boolean)
local c = if raw then get() else get().color
return c
end :: ((true) -> typeof(oklch {}) ) & (false?) -> Color3
end
local function compute_acc(depth: Depth)
local function get()
local bg = accent()
-- print(bg.l + 0.01 * depth, depth, bg.c, bg.h)
return oklch {
math.clamp(bg.l + 0.02 * depth, 0, 1),
bg.c,
bg.h
}
-- if should_flip(bg.l) then
-- return oklch {
-- math.clamp(bg.l - 0.02 * depth, 0, 1),
-- bg.c,
-- bg.h
-- }
-- else
-- return oklch {
-- math.clamp(bg.l + 0.02 * depth, 0, 1),
-- bg.c,
-- bg.h
-- }
-- end
end
return function(raw: boolean)
local c = if raw then get() else get().color
return c
end :: ((true) -> typeof(oklch {}) ) & (false?) -> Color3
end
local function compute_fg_on_bg_high(depth: Depth)
local get_bg = compute_bg(depth)
return function()
local bg = get_bg(true)
if should_flip(bg.l) then
return oklch {
bg.l - CONTRAST_HIGH,
bg.c, bg.h
}.color
else
return oklch {
bg.l + CONTRAST_HIGH,
bg.c, bg.h
}.color
end
end
end
local function compute_fg_on_bg_low(depth: Depth)
local get_bg = compute_bg(depth)
return function()
local bg = get_bg(true)
if should_flip(bg.l) then
return oklch {
bg.l - CONTRAST_LOW,
bg.c, bg.h
}.color
else
return oklch {
bg.l + CONTRAST_LOW,
bg.c, bg.h
}.color
end
end
end
local function compute_fg_on_acc_low(depth: Depth)
local get_bg = compute_acc(depth)
return function()
local bg = get_bg(true)
if should_flip(bg.l) then
return oklch {
bg.l - CONTRAST_LOW,
bg.c, bg.h
}.color
else
return oklch {
bg.l + CONTRAST_LOW,
bg.c, bg.h
}.color
end
end
end
local function compute_fg_on_acc_high(depth: Depth)
local get_bg = compute_acc(depth)
return function()
local bg = get_bg(true)
if should_flip(bg.l) then
return oklch {
bg.l - CONTRAST_HIGH,
bg.c, bg.h
}.color
else
return oklch {
bg.l + CONTRAST_HIGH,
bg.c, bg.h
}.color
end
end
end
type Theme = {
bg: {[Depth]: number},
accent: Color3,
fg_on_bg_high: {[Depth]: Color3},
fg_on_bg_low: {[Depth]: Color3},
fg_on_accent_high: {[Depth]: Color3},
fg_on_accent_low: {[Depth]: Color3},
body: number,
header: number,
font: Font,
font_code: Font,
}
local function compute_theme()
local MAX_DEPTH = 20
local MIN_DEPTH = -20
local theme = {
bg = {},
acc = {},
fg_on_bg_high = {},
fg_on_bg_low = {},
fg_on_acc_high = {},
fg_on_acc_low = {},
body = BODY_SIZE,
header = HEADER_SIZE,
font = FONT,
code = FONT_CODE
}
for i = MIN_DEPTH, MAX_DEPTH do
theme.bg[i] = compute_bg(i)
theme.acc[i] = compute_acc(i)
theme.fg_on_acc_high[i] = compute_fg_on_acc_high(i)
theme.fg_on_acc_low[i] = compute_fg_on_acc_low(i)
theme.fg_on_bg_high[i] = compute_fg_on_bg_high(i)
theme.fg_on_bg_low[i] = compute_fg_on_bg_low(i)
end
return theme
end
return setmetatable({
settings = {
background = background,
accent = accent
},
}, {
__index = compute_theme()
})

View file

@ -0,0 +1,25 @@
type Action = {
priority: number,
callback: (Instance) -> ()
}
local ActionMT = table.freeze {}
local function is_action(v: any)
return getmetatable(v) == ActionMT
end
local function action(callback: (Instance) -> (), priority: number?): Action
local a = {
priority = priority or 1,
callback = callback
}
setmetatable(a :: any, ActionMT)
return table.freeze(a)
end
return function()
return action, is_action
end

Some files were not shown because too many files have changed in this diff Show more