local D={}local Y=game:GetService("\080\108\097\121\101\114\115")local R=game:GetService("\083\116\117\100\105\111\083\101\114\118\105\099\101")local y=game:GetService("\082\117\110\083\101\114\118\105\099\101")local c=game:GetService("\067\104\097\110\103\101\072\105\115\116\111\114\121\083\101\114\118\105\099\101")local i=game:GetService("\083\101\108\101\099\116\105\111\110")local v=require(script:FindFirstAncestor("\080\108\117\103\105\110\070\114\097\109\101\119\111\114\107"))local H local I local p local g local F local m local q local d local Q local r local u=2 local K=y:IsServer()local b=y:IsClient()local k=y:IsRunning()local function L()local D={type=p.RoduxActionType.MACROS.REFRESH,data={}};(g:GetStore()):dispatch(D)end local function P(D,Y,...)local R={...}local y local i local v local H=table.pack(xpcall(function()v=Y and c:TryBeginRecording("\077\097\099\114\111")or nil D(table.unpack(R))if Y then if v then c:FinishRecording(v,Enum.FinishRecordingOperation.Commit)else I:Warn("\082\101\099\111\114\100\105\110\103 \070\097\105\108\101\100")end end end,function(D)y=debug.traceback()i=D if v then c:FinishRecording(v,Enum.FinishRecordingOperation.Cancel)end end))if H[1]then return true,nil,nil end return false,i,y end function D.RunMacro(Y,D)for Y,R in pairs(D.Fields)do local y=D.State.FieldValues[R.Name]if R.IsRequired and y==nil then I:MacroWarn(D,("\070\105\101\108\100 \037\113 \105\115 \114\101\113\117\105\114\101\100\033"):format(R.Name))return end local c=R.Validator and R.Validator(y)if c then I:MacroWarn(D,("\070\105\101\108\100 \037\113\058 \037\115"):format(R.Name,c))return end end local R=false task.spawn(function()task.wait(u)if not R then I:MacroWarn(D,("\077\097\099\114\111 \104\097\115 \098\101\101\110 \121\105\101\108\100\105\110\103 \102\111\114 \109\111\114\101 \116\104\097\110 \037\100 \115\101\099\111\110\100\115\046\046 \119\105\108\108 \099\097\117\115\101 \117\110\105\110\116\101\110\100\101\100 \098\101\104\097\118\105\111\117\114\046 \073\102 \121\111\117 \109\117\115\116 \121\105\101\108\100\044 \117\115\101 \115\111\109\101\116\104\105\110\103 \108\105\107\101 \116\097\115\107\046\100\101\102\101\114\040\041"):format(u))end end)local y,c,i=P(D.Function,D.EnableAutomaticUndo,D,F:GetPlugin())R=true if not y then I:MacroWarn(D,("\069\082\082\079\082\058 \037\115\n\037\115"):format(c,i))end if D.State.IsRunning~=nil then L()end return D.State end function D.RunMacroAt(v,y,c,i)if not k then I:Error("\066\097\100 \065\080\073\059 \111\110\108\121 \117\115\101 \082\117\110\077\097\099\114\111\065\116 \119\104\101\110 \112\108\097\121\105\110\103\046")end if K and c then local Y=D:RunMacro(y)y.State._Server.IsRunning=Y.IsRunning==nil and y.State._Server.IsRunning or Y.IsRunning L()elseif b and i then local Y=D:RunMacro(y)y.State._Client.IsRunning=Y.IsRunning==nil and y.State._Client.IsRunning or Y.IsRunning L()end if K and i then local D=Y:GetPlayerByUserId(R:GetUserId())local c=q:RunMacroOnClient(D,y)y.State._Client.IsRunning=c.IsRunning==nil and y.State._Client.IsRunning or c.IsRunning L()return end if b and c then local D=q:RunMacroOnServer(y)y.State._Server.IsRunning=D.IsRunning==nil and y.State._Server.IsRunning or D.IsRunning L()return end end function D.ShowDescription(Y,D)local R=("\061\061\061\061\061\061\061\061\061\061\061\061\061\061\061\061 \037\115 \040\037\115\041 \124 \068\069\083\067\082\073\080\084\073\079\078 \061\061\061\061\061\061\061\061\061\061\061\061\061\061\061\061"):format(D.Name,D.Group)print("\n",R,"\n\n",D.Description,"\n\n",R)end function D.ViewSource(Y,D)(F:GetPlugin()):OpenScript(D)i:Set({D})end function D.ToggleKeybind(Y,D)local R=g:GetStore()local y={type=p.RoduxActionType.MACROS.TOGGLE_KEYBIND;data={script=D}}R:dispatch(y)end local function x(D,Y,R,y,c)local i=Y[y]local v=i and typeof(i)if Y[y]==nil and R then I:Warn(("\077\097\099\114\111 \037\115 \100\111\101\115 \110\111\116 \104\097\118\101 \114\101\113\117\105\114\101\100 \101\110\116\114\121 \037\113 \040\037\115\041 \040\037\115\041"):format(D.Name,y,c,D:GetFullName()))return false end if Y[y]and typeof(Y[y])~=c then I:Warn(("\077\097\099\114\111 \037\115 \101\110\116\114\121 \037\113 \101\120\112\101\099\116\101\100 \116\121\112\101 \037\113\044 \103\111\116 \037\113 \040\037\115\041"):format(D.Name,y,c,v,D:GetFullName()))return false end return true end local function l(D)local Y=Q:GetSetting("\079\083\084\121\112\101")local R=H.MacKeyCodes local y=Y=="\077\097\099"and R.WindowsToMac or R.MacToWindows for Y,R in pairs(D)do D[Y]=y[R]or R end end function D.CleanMacroDefinition(R,D,Y)if not x(D,Y,false,"\068\105\115\097\098\108\101\100","\098\111\111\108\101\097\110")then return end Y.Disabled=Y.Disabled or false if Y.Disabled then Y.AutoRun=false Y.Name=Y.Name or"\068\105\115\097\098\108\101\100"Y.Function=function() end Y.BindToClose=function() end Y.BindToOpen=function() end end Y.Group=Y.Group or"\078\111 \071\114\111\117\112"if not x(D,Y,true,"\071\114\111\117\112","\115\116\114\105\110\103")then return end if not x(D,Y,false,"\071\114\111\117\112\067\111\108\111\114","\067\111\108\111\114\051")then return end if not x(D,Y,false,"\071\114\111\117\112\073\099\111\110","\115\116\114\105\110\103")then return end if not x(D,Y,false,"\071\114\111\117\112\073\099\111\110\067\111\108\111\114","\067\111\108\111\114\051")then return end if not x(D,Y,true,"\078\097\109\101","\115\116\114\105\110\103")then return end if not x(D,Y,false,"\078\097\109\101\067\111\108\111\114","\067\111\108\111\114\051")then return end if not x(D,Y,false,"\073\099\111\110","\115\116\114\105\110\103")then return end if not x(D,Y,false,"\073\099\111\110\067\111\108\111\114","\067\111\108\111\114\051")then return end Y.Description=Y.Description or"\078\111 \068\101\115\099\114\105\112\116\105\111\110"if not x(D,Y,true,"\068\101\115\099\114\105\112\116\105\111\110","\115\116\114\105\110\103")then return end Y.LayoutOrder=Y.LayoutOrder or 0 if not x(D,Y,false,"\076\097\121\111\117\116\079\114\100\101\114","\110\117\109\098\101\114")then return end if not x(D,Y,false,"\083\116\097\116\101","\116\097\098\108\101")then return end Y.State=Y.State or{}Y.State.IsRunning=false Y.State.FieldValues=Y.State.FieldValues or{}Y.State.IsKeybindDisabled=false Y.State._Server=Y.State._Server or{}Y.State._Client=Y.State._Client or{}if not x(D,Y,false,"\069\110\097\098\108\101\065\117\116\111\109\097\116\105\099\085\110\100\111","\098\111\111\108\101\097\110")then return end if not x(D,Y,false,"\065\117\116\111\082\117\110","\098\111\111\108\101\097\110")then return end if not x(D,Y,false,"\073\103\110\111\114\101\071\097\109\101\080\114\111\099\101\115\115\101\100\075\101\121\098\105\110\100\115","\098\111\111\108\101\097\110")then return end Y.IsLocalMacro=D:IsDescendantOf(r:GetOurDirectory())and true or false Y.Keybind=Y.Keybind or{}if not x(D,Y,false,"\075\101\121\098\105\110\100","\116\097\098\108\101")then return end for Y,R in pairs(Y.Keybind)do if not R.EnumType==Enum.KeyCode then I:Warn(("\077\097\099\114\111 \037\115 \096\075\101\121\098\105\110\100\096 \109\117\115\116 \104\097\118\101 \118\097\108\117\101\115 \111\102 \111\110\108\121 \116\121\112\101 \096\069\110\117\109\046\075\101\121\067\111\100\101\096 \040\037\115\041"):format(D.Name,D:GetFullName()))return end end l(Y.Keybind)Y.Fields=Y.Fields or{}if not x(D,Y,false,"\070\105\101\108\100\115","\116\097\098\108\101")then return end local y={}for R,c in pairs(Y.Fields)do if typeof(c)~="\116\097\098\108\101"then("\077\097\099\114\111 \037\115 \096\070\105\101\108\100\115\096 \109\117\115\116 \099\111\110\116\097\105\110 \116\097\098\108\101\115\044 \097\116\108\101\097\115\116 \119\105\116\104 \101\110\116\114\105\101\115 \096\078\097\109\101\096 \097\110\100 \096\084\121\112\101\096 \040\037\115\041"):format(D.Name,D:GetFullName())return end local i=c.Name if not(i and typeof(i)=="\115\116\114\105\110\103")then I:Warn(("\077\097\099\114\111 \037\115 \104\097\115 \097 \102\105\101\108\100 \119\105\116\104 \105\110\118\097\108\105\100 \096\078\097\109\101\096 \040\115\116\114\105\110\103\041 \040\037\115\041"):format(D.Name,D:GetFullName()))return end if y[i]then I:Warn(("\070\105\101\108\100 \037\113 \105\115 \097 \100\117\112\108\105\099\097\116\101 \110\097\109\101 \040\037\115\041"):format(c.Name,D:GetFullName()))return end local v=c.Type local p=H.FieldType[v]if not p then I:Warn(("\070\105\101\108\100 \037\113 \104\097\115 \105\110\118\097\108\105\100 \102\105\101\108\100 \116\121\112\101 \037\113 \040\037\115\041"):format(c.Name,v,D:GetFullName()))return end local g=c.IsRequired if g~=nil and typeof(g)~="\098\111\111\108\101\097\110"then I:Warn(("\077\097\099\114\111 \037\115 \104\097\115 \097 \102\105\101\108\100 \119\105\116\104 \105\110\118\097\108\105\100 \096\073\115\082\101\113\117\105\114\101\100\096 \040\098\111\111\108\101\097\110\041 \040\037\115\041"):format(D.Name,D:GetFullName()))return end local F=c.Validator if F~=nil and typeof(F)~="\102\117\110\099\116\105\111\110"then I:Warn(("\077\097\099\114\111 \037\115 \104\097\115 \097 \102\105\101\108\100 \119\105\116\104 \105\110\118\097\108\105\100 \096\086\097\108\105\100\097\116\111\114\096 \040\102\117\110\099\116\105\111\110\041 \040\037\115\041"):format(D.Name,D:GetFullName()))return end local m=Y.State.FieldValues[i]if m~=nil then local Y,R=pcall(function()return H.FieldType[v].Validate(m)end)if not Y or R==nil then I:Warn(("\077\097\099\114\111 \037\115 \070\105\101\108\100 \037\115 \104\097\115 \097 \098\097\100 \100\101\102\097\117\108\116 \118\097\108\117\101 \115\101\116\046 \070\105\101\108\100 \104\097\115 \116\121\112\101 \037\115\044 \100\101\102\097\117\108\116 \118\097\108\117\101 \105\115 \037\113 \040\037\115\041 \040\037\115\041"):format(D.Name,i,v,tostring(m),tostring(R),D:GetFullName()))return end end c.Type=p end if not x(D,Y,false,"\070\105\101\108\100\067\104\097\110\103\101\100","\066\105\110\100\097\098\108\101\069\118\101\110\116")then return end Y.FieldChanged=Y.FieldChanged or Instance.new("\066\105\110\100\097\098\108\101\069\118\101\110\116")if not x(D,Y,false,"\074\097\110\105\116\111\114","\116\097\098\108\101")then return end Y.RunJanitor=Y.RunJanitor or d.new()if not x(D,Y,false,"\084\111\103\103\108\101\073\115\082\117\110\110\105\110\103","\102\117\110\099\116\105\111\110")then return end Y.ToggleIsRunning=function(D)D.State.IsRunning=not(D.State.IsRunning and true or false)D.RunJanitor:Cleanup()end if not x(D,Y,false,"\073\115\082\117\110\110\105\110\103","\102\117\110\099\116\105\111\110")then return end Y.IsRunning=function(D)return D.State.IsRunning and true or false end if not x(D,Y,false,"\071\101\116\070\105\101\108\100\086\097\108\117\101","\102\117\110\099\116\105\111\110")then return end Y.GetFieldValue=function(D,Y)return D.State.FieldValues[Y]end if not x(D,Y,true,"\070\117\110\099\116\105\111\110","\102\117\110\099\116\105\111\110")then return end if not x(D,Y,false,"\066\105\110\100\084\111\079\112\101\110","\102\117\110\099\116\105\111\110")then return end if not x(D,Y,false,"\066\105\110\100\084\111\067\108\111\115\101","\102\117\110\099\116\105\111\110")then return end Y._BindToClose=function(D,R)if D.BindToClose then D.BindToClose(Y,R)end D.State.IsRunning=false D.RunJanitor:Cleanup()end Y._script=D Y._isBroken=Y._isBroken or false return Y end function D.ClearField(R,D,Y)D.State.FieldValues[Y.Name]=nil L()end function D.UpdateField(y,D,Y,R)local c=Y.Type.Validate(R)if c==nil then local R=D.State.FieldValues[Y.Name]return R,false end D.State.FieldValues[Y.Name]=c D.FieldChanged:Fire(Y.Name,c)L()return c,true end function D.FrameworkInit(D)I=v:Require("\076\111\103\103\101\114")H=v:Require("\077\097\099\114\111\067\111\110\115\116\097\110\116\115")g=v:Require("\083\111\099\107\101\116\067\111\110\116\114\111\108\108\101\114")p=v:Require("\083\111\099\107\101\116\067\111\110\115\116\097\110\116\115")F=v:Require("\080\108\117\103\105\110\072\097\110\100\108\101\114")m=v:Require("\083\116\117\100\105\111\072\097\110\100\108\101\114")q=v:Require("\077\097\099\114\111\067\108\105\101\110\116\083\101\114\118\101\114")d=v:Require("\074\097\110\105\116\111\114")Q=v:Require("\083\111\099\107\101\116\083\101\116\116\105\110\103\115")r=v:Require("\076\111\099\097\108\077\097\099\114\111\115")end function D.FrameworkStart(D) end return D
---
---Helper functions for macros
---
---@class MacroHelper
---
local MacroHelper = {}

