Files
PlaneBuilding/scripts/modules/ProgramModule.lua
T

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}