Download CommonLua/GedOps.lua from sirnii/myspace: direct link, hf CLI and curl.
- Browser
- Download file 66.3 kB
-
https://huggingface.co/spaces/sirnii/myspace/resolve/main/CommonLua/GedOps.lua
- Command line
-
hf download hf://spaces/sirnii/myspace/CommonLua/GedOps.lua
-
curl -L -o GedOps.lua https://huggingface.co/spaces/sirnii/myspace/resolve/main/CommonLua/GedOps.lua
66.3 kB
| ----- Generic undo support for object properties | |
| function CompareValues(val1, val2) | |
| if not IsT(val1) and not IsT(val2) and type(val1) == "table" and type(val2) == "table" then | |
| local pstr1, pstr2 = pstr("", 1024), pstr("", 1024) | |
| ValueToLuaCode(val1, nil, pstr1) | |
| ValueToLuaCode(val2, nil, pstr2) | |
| return pstr1 == pstr2 | |
| end | |
| return val1 == val2 | |
| end | |
| function GedPropCapture(obj) | |
| if not IsKindOf(obj, "PropertyObject") or IsKindOf(obj, "CObject") and not IsValid(obj) then | |
| return | |
| end | |
| local prop_capture = {} | |
| for _, prop_meta in ipairs(obj:GetProperties()) do | |
| local no_edit = prop_eval(prop_meta.no_edit, obj, prop_meta) | |
| local read_only = prop_eval(prop_meta.read_only, obj, prop_meta) | |
| local dont_save = prop_eval(prop_meta.dont_save, obj, prop_meta) | |
| if not no_edit and not read_only and not dont_save then | |
| local editor = prop_meta.editor | |
| if editor == "object" or editor == "objects" then -- can't really undo nested game objects that get changed via a Ged button | |
| return | |
| end | |
| local id = prop_meta.id | |
| local value = obj:GetProperty(id) | |
| if not obj:IsDefaultPropertyValue(id, prop_meta, value) then | |
| prop_capture[id] = obj:ClonePropertyValue(value, prop_meta) | |
| end | |
| end | |
| end | |
| return prop_capture | |
| end | |
| function GedCreatePropValuesUndoFn(obj, old_capture) | |
| local new_capture = GedPropCapture(obj) | |
| if not old_capture or not new_capture then return end | |
| local changed, nils = {}, {} | |
| for _, key in ipairs(table.union(table.keys(new_capture), table.keys(old_capture))) do | |
| local old_value, new_value = rawget(old_capture, key), rawget(new_capture, key) | |
| if not CompareValues(old_value, new_value) then | |
| changed[key] = old_value | |
| nils[key] = old_value == nil or nil | |
| end | |
| end | |
| if next(changed) or next(nils) then | |
| return function() | |
| for key, value in pairs(changed) do obj:SetProperty(key, value) end | |
| for key, value in pairs(nils) do obj:SetProperty(key, nil) end | |
| if obj:IsKindOf("Preset") then | |
| obj:SortPresets() | |
| end | |
| ObjModified(obj) | |
| end | |
| end | |
| end | |
| ----- Multiple-selection op helpers | |
| -- | |
| -- Use to perform ops over multiple selection, if performing the | |
| -- single op multiple times either forwards or backwards works | |
| function GedListMultiOp(socket, obj, op, mode, selection, ...) | |
| if not selection or not selection[1] then return end | |
| table.sort(selection) | |
| local idx1 = mode == "forward" and 1 or #selection | |
| local idx2 = mode == "forward" and #selection or 1 | |
| local step = mode == "forward" and 1 or -1 | |
| local new_sel, undo_fns = {}, {} | |
| for i = idx1, idx2, step do | |
| local sel, undo_fn = _G[op](socket, obj, selection[i], ...) | |
| if not sel or type(sel) == "string" then -- error | |
| for i = 1, #undo_fns do undo_fns[i]() end | |
| return sel | |
| end | |
| table.insert(undo_fns, 1, undo_fn) | |
| table.insert(new_sel, sel) | |
| end | |
| if mode == "delete" then | |
| new_sel = { new_sel[#new_sel] } | |
| else | |
| table.sort(new_sel) | |
| end | |
| return new_sel, function() for i = 1, #undo_fns do undo_fns[i]() end ObjModified(obj) end | |
| end | |
| function GedTreeMultiOp(socket, obj, op, mode, selection, ...) | |
| if not selection or not selection[1] then return end | |
| local node_path = selection[1] | |
| selection = selection[2] | |
| table.sort(selection) | |
| local idx1 = mode:starts_with("forward") and 1 or #selection | |
| local idx2 = mode:starts_with("forward") and #selection or 1 | |
| local step = mode:starts_with("forward") and 1 or -1 | |
| local shift = 0 | |
| local new_sel_node | |
| local new_sel, undo_fns = {}, {} | |
| for i = idx1, idx2, step do | |
| local path = table.copy(node_path) | |
| path[#path] = selection[i] - (mode == "forward_inwards" and shift or 0) | |
| if mode == "forward_inwards" then | |
| path.parent_leaf_idx = new_sel_node and new_sel_node[#new_sel_node - 1] | |
| end | |
| local sel, undo_fn = _G[op](socket, obj, path, ...) | |
| if sel == nil or type(sel) == "string" then -- error | |
| for i = 1, #undo_fns do undo_fns[i]() end | |
| return sel | |
| end | |
| table.insert(undo_fns, 1, undo_fn) | |
| new_sel_node = mode ~= "delete" and new_sel_node or sel | |
| if sel then | |
| table.insert(new_sel, sel[#sel] + (mode == "backward_outwards" and shift or 0)) | |
| end | |
| shift = shift + 1 | |
| end | |
| if mode == "delete" then | |
| new_sel = { new_sel[#new_sel] } | |
| else | |
| table.sort(new_sel) | |
| end | |
| return { new_sel_node, new_sel }, function() for i = 1, #undo_fns do undo_fns[i]() end ObjModified(obj) end | |
| end | |
| function GedDisplayTempStatus(id, text, delay) | |
| delay = delay or 600 | |
| return GedSetUiStatus(id, text, delay) | |
| end | |
| ----- clipboard & duplicate | |
| if FirstLoad then | |
| GedClipboard = { stored_objs = false, base_class = false, } | |
| GedPropertiesContainer = { data = false } | |
| GedSerializeInProgress = false | |
| end | |
| -- used when Ged needs to fully serialize a value for copy/paste/duplicate, or for undo purposes | |
| -- ModItemWithFiles uses this to serialize the files associated to a ModItem as well, preventing "partial" copies | |
| function GedSerialize(value) | |
| assert(not GedSerializeInProgress) -- sanity check | |
| GedSerializeInProgress = true | |
| local data = ValueToLuaCode(value) | |
| GedSerializeInProgress = false | |
| return data | |
| end | |
| -- 'class' is a base class for all objects, used for type-safe pasting | |
| function GedCopyToClipboard(obj, base_class, idx_list, message) | |
| table.sort(idx_list) | |
| GedClipboard.base_class = base_class | |
| GedClipboard.mod_ids = {} | |
| local objs = {} | |
| for i = 1, #idx_list do | |
| local item = TreeNodeByPath(obj, idx_list[i]) | |
| table.insert(objs, item) | |
| assert(item:IsKindOf(base_class)) | |
| -- Save the mod ids of copied items so we can assign each to the correct mod on paste | |
| table.insert(GedClipboard.mod_ids, item.mod and item.mod.id) | |
| end | |
| GedClipboard.stored_objs = GedSerialize(objs) | |
| GedDisplayTempStatus("clipboard", string.format("%d %s copied", #idx_list, base_class)) | |
| end | |
| local function __paste(class_name, ...) | |
| local class = g_Classes[class_name] | |
| return class:HasMember("__paste") and class:__paste(...) or class:__fromluacode(...) | |
| end | |
| function GedPasteObjCode(code, error_text) | |
| local old_place_obj = PlaceObj | |
| PlaceObj = __paste | |
| local err, objs = LuaCodeToTuple(code, _G) | |
| PlaceObj = old_place_obj | |
| if err then | |
| print(error_text, err) | |
| return | |
| end | |
| GenerateLocalizationIDs(objs) | |
| return objs | |
| end | |
| function GedRestoreFromClipboard(base_class) | |
| local clipboard_class = rawget(_G, GedClipboard.base_class) | |
| if not clipboard_class or base_class and not clipboard_class:IsKindOf(base_class) then | |
| return | |
| end | |
| return GedPasteObjCode(GedClipboard.stored_objs, "Ged: Error restoring object") | |
| end | |
| function GedDuplicateObjects(obj, idx_list) | |
| table.sort(idx_list) | |
| local objs = {} | |
| local mod_ids = {} | |
| for i = 1, #idx_list do | |
| local item = TreeNodeByPath(obj, idx_list[i]) | |
| table.insert(objs, item) | |
| -- Temporary fix don't kill me - save mod_ids to assign them again on paste | |
| table.insert(mod_ids, item.mod and item.mod.id or false) | |
| end | |
| return GedPasteObjCode(GedSerialize(objs), "Ged: Error duplicating object"), mod_ids | |
| end | |
| ----- generic ops (SetProperty, PropEditorButton, etc.) | |
| local function ged_verify_set_prop(obj, prop_meta, prop_id, value, socket) | |
| value = GedToGameValue(value, prop_meta, obj) | |
| if value == nil then | |
| value = obj:GetDefaultPropertyValue(prop_id, prop_meta) | |
| elseif prop_meta.validate then | |
| local err, corrected_value = prop_meta.validate(obj, value, socket) | |
| if err then | |
| -- If the new value causes an error, display the old value again | |
| GedForceUpdateObject(obj) | |
| ObjModified(obj) | |
| return GedTranslate(err, obj, false) | |
| elseif corrected_value ~= nil then | |
| value = corrected_value | |
| end | |
| end | |
| local old_value | |
| if type(prop_id) == "string" then | |
| old_value = obj:GetProperty(prop_id) | |
| else | |
| old_value = obj[prop_id] | |
| end | |
| if ValueToLuaCode(value) ~= ValueToLuaCode(old_value) then | |
| return nil, value, obj:ClonePropertyValue(old_value, prop_meta) | |
| end | |
| end | |
| local function ged_set_prop(socket, obj, prop_id, value, old_value, multi) | |
| if type(prop_id) == "string" then | |
| obj:SetProperty(prop_id, value) | |
| else | |
| obj[prop_id] = value | |
| end | |
| ParentTableModified(value, obj, "recursive") | |
| GedMultiSelectAdapterObjModified(obj) | |
| socket:NotifyEditorSetProperty(obj, prop_id, old_value, multi) | |
| ObjModified(obj) | |
| end | |
| local function ged_set_props(socket, prop_id, values, old_values) | |
| local multi = #table.keys(values) > 1 | |
| for obj, value in pairs(values) do | |
| ged_set_prop(socket, obj, prop_id, value, old_values[obj], multi) | |
| end | |
| end | |
| function GedSetProperty(socket, obj, prop_id, value, disable_undo, slider_drag_id) | |
| local objs = IsKindOf(obj, "GedMultiSelectAdapter") and obj.__objects or { obj } | |
| local values, old_values, prop_captures = {}, {}, {} | |
| for _, item in ipairs(objs) do | |
| local prop_meta = GedIsValidObject(item) and item:GetPropertyMetadata(prop_id) | |
| if prop_meta then | |
| local err, val, old_val = ged_verify_set_prop(item, prop_meta, prop_id, value, socket) | |
| if err then | |
| return err | |
| elseif val ~= nil then | |
| values[item] = val | |
| old_values[item] = old_val | |
| prop_captures[item] = GedPropCapture(item) | |
| end | |
| end | |
| end | |
| -- only perform the op if a property value would be changed | |
| if next(values) then | |
| ged_set_props(socket, prop_id, values, old_values) | |
| if IsKindOf(obj, "GedMultiSelectAdapter") and not value then | |
| obj:ClearNestedProperty(prop_id) | |
| end | |
| ObjModified(obj) | |
| if not disable_undo then | |
| -- the undo function will encompasses all object properties, so that extra changes to an object made in OnEditorSetProperty are also handled | |
| local undo_fns = {} | |
| for _, item in ipairs(objs) do | |
| undo_fns[#undo_fns + 1] = GedCreatePropValuesUndoFn(item, prop_captures[item]) | |
| end | |
| return nil, function() for _, undo in ipairs(undo_fns) do undo() end ObjModified(obj) end, slider_drag_id | |
| else | |
| return nil, nil, slider_drag_id | |
| end | |
| end | |
| end | |
| function GedPropEditorButton(socket, obj, root_name, prop_id, btn_name, btn_func, btn_param, idx) | |
| if not btn_func then | |
| local prop_meta = obj:GetPropertyMetadata(prop_id) | |
| local buttons = prop_eval(prop_meta.buttons, obj, prop_meta) | |
| btn_func = table.find_value(buttons, "name", btn_name).func | |
| end | |
| local game_objects = IsEditorActive() and socket:GatherAffectedGameObjects(obj) | |
| if game_objects then | |
| XEditorUndo:BeginOp{ name = btn_name, objects = game_objects } | |
| end | |
| Msg("GedExecPropButtonStarted", obj) | |
| local root = socket:ResolveObj(root_name) | |
| local new_sel_or_err, undo_fn | |
| if IsKindOf(obj, "GedMultiSelectAdapter") then | |
| new_sel_or_err, undo_fn = obj:ExecPropButton(root, prop_id, socket, btn_func, btn_param, idx) | |
| else | |
| local prop_capture = not game_objects and GedPropCapture(obj) | |
| if type(btn_func) == "function" then | |
| new_sel_or_err, undo_fn = btn_func(obj, root, prop_id, socket, btn_param, idx) | |
| else | |
| new_sel_or_err, undo_fn = "Couldn't find property button function " .. (btn_func or "nil"), nil | |
| if PropObjHasMember(obj, btn_func) then | |
| new_sel_or_err, undo_fn = obj[btn_func](obj, root, prop_id, socket, btn_param, idx) | |
| elseif obj:IsKindOf("XTemplateWindow") and PropObjHasMember(g_Classes[obj.__class], btn_func) then | |
| new_sel_or_err, undo_fn = g_Classes[obj.__class][btn_func](obj, root, prop_id, socket, btn_param, idx) | |
| elseif PropObjHasMember(root, btn_func) then | |
| new_sel_or_err, undo_fn = root[btn_func](root, obj, prop_id, socket, btn_param, idx) | |
| elseif rawget(_G, btn_func) then | |
| new_sel_or_err, undo_fn = _G[btn_func](root, obj, prop_id, socket, btn_param, idx) | |
| end | |
| end | |
| -- setup automatic undo in Ged for properties | |
| local undo_props = not game_objects and GedCreatePropValuesUndoFn(obj, prop_capture) | |
| if undo_props then | |
| local old_undo_fn = undo_fn or empty_func | |
| undo_fn = function() old_undo_fn() undo_props() end | |
| end | |
| end | |
| Msg("GedExecPropButtonCompleted", obj) | |
| if game_objects then | |
| XEditorUndo:EndOp(game_objects) | |
| elseif type(new_sel_or_err) ~= "string" and undo_fn then | |
| GedDisplayTempStatus("buttons_undo", "Press Ctrl-Z for undo", 800) | |
| end | |
| return new_sel_or_err, undo_fn | |
| end | |
| function GedInvokeMethod(socket, obj, fn_name, ...) | |
| obj[fn_name](obj, ...) | |
| end | |
| function GedDiscardEditorChanges(ged) | |
| for _, group in ipairs(ged:ResolveObj("root")) do | |
| for _, preset in ipairs(group) do | |
| QueueReloadAllPresets(preset:GetLastSavePath(), "Modified", "force_reload") | |
| end | |
| end | |
| end | |
| function GedNotifyPropertyChanged(socket, parent_name, prop_id) | |
| socket:NotifyEditorSetProperty(socket:ResolveObj(parent_name), prop_id) | |
| end | |
| function GedGetObjectClass(obj) | |
| return obj and obj.class | |
| end | |
| ----- tree ops | |
| function TreeNodeChildren(node) | |
| local f = node and node.GedTreeChildren | |
| return f and (f(node) or empty_table) or node | |
| end | |
| function RecursiveFindTreeItemPath(parent, obj, path) | |
| path = path or {} | |
| for idx, node in ipairs(TreeNodeChildren(parent)) do | |
| path[#path + 1] = idx | |
| if node == obj or RecursiveFindTreeItemPath(node, obj, path) then | |
| return path | |
| end | |
| table.remove(path) | |
| end | |
| end | |
| function RecursiveFindTreeItemPaths(root, objects) | |
| local obj_paths = {} | |
| for _, obj in ipairs(objects) do | |
| obj_paths[#obj_paths + 1] = RecursiveFindTreeItemPath(root, obj) | |
| end | |
| local parent_path = obj_paths[1] | |
| if parent_path then | |
| parent_path = table.copy(parent_path) | |
| local objects_idxs = { table.remove(parent_path) } | |
| for i = 2, #objects do | |
| local obj_path = obj_paths[i] | |
| objects_idxs[#objects_idxs + 1] = table.remove(obj_path) | |
| if not table.iequal(parent_path, obj_path) then | |
| return | |
| end | |
| end | |
| return obj_paths[1], objects_idxs | |
| end | |
| end | |
| function ParentNodeByPath(root, path) | |
| for i = 1, #path - 1 do | |
| if not root then return end | |
| local f = root.GedTreeChildren | |
| root = rawget(f and (f(root) or empty_table) or root, path[i]) | |
| end | |
| return root | |
| end | |
| function GetNodeByPath(root, path) | |
| return TreeNodeChildren(ParentNodeByPath(root, path))[path[#path]] | |
| end | |
| function TreeRelocateNode(root, old_path, new_path) | |
| local old_parent = ParentNodeByPath(root, old_path) | |
| local new_parent = ParentNodeByPath(root, new_path) | |
| local node = table.remove(TreeNodeChildren(old_parent), old_path[#old_path]) | |
| local parent_table = TreeNodeChildren(new_parent) | |
| local new_path_idx = Min(new_path[#new_path], #parent_table + 1) | |
| table.insert(parent_table, new_path_idx, node) | |
| ParentTableModified(node, parent_table) | |
| ObjModified(old_parent) | |
| ObjModified(new_parent) | |
| ObjModified(root) | |
| end | |
| function GetLowestCommonAncestor(path1, path2) | |
| local lca_path = {} | |
| local index = 1 | |
| local node1 = path1[1] | |
| if not next(path1) or not next(path2) then return lca_path end | |
| while path1[index] == path2[index] do | |
| table.insert(lca_path, path1[index]) | |
| index = index + 1 | |
| end | |
| return lca_path | |
| end | |
| function IsTreeMultiSelection(selection) | |
| return type(selection) == "table" and (selection[1] == false or type(selection[1]) == "table") | |
| end | |
| function GedOpTreeMoveItemUp(socket, root, selection) | |
| if IsTreeMultiSelection(selection) then | |
| return GedTreeMultiOp(socket, root, "GedOpTreeMoveItemUp", "forward", selection) | |
| end | |
| if not selection then return end | |
| local new_path = table.copy(selection) | |
| local leaf_idx = table.remove(new_path) | |
| if leaf_idx > 1 then | |
| table.insert(new_path, leaf_idx - 1) | |
| TreeRelocateNode(root, selection, new_path) | |
| return new_path, function() TreeRelocateNode(root, new_path, selection) end | |
| end | |
| end | |
| function GedOpTreeMoveItemDown(socket, root, selection) | |
| if IsTreeMultiSelection(selection) then | |
| return GedTreeMultiOp(socket, root, "GedOpTreeMoveItemDown", "backward", selection) | |
| end | |
| if not selection then return end | |
| local parent = ParentNodeByPath(root, selection) | |
| local new_path = table.copy(selection) | |
| local leaf_idx = table.remove(new_path) | |
| if leaf_idx < #TreeNodeChildren(parent) then | |
| table.insert(new_path, leaf_idx + 1) | |
| TreeRelocateNode(root, selection, new_path) | |
| return new_path, function() TreeRelocateNode(root, new_path, selection) end | |
| end | |
| end | |
| function GedOpTreeMoveItemInwards(socket, root, selection) | |
| if IsTreeMultiSelection(selection) then | |
| return GedTreeMultiOp(socket, root, "GedOpTreeMoveItemInwards", "forward_inwards", selection) | |
| end | |
| if not selection then return end | |
| local parent = ParentNodeByPath(root, selection) | |
| local new_path = table.copy(selection) | |
| local leaf_idx = table.remove(new_path) | |
| if leaf_idx > 1 then | |
| local parent_leaf_idx = selection.parent_leaf_idx or (leaf_idx - 1) | |
| local new_parent = TreeNodeChildren(parent)[parent_leaf_idx] | |
| if not IsKindOf(new_parent, "Container") or not new_parent:IsValidSubItem(TreeNodeChildren(ParentNodeByPath(root, selection))[leaf_idx]) then | |
| return "error" | |
| end | |
| table.insert(new_path, parent_leaf_idx) | |
| table.insert(new_path, #TreeNodeChildren(new_parent) + 1) | |
| TreeRelocateNode(root, selection, new_path) | |
| return new_path, function() TreeRelocateNode(root, new_path, selection) end | |
| end | |
| end | |
| function GedOpTreeMoveItemOutwards(socket, root, selection) | |
| if IsTreeMultiSelection(selection) then | |
| return GedTreeMultiOp(socket, root, "GedOpTreeMoveItemOutwards", "backward_outwards", selection) | |
| end | |
| if not selection then return end | |
| local new_path = table.copy(selection) | |
| local leaf_idx = table.remove(new_path) | |
| if #new_path > 0 then | |
| table.insert(new_path, table.remove(new_path) + 1) | |
| local new_parent = ParentNodeByPath(root, new_path) | |
| if IsKindOf(new_parent, "Container") and not new_parent:IsValidSubItem(TreeNodeChildren(ParentNodeByPath(root, selection))[leaf_idx]) then | |
| return "error" | |
| end | |
| TreeRelocateNode(root, selection, new_path) | |
| return new_path, function() TreeRelocateNode(root, new_path, selection) end | |
| end | |
| end | |
| function RelocateTreeNodes(socket, root, selection, target_path) | |
| if not selection then return end | |
| -- 1) replace_default all the members in the selection with false and add them to another table | |
| -- 2) add them in reverse order to the target path | |
| -- 3) in reverse order remove all the falses from the original selection | |
| local node_path, sel_idxs | |
| if IsTreeMultiSelection(selection) then | |
| node_path = selection[1] | |
| sel_idxs = selection[2] | |
| table.sort(sel_idxs) | |
| else | |
| node_path = selection | |
| sel_idxs = { selection[#selection] } | |
| end | |
| local target_parent = ParentNodeByPath(root, target_path) | |
| if not IsKindOf(target_parent, "Container") then | |
| return "error" | |
| end | |
| local old_parent = ParentNodeByPath(root, node_path) | |
| local old_children = TreeNodeChildren(old_parent) | |
| for _, sel_idx in ipairs(sel_idxs) do | |
| if not target_parent:IsValidSubItem(old_children[sel_idx]) then | |
| return "error" | |
| end | |
| end | |
| local nodes = sync_set() | |
| for _, sel_idx in ipairs(sel_idxs) do | |
| nodes:insert(old_children[sel_idx]) | |
| old_children[sel_idx] = false | |
| end | |
| local target_children = TreeNodeChildren(target_parent) | |
| local target_idx = target_path[#target_path] | |
| for _, node in ripairs(nodes) do | |
| table.insert(target_children, target_idx, node) | |
| ParentTableModified(node, target_children) | |
| end | |
| local hole_idxs = {} | |
| for i = #old_children, 1, -1 do | |
| if not old_children[i] then | |
| table.remove(old_children, i) | |
| hole_idxs[#hole_idxs + 1] = i | |
| end | |
| end | |
| local new_sel_idxs = {} | |
| for i, child in ipairs(target_children) do | |
| if nodes[child] then | |
| new_sel_idxs[#new_sel_idxs + 1] = i | |
| end | |
| end | |
| ObjModified(old_parent) | |
| ObjModified(target_parent) | |
| ObjModified(root) | |
| local sel_path = RecursiveFindTreeItemPath(root, target_children[new_sel_idxs[1]]) | |
| local new_selection = { sel_path, new_sel_idxs } | |
| return new_selection, function() | |
| -- undo | |
| -- 1) add all the falses that were removed by the last do step | |
| -- 2) remove all the nodes that were added to the target path | |
| -- 3) fill all the falses with the removed nodes | |
| for _, hole_idx in ripairs(hole_idxs) do | |
| table.insert(old_children, hole_idx, false) | |
| end | |
| for i in ripairs(nodes) do | |
| table.remove(target_children, target_idx + i - 1) | |
| end | |
| for i, sel_idx in ipairs(sel_idxs) do | |
| local node = nodes[i] | |
| old_children[sel_idx] = node | |
| ParentTableModified(node, target_children) | |
| end | |
| ObjModified(old_parent) | |
| ObjModified(target_parent) | |
| ObjModified(root) | |
| end | |
| end | |
| function GedOpTreeDropItemUp(socket, root, selection, drop_path) | |
| -- drop path is unchanged | |
| return RelocateTreeNodes(socket, root, selection, drop_path) | |
| end | |
| function GedOpTreeDropItemDown(socket, root, selection, drop_path) | |
| drop_path[#drop_path] = drop_path[#drop_path] + 1 | |
| return RelocateTreeNodes(socket, root, selection, drop_path) | |
| end | |
| function GedOpTreeDropItemInwards(socket, root, selection, drop_path) | |
| drop_path[#drop_path + 1] = 1 | |
| return RelocateTreeNodes(socket, root, selection, drop_path) | |
| end | |
| function GedOpTreeDeleteItem(socket, root, selection) | |
| if IsTreeMultiSelection(selection) then | |
| return GedTreeMultiOp(socket, root, "GedOpTreeDeleteItem", "delete", selection) | |
| end | |
| if not selection then return end | |
| local parent = ParentNodeByPath(root, selection) | |
| local orig_path = table.copy(selection) | |
| local leaf_idx = table.remove(selection) | |
| local children = TreeNodeChildren(parent) | |
| local node = table.remove(children, leaf_idx) | |
| if not node then | |
| return "invalid selection" | |
| end | |
| local new_path = table.copy(selection) | |
| if leaf_idx <= #children then | |
| table.insert(new_path, leaf_idx) | |
| elseif leaf_idx > 1 then | |
| table.insert(new_path, leaf_idx - 1) | |
| elseif #new_path == 0 then | |
| new_path = false | |
| end | |
| ObjModified(root) | |
| if root ~= parent then | |
| ObjModified(parent) | |
| end | |
| local mod_id = node.mod and node.mod.id | |
| local undo_data = GedSerialize(node) | |
| GedNotifyRecursive(node, "OnEditorDelete", parent, socket) | |
| node:delete() | |
| GedNotifyRecursive(node, "OnAfterEditorDelete", parent, socket) | |
| return new_path, function() | |
| local err, node = LuaCodeToTuple(undo_data, _G) | |
| if err then | |
| print("Ged: Error restoring object", err) | |
| return | |
| end | |
| GedOpTreeNewItem(socket, root, table.copy(orig_path), node, leaf_idx, mod_id) | |
| end | |
| end | |
| function GedOpTreeNewItemInContainer(socket, root, path, class) | |
| path = path or {} | |
| -- find the first parent container we can insert the class into | |
| local path_len | |
| local obj = root | |
| local item_class = g_Classes[class] | |
| for i, key in ipairs(path) do | |
| if not obj then break end | |
| if IsKindOf(obj, "Container") and obj:IsValidSubItem(item_class) then | |
| path_len = i | |
| end | |
| obj = rawget(TreeNodeChildren(obj), key) | |
| end | |
| -- if there is no suitable parent try the leaf | |
| if (not path_len or path_len ~= #path) and IsKindOf(obj, "Container") and obj:IsValidSubItem(item_class) then | |
| path[#path + 1] = #obj | |
| path_len = #path | |
| end | |
| if path_len then | |
| -- cut the unused part of the path | |
| table.iclear(path, path_len + 1) | |
| return GedOpTreeNewItem(socket, root, path, class) | |
| end | |
| end | |
| -- pass idx == "child" to insert a child node (as opposed to a sibling after the currently selected one) | |
| function GedOpTreeNewItem(socket, root, path, class_or_instance, idx, mod_id) | |
| path = path or {} | |
| local parent = ParentNodeByPath(root, path) | |
| local selected = TreeNodeByPath(root, unpack_params(path)) | |
| if not GedIsValidObject(parent) and not GedIsValidObject(selected) or idx == "child" then | |
| idx, parent = "child", selected | |
| end | |
| while parent ~= root and IsKindOf(parent, "Container") and not parent:IsValidSubItem(class_or_instance) do | |
| table.remove(path) | |
| parent = ParentNodeByPath(root, path) | |
| end | |
| if IsKindOf(parent, "Container") and not parent:IsValidSubItem(class_or_instance) then | |
| return "error" | |
| end | |
| local item = class_or_instance | |
| if type(item) == "string" then | |
| item = _G[item]:new() | |
| GedNotifyRecursive(item, "OnEditorNew", parent, socket, nil, nil, mod_id) | |
| end | |
| local children = TreeNodeChildren(parent) | |
| if not IsParentTableOf(root, item) then -- presets add themselves to the tree in OnEditorNew | |
| local leaf_idx = max_int | |
| if idx ~= "child" then | |
| leaf_idx = idx or (#path == 0 and 1 or path[#path] + 1) | |
| end | |
| if leaf_idx > #children then | |
| table.insert(children, item) | |
| else | |
| table.insert(children, leaf_idx, item) | |
| end | |
| ParentTableModified(item, children) | |
| end | |
| PopulateParentTableCache(item) | |
| GedNotifyRecursive(item, "OnAfterEditorNew", parent, socket, nil, nil, mod_id) | |
| ObjModified(root) | |
| if parent ~= root then | |
| ObjModified(parent) | |
| end | |
| -- find newly added item path in the tree | |
| if ParentTableCache[item] == children then | |
| if idx ~= "child" then table.remove(path) end | |
| table.insert(path, table.find(children, item)) | |
| else | |
| path = RecursiveFindTreeItemPath(root, item) | |
| end | |
| return path, function() GedOpTreeDeleteItem(socket, root, table.copy(path)) end | |
| end | |
| function GedOpTreeCut(socket, root, selection, item_class) | |
| GedOpTreeCopy(socket, root, selection, item_class) | |
| return GedOpTreeDeleteItem(socket, root, selection) | |
| end | |
| function GedOpTreeCopy(socket, root, selection, item_class) | |
| local node_path, idxs = selection[1], selection[2] | |
| if node_path then | |
| local parent = ParentNodeByPath(root, node_path) | |
| GedCopyToClipboard(parent, item_class, idxs) | |
| end | |
| end | |
| function GedOpTreePaste(socket, root, selection, items, mod_ids) | |
| if type(items) ~= "table" and GedClipboard.base_class == "PropertiesContainer" then | |
| return GedOpPropertyPaste(socket) | |
| end | |
| local path = selection[1] | |
| local parent = path and ParentNodeByPath(root, path) or root | |
| local paste_into = false | |
| local restore_from_clipboard = not items or type(items) == "string" | |
| if restore_from_clipboard then -- restore items from clipboard | |
| local wrong_container_sibling = GedClipboard.base_class and IsKindOf(parent, "Container") and not parent:IsValidSubItem(GedClipboard.base_class) | |
| if parent ~= root and (not GedIsValidObject(parent) or wrong_container_sibling) then -- try to paste as child of selected item | |
| local children = TreeNodeChildren(parent) | |
| local selected_idxs = selection[2] or empty_table | |
| table.sort(selected_idxs) | |
| local idx = (#selected_idxs ~= 0 and selected_idxs[#selected_idxs] or #children) | |
| local node = children[idx] | |
| if not GedIsValidObject(node) or GedClipboard.base_class and IsKindOf(node, "Container") and node:IsValidSubItem(GedClipboard.base_class) then | |
| parent = node | |
| table.insert(selection[1], 1) | |
| selection[2] = { 1 } | |
| path = selection[1] | |
| paste_into = true | |
| end | |
| end | |
| if GedClipboard.base_class and IsKindOf(parent, "Container") and not parent:IsValidSubItem(GedClipboard.base_class) then | |
| return string.format("Can't paste %s items as children of this object, container of %s.", GedClipboard.base_class, tostring(parent.ContainerClass)) | |
| end | |
| items = GedRestoreFromClipboard(items) | |
| end | |
| if items then | |
| local selected_idxs = selection[2] or empty_table | |
| table.sort(selected_idxs) | |
| local children = TreeNodeChildren(parent) | |
| local leaf_idx = (#selected_idxs ~= 0 and selected_idxs[#selected_idxs] or #children) + 1 | |
| if paste_into then leaf_idx = leaf_idx - 1 end | |
| for i, item in ipairs(items) do | |
| local mod_id = restore_from_clipboard and GedClipboard.mod_ids and GedClipboard.mod_ids[i] or (mod_ids and mod_ids[i]) | |
| GedNotifyRecursive(item, "OnEditorNew", parent, socket, "paste", nil, mod_id) | |
| if not IsParentTableOf(root, item) then -- presets add themselves to the tree in OnEditorNew | |
| if leaf_idx > #children then | |
| table.insert(children, item) | |
| else | |
| table.insert(children, leaf_idx, item) | |
| end | |
| ParentTableModified(item, children) | |
| end | |
| PopulateParentTableCache(item) | |
| leaf_idx = leaf_idx + 1 | |
| end | |
| if items[1] and items[1]:IsKindOf("Preset") then | |
| items[1]:SortPresets() | |
| end | |
| -- call OnAfterEditorNew for each item, then find their current indices; it might have re-sorted the children | |
| for i, item in ipairs(items) do | |
| local mod_id = restore_from_clipboard and GedClipboard.mod_ids and GedClipboard.mod_ids[i] or (mod_ids and mod_ids[i]) | |
| GedNotifyRecursive(item, "OnAfterEditorNew", parent, socket, "paste", nil, mod_id) | |
| end | |
| ObjModified(root) | |
| if parent ~= root then | |
| ObjModified(parent) | |
| end | |
| -- if items are not pasted in the 'children' table, perform a fully recursive search for them to get the selection for Ged | |
| local items_in_children = true | |
| for _, item in ipairs(items) do | |
| if ParentTableCache[item] ~= children then | |
| items_in_children = false | |
| break | |
| end | |
| end | |
| local sel = {} | |
| if items_in_children then | |
| for _, item in ipairs(items) do | |
| table.insert(sel, table.find(children, item)) | |
| end | |
| path = table.copy(path) | |
| path[Max(#path, 1)] = sel[1] | |
| else | |
| path, sel = RecursiveFindTreeItemPaths(root, items) | |
| end | |
| local selection = path and { path, sel } or nil | |
| return selection, function() GedOpTreeDeleteItem(socket, root, selection) end | |
| end | |
| end | |
| function GedOpTreeDuplicate(socket, root, selection, duplicated_class) | |
| local node_path, idxs = selection[1], selection[2] | |
| if not node_path then return end | |
| local parent = ParentNodeByPath(root, node_path) | |
| for _, idx in ipairs(idxs) do | |
| if duplicated_class and not IsKindOf(TreeNodeChildren(parent)[idx], duplicated_class) then | |
| return "error" | |
| end | |
| end | |
| local items, mod_ids = GedDuplicateObjects(parent, idxs) | |
| if items then | |
| return GedOpTreePaste(socket, root, selection, items, mod_ids) -- handles undo | |
| end | |
| end | |
| ----- list ops | |
| function GedOpListNewItemInClass(socket, obj, index, class_or_instance, parent_class) | |
| if not IsKindOf(obj, parent_class) then return "error" end | |
| return GedOpListNewItem(socket, obj, index, class_or_instance) | |
| end | |
| function GedOpListNewItem(socket, obj, index, class_or_instance) | |
| if IsKindOf(obj, "Container") and not obj:IsValidSubItem(class_or_instance) then return "error" end | |
| local item = class_or_instance | |
| if type(item) == "string" then | |
| item = _G[item]:new() | |
| GedNotifyRecursive(item, "OnEditorNew", obj, socket) | |
| end | |
| index = (index or #obj) + 1 | |
| if index > #obj then | |
| table.insert(obj, item) | |
| else | |
| table.insert(obj, index, item) | |
| end | |
| ParentTableModified(item, obj, "recursive") | |
| GedNotifyRecursive(item, "OnAfterEditorNew", obj, socket) | |
| ObjModified(obj) | |
| index = table.find(obj, item) -- OnAfterEditorNew might have re-sorted the children | |
| return index, function() GedOpListDeleteItem(socket, obj, index) end | |
| end | |
| function GedOpListDeleteItem(socket, obj, selection) | |
| if type(selection) == "table" then | |
| return GedListMultiOp(socket, obj, "GedOpListDeleteItem", "delete", selection) | |
| end | |
| if not selection or not obj[selection] then return end | |
| local item = table.remove(obj, selection) | |
| local undo_data = GedSerialize(item) | |
| GedNotifyRecursive(item, "OnEditorDelete", obj, socket) | |
| if item then | |
| item:delete() | |
| end | |
| GedNotifyRecursive(item, "OnAfterEditorDelete", obj, socket) | |
| ObjModified(obj) | |
| return Clamp(selection, 1, #obj), function() | |
| local err, item = LuaCodeToTuple(undo_data, _G) | |
| if err then | |
| print("Ged: Error restoring object", err) | |
| return | |
| end | |
| GedOpListNewItem(socket, obj, selection - 1, item) | |
| end | |
| end | |
| function GedOpListCut(socket, obj, selection, base_class) | |
| if #selection == 0 then return end | |
| GedOpListCopy(socket, obj, selection, base_class) | |
| return GedOpListDeleteItem(socket, obj, selection) | |
| end | |
| function GedOpListCopy(socket, obj, selection, base_class) | |
| if #selection == 0 then return end | |
| GedCopyToClipboard(obj, base_class, selection) | |
| end | |
| function GedOpListPaste(socket, obj, selection, items) | |
| if type(items) ~= "table" and GedClipboard.base_class == "PropertiesContainer" then | |
| local target = obj[selection[#selection]] | |
| return GedOpPropertyPaste(socket, target) | |
| end | |
| local index = (#selection > 0 and selection[#selection] or #obj) + 1 -- after last selected object, or at the end | |
| if not items or type(items) == "string" then -- restore from clipboard | |
| if GedClipboard.base_class and IsKindOf(obj, "Container") and not obj:IsValidSubItem(GedClipboard.base_class) then | |
| return "error" | |
| end | |
| items = GedRestoreFromClipboard(items) | |
| end | |
| if items then | |
| for i = 1, #items do | |
| local item = items[i] | |
| GedNotifyRecursive(item, "OnEditorNew", obj, socket, "paste") | |
| if index > #obj then | |
| table.insert(obj, item) | |
| else | |
| table.insert(obj, index, item) | |
| end | |
| ParentTableModified(item, obj, "recursive") | |
| GedNotifyRecursive(item, "OnAfterEditorNew", obj, socket, "paste") | |
| index = index + 1 | |
| end | |
| ObjModified(obj) | |
| local sel = {} | |
| for _, item in ipairs(items) do | |
| table.insert(sel, table.raw_find(obj, item)) -- OnAfterEditorNew might have re-sorted the children | |
| end | |
| return sel, function() GedOpListDeleteItem(socket, obj, sel) end | |
| end | |
| end | |
| function GedOpListDuplicate(socket, obj, selection) | |
| if #selection == 0 then return end | |
| local items = GedDuplicateObjects(obj, selection) | |
| if items then | |
| return GedOpListPaste(socket, obj, { selection[#selection] }, items) -- paste after last object | |
| end | |
| end | |
| function GedOpListMoveUp(socket, obj, selection) | |
| if type(selection) == "table" then | |
| return GedListMultiOp(socket, obj, "GedOpListMoveUp", "forward", selection) | |
| end | |
| if not selection or selection <= 1 or selection > #obj then return end | |
| obj[selection], obj[selection - 1] = obj[selection - 1], obj[selection] | |
| GedNotify(obj[selection], "OnAfterEditorSwap", obj, socket, selection - 1, selection) | |
| ObjModified(obj) | |
| return selection - 1, function() GedOpListMoveDown(socket, obj, selection - 1) end | |
| end | |
| function GedOpListMoveDown(socket, obj, selection) | |
| if type(selection) == "table" then | |
| return GedListMultiOp(socket, obj, "GedOpListMoveDown", "backward", selection) | |
| end | |
| if not selection or selection <= 0 or selection >= #obj then return end | |
| obj[selection], obj[selection + 1] = obj[selection + 1], obj[selection] | |
| GedNotify(obj[selection], "OnAfterEditorSwap", obj, socket, selection, selection + 1) | |
| ObjModified(obj) | |
| return selection + 1, function() GedOpListMoveUp(socket, obj, selection + 1) end | |
| end | |
| local function ListRelocateItems(socket, list, selection, drop_idx) | |
| if #selection == 0 or not drop_idx then return end | |
| local sel_map = {} | |
| for _, sel_idx in ipairs(selection) do | |
| sel_map[sel_idx] = true | |
| end | |
| local new_list = {} | |
| local i = 1 | |
| local new_list_cnt = 0 | |
| for _, list_item in ipairs(list) do | |
| if new_list_cnt == drop_idx - 1 then | |
| break | |
| end | |
| if not sel_map[i] then | |
| new_list_cnt = new_list_cnt + 1 | |
| new_list[new_list_cnt] = list_item | |
| end | |
| i = i + 1 | |
| end | |
| local new_selection = {} | |
| for _, sel_idx in ipairs(selection) do | |
| new_list_cnt = new_list_cnt + 1 | |
| new_list[new_list_cnt] = list[sel_idx] | |
| new_selection[#new_selection + 1] = new_list_cnt | |
| end | |
| for j = i, #list do | |
| if not sel_map[j] then | |
| new_list_cnt = new_list_cnt + 1 | |
| new_list[new_list_cnt] = list[j] | |
| end | |
| end | |
| local old_list = {} | |
| for j = 1, new_list_cnt do | |
| old_list[j] = list[j] | |
| list[j] = new_list[j] | |
| end | |
| GedNotify(list[selection], "OnAfterEditorDragAndDrop", list, socket) | |
| ObjModified(list) | |
| return new_selection, function() | |
| for j = 1, new_list_cnt do | |
| list[j] = old_list[j] | |
| end | |
| GedNotify(list[selection], "OnAfterEditorDragAndDrop", list, socket) | |
| ObjModified(list) | |
| return selection | |
| end | |
| end | |
| function GedOpListDropUp(socket, obj, selection, drop_idx) | |
| if type(selection) == "number" then | |
| selection = { selection } | |
| end | |
| if selection[1] < drop_idx then | |
| drop_idx = Max(1, drop_idx - 1) | |
| end | |
| return ListRelocateItems(socket, obj, selection, drop_idx) | |
| end | |
| function GedOpListDropDown(socket, obj, selection, drop_idx) | |
| if type(selection) == "number" then | |
| selection = { selection } | |
| end | |
| if selection[1] > drop_idx then | |
| drop_idx = Min(#obj, drop_idx + 1) | |
| end | |
| return ListRelocateItems(socket, obj, selection, drop_idx) | |
| end | |
| ----- Nested obj / nested list / script | |
| function GedCreateNestedObj(socket, obj, prop_id, class) | |
| if not class or class == "" then return end | |
| local prop_meta = obj:GetPropertyMetadata(prop_id) | |
| local old_value = obj:GetProperty(prop_id) | |
| local new_value = _G[class]:new() | |
| GedNotifyRecursive(new_value, "OnEditorNew", obj, socket) | |
| ged_set_prop(socket, obj, prop_id, new_value, old_value) | |
| GedNotifyRecursive(new_value, "OnAfterEditorNew", obj, socket) | |
| return nil, function() ged_set_prop(socket, obj, prop_id, old_value, new_value) end | |
| end | |
| if FirstLoad then | |
| g_EditedScript = false | |
| g_EditedScriptParent = false | |
| g_EditedScriptPropMeta = false | |
| end | |
| function OnMsg.GedObjectModified(obj) | |
| if obj == g_EditedScript then | |
| g_EditedScript:Compile() | |
| ObjModified(g_EditedScriptParent) | |
| end | |
| end | |
| function OnMsg.GedClosing(ged_id) | |
| if GedConnections[ged_id].app_template == "GedScriptEditor" then | |
| local parent = g_EditedScriptParent | |
| g_EditedScript = false | |
| g_EditedScriptParent = false | |
| g_EditedScriptPropMeta = false | |
| ObjModified(parent) -- remove highlight from currently edited script property | |
| end | |
| end | |
| function GedSaveScriptParentPreset(socket) | |
| if g_EditedScript then | |
| local preset = GetParentTableOfKind(g_EditedScript, "Preset") | |
| CreateRealTimeThread(function() | |
| GedSetUiStatus("preset_save", "Saving...") | |
| preset:Save(true) | |
| Sleep(200) | |
| GedSetUiStatus("preset_save") | |
| end, preset) | |
| end | |
| end | |
| function GedTestScript(socket) | |
| g_EditedScriptParent:Test(nil, nil, socket) | |
| end | |
| function GedCreateOrEditScript(socket, obj, prop_id, value) | |
| value = obj:GetProperty(prop_id) or value | |
| local undo_fn | |
| local prop_meta = obj:GetPropertyMetadata(prop_id) | |
| if not value or type(value) == "string" then | |
| local class = value and g_Classes[value] or ScriptProgram | |
| value = class:new{ Params = prop_eval(prop_meta.params, obj, prop_meta) } | |
| ged_set_prop(socket, obj, prop_id, value) | |
| undo_fn = function() ged_set_prop(socket, obj, prop_id, nil) end | |
| end | |
| g_EditedScript, g_EditedScriptParent, g_EditedScriptPropMeta = value, obj, prop_meta | |
| ObjModified(obj) -- add highlight to currently edited script property | |
| if value.err then | |
| value:Compile() -- make sure GetFuncSource will work, and the script code will be displayed in 'eval', in the case of a compilation error | |
| end | |
| local ged = OpenGedAppSingleton("GedScriptEditor", value, { | |
| WarningsUpdateRoot = "root", | |
| ItemClass = value.ContainerClass, | |
| ScriptPresetClass = GetParentTableOfKind(g_EditedScript, "Preset").class, | |
| ScriptDomain = prop_meta.script_domain | |
| }) | |
| local params = prop_eval(prop_meta.params, obj, prop_meta) | |
| if value.Params ~= params then | |
| ged:ShowMessage("Warning", string.format("Parameters changed from '%s' to '%s'.\n\nPlease fix the script accordingly!", value.Params, params)) | |
| value.Params = params | |
| end | |
| return nil, undo_fn | |
| end | |
| function GedOpNestedListNewItem(socket, parent, parent_name, property_name, index, class_or_instance) | |
| local list_created = false | |
| local obj = parent:GetProperty(property_name) | |
| local obj_name = string.format("%s.%s", parent_name, property_name) | |
| if not obj then | |
| obj = {} | |
| parent:SetProperty(property_name, obj) | |
| -- send the Msg immediately, so GedMultiSelectAdapter updates nested_obj/nested_list props before the OnEditorSetProperty call | |
| ObjModified(parent, "instant") | |
| ParentTableModified(obj, parent) | |
| socket:rfnBindObj(obj_name, { parent_name, property_name }) | |
| list_created = true | |
| end | |
| local selection, undo = GedOpListNewItem(socket, obj, index, class_or_instance) | |
| socket:NotifyEditorSetProperty(parent, property_name) | |
| ObjModified(parent) | |
| return nil, function() | |
| undo() | |
| if list_created then | |
| parent:SetProperty(property_name, false) | |
| end | |
| ObjModified(parent) | |
| end | |
| end | |
| function GedNestedObjCopy(socket, parent, prop_id, base_class) | |
| GedCopyToClipboard(parent, base_class, { prop_id }) | |
| end | |
| function GedNestedObjPaste(socket, parent, prop_id, base_class) | |
| if not IsKindOf(g_Classes[GedClipboard.base_class], base_class) then | |
| local obj_text = GedClipboard.base_class == "PropertiesContainer" and "properties" or string.format("a %s object", GedClipboard.base_class) | |
| return string.format("Can't paste %s here.", obj_text) | |
| end | |
| local old_value = parent:GetProperty(prop_id) | |
| local new_value = GedRestoreFromClipboard(base_class)[1] | |
| GedNotifyRecursive(new_value, "OnEditorNew", parent, socket, "paste") | |
| parent:SetProperty(prop_id, new_value) | |
| ParentTableModified(new_value, parent, "recursive") | |
| GedNotifyRecursive(new_value, "OnAfterEditorNew", parent, socket, "paste") | |
| ObjModified(parent) | |
| return nil, function() parent:SetProperty(prop_id, old_value) ObjModified(parent) end | |
| end | |
| function GedNestedListCopy(socket, obj, base_class) | |
| if not obj then return end | |
| local idxs = {} | |
| for i = 1, #obj do idxs[#idxs + 1] = i end | |
| GedCopyToClipboard(obj, base_class, idxs) | |
| end | |
| function GedNestedListPaste(socket, parent, prop_id, base_class) | |
| if not IsKindOf(g_Classes[GedClipboard.base_class], base_class) then | |
| local obj_text = GedClipboard.base_class == "PropertiesContainer" and "properties" or string.format("%s objects", GedClipboard.base_class) | |
| return string.format("Can't paste %s here.", obj_text) | |
| end | |
| local old_value = parent:GetProperty(prop_id) | |
| local new_value = GedRestoreFromClipboard(base_class) | |
| for _, item in ipairs(new_value) do | |
| GedNotifyRecursive(item, "OnEditorNew", parent, socket, "paste") | |
| end | |
| parent:SetProperty(prop_id, new_value) | |
| ParentTableModified(new_value, parent, "recursive") | |
| for _, item in ipairs(new_value) do | |
| GedNotifyRecursive(item, "OnAfterEditorNew", parent, socket, "paste") | |
| end | |
| ObjModified(parent) | |
| return nil, function() parent:SetProperty(prop_id, old_value) ObjModified(parent) end | |
| end | |
| ----- Preset ops | |
| function GedPresetTree(obj, filter, format) | |
| format = format and format ~= "" and T{format} or nil | |
| local tree = {} | |
| local start_time = GetPreciseTicks() | |
| if filter then | |
| filter:PrepareForFiltering() | |
| end | |
| local total_displayed_items = 0 | |
| for i, group in ipairs(obj or empty_table) do | |
| local tree_group = {} | |
| local preset_ids = { id = tostring(group) } | |
| local displayed_in_group = 0 | |
| for j, preset in ipairs(group) do | |
| local item = { | |
| id = tostring(preset), | |
| name = format and GedTranslate(format, preset, not "check") or preset.id, | |
| rollover = preset:GetPresetRolloverText(), | |
| } | |
| if filter and not filter:FilterObject(preset) then | |
| item.filtered = true | |
| else | |
| displayed_in_group = displayed_in_group + 1 | |
| end | |
| tree_group[j] = item | |
| end | |
| tree_group.name = string.format("%s <color 75 105 198>(%d)", group[1].group, displayed_in_group) | |
| tree_group.collapsed = GedTreePanelCollapsedNodes[group] | |
| tree_group.id = tostring(group) | |
| tree_group.filtered = filter and displayed_in_group == 0 | |
| total_displayed_items = total_displayed_items + displayed_in_group | |
| tree[i] = tree_group | |
| end | |
| local _time = GetPreciseTicks() - start_time | |
| if _time > 500 then | |
| print("GedPresetTree took", _time, "ms") | |
| elseif _time > 50 then | |
| preset_print("GedPresetTree took %i ms", _time) | |
| end | |
| if filter then | |
| filter:DoneFiltering(total_displayed_items) | |
| end | |
| return tree | |
| end | |
| function GedFormatPresets(obj, filter, format) | |
| local count = 0 | |
| for _, group in ipairs(obj or empty_table) do | |
| count = count + #group | |
| end | |
| return string.format("%s (%d)", GedFormatObject(obj, filter, format), count) | |
| end | |
| GedOpNewPreset = GedOpTreeNewItem | |
| GedOpPresetDelete = GedOpTreeDeleteItem | |
| GedOpPresetCut = GedOpTreeCut | |
| GedOpPresetCopy = GedOpTreeCopy | |
| GedOpPresetPaste = GedOpTreePaste | |
| GedOpPresetDuplicate = GedOpTreeDuplicate | |
| if FirstLoad then | |
| GedPresetSaveInProgress = false | |
| GedPresetSaveModsInProgress = false | |
| end | |
| function GedPresetSave(ged, obj, class_name, force_save_all) | |
| local class = _G[class_name] | |
| if class and not IsValidThread(GedPresetSaveInProgress) then | |
| local thread = CanYield() and CurrentThread() | |
| GedPresetSaveInProgress = CreateRealTimeThread(function() | |
| SuspendObjModified("preset_save") | |
| GedSetUiStatus("preset_save", "Saving...") | |
| class:SaveAll(force_save_all, "by_user_request", ged) | |
| GedSetUiStatus("preset_save") | |
| ResumeObjModified("preset_save") | |
| GedPresetSaveInProgress = false | |
| Wakeup(thread) | |
| end) | |
| if thread then WaitWakeup(30000) end | |
| end | |
| end | |
| function GedPresetSaveOne(ged, obj, class_name) | |
| obj = ged:ResolveObj(obj) | |
| if IsKindOf(obj, "Preset") and not IsValidThread(GedPresetSaveInProgress) then | |
| local thread = CanYield() and CurrentThread() | |
| GedPresetSaveInProgress = CreateRealTimeThread(function() | |
| SuspendObjModified("preset_save") | |
| GedSetUiStatus("preset_save", "Saving...") | |
| obj:Save("by_user_request", ged) | |
| GedSetUiStatus("preset_save") | |
| ResumeObjModified("preset_save") | |
| GedPresetSaveInProgress = false | |
| Wakeup(thread) | |
| end) | |
| if thread then WaitWakeup(30000) end | |
| end | |
| end | |
| function GedPresetSaveMods(ged, obj, class_name) | |
| if not IsValidThread(GedPresetSaveModsInProgress) then | |
| local thread = CanYield() and CurrentThread() | |
| GedPresetSaveModsInProgress = CreateRealTimeThread(function() | |
| -- Save the mods of edited ModItemPresets | |
| local mods = {} | |
| ForEachPresetExtended(class_name, function(preset, group) | |
| if preset.mod and (preset:IsDirty() or preset.mod:IsDirty()) and IsKindOf(preset, "ModItemPreset") and not preset:IsPacked() then | |
| mods[preset.mod] = preset.mod | |
| end | |
| end) | |
| local can_save = true | |
| for _, mod in pairs(mods) do | |
| if not mod:CanSaveMod(ged) then | |
| can_save = false | |
| break | |
| end | |
| end | |
| if can_save then | |
| GedSetUiStatus("mod_save", "Saving mods...") | |
| for _, mod in pairs(mods) do | |
| mod:SaveWholeMod() | |
| -- Update Mod Editor tree panel (if open) | |
| ObjModifiedMod(mod) | |
| end | |
| GedSetUiStatus("mod_save") | |
| end | |
| GedSetUiStatus("mod_save") | |
| GedPresetSaveModsInProgress = false | |
| Wakeup(thread) | |
| end) | |
| if thread then WaitWakeup(30000) end | |
| end | |
| end | |
| function GedOpOpenPresetEditor(socket, presets, selection, class) | |
| if not selection or #selection ~= 2 or #selection[1] ~= 2 then return end | |
| local group = presets[selection[1][1]] | |
| CreateRealTimeThread(function() | |
| for _, sel in ipairs(selection[2]) do | |
| local preset_id = group[sel].id | |
| if group[sel].class == "PresetDef" and PresetDefs[preset_id].DefEditorName then | |
| OpenPresetEditor(preset_id) | |
| end | |
| end | |
| end) | |
| end | |
| function GedOpSVNShowLog(socket, obj) | |
| if obj and IsKindOf(obj, "Preset") then | |
| local file_path = obj:GetSavePath() | |
| if not file_path then return end | |
| SVNShowLog(file_path) | |
| end | |
| end | |
| function GedOpSVNShowBlame(socket, obj) | |
| if obj and IsKindOf(obj, "Preset") then | |
| local file_path = obj:GetSavePath() | |
| if not file_path then return end | |
| SVNShowBlame(file_path) | |
| end | |
| end | |
| local function LocatePreset(obj, class) | |
| if obj == class then | |
| return true | |
| end | |
| if type(obj) ~= "table" or IsT(obj) then | |
| return | |
| end | |
| if obj.class == class then | |
| return true | |
| end | |
| for _, item in ipairs(obj) do | |
| if LocatePreset(item, class) then | |
| return true | |
| end | |
| end | |
| if not obj.class then | |
| return | |
| end | |
| for _, prop in ipairs(obj:GetProperties()) do | |
| if prop.editor == "nested_obj" or prop.editor == "nested_list" or prop.editor == "script" or | |
| prop.editor == "preset_id" or prop.editor == "preset_id_list" or prop.editor == "property_array" or | |
| prop.editor == "combo" or prop.editor == "choice" or prop.editor == "dropdownlist" or prop.editor == "text_picker" | |
| then | |
| local item = obj:GetProperty(prop.id) | |
| if LocatePreset(item, class) then | |
| return true | |
| end | |
| end | |
| end | |
| end | |
| function GedOpLocatePreset(socket, obj) | |
| if not obj or not IsKindOf(obj, "Preset") then | |
| print("Can locate only presets!") | |
| return | |
| end | |
| local id = obj.id | |
| local hits = {} | |
| PauseInfiniteLoopDetection("LocatePreset") | |
| for name, groups in pairs(Presets) do | |
| for _, presets in ipairs(groups) do | |
| for _, preset in ipairs(presets) do | |
| if preset ~= obj and LocatePreset(preset, id) then | |
| hits[#hits + 1] = {name, preset.group, preset.id, preset.save_in} | |
| end | |
| end | |
| end | |
| end | |
| ResumeInfiniteLoopDetection("LocatePreset") | |
| print(#hits, "locations found for", obj.id) | |
| for i, hit in ipairs(hits) do | |
| print(" ", table.unpack(hit)) | |
| end | |
| end | |
| if FirstLoad then | |
| l_GoToNext_id = false | |
| l_GoToNext_last = false | |
| end | |
| function GedOpGoToNext(socket, obj) | |
| if not obj or not IsKindOf(obj, "Preset") then | |
| print("Can locate only presets!") | |
| return | |
| end | |
| local id = obj.id | |
| while true do | |
| if l_GoToNext_id ~= id then | |
| l_GoToNext_id = id | |
| l_GoToNext_last = false | |
| end | |
| local search = not l_GoToNext_last | |
| for name, groups in pairs(Presets) do | |
| for _, presets in ipairs(groups) do | |
| for _, preset in ipairs(presets) do | |
| if search then | |
| if preset ~= obj and LocatePreset(preset, id) then | |
| l_GoToNext_last = preset | |
| print("Next occurence of", id, "is located in", preset.class, preset.group, preset.id, preset.save_in) | |
| CreateRealTimeThread(function() | |
| local ged = OpenPresetEditor(preset.class, preset:EditorContext()) | |
| if ged then | |
| ged:SetSelection("root", PresetGetPath(preset)) | |
| end | |
| end) | |
| return | |
| end | |
| elseif l_GoToNext_last == preset then | |
| search = true | |
| end | |
| end | |
| end | |
| end | |
| if not l_GoToNext_last then | |
| break | |
| end | |
| l_GoToNext_last = false | |
| end | |
| print("Cannot find any reference of", obj.id) | |
| end | |
| function GedOpSVNShowDiff(socket, obj) | |
| if obj and IsKindOf(obj, "Preset") then | |
| local file_path = obj:GetSavePath() | |
| if not file_path then return end | |
| SVNShowDiff(file_path) | |
| end | |
| end | |
| ----- Game object ops (in list panel) | |
| function GedOpObjectCut(socket, obj, selection) | |
| if #selection == 0 then return end | |
| GedOpObjectCopy(socket, obj, selection) | |
| return GedOpListDeleteItem(socket, obj, selection) | |
| end | |
| function GedOpObjectCopy(socket, obj, selection) | |
| if #selection == 0 then return end | |
| local objs = {} | |
| for _, idx in ipairs(selection) do | |
| table.insert(objs, obj[idx]) | |
| end | |
| editor.ClearSel() | |
| editor.AddToSel(objs) | |
| editor.CopyToClipboard() | |
| GedClipboard.base_class = false -- to make sure any stored objects in Ged's own clipboard won't mess up the Paste op | |
| GedDisplayTempStatus("clipboard", string.format("%d game objects copied", #objs)) | |
| end | |
| function GedOpObjectPaste(socket, obj) | |
| if GedClipboard.base_class == "PropertiesContainer" then | |
| return GedOpPropertyPaste(socket) | |
| end | |
| editor.PasteFromClipboard() | |
| return nil, function() XEditorUndoQueue:UndoRedo("undo") end | |
| end | |
| function GedOpObjectDuplicate(socket, obj, selection) | |
| GedOpObjectCopy(socket, obj, selection) | |
| return GedOpObjectPaste(socket, obj) | |
| end | |
| ----- General | |
| function GedCallMethod(ged, obj_name, method, ...) | |
| local obj = ged:ResolveObj(obj_name) | |
| if not obj then return end | |
| obj[method](obj, ...) | |
| end | |
| function GedObjModified(ged, obj_name, ...) | |
| local obj = ged:ResolveObj(obj_name) | |
| if not obj then return end | |
| ObjModified(obj) | |
| end | |
| ----- Copy/paste of property values | |
| local eval = prop_eval | |
| function GedOpPropertyCopy(socket, obj, properties, panel_context) | |
| if #properties == 0 then return end | |
| GedClipboard.base_class = "PropertiesContainer" | |
| local copy_data = { id = {}, value = {}, editor = {}, panel_context = false, } | |
| local os_clipboard_data = {} | |
| for i, id in ipairs(properties) do | |
| local property_value = obj:GetProperty(id) | |
| local meta_data = obj:GetPropertyMetadata(id) | |
| copy_data.id[#copy_data.id + 1] = id | |
| copy_data.value[#copy_data.value + 1] = property_value | |
| local editor = eval(meta_data.editor, obj, meta_data) | |
| copy_data.editor[#copy_data.editor + 1] = editor | |
| local editor_class = g_Classes[GedPropEditors[editor]] | |
| if editor_class and editor_class.ConvertToText then | |
| local text = editor_class:ConvertToText(property_value, meta_data) | |
| text = IsT(text) and TDevModeGetEnglishText(text) or text | |
| if type(text) == "string" and #text > 0 then | |
| os_clipboard_data[#os_clipboard_data + 1] = IsT(text) and TDevModeGetEnglishText(text) or text | |
| end | |
| end | |
| end | |
| copy_data.panel_context = panel_context | |
| GedPropertiesContainer.data = copy_data | |
| GedClipboard.stored_objs = ValueToLuaCode(GedPropertiesContainer) | |
| if #os_clipboard_data > 0 then | |
| CopyToClipboard(table.concat(os_clipboard_data, "\n")) | |
| end | |
| GedDisplayTempStatus("clipboard", string.format("%d properties copied", #properties)) | |
| end | |
| function ShowPropertyPasteWarning(socket, prop_ids) | |
| local text = "Properties with IDs: " | |
| for i=1, #prop_ids do | |
| text = text..prop_ids[i] | |
| if i == #prop_ids then | |
| text = text.." could not be pasted!" | |
| else | |
| text = text..", " | |
| end | |
| end | |
| socket:ShowMessage("Warning!", text) | |
| end | |
| local function CanUseTargetProperties(target, source_ids, source_editors, target_ids) | |
| if #source_ids ~= #(target_ids or "") then | |
| return false | |
| end | |
| for i = 1, #source_ids do | |
| local dst_id = target_ids[i] | |
| local dst_meta_data = target:GetPropertyMetadata(dst_id) | |
| if not dst_meta_data then | |
| return false | |
| end | |
| if source_editors[i] ~= eval(dst_meta_data.editor, target, dst_meta_data) then | |
| return false | |
| end | |
| local src_id = source_ids[i] | |
| local src_prefix = string.find(src_id, "%d+$" ) | |
| local dst_prefix = string.find(dst_id, "%d+$" ) | |
| if not src_prefix or src_prefix ~= dst_prefix then | |
| return false | |
| end | |
| if string.sub(src_id, 1, src_prefix - 1) ~= string.sub(dst_id, 1, dst_prefix - 1) then | |
| return false | |
| end | |
| end | |
| return true | |
| end | |
| function GedOpPropertyOSClipboardPaste(socket, target, target_properties, panel_context) | |
| if not IsKindOf(target, "PropertyObject") or #target_properties == 0 then return end | |
| local objs = IsKindOf(target, "GedMultiSelectAdapter") and target.__objects or { target } | |
| if not objs then return end | |
| local clipboard_split = string.split(GetFromClipboard(-1), "\n") | |
| local old_values | |
| for i = 1, Min(#clipboard_split, #target_properties) do | |
| local target_property = target_properties[i] | |
| local clipboard = clipboard_split[i] | |
| local meta_data = target:GetPropertyMetadata(target_property) | |
| if meta_data and not eval(meta_data.no_edit, target, meta_data) then | |
| local editor_class = g_Classes[GedPropEditors[meta_data.editor]] | |
| if editor_class and editor_class.accept_os_clipboard_paste and editor_class.ConvertFromText then | |
| local ok, value, invalid = pcall(editor_class.ConvertFromText, editor_class, clipboard, meta_data) | |
| if ok or invalid then | |
| local id = meta_data.id | |
| for _, obj in ipairs(objs) do | |
| local old_value = obj:GetProperty(id) | |
| old_values = old_values or {} | |
| old_values[obj] = old_values[obj] or {} | |
| old_values[obj][id] = old_value | |
| ged_set_prop(socket, obj, id, value, old_value) | |
| end | |
| end | |
| end | |
| end | |
| end | |
| if not old_values then return end | |
| ObjModified(target) | |
| socket:OnParentsModified(panel_context) -- the change might affect how our object is displayed in the parent object(s) | |
| return nil, function() | |
| for obj, obj_data in pairs(old_values) do | |
| for id, value in pairs(obj_data) do | |
| ged_set_prop(socket, obj, id, value, obj:GetProperty(id)) | |
| end | |
| end | |
| ObjModified(target) | |
| socket:OnParentsModified(panel_context) -- the change might affect how our object is displayed in the parent object(s) | |
| end | |
| end | |
| function GedOpPropertyPaste(socket, target_override, target_properties, panel_context) | |
| local _, os_clipboard_undo_func = GedOpPropertyOSClipboardPaste(socket, target_override, target_properties, panel_context) | |
| if os_clipboard_undo_func then | |
| return nil, os_clipboard_undo_func | |
| end | |
| if GedClipboard.base_class ~= "PropertiesContainer" or not GedClipboard.stored_objs then return end | |
| local err, container = LuaCodeToTuple(GedClipboard.stored_objs, _G) | |
| if err then | |
| print("Ged: Error restoring object", err) | |
| return | |
| end | |
| GenerateLocalizationIDs(container) | |
| local data = container.data | |
| local target = socket:ResolveObj(data.panel_context) | |
| if target_override and target_override:IsKindOf("PropertyObject") then | |
| if CanUseTargetProperties(target_override, data.id, data.editor, data.id) then | |
| target = target_override | |
| end | |
| end | |
| if not target then | |
| print("Failed to find a target to receive the copied properties!") | |
| return | |
| end | |
| local objs = target:IsKindOf("GedMultiSelectAdapter") and target.__objects or { target } | |
| local dst_prop_ids = CanUseTargetProperties(target, data.id, data.editor, target_properties) and target_properties or data.id | |
| local old_values = {} -- obj => { id => value } | |
| local pasted_ids, unmatched_ids = {}, {} | |
| for i = 1, #dst_prop_ids do | |
| local id = dst_prop_ids[i] | |
| local meta_data = target:GetPropertyMetadata(id) | |
| if meta_data and eval(meta_data.editor, target, meta_data) == data.editor[i] and not eval(meta_data.no_edit, target, meta_data) then | |
| local value = data.value[i] | |
| for _, obj in ipairs(objs) do | |
| local old_value = obj:GetProperty(id) | |
| old_values[obj] = old_values[obj] or {} | |
| old_values[obj][id] = old_value | |
| ged_set_prop(socket, obj, id, value, old_value) | |
| end | |
| pasted_ids[#pasted_ids + 1] = id | |
| else | |
| unmatched_ids[#unmatched_ids + 1] = id | |
| end | |
| end | |
| ObjModified(target) | |
| socket:OnParentsModified(data.panel_context) -- the change might affect how our object is displayed in the parent object(s) | |
| socket:Send("rfnApp", "SetPropSelection", data.panel_context, pasted_ids) | |
| if #unmatched_ids ~= 0 then | |
| ShowPropertyPasteWarning(socket, unmatched_ids) | |
| end | |
| local undo_func = function() | |
| for obj, obj_data in pairs(old_values) do | |
| for id, value in pairs(obj_data) do | |
| ged_set_prop(socket, obj, id, value, obj:GetProperty(id)) | |
| end | |
| end | |
| ObjModified(target) | |
| socket:OnParentsModified(data.panel_context) -- the change might affect how our object is displayed in the parent object(s) | |
| end | |
| return nil, undo_func | |
| end | |
| function GedEditFunction(ged, obj_name, props) | |
| local obj = ged:ResolveObj(obj_name) | |
| if not obj then return end | |
| if not props or #props ~= 1 then return end | |
| local prop = props[1] | |
| if not prop then return end | |
| local value = obj:GetProperty(prop) | |
| if IsKindOf(value, "ScriptProgram") then | |
| value = value.eval | |
| end | |
| if type(value) ~= "function" then return end | |
| local info = debug.getinfo(value, "S") | |
| local first, last = info.linedefined, info.lastlinedefined | |
| local source = info.source | |
| if not info or not source or not first or not last then return end | |
| OpenFileLineInHaerald(source:sub(2), first) | |
| end | |
| function GedOpPresetIdNewInstance(ged, obj, prop_id, class_name) | |
| local id = ged:WaitUserInput("New Preset Name", class_name) | |
| if not id then return end | |
| local class = g_Classes[class_name] | |
| local preset = class:new{ id = id } | |
| local presets = Presets[class.PresetClass or class.class] | |
| GedNotifyRecursive(preset, "OnEditorNew", presets[preset.group] or presets, ged) | |
| PopulateParentTableCache(preset) | |
| GedNotifyRecursive(preset, "OnAfterEditorNew", presets[preset.group], ged) | |
| preset:OpenEditor() | |
| local old_value = obj:GetProperty(prop_id) | |
| obj:SetProperty(prop_id, preset.id) | |
| ObjModified(obj) | |
| return false, function() | |
| preset:delete() | |
| obj:SetProperty(prop_id, old_value) | |
| ObjModified(presets) | |
| ObjModified(obj) | |
| end | |
| end | |
| function PresetIdPropFindInstance(obj, prop_meta, id) | |
| local class_name = eval(prop_meta.preset_class, obj, prop_meta) | |
| local group_name = eval(prop_meta.preset_group, obj, prop_meta) | |
| local class = class_name and g_Classes[class_name] | |
| if not class then | |
| assert(false, "Preset class not found: " .. tostring(class_name)) | |
| return | |
| end | |
| assert(class.GlobalMap or IsPresetWithConstantGroup(class) or group_name) | |
| if class.GlobalMap then | |
| return _G[class.GlobalMap][id] | |
| else | |
| group_name = group_name or class.group | |
| local preset_class = class.PresetClass or class_name | |
| local groups = Presets[preset_class] | |
| local presets = groups and groups[group_name] | |
| return presets and presets[id] | |
| end | |
| end | |
| function GedRpcEditPreset(ged, obj_name, prop_id, preset_id) | |
| local obj = ged:ResolveObj(obj_name) | |
| if not obj then return end | |
| if not prop_id and not preset_id then | |
| obj:OpenEditor() -- used by "linked_presets" property | |
| return | |
| end | |
| preset_id = preset_id or obj:GetProperty(prop_id) | |
| local prop_meta = obj:GetPropertyMetadata(prop_id) | |
| local preset = PresetIdPropFindInstance(obj, prop_meta, preset_id) | |
| if preset then | |
| preset:OpenEditor() | |
| else | |
| local class = g_Classes[prop_meta.preset_class] | |
| if class then | |
| OpenPresetEditor(class.PresetClass or class.class) | |
| end | |
| end | |
| end | |
| function GedRpcBindPreset(ged, name, obj_name, prop_id, preset_id) | |
| local obj = ged:ResolveObj(obj_name) | |
| if not obj then return end | |
| preset_id = preset_id or obj:GetProperty(prop_id) | |
| local prop_meta = obj:GetPropertyMetadata(prop_id) | |
| local preset = prop_meta and PresetIdPropFindInstance(obj, prop_meta, preset_id) | |
| if preset then | |
| ged:BindObj(name, preset) | |
| else | |
| ged:UnbindObj(name) | |
| end | |
| end | |
| function GedRpcInspectObj(ged, obj_name, prop_id, prop_obj) | |
| local obj = ged:ResolveObj(obj_name) | |
| if not obj then return end | |
| prop_obj = prop_obj or obj:GetProperty(prop_id) | |
| OpenGedGameObjectEditorInGame(prop_obj) | |
| end | |
| function GedViewPosButton(root, obj, prop_id, ged) | |
| local pos = obj:GetProperty(prop_id) | |
| if not IsValidPos(pos) then | |
| return | |
| end | |
| local local_space = pos:Len() < 5000 or table.get(obj:GetPropertyMetadata(prop_id), "local_space") | |
| if local_space and (not pos:z() or not IsValidPos(obj)) then | |
| return | |
| end | |
| pos = ValidateZ(pos) | |
| local pos0 = local_space and obj:GetVisualPos() or pos | |
| local vec = local_space and SetLen(pos, 2*guim) or 2*guim | |
| local color = local_space and red or white | |
| local v = PlaceVector(pos0, vec, color) | |
| local c = PlaceCircle(pos0, guim/2, color) | |
| if local_space then | |
| c:SetOrientation(vec) | |
| end | |
| ViewPos(pos0) | |
| Msg("RpcViewPos") | |
| CreateRealTimeThread(function() | |
| WaitMsg("RpcViewPos", 5000) | |
| DoneObject(v) | |
| DoneObject(c) | |
| end) | |
| end | |
| ----- Support for "linked_presets" property | |
| function FindLinkedPresetOfClass(obj, preset_class, id, save_in) | |
| id = id or obj.id | |
| save_in = save_in or obj.save_in | |
| local found | |
| ForEachPresetExtended(preset_class, function(preset) | |
| if preset.id == id and preset.save_in == save_in then | |
| found = preset | |
| return "break" | |
| end | |
| end) | |
| return found | |
| end | |
| function GedCreateLinkedPresets(root, obj, prop_id, ged) | |
| local prop_meta = obj:GetPropertyMetadata(prop_id) | |
| local classes = eval(prop_meta.preset_classes, obj, prop_meta) | |
| for _, preset_class in ipairs(classes) do | |
| local preset = g_Classes[preset_class]:new{ id = obj.id, save_in = obj.save_in } | |
| preset:Register() | |
| for prop, value in pairs(prop_meta.default_props[preset_class]) do | |
| preset:SetProperty(prop, value) | |
| end | |
| for prop, how in pairs(prop_meta.mirror_props[preset_class]) do | |
| if type(how) == "function" then | |
| preset:SetProperty(prop, how(obj)) | |
| else | |
| preset:SetProperty(prop, obj:GetProperty(how)) -- "how" is name of property to copy | |
| end | |
| end | |
| preset:MarkDirty() | |
| ObjModified(Presets[preset_class]) | |
| end | |
| ObjModified(obj) | |
| end | |
| function GedDeleteLinkedPresets(root, obj, prop_id, ged) | |
| local prop_meta = obj:GetPropertyMetadata(prop_id) | |
| local classes = eval(prop_meta.preset_classes, obj, prop_meta) | |
| for _, preset_class in ipairs(classes) do | |
| local preset = FindLinkedPresetOfClass(obj, preset_class) | |
| if preset then | |
| preset:delete() | |
| ObjModified(Presets[preset_class]) | |
| end | |
| end | |
| ObjModified(obj) | |
| end | |
| function GedRpcBindLinkedPreset(ged, name, obj_name, preset_class) | |
| local obj = ged:ResolveObj(obj_name) | |
| if not obj then return end | |
| local preset = FindLinkedPresetOfClass(obj, preset_class) | |
| if preset then | |
| ged:BindObj(name, preset) | |
| else | |
| ged:UnbindObj(name) | |
| end | |
| end | |
| LinkedPresetClasses = {} -- <parent preset class> => { <linked class 1>, <linked class 2>, ... } | |
| LinkedPresetMirrorProps = {} -- <property id> => { <copy from> => { <copy to>, <how> }, ... } | |
| local function add_mirror_prop(class, linked_class, prop_id, how, final) | |
| local prop_data = LinkedPresetMirrorProps[prop_id] or {} | |
| table.insert(prop_data, { copy_from_class = class, copy_to_class = linked_class, how = how }) | |
| LinkedPresetMirrorProps[prop_id] = prop_data | |
| -- directly mirrored properties are also copied back | |
| if not final and type(how) == "string" then | |
| add_mirror_prop(linked_class, class, how, prop_id, "final") | |
| end | |
| end | |
| function OnMsg.ClassesPostBuilt() | |
| ClassDescendantsList("Preset", function(class_name, class) | |
| if class.GedEditor and (not class:IsKindOf("CompositeDef") or class.ObjectBaseClass) then | |
| for _, prop_meta in ipairs(class:GetProperties()) do | |
| if prop_meta.editor == "linked_presets" then | |
| local classes = eval(prop_meta.preset_classes, class, prop_meta) | |
| LinkedPresetClasses[class_name] = table.iappend(LinkedPresetClasses[class_name] or {}, prop_meta.classes) | |
| for _, linked_class in ipairs(classes) do | |
| for prop_id, how in pairs(prop_meta.mirror_props[linked_class]) do | |
| add_mirror_prop(class_name, linked_class, prop_id, how) -- "BuildingCompositeDef", "Resource", "display_name", "display_name" | |
| end | |
| add_mirror_prop(class_name, linked_class, "SaveIn", "SaveIn") | |
| add_mirror_prop(class_name, linked_class, "Id", "Id") | |
| end | |
| end | |
| end | |
| end | |
| end) | |
| end | |
| local function mirror_prop(mirror_data, class, obj, prop_id, preset_id, preset_savein, processed_classes) | |
| for _, data in ipairs(mirror_data) do | |
| local from, to = data.copy_from_class, data.copy_to_class | |
| if from == class and not processed_classes[to] then | |
| -- mirror property | |
| local how = data.how | |
| local target = FindLinkedPresetOfClass(obj, to, preset_id, preset_savein) | |
| if target then | |
| if type(how) == "function" then | |
| target:SetProperty(prop_id, how(obj)) | |
| else | |
| print("Set", prop_id, "from", from, "to", to) | |
| target:SetProperty(prop_id, obj:GetProperty(how)) -- "how" is name of property to copy | |
| end | |
| end | |
| processed_classes[to] = true | |
| mirror_prop(mirror_data, to, obj, prop_id, preset_id, preset_savein, processed_classes) | |
| end | |
| end | |
| end | |
| local function mirror_prop_message(ged, classes, prop_id) | |
| ged:ShowMessage("Linked Presets", | |
| string.format("<center>The '%s' property was also updated\nin linked preset(s) %s.", prop_id, table.concat(table.keys2(classes, "sorted"), ", "))) | |
| end | |
| function OnMsg.GedPropertyEdited(ged_id, obj, prop_id, old_value) | |
| local data = LinkedPresetMirrorProps[prop_id] | |
| if data then | |
| local preset_id = prop_id == "Id" and old_value or obj.id | |
| local preset_savein = prop_id == "SaveIn" and old_value or obj.save_in | |
| local processed_classes = { [obj.class] = true } | |
| mirror_prop(data, obj.class, obj, prop_id, preset_id, preset_savein, processed_classes) | |
| processed_classes[obj.class] = nil | |
| if next(processed_classes) then | |
| DelayedCall(50, mirror_prop_message, GedConnections[ged_id], processed_classes, prop_id) | |
| end | |
| end | |
| end | |
| function OnMsg.PresetSave(class) | |
| -- go through dirty objects; if the preset they are linked to is of 'class', save them | |
| local dirty_paths = {} | |
| for obj in pairs(g_DirtyObjects) do | |
| for parent_preset, linked_presets in pairs(LinkedPresetClasses) do | |
| if table.find(linked_presets, obj.class) then | |
| local parent_preset = FindLinkedPresetOfClass(obj, parent_preset) | |
| if parent_preset and parent_preset.class == class then | |
| local paths = dirty_paths[obj.class] or {} | |
| paths[obj:GetNormalizedSavePath()] = true | |
| paths[obj:GetLastSavePath()] = true | |
| dirty_paths[obj.class] = paths | |
| end | |
| end | |
| end | |
| end | |
| for class, paths in pairs(dirty_paths) do | |
| _G[class]:SaveFiles(paths) | |
| end | |
| end | |