--------------------------------------------------
-- Types
-- ...

--------------------------------------------------
-- Dependencies
local Players = game:GetService("Players") ---@type Players
local StudioService = game:GetService("StudioService") ---@type StudioService
local RunService = game:GetService("RunService") ---@type RunService
local ChangeHistoryService = game:GetService("ChangeHistoryService") ---@type ChangeHistoryService
local Selection = game:GetService("Selection") ---@type Selection
local PluginFramework = require(script:FindFirstAncestor("PluginFramework")) ---@type Framework
local MacroConstants ---@type MacroConstants
local Logger ---@type Logger
local SocketConstants ---@type SocketConstants
local SocketController ---@type SocketController
local PluginHandler ---@type PluginHandler
local StudioHandler ---@type StudioHandler
local MacroClientServer ---@type MacroClientServer
local Janitor ---@type Janitor
local SocketSettings ---@type SocketSettings
local LocalMacros ---@type LocalMacros

--------------------------------------------------
-- Constants
local YIELD_TIME = 2
local IS_SERVER = RunService:IsServer()
local IS_CLIENT = RunService:IsClient()
local IS_RUNNING = RunService:IsRunning()

--------------------------------------------------
-- Members

---Refreshes the state after we update a field
local function refreshState()
    -- Update RoduxStore
    ---@type RoduxAction
    local action = {
        type = SocketConstants.RoduxActionType.MACROS.REFRESH,
        data = {},
    }
    SocketController:GetStore():dispatch(action)
