Compare commits

..

30 commits

Author SHA1 Message Date
Ukendio
19823453aa Explicit error message for double disconnect
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
build-studio-docs / Build Studio Docs (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
2026-03-10 03:14:31 +01:00
Ukendio
7170dbf6a1 Throw error at structural changes within on_remove hooks 2026-03-10 03:11:43 +01:00
Ukendio
4d76e28425 emplace the iD 2026-03-05 22:26:41 +01:00
Ukendio
99c2b1b56e Update temperance 2026-03-01 19:45:38 +01:00
Ukendio
8ea8f1f235 Check that both elements of the pair are tags 2026-03-01 19:37:19 +01:00
Ukendio
a0f6a9a632 Consolidate PerfGraph 2026-03-01 19:36:45 +01:00
Ukendio
96dfde0d2e Allow contents to be written 2026-02-22 17:04:51 +01:00
Ukendio
043bff1ff8 Remove auth token and avoid overriding permissions 2026-02-22 16:59:54 +01:00
Ukendio
74254717f1 Add deserialize 2026-02-22 16:47:38 +01:00
Ukendio
0a8c827573 Url is case sensitive 2026-02-22 16:47:02 +01:00
Ukendio
683c7f28aa workflow_dispatch needs to be its own key 2026-02-22 16:46:30 +01:00
Ukendio
e2bfe80bb8 Delete tag 2026-02-20 12:08:43 +01:00
Ukendio
360423b634 Fix repo url in package.json 2026-02-20 12:04:06 +01:00
Ukendio
73019547bd Revoke token from example 2026-02-20 12:02:21 +01:00
Ukendio
9acec0e954 Update 2026-02-20 11:57:15 +01:00
Ukendio
f9764634e6 Ensure that world:exists uses the safe try_get_any 2026-02-20 04:42:13 +01:00
Ukendio
4b10b622bf Update npm 2026-02-19 23:28:26 +01:00
dai
29c93e5b0c
Add Trusted Publishing (#307) 2026-02-19 23:25:46 +01:00
Ukendio
6552a5d2d1 Remove the exact terms lookup set and detect bulk operation for removal of pairs 2026-02-19 23:16:18 +01:00
Ukendio
5f76674723 Fix import 2026-02-19 22:52:26 +01:00
Ukendio
622c7c9638 Update Wally 2026-02-19 22:24:01 +01:00
Ukendio
4236bd02fd Prune on cascaded deletion 2026-02-19 22:14:49 +01:00
Ukendio
aeedea2fcb Add Jabby module 2026-02-18 01:39:54 +01:00
Ukendio
22dd91b111 Add types 2026-02-16 02:04:35 +01:00
Ukendio
30597ed389 Add query:fini and query:archetypes(override) and changes to OB 2026-02-16 01:58:29 +01:00
Ukendio
d4a7f1d86c v0.10 2026-02-15 00:06:34 +01:00
Ukendio
33f7c08025 Expand the How To series 2026-02-14 17:58:41 +01:00
Ukendio
eacc343551 Fix example on networking 2026-02-14 17:57:44 +01:00
Micah
8ba09057be
Input example (#305) 2026-02-14 00:04:01 +01:00
onion
fa6f5fc52a
add predicted_transforms and fix some typos (#299) 2026-02-07 02:17:07 +01:00
152 changed files with 20512 additions and 737 deletions

View file

@ -1,17 +0,0 @@
name: publish-npm
on:
push:
branches: [main]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
node-version: "20"
- uses: JS-DevTools/npm-publish@v3
with:
token: ${{ secrets.NPM_AUTH_TOKEN }}

View file

@ -2,7 +2,13 @@ name: release
on:
push:
tags: ["v*", "workflow_dispatch"]
tags:
- "v*"
workflow_dispatch:
permissions:
id-token: write
contents: write
jobs:
build:
@ -29,15 +35,13 @@ jobs:
release:
name: Release
needs: [build]
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout Project
uses: actions/checkout@v4
- name: Download Jecs Build
- name: Download Build
uses: actions/download-artifact@v4
with:
name: build
@ -50,12 +54,12 @@ jobs:
uses: softprops/action-gh-release@v1
with:
name: Jecs ${{ github.ref_name }}
files: |
jecs.rbxm
tag_name: ${{ github.ref_name }}
files: jecs.rbxm
publish:
name: Publish
needs: [release]
publish-wally:
name: Publish to Wally
needs: release
runs-on: ubuntu-latest
steps:
- name: Checkout Project
@ -69,3 +73,23 @@ jobs:
- name: Publish
run: wally publish
publish-npm:
name: Publish to NPM
needs: release
runs-on: ubuntu-latest
steps:
- name: Checkout Project
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Install
run: npm install
- name: Publish
run: npm publish

View file

@ -47,6 +47,7 @@ end
-- local tgt = tonumber(tokens[2]) :: jecs.Entity
-- rel = ecs_ensure_entity(world, rel)
--
-- tgt = ecs_ensure_entity(world, tgt)
-- return jecs.pair(rel, tgt)

View file

@ -169,11 +169,12 @@ local function networking_send(world: jecs.World)
local set_values = {} :: { any }
local set_n = 0
local removed_n = 0
local component_is_a_tag = jecs.is_tag(world, component)
for e, v in storage do
if v ~= "jecs.Remove" then
set_n += 1
set_ids[set_n] = e
set_values[set_n] = if is_tag then 0 else v
set_values[set_n] = if component_is_a_tag then 0 else v
elseif world:contains(e) then
removed_n += 1
removed_ids[removed_n] = e

View file

@ -43,6 +43,12 @@ world:add(entity, Dead) -- Adds the tag
print(world:has(entity, Dead)) -- true
-- jecs.is_tag(world, id) returns true if the id is a tag (no data).
print(jecs.is_tag(world, Dead)) -- true
local Position = world:component() :: jecs.Id<number>
print(jecs.is_tag(world, Position)) -- false
-- Tags are removed using world:remove(entity, component)
world:remove(entity, Dead)

View file

View file

@ -82,3 +82,23 @@ world:set(e1, Position, vector.create(10, 20, 30))
- [Position, Velocity] -> [Position] (for removing Velocity)
]]
local pair = jecs.pair
local Likes = world:component()
local alice = world:entity()
local bob = world:entity()
local charlie = world:entity()
local e2 = world:entity()
world:add(e2, pair(Likes, alice)) -- Creates archetype [pair(Likes, alice)]
local e3 = world:entity()
world:add(e3, pair(Likes, bob)) -- Creates archetype [pair(Likes, bob)]
local e4 = world:entity()
world:add(e3, pair(Likes, charlie)) -- Creates archetype [pair(Likes, charlie)]
world:add(e3, pair(Likes, bob)) -- Creates archetype [pair(Likes, bob), pair(Likes, charlie)]
-- Each different target creates a new archetype, leading to fragmentation
-- This is why relationships can increase archetype count significantly

View file

@ -1,9 +1,3 @@
local jecs = require("@jecs")
local pair = jecs.pair
local world = jecs.world()
local Likes = world:component()
--[[
Fragmentation is a property of archetype-based ECS implementations where entities
are spread out over more archetypes as the number of different component combinations
@ -30,19 +24,3 @@ local Likes = world:component()
pair(jecs.Wildcard, Apples) indices. For this reason, creating new archetypes
with relationships has a higher overhead than an archetype without relationships.
]]
local alice = world:entity()
local bob = world:entity()
local charlie = world:entity()
local e1 = world:entity()
world:add(e1, pair(Likes, alice)) -- Creates archetype [pair(Likes, alice)]
local e2 = world:entity()
world:add(e2, pair(Likes, bob)) -- Creates archetype [pair(Likes, bob)]
local e3 = world:entity()
world:add(e3, pair(Likes, charlie)) -- Creates archetype [pair(Likes, charlie)]
-- Each different target creates a new archetype, leading to fragmentation
-- This is why relationships can increase archetype count significantly

View file

@ -2,7 +2,7 @@
Relationships makes it possible to describe entity graphs natively in ECS.
Adding/removing relationships is similar to adding/removing regular components,
with as difference that instead of a single component id, a relationship adds
with the difference that instead of a single component id, a relationship adds
a pair of two things to an entity. In this pair, the first element represents
the relationship (e.g. "Eats"), and the second element represents the relationship
target (e.g. "Apples").
@ -36,9 +36,6 @@ world:add(alice, pair(Likes, bob))
-- Test if entity has a relationship pair
print(world:has(bob, pair(Eats, Apples))) -- true
-- Test if entity has a relationship wildcard
print(world:has(bob, pair(Eats, jecs.Wildcard))) -- true
--[[
Querying for relationship targets
@ -77,26 +74,6 @@ for child in world:query(pair(ChildOf, parent)) do
print(`Entity {child} is a child of parent {parent}`)
end
--[[
Querying with wildcards and getting targets
When you query with a wildcard, you can use world:target() to get the
actual target entity. This is useful when you want to find all entities
with a relationship, regardless of the target.
]]
-- Find all entities that eat something (any target)
for entity in world:query(pair(Eats, jecs.Wildcard)) do
local food = world:target(entity, Eats) -- Get the actual target
print(`Entity {entity} eats {food}`)
end
-- Find all entities that like someone (any target)
for entity in world:query(pair(Likes, jecs.Wildcard)) do
local target = world:target(entity, Likes)
print(`Entity {entity} likes {target}`)
end
--[[
Combining relationship queries with regular components
@ -118,39 +95,6 @@ for entity, pos, health in world:query(Position, Health, pair(ChildOf, parent))
print(`Child {entity} has position {pos} and health {health}`)
end
--[[
Querying for entities with multiple relationship targets
An entity can have multiple relationships with the same relationship type
but different targets. For example, bob might like both alice and charlie.
When querying with a wildcard, you'll get the entity once, but world:target()
will return the first matching target. If you need all targets, you'll need
to use a different approach (see the targets example for advanced usage).
]]
local charlie = world:entity()
world:add(bob, pair(Likes, charlie))
-- This query will return bob once, even though bob likes both alice and charlie
for entity in world:query(pair(Likes, jecs.Wildcard)) do
local target = world:target(entity, Likes)
print(`Entity {entity} likes {target}`) -- Will show one target per entity
end
--[[
Querying for all relationships with a specific target
You can also query for all entities that have any relationship with a
specific target using a wildcard for the relationship part.
]]
-- Find all entities that have any relationship with alice as the target
for entity in world:query(pair(jecs.Wildcard, alice)) do
-- Note: This is less common and may have performance implications
print(`Entity {entity} has some relationship with alice`)
end
--[[
Relationship pairs, just like regular component, can be associated with data.
]]
@ -178,30 +122,4 @@ for entity, eats_data in world:query(pair(Eats, Apples)) do
print(`Entity {entity} eats apples: amount = {eats_data.amount}`)
end
--[[
When querying for relationship pairs, it is often useful to be able to find
all instances for a given relationship or target. To accomplish this, a game
can use wildcard expressions.
Wildcards may used for the relationship or target part of a pair:
pair(Likes, jecs.Wildcard) -- Matches all Likes relationships
pair(jecs.Wildcard, Alice) -- Matches all relationships with Alice as target
Using world:target() is the recommended way to get the target in a wildcard
query. However, if you're in a very hot path and need maximum performance,
you can access the relationship column directly (see advanced examples).
]]
for entity in world:query(pair(Eats, jecs.Wildcard)) do
local nth = 0
local food = world:target(entity, Eats, nth)
while food do
local eats_data = world:get(entity, pair(Eats, food))
assert(eats_data) -- This coerces the type to be non-nilable for the type checker
print(`Entity {entity} eats {food}: amount = {eats_data.amount}`)
nth += 1
food = world:target(entity, Eats, nth)
end
end
-- For wildcard queries and world:target (0-based index), see 042_target.luau and 043_wildcards.luau.

37
how_to/042_target.luau Normal file
View file

@ -0,0 +1,37 @@
--[[
world:target(entity, relation, index?) returns the target of a relationship
on an entity. The index is 0-based. If no target exists at that index, it
returns nil.
Use target when you have queried with a wildcard (e.g. pair(Eats, jecs.Wildcard))
and need the actual target entity for each result. Without an index, the
default is 0 (the first target).
]]
local jecs = require("@jecs")
local pair = jecs.pair
local world = jecs.world()
local Eats = world:entity()
local Apples = world:entity()
local Oranges = world:entity()
local bob = world:entity()
world:add(bob, pair(Eats, Apples))
world:add(bob, pair(Eats, Oranges))
-- First target is at index 0
local first = world:target(bob, Eats, 0)
print(first == Apples) -- true
-- Second target is at index 1
local second = world:target(bob, Eats, 1)
print(second == Oranges) -- true
-- No third target: index 2 returns nil
local third = world:target(bob, Eats, 2)
print(third == nil) -- true
-- Omitting the index is the same as index 0
local default = world:target(bob, Eats)
print(default == Apples) -- true

58
how_to/043_wildcards.luau Normal file
View file

@ -0,0 +1,58 @@
--[[
Wildcards let you query relationships without specifying the exact target or
relationship. jecs.Wildcard matches any entity in that slot.
- pair(relation, jecs.Wildcard) matches that relationship with any target.
- pair(jecs.Wildcard, target) matches any relationship with that target.
Use world:target(entity, relation, index) to get the actual target when
querying with a wildcard. The index is 0-based (see 042_target.luau).
]]
local jecs = require("@jecs")
local pair = jecs.pair
local world = jecs.world()
local Eats = world:component() :: jecs.Id<{ amount: number }>
local Likes = world:entity()
local Apples = world:entity()
local alice = world:entity()
local bob = world:entity()
world:add(bob, pair(Eats, Apples))
world:set(bob, pair(Eats, Apples), { amount = 1 })
world:add(bob, pair(Likes, alice))
-- world:has with wildcard
print(world:has(bob, pair(Eats, jecs.Wildcard))) -- true
-- Query with wildcard: all entities that eat something
for entity in world:query(pair(Eats, jecs.Wildcard)) do
local food = world:target(entity, Eats)
print(`Entity {entity} eats {food}`)
end
-- Query with wildcard: all entities that like someone
for entity in world:query(pair(Likes, jecs.Wildcard)) do
local target = world:target(entity, Likes)
print(`Entity {entity} likes {target}`)
end
-- Multiple targets: index is 0-based. Iterate until nil.
local charlie = world:entity()
world:add(bob, pair(Likes, charlie))
for entity in world:query(pair(Likes, jecs.Wildcard)) do
local nth = 0
local target = world:target(entity, Likes, nth)
while target do
print(`Entity {entity} likes {target}`)
nth += 1
target = world:target(entity, Likes, nth)
end
end
-- pair(jecs.Wildcard, target): all relationships that have this target
for entity in world:query(pair(jecs.Wildcard, alice)) do
print(`Entity {entity} has some relationship with alice`)
end

View file

@ -70,7 +70,7 @@ end)
local Health = world:component()
local Dead = world:component()
world:set(Health, jecs.OnRemove, function(entity, id, delete)
world:set(Health, jecs.OnRemove, function(entity: jecs.Entity, id, delete)
if delete then
-- Entity is being deleted, don't try to clean up
return

View file

@ -0,0 +1,81 @@
--[[
Signals let you subscribe to component add, change, and remove events with
multiple listeners per component. Unlike hooks (see 110_hooks.luau), which
allow only one OnAdd, OnChange, and OnRemove per component, signals support
any number of subscribers and each subscription returns an unsubscribe
function so you can clean up when you no longer need to listen.
Use signals when you need several independent systems to react to the same
component lifecycle events, or when you want to subscribe and unsubscribe
dynamically (e.g. a UI that only cares while it's mounted).
]]
local jecs = require("@jecs")
local world = jecs.world()
local Position = world:component() :: jecs.Id<{ x: number, y: number }>
--[[
world:added(component, fn)
Subscribe to "component added" events. Your callback is invoked with:
(entity, id, value, oldarchetype) whenever the component is added to an entity.
Returns a function; call it to unsubscribe.
]]
local unsub_added = world:added(Position, function(entity, id, value, oldarchetype)
print(`Position added to entity {entity}: ({value.x}, {value.y})`)
end)
--[[
world:changed(component, fn)
Subscribe to "component changed" events. Your callback is invoked with:
(entity, id, value, oldarchetype) whenever the component's value is updated
on an entity (e.g. via world:set).
Returns a function; call it to unsubscribe.
]]
local unsub_changed = world:changed(Position, function(entity, id, value, oldarchetype)
print(`Position changed on entity {entity}: ({value.x}, {value.y})`)
end)
--[[
world:removed(component, fn)
Subscribe to "component removed" events. Your callback is invoked with:
(entity, id, delete?) when the component is removed. The third argument
`delete` is true when the entity is being deleted, false or nil when
only the component was removed (same semantics as OnRemove in 110_hooks).
Returns a function; call it to unsubscribe.
]]
local unsub_removed = world:removed(Position, function(entity, id, delete)
if delete then
print(`Entity {entity} deleted (had Position)`)
else
print(`Position removed from entity {entity}`)
end
end)
local e = world:entity()
world:set(e, Position, { x = 10, y = 20 }) -- added
world:set(e, Position, { x = 30, y = 40 }) -- changed
world:remove(e, Position) -- removed
world:added(Position, function(entity)
print("Second listener: Position added")
end)
world:set(e, Position, { x = 0, y = 0 }) -- Multiple listeners are all invoked
-- Unsubscribe when you no longer need to listen
unsub_added()
unsub_changed()
unsub_removed()
world:set(e, Position, { x = 1, y = 1 })
world:remove(e, Position)

View file

@ -0,0 +1,54 @@
-- These notes are my thoughts jotted down from having experienced these
-- problems myself and gathered insights from many admired individuals such as
-- Sander Mertens, Ryan Fleury, Jonathon Blow, Benjamin Saunders and many more...
--[[
In 1993, the original source code for DOOM was about 50,000 lines of code. And
when your code gets into that neighbourhood, it should provide a large amount of
interesting and novel functionality. If it doesn't, perhaps it is time to ask questions.
Please, try to write code that is small, and that does a lot for its size.
Please, after you finish writing something, ask yourself whether you are
satisfied with how robust it is, and with how much it gets done for how much
code there is.
- Transfer of tacit knowledge is incredibly important. If tacit knowledge
about the code base is lost, ability to work on it at the same level of quality
is lost. Over time code quality will decline as code size grows.
- Tacit knowledge is very hard to recover by looking at a maze of code,
and it takes a long time to do so.
- You will often hear that "every semantic distinction deserves its own
component or tag". Sometimes this is correct. A well chosen component boundary
can make queries clear and systems obvious. But sometimes this distinction would
be better served as a field, a bitset, or a local data structure. The
representation should match the problem.
Sub-Essay Here: Code Should Not Try To Meet Every Need Anyone May Ever Have.
Over-generalization leads to bloat and poor functionality. A common failure mode
is writing code "for everyone" while building configuration for every scenario,
abstractions for every future feature, and extension points for every imagined
consumer. It sounds responsible. It usually isn't.
Specialization is good in many cases. Think about the guy with the truck full of
automotive tools, who has a bunch of different wrenches. He doesn't carry one
wrench that transforms into every tool; he carries a few specialized tools that
are reliable and fast to use. One reason we have endless bloat is that we teach
that all code should expand until it meets all needs. This is wrong,
empirically, and we should stop teaching it.
- Relationships are very powerful however, with each pair being an unique
component, it can be an easy way to accidentally increase the number of archetypes which can cause
higher churn in systems.
- A hook is not a replacement for systems. They are for enforcing invariants when data changes during different lifecycles.
When gameplay logic that should run predictably each frame is instead scattered
across hooks, behaviour becomes implicit when it is triggered indirectly through
a cascade of changes that, logic split across many small
callbacks that fire in surprising order. Which also gets harder to reason about
and optimize.
]]

View file

@ -0,0 +1,35 @@
local RunService = game:GetService("RunService")
local Input = require("@modules/Input/module")
local function cameraSystem()
local look = Input.value2d("look")
-- rotate camera with look
end
local function characterMovement()
local move = Input.clamped2d("move")
-- humanoid:Move(move)
if Input.justPressed("jump") then
-- humanoid.Jump = true
end
end
RunService.RenderStepped:Connect(function(deltaTime)
Input.update(deltaTime)
Input.runPhase("RenderStepped", function()
cameraSystem()
end)
end)
RunService.PreSimulation:Connect(function(deltaTime)
Input.update(deltaTime)
Input.runPhase("PreSimulation", function()
characterMovement()
end)
end)

269
modules/Input/module.luau Normal file
View file

@ -0,0 +1,269 @@
local UserInputService = game:GetService("UserInputService")
local UserGameSettings = UserSettings():GetService("UserGameSettings")
-- Phase 1: Collect raw inputs from Roblox APIs. Here we are using UserInputService but you could use the other APIs.
local rawInput = {
space = false,
w = false,
a = false,
s = false,
d = false,
buttonA = false,
mouseDelta = Vector2.zero,
leftThumbstickDelta = Vector2.zero,
rightThumbstickDelta = Vector2.zero,
}
UserInputService.InputBegan:Connect(function(input, sink)
if sink then
return
end
if input.KeyCode == Enum.KeyCode.W then
rawInput.w = true
elseif input.KeyCode == Enum.KeyCode.A then
rawInput.a = true
elseif input.KeyCode == Enum.KeyCode.S then
rawInput.s = true
elseif input.KeyCode == Enum.KeyCode.D then
rawInput.d = true
elseif input.KeyCode == Enum.KeyCode.Space then
rawInput.space = true
elseif input.KeyCode == Enum.KeyCode.ButtonA then
rawInput.buttonA = true
end
end)
UserInputService.InputChanged:Connect(function(input, sink)
if input.UserInputType == Enum.UserInputType.MouseMovement then
rawInput.mouseDelta = Vector2.new(input.Delta.X, -input.Delta.Y)
elseif input.KeyCode == Enum.KeyCode.Thumbstick1 then
rawInput.leftThumbstickDelta = Vector2.new(input.Position.X, input.Position.Y)
elseif input.KeyCode == Enum.KeyCode.Thumbstick2 then
rawInput.rightThumbstickDelta = Vector2.new(input.Position.X, input.Position.Y)
end
end)
UserInputService.InputEnded:Connect(function(input, sink)
if input.KeyCode == Enum.KeyCode.W then
rawInput.w = false
elseif input.KeyCode == Enum.KeyCode.A then
rawInput.a = false
elseif input.KeyCode == Enum.KeyCode.S then
rawInput.s = false
elseif input.KeyCode == Enum.KeyCode.D then
rawInput.d = false
elseif input.KeyCode == Enum.KeyCode.Space then
rawInput.space = false
elseif input.KeyCode == Enum.KeyCode.ButtonA then
rawInput.buttonA = false
end
end)
-- Phase 2: Derive action state from raw inputs.
local SENSITIVITY_MOUSE = Vector2.new(1, 0.77) * math.rad(0.5)
local SENSITIVITY_GAMEPAD = Vector2.new(1, 0.77) * math.rad(4) * 60
local function virtualVector2(up: boolean, down: boolean, left: boolean, right: boolean): Vector2
local x = 0
local y = 0
if up then
y += 1
end
if down then
y -= 1
end
if left then
x -= 1
end
if right then
x += 1
end
return Vector2.new(x, y)
end
local function scaledDeadZone(value: number, lowerThreshold: number): number
local lowerBound = math.max(math.abs(value) - lowerThreshold, 0)
local scaledValue = lowerBound / (1 - lowerThreshold)
return math.min(scaledValue, 1) * math.sign(value)
end
local function radialDeadZone(value: Vector2, threshold: number): Vector2
local magnitude = value.Magnitude
if magnitude == 0 then
return Vector2.zero
else
return value.Unit * scaledDeadZone(magnitude, threshold)
end
end
-- Convert raw inputs into action state. This function will apply modifiers like dead zones or sensitivity multipliers.
local function deriveActionState(deltaTime: number)
local keyboardMove = virtualVector2(rawInput.w, rawInput.s, rawInput.a, rawInput.d)
local gamepadMove = radialDeadZone(rawInput.leftThumbstickDelta, 0.2)
local mouseLook = rawInput.mouseDelta * SENSITIVITY_MOUSE
local gamepadLook = radialDeadZone(rawInput.rightThumbstickDelta, 0.2)
* UserGameSettings.GamepadCameraSensitivity
* SENSITIVITY_GAMEPAD
* deltaTime
return {
boolean = {
jump = rawInput.space or rawInput.buttonA,
},
value2d = {
move = keyboardMove + gamepadMove,
look = mouseLook + gamepadLook,
},
}
end
-- UserGameSettings.GamepadCameraSensitivity is only updated if this is called.
UserGameSettings:SetGamepadCameraSensitivityVisible()
-- 3. The API
local ACTIONS_BOOLEAN = {
jump = true,
}
local ACTIONS_2D = {
move = true,
look = true,
}
local DEFAULT_PHASE_STATE = {
boolean = {},
justPressedCounts = {} :: { [string]: number },
justReleasedCounts = {} :: { [string]: number },
value2d = {},
}
for action in ACTIONS_BOOLEAN :: any do
DEFAULT_PHASE_STATE.boolean[action] = false
end
for action in ACTIONS_2D :: any do
DEFAULT_PHASE_STATE.value2d[action] = Vector2.zero
end
local function copyDeep<T>(value: T): T
if typeof(value) == "table" then
local clone = table.clone(value) :: any
for key, value in clone do
clone[key] = copyDeep(value)
end
return clone
else
return value
end
end
local lastInputState = deriveActionState(0)
local currentPhase = DEFAULT_PHASE_STATE
local phases = {}
local Input = {}
function Input.justPressed(action: keyof<typeof(ACTIONS_BOOLEAN)>): boolean
return currentPhase.justPressedCounts[action] ~= nil
end
function Input.justReleased(action: keyof<typeof(ACTIONS_BOOLEAN)>): boolean
return currentPhase.justReleasedCounts[action] ~= nil
end
function Input.pressed(action: keyof<typeof(ACTIONS_BOOLEAN)>): boolean
return currentPhase.boolean[action]
end
function Input.released(action: keyof<typeof(ACTIONS_BOOLEAN)>): boolean
return not currentPhase.boolean[action]
end
function Input.value2d(action: keyof<typeof(ACTIONS_2D)>): Vector2
return currentPhase.value2d[action]
end
function Input.unit2d(action: keyof<typeof(ACTIONS_2D)>): Vector2
local value = currentPhase.value2d[action]
if value.Magnitude > 0 then
return value.Unit
end
return value
end
function Input.clamped2d(action: keyof<typeof(ACTIONS_2D)>): Vector2
local value = currentPhase.value2d[action]
if value.Magnitude > 1 then
return value.Unit
end
return value
end
function Input.runPhase(name: string, callback: () -> ())
if not phases[name] then
phases[name] = copyDeep(DEFAULT_PHASE_STATE)
end
currentPhase = phases[name]
callback()
table.clear(currentPhase.justPressedCounts)
table.clear(currentPhase.justReleasedCounts)
for action in currentPhase.boolean do
currentPhase.boolean[action] = false
end
for action in currentPhase.value2d do
currentPhase.value2d[action] = Vector2.zero
end
currentPhase = DEFAULT_PHASE_STATE
end
function Input.update(deltaTime: number)
local inputState = deriveActionState(deltaTime)
local presses = {}
local releases = {}
for action, value in inputState.boolean do
if value and not lastInputState.boolean[action] then
table.insert(presses, action)
elseif not value and lastInputState.boolean[action] then
table.insert(releases, action)
end
end
for _, phase in phases do
for _, action in presses do
phase.justPressedCounts[action] = (phase.justPressedCounts[action] or 0) + 1
end
for _, action in releases do
phase.justReleasedCounts[action] = (phase.justReleasedCounts[action] or 0) + 1
end
for action, value in inputState.boolean do
phase.boolean[action] = phase.boolean[action] or value
end
for action, value in inputState.value2d do
phase.value2d[action] += value
end
end
lastInputState = inputState
-- Reset the mouse delta.
rawInput.mouseDelta = Vector2.zero
end
return Input

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/module")
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

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