const.TagLookupTable["keyword"] = "" const.TagLookupTable["/keyword"] = "" if Platform.developer then function prgdbg(li, level, idx) li[level] = idx Msg("OnPrgLine", li) end else prgdbg = empty_func end g_PrgPresetPropsCache = {} DefineClass.PrgPreset = { __parents = { "Preset" }, properties = { { id = "Params", editor = "string_list", default = {} }, }, SingleFile = false, ContainerClass = "PrgStatement", EditorMenubarName = false, HasCompanionFile = true, StatementTags = { "Basics" }, -- list the PrgStatement tags usable in this Prg class FuncTable = "Prgs", -- override in child classes } function PrgPreset:GenerateFuncName() return self.id end function PrgPreset:GetParamString() return table.concat(self.Params, ", ") end function PrgPreset:GenerateCodeAtFunctionStart(code) code:append("\tlocal rand = BraidRandomCreate(seed or AsyncRand())\n") end function PrgPreset:GenerateCompanionFileCode(code) -- generate static code local has_statements = false for _, statement in ipairs(self) do if not statement.Disable then local len = code:size() statement:GenerateStaticCode(code) if len ~= code:size() then code:append("\n") has_statements = true end end end if has_statements then code:append("\n") end -- generate function code code:appendf("rawset(_G, '%s', rawget(_G, '%s') or {})\n", self.FuncTable, self.FuncTable) code:appendf("%s.%s = function(seed, %s)\n", self.FuncTable, self:GenerateFuncName(), self:GetParamString()) code:appendf("\tlocal li = { id = \"%s\" }\n", self.id) self:GenerateCodeAtFunctionStart(code) for idx, statement in ipairs(self) do if not statement.Disable then statement:GenerateCode(code, "\t", idx) code:append("\n") end end code:append("end") end function PrgPreset:EditorContext() local context = Preset.EditorContext(self) context.ContainerTree = true return context end function PrgPreset:FilterSubItemClass(class) return not class.StatementTag or table.find(self.StatementTags, class.StatementTag) end function OnMsg.ClassesBuilt() local undefined = ClassLeafDescendantsList("PrgStatement", function(name, class) return not class.StatementTag end) assert(#undefined == 0, string.format("Prg statement %s has no StatementTag defined.", undefined[1])) -- add a validate function to enforce variables names to be identifiers ClassLeafDescendantsList("PrgStatement", function(name, class) for _, prop_meta in ipairs(class:GetProperties()) do if prop_meta.items == PrgVarsCombo or prop_meta.variable then prop_meta.validate = function(self, value) return ValidateIdentifier(self, value) end end end end) end ----- Statement & Block function PrgVarsCombo() return function(obj) local vars = table.keys(obj:VarsInScope()) table.insert(vars, "") table.sort(vars) return vars end end function PrgLocalVarsCombo() return function(obj) local vars = {} for k, v in pairs(obj:VarsInScope()) do if v ~= "static" then vars[#vars + 1] = k end end table.insert(vars, "") table.sort(vars) return vars end end DefineClass.PrgStatement = { __parents = { "PropertyObject" }, properties = { { id = "Disable", editor = "bool", default = false, }, }, DisabledPrefix = Untranslated(""), EditorName = "Command", StoreAsTable = true, StatementTag = false, -- each PrgStatement must have a tag; each PrgPreset defines the list of tags that can be used in it } function PrgStatement:VarsInScope() local vars = {} local current = self local block = GetParentTableOfKindNoCheck(self, "PrgBlock", "PrgPreset") while block do for _, statement in ipairs(block) do if statement ~= self then statement:GatherVars(vars) end if statement == current then break end end current = block block = GetParentTableOfKindNoCheck(block, "PrgBlock", "PrgPreset") end -- 'current' is now the PrgPreset for _, var in ipairs(current.Params) do vars[var] = true end vars[""] = nil -- skip "" vars due to unset properties return vars end function PrgStatement:LinePrefix(indent, idx) return string.format("%sprgdbg(li, %d, %d) ", indent, #indent, idx) end function PrgStatement:GatherVars(vars) -- add variables declared by this statement as keys in the 'vars' table, with value "local" or "static" end function PrgStatement:GenerateStaticCode(code) -- generate code to be inserted before the Prg function body, generates the code that declares the static vars end function PrgStatement:GenerateCode(code, indent, idx) -- generate code for the Prg function body end function PrgStatement:GetEditorView() return _InternalTranslate(Untranslated(""), self, false) .. _InternalTranslate(self.EditorView, self, false) end DefineClass.PrgBlock = { __parents = { "PrgStatement", "Container" }, ContainerClass = "PrgStatement", } function PrgBlock:GenerateStaticCode(code) if #self == 0 then return end for i = 1, #self - 1 do self[i]:GenerateStaticCode(code) end self[#self]:GenerateStaticCode(code) end function PrgBlock:GenerateCode(code, indent, idx) if #self == 0 then return end indent = indent .. "\t" for i = 1, #self - 1 do if not self[i].Disable then self[i]:GenerateCode(code, indent, i) code:append("\n") end end if not self[#self].Disable then self[#self]:GenerateCode(code, indent, #self) code:appendf(" li[%d] = nil", #indent) end end ----- Variables local function get_expr_string(expr) if not expr or expr == empty_func then return "nil" end local name, parameters, body = GetFuncSource(expr) body = type(body) == "table" and table.concat(body, "\n") or body return body:match("^%s*return%s*(.*)") or body end DefineClass.PrgAssign = { __parents = { "PrgStatement" }, properties = { { id = "Variable", editor = "combo", default = "", items = PrgLocalVarsCombo, }, }, EditorView = Untranslated(" = "), } function PrgAssign:GatherVars(vars) vars[self.Variable] = "local" end function PrgAssign:GenerateCode(code, indent, idx) local var_exists = self:VarsInScope()[self.Variable] code:appendf("%s%s%s = %s", self:LinePrefix(indent, idx), var_exists and "" or "local ", self.Variable, self:GetValueCode()) end function PrgAssign:GetValueCode() -- define in child classes end function PrgAssign:GetValueDescription() -- define in child classes end DefineClass.PrgAssignExpr = { __parents = { "PrgAssign" }, properties = { { id = "Value", editor = "expression", default = empty_func }, }, EditorName = "Set variable", EditorSubmenu = "Basics", StatementTag = "Basics", } function PrgAssignExpr:GetValueCode() return get_expr_string(self.Value) end function PrgAssignExpr:GetValueDescription() return get_expr_string(self.Value) end ----- Flow control - if / else, while, loops DefineClass.PrgIf = { __parents = { "PrgBlock" }, properties = { { id = "Repeat", name = "Repeat while satisfied", editor = "bool", default = false, }, { id = "Condition", editor = "expression", default = empty_func, }, }, EditorName = "Condition check (if/while)", EditorSubmenu = "Code flow", StatementTag = "Basics", } function PrgIf:GetExprCode(for_preview) return get_expr_string(self.Condition) end function PrgIf:GenerateCode(code, indent, idx) code:appendf(self.Repeat and "%swhile %s do\n" or "%sif %s then\n", self:LinePrefix(indent, idx), self:GetExprCode(false)) PrgBlock.GenerateCode(self, code, indent) local parent = GetParentTableOfKind(self, "PrgBlock") or GetParentTableOfKind(self, "PrgPreset") local next_statement = parent[table.find(parent, self) + 1] if not IsKindOf(next_statement, "PrgElse") then code:appendf("\n%send", indent) end end function PrgIf:GetEditorView() return Untranslated("" .. (self.Repeat and "while " or "if ") .. self:GetExprCode(true)) end DefineClass.PrgElse = { __parents = { "PrgBlock" }, EditorName = "Condition else", EditorView = Untranslated("else"), EditorSubmenu = "Code flow", StatementTag = "Basics", } function PrgElse:GenerateCode(code, indent, idx) if self:CheckPrgError() then return end code:appendf("%selse\n\t%s\n", indent, self:LinePrefix(indent, idx)) PrgBlock.GenerateCode(self, code, indent, idx) code:appendf("\n%send", indent) end function PrgElse:CheckPrgError() local parent = GetParentTableOfKind(self, "PrgBlock") or GetParentTableOfKind(self, "PrgPreset") local prev_statement = parent[table.find(parent, self) - 1] return not IsKindOf(prev_statement, "PrgIf") or prev_statement.Repeat end DefineClass.PrgForEach = { __parents = { "PrgBlock" }, properties = { { id = "List", name = "List variable", editor = "choice", default = "", items = PrgVarsCombo, }, { id = "Value", name = "Value variable", editor = "text", default = "value" }, { id = "Index", name = "Index variable", editor = "text", default = "i", }, }, EditorName = "For each", EditorView = Untranslated("for each '' in ''"), EditorSubmenu = "Code flow", StatementTag = "Basics", } function PrgForEach:GatherVars(vars) vars[self.List] = "local" vars[self.Value] = "local" vars[self.Index] = "local" end function PrgForEach:GenerateCode(code, indent, idx) if self.List == "" then return end code:appendf("%sfor %s, %s in ipairs(%s) do\n", self:LinePrefix(indent, idx), self.Index, self.Value, self.List) PrgBlock.GenerateCode(self, code, indent) code:appendf("\n%send", indent) end ----- Calls (function and Prg) -- calls self:Exec with sprocall, passing all property values in order as parameters DefineClass.PrgExec = { __parents = { "PrgStatement" }, ExtraParams = {}, -- extra params to pass before the properties, e.g. { "rand" } to use the random generator for the Prg AssignTo = "", -- variable name to assign function result to; create a property with the same name to allow the user specify it PassClassAsSelf = true, } function PrgExec:GetParamProps() return self:GetProperties() end function PrgExec:GetParamString() local params = self.PassClassAsSelf and { self.class } or {} table.iappend(params, self.ExtraParams) for _, prop in ipairs(self:GetParamProps()) do if prop.editor ~= "help" and prop.editor ~= "buttons" and prop.id ~= "Disable" then local value = self:GetProperty(prop.id) params[#params + 1] = type(value) == "function" and get_expr_string(value) or prop.variable and value == "" and "nil" or prop.variable and value ~= "" and value or ValueToLuaCode(value):gsub("[\t\r\n]", "") end end return table.concat(params, ", ") end function PrgExec:GatherVars(vars) vars[self.AssignTo] = "local" end function PrgExec:GenerateCode(code, indent, idx) if self.AssignTo and self.AssignTo ~= "" then local var_exists = self:VarsInScope()[self.AssignTo] code:appendf("%slocal _%s\n", indent, var_exists and "" or ", " .. self.AssignTo) code:appendf("%s_, %s = sprocall(%s.Exec, %s)", self:LinePrefix(indent, idx), self.AssignTo, self.class, self:GetParamString()) else code:appendf("%ssprocall(%s.Exec, %s)", self:LinePrefix(indent, idx), self.class, self:GetParamString()) end end function PrgExec:Exec(...) -- IMPORTANT: 'self' will be the class and not the instance -- implement the function to execute here; all properties of your class are passed AS PARAMETERS in the order of their declaration end -- override to "export" an existing Lua function as a Prg statement DefineClass.PrgFunction = { __parents = { "PrgExec" }, properties = { { id = "VarArgs", name = "Add extra parameters", editor = "string_list", default = false, no_edit = function(self) return not self.HasExtraParams end }, }, PassClassAsSelf = false, Params = "", -- specify comma-separated parameters here HasExtraParams = false, -- has variable arguments? Exec = empty_func, -- function to execute } function PrgFunction:GetParamProps() local props = {} for param in string.gmatch(self.Params, "[^, ]+") do props[#props + 1] = { id = param, editor = "expression", default = empty_func, } end return props end function PrgFunction:GetProperties() local props = g_PrgPresetPropsCache[self] if not props then props = self:GetParamProps() local class_props = table.copy(PropertyObject.GetProperties(self), "deep") local idx = table.find(class_props, "id", "VarArgs") if idx then table.insert(props, class_props[idx]) table.remove(class_props, idx) end table.iappend(class_props, props) g_PrgPresetPropsCache[self] = class_props end return props end function PrgFunction:GetParamString() local ret = PrgExec.GetParamString(self) if self.HasExtraParams and self.VarArgs then local extra = table.concat(self.VarArgs, ", ") ret = ret == "" and extra or (ret .. ", " .. extra) end return ret end -- call any Lua function by name DefineClass.PrgCallLuaFunction = { __parents = { "PrgFunction" }, properties = { { id = "FunctionName", name = "Function name", editor = "text", default = "", validate = function(self, value) return value ~= "" and not self:FindFunction(value) and "Can't find function with the specified name" end, help = "Lua function to call - use Object:MethodName if you'd like to call a class method." }, }, StoreAsTable = false, -- so SetFunctionName gets called upon loading EditorName = "Call function", EditorView = Untranslated("Call ()"), EditorSubmenu = "Code flow", StatementTag = "Basics", } function PrgCallLuaFunction:FindFunction(fn_name) local ret = _G for field in string.gmatch(fn_name, "[^:. ]+") do ret = rawget(ret, field) if not ret then return end end return fn_name ~= "" and ret end function PrgCallLuaFunction:SetFunctionName(fn_name) if self.FunctionName == fn_name then return end local fn = self:FindFunction(fn_name) local name, parameters, body = GetFuncSource(fn) if name then local extra = parameters:ends_with(", ...") self.Params = extra and parameters:sub(1, -6) or parameters self.HasExtraParams = extra else self.Params = nil self.HasExtraParams = nil end if string.find(fn_name, ":") then self.Params = self.Params == "" and "self" or ("self, " .. self.Params) end self.FunctionName = fn_name g_PrgPresetPropsCache[self] = nil end function PrgCallLuaFunction:GenerateCode(code, indent, idx) code:appendf("%ssprocall(%s, %s)", self:LinePrefix(indent, idx), self.FunctionName:gsub(":", "."), self:GetParamString()) end DefineClass.PrgCallPrgBase = { __parents = { "PrgStatement" }, properties = { { id = "PrgClass", name = "Prg class", editor = "choice", default = "", items = ClassDescendantsCombo("PrgPreset") }, { id = "PrgGroup", name = "Prg preset group", editor = "choice", default = "", items = function(self) return PresetGroupsCombo(self.PrgClass) end, no_edit = function(self) return self.PrgClass == "" or g_Classes[self.PrgClass].GlobalMap end, }, { id = "Prg", editor = "preset_id", default = "", preset_group = function(self) return self.PrgGroup ~= "" and self.PrgGroup end, preset_class = function(self) return self.PrgClass ~= "" and self.PrgClass or "PrgPreset" end }, }, EditorName = "Call Prg", EditorView = Untranslated("Call Prg ''"), EditorSubmenu = "Code flow", StatementTag = "Basics", } function PrgCallPrgBase:GetProperties() local prg = self.Prg ~= "" and PresetIdPropFindInstance(self, table.find_value(self.properties, "id", "Prg"), self.Prg) if not prg then return self.properties end local props = g_PrgPresetPropsCache[self] if not props then props = table.copy(PropertyObject.GetProperties(self), "deep") for _, param in ipairs(prg.Params or empty_table) do props[#props + 1] = { id = param, editor = "expression", default = empty_func, } end g_PrgPresetPropsCache[self] = props end return props end function PrgCallPrgBase:OnEditorSetProperty(prop_id, old_value, ged) if prop_id == "PrgClass" then self.PrgGroup = self.PrgClass ~= "" and not g_Classes[self.PrgClass].GlobalMap and PresetGroupsCombo(self.PrgClass)()[2] or "" end if prop_id == "PrgClass" or prop_id == "PrgGroup" then self.Prg = nil end if prop_id == "PrgClass" or prop_id == "PrgGroup" or prop_id == "Prg" then local prop_cache = g_PrgPresetPropsCache[self] for _, prop in ipairs(prop_cache) do if not table.find(self.properties, "id", prop.id) then self[prop.id] = nil end end g_PrgPresetPropsCache[self] = nil end end function PrgCallPrgBase:OnAfterEditorNew() local parent_prg = GetParentTableOfKind(self, "PrgPreset") self.PrgClass = parent_prg.class self.PrgGroup = self.PrgClass ~= "" and not g_Classes[self.PrgClass].GlobalMap and PresetGroupsCombo(self.PrgClass)()[2] or "" end DefineClass("PrgCallPrg", "PrgCallPrgBase") function PrgCallPrg:GetParamString() local prg = PresetIdPropFindInstance(self, table.find_value(self.properties, "id", "Prg"), self.Prg) local params = {} for _, param in ipairs(prg.Params or empty_table) do params[#params + 1] = get_expr_string(rawget(self, param)) end return table.concat(params, ", ") end function PrgCallPrg:GenerateCode(code, indent, idx) if self.PrgClass == "" or self.Prg == "" then return end local prg = PresetIdPropFindInstance(self, table.find_value(self.properties, "id", "Prg"), self.Prg) if prg then code:appendf("%ssprocall(%s.%s, rand(), %s)", self:LinePrefix(indent, idx), prg.FuncTable, prg:GenerateFuncName(), self:GetParamString()) end end DefineClass.PrgPrint = { __parents = { "PrgFunction" }, Params = "", HasExtraParams = true, Exec = print, EditorName = "Print on console", EditorView = Untranslated("Print "), EditorSubmenu = "Basics", StatementTag = "Basics", } DefineClass.PrgExecuteEffects = { __parents = { "PrgExec" }, properties = { { id = "Effects", editor = "nested_list", default = false, base_class = "Effect", all_descendants = true }, }, EditorName = "Execute effects", EditorSubmenu = "Basics", StatementTag = "Effects", } function PrgExecuteEffects:GetEditorView() local items = { _InternalTranslate("Execute effects:", self, false) } for _, effect in ipairs(self.Effects or empty_table) do items[#items + 1] = "--> " .. _InternalTranslate(Untranslated(""), effect, false) end return table.concat(items, "\n") end function PrgExecuteEffects:Exec(effects) return ExecuteEffectList(effects) end ----- Get objects (add/remove/assign a list of objects to a variable) DefineClass.PrgGetObjs = { __parents = { "PrgExec" }, properties = { { id = "Action", editor = "choice", default = "Assign", items = { "Assign", "Add to", "Remove from" }, }, { id = "AssignTo", name = "Objects variable", editor = "combo", default = "", items = PrgVarsCombo, variable = true, }, }, EditorSubmenu = "Objects", StatementTag = "Objects", } function PrgGetObjs:GetEditorView() local prefix = _InternalTranslate("", self, false) if self.Action == "Assign" then return string.format("'%s' = %s", self.AssignTo, self:GetObjectsDescription()) elseif self.Action == "Add to" then return string.format("'%s' += %s", self.AssignTo, self:GetObjectsDescription()) else -- if self.Action == "Remove from" then return string.format("'%s' -= %s", self.AssignTo, self:GetObjectsDescription()) end end function PrgGetObjs:Exec(Action, AssignTo, ...) if self.Action == "Assign" then return self:GetObjects(...) elseif self.Action == "Add to" then local objs = IsKindOf(AssignTo, "Object") and { AssignTo } or AssignTo or {} return table.iappend(objs, self:GetObjects(...)) else -- if self.Action == "Remove from" then local objs = IsKindOf(AssignTo, "Object") and { AssignTo } or AssignTo or {} return table.subtraction(objs, self:GetObjects(...)) end return AssignTo end function PrgGetObjs:GetObjectsDescription() -- return a text that describes the objects here, e.g. "enemy units of the unit in 'Variable'" end function PrgGetObjs:GetObjects(...) -- the properties after Action and Variable are passed to this function; return the list of objects here end DefineClass.GetObjectsInGroup = { __parents = { "PrgGetObjs" }, properties = { { id = "Group", editor = "choice", default = "", items = function() return table.keys2(Groups, true, "") end, }, }, EditorName = "Get objects from group", } function GetObjectsInGroup:GetObjectsDescription() return string.format("objects from group '%s'", self.Group) end function GetObjectsInGroup:GetObjects(Group) return table.copy(Groups[Group] or empty_table) end ----- Object list filtering DefineClass.PrgFilterObjs = { __parents = { "PrgExec" }, properties = { { id = "AssignTo", name = "Objects variable", editor = "combo", default = "", items = PrgVarsCombo, variable = true, }, }, EditorSubmenu = "Objects", StatementTag = "Objects", } DefineClass.FilterByClass = { __parents = { "PrgFilterObjs" }, properties = { { id = "Classes", editor = "string_list", default = false, items = ClassDescendantsCombo("Object"), arbitrary_value = true, }, { id = "Negate", editor = "bool", default = false, }, }, EditorName = "Filter by class", } function FilterByClass:GetEditorView() return self.Negate and string.format("Leave only objects of classes %s in '%s'", table.concat(self.Classes, ", "), self.AssignTo) or string.format("Remove objects of classes %s in '%s'", table.concat(self.Classes, ", "), self.AssignTo) end function FilterByClass:Exec(objs, Negate, Classes) return table.ifilter(objs, function(i, obj) return Negate == not IsKindOfClasses(obj, table.unpack(Classes)) end) end DefineClass.SelectObjectsAtRandom = { __parents = { "PrgFilterObjs" }, properties = { { id = "Percentage", editor = "number", default = 100, min = 1, max = 100, slider = true }, { id = "MaxCount", editor = "number", default = 0, }, }, ExtraParams = { "rand" }, EditorName = "Filter at random", } function SelectObjectsAtRandom:GetEditorView() if self.MaxCount <= 0 then return string.format("Leave %d%% of the objects in '%s'", self.Percentage, self.AssignTo) elseif self.Percentage == 100 then return string.format("Leave no more than %d objects in '%s'", self.MaxCount, self.AssignTo) else return string.format("Leave %d%% of the objects in '%s', but no more than %d", self.Percentage, self.AssignTo, self.MaxCount) end end function SelectObjectsAtRandom:Exec(rand, objs, Percentage, MaxCount) local count = MulDivRound(#objs, Percentage, 100) if MaxCount > 0 then count = Min(count, MaxCount) end local ret, taken, len = {}, {}, #objs --local added = {} while count > 0 do local idx = rand(len) + 1 ret[count] = objs[taken[idx] or idx] --assert(not added[ret[count]]) --added[ret[count]] = true count, len = count - 1, len - 1 taken[idx] = taken[len] or len end return ret end ----- Others DefineClass.DeleteObjects = { __parents = { "PrgExec" }, properties = { { id = "ObjectsVar", name = "Objects variable", editor = "choice", default = "", items = PrgLocalVarsCombo, variable = true, }, }, EditorName = "Delete objects", EditorView = Untranslated("Delete the objects in ''"), EditorSubmenu = "Objects", StatementTag = "Objects", } function DeleteObjects:Exec(ObjectsVar) ObjectsVar = ObjectsVar or empty_table XEditorUndo:BeginOp{ objects = ObjectsVar } -- does nothing if outside of editor if IsEditorActive() then Msg("EditorCallback", "EditorCallbackDelete", ObjectsVar) end for _, obj in ipairs(ObjectsVar) do obj:delete() end XEditorUndo:EndOp() end