end

local function getMessageAndTracebackOnError(func, enableUndo, ...) -- similar to "tpcall" below
    local args = { ... }

    local traceback
    local errMsg
    local recording
    local result = table.pack(xpcall(function()
        recording = enableUndo and ChangeHistoryService:TryBeginRecording("Macro") or nil

        func(table.unpack(args))

        if enableUndo then
            if recording then
                ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit)
            else
                Logger:Warn("Recording Failed")
            end
        end
    end, function(err)
        traceback = debug.traceback()
        errMsg = err

        if recording then
            ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
        end
    end))

    if result[1] then
        return true, nil, nil
    end

    return false, errMsg, traceback
end

---
---Runs a macro, then returns its state after the fact.
---@param macro MacroDefinition
---@return MacroState
---
function MacroHelper:RunMacro(macro)
    -- Check Fields
    for _, field in pairs(macro.Fields) do
        local currentValue = macro.State.FieldValues[field.Name]
        if field.IsRequired and currentValue == nil then
            Logger:MacroWarn(macro, ("Field %q is required!"):format(field.Name))
            return
        end

        local validatorMessage = field.Validator and field.Validator(currentValue)
        if validatorMessage then
            Logger:MacroWarn(macro, ("Field %q: %s"):format(field.Name, validatorMessage))
            return
        end
    end

    -- Task to ensure the function isn't yielding..
    local threadIsGood = false
    task.spawn(function()
        task.wait(YIELD_TIME)
        if not threadIsGood then
            Logger:MacroWarn(
                macro,
                ("Macro has been yielding for more than %d seconds.. will cause unintended behaviour. If you must yield, use something like task.defer()"):format(
                    YIELD_TIME
                )
            )
        end
    end)

    -- Run Macro
    local success, err, traceback =
        getMessageAndTracebackOnError(macro.Function, macro.EnableAutomaticUndo, macro, PluginHandler:GetPlugin())
    threadIsGood = true

    -- Show error
    if not success then
        Logger:MacroWarn(macro, ("ERROR: %s\n%s"):format(err, traceback))
    end

    -- Macro has `isRunning` enabled, so ensure to refresh the state!
    if macro.State.IsRunning ~= nil then
        refreshState()
    end

    return macro.State
