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}