1102 lines
34 KiB
Lua
1102 lines
34 KiB
Lua
require(game.ReplicatedFirst.ReadyModule)("ProgramModule")
|
|
-- Need interpreter for graph
|
|
-- Need program graph for JSON export
|
|
-- Need compiler for graph
|
|
-- Need data structure links to gui
|
|
|
|
--[[
|
|
So I SHOULD definitely work backwards, starting from the interpreter, since it connects to the structure best
|
|
The graph is a bit funky... :/
|
|
|
|
So, where does execution even start? Do we have an endpoint and then call everything backwards to bubble up the stack?
|
|
Or do we just start from the inputs and try and flow through there? Both sound a bit stupid to be honest.
|
|
There are some inputs that just won't really appear.
|
|
|
|
There is a concern with loops however, but this probably shouldn't be handled with real physical loops but
|
|
more as a processor cycle. I could always increase the clock speed? I'm not sure.
|
|
In a way, that means we should iterate through every single object and provide hooks for each one. So lots of coroutines?
|
|
Is that sensible? No. Kind of.
|
|
|
|
This means every node should have a function to produce a given value associated with it,
|
|
an operation to I guess, make that producer? Or does that count. I guess not.
|
|
So, the structure should have a list of outputs with types,
|
|
a list of inputs with types.
|
|
There could also be blocks which have a flag saying that they produce some sort of tangible output. I guess.
|
|
So that would be a 'callback'? I guess so.
|
|
|
|
So each 'node' in the table structure would just be implicitly in the graph... Uh. Great. Now what.
|
|
Well, I guess, how is the JSON data reaaaalllly any different here?
|
|
Using IDs doesn't seem like an extraordinarily stupid idea except for adding new tables causes problems.
|
|
Ok so we can't use JSON lol.
|
|
|
|
]]
|
|
|
|
--[[
|
|
Every node would also have its own sort of global rules associated. Which does make me think more about the GUI stuff.
|
|
That is indeed quite interesting.
|
|
]]
|
|
local function class(name)
|
|
return {name=name,[name]=true}
|
|
end
|
|
|
|
-- Language types for everything
|
|
local VECTOR = class("Vector")
|
|
local STRING = class("String")
|
|
local NUMBER = class("Number")
|
|
local BOOLEAN = class("Boolean")
|
|
local CONTROL = class("Control") -- Enum for vehicle controls
|
|
local PIN = class("Pin")
|
|
local ANY = class("Any")
|
|
|
|
local PINS_LIST = {}
|
|
for char in string.gmatch("12345ABCDEXY",".") do table.insert(PINS_LIST,char) end
|
|
local CONTROL_LIST = {}
|
|
for control in string.gmatch("throttle,yaw,pitch,roll,fire,action1,gear,action2,flaps,bomb,auto","[^,]+") do table.insert(CONTROL_LIST,control) end
|
|
|
|
local TYPE_COLOURS = {
|
|
Vector = Color3.new(1,0.5,0),
|
|
String = Color3.new(0,1,0.5),
|
|
Number = Color3.new(0,0.5,1),
|
|
Boolean = Color3.new(0.5,0,1),
|
|
Control = Color3.new(1,0,0.5),
|
|
Pin = Color3.new(0.5,1,0),
|
|
Any = Color3.new(0.5,0.5,0.5)
|
|
}
|
|
|
|
-- Types of row for GUI
|
|
local GUI_ROW_TEXT = class("Text") -- Name
|
|
local GUI_ROW_INPUT = class("Input") -- Type Name
|
|
local GUI_ROW_OUTPUT = class("Output") -- Name Type
|
|
local GUI_ROW_LITERAL = class("Literal") -- Name Type Options
|
|
|
|
-- Components of the GUI
|
|
|
|
local gui
|
|
|
|
local programFrame
|
|
local programTopBar
|
|
local programsList
|
|
local programsSelectionBox
|
|
local programNameBox
|
|
local programUploadButton
|
|
local programCancelButton
|
|
local programContainer
|
|
local programInventory
|
|
local programViewport
|
|
local programViewportEmpty
|
|
|
|
local templateNode
|
|
local templateNodeIO
|
|
local templateNodeText
|
|
local templateNodeSelect
|
|
local templateNodeInput
|
|
local templateNodeButton
|
|
local templateNodeName
|
|
local templateProgramTab
|
|
local highlight
|
|
|
|
if _G.IsClient then
|
|
gui = game.Players.LocalPlayer.PlayerGui:WaitForChild("ScreenGui")
|
|
|
|
programFrame = _G:WaitFor("ProgramFrame")
|
|
programTopBar = programFrame:WaitForChild("TopBar")
|
|
programsList = programTopBar:WaitForChild("Programs"):WaitForChild("ScrollingFrame")
|
|
programsSelectionBox = programsList.Parent:WaitForChild("Template"):WaitForChild("UIStroke")
|
|
programNameBox = programTopBar:WaitForChild("ProgramNameBox")
|
|
programUploadButton = programTopBar:WaitForChild("ProgramUploadButton")
|
|
programCancelButton = programTopBar:WaitForChild("ProgramCancelButton")
|
|
programContainer = programFrame:WaitForChild("Container")
|
|
programInventory = programContainer:WaitForChild("Inventory"):WaitForChild("ScrollingFrame")
|
|
local programViewportFrame = programContainer:WaitForChild("Program")
|
|
programViewport = programViewportFrame:WaitForChild("Viewport")
|
|
programViewportEmpty = programViewportFrame:WaitForChild("Empty")
|
|
|
|
templateNode = programContainer:WaitForChild("Template"):WaitForChild("Node")
|
|
templateNodeIO = templateNode:WaitForChild("IO")
|
|
templateNodeText = templateNode:WaitForChild("TextLabel")
|
|
templateNodeSelect = templateNode:WaitForChild("LiteralSelect")
|
|
templateNodeInput = templateNode:WaitForChild("LiteralInput")
|
|
templateNodeButton = templateNode:WaitForChild("LiteralButton")
|
|
templateNodeName = templateNode:WaitForChild("NodeLabel")
|
|
templateProgramTab = programsList:WaitForChild("Template"):WaitForChild("Script")
|
|
highlight = script.Highlight
|
|
|
|
templateNode.Visible = false
|
|
templateProgramTab.Visible = false
|
|
end
|
|
|
|
local nodeLibrary = {}
|
|
|
|
do -- General nodes library
|
|
-- Node constructor functions:
|
|
|
|
-- Node input dragged out from the right as a point
|
|
local function input(name,type)
|
|
return function(node)
|
|
table.insert(node.rows,{name = name, text = name, input = type})
|
|
end
|
|
end
|
|
|
|
-- Node output dragged in from the left as a point
|
|
local function output(name,type,producer)
|
|
return function(node)
|
|
table.insert(node.rows,{name = name, text = name, output = type, producer = producer})
|
|
end
|
|
end
|
|
|
|
-- Node needs to call a function when done
|
|
local function callback(callback)
|
|
return function(node)
|
|
node.callback = callback
|
|
end
|
|
end
|
|
|
|
-- Node literal value typed in by user
|
|
local function literal(name,type,options)
|
|
return function(node)
|
|
local row = {name = name, text = name, literal = type}
|
|
table.insert(node.rows,row)
|
|
end
|
|
end
|
|
|
|
-- Just a row of text on the node
|
|
local function text(text)
|
|
return function(node)
|
|
table.insert(node.rows,{text = text})
|
|
end
|
|
end
|
|
|
|
-- The name tag at the top of the node
|
|
local function name(name)
|
|
return function(node)
|
|
node.name = name
|
|
end
|
|
end
|
|
|
|
-- The overall node
|
|
local function node(...)
|
|
local node = {
|
|
inputs = {},
|
|
outputs = {},
|
|
rows = {},
|
|
callback = nil,
|
|
name = ""
|
|
}
|
|
for _,item in pairs({...}) do
|
|
item(node)
|
|
end
|
|
return node
|
|
end
|
|
|
|
-- Raw constructors for nodes in the instruction set
|
|
local library = {
|
|
node(
|
|
name("Control"),
|
|
literal("Control",CONTROL),
|
|
output("Value",NUMBER,function(node,program)
|
|
return program.context.control(node.Control)
|
|
end)
|
|
),
|
|
node(
|
|
name("Input"),
|
|
literal("Pin",PIN),
|
|
output("Value",ANY,function(node,program)
|
|
return program.context.input(node.Pin)
|
|
end)
|
|
),
|
|
node(
|
|
name("Output"),
|
|
literal("Pin",PIN),
|
|
literal("Header",STRING),
|
|
input("Value",ANY),
|
|
callback(function(node,program)
|
|
program.context.output(node.Pin,node.Header,node.Value())
|
|
end)
|
|
),
|
|
node(
|
|
name("Print"),
|
|
input("Text",ANY),
|
|
callback(function(node)
|
|
print(node.Text() or "nil")
|
|
end)
|
|
),
|
|
node(
|
|
name("Notify"),
|
|
input("Title",ANY),
|
|
input("Text",ANY),
|
|
callback(function(node,program)
|
|
program.context.message(node.Title or "Program Message",node.Text or "Nothing")
|
|
end)
|
|
),
|
|
node(
|
|
name("Number"),
|
|
literal("Number",NUMBER),
|
|
output("Value",NUMBER,function(node)
|
|
return node.Number or 0
|
|
end)
|
|
),
|
|
node(
|
|
name("Boolean"),
|
|
literal("Truth",BOOLEAN),
|
|
output("Value",BOOLEAN,function(node)
|
|
return node.Truth or false
|
|
end)
|
|
),
|
|
node(
|
|
name("String"),
|
|
literal("String",STRING),
|
|
output("Value",STRING,function(node)
|
|
return node.String or ""
|
|
end)
|
|
),
|
|
node(
|
|
name("Neg"),
|
|
input("A",NUMBER),
|
|
output("-A",NUMBER,function(node)
|
|
return -(tonumber(node.A() or 0) or 0)
|
|
end)
|
|
),
|
|
node(
|
|
name("Con"),
|
|
input("A",STRING),
|
|
output("A .. B",STRING,function(node)
|
|
return (tostring(node.A() or "") or "") .. (tostring(node.B() or "") or "")
|
|
end),
|
|
input("B",STRING)
|
|
),
|
|
node(
|
|
name("Add"),
|
|
input("A",NUMBER),
|
|
output("A + B",NUMBER,function(node)
|
|
return (tonumber(node.A() or 0) or 0) + (tonumber(node.B() or 0) or 0)
|
|
end),
|
|
input("B",NUMBER)
|
|
),
|
|
node(
|
|
name("Sub"),
|
|
input("A",NUMBER),
|
|
output("A - B",NUMBER,function(node)
|
|
return (tonumber(node.A() or 0) or 0) - (tonumber(node.B() or 0) or 0)
|
|
end),
|
|
input("B",NUMBER)
|
|
),
|
|
node(
|
|
name("Div"),
|
|
input("A",NUMBER),
|
|
output("A / B",NUMBER,function(node)
|
|
return (tonumber(node.A() or 1) or 1) / (tonumber(node.B() or 1) or 1)
|
|
end),
|
|
input("B",NUMBER)
|
|
),
|
|
node(
|
|
name("Mul"),
|
|
input("A",NUMBER),
|
|
output("A * B",NUMBER,function(node)
|
|
return (tonumber(node.A() or 1) or 1) * (tonumber(node.B() or 1) or 1)
|
|
end),
|
|
input("B",NUMBER)
|
|
),
|
|
node(
|
|
name("Comp"),
|
|
input("A",NUMBER),
|
|
output("A < B",BOOLEAN,function(node)
|
|
return (tonumber(node.A() or math.huge) or math.huge) < (tonumber(node.B() or -math.huge) or -math.huge)
|
|
end),
|
|
input("B",NUMBER)
|
|
),
|
|
node(
|
|
name("Comp Eq"),
|
|
input("A",NUMBER),
|
|
output("A <= B",BOOLEAN,function(node)
|
|
return (tonumber(node.A() or math.huge) or math.huge) <= (tonumber(node.B() or -math.huge) or -math.huge)
|
|
end),
|
|
input("B",NUMBER)
|
|
),
|
|
node(
|
|
name("Cos"),
|
|
input("Value",NUMBER),
|
|
output("Cosine",NUMBER,function(node)
|
|
return math.cos(tonumber(node.Value() or 0) or 0)
|
|
end)
|
|
),
|
|
node(
|
|
name("Sin"),
|
|
input("Value",NUMBER),
|
|
output("Sin",NUMBER,function(node)
|
|
return math.sin(tonumber(node.Value() or 0) or 0)
|
|
end)
|
|
),
|
|
node(
|
|
name("Equal"),
|
|
input("A",ANY),
|
|
output("A == B",BOOLEAN,function(node)
|
|
return node.A() == node.B()
|
|
end),
|
|
input("B",ANY)
|
|
),
|
|
node(
|
|
name("Gate"),
|
|
input("Continue",BOOLEAN),
|
|
output("Output",ANY,function(node)
|
|
if node.Continue() then
|
|
return node.Input()
|
|
else
|
|
coroutine.yield()
|
|
end
|
|
end),
|
|
input("Input",ANY)
|
|
),
|
|
node(
|
|
name("Set"),
|
|
literal("Name",STRING),
|
|
input("Value",ANY),
|
|
callback(function(node,this)
|
|
this.variables[node.Name()] = node.Value
|
|
end)
|
|
),
|
|
node(
|
|
name("Get"),
|
|
literal("Name",STRING),
|
|
output("Value",ANY,function(node,this)
|
|
return this.variables[node.Name()]
|
|
end)
|
|
),
|
|
node(
|
|
name("Time"),
|
|
output("Value",ANY,function(node,this)
|
|
return tick()
|
|
end)
|
|
),
|
|
node(
|
|
name("Frequency"),
|
|
literal("Number",NUMBER,{min = 1, max = 60}),
|
|
callback(function(node,program)
|
|
program.frequency = node.Number or 30
|
|
end)
|
|
)
|
|
}
|
|
|
|
-- Create all node frames for the library to select nodes from
|
|
local function init()
|
|
programFrame.Visible = false
|
|
for index,node in ipairs(library) do
|
|
local frame = templateNode:Clone()
|
|
-- Remove all things already in the template, they may not be needed.
|
|
for _,item in pairs(frame:GetChildren()) do
|
|
if item:IsA("GuiObject") then item:Destroy() end
|
|
end
|
|
-- Node name tag at the top
|
|
local name = templateNodeName:Clone()
|
|
name.Text = node.name or error("Node missing a name")
|
|
name.Parent = frame
|
|
-- Massive if thingy to make all the different types of row in a node
|
|
for index,row in ipairs(node.rows) do
|
|
local item
|
|
if row.input or row.output then -- Input/Output
|
|
local io = templateNodeIO:Clone()
|
|
io.Point.BackgroundColor3 = TYPE_COLOURS[(row.input or row.output).name]
|
|
if row.input then
|
|
io.Label.TextXAlignment = Enum.TextXAlignment.Left
|
|
elseif row.output then
|
|
io.Point.Position = UDim2.new(1,6,0.5,0)
|
|
io.Label.TextXAlignment = Enum.TextXAlignment.Right
|
|
end
|
|
io.Label.Text = row.text
|
|
item = io
|
|
elseif row.literal then -- Literal
|
|
local literal
|
|
local type = row.literal
|
|
if row.literal == NUMBER or row.literal == STRING then
|
|
literal = templateNodeInput:Clone()
|
|
literal.Text = ""
|
|
if row.literal == NUMBER then
|
|
literal.PlaceholderText = "0"
|
|
elseif row.literal == STRING then
|
|
literal.PlaceholderText = "Text"
|
|
end
|
|
elseif row.literal == BOOLEAN then
|
|
literal = templateNodeButton:Clone()
|
|
literal.Text = "False"
|
|
elseif row.literal == PIN or row.literal == CONTROL then
|
|
literal = templateNodeSelect:Clone()
|
|
if row.literal == PIN then
|
|
literal.Value.Text = "1"
|
|
elseif row.literal == CONTROL then
|
|
literal.Value.Text = "throttle"
|
|
end
|
|
else
|
|
error("No row type for "..tostring(row.literal))
|
|
end
|
|
item = literal
|
|
elseif row.text then -- Text
|
|
local text = templateNodeText:Clone()
|
|
text.Text = row.text
|
|
item = text
|
|
else
|
|
error("Row doesn't satisfy any template rows")
|
|
end
|
|
item.Parent = frame
|
|
item.LayoutOrder = index
|
|
if row.name then item.Name = row.name end
|
|
end
|
|
-- Darker nodes indicate where the rest of the nodes get originally called from!
|
|
if node.callback then frame.UIStroke.Enabled = true end
|
|
|
|
frame.LayoutOrder = index
|
|
frame.Parent = programInventory
|
|
frame.Visible = true
|
|
|
|
node.frame = frame
|
|
library[frame] = node
|
|
end
|
|
end
|
|
|
|
if _G.IsClient then init() end
|
|
|
|
for index,node in pairs(library) do
|
|
library[node.name] = node
|
|
end
|
|
|
|
nodeLibrary = library
|
|
end
|
|
|
|
-- Neat way of making classes in Lua
|
|
local function factory()
|
|
local meta = {}
|
|
meta.__index = meta
|
|
setmetatable(meta,{__call = function(meta,...)
|
|
return setmetatable(meta.new(...),meta)
|
|
end})
|
|
return meta
|
|
end
|
|
|
|
local currentProgram
|
|
local editSource
|
|
local openProgram
|
|
|
|
-- The infamous dragger
|
|
local dragger = function(a,b) return require(_G.Modules.DraggingModule).dragger(a,b,programFrame) end
|
|
|
|
do -- General nodes programming
|
|
local programs = {} -- Live program data being edited. programs[program{options...=...,node{}...}]
|
|
|
|
-- Make a line shape to be going from the two AbsolutePositions given
|
|
local function position_line(line,start,finish)
|
|
local diff = start-finish
|
|
local rotation = math.atan2(diff.y,diff.x)
|
|
local mid = finish:Lerp(start,0.5)
|
|
line.Position = UDim2.new(0,mid.X-line.Parent.AbsolutePosition.X,0,mid.Y-line.Parent.AbsolutePosition.Y)
|
|
line.Rotation = math.deg(rotation)
|
|
line.Size = UDim2.new(0,(diff.Magnitude)+line.Size.Y.Offset,0,line.Size.Y.Offset)
|
|
end
|
|
|
|
local row = factory()
|
|
row.new = function(template,node)
|
|
local this = {}
|
|
this.parent = node
|
|
this.parent.rows[template.name] = this
|
|
this.template = template
|
|
this.frame = nil
|
|
if this.template.input then
|
|
this.link = nil
|
|
this.line = nil
|
|
end
|
|
if this.template.output then
|
|
this.links = {}
|
|
end
|
|
return this
|
|
end
|
|
local function matches(this,other)
|
|
local function types(input,output)
|
|
if not (input.template.input and output.template.output) then return nil end
|
|
if input.template.input == output.template.output then return input.template.input,input,output
|
|
elseif input.template.input == ANY then return output.template.output,input,output
|
|
elseif output.template.output == ANY then return input.template.input,input,output
|
|
else return ANY,input,output end
|
|
end
|
|
local typed,i,o = types(this,other)
|
|
if typed then return typed,i,o end
|
|
local typed,i,o = types(other,this)
|
|
if typed then return typed,i,o end
|
|
return nil
|
|
end
|
|
row.adopt = function(this,frame)
|
|
this.frame = frame
|
|
if this.template.output or this.template.input then
|
|
local point = frame.Point
|
|
this.parent.parent.points[point] = this
|
|
dragger(point,function(initial)
|
|
-- Create a line from the point we are dragging from
|
|
local line = frame.Point:Clone()
|
|
line.AnchorPoint = Vector2.new(0.5,0.5)
|
|
line.Parent = gui
|
|
local color = line.BackgroundColor3
|
|
-- Offset of point, something to do with AnchorPoint I think...
|
|
local offset = Vector2.new(frame.Point.Size.X.Offset,frame.Point.Size.Y.Offset)/2
|
|
-- Other point connected to
|
|
local connected
|
|
return {
|
|
drag = function(position)
|
|
local position = Vector2.new(position.X,position.Y)
|
|
local start = frame.Point.AbsolutePosition + offset
|
|
position_line(line,start,position)
|
|
line.BackgroundColor3 = color
|
|
connected = nil
|
|
for _,object in _G.Gui:GetGuiObjectsAtPosition(position.X,position.Y) do
|
|
local row = this.parent.parent.points[object]
|
|
if row then
|
|
local typed = matches(this,row)
|
|
if typed then
|
|
local point = row.frame.Point
|
|
connected = row
|
|
position_line(line,start,point.AbsolutePosition + offset)
|
|
line.BackgroundColor3 = TYPE_COLOURS[typed.name]
|
|
end
|
|
end
|
|
end
|
|
end,
|
|
ended = function()
|
|
if connected then
|
|
local typed,input,output = matches(this,connected)
|
|
input:connect(output)
|
|
end
|
|
line:Destroy()
|
|
end,
|
|
}
|
|
end)
|
|
elseif this.template.literal then
|
|
local typed = this.template.literal
|
|
if typed == NUMBER or typed == STRING then
|
|
if typed == NUMBER then this.value = this.value or 0
|
|
elseif typed == STRING then this.value = this.value or ""
|
|
end
|
|
this.frame.Text = tostring(this.value)
|
|
this.frame.FocusLost:Connect(function(enter)
|
|
if enter then
|
|
if typed == NUMBER then
|
|
this.value = tonumber(this.frame.Text) or 0
|
|
elseif typed == STRING then
|
|
this.value = string.sub(this.frame.Text or "",1,25565)
|
|
end
|
|
this.frame.Text = tostring(this.value)
|
|
end
|
|
end)
|
|
elseif typed == BOOLEAN then
|
|
if this.value then this.frame.Text = "True" else this.frame.Text = "False" end
|
|
this.frame.Activated:Connect(function()
|
|
this.value = not this.value
|
|
if this.value then this.frame.Text = "True" else this.frame.Text = "False" end
|
|
end)
|
|
elseif typed == PIN or typed == CONTROL then
|
|
local tab
|
|
if typed == PIN then
|
|
tab = PINS_LIST
|
|
elseif typed == CONTROL then
|
|
tab = CONTROL_LIST
|
|
end
|
|
this.value = this.value or tab[1]
|
|
this.frame.Value.Text = this.value
|
|
this.frame.Next.Activated:Connect(function()
|
|
local index = table.find(tab,this.value) + 1
|
|
if index > #tab then index = 1 end
|
|
this.value = tab[index]
|
|
this.frame.Value.Text = this.value
|
|
end)
|
|
this.frame.Previous.Activated:Connect(function()
|
|
local index = table.find(tab,this.value) - 1
|
|
if index < 1 then index = #tab end
|
|
this.value = tab[index]
|
|
this.frame.Value.Text = this.value
|
|
end)
|
|
else
|
|
error("No row type for "..tostring(row.literal))
|
|
end
|
|
end
|
|
end
|
|
-- Make a row's line position correctly if it exists
|
|
row.reconnect = function(this)
|
|
if this.template.input and this.link and this.line then
|
|
local offset = Vector2.new(this.frame.Point.Size.X.Offset,this.frame.Point.Size.Y.Offset)/2
|
|
position_line(
|
|
this.line,
|
|
this.frame.Point.AbsolutePosition + offset,
|
|
this.link.frame.Point.AbsolutePosition + offset
|
|
)
|
|
elseif this.template.output then
|
|
-- As an output we have multiple links, inputs have one, let's just let the inputs do it.
|
|
for _,input in pairs(this.links) do
|
|
input:reconnect()
|
|
end
|
|
end
|
|
end
|
|
-- Disconnect a node from it's incoming link
|
|
row.disconnect = function(this)
|
|
if this.template.output then
|
|
-- Just pass it to the input to handle this.
|
|
local links = {}
|
|
for _,row in pairs(this.links) do
|
|
table.insert(links,row)
|
|
end
|
|
for _,row in pairs(links) do
|
|
row:disconnect()
|
|
end
|
|
--error("Can't disconnect from output, do it on input.",2)
|
|
elseif this.template.input then
|
|
if this.link then -- Check if it is linked
|
|
--print("disc",debug.traceback())
|
|
-- Remove that link from whatever it was linked to as well
|
|
table.remove(this.link.links,table.find(this.link.links,this))
|
|
-- Remove it here
|
|
this.link = nil
|
|
-- Remove the GUI
|
|
if this.line then this.line:Destroy() this.line = nil end
|
|
end
|
|
end
|
|
end
|
|
row.close = function(this)
|
|
if this.template.output or this.template.input then
|
|
if this.line then this.line:Destroy() this.line = nil end
|
|
this.parent.parent.points[this.frame.Point] = nil
|
|
this.frame = nil
|
|
-- No need for this.frame:Destroy() since a row can only be destroyed when a node is, and a row is in the node's frame
|
|
end
|
|
end
|
|
row.connect = function(this,other)
|
|
if this.template.output then
|
|
-- It's not really good to do this either. Input's only have one, simple.
|
|
error("Can't connect from output, do it on input.",2)
|
|
elseif this.template.input then
|
|
this:disconnect()
|
|
this.link = other -- Link up here
|
|
table.insert(other.links,this) -- Link up on the output too
|
|
--print(this,debug.traceback())
|
|
if this.frame then -- Create a line if we have a frame
|
|
local line = this.frame.Point:Clone()
|
|
line.AnchorPoint = Vector2.new(0.5,0.5)
|
|
line.Parent = programViewport
|
|
line.ZIndex = line.ZIndex - 1
|
|
line.InputEnded:Connect(function(input)
|
|
if input.UserInputType == Enum.UserInputType.MouseButton1 or
|
|
input.UserInputType == Enum.UserInputType.Touch then
|
|
this:disconnect()
|
|
end
|
|
end)
|
|
line.BackgroundColor3 = TYPE_COLOURS[matches(this,other).name]
|
|
this.line = line
|
|
end
|
|
this:reconnect()
|
|
end
|
|
end
|
|
|
|
local node = factory()
|
|
node.new = function(template,parent)
|
|
local this = {}
|
|
this.template = template
|
|
-- Row: {frame, template, }
|
|
-- Node: {...,rows[int] = {},[producer_name] = function() end...,[literal_name] = value...}
|
|
this.rows = {}
|
|
for _,template in ipairs(template.rows) do
|
|
row(template,this)
|
|
end
|
|
this.parent = parent
|
|
this.position = Vector2.new(0,0)
|
|
this.frame = nil
|
|
return this
|
|
end
|
|
-- Adopt a frame that this node will respect
|
|
node.adopt = function(this,frame)
|
|
this.position = frame.AbsolutePosition - programViewport.AbsolutePosition
|
|
this.frame = frame
|
|
dragger(frame.NodeLabel,function(initial)
|
|
local offset = frame.AbsolutePosition - initial - programViewport.AbsolutePosition
|
|
return {
|
|
drag = function(position)
|
|
frame.Position = UDim2.new(0,position.X + offset.X,0,position.Y + offset.Y)
|
|
this.position = frame.AbsolutePosition - programViewport.AbsolutePosition
|
|
for _,row in pairs(this.rows) do
|
|
row:reconnect()
|
|
end
|
|
end,
|
|
ended = function(position)
|
|
-- TODO: Add the stupid out of bounds check
|
|
end,
|
|
}
|
|
end)
|
|
frame.NodeLabel.CloseButton.Activated:Connect(function()
|
|
this:destroy()
|
|
this:close()
|
|
end)
|
|
for _,row in pairs(this.rows) do
|
|
-- For every I/O row in the node
|
|
row:adopt(this.frame:FindFirstChild(row.template.name))
|
|
end
|
|
end
|
|
-- Remove all associated GUI
|
|
node.close = function(this)
|
|
if this.frame then
|
|
this.position = this.frame.AbsolutePosition - programViewport.AbsolutePosition
|
|
for _,row in pairs(this.rows) do row:close() end
|
|
this.frame:Destroy()
|
|
this.frame = nil
|
|
end
|
|
end
|
|
-- Remove all links to other structures
|
|
node.destroy = function(this)
|
|
for _,row in pairs(this.rows) do
|
|
row:disconnect()
|
|
end
|
|
table.remove(this.parent.nodes,table.find(this.parent.nodes,this))
|
|
end
|
|
-- Program data that can be executed and has GUI instances bound to it
|
|
local program = factory()
|
|
-- Literally write 'program()' to do program.new
|
|
program.new = function(value,upload)
|
|
local this = {}
|
|
if upload then
|
|
if programs[value] then
|
|
return programs[value]
|
|
else
|
|
programs[value] = this -- Add it to the programs list end
|
|
end
|
|
end
|
|
this.upload_callback = upload
|
|
this.value = value
|
|
this.name = "Unnamed"
|
|
this.frequency = 30
|
|
this.nodes = {}
|
|
this.points = {} -- point to row
|
|
this.tab = nil
|
|
this.variables = {}
|
|
-- Hooks for the value changing in game, the whole program needs to be updated in this case.
|
|
if value.Changed then
|
|
value.Changed:Connect(function()
|
|
if currentProgram == this then this:close() end
|
|
this:load()
|
|
this:compile()
|
|
if currentProgram == this then this:open() end
|
|
print("reloaded")
|
|
-- TODO: Add a program.new here so that users don't lose their program data!
|
|
end)
|
|
end
|
|
if value.Destroying then
|
|
value.Destroying:Connect(function()
|
|
this:close() -- ... So why are they still there then?
|
|
if this.tab then this.tab:Destroy() end
|
|
programs[this.value] = nil
|
|
end)
|
|
end
|
|
return this
|
|
end
|
|
-- Make program adopt a tab
|
|
program.adopt = function(this,tab)
|
|
this.tab = tab
|
|
this.tab.Text = this.name
|
|
this.tab.Close.Activated:Connect(function()
|
|
wait()
|
|
this:close()
|
|
this.tab:Destroy()
|
|
this.tab = nil
|
|
end)
|
|
this.tab.Activated:Connect(function()
|
|
this:open()
|
|
end)
|
|
end
|
|
-- Load into live program data from JSON
|
|
NAME_INDEX = 1 -- For each row and each node too!
|
|
POSITION_INDEX = 2
|
|
ROWS_INDEX = 3
|
|
VALUE_INDEX = 2
|
|
LINK_ID_INDEX = 2
|
|
LINK_ROW_INDEX = 3
|
|
INDEX_TO_NAME = {}
|
|
NAME_TO_INDEX = {}
|
|
for word in ("Value,Input,Output,Text,Add,Div,Mul,Neg,Boolean,Number,Print,Message,Output,Input"):gmatch("[^,%s]+") do -- This string CANNOT change.
|
|
table.insert(INDEX_TO_NAME,word)
|
|
end
|
|
for index,item in pairs(INDEX_TO_NAME) do
|
|
NAME_TO_INDEX[item] = index
|
|
end
|
|
--print(NAME_TRANSLATION)
|
|
program.load = function(this)
|
|
local current = currentProgram == this
|
|
xpcall(function() -- Protection on saves
|
|
local text = this.value.Value
|
|
|
|
if not text then return end
|
|
if text == "" then return end
|
|
|
|
local data = _G.Http:JSONDecode(text)
|
|
|
|
this.name = data.name
|
|
this.version = data.version
|
|
if data.position then this.position = Vector2.new(data.position[1],data.position[2]) end
|
|
|
|
this.nodes = {}
|
|
for index,node in ipairs(data.nodes) do
|
|
local name = node[NAME_INDEX]
|
|
local template = nodeLibrary[INDEX_TO_NAME[name] or name]
|
|
if not template then warn("Could not find node of name: "..INDEX_TO_NAME[name] or name) print(nodeLibrary) continue end
|
|
this.nodes[index] = this:node(template)
|
|
this.nodes[index].position = Vector2.new(node[POSITION_INDEX][1],node[POSITION_INDEX][2])
|
|
end
|
|
|
|
for index,data_node in ipairs(data.nodes) do
|
|
local node = this.nodes[index]
|
|
for _,data_row in pairs(data_node[ROWS_INDEX]) do
|
|
local name = data_row[NAME_INDEX]
|
|
local row = node.rows[INDEX_TO_NAME[name] or name]
|
|
if not row then warn("Could not find row of name: "..(INDEX_TO_NAME[name] or name)) continue end
|
|
if row.template.input then
|
|
row:connect(this.nodes[data_row[LINK_ID_INDEX]].rows[INDEX_TO_NAME[data_row[LINK_ROW_INDEX]] or data_row[LINK_ROW_INDEX]])
|
|
elseif row.template.literal then
|
|
row.value = data_row[VALUE_INDEX]
|
|
end
|
|
end
|
|
end
|
|
|
|
if this.tab then this.tab.Text = this.name end
|
|
this:compile()
|
|
end,function(err)
|
|
warn(debug.traceback(err,2))
|
|
end)
|
|
end
|
|
-- Serialises the live program data
|
|
program.save = function(this)
|
|
local data = {nodes = {}, name = this.name, version = (this.version or 0) + 1}
|
|
|
|
if this.position then data.position = {this.position.X,this.position.Y} end
|
|
|
|
local ids = {}
|
|
-- Make a table where we can get the id of a node using it as a key, ordered.
|
|
for index,node in ipairs(this.nodes) do
|
|
ids[node] = index
|
|
if node.frame then node.position = node.frame.AbsolutePosition - programViewport.AbsolutePosition end
|
|
end
|
|
|
|
for _,node in ipairs(this.nodes) do
|
|
local rows = {}
|
|
for _,row in pairs(node.rows) do
|
|
local name = row.template.name
|
|
if row.template.input and row.link then
|
|
table.insert(rows,{NAME_TO_INDEX[name] or name,ids[row.link.parent],NAME_TO_INDEX[row.link.template.name] or row.link.template.name})
|
|
elseif row.template.literal then
|
|
table.insert(rows,{NAME_TO_INDEX[name] or name,row.value})
|
|
end
|
|
end
|
|
data.nodes[ids[node]] = {
|
|
[NAME_INDEX] = NAME_TO_INDEX[node.template.name] or node.template.name,
|
|
[POSITION_INDEX] = {node.position.X,node.position.Y},
|
|
[ROWS_INDEX] = rows
|
|
}
|
|
end
|
|
|
|
return _G.Http:JSONEncode(data)
|
|
end
|
|
program.upload = function(this)
|
|
this.upload_callback(this:save())
|
|
end
|
|
-- Connects everything together in a program so it is accessible from the node
|
|
program.compile = function(this)
|
|
for _,node in pairs(this.nodes) do
|
|
for _,row in pairs(node.rows) do
|
|
if row.template.input then
|
|
if row.link then
|
|
node[row.template.name] = function() return row.link.template.producer(row.link.parent,this) end
|
|
else
|
|
node[row.template.name] = function() end
|
|
end
|
|
end
|
|
if row.template.literal then
|
|
node[row.template.name] = row.value
|
|
end
|
|
end
|
|
end
|
|
end
|
|
-- Run a program from its data
|
|
program.run = function(this,context)
|
|
program.context = context
|
|
for _,node in pairs(this.nodes) do
|
|
if node.template.callback then coroutine.wrap(_G.Protect(node.template.callback))(node,this) end
|
|
end
|
|
end
|
|
-- Open a program into the editor/viewport
|
|
program.open = function(this)
|
|
-- TODO: Add a selection box around the currently operating tab
|
|
-- TODO: Add a background tint if the script has not been saved yet
|
|
if currentProgram then currentProgram:close() end
|
|
|
|
currentProgram = this
|
|
programViewportEmpty.Visible = false
|
|
highlight.Adornee = this.value.Parent
|
|
programsSelectionBox.Parent = this.tab
|
|
for _,node in ipairs(this.nodes) do
|
|
local frame = node.template.frame:Clone()
|
|
frame.Parent = programViewport
|
|
frame.Position = UDim2.new(0,node.position.X,0,node.position.Y)
|
|
node:adopt(frame)
|
|
end
|
|
if this.position then
|
|
local pos = programViewport.Position
|
|
programViewport.Position = UDim2.new(pos.X.Scale,this.position.X,pos.Y.Scale,this.position.Y)
|
|
end
|
|
-- This is quite bad:
|
|
for _,node in ipairs(this.nodes) do
|
|
for _,row in pairs(node.rows) do
|
|
if row.link then row:connect(row.link) end -- To get links to regenerate after everything is adopted
|
|
end
|
|
end
|
|
end
|
|
-- Remove all GUI hooks in data
|
|
program.close = function(this)
|
|
for _,node in pairs(this.nodes) do
|
|
node:close()
|
|
end
|
|
if _G.IsClient then
|
|
programViewportEmpty.Visible = true
|
|
programsSelectionBox.Parent = nil
|
|
currentProgram = nil
|
|
highlight.Adornee = nil
|
|
end
|
|
end
|
|
-- Remove a node, resolve connections and remove GUI
|
|
program.remove = function(this,node)
|
|
node:close()
|
|
node:destroy()
|
|
table.remove(this.nodes,table.find(this.nodes,node))
|
|
end
|
|
-- Add a node, candidly on the ScreenGui (lol)
|
|
program.node = function(this,...)
|
|
local node = node(...,this)
|
|
table.insert(this.nodes,node)
|
|
return node
|
|
end
|
|
-- Make a new program, add it to the list, read into it and open it out.
|
|
local function edit(value,upload)
|
|
-- Value is a StringValue instance or a table binding to this, so return if it's already here!
|
|
|
|
local this = program(value,upload)
|
|
|
|
if not this.tab then
|
|
local tab = templateProgramTab:Clone()
|
|
tab.Parent = programsList
|
|
tab.Visible = true
|
|
this:adopt(tab)
|
|
end
|
|
|
|
if currentProgram then currentProgram:close() end
|
|
this:load()
|
|
this:open()
|
|
end
|
|
|
|
editSource = edit
|
|
openProgram = program
|
|
end
|
|
|
|
if _G.IsClient then -- General GUI interaction
|
|
programCancelButton.Activated:Connect(function()
|
|
programFrame.Visible = false
|
|
if currentProgram then currentProgram:close() end
|
|
end)
|
|
programUploadButton.Activated:Connect(function()
|
|
if currentProgram then
|
|
local program = currentProgram
|
|
_G.Events.Notification:Fire("Program Upload","Attempting program upload.")
|
|
program:upload()
|
|
-- Optional, this should always work but it's not necessary
|
|
program:close()
|
|
program:load()
|
|
program:open()
|
|
else
|
|
_G.Events.Notification:Fire("Program Upload","No program is currently loaded.")
|
|
end
|
|
end)
|
|
dragger(programViewport,function(last)
|
|
local lower = Vector2.new(-math.huge,-math.huge)
|
|
local upper = Vector2.new(math.huge,math.huge)
|
|
if currentProgram then
|
|
for _,node in pairs(currentProgram.nodes) do
|
|
node = node.frame
|
|
lower = Vector2.max(lower,node.AbsolutePosition - programViewport.AbsolutePosition)
|
|
upper = Vector2.min(upper,node.AbsolutePosition - programViewport.AbsolutePosition + node.AbsoluteSize - programViewport.AbsoluteSize)
|
|
end
|
|
end
|
|
local original = programViewport.Position
|
|
local positions = {}
|
|
return {
|
|
drag = function(position)
|
|
local offset = position - last
|
|
programViewport.Position = UDim2.new(original.X.Scale,original.X.Offset + offset.X,original.Y.Scale,original.Y.Offset + offset.Y)
|
|
currentProgram.position = Vector2.new(original.X.Offset + offset.X,original.Y.Offset + offset.Y)
|
|
--[[if not currentProgram then return end
|
|
for _,node in pairs(currentProgram.nodes) do
|
|
if not positions[node] then positions[node] = node.position end
|
|
node.position = positions[node] + Vector2.min(Vector2.max(position - last,-lower),-upper)
|
|
node.frame.Position = UDim2.new(0,node.position.X,0,node.position.Y)
|
|
for _,row in pairs(node.rows) do
|
|
row:reconnect()
|
|
end
|
|
end]]
|
|
end
|
|
}
|
|
end)
|
|
--[[local pinchState
|
|
local pinchScale
|
|
programViewport.TouchPinch:Connect(function(_,scale,_,state)
|
|
pinchState = state
|
|
pinchScale
|
|
end)
|
|
]]
|
|
programViewport.MouseWheelBackward:Connect(function()
|
|
programViewport.UIScale.Scale = math.clamp(programViewport.UIScale.Scale - 0.1,0.2,2)
|
|
end)
|
|
programViewport.MouseWheelForward:Connect(function()
|
|
programViewport.UIScale.Scale = math.clamp(programViewport.UIScale.Scale + 0.1,0.2,2)
|
|
end)
|
|
programNameBox.FocusLost:Connect(function(enter)
|
|
if enter and currentProgram then
|
|
currentProgram.name = string.sub(programNameBox.Text,1,128)
|
|
currentProgram.tab.Text = currentProgram.name
|
|
end
|
|
end)
|
|
-- GIVEN 'init()' on inventory
|
|
for _,frame in pairs(programInventory:GetChildren()) do
|
|
if nodeLibrary[frame] then -- This is a real node frame we can place
|
|
frame.ZIndex = -1
|
|
dragger(frame.NodeLabel,function(initial)
|
|
local offset = frame.AbsolutePosition - initial
|
|
frame.Parent = gui
|
|
return {
|
|
drag = function(position)
|
|
frame.Position = UDim2.new(0,position.X + offset.X,0,position.Y + offset.Y)
|
|
end,
|
|
ended = function(position)
|
|
local lowX = programViewport.AbsolutePosition.X
|
|
local highX = lowX + programViewport.AbsoluteSize.X
|
|
local lowY = programViewport.AbsolutePosition.Y
|
|
local highY = lowY + programViewport.AbsoluteSize.Y
|
|
if position.X > lowX and
|
|
position.X < highX and
|
|
position.Y > lowY and
|
|
position.Y < highY and
|
|
currentProgram
|
|
then
|
|
-- Create a new node frame
|
|
local template = frame
|
|
local frame = template:Clone()
|
|
local position = template.AbsolutePosition - programViewport.AbsolutePosition
|
|
template.Parent = programInventory
|
|
frame.Parent = programViewport
|
|
frame.Position = UDim2.new(0,position.X,0,position.Y)
|
|
-- Create new node data
|
|
local node = currentProgram:node(nodeLibrary[template])
|
|
node:adopt(frame) -- Make the data adopt the frame
|
|
end
|
|
frame.Parent = programInventory
|
|
end,
|
|
}
|
|
end)
|
|
end
|
|
end
|
|
end
|
|
|
|
return {edit_source = editSource, open_program = openProgram} |