end

---
---Will run the macro at the specified location, regardless of this being called on server or client.
---Warning: Can be called on both client and server simultaneously
---@param macro MacroDefinition
---@param onServer boolean
---@param onClient boolean
---
function MacroHelper:RunMacroAt(macro, onServer, onClient)
    -- ERROR: Bad api
    if not IS_RUNNING then
        Logger:Error("Bad API; only use RunMacroAt when playing.")
    end

    -- Run in this context
    if IS_SERVER and onServer then
        local macroState = MacroHelper:RunMacro(macro)
        macro.State._Server.IsRunning = (macroState.IsRunning == nil and macro.State._Server.IsRunning) or macroState.IsRunning
        refreshState()
    elseif IS_CLIENT and onClient then
        local macroState = MacroHelper:RunMacro(macro)
        macro.State._Client.IsRunning = (macroState.IsRunning == nil and macro.State._Client.IsRunning) or macroState.IsRunning
        refreshState()
    end

    -- Server -> Client
    if IS_SERVER and onClient then
        local player = Players:GetPlayerByUserId(StudioService:GetUserId())
        local macroState = MacroClientServer:RunMacroOnClient(player, macro)
        macro.State._Client.IsRunning = (macroState.IsRunning == nil and macro.State._Client.IsRunning) or macroState.IsRunning
        refreshState()
        return
    end

    -- Client -> Server
    if IS_CLIENT and onServer then
        local macroState = MacroClientServer:RunMacroOnServer(macro)
        macro.State._Server.IsRunning = (macroState.IsRunning == nil and macro.State._Server.IsRunning) or macroState.IsRunning
        refreshState()
        return
    end
end

---
---@param macro MacroDefinition
---
function MacroHelper:ShowDescription(macro)
    local descriptionHolderString = ("================ %s (%s) | DESCRIPTION ================"):format(macro.Name, macro.Group)
    print("\n", descriptionHolderString, "\n\n", macro.Description, "\n\n", descriptionHolderString)
end

---
---@param macroScript ModuleScript
---
function MacroHelper:ViewSource(macroScript)
    PluginHandler:GetPlugin():OpenScript(macroScript)
    Selection:Set({ macroScript })
end

---
---@param macroScript ModuleScript
---
function MacroHelper:ToggleKeybind(macroScript)
    -- Get Store
    local roduxStore = SocketController:GetStore()

    -- Send action
    ---@type RoduxAction
    local action = {
        type = SocketConstants.RoduxActionType.MACROS.TOGGLE_KEYBIND,
        data = {
            script = macroScript,
        },
    }
    roduxStore:dispatch(action)
end

---Checks the type of a macro member is correct.
---Returns true if good.
---@param macro MacroDefinition
---@param key string
---@param value any
---@param type string
---@return boolean
local function validateType(macroScript, macro, isRequired, key, type)
    -- Get Variables
    local ourValue = macro[key]
    local ourType = ourValue and typeof(ourValue)

    -- FALSE: Required key is missing
    if macro[key] == nil and isRequired then
        Logger:Warn(("Macro %s does not have required entry %q (%s) (%s)"):format(macroScript.Name, key, type, macroScript:GetFullName()))
        return false
    end

    -- FALSE: Wrong type
    if macro[key] and typeof(macro[key]) ~= type then
        Logger:Warn(
            ("Macro %s entry %q expected type %q, got %q (%s)"):format(macroScript.Name, key, type, ourType, macroScript:GetFullName())
        )
        return false
    end

    -- TRUE
    return true
end

---Given a keybind, ensures it matches the clients OS
---@param keybind Enum.KeyCode[]
local function translateKeybindToOs(keybind)
    local osType = SocketSettings:GetSetting("OSType")
    local macKeyCodes = MacroConstants.MacKeyCodes
    local keycodeMap = osType == "Mac" and macKeyCodes.WindowsToMac or macKeyCodes.MacToWindows

    for k, keyCode in pairs(keybind) do
        keybind[k] = keycodeMap[keyCode] or keyCode
    end
end

---Will clean up a macro definiton direct from require().
---Injects expected data structures.
---Ensures inputted data is valid.
---Warns of any issues.
---Returns when missing vital information.
---@param macroScript ModuleScript
---@param macro MacroDefinition
---@return MacroDefinition
function MacroHelper:CleanMacroDefinition(macroScript, macro)
    -- Disabled
    if not validateType(macroScript, macro, false, "Disabled", "boolean") then
        return
    end
    macro.Disabled = macro.Disabled or false

    -- Disable some stuff if the macro is disabled!
    if macro.Disabled then
        macro.AutoRun = false
        macro.Name = macro.Name or "Disabled"
        macro.Function = function() end
        macro.BindToClose = function() end
        macro.BindToOpen = function() end
    end

    -- Group
    macro.Group = macro.Group or "No Group"
    if not validateType(macroScript, macro, true, "Group", "string") then
        return
    end
    if not validateType(macroScript, macro, false, "GroupColor", "Color3") then
        return
    end
    if not validateType(macroScript, macro, false, "GroupIcon", "string") then
        return
    end
    if not validateType(macroScript, macro, false, "GroupIconColor", "Color3") then
        return
    end

    -- Name
    if not validateType(macroScript, macro, true, "Name", "string") then
        return
    end
    if not validateType(macroScript, macro, false, "NameColor", "Color3") then
        return
    end

    -- Icon
    if not validateType(macroScript, macro, false, "Icon", "string") then
        return
    end
    if not validateType(macroScript, macro, false, "IconColor", "Color3") then
        return
    end

    -- Description
    macro.Description = macro.Description or "No Description"
    if not validateType(macroScript, macro, true, "Description", "string") then
        return
    end

    -- LayoutOrder
    macro.LayoutOrder = macro.LayoutOrder or 0
    if not validateType(macroScript, macro, false, "LayoutOrder", "number") then
        return
    end

    -- State
    if not validateType(macroScript, macro, false, "State", "table") then
        return
    end
    macro.State = macro.State or {}
    macro.State.IsRunning = false
    macro.State.FieldValues = macro.State.FieldValues or {}
    macro.State.IsKeybindDisabled = false
    macro.State._Server = macro.State._Server or {}
    macro.State._Client = macro.State._Client or {}

    -- EnableAutomaticUndo
    if not validateType(macroScript, macro, false, "EnableAutomaticUndo", "boolean") then
        return
    end

    -- AutoRun
    if not validateType(macroScript, macro, false, "AutoRun", "boolean") then
        return
    end

    -- IgnoreGameProcessedKeybinds
    if not validateType(macroScript, macro, false, "IgnoreGameProcessedKeybinds", "boolean") then
        return
    end

    -- IsLocalMacro
    macro.IsLocalMacro = macroScript:IsDescendantOf(LocalMacros:GetOurDirectory()) and true or false

    -- Keybind
    macro.Keybind = macro.Keybind or {}
    if not validateType(macroScript, macro, false, "Keybind", "table") then
        return
    end
    for _, keyCode in pairs(macro.Keybind) do
        if not keyCode.EnumType == Enum.KeyCode then
            Logger:Warn(
                ("Macro %s `Keybind` must have values of only type `Enum.KeyCode` (%s)"):format(macroScript.Name, macroScript:GetFullName())
            )
            return
        end
    end
    translateKeybindToOs(macro.Keybind)

    -- Fields
    macro.Fields = macro.Fields or {}
    if not validateType(macroScript, macro, false, "Fields", "table") then
        return
    end
    local fieldNames = {}
    for _, field in pairs(macro.Fields) do
        if typeof(field) ~= "table" then
            ("Macro %s `Fields` must contain tables, atleast with entries `Name` and `Type` (%s)"):format(
                macroScript.Name,
                macroScript:GetFullName()
            )
            return
        end

        local fieldName = field.Name
        if not (fieldName and typeof(fieldName) == "string") then
            Logger:Warn(("Macro %s has a field with invalid `Name` (string) (%s)"):format(macroScript.Name, macroScript:GetFullName()))
            return
        end
        if fieldNames[fieldName] then
            Logger:Warn(("Field %q is a duplicate name (%s)"):format(field.Name, macroScript:GetFullName()))
            return
        end

        local fieldTypeId = field.Type
        local fieldType = MacroConstants.FieldType[fieldTypeId]
        if not fieldType then
            Logger:Warn(("Field %q has invalid field type %q (%s)"):format(field.Name, fieldTypeId, macroScript:GetFullName()))
            return
        end

        local isRequired = field.IsRequired
        if isRequired ~= nil and (typeof(isRequired) ~= "boolean") then
            Logger:Warn(
                ("Macro %s has a field with invalid `IsRequired` (boolean) (%s)"):format(macroScript.Name, macroScript:GetFullName())
            )
            return
        end

        local validator = field.Validator
        if validator ~= nil and typeof(validator) ~= "function" then
            Logger:Warn(
                ("Macro %s has a field with invalid `Validator` (function) (%s)"):format(macroScript.Name, macroScript:GetFullName())
            )
            return
        end

        local defaultValue = macro.State.FieldValues[fieldName]
        if defaultValue ~= nil then
            local success, result = pcall(function()
                return MacroConstants.FieldType[fieldTypeId].Validate(defaultValue)
            end)
            if not success or result == nil then
                Logger:Warn(
                    ("Macro %s Field %s has a bad default value set. Field has type %s, default value is %q (%s) (%s)"):format(
                        macroScript.Name,
                        fieldName,
                        fieldTypeId,
                        tostring(defaultValue),
                        tostring(result),
                        macroScript:GetFullName()
                    )
                )
                return
            end
        end

        field.Type = fieldType
    end

    -- FieldChanged Event
    if not validateType(macroScript, macro, false, "FieldChanged", "BindableEvent") then
        return
    end
    macro.FieldChanged = macro.FieldChanged or Instance.new("BindableEvent")

    -- Run Janitor
    if not validateType(macroScript, macro, false, "Janitor", "table") then
        return
    end
    macro.RunJanitor = macro.RunJanitor or Janitor.new()

    -- Functions
    if not validateType(macroScript, macro, false, "ToggleIsRunning", "function") then
        return
    end
    macro.ToggleIsRunning = function(someMacro)
        someMacro.State.IsRunning = not (someMacro.State.IsRunning and true or false)
        someMacro.RunJanitor:Cleanup()
    end

    if not validateType(macroScript, macro, false, "IsRunning", "function") then
        return
    end
    macro.IsRunning = function(someMacro)
        return someMacro.State.IsRunning and true or false
    end

    if not validateType(macroScript, macro, false, "GetFieldValue", "function") then
        return
    end
    macro.GetFieldValue = function(someMacro, fieldName)
        return someMacro.State.FieldValues[fieldName]
    end

    if not validateType(macroScript, macro, true, "Function", "function") then
        return
    end
    if not validateType(macroScript, macro, false, "BindToOpen", "function") then
        return
    end
    if not validateType(macroScript, macro, false, "BindToClose", "function") then
        return
    end
    macro._BindToClose = function(someMacro, plugin)
        if someMacro.BindToClose then
            someMacro.BindToClose(macro, plugin)
        end
        someMacro.State.IsRunning = false
        someMacro.RunJanitor:Cleanup()
    end

    -- Give script reference
    macro._script = macroScript
    macro._isBroken = macro._isBroken or false

    return macro
end

---
---@param macro MacroDefinition
---@param field MacroField
---
function MacroHelper:ClearField(macro, field)
    macro.State.FieldValues[field.Name] = nil
    refreshState()
end

---
---UI has requested to update this field.
---Returns the current value, and true if it was updated.
---@param macro MacroDefinition
---@param field MacroField
---@param text string
---@return any, boolean
---
function MacroHelper:UpdateField(macro, field, text)
    -- Validate value
    local value = field.Type.Validate(text)
    if value == nil then
        -- Validation failed; return stored value.
        local currentValue = macro.State.FieldValues[field.Name]
        return currentValue, false
    end

    -- Update
    macro.State.FieldValues[field.Name] = value
    macro.FieldChanged:Fire(field.Name, value)
    refreshState()
    return value, true
end

---
---@private
---
---Asynchronously called with all other FrameworkInit()
---
function MacroHelper:FrameworkInit()
    Logger = PluginFramework:Require("Logger")
    MacroConstants = PluginFramework:Require("MacroConstants")
    SocketController = PluginFramework:Require("SocketController")
    SocketConstants = PluginFramework:Require("SocketConstants")
    PluginHandler = PluginFramework:Require("PluginHandler")
    StudioHandler = PluginFramework:Require("StudioHandler")
    MacroClientServer = PluginFramework:Require("MacroClientServer")
    Janitor = PluginFramework:Require("Janitor")
    SocketSettings = PluginFramework:Require("SocketSettings")
    LocalMacros = PluginFramework:Require("LocalMacros")
end

---
---@private
---
---Synchronously called, one after the other, with all other FrameworkStart()
---
function MacroHelper:FrameworkStart() end

return MacroHelper
