Download CommonLua/Ged.lua from sirnii/myspace: direct link, hf CLI and curl.
- Browser
- Download file 101 kB
-
https://huggingface.co/spaces/sirnii/myspace/resolve/main/CommonLua/Ged.lua
- Command line
-
hf download hf://spaces/sirnii/myspace/CommonLua/Ged.lua
-
curl -L -o Ged.lua https://huggingface.co/spaces/sirnii/myspace/resolve/main/CommonLua/Ged.lua
101 kB
| ----- GedFilter | |
| DefineClass.GedFilter = { | |
| __parents = { "InitDone" }, | |
| properties = {}, | |
| ged = false, | |
| target_name = false, | |
| supress_filter_reset = false, | |
| FilterName = Untranslated("Filter"), | |
| } | |
| function GedFilter:ResetTarget(socket) | |
| if self.target_name and socket then | |
| local obj = socket:ResolveObj(self.target_name) | |
| self.supress_filter_reset = true | |
| ObjModified(obj) | |
| self.supress_filter_reset = false | |
| end | |
| end | |
| function GedFilter:OnEditorSetProperty(prop_id, old_value, ged) | |
| self:ResetTarget(ged) | |
| end | |
| function GedFilter:TryReset(ged) | |
| if self.supress_filter_reset then return false end | |
| for _, prop in ipairs(self:GetProperties()) do | |
| self:SetProperty(prop.id, self:GetDefaultPropertyValue(prop.id, prop)) | |
| end | |
| GedForceUpdateObject(self) | |
| ObjModified(self) | |
| return true | |
| end | |
| function GedFilter:FilterObject(object) | |
| return true | |
| end | |
| function GedFilter:PrepareForFiltering() | |
| end | |
| function GedFilter:DoneFiltering(displayed_count, filtered --[[ passed for GedListPanel filters only ]]) | |
| end | |
| if FirstLoad then | |
| GedConnections = setmetatable({}, weak_values_meta) | |
| GedObjects = {} -- global mapping object -> { name1, socket1, name2, socket1, name3, socket2, ... } | |
| GedTablePropsCache = {} -- caches properties of the bound objects; this prevents issues when editing nested_obj/list props that work via Get/Set functions | |
| g_gedListener = false | |
| GedTreePanelCollapsedNodes = setmetatable({}, weak_keys_meta) | |
| end | |
| config.GedPort = config.GedPort or 44000 | |
| function ListenForGed(search_for_port) | |
| StopListenForGed() | |
| g_gedListener = BaseSocket:new{ | |
| socket_type = "GedGameSocket", | |
| } | |
| local port_start = config.GedPort or 44000 | |
| local port_end = port_start + (search_for_port and 100 or 1) | |
| for port = port_start, port_end do | |
| local err = g_gedListener:Listen("*", port) | |
| if not err then | |
| g_gedListener.port = port | |
| return true | |
| elseif err == "address in use" then | |
| print("ListenForGed: Address in use. Trying with another port...") | |
| else | |
| return false | |
| end | |
| end | |
| return false | |
| end | |
| function StopListenForGed() | |
| if g_gedListener then | |
| g_gedListener:delete() | |
| g_gedListener = false | |
| end | |
| end | |
| if config.GedLanguageEnglish then | |
| if FirstLoad or ReloadForDlc then | |
| TranslationTableEnglish = false -- for the Mod Editor on PC | |
| end | |
| function GedTranslate(T, context_obj, check) | |
| local old_table = TranslationTable | |
| TranslationTable = TranslationTableEnglish | |
| local ret = _InternalTranslate(T, context_obj, check) | |
| TranslationTable = old_table | |
| return ret | |
| end | |
| else | |
| GedTranslate = _InternalTranslate | |
| end | |
| function OpenGed(id, in_game) | |
| if not g_gedListener then | |
| ListenForGed(true) | |
| end | |
| if config.GedLanguageEnglish and not TranslationTableEnglish then | |
| if GetLanguage() == "English" then | |
| TranslationTableEnglish = TranslationTable | |
| else | |
| TranslationTableEnglish = {} | |
| LoadTranslationTablesFolder("EnglishLanguage/CurrentLanguage/", "English", TranslationTableEnglish) | |
| end | |
| end | |
| local port = g_gedListener.port | |
| if not port then | |
| print("Could not start the ged listener") | |
| return | |
| end | |
| id = id or AsyncRand() | |
| if in_game then | |
| assert(GedSocket, "Ged source files not loaded") | |
| local socket = GedSocket:new() -- if GedSocket is missing the Ged sources are not loaded | |
| local err = socket:WaitConnect(10000, "localhost", port) | |
| if err then | |
| socket:delete() | |
| else | |
| socket:Call("rfnGedId", id) | |
| end | |
| else | |
| local exec_path = GetExecDirectory() .. GetExecName() | |
| local path = string.format('"%s" %s -ged=%s -address=127.0.0.1:%d %s', exec_path, GetIgnoreDebugErrors() and "-no_interactive_asserts" or "", tostring(id), port, config.RunUnpacked and "-unpacked" or "") | |
| local start_func | |
| if Platform.linux or Platform.osx then | |
| start_func = function(path) | |
| local exit_code, _, std_error = os.execute(path .. " &") | |
| return exit_code, std_error | |
| end | |
| else | |
| start_func = function(path) | |
| local cmd = string.format('cmd /c start "GED" %s', path) | |
| local err, exit_code, output, err_messsage = AsyncExec(cmd, nil, true) | |
| if err then return false, err end | |
| return exit_code, err_messsage | |
| end | |
| end | |
| local exit_code, std_error = start_func(path) | |
| if exit_code ~= 0 then | |
| print("Could not launch Ged from:", path, "\nExec error:", std_error) | |
| return | |
| end | |
| end | |
| local timeout = 60000 | |
| while timeout do | |
| if GedConnections[id] then | |
| return GedConnections[id] | |
| end | |
| timeout = WaitMsg("GedConnection", timeout) | |
| end | |
| end | |
| local ged_print = CreatePrint{ | |
| "ged", | |
| format = "printf", | |
| output = DebugPrint, | |
| } | |
| function OpenGedApp(template, root, context, id, in_game) | |
| assert(root ~= nil) | |
| if not IsRealTimeThread() or not CanYield() then | |
| CreateRealTimeThread(OpenGedApp, template, root, context, id, in_game) | |
| return | |
| end | |
| if in_game == nil then | |
| in_game = (g_Classes[template] or XTemplates[template] and XTemplates[template].save_in ~= "Ged" and XTemplates[template].save_in ~= "GameGed") and true | |
| end | |
| context = context or {} | |
| if context.dark_mode == nil then | |
| context.dark_mode = GetDarkModeSetting() | |
| end | |
| context.color_palette = CurrentColorPalette and CurrentColorPalette:ColorsPlainObj() or false | |
| context.color_picker_scale = rawget(_G, "g_GedApp") and g_GedApp.color_picker_scale or EditorSettings:GetColorPickerScale() | |
| context.ui_scale = rawget(_G, "g_GedApp") and g_GedApp.ui_scale or EditorSettings:GetGedUIScale() | |
| context.max_fps = rawget(_G, "g_GedApp") and g_GedApp.max_fps or hr.MaxFps | |
| context.in_game = in_game | |
| context.game_real_time = RealTime() | |
| context.mantis_project_id = const.MantisProjectID | |
| context.mantis_copy_url_btn = const.MantisCopyUrlButton | |
| context.bug_report_tags = GetBugReportTagsForGed() | |
| local ged = OpenGed(id, in_game) | |
| if not ged then return end | |
| ged:BindObj("root", root) | |
| ged.app_template = template | |
| ged.context = context | |
| ged.in_game = in_game | |
| local err = ged:Call("rfnOpenApp", template, context, id) | |
| if err then | |
| printf("OpenGedApp('%s') error: %s", tostring(template), tostring(err)) | |
| end | |
| Msg("GedOpened", ged.ged_id) | |
| local preset_class = context and context.PresetClass | |
| ged_print("Opened %s with class %s, id %s", tostring(template), tostring(preset_class), tostring(ged.ged_id)) | |
| return ged | |
| end | |
| function CloseGedApp(gedsocket, wait) | |
| if GedConnections[gedsocket.ged_id] then | |
| gedsocket:Close() | |
| if wait then | |
| local id | |
| repeat | |
| local _, id = WaitMsg("GedClosing") | |
| until id == gedsocket.ged_id | |
| end | |
| end | |
| end | |
| function FindGedApp(template, preset_class) | |
| for id, conn in pairs(GedConnections) do | |
| if conn.app_template == template and | |
| (not preset_class or conn.context.PresetClass == preset_class) then | |
| return conn | |
| end | |
| end | |
| end | |
| function FindAllGedApps(template, preset_class) | |
| local connections = setmetatable({}, weak_values_meta) | |
| for id, conn in pairs(GedConnections) do | |
| if conn.app_template == template and | |
| (not preset_class or conn.context.PresetClass == preset_class) then | |
| table.insert(connections, conn) | |
| end | |
| end | |
| return connections | |
| end | |
| function OpenGedAppSingleton(template, root, context, id, in_game) | |
| local app = FindGedApp(template) | |
| if app then | |
| app:Call("rfnApp", "Activate", context) | |
| app:BindObj("root", root) | |
| if app.last_app_state and app.last_app_state.root then | |
| local sel = app.last_app_state.root.selection | |
| if sel and type(sel[1]) == "table" then | |
| app:SetSelection("root", {1}, {1}) -- tree panel | |
| else | |
| app:SetSelection("root", 1, {1}) -- list panel | |
| end | |
| end | |
| app:ResetUndoQueue() | |
| app.last_app_state = false -- the last app state won't make sense for a new root object | |
| return app | |
| end | |
| return OpenGedApp(template, root, context, id, in_game) | |
| end | |
| function OnMsg.BugReportStart(print_func) | |
| local list = {} | |
| for key, ged in sorted_pairs(GedConnections) do | |
| local preset_class = ged.context and ged.context.PresetClass | |
| list[#list+1] = "\t" .. tostring(ged.app_template) .. " with preset class " .. tostring(preset_class) .. " and id " .. tostring(ged.ged_id) | |
| end | |
| if #list == 0 then | |
| return | |
| end | |
| print_func("Opened GedApps:\n" .. table.concat(list, "\n") .. "\n") | |
| end | |
| ----- GedGameSocket | |
| DefineClass.GedGameSocket = { | |
| __parents = { "MessageSocket" }, | |
| msg_size_max = 256*1024*1024, | |
| call_timeout = false, | |
| ged_id = false, | |
| app_template = false, | |
| context = false, | |
| root_names = false, -- array of the names of root objects; these are always updated when any object is edited | |
| bound_objects = false, -- mapping name -> object | |
| bound_objects_svalue = false, -- mapping name -> cached value (string) | |
| bound_objects_func = false, -- mapping name -> process_function | |
| bound_objects_path = false, -- mapping name -> list of BindObj calls from root to this object | |
| bound_objects_filter = false, -- mapping name -> GedFilter object | |
| prop_bindings = false, | |
| last_app_state = false, | |
| selected_object = false, | |
| tree_panel_collapsed_nodes = false, | |
| -- undo/redo support | |
| undo_position = 0, -- idx of the next undo entry that will be executed with Ctrl-Z | |
| undo_queue = false, | |
| redo_thread = false, | |
| } | |
| function GedGameSocket:Init() | |
| self.root_names = { "root" } | |
| self.bound_objects = {} | |
| self.bound_objects_svalue = {} | |
| self.bound_objects_func = {} | |
| self.bound_objects_path = {} | |
| self.bound_objects_filter = {} | |
| self.prop_bindings = {} | |
| self:ResetUndoQueue() | |
| end | |
| function GedGameSocket:Done() | |
| Msg("GedClosing", self.ged_id) | |
| ged_print("Closed %s with id %s", tostring(self.app_template), tostring(self.ged_id)) | |
| GedNotify(self.selected_object, "OnEditorSelect", false, self) | |
| Msg("GedOnEditorSelect", self.selected_object, false, self) | |
| for name in pairs(self.bound_objects) do | |
| self:UnbindObj(name, "leave_values") | |
| end | |
| Msg("GedClosed", self) | |
| GedSetUiStatus("ged_multi_select") | |
| end | |
| function GedGameSocket:Close() | |
| self:Send("rfnGedQuit") | |
| end | |
| function GedGameSocket:ResetUndoQueue() | |
| self.undo_position = 0 | |
| self.undo_queue = {} | |
| end | |
| function GedGameSocket:rfnGedId(id) | |
| assert(not GedConnections[id], "Duplicate Ged id " .. tostring(id)) | |
| GedConnections[id] = self | |
| self.ged_id = id | |
| Msg("GedConnection", id) | |
| end | |
| function GedGameSocket:OnDisconnect(reason) | |
| if GedConnections[self.ged_id] then | |
| self:delete() | |
| GedConnections[self.ged_id] = nil | |
| end | |
| end | |
| local prop_prefix = "prop:" | |
| function TreeNodeByPath(root, key1, key2, ...) | |
| if key1 == nil or not root then | |
| return root | |
| end | |
| local key_type = type(key1) | |
| assert(key_type == "number" or key_type == "string") | |
| if key_type == "number" then | |
| local f = root.GedTreeChildren | |
| root = f and f(root) or root | |
| end | |
| local prop_name = type(key1) == "string" and key1:starts_with(prop_prefix) and key1:sub(#prop_prefix + 1) | |
| if prop_name then | |
| if GedTablePropsCache[root] and GedTablePropsCache[root][prop_name] ~= nil then | |
| root = GedTablePropsCache[root][prop_name] | |
| else | |
| root = root:GetProperty(prop_name) | |
| end | |
| if key2 == nil then | |
| return root, prop_name | |
| end | |
| else | |
| root = rawget(root, key1) | |
| end | |
| return TreeNodeByPath(root, key2, ...) | |
| end | |
| function GedGameSocket:ResolveObj(name, ...) | |
| if name then | |
| local idx = string.find(name, "|") | |
| if idx then | |
| name = string.sub(name, 1, idx - 1) | |
| end | |
| end | |
| return TreeNodeByPath(self.bound_objects[name or false], ...) | |
| end | |
| function GedGameSocket:ResolveName(obj) | |
| if not obj then return end | |
| for name, bobj in pairs(self.bound_objects) do | |
| if obj == bobj then | |
| return name | |
| end | |
| end | |
| end | |
| function GedGameSocket:FindFilter(name) | |
| local filter = self.bound_objects_filter[name] | |
| if filter then return filter end | |
| local obj_name, view = name:match("(.+)|(.+)") | |
| if obj_name and view then | |
| return self.bound_objects_filter[obj_name] | |
| end | |
| for filter_name, filter in pairs(self.bound_objects_filter) do | |
| local obj_name, view = filter_name:match("(.+)|(.+)") | |
| if obj_name and view and obj_name == name then | |
| return filter | |
| end | |
| end | |
| end | |
| function GedGameSocket:ResetFilter(obj_name) | |
| local filter = self:FindFilter(obj_name) | |
| if filter and filter:TryReset(self) then | |
| filter:ResetTarget(self) | |
| end | |
| end | |
| function GedGameSocket:BindObj(name, obj, func, dont_send) | |
| if not obj then return end | |
| if not func and rawequal(obj, self.bound_objects[name]) then return end | |
| self:UnbindObj(name) | |
| func = func or function(obj, filter) return tostring(obj) end | |
| local sockets = GedObjects[obj] or {} | |
| GedObjects[obj] = sockets | |
| sockets[#sockets + 1] = name | |
| sockets[#sockets + 1] = self | |
| self.bound_objects[name] = obj | |
| self.bound_objects_func[name] = func | |
| if not dont_send then | |
| local values = func(obj, self:FindFilter(name)) | |
| local vpstr = ValueToLuaCode(values, nil, pstr("", 1024)) | |
| if vpstr ~= self.bound_objects_svalue[name] then | |
| self.bound_objects_svalue[name] = vpstr | |
| self:Send("rfnObjValue", name, vpstr:str(), true) | |
| end | |
| end | |
| Msg("GedBindObj", obj) | |
| end | |
| function GedGameSocket:UnbindObj(name, leave_values) | |
| local obj = self.bound_objects[name] | |
| if obj then | |
| local sockets = GedObjects[obj] | |
| if sockets then | |
| for i = 1, #sockets - 1, 2 do | |
| if sockets[i] == name and sockets[i + 1] == self then | |
| table.remove(sockets, i) | |
| table.remove(sockets, i) | |
| end | |
| end | |
| if #sockets == 0 then | |
| GedObjects[obj] = nil | |
| end | |
| end | |
| self.prop_bindings[obj] = nil | |
| GedTablePropsCache[obj] = nil | |
| end | |
| if leave_values then return end | |
| self.bound_objects[name] = nil | |
| self.bound_objects_svalue[name] = nil | |
| self.bound_objects_func[name] = nil | |
| self.bound_objects_path[name] = nil | |
| end | |
| function GedGameSocket:UnbindObjs(name_prefix, leave_values) | |
| for name in pairs(self.bound_objects) do | |
| if string.starts_with(name, name_prefix) then | |
| self:UnbindObj(name, leave_values) | |
| end | |
| end | |
| end | |
| function GedGameSocket:GetParentOfKind(name, type_name) | |
| -- find object's bind name if we are searching by object | |
| if type(name) == "table" then | |
| for objname, obj in pairs(self.bound_objects) do | |
| if name == obj then | |
| name = objname | |
| break | |
| end | |
| end | |
| if type(name) == "table" then return end | |
| end | |
| local bind_path = self.bound_objects_path[name:match("(.+)|.+") or name] | |
| if not bind_path then return end | |
| -- return the last parent that matches | |
| local indexes_flattened = {} | |
| for i = 2, #bind_path do | |
| local subpath = bind_path[i] | |
| if type(subpath) == "table" then | |
| for u = 1, #subpath do | |
| -- a table here represents multiple selection handled by GedMultiSelectAdapter; we can't find bind parents below that level | |
| if type(subpath[u]) == "table" then | |
| goto completed | |
| end | |
| table.insert(indexes_flattened, subpath[u]) | |
| end | |
| else | |
| table.insert(indexes_flattened, subpath) | |
| end | |
| end | |
| ::completed:: | |
| local obj, last_matching = self.bound_objects[bind_path[1]], nil | |
| for _, key in ipairs(indexes_flattened) do | |
| obj = TreeNodeByPath(obj, key) | |
| if IsKindOf(obj, type_name) then | |
| last_matching = obj | |
| end | |
| end | |
| return last_matching | |
| end | |
| function GedGameSocket:GetParentsList(name) | |
| local all_parents = {} | |
| local bind_path = self.bound_objects_path[name:match("(.+)|.+") or name] | |
| if bind_path then | |
| local obj = self.bound_objects[bind_path[1]] | |
| table.insert(all_parents, obj) | |
| for i = 2, #bind_path - 1 do | |
| table.insert(all_parents, self.bound_objects[bind_path[i].name]) | |
| end | |
| end | |
| return all_parents | |
| end | |
| function GedGameSocket:OnParentsModified(name) | |
| local all_parents = self:GetParentsList(name) | |
| -- call in reverse order, so a preset would be marked as dirty before the preset tree is refreshed | |
| for i = #all_parents, 1, -1 do | |
| ObjModified(all_parents[i]) | |
| end | |
| assert(next(SuspendObjModifiedReasons)) -- assume that SuspendObjModified is called and will prevent multiple updates of the same object | |
| for _, name in ipairs(self.root_names) do | |
| ObjModified(self.bound_objects[name]) | |
| end | |
| end | |
| function GedGameSocket:GatherAffectedGameObjects(obj) | |
| local ret = {} | |
| local objs_and_parents = self:GetParentsList(self:ResolveName(obj)) | |
| table.insert(objs_and_parents, obj) | |
| for _, obj in ipairs(objs_and_parents) do | |
| if IsValid(obj) then | |
| table.insert(ret, obj) | |
| elseif IsKindOf(obj, "GedMultiSelectAdapter") then | |
| table.iappend(ret, obj.__objects) | |
| end | |
| end | |
| ret = table.validate(table.get_unique(ret)) | |
| return #ret > 0 and ret | |
| end | |
| function GedGameSocket:RestoreAppState(undo_entry) | |
| -- 1. Set pending selection in all panels (will be set when panel data arrives) | |
| local app_state = undo_entry and undo_entry.app_state or self.last_app_state | |
| local focused_panel = app_state.focused_panel | |
| for context, state in pairs(app_state) do | |
| local sel = state.selection | |
| if sel then | |
| self:SetSelection(context, sel[1], sel[2], not "notify", "restoring_state", focused_panel == context) | |
| end | |
| end | |
| -- 2. Rebind each panel to its former object | |
| if undo_entry then | |
| self.bound_objects_path = table.copy(undo_entry.bound_objects_path, "deep") | |
| self.bound_objects_func = table.copy(undo_entry.bound_objects_func) | |
| self:SetLastAppState(app_state) | |
| end | |
| self:RebindAll() | |
| end | |
| function GedGameSocket:RebindAll() | |
| -- iterate a copy of the keys as the bound_objects changes while iterating | |
| for idx, name in ipairs(table.keys(self.bound_objects)) do | |
| if not name:find("|", 1, true) then | |
| local obj_path = self.bound_objects_path[name] | |
| local obj = self.bound_objects[obj_path and obj_path[1] or name] | |
| if obj then | |
| if obj_path then | |
| for i = 2, #obj_path do | |
| local path = obj_path[i] | |
| local last_entry = #path > 0 and path[#path] | |
| if type(last_entry) == "table" then | |
| obj = TreeNodeByPath(obj, unpack_params(path, 1, #path - 1)) | |
| obj = GedMultiSelectAdapter:new{ __objects = table.map(last_entry, function(idx) return TreeNodeByPath(obj, idx) end) } | |
| else | |
| obj = TreeNodeByPath(obj, unpack_params(path)) | |
| end | |
| end | |
| end | |
| if obj then | |
| local func = self.bound_objects_func[name] | |
| self:UnbindObj(name) | |
| self:UnbindObjs(name .. "|") -- unbind all views, Ged will rebind them | |
| self:BindObj(name, obj, func) | |
| self.bound_objects_path[name] = obj_path -- restore bind path as UnbindObj removes it | |
| elseif not self.bound_objects_filter[name] then | |
| self:UnbindObj(name) | |
| end | |
| end | |
| end | |
| end | |
| end | |
| function GedGameSocket:rfnBindFilterObj(name, filter_name, filter_class_or_instance) | |
| local filter = filter_class_or_instance | |
| if type(filter) == "string" then | |
| filter = _G[filter]:new() | |
| elseif not filter then | |
| filter = self:ResolveObj(filter_name) | |
| end | |
| assert(IsKindOf(filter, "GedFilter")) | |
| filter.ged = self | |
| filter.target_name = name | |
| self:BindObj(filter_name, filter) | |
| self.bound_objects_filter[name] = filter | |
| end | |
| function GedGameSocket:rfnBindObj(name, obj_address, func_name, ...) | |
| if func_name and not (type(func_name) == "string" and string.starts_with(func_name, "Ged")) then | |
| assert(not "func_name should start with 'Ged'") | |
| return | |
| end | |
| local parent_name, path | |
| if type(obj_address) == "table" then | |
| parent_name, path = obj_address[1], obj_address | |
| table.remove(path, 1) | |
| else | |
| parent_name, path = obj_address, empty_table | |
| end | |
| local params = pack_params(...) | |
| local obj, prop_id = self:ResolveObj(parent_name, unpack_params(path)) | |
| self:BindObj(name, obj, func_name and function(obj, filter) return _G[func_name](obj, filter, unpack_params(params)) end) | |
| if next(path) then | |
| local bind_path = self.bound_objects_path[parent_name] | |
| bind_path = bind_path and table.copy(bind_path, "deep") or { parent_name } | |
| path.name = name | |
| table.insert(bind_path, path) | |
| self.bound_objects_path[name] = bind_path | |
| end | |
| if obj and prop_id and not name:find("|", 1, true) then | |
| assert(#path == 1) | |
| self.prop_bindings[obj] = { parent = self.bound_objects[parent_name], prop_id = prop_id } | |
| end | |
| end | |
| function GedGameSocket:SetLastAppState(app_state) | |
| self.last_app_state = app_state | |
| for key, value in pairs(app_state) do | |
| if type(value) == "table" and value.selection then | |
| self:UpdateObjectsFromPanelSelection(key) | |
| end | |
| end | |
| end | |
| function GedGameSocket:GetSelectedObjectsParent(panel, selection) | |
| local parent = self:ResolveObj(panel) | |
| if not parent then return end | |
| local path = selection[1] | |
| if type(path) == "table" then | |
| local children_fn = function(obj) return obj.GedTreeChildren and obj.GedTreeChildren(obj) or obj end | |
| for i = 1, #path - 1 do | |
| parent = children_fn(parent)[path[i]] | |
| if not parent then return end | |
| end | |
| parent = children_fn(parent) | |
| end | |
| return parent | |
| end | |
| function GedGameSocket:UpdateObjectsFromPanelSelection(panel) | |
| local state = self.last_app_state[panel] | |
| local selection = state.selection | |
| if type(selection[2]) == "table" and next(selection[2]) then | |
| local objects = {} | |
| local parent = self:GetSelectedObjectsParent(panel, selection) | |
| if not parent then return end | |
| for i, idx in ipairs(selection[2]) do | |
| objects[i] = parent[idx] | |
| end | |
| state.selected_objects = objects | |
| else | |
| state.selected_objects = nil | |
| end | |
| end | |
| function GedGameSocket:UpdatePanelSelectionFromObjects(panel) | |
| if not self.last_app_state then return end | |
| panel = panel:match("(.+)|.+") | |
| local state = self.last_app_state[panel] | |
| local selection = state.selection | |
| local objects = state.selected_objects | |
| if selection and type(objects) == "table" then | |
| local objects_idxs = {} | |
| local parent = self:GetSelectedObjectsParent(panel, selection) | |
| if not parent then return end | |
| for _, obj in ipairs(objects) do | |
| objects_idxs[#objects_idxs + 1] = table.find(parent, obj) or nil | |
| end | |
| if #objects_idxs ~= #objects then | |
| -- we failed finding the objects in their former parent, perform a fully recursive search | |
| local root = self:ResolveObj(panel) | |
| local path, selected_idxs = RecursiveFindTreeItemPaths(root, objects) | |
| if path then | |
| self:SetSelection(panel, path, selected_idxs, not "notify") | |
| return | |
| end | |
| end | |
| if not table.iequal(state.selection[2], objects_idxs) then | |
| if type(selection[1]) == "table" then | |
| selection[1][#selection[1]] = objects_idxs[1] | |
| end | |
| selection[2] = objects_idxs | |
| self:SetSelection(panel, selection[1], selection[2], not "notify") | |
| end | |
| end | |
| end | |
| function GedGameSocket:rfnStoreAppState(app_state) | |
| self:SetLastAppState(app_state) | |
| end | |
| function GedGameSocket:rfnSelectAndBindObj(name, obj_address, func_name, ...) | |
| local panel_context = obj_address and obj_address[1] | |
| local sel = self.selected_object | |
| self:rfnBindObj(name, obj_address, func_name, ...) | |
| local obj = self:ResolveObj(name) | |
| if obj ~= sel then | |
| if sel then | |
| GedNotify(sel, "OnEditorSelect", false, self) | |
| Msg("GedOnEditorSelect", sel, false, self, panel_context) | |
| end | |
| if obj then | |
| GedNotify(obj, "OnEditorSelect", true, self) | |
| Msg("GedOnEditorSelect", obj, true, self, panel_context) | |
| end | |
| self.selected_object = obj | |
| end | |
| if self.last_app_state and self.last_app_state[name] then | |
| self.last_app_state[name].selection = nil | |
| end | |
| end | |
| function GedGameSocket:rfnSelectAndBindMultiObj(name, obj_address, obj_children_list, func_name, ...) | |
| PauseInfiniteLoopDetection("BindMultiObj") | |
| if #obj_children_list > 80 then | |
| GedSetUiStatus("ged_multi_select", "Please wait...") -- cleared in GedGetValues | |
| end | |
| GedNotify(self.selected_object, "OnEditorSelect", false, self) | |
| self:rfnBindMultiObj(name, obj_address, obj_children_list, func_name, ...) | |
| local obj = self:ResolveObj(name) | |
| Msg("GedOnEditorMultiSelect", obj, false, self) | |
| GedNotify(obj, "OnEditorSelect", true, self) | |
| Msg("GedOnEditorMultiSelect", obj, true, self) | |
| self.selected_object = obj | |
| ResumeInfiniteLoopDetection("BindMultiObj") | |
| if self.last_app_state and self.last_app_state[name] then | |
| self.last_app_state[name].selection = nil | |
| end | |
| end | |
| function GedGameSocket:rfnBindMultiObj(name, obj_address, obj_children_list, func_name, ...) | |
| local parent_name, path | |
| if type(obj_address) == "table" then | |
| parent_name, path = obj_address[1], obj_address | |
| table.remove(path, 1) | |
| else | |
| parent_name, path = obj_address, {} | |
| end | |
| local obj = self:ResolveObj(parent_name, unpack_params(path)) | |
| if not obj then | |
| return | |
| end | |
| table.sort(obj_children_list) | |
| local obj_list = table.map(obj_children_list, function(el) return TreeNodeByPath(obj, el) end) | |
| if #obj_list == 0 then | |
| return | |
| end | |
| obj = GedMultiSelectAdapter:new{ __objects = obj_list } | |
| local params = pack_params(...) | |
| self:BindObj(name, obj, func_name and function(obj) return _G[func_name](obj, unpack_params(params)) end) | |
| local bind_path = self.bound_objects_path[parent_name] | |
| bind_path = bind_path and table.copy(bind_path, "deep") or { parent_name } | |
| table.insert(path, obj_children_list) | |
| path.name = name | |
| table.insert(bind_path, path) | |
| self.bound_objects_path[name] = bind_path | |
| end | |
| function GedGameSocket:rfnUnbindObj(name, to_prefix) | |
| self:UnbindObj(name) | |
| if to_prefix then | |
| self:UnbindObjs(name .. to_prefix) | |
| end | |
| end | |
| function GedGameSocket:rfnGedActivated(initial) | |
| Msg("GedActivated", self, initial) | |
| end | |
| function GedGameSocket:NotifyEditorSetProperty(obj, prop_id, old_value, multi) | |
| Msg("GedPropertyEdited", self.ged_id, obj, prop_id, old_value) | |
| GedNotify(obj, "OnEditorSetProperty", prop_id, old_value, self, multi) | |
| end | |
| function GedGameSocket:Op(app_state, op_name, obj_name, params) | |
| local op_fn = _G[op_name] | |
| if not op_fn then | |
| print("Ged - unrecognized op", op_name) | |
| return "not found" | |
| end | |
| SuspendObjModified("GedOp") | |
| local obj = self:ResolveObj(obj_name) | |
| local game_objects = IsEditorActive() and obj and self:GatherAffectedGameObjects(obj) | |
| if game_objects then | |
| local name = "Edit objects" | |
| if op_name == "GedSetProperty" then | |
| local prop_id = params[1] | |
| local prop_meta = obj:GetPropertyMetadata(prop_id) | |
| name = string.format("Edit %s", prop_meta.name or prop_id) | |
| end | |
| XEditorUndo:BeginOp{ name = name, objects = game_objects, collapse_with_previous = (op_name == "GedSetProperty") } | |
| end | |
| local op_params = table.copy(params, "deep") -- keep a copy for the undo queue | |
| local ok, new_selection, undo_fn, slider_drag_id = sprocall(op_fn, self, obj, unpack_params(params)) | |
| if ok then | |
| if type(new_selection) == "string" then -- error | |
| local error_msg = new_selection | |
| self:Send("rfnApp", "GedOpError", error_msg) | |
| ResumeObjModified("GedOp") | |
| return new_selection | |
| elseif new_selection then | |
| self:ResetFilter(obj_name) | |
| end | |
| if undo_fn then | |
| assert(type(undo_fn) == "function") | |
| while #self.undo_queue ~= self.undo_position do | |
| table.remove(self.undo_queue) | |
| end | |
| local current = self.undo_queue[self.undo_position] | |
| if not (slider_drag_id and current and current.slider_drag_id == slider_drag_id) then | |
| self.undo_position = self.undo_position + 1 | |
| self.undo_queue[self.undo_position] = { | |
| app_state = app_state, | |
| op_fn = op_fn, | |
| obj_name = obj_name, | |
| op_params = op_params, | |
| bound_objects_path = table.copy(self.bound_objects_path, "deep"), | |
| bound_objects_func = table.copy(self.bound_objects_func), | |
| clipboard = table.copy(GedClipboard), | |
| slider_drag_id = slider_drag_id, | |
| undo_fn = undo_fn, | |
| } | |
| end | |
| end | |
| end | |
| obj = self:ResolveObj(obj_name) -- might change, e.g. new list item in a nested_list that was false | |
| if not slider_drag_id and ObjModifiedIsScheduled(obj) then | |
| self:OnParentsModified(obj_name) -- the change might affect how our object is displayed in the parent object(s) | |
| end | |
| ResumeObjModified("GedOp") | |
| if game_objects then | |
| XEditorUndo:EndOp(game_objects) | |
| end | |
| if ok and new_selection then | |
| self:SetSelection(obj_name, new_selection) | |
| end | |
| end | |
| function GedGameSocket:rfnGetLastError() | |
| return GetLastError() | |
| end | |
| function GedGameSocket:rfnOp(app_state, op_name, obj_name, ...) | |
| local params = table.pack(...) | |
| -- execute SetProperty immediately; it is sent as the selection is being changed and affects the newly selected object otherwise | |
| if op_name == "GedSetProperty" then | |
| self:Op(app_state, op_name, obj_name, params) | |
| return | |
| end | |
| CreateRealTimeThread(self.Op, self, app_state, op_name, obj_name, params) | |
| end | |
| function GedGameSocket:rfnUndo() | |
| if self.undo_position == 0 or IsValidThread(self.redo_thread) then return end | |
| local entry = self.undo_queue[self.undo_position] | |
| self.undo_position = self.undo_position - 1 | |
| SuspendObjModified("GedUndo") | |
| procall(entry.undo_fn) | |
| self:RestoreAppState(entry) | |
| self:ResetFilter(entry.obj_name) | |
| self:OnParentsModified(entry.obj_name) | |
| ResumeObjModified("GedUndo") | |
| end | |
| function GedGameSocket:rfnRedo() | |
| if self.undo_position == #self.undo_queue then return end | |
| self.undo_position = self.undo_position + 1 | |
| local entry = self.undo_queue[self.undo_position] | |
| self.redo_thread = CreateRealTimeThread(function() | |
| SuspendObjModified("GedRedo") | |
| local clipboard = GedClipboard | |
| GedClipboard = entry.clipboard | |
| self:RestoreAppState(entry) | |
| self:ResetFilter(entry.obj_name) | |
| local obj = self:ResolveObj(entry.obj_name) | |
| local params = table.copy(entry.op_params, "deep") -- make sure 'entry.op_params' is not modified | |
| local ok, new_selection, undo_fn = sprocall(entry.op_fn, self, obj, unpack_params(params)) | |
| if ok then | |
| assert(type(new_selection) ~= "string") -- no errors are expected with redo | |
| if new_selection then | |
| self:SetSelection(entry.obj_name, new_selection) | |
| end | |
| entry.undo_fn = undo_fn | |
| end | |
| self:OnParentsModified(entry.obj_name) | |
| GedClipboard = clipboard | |
| ResumeObjModified("GedRedo") | |
| end) | |
| end | |
| function GedGameSocket:SetSelection(panel_context, selection, multiple_selection, notify, restoring_state, focus) | |
| assert(not selection or type(selection) == "number" or type(selection) == "table") | |
| assert(not string.find(panel_context, "|")) | |
| self:Send("rfnApp", "SetSelection", panel_context, selection, multiple_selection, notify, restoring_state, focus) | |
| end | |
| function GedGameSocket:SetUiStatus(id, text, delay) | |
| self:Send("rfnApp", "SetUiStatus", id, text, delay) | |
| end | |
| function GedGameSocket:SetSearchString(search_string, panel) | |
| self:Send("rfnApp", "SetSearchString", panel or "root", search_string) | |
| end | |
| function GedGameSocket:SelectAll(panel) | |
| local objects, selection = self:ResolveObj(panel), {} | |
| if #objects > 0 then | |
| for i, _ in ipairs(objects) do | |
| table.insert(selection, i) | |
| end | |
| assert(not string.find(panel, "|")) | |
| self:Send("rfnApp", "SetSelection", panel, { 1 }, selection) | |
| end | |
| end | |
| function GedGameSocket:SelectSiblingsInFocusedPanel(selection, selected) | |
| self:Send("rfnApp", "SelectSiblingsInFocusedPanel", selection, selected) | |
| end | |
| function GedGameSocket:rfnRunGlobal(func_name, ...) | |
| if not string.starts_with(func_name, "Ged") then | |
| assert(not "func_name should start with 'Ged'") | |
| return | |
| end | |
| local fn = _G[func_name] | |
| if not fn then | |
| print("Ged - function not found", func_name) | |
| return "not found" | |
| end | |
| return fn(self, ...) | |
| end | |
| function GedGameSocket:rfnInvokeMethod(obj_name, func_name, ...) | |
| local obj = self:ResolveObj(obj_name) | |
| if not obj or IsKindOf(obj, "GedMultiSelectAdapter") then return false end | |
| if PropObjHasMember(obj, func_name) then | |
| if CanYield() then -- :Call() expects the result of the method call | |
| return obj[func_name](obj, self, ...) | |
| else | |
| CreateRealTimeThread(obj[func_name], obj, self, ...) | |
| end | |
| else | |
| print("The object has no method: ", func_name) | |
| end | |
| end | |
| function GedCustomEditorAction(ged, obj_name, func_name) | |
| local obj = ged:ResolveObj(obj_name) | |
| if not obj then return false end | |
| if PropObjHasMember(obj, func_name) then | |
| CreateRealTimeThread(function() obj[func_name](obj, ged) end) | |
| elseif rawget(_G, func_name) then | |
| CreateRealTimeThread(function() _G[func_name](ged, obj) end) | |
| else | |
| print("Could not find CustomEditorAction's method by name", func_name) | |
| end | |
| end | |
| function GedGetToggledActionState(ged, func_name) | |
| return _G[func_name](ged) | |
| end | |
| function GedGameSocket:ShowMessage(title, text) | |
| title = GedTranslate(title or "", nil, false) | |
| text = GedTranslate(text or "", nil, false) | |
| self:Send("rfnApp", "ShowMessage", title, text) | |
| end | |
| function GedGameSocket:WaitQuestion(title, text, ok_text, cancel_text) | |
| title = GedTranslate(title or "", nil, false) | |
| text = GedTranslate(text or "", nil, false) | |
| ok_text = GedTranslate(ok_text or "", nil, false) | |
| cancel_text = GedTranslate(cancel_text or "", nil, false) | |
| return self:Call("rfnApp", "WaitQuestion", title, text, ok_text, cancel_text) | |
| end | |
| function GedGameSocket:DeleteQuestion() | |
| return self:Call("rfnApp", "DeleteQuestion") | |
| end | |
| function GedGameSocket:WaitUserInput(title, default_text, combo_items) | |
| title = GedTranslate(title or "", nil, false) | |
| default_text = GedTranslate(default_text or "", nil, false) | |
| return self:Call("rfnApp", "WaitUserInput", title, default_text, combo_items) | |
| end | |
| function GedGameSocket:WaitListChoice(items, caption, start_selection, lines) | |
| if not caption or caption == "" then caption = "Please select:" end | |
| if not items or type(items) ~= "table" or #items == 0 then items = {""} end | |
| if not start_selection then start_selection = items[1] end | |
| return self:Call("rfnApp", "WaitListChoice", items, caption, start_selection, lines) | |
| end | |
| function GedGameSocket:WaitBrowseDialog(folder, filter, create, multiple) | |
| return self:Call("rfnApp", "WaitBrowseDialog", folder, filter, create, multiple) | |
| end | |
| function GedGameSocket:SetProgressStatus(text, progress, total_progress) | |
| self:Send("rfnApp", "SetProgressStatus", text, progress, total_progress) | |
| end | |
| -- We only send the text representation of the items for combo & choice props (assuming uniqueness), | |
| -- as the actual values could be complex objects that can't go through the socket. | |
| local function GedFormatComboItem(item, obj) | |
| if type(item) == "table" and not IsT(item) then | |
| return GedTranslate(item.name or item.text or Untranslated(item.id), obj, false) | |
| else | |
| return IsT(item) and GedTranslate(item, obj) or tostring(item) | |
| end | |
| end | |
| local function ComboGetItemIdByName(value, items, obj, allow_arbitrary_values, translate) | |
| if not value then return end | |
| for _, item in ipairs(items or empty_table) do | |
| if GedFormatComboItem(item, obj) == value then | |
| if type(item) == "table" then | |
| return item.id or (item.value ~= nil and item.value) | |
| else | |
| return item | |
| end | |
| end | |
| end | |
| if not allow_arbitrary_values then return end | |
| return translate and T{RandomLocId(), value} or value | |
| end | |
| local function ComboGetItemNameById(id, items, obj, allow_arbitrary_values) | |
| if not items then return end | |
| for _, item in ipairs(items) do | |
| if item == id or type(item) == "table" and not IsT(item) and (item.id or (item.value ~= nil and item.value)) == id then | |
| return GedFormatComboItem(item, obj) | |
| end | |
| end | |
| return IsT(id) and GedTranslate(id, obj) or id | |
| end | |
| local eval = prop_eval | |
| function GedGameSocket:rfnGetPropItems(obj_name, prop_id) | |
| local obj = self:ResolveObj(obj_name) | |
| if not obj then return empty_table end | |
| local meta = obj:GetPropertyMetadata(prop_id) | |
| local items = meta and eval(meta.items, obj, meta, {}) | |
| if not items then return empty_table end | |
| local ret = {} | |
| for i, item in ipairs(items) do | |
| local text = GedFormatComboItem(item, obj) | |
| ret[#ret + 1] = type(item) == "table" and item or text | |
| end | |
| return ret | |
| end | |
| function GedGameSocket:rfnGetPresetItems(obj_name, prop_id) | |
| local obj = self:ResolveObj(obj_name) | |
| local meta = GedIsValidObject(obj) and obj:GetPropertyMetadata(prop_id) | |
| if not meta then return empty_table end -- can happen when the selected object in Ged changes and GetPresetItems RPC is sent after that | |
| local preset_class = eval(meta.preset_class, obj, meta) | |
| if not preset_class or not g_Classes[preset_class] then return empty_table end | |
| local extra_item = eval(meta.extra_item, obj, meta) or nil | |
| local combo_format = _G[preset_class]:HasMember("ComboFormat") and _G[preset_class].ComboFormat | |
| local enumerator | |
| local preset_group = eval(meta.preset_group, obj, meta) | |
| if preset_group then | |
| enumerator = PresetGroupCombo(preset_class, preset_group, meta.preset_filter, extra_item, combo_format) | |
| elseif _G[preset_class].GlobalMap or IsPresetWithConstantGroup(_G[preset_class]) then | |
| enumerator = PresetsCombo(preset_class, nil, extra_item, meta.preset_filter, combo_format) | |
| else | |
| return { "" } | |
| end | |
| return table.iappend({ "" }, eval(enumerator, obj, meta)) | |
| end | |
| function GedGameSocket:MapGetGameObjects(obj_name, prop_id) | |
| local obj = self:ResolveObj(obj_name) | |
| if not obj then return empty_table end | |
| local meta = obj:GetPropertyMetadata(prop_id) | |
| if not meta.base_class then return empty_table end | |
| local base_class = eval(meta.base_class, obj, meta) or "Object" | |
| local objects = MapGet("map", base_class) or {} | |
| return objects | |
| end | |
| function GetObjectPropEditorFormatFuncDefault(gameobj) | |
| if gameobj and IsValid(gameobj) then | |
| local x, y = gameobj:GetPos():xy() | |
| local label = gameobj:GetProperty("EditorLabel") or gameobj.class | |
| return string.format("%s x:%d y:%d", label, x, y) | |
| else | |
| return "" | |
| end | |
| end | |
| function GetObjectPropEditorFormatFunc(prop_meta) | |
| local format_func = GetObjectPropEditorFormatFuncDefault | |
| if prop_meta.format_func then | |
| format_func = prop_meta.format_func | |
| end | |
| return format_func | |
| end | |
| function GedGameSocket:rfnMapGetGameObjects(obj_name, prop_id) | |
| local obj = self:ResolveObj(obj_name) | |
| if not obj then return { {value = false, text = ""} } end | |
| local meta = obj:GetPropertyMetadata(prop_id) | |
| local objects = self:MapGetGameObjects(obj_name, prop_id) | |
| local format_func = GetObjectPropEditorFormatFunc(meta) | |
| local items = { {value = false, text = ""} } | |
| for key, value in ipairs(objects) do | |
| table.insert(items, { | |
| value = value.handle, | |
| text = format_func(value), | |
| }) | |
| end | |
| return items | |
| end | |
| function GedGameSocket:rfnTreePanelNodeCollapsed(obj_name, path, collapsed) | |
| local obj = self:ResolveObj(obj_name, unpack_params(path)) | |
| if not obj then return end | |
| GedTreePanelCollapsedNodes[obj] = collapsed or nil | |
| if self.context.PresetClass then | |
| Msg("GedTreeNodeCollapsedChanged") | |
| end | |
| end | |
| function GedGameSocket:GetMatchingBoundObjects(view_to_function) | |
| local results = {} | |
| for name, object in ipairs(self.bound_objects) do | |
| local name = name:match("^(%w+)$") | |
| if not name then | |
| goto no_match | |
| end | |
| for view, func in pairs(view_to_function) do | |
| local full_name = name .. "|" .. view | |
| if not self.bound_objects_func[full_name] ~= func then | |
| goto no_match | |
| end | |
| end | |
| table.insert(results, object) | |
| ::no_match:: | |
| end | |
| return results | |
| end | |
| function GedForceUpdateObject(obj) | |
| local sockets = GedObjects[obj] | |
| if not sockets then return end | |
| for i = 1, #sockets - 1, 2 do | |
| local name, socket = sockets[i], sockets[i + 1] | |
| if socket.bound_objects[name] == obj then | |
| socket.bound_objects_svalue[name] = nil | |
| end | |
| end | |
| end | |
| function GedUpdateObjectValue(socket, obj, name) | |
| local func = socket.bound_objects_func[name] | |
| if not func then return end | |
| local values = func(obj or socket.bound_objects[name], socket:FindFilter(name)) | |
| local vpstr = ValueToLuaCode(values, nil, pstr("", 1024)) | |
| if vpstr ~= socket.bound_objects_svalue[name] then | |
| socket.bound_objects_svalue[name] = vpstr | |
| socket:Send("rfnObjValue", name, vpstr:str(), true) | |
| if name:ends_with("|list") or name:ends_with("|tree") then | |
| socket:UpdatePanelSelectionFromObjects(name) | |
| end | |
| end | |
| end | |
| function GedObjectModified(obj, view) | |
| local sockets = GedObjects[obj] | |
| if not sockets then return end | |
| Msg("GedObjectModified", obj, view) | |
| sockets = table.copy(sockets) -- rfnBindObj and other calls could be received during socket:Send in GedUpdateObjectValue | |
| for i = 1, #sockets - 1, 2 do | |
| local name, socket = sockets[i], sockets[i + 1] | |
| if socket.bound_objects[name] == obj and (not view or name:ends_with("|" .. view)) then | |
| GedUpdateObjectValue(socket, obj, name) | |
| end | |
| -- when a nested_obj / nested_list changes, call its property setter | |
| -- this allow nested_obj/nested_list properties implemented via Get/Set to work | |
| local prop_binding = socket.prop_bindings[obj] | |
| if prop_binding then | |
| prop_binding.parent:SetProperty(prop_binding.prop_id, obj) | |
| end | |
| end | |
| end | |
| function OnMsg.ObjModified(obj) | |
| GedObjectModified(obj) | |
| end | |
| function GedObjectDeleted(obj) | |
| if GedObjects[obj] then | |
| for id, conn in pairs(GedConnections) do | |
| if conn:ResolveObj("root") == obj then | |
| conn:Send("rfnClose") | |
| end | |
| end | |
| end | |
| end | |
| -- delayed rebinding of root in all GedApps | |
| -- can now be used with any bind name, not only root | |
| function GedRebindRoot(old_value, new_value, bind_name, func, dont_restore_app_state) | |
| if not old_value then return end | |
| bind_name = bind_name or "root" | |
| CreateRealTimeThread(function() | |
| for id, conn in pairs(GedConnections) do | |
| if conn:ResolveObj(bind_name) == old_value then | |
| conn:BindObj(bind_name, new_value, func) | |
| if not dont_restore_app_state then | |
| -- Will rebind all panels; everything that was bound "relatively" from the root might be invalidated as well. | |
| -- Keeps the selection and last focused panel the same (as stored in last_app_state). | |
| conn:RestoreAppState() | |
| conn:ResetUndoQueue() | |
| end | |
| end | |
| end | |
| end) | |
| end | |
| AutoResolveMethods.OnEditorSetProperty = true | |
| RecursiveCallMethods.OnEditorNew = "sprocall" | |
| RecursiveCallMethods.OnAfterEditorNew = "sprocall" | |
| RecursiveCallMethods.OnEditorDelete = "sprocall" | |
| RecursiveCallMethods.OnAfterEditorDelete = "sprocall" | |
| RecursiveCallMethods.OnAfterEditorSwap = "sprocall" | |
| RecursiveCallMethods.OnAfterEditorDragAndDrop = "procall" | |
| AutoResolveMethods.OnEditorSelect = true | |
| AutoResolveMethods.OnEditorDirty = true | |
| function GedNotify(obj, method, ...) | |
| if not obj then return end | |
| Msg("GedNotify", obj, method, ...) | |
| if PropObjHasMember(obj, method) then | |
| local ok, result = sprocall(obj[method], obj, ...) | |
| return ok and result | |
| end | |
| end | |
| function GedNotifyRecursive(obj, method, parent, ...) | |
| if not obj then return end | |
| assert(type(parent) == "table") | |
| obj:ForEachSubObject(function(obj, parents, key, ...) | |
| GedNotify(obj, method, parents[#parents] or parent, ...) | |
| end, ...) | |
| end | |
| ----- Data formatting functions | |
| function GedIsValidObject(obj) | |
| return IsKindOf(obj, "PropertyObject") and (not IsKindOf(obj, "CObject") or IsValid(obj)) | |
| end | |
| function GedGlobalPropertyCategories() | |
| return PropertyCategories | |
| end | |
| function GedPresetPropertyUsageStats(root, filter, preset_class) | |
| local stats, used_in = {}, {} | |
| ForEachPreset(preset_class, function(preset) | |
| for _, prop in ipairs(preset:GetProperties()) do | |
| local id = prop.id | |
| stats[id] = stats[id] or 0 | |
| if not preset:IsPropertyDefault(id, prop) then | |
| stats[id] = stats[id] + 1 | |
| used_in[id] = preset.id | |
| end | |
| end | |
| end) | |
| for id, count in pairs(stats) do | |
| if count == 1 then | |
| stats[id] = used_in[id] | |
| elseif count ~= 0 then | |
| stats[id] = nil | |
| end | |
| end | |
| return stats | |
| end | |
| local function ConvertSlashes(path) | |
| return string.gsub(path, "\\", "/") | |
| end | |
| local function OSFolderObject(os_path) | |
| local os_path, err = ConvertToOSPath(os_path) | |
| -- if an error occurred, then this path doesn't exist in the OS filesystem. | |
| -- if we return nil, the result will not be added to the list of paths for this control (see GedGetFolders) | |
| -- thus a button will not be created for it | |
| if not err then | |
| return { os_path = ConvertSlashes(os_path) } | |
| end | |
| end | |
| local function GameFolderObject(game_path) | |
| local os_path, err = ConvertToOSPath(SlashTerminate(game_path)) | |
| if not err then | |
| return { game_path = game_path, os_path = ConvertSlashes(os_path) } | |
| end | |
| end | |
| local function ToFolderObject(path, path_type) | |
| path = SlashTerminate(path) | |
| return path_type == "os" and OSFolderObject(path) or GameFolderObject(path) | |
| end | |
| local function GedGetFolders(obj, prop_meta, mod_def) | |
| local result = {} | |
| local folder = eval(prop_meta.folder, obj, prop_meta) | |
| local os_path = eval(prop_meta.os_path, obj, prop_meta) | |
| if folder then | |
| local default_type = os_path and "os" or "game" | |
| if type(folder) == "string" then | |
| result = { ToFolderObject(folder, default_type) } | |
| elseif type(folder) == "table" then | |
| for i,entry in ipairs(folder) do | |
| if type(entry) == "string" then | |
| table.insert(result, ToFolderObject(entry, default_type)) | |
| elseif type(entry) == "table" then | |
| local path_type = entry.os_path and "os" or entry.game_path and "game" or default_type | |
| table.insert(result, ToFolderObject(entry[1], path_type)) | |
| end | |
| end | |
| end | |
| end | |
| if not os_path then | |
| -- add built-in paths for image files | |
| if prop_meta.editor == "ui_image" then | |
| local preset = GetParentTableOfKindNoCheck(obj, "Preset") | |
| local common_preset = preset and preset:GetSaveLocationType() == "common" | |
| local builtin_paths = common_preset and { "CommonAssets/UI/" } or { "UI/", "CommonAssets/UI/" } | |
| for i, path in ipairs(builtin_paths) do | |
| if not table.find_value(result, "game_path", path) then | |
| table.insert(result, { | |
| game_path = path, | |
| os_path = ConvertToOSPath(path), | |
| }) | |
| end | |
| end | |
| elseif not next(result) then | |
| table.insert(result, OSFolderObject("./")) | |
| end | |
| if mod_def then | |
| table.insert(result, { | |
| os_path = mod_def.path, --backwards compatibility | |
| }) | |
| table.insert(result, { | |
| game_path = mod_def.content_path, | |
| os_path = ConvertToOSPath(mod_def.content_path), | |
| }) | |
| end | |
| end | |
| return result | |
| end | |
| local function GedPopulateClassUseCounts(class_list, obj) | |
| local parent = IsKindOf(obj, "Preset") and obj or GetParentTableOfKindNoCheck(obj, "Preset") | |
| if not parent then return end | |
| local counts = {} | |
| local iterations = 1 | |
| ForEachPreset(parent.class, function(preset) | |
| preset:ForEachSubObject(function(obj) | |
| local class = obj.class | |
| if obj.class then | |
| counts[class] = (counts[class] or 0) + 1 | |
| end | |
| iterations = iterations + 1 | |
| if iterations > 9999 then return "break" end | |
| end) | |
| if iterations > 9999 then return "break" end | |
| end) | |
| for _, item in ipairs(class_list) do | |
| item.use_count = counts[item.value] or 0 | |
| item.use_count_in_preset = parent.PresetClass or parent.class | |
| end | |
| end | |
| function GedGetSubItemClassList(socket, obj, path, prop_script_domain) | |
| local obj = socket:ResolveObj(obj, unpack_params(path)) | |
| if not obj then return end | |
| local items = obj:EditorItemsMenu() | |
| local filtered_items = {} | |
| for _, item in ipairs(items) do | |
| -- Filter script blocks by script domain | |
| if not item.ScriptDomain or item.ScriptDomain == prop_script_domain then | |
| table.insert(filtered_items, { | |
| text = item.EditorName, | |
| value = item.Class, | |
| documentation = GedGetDocumentation(g_Classes[item.Class]), | |
| category = item.EditorSubmenu | |
| }) | |
| end | |
| end | |
| GedPopulateClassUseCounts(filtered_items, obj) | |
| return filtered_items | |
| end | |
| function GedGetSiblingClassList(socket, obj, path) | |
| table.remove(path) | |
| return GedGetSubItemClassList(socket, obj, path) | |
| end | |
| ClassNonInheritableMembers.EditorExcludeAsNested = true | |
| function GedGetNestedClassItems(socket, obj, prop_id) | |
| local obj = socket:ResolveObj(obj) | |
| local prop_meta = obj:GetPropertyMetadata(prop_id) | |
| local base_class = eval(prop_meta.base_class, obj, prop_meta) or eval(prop_meta.class, obj, prop_meta) | |
| local def = base_class and g_Classes[base_class] | |
| if not def or base_class == "PropertyObject" then | |
| assert(false, "Invalid base_class or class for a nested obj/list property") | |
| return {} | |
| end | |
| local list = {} | |
| local default_format = T(243864368637, "<class>") | |
| local function AddList(list, name, class, default_format) | |
| list[#list + 1] = { | |
| text = GedTranslate(class:HasMember("ComboFormat") and class.ComboFormat or default_format, class, false), | |
| value = name, | |
| documentation = GedGetDocumentation(class), | |
| category = class:HasMember("EditorNestedObjCategory") and class.EditorNestedObjCategory, | |
| } | |
| end | |
| if prop_meta.base_class then | |
| local inclusive = eval(prop_meta.inclusive, obj, prop_meta) | |
| if not eval(prop_meta.no_descendants, obj, prop_meta) then | |
| local all_descendants = eval(prop_meta.all_descendants, obj, prop_meta) | |
| local class_filter = prop_meta.class_filter | |
| local descendants_func = all_descendants and ClassDescendantsList or ClassLeafDescendantsList | |
| descendants_func(base_class, function(name, class) | |
| if not (class:HasMember("EditorExcludeAsNested") and class.EditorExcludeAsNested) and | |
| (not class_filter or class_filter(name, class, obj)) and | |
| not class:IsKindOf("Preset") | |
| then | |
| AddList(list, name, class, default_format) | |
| end | |
| end) | |
| end | |
| if inclusive or #list == 0 then | |
| AddList(list, base_class, def, default_format) | |
| end | |
| table.sortby_field(list, "value") | |
| else | |
| AddList(list, base_class, def, default_format) | |
| end | |
| GedPopulateClassUseCounts(list, obj) | |
| return list | |
| end | |
| local function GedGetProperty(obj, prop_meta) | |
| if eval(prop_meta.no_edit, obj, prop_meta) or not prop_meta.editor then | |
| return | |
| end | |
| local prop_id = prop_meta.id | |
| local name = eval(prop_meta.name, obj, prop_meta, "") | |
| if name and not IsT(name) then | |
| name = tostring(name) | |
| end | |
| local help = eval(prop_meta.help, obj, prop_meta, "") | |
| local editor = eval(prop_meta.editor, obj, prop_meta, "") | |
| local lines | |
| local scale = eval(prop_meta.scale, obj, prop_meta) | |
| local scale_name | |
| if scale and type(scale) == "string" then | |
| scale_name = scale | |
| elseif prop_meta.translate == true then | |
| scale_name = "T" | |
| end | |
| scale = type(scale) == "string" and const.Scale[scale] or scale | |
| scale = scale ~= 1 and scale or nil | |
| local buttons = eval(prop_meta.buttons, obj, prop_meta) or nil | |
| local buttons_data | |
| if editor == "number" and ((obj:IsKindOf("Preset") and obj.HasParameters) or (not obj:IsKindOf("Preset") and obj:HasMember("param_bindings"))) then | |
| buttons_data = { { name = "Param", func = "PickParam" } } | |
| end | |
| if buttons then | |
| buttons_data = buttons_data or {} | |
| for _, button in ipairs(buttons) do | |
| button.name = button.name or button[1] | |
| button.func = button.func or button[2] or button[1] | |
| assert(not table.find(buttons_data, "name", button.name), "Duplicate property button names!") | |
| if not button.is_hidden or not button.is_hidden(obj, prop_meta) then | |
| table.insert(buttons_data, { | |
| name = button.name, | |
| func = type(button.func) == "string" and button.func or nil, | |
| param = button.param, | |
| icon = button.icon, | |
| icon_scale = button.icon_scale, | |
| toggle = button.toggle, | |
| toggled = button.toggle and button.is_toggled and button.is_toggled(obj), | |
| rollover = button.rollover, | |
| }) | |
| end | |
| end | |
| if editor == "buttons" and not next(buttons_data) then | |
| return | |
| end | |
| end | |
| local editor_class = g_Classes[GedPropEditors[editor]] | |
| local items = rawget(prop_meta, "items") and eval(prop_meta.items, obj, prop_meta) or nil | |
| local prop = { | |
| id = prop_id, | |
| category = eval(prop_meta.category, obj, prop_meta), | |
| editor = editor, | |
| script_domain = eval(prop_meta.script_domain, obj, prop_meta), | |
| default = GameToGedValue(obj:GetDefaultPropertyValue(prop_id, prop_meta), prop_meta, obj, items), | |
| sort_order = eval(prop_meta.sort_order, obj, prop_meta) or nil, | |
| name_on_top = eval(prop_meta.name_on_top, obj, prop_meta) or nil, | |
| name = name ~= "" and (IsT(name) and GedTranslate(name, obj) or name) or nil, | |
| help = help ~= "" and (IsT(help) and GedTranslate(help, obj) or help) or nil, | |
| read_only = eval(prop_meta.read_only, obj, prop_meta) or editor == "image" or editor == "grid" or nil, | |
| hide_name = eval(prop_meta.hide_name, obj, prop_meta) or nil, | |
| buttons = buttons_data, | |
| scale = scale, | |
| scale_name = scale_name, | |
| dlc_name = prop_meta.dlc, | |
| min = (editor == "number" or editor == "range" or editor == "point" or editor == "box") and eval(prop_meta.min, obj, prop_meta) or nil, | |
| max = (editor == "number" or editor == "range" or editor == "point" or editor == "box") and eval(prop_meta.max, obj, prop_meta) or nil, | |
| step = (editor == "number" or editor == "range") and eval(prop_meta.step, obj, prop_meta) or nil, | |
| float = (editor == "number" or editor == "range") and eval(prop_meta.float, obj, prop_meta) or nil, | |
| buttons_step = (editor == "number" or editor == "range") and prop_meta.slider and eval(prop_meta.buttons_step, obj, prop_meta) or nil, | |
| slider = (editor == "number" or editor == "range") and eval(prop_meta.slider, obj, prop_meta) or nil, | |
| params = (editor == "func" or editor == "expression") and eval(prop_meta.params, obj, prop_meta) or nil, | |
| translate = editor == "text" and eval(prop_meta.translate, obj, prop_meta) or nil, | |
| lines = (editor == "text" or editor == "func" or editor == "prop_table") and (eval(prop_meta.lines, obj, prop_meta) or lines) or nil, | |
| max_lines = (editor == "text" or editor == "func" or editor == "prop_table") and eval(prop_meta.max_lines, obj, prop_meta) or nil, | |
| max_len = editor == "text" and eval(prop_meta.max_len, obj, prop_meta) or nil, | |
| trim_spaces = editor == "text" and eval(prop_meta.trim_spaces, obj, prop_meta) or nil, | |
| realtime_update = editor == "text" and eval(prop_meta.realtime_update, obj, prop_meta) or nil, | |
| allowed_chars = editor == "text" and eval(prop_meta.allowed_chars, obj, prop_meta) or nil, | |
| size = editor == "flags" and (eval(prop_meta.size, obj, prop_meta) or 32) or nil, | |
| items = (editor == "flags" or editor == "set" or editor == "number_list" or editor == "string_list" or editor == "texture_picker" or editor == "text_picker") | |
| and items or nil, -- N.B: 'items' for combo properties are fetched on demand as an optimization, | |
| item_default = (editor == "number_list" or editor == "string_list" or editor == "preset_id_list") and eval(prop_meta.item_default, obj, prop_meta) or nil, | |
| arbitrary_value = (editor == "string_list" and items) and eval(prop_meta.arbitrary_value, obj, prop_meta) or nil, | |
| max_items = (editor == "number_list" or editor == "string_list" or editor == "preset_id_list" or editor == "T_list") and eval(prop_meta.max_items, obj, prop_meta) or nil, | |
| exponent = editor == "number" and eval(prop_meta.exponent, obj, prop_meta) or nil, | |
| per_item_buttons = (editor == "number_list" or editor == "string_list" or editor == "preset_id_list" or editor == "T_list") and prop_meta.per_item_buttons or nil, | |
| lock_ratio = IsKindOf(editor_class, "GedCoordEditor") and prop_meta.lock_ratio or nil, | |
| } | |
| if editor == "number_list" or editor == "string_list" or editor == "preset_id_list" then | |
| if eval(prop_meta.weights, obj, prop_meta) then | |
| prop.weights = true | |
| prop.weight_default = eval(prop_meta.weight_default, obj, prop_meta) or nil | |
| prop.weight_key = eval(prop_meta.weight_key, obj, prop_meta) or nil | |
| prop.value_key = eval(prop_meta.value_key, obj, prop_meta) or nil | |
| end | |
| end | |
| if items then -- all kinds or properties that use a combo box, including *_list properties | |
| prop.items_allow_tags = eval(prop_meta.items_allow_tags, obj, prop_meta) or nil | |
| prop.show_recent_items = eval(prop_meta.show_recent_items, obj, prop_meta) or nil | |
| prop.mru_storage_id = prop.show_recent_items and (prop_meta.mru_storage_id or string.format("%s.%s", obj.class, prop_meta.id)) -- ideally would use the class where the property was defined | |
| end | |
| if editor == "number" or editor == "text" or editor == "prop_table" or editor == "object" or editor == "range" or | |
| editor == "point" or editor == "box" or editor == "rect" or editor == "expression" or editor == "func" | |
| then | |
| prop.auto_select_all = eval(prop_meta.auto_select_all, obj, prop_meta) or nil | |
| prop.no_auto_select = eval(prop_meta.no_auto_select, obj, prop_meta) or nil | |
| end | |
| if editor == "nested_list" or editor == "nested_obj" then | |
| local base_class = eval(prop_meta.base_class, obj, prop_meta) | |
| local class = eval(prop_meta.class, obj, prop_meta) | |
| prop.base_class = base_class or class -- used for clipboard class when copying items | |
| prop.format = eval(prop_meta.format, obj, prop_meta) or nil | |
| prop.auto_expand = eval(prop_meta.auto_expand, obj, prop_meta) or nil | |
| prop.suppress_props = eval(prop_meta.suppress_props, obj, prop_meta) or nil | |
| end | |
| if editor == "property_array" then | |
| prop.base_class = "GedDynamicProps" | |
| prop.auto_expand = true | |
| end | |
| if editor == "texture_picker" or editor == "text_picker" then | |
| prop.max_rows = eval(prop_meta.max_rows, obj, prop_meta) or nil | |
| prop.multiple = eval(prop_meta.multiple, obj, prop_meta) or nil | |
| prop.small_font = eval(prop_meta.small_font, obj, prop_meta) or nil | |
| prop.filter_by_prop = eval(prop_meta.filter_by_prop, obj, prop_meta) or nil | |
| if editor == "texture_picker" then | |
| prop.thumb_size = eval(prop_meta.thumb_size, obj, prop_meta) or nil | |
| prop.thumb_width = eval(prop_meta.thumb_width, obj, prop_meta) or nil | |
| prop.thumb_height = eval(prop_meta.thumb_height, obj, prop_meta) or nil | |
| prop.thumb_zoom = eval(prop_meta.thumb_zoom, obj, prop_meta) or nil | |
| prop.alt_prop = eval(prop_meta.alt_prop, obj, prop_meta) or nil | |
| prop.base_color_map = eval(prop_meta.base_color_map, obj, prop_meta) or nil | |
| else -- text_picker | |
| prop.horizontal = eval(prop_meta.horizontal, obj, prop_meta) or nil | |
| prop.virtual_items = eval(prop_meta.virtual_items, obj, prop_meta) or nil | |
| prop.bookmark_fn = eval(prop_meta.bookmark_fn, obj, prop_meta) or nil | |
| end | |
| end | |
| if editor == "set" then | |
| prop.horizontal = eval(prop_meta.horizontal, obj, prop_meta) or nil | |
| prop.small_font = eval(prop_meta.small_font, obj, prop_meta) or nil | |
| prop.three_state = eval(prop_meta.three_state, obj, prop_meta) or nil | |
| prop.max_items_in_set = eval(prop_meta.max_items_in_set, obj, prop_meta) or nil | |
| prop.arbitrary_value = eval(prop_meta.arbitrary_value, obj, prop_meta) or nil | |
| end | |
| if editor == "preset_id" or editor == "preset_id_list" then | |
| prop.preset_class = eval(prop_meta.preset_class, obj, prop_meta) | |
| prop.preset_group = eval(prop_meta.preset_group, obj, prop_meta) | |
| prop.editor_preview = eval(prop_meta.editor_preview, obj, prop_meta) | |
| if prop.editor_preview == true then | |
| prop.editor_preview = g_Classes[prop.preset_class].EditorPreview | |
| end | |
| prop.editor_preview = prop.editor_preview and TDevModeGetEnglishText(prop.editor_preview, not "deep", "no_assert") or nil | |
| end | |
| if editor == "browse" or editor == "ui_image" or editor == "font" then | |
| local mod_def = TryGetModDefFromObj(obj) | |
| prop.image_preview_size = eval(prop_meta.image_preview_size, obj, prop_meta) or nil | |
| prop.filter = eval(prop_meta.filter, obj, prop_meta) or nil | |
| prop.dont_validate = eval(prop_meta.dont_validate, obj, prop_meta) or nil | |
| prop.os_path = eval(prop_meta.os_path, obj, prop_meta) or nil | |
| prop.folder = GedGetFolders(obj, prop_meta, mod_def) or nil | |
| prop.allow_missing = eval(prop_meta.allow_missing, obj, prop_meta) or nil | |
| prop.force_extension = eval(prop_meta.force_extension, obj, prop_meta) or nil | |
| prop.mod_dst = eval(prop_meta.mod_dst, obj, prop_meta) or nil | |
| end | |
| if editor == "text" then -- nil means 'true' for wordwrap, so we need a separate if statement | |
| prop.wordwrap = eval(prop_meta.wordwrap, obj, prop_meta) | |
| prop.text_style = eval(prop_meta.text_style, obj, prop_meta) | |
| prop.code = eval(prop_meta.code, obj, prop_meta) | |
| prop.trim_spaces = eval(prop_meta.trim_spaces, obj, prop_meta) | |
| end | |
| if editor == "func" then | |
| prop.trim_spaces = false | |
| end | |
| if editor == "image" then | |
| prop.img_back = eval(prop_meta.img_back, obj, prop_meta) or nil | |
| prop.img_size = eval(prop_meta.img_size, obj, prop_meta) or nil | |
| prop.img_width = eval(prop_meta.img_width, obj, prop_meta) or nil | |
| prop.img_height = eval(prop_meta.img_height, obj, prop_meta) or nil | |
| prop.img_box = eval(prop_meta.img_box, obj, prop_meta) or nil | |
| prop.img_draw_alpha_only = eval(prop_meta.img_draw_alpha_only, obj, prop_meta) or nil | |
| prop.img_polyline_color = eval(prop_meta.img_polyline_color, obj, prop_meta) or nil | |
| prop.img_polyline = eval(prop_meta.img_polyline, obj, prop_meta) or nil | |
| local img_polyline_closed = eval(prop_meta.img_polyline_closed, obj, prop_meta) or nil | |
| if img_polyline_closed and prop.img_polyline and prop.img_polyline_color then | |
| prop.img_polyline = table.copy(prop.img_polyline, "deep") | |
| for _, v in ipairs(prop.img_polyline) do | |
| if type(v) == "table" then | |
| v[#v+1] = v[1] | |
| end | |
| end | |
| end | |
| prop.base_color_map = eval(prop_meta.base_color_map, obj, prop_meta) or nil | |
| end | |
| if editor == "grid" then | |
| prop.frame = eval(prop_meta.frame, obj, prop_meta) or nil | |
| prop.color = eval(prop_meta.color, obj, prop_meta) or nil | |
| prop.min = eval(prop_meta.min, obj, prop_meta) or nil | |
| prop.max = eval(prop_meta.max, obj, prop_meta) or nil | |
| prop.invalid_value = eval(prop_meta.invalid_value, obj, prop_meta) or nil | |
| prop.grid_offset = eval(prop_meta.grid_offset, obj, prop_meta) or nil | |
| prop.dont_normalize = eval(prop_meta.dont_normalize, obj, prop_meta) or nil | |
| end | |
| if editor == "color" then | |
| prop.alpha = (prop_meta.alpha == nil) or eval(prop_meta.alpha, obj, prop_meta) or false | |
| end | |
| if editor == "packedcurve" then | |
| prop.display_scale_x = eval(prop_meta.display_scale_x, obj, prop_meta) or nil | |
| prop.max_amplitude = eval(prop_meta.max_amplitude, obj, prop_meta) or nil | |
| prop.min_amplitude = eval(prop_meta.min_amplitude, obj, prop_meta) or nil | |
| prop.color_args = eval(prop_meta.color_args, obj, prop_meta) or nil | |
| end | |
| if editor == "curve4" then | |
| prop.scale_x = eval(prop_meta.scale_x, obj, prop_meta) or nil | |
| prop.max_x = eval(prop_meta.max_x, obj, prop_meta) or nil | |
| prop.min_x = eval(prop_meta.min_x, obj, prop_meta) or nil | |
| prop.color_args = eval(prop_meta.color_args, obj, prop_meta) or nil | |
| prop.no_minmax = eval(prop_meta.no_minmax, obj, prop_meta) or nil | |
| prop.max = eval(prop_meta.max, obj, prop_meta) or nil | |
| prop.min = eval(prop_meta.min, obj, prop_meta) or nil | |
| prop.scale = eval(prop_meta.scale, obj, prop_meta) or nil | |
| prop.control_points = eval(prop_meta.control_points, obj, prop_meta) or nil | |
| prop.fixedx = eval(prop_meta.fixedx, obj, prop_meta) or nil | |
| end | |
| if editor == "script" then | |
| prop.name = string.format("%s(%s)", prop.name or prop.id, eval(prop_meta.params, obj, prop_meta) or "") | |
| if g_EditedScript and g_EditedScript == obj:GetProperty(prop.id) then | |
| prop.name = "<style GedHighlight>" .. prop.name | |
| end | |
| prop.class = eval(prop_meta.class, obj, prop_meta) or "ScriptProgram" | |
| end | |
| if editor == "linked_presets" then | |
| assert(IsKindOfClasses(obj, "Preset", "GedMultiSelectAdapter")) | |
| prop.preset_classes = eval(prop_meta.preset_classes, obj, prop_meta) or nil | |
| local data_exists | |
| for _, preset_class in ipairs(prop.preset_classes) do | |
| data_exists = data_exists or FindLinkedPresetOfClass(obj, preset_class) | |
| end | |
| -- setup .buttons, .help, .suppress_props in props | |
| local name = prop.name or prop.id | |
| prop.buttons = prop.buttons or {} | |
| table.insert(prop.buttons, not data_exists and | |
| { name = "Create " .. name, func = "GedCreateLinkedPresets" } or | |
| { name = "Delete " .. name, func = "GedDeleteLinkedPresets" }) | |
| prop.help = "<center><style GedHighlight>" .. (prop.help or name) | |
| prop.suppress_props = table.copy(eval(prop_meta.suppress_props, obj, prop_meta) or empty_table) | |
| for _, id in ipairs({ "Id", "SaveIn", "Group", "SortKey", "Parameters", "Comment", "TODO" }) do | |
| assert(not prop.suppress_props[id]) -- preset class name coincides with one of the strings above? | |
| prop.suppress_props[id] = true | |
| end | |
| end | |
| if editor == "shortcut" then | |
| prop.shortcut_type = eval(prop_meta.shortcut_type, obj, prop_meta) or nil | |
| end | |
| if editor == "documentation" then | |
| local documentation = obj:GetProperty("Documentation") | |
| if not documentation or documentation == Undefined() then return end -- no documentation to show | |
| prop.help = not IsDocumentationHidden(obj) and documentation | |
| prop.buttons = prop.buttons or {} | |
| if prop.help then | |
| if obj.DocumentationLink then | |
| table.insert(prop.buttons, { name = "Read More", func = "GedOpenDocumentationLink" }) | |
| end | |
| table.insert(prop.buttons, { name = "Hide Help", func = "GedHideDocumentation" }) | |
| else | |
| table.insert(prop.buttons, { name = "Show Help", func = "GedShowDocumentation" }) | |
| end | |
| end | |
| return prop | |
| end | |
| function GedGetProperties(obj, filter, suppress_props) | |
| suppress_props = suppress_props or empty_table | |
| local ged_props = {} | |
| if not GedIsValidObject(obj) then return ged_props end | |
| for _, prop_meta in ipairs(obj:GetProperties()) do | |
| if not suppress_props[prop_meta.id] then | |
| local ok, prop_or_err = procall(GedGetProperty, obj, prop_meta) | |
| if not ok then | |
| assert(false, string.format("[Ged] Failed to send property %s, likely a metadata function such as 'items' crashed. Internal err:\n%s", prop_meta.id, tostring(prop_or_err))) | |
| end | |
| ged_props[#ged_props + 1] = prop_or_err | |
| end | |
| end | |
| ged_props.tabs = | |
| obj:HasMember("PropertyTabs") and obj.PropertyTabs or | |
| obj:HasMember("GetPropertyTabs") and obj:GetPropertyTabs() | |
| ged_props.read_only = not not obj:IsReadOnly() | |
| return ged_props | |
| end | |
| function GedGetValues(obj) | |
| local values = {} | |
| if not GedIsValidObject(obj) then return values end | |
| obj:PrepareForEditing() | |
| for _, prop_meta in ipairs(obj:GetProperties()) do | |
| if not eval(prop_meta.no_edit, obj, prop_meta) then | |
| local prop_id = prop_meta.id | |
| local value = obj:GetProperty(prop_id) | |
| if type(value) == "table" then | |
| GedTablePropsCache[obj] = GedTablePropsCache[obj] or {} | |
| GedTablePropsCache[obj][prop_id] = value | |
| end | |
| if not obj:IsDefaultPropertyValue(prop_id, prop_meta, value) then | |
| if rawget(obj, "param_bindings") and obj.param_bindings[prop_id] then | |
| values[prop_id] = obj.param_bindings[prop_id] | |
| else | |
| values[prop_id] = GameToGedValue(value, prop_meta, obj) | |
| end | |
| end | |
| end | |
| end | |
| GedSetUiStatus("ged_multi_select") | |
| return values | |
| end | |
| function GedGetReadOnly(obj) | |
| -- Mod items from unloaded or packed mods should be read-only | |
| if obj.mod and (not obj.mod:ItemsLoaded() or obj.mod:IsPacked()) then | |
| return true | |
| end | |
| local obj_read_only | |
| if obj and type(obj.IsReadOnly) == "function" then | |
| obj_read_only = obj:IsReadOnly() or false | |
| if obj_read_only then | |
| return obj_read_only | |
| end | |
| end | |
| local parent = obj | |
| repeat | |
| parent = ParentTableCache[parent] | |
| if parent and type(parent.IsReadOnly) == "function" then | |
| if parent:IsReadOnly() then | |
| return true | |
| end | |
| end | |
| until parent == nil | |
| return obj_read_only | |
| end | |
| -- Returns the mod id of a preset if it has one | |
| function GedGetPresetMod(obj) | |
| if obj.mod and obj.mod.id then | |
| return obj.mod.id | |
| end | |
| return false | |
| end | |
| function GedListObjects(obj, filter, format, allow_objects_only) | |
| if allow_objects_only and (not GedIsValidObject(obj) or IsKindOf(obj, "GedMultiSelectAdapter")) then | |
| return {} | |
| end | |
| if not filter and IsKindOf(obj, "GedFilter") then | |
| filter = obj | |
| end | |
| local format = T{format} | |
| local objects, ids = {}, {} | |
| local filtered = filter and {} | |
| if filter then | |
| filter:PrepareForFiltering() | |
| end | |
| local displayed_items = 0 | |
| for i = 1, #obj do | |
| local item = obj[i] | |
| ids[i] = tostring(item) | |
| objects[i] = type(item) == "string" and item or GedTranslate(format, item, not "check") | |
| if filter and not filter:FilterObject(item) then | |
| filtered[i] = true | |
| else | |
| displayed_items = displayed_items + 1 | |
| end | |
| end | |
| if filter then | |
| objects.filtered = filtered | |
| filter:DoneFiltering(displayed_items, filtered) | |
| end | |
| objects.ids = ids | |
| return objects | |
| end | |
| function GedGetGraphData(obj) | |
| if GedIsValidObject(obj) then | |
| assert(IsKindOf(obj, "GraphContainer")) | |
| return obj:GetGraphData() | |
| end | |
| end | |
| function GedSetGraphData(socket, obj, data) | |
| assert(IsKindOf(obj, "GraphContainer")) | |
| obj:SetGraphData(data) | |
| end | |
| function GedBindGraphNode(socket, graph_name, node_handle, bind_name) | |
| local graph = socket:ResolveObj(graph_name) | |
| local node_idx = table.find(graph, "handle", node_handle) | |
| if node_idx then | |
| socket:rfnBindObj(bind_name, { graph_name, node_idx }) | |
| end | |
| end | |
| local function GedDeduceSubitemMenus(node, obj, parent) | |
| local button_mode = IsKindOf(obj, "Container") and obj:GetContainerAddNewButtonMode() | |
| if not button_mode then return end | |
| local obj_container = IsKindOf(obj, "Container") and obj.ContainerClass ~= "" | |
| local parent_container = IsKindOf(parent, "Container") and parent.ContainerClass ~= "" | |
| if not obj_container and not parent_container then return end | |
| node.child_button_mode = button_mode | |
| node.child_class = obj_container and obj.ContainerClass or nil | |
| node.sibling_class = parent_container and parent.ContainerClass or nil | |
| node.child_name = node.child_class and g_Classes[node.child_class].EditorName | |
| node.sibling_name = node.sibling_class and g_Classes[node.sibling_class].EditorName | |
| --[[if node.child_button_mode:ends_with("combined") and node.child_class and node.sibling_class then | |
| -- if all child classes can also be added as siblings, only allow sibling creation; this skips the "Add child"/"Add below" choice step | |
| -- (in this case, the user can use the "Move in" afterwards if they wanted to create a child) | |
| local children, siblings = obj:EditorItemsMenu(), parent:EditorItemsMenu() | |
| for _, item in ipairs(siblings) do | |
| siblings[item.Class] = true | |
| end | |
| local all_children_can_be_siblings = true | |
| for _, item in ipairs(children) do | |
| if not siblings[item.Class] then | |
| all_children_can_be_siblings = false | |
| break | |
| end | |
| end | |
| if all_children_can_be_siblings then | |
| node.child_class = nil | |
| end | |
| end]] | |
| end | |
| local function GedCreateNode(obj, parent, format_fn, enable_rollover) | |
| local collapsed = GedTreePanelCollapsedNodes[obj] | |
| if collapsed == nil and IsKindOf(obj, "PropertyObject") and obj:HasMember("GedTreeCollapsedByDefault") then | |
| collapsed = obj.GedTreeCollapsedByDefault or nil | |
| end | |
| local node = { | |
| id = tostring(obj), | |
| name = format_fn(obj), | |
| collapsed = collapsed, | |
| class = obj.class, | |
| rollover = enable_rollover and GedGetDocumentation(obj), | |
| } | |
| GedDeduceSubitemMenus(node, obj, parent) | |
| return node | |
| end | |
| function GedExpandNode(obj, parent, format_fn, children_fn, filter_fn, enable_rollover) | |
| if type(obj) ~= "table" then | |
| return tostring(obj), 1 | |
| end | |
| local node = GedCreateNode(obj, parent, format_fn, enable_rollover) | |
| local total_displayed_items = 0 | |
| local children_table = children_fn(obj) or empty_table | |
| for i = 1, #children_table do | |
| local item = children_table[i] | |
| if not IsKindOf(item, "CObject") or IsValid(item) then | |
| local child_node, displayed_items = GedExpandNode(item, obj, format_fn, children_fn, filter_fn, enable_rollover) | |
| total_displayed_items = total_displayed_items + displayed_items | |
| node[#node + 1] = child_node | |
| end | |
| end | |
| node.filtered = filter_fn and not filter_fn(obj) and total_displayed_items == 0 or nil | |
| return node, total_displayed_items + (node.filtered and 0 or 1) | |
| end | |
| function GedObjectTree(obj, filter, format, allow_objects_only, enable_rollover) | |
| if allow_objects_only and (not GedIsValidObject(obj) or IsKindOf(obj, "GedMultiSelectAdapter")) then | |
| return {} | |
| end | |
| if filter then | |
| filter:PrepareForFiltering() | |
| end | |
| local format = type(format) == "string" and T{format} or format | |
| local format_fn = function(obj) return GedTranslate(format, obj, not "check") end | |
| local children_fn = function(obj) return obj.GedTreeChildren and obj.GedTreeChildren(obj) or obj end | |
| local filter_fn = function(obj) return not filter or filter:FilterObject(obj) end | |
| local tree, total_displayed_items = GedExpandNode(obj, nil, format_fn, children_fn, filter_fn, enable_rollover) | |
| if filter then | |
| filter:DoneFiltering(total_displayed_items or 0) | |
| end | |
| return tree | |
| end | |
| function GedFormatObject(obj, filter, format) | |
| if type(obj) == "string" then | |
| return format == "" and obj or format | |
| end | |
| if not string.find(format, "<", 1, true) then | |
| return format | |
| end | |
| return GedIsValidObject(obj) and GedTranslate(T{format}, obj, not "check") or "" | |
| end | |
| function GedFormatObjectWithCount(obj, filter, format) | |
| local str = GedFormatObject(obj, filter, format) | |
| if type(obj) == "table" then | |
| str = string.format("%s (%s)", str, #obj) | |
| end | |
| return str | |
| end | |
| ----- Handling T values for Ged | |
| -- | |
| -- Ged is sent the actual T values, always in the { id, text } format (even in Gold Master). | |
| -- This allows Ged to preserve loc IDs of Ts as they are edited. | |
| local function GedToGameT(value) | |
| return type(value) == "table" and setmetatable(value, TMeta) or value | |
| end | |
| local function GameToGedT(value) | |
| if type(value) == "userdata" then | |
| return { TGetID(value), TDevModeGetEnglishText(value, not "deep", "no_assert") } | |
| elseif type(value) == "table" then | |
| -- strip T metatables and arguments (they can't pass through sockets and cause an assert); Ged can only show/edit plain Ts | |
| return table.raw_copy(TStripArgs(value)) | |
| else | |
| return value -- could be "" or maybe false | |
| end | |
| end | |
| ----- Handling function values for Ged | |
| -- | |
| -- The user can abandon editing a function that doesn't compile and can't be saved. In this case: | |
| -- * the last compilable source is in the FuncSource cache - this is what will be saved | |
| -- * the user's uncompilable version is stored aside and is sent to Ged when the prop value is requested | |
| if FirstLoad then | |
| UncompilableFuncPropsSources = {} -- object -> prop_id -> source code | |
| end | |
| function GedCompileCode(compile_func, source, prop_meta, obj) | |
| assert(type(source) == "string" or not source) | |
| if not source or source:match("^%s*$") then return nil end | |
| local prop_id = prop_meta.id | |
| local storage = UncompilableFuncPropsSources[obj] | |
| local fn, err = compile_func(prop_id, eval(prop_meta.params, obj, prop_meta) or "self", source) | |
| if err then | |
| storage = storage or {} | |
| storage[prop_id] = source | |
| UncompilableFuncPropsSources[obj] = storage | |
| return obj:GetProperty(prop_id) -- return previous valid function | |
| end | |
| if storage then | |
| storage[prop_id] = nil | |
| end | |
| return fn | |
| end | |
| local env | |
| function GedToGameValue(value, prop_meta, object) | |
| local prop_type = eval(prop_meta.editor, object, prop_meta) | |
| if prop_type == "func" or prop_type == "expression" then | |
| return GedCompileCode(prop_type == "func" and CompileFunc or CompileExpression, value, prop_meta, object) | |
| elseif prop_type == "text" and eval(prop_meta.translate, object, prop_meta) then | |
| return GedToGameT(value) | |
| elseif prop_type == "T_list" and type(value) == "table" then | |
| for idx, item in ipairs(value) do | |
| value[idx] = GedToGameT(item) | |
| end | |
| return setmetatable(value, TConcatMeta) | |
| elseif prop_type == "browse" then | |
| if value == "" or value == nil then | |
| return value | |
| end | |
| local existence_check_path = value | |
| local vbar_idx = string.find(existence_check_path, "|") | |
| if vbar_idx then | |
| existence_check_path = string.sub(existence_check_path, 1, vbar_idx - 1) | |
| end | |
| if io.exists(existence_check_path) then | |
| return value | |
| else | |
| return value, "File does not exist." | |
| end | |
| elseif prop_type == "prop_table" then | |
| env = env or LuaValueEnv{} | |
| local str = "return " .. (value or "") | |
| return dostring(str, env) or false | |
| elseif prop_type == "choice" or prop_type == "combo" or prop_type == "dropdownlist" then | |
| local items = eval(prop_meta.items, object, prop_meta) | |
| local translate = eval(prop_meta.translate, object, prop_meta) | |
| return ComboGetItemIdByName(value, items, object, prop_type == "combo", translate) | |
| elseif prop_type == "object" then | |
| return HandleToObject[value and value.handle] or false | |
| elseif prop_type == "packedcurve" then | |
| if not value then return value end | |
| return PackCurveParams(value[1], value[2], value[3], value[4], value.range_y) | |
| end | |
| return value | |
| end | |
| function GameToGedValue(value, prop_meta, object, items) | |
| if rawequal(value, Undefined()) then return value end | |
| local prop_type = eval(prop_meta.editor, object, prop_meta) | |
| if prop_type == "script" then | |
| return value and value:GetHumanReadableScript() or (prop_meta.class == "ScriptConditionList" and "empty condition list" or "empty script") | |
| elseif prop_type == "bool" then | |
| return value and true | |
| elseif prop_type == "func" or prop_type == "expression" then | |
| local storage = UncompilableFuncPropsSources[object] | |
| if storage and storage[prop_meta.id] then | |
| return storage[prop_meta.id] -- return last-entered code for uncompilable functions | |
| end | |
| if type(value) ~= "function" then return false end | |
| local name, params, body = GetFuncSource(value) | |
| if type(body) == "table" then | |
| body = table.concat(body, "\n") | |
| end | |
| if prop_type == "expression" and body then | |
| body = body:match("^%s*(.-)%s*$") | |
| if body:starts_with("return ") and not body:find("return ", 8, true) then | |
| body = body:match("^return%s*(.*)") | |
| end | |
| end | |
| return body or "" | |
| elseif prop_type == "browse" then | |
| value = value or "" | |
| value = const.XboxToPlayStationButtons[value] and GetPlatformSpecificImagePath(value) or value | |
| return value | |
| elseif prop_type == "prop_table" then | |
| local indent = eval(prop_meta.indent, object, prop_meta) or " " | |
| return type(value) == "table" and TableToLuaCode(value, indent) or not value and "" or Undefined() | |
| elseif prop_type == "choice" or prop_type == "combo" or prop_type == "dropdownlist" then | |
| items = items or eval(prop_meta.items, object, prop_meta) | |
| return ComboGetItemNameById(value, items, object, prop_type == "combo") | |
| elseif prop_type == "image" then | |
| if type(value) == "string" and value ~= "" then | |
| if prop_meta.os_path then | |
| return value | |
| end | |
| local os_path, err = ConvertToOSPath(value) | |
| if not err then | |
| return os_path | |
| end | |
| end | |
| return "" | |
| elseif prop_type == "text" and eval(prop_meta.translate, object, prop_meta) then | |
| return GameToGedT(value) | |
| elseif prop_type == "T_list" and type(value) == "table" then | |
| local temp = {} | |
| for _, item in ipairs(value) do | |
| temp[#temp + 1] = GameToGedT(item) | |
| end | |
| return temp | |
| elseif prop_type == "object" then | |
| if value and IsValid(value) and rawget(value, "handle") then | |
| local formatter = GetObjectPropEditorFormatFunc(prop_meta) | |
| return { handle = value.handle, text = formatter(value) } | |
| else | |
| return false | |
| end | |
| elseif prop_type == "objects" then | |
| return false -- not supported in Ged, but don't try to send it over the network | |
| elseif prop_type == "grid" then | |
| if IsGrid(value) then | |
| local w, h = value:size() | |
| local size, max_size = Max(w, h), eval(prop_meta.max, object, prop_meta) | |
| local resample | |
| if max_size and size > max_size then | |
| resample = true | |
| value = GridResample(value, w * max_size / size, h * max_size / size, false) | |
| end | |
| local str, err = GridWriteStr(value) | |
| if not err then | |
| return resample and { str, w, h } or str | |
| end | |
| end | |
| return "" | |
| elseif prop_type == "nested_obj" or prop_type == "property_array" then | |
| if prop_type == "property_array" and value then | |
| value = GedDynamicProps:Instance(object, value, prop_meta) | |
| end | |
| return value and table.hash(value) or false | |
| elseif prop_type == "nested_list" then | |
| local addresses = { table_addr = tostring(value) } | |
| for idx, item in ipairs(value) do | |
| addresses[idx] = tostring(item) | |
| end | |
| return addresses | |
| elseif prop_type == "packedcurve" then | |
| local pt1, pt2, pt3, pt4, range_y = UnpackCurveParams(value) | |
| return { pt1, pt2, pt3, pt4, range_y = range_y } | |
| elseif prop_type == "curve4" then | |
| if not value then return value end | |
| if type(value) == "number" then return false end | |
| return value | |
| end | |
| assert(value == "" or not IsT(value)) -- T values should pass through GedToGameT (maybe a property with translate == false has a T value?) | |
| return value | |
| end | |
| function GedItemsMenu(base_class, filter_fn, filter_param) | |
| local menu = {} | |
| local classes = ClassLeafDescendantsList(base_class, function(name, class) return not filter_fn or filter_fn(filter_param, class) end) | |
| if #classes == 0 and base_class and base_class ~= "" then | |
| classes = { base_class } | |
| end | |
| for _, class_name in ipairs(classes) do | |
| local class = g_Classes[class_name] | |
| local hasEditorName = class:HasMember("EditorName") | |
| if not hasEditorName or (class.EditorName or "") ~= "" then | |
| local name = hasEditorName and GedTranslate(class.EditorName, class, false) or class_name | |
| menu[#menu + 1] = { | |
| Class = class_name, | |
| EditorName = name, | |
| EditorIcon = rawget(class, "EditorIcon"), | |
| EditorShortcut = rawget(class, "EditorShortcut"), | |
| EditorSubmenu = class.EditorSubmenu or "New "..GedTranslate(g_Classes[base_class].EditorName or base_class, nil, false), | |
| ScriptDomain = rawget(class, "ScriptDomain"), | |
| Documentation = class.Documentation and string.format("<style GedTitleSmall><center>%s</style><left>\n%s", name, class.Documentation), | |
| } | |
| end | |
| end | |
| table.sortby_field(menu, "EditorName") | |
| return menu | |
| end | |
| function GedDynamicItemsMenu(obj, filter, class, path) | |
| local parent = (not class or IsKindOf(obj, class)) and obj | |
| for i, key in ipairs(path or empty_table) do | |
| obj = obj and rawget(TreeNodeChildren(obj), key) | |
| if IsKindOf(obj, class) then | |
| parent = obj | |
| end | |
| end | |
| return IsKindOf(parent, "Container") and parent:EditorItemsMenu() | |
| end | |
| function GedExecMemberFunc(obj, filter, member, ...) | |
| if obj and obj:HasMember(member) then | |
| return obj[member](obj, ...) | |
| end | |
| end | |
| function GedGetWarning(obj, filter) | |
| if not GedIsValidObject(obj) then return end | |
| if obj:HasMember("param_bindings") then | |
| local sockets = GedObjects[obj] | |
| local parent = #sockets >= 2 and sockets[2]:GetParentOfKind(obj, "Preset") or obj | |
| for property, param in pairs(obj.param_bindings or empty_table) do | |
| if not ResolveValue(parent, param) then | |
| return "Undefined parameter '"..param.."' for property '"..property.."'" | |
| end | |
| end | |
| end | |
| local diag_msg = GetDiagnosticMessage(obj) | |
| if diag_msg and type(diag_msg) == "table" and type(diag_msg[1]) == "table" then | |
| return diag_msg[1] | |
| end | |
| return diag_msg | |
| end | |
| function GedGetDocumentation(obj) | |
| if IsKindOfClasses(obj, "ScriptBlock", "FunctionObject") then | |
| local documentation = GetDocumentation(obj) | |
| if (documentation or "") == "" then return end | |
| local docs = { "<style GedTitleSmall><center>" .. obj.class .. "</style><left>"} | |
| docs[#docs+1] = "<style GedMultiLine>" .. documentation .. "</style>\n" | |
| for _, prop in ipairs(obj:GetProperties()) do | |
| local name = prop.name or prop.id | |
| if type(name) ~= "function" and (name ~= "Negate" or obj.HasNegate) then -- filters out ScriptSimpleStatement's Param1/2/3 properties | |
| if prop.help and prop.help ~= "" then | |
| docs[#docs + 1] = "<style GedPropertyName>" .. name .. ":</style> <style GedMultiLine>" .. prop.help .. "</style>" | |
| else | |
| docs[#docs + 1] = "<style GedPropertyName>" .. name .. "</style>" | |
| end | |
| end | |
| end | |
| return table.concat(docs, "\n") | |
| end | |
| return GetDocumentation(obj) | |
| end | |
| function GedGetDocumentationLink(obj) | |
| return GetDocumentationLink(obj) | |
| end | |
| -- hide/show functions for inline documentation at the place of the editor = "documentation" property (for Presets and Mod Items) | |
| function GedHideDocumentation(root, obj, prop_id, ged, btn_param, idx) | |
| local hidden = LocalStorage.DocumentationHidden or {} | |
| hidden[obj.class] = true | |
| LocalStorage.DocumentationHidden = hidden | |
| SaveLocalStorageDelayed() | |
| ObjModified(obj) | |
| end | |
| function GedShowDocumentation(root, obj, prop_id, ged, btn_param, idx) | |
| local hidden = LocalStorage.DocumentationHidden or {} | |
| hidden[obj.class] = nil | |
| LocalStorage.DocumentationHidden = hidden | |
| SaveLocalStorageDelayed() | |
| ObjModified(obj) | |
| end | |
| function IsDocumentationHidden(obj) | |
| return LocalStorage.DocumentationHidden and LocalStorage.DocumentationHidden[obj.class] | |
| end | |
| function GedTestFunctionObject(socket, obj_name) | |
| local obj = socket:ResolveObj(obj_name) | |
| local subject = obj:HasMember("RequiredObjClasses") and SelectedObj or nil | |
| obj:TestInGed(subject, socket) | |
| end | |
| function GedPickerItemDoubleClicked(socket, obj_name, prop_id, item_id) | |
| local obj = socket:ResolveObj(obj_name) | |
| GedNotify(obj, "OnPickerItemDoubleClicked", prop_id, item_id, socket) | |
| end | |
| function OnMsg.LuaFileChanged() GedSetUiStatus("lua_reload", "Reloading Lua...") end -- unable to make remote calls in OnMsg.ReloadLua | |
| function OnMsg.Autorun() GedSetUiStatus("lua_reload") end | |
| function OnMsg.ChangeMap() GedSetUiStatus("change_map", "Changing map...") end | |
| function OnMsg.ChangeMapDone() GedSetUiStatus("change_map") end | |
| function OnMsg.PreSaveMap() GedSetUiStatus("save_map", "Saving map...") end | |
| function OnMsg.SaveMapDone() GedSetUiStatus("save_map") end | |
| function OnMsg.DataReload() GedSetUiStatus("data_reload", "Reloading presets...") end | |
| function OnMsg.DataReloadDone() GedSetUiStatus("data_reload") end | |
| function OnMsg.ValidatingPresets() GedSetUiStatus("validating_presets", "Validating presets...") end | |
| function OnMsg.ValidatingPresetsDone() GedSetUiStatus("validating_presets") end | |
| function OnMsg.DebuggerBreak() GedSetUiStatus("pause", "Debugger Break") end | |
| function OnMsg.DebuggerContinue() GedSetUiStatus("pause") end | |
| function GedSetUiStatus(id, text, delay) | |
| for _, socket in pairs(GedConnections or empty_table) do | |
| socket:SetUiStatus(id, text, delay) | |
| end | |
| end | |
| function OnMsg.ApplicationQuit() | |
| for _, socket in pairs(GedConnections or empty_table) do | |
| socket:Send("rfnGedQuit") | |
| end | |
| end | |
| ----- GedDynamicProps | |
| -- A dummy class to generate the properties of the nested_obj that represents the value of a property_array property | |
| DefineClass.GedDynamicProps = { | |
| __parents = { "PropertyObject" }, | |
| prop_meta = false, | |
| parent_obj = false, | |
| } | |
| function GedDynamicProps:Instance(parent, value, prop_meta) | |
| local meta = { prop_meta = prop_meta, parent_obj = parent } | |
| meta.__index = meta | |
| setmetatable(meta, self) | |
| return setmetatable(value, meta) | |
| end | |
| function GedDynamicProps:__toluacode(indent, pstr, ...) | |
| -- remove default values from the table | |
| for _, prop_meta in ipairs(self:GetProperties()) do | |
| if rawget(self, prop_meta.id) == prop_meta.default then | |
| rawset(self, prop_meta.id, nil) | |
| end | |
| end | |
| return TableToLuaCode(self, indent, pstr) | |
| end | |
| function GedDynamicProps:GetProperties() | |
| local props = {} | |
| local meta = self.prop_meta | |
| if not meta then | |
| return props | |
| end | |
| local prop_meta = meta.prop_meta | |
| local idx = 1 | |
| local parent_obj = self.parent_obj | |
| local prop_meta_update = meta.prop_meta_update or empty_func | |
| if IsKindOf(g_Classes[meta.from], "Preset") then | |
| ForEachPreset(meta.from, function(preset) | |
| local prop = table.copy(prop_meta) | |
| prop.id = preset.id | |
| prop.index = idx | |
| prop.preset = preset | |
| if prop_meta_update then | |
| prop_meta_update(parent_obj, prop) | |
| end | |
| props[idx] = prop | |
| idx = idx + 1 | |
| end) | |
| return props | |
| end | |
| for k, v in sorted_pairs(eval(meta.items, self.parent_obj, meta)) do | |
| local prop = table.copy(prop_meta) | |
| prop.id = | |
| meta.from == "Table keys" and k or | |
| meta.from == "Table values" and v or | |
| meta.from == "Table field values" and type(v) == "table" and v[meta.field] | |
| if type(prop.id) == "string" or type(prop.id) == "number" then | |
| prop.index = idx | |
| prop.value = v | |
| if prop_meta_update then | |
| prop_meta_update(parent_obj, prop) | |
| end | |
| props[idx] = prop | |
| idx = idx + 1 | |
| end | |
| end | |
| return props | |
| end | |
| function GedDynamicProps:Clone(class, ...) | |
| class = class or self.class | |
| local obj = g_Classes[class]:new(...) | |
| setmetatable(obj, getmetatable(self)) | |
| obj:CopyProperties(self) | |
| return obj | |
| end | |
| ----- Support for cached incremental recursive search in property values | |
| if FirstLoad then | |
| ValueSearchCache = false | |
| ValueSearchCacheInProgress = false | |
| end | |
| local function populate_texts_cache_simple(obj, value, prop_meta) | |
| if type(value) == "table" and not IsT(value) then | |
| for k, v in pairs(value) do | |
| populate_texts_cache_simple(obj, k, prop_meta) | |
| populate_texts_cache_simple(obj, v, prop_meta) | |
| end | |
| return | |
| end | |
| local str, _ | |
| if type(value) == "string" and value ~= "" then | |
| str = value | |
| elseif type(value) == "number" then | |
| str = tostring(value) -- TODO: Properly format the number values as Ged would display them? | |
| elseif type(value) == "function" then | |
| -- don't search in functions/expressions that are defaults (slow and these matches are of no interest most of the time) | |
| if value ~= (prop_meta.default or obj:HasMember(prop_meta.id) and obj[prop_meta.id]) then | |
| _, _, str = GetFuncSource(value) | |
| str = type(str) == "table" and table.concat(str, "\n") or str | |
| end | |
| elseif IsT(value) then | |
| str = TDevModeGetEnglishText(value, "deep", "no_assert") | |
| end | |
| if str and str ~= "" then | |
| local cache = ValueSearchCache | |
| table.insert(cache.objs, obj) | |
| table.insert(cache.texts, string.lower(str)) | |
| table.insert(cache.props, prop_meta.id) | |
| end | |
| end | |
| local function populate_texts_cache(obj, parent) | |
| ValueSearchCache.obj_parent[obj] = parent | |
| for _, subobj in ipairs(obj) do | |
| if type(subobj) == "table" then | |
| populate_texts_cache(subobj, obj) | |
| end | |
| end | |
| if IsKindOf(obj, "PropertyObject") then | |
| for _, prop_meta in ipairs(obj:GetProperties()) do | |
| local id, editor = prop_meta.id, prop_meta.editor | |
| local value = obj:GetProperty(id) | |
| if editor == "nested_obj" and value then | |
| populate_texts_cache(value, obj) | |
| elseif editor == "nested_list" then | |
| for _, subobj in ipairs(value) do | |
| populate_texts_cache(subobj, obj) | |
| end | |
| else | |
| populate_texts_cache_simple(obj, value, prop_meta) | |
| end | |
| end | |
| end | |
| end | |
| local function search_in_cache(root, search_text, results) | |
| local cache = ValueSearchCache | |
| local old_text = cache.search_text | |
| local objs, texts, props = cache.objs, cache.texts, cache.props | |
| cache.search_text = search_text | |
| local match_idxs, i = { n = 0 }, 1 | |
| if not old_text or old_text == search_text or not search_text:starts_with(old_text) then | |
| for idx, text in ipairs(cache.texts) do | |
| if string.find(text, search_text, 1, true) then | |
| match_idxs[i] = idx | |
| match_idxs.n = i | |
| i = i + 1 | |
| end | |
| end | |
| else -- incremental search | |
| match_idxs = cache.matches | |
| local texts = cache.texts | |
| for i = 1, match_idxs.n do | |
| local idx = match_idxs[i] | |
| if idx and not string.find(texts[idx], search_text, 1, true) then | |
| match_idxs[i] = nil | |
| end | |
| end | |
| end | |
| cache.matches = match_idxs | |
| local hidden = {} | |
| for obj in pairs(cache.obj_parent) do | |
| hidden[tostring(obj)] = true | |
| end | |
| local objs, parents = cache.objs, cache.obj_parent | |
| for i = 1, match_idxs.n do | |
| local idx = match_idxs[i] | |
| if idx then | |
| local obj, prop = objs[idx], props[idx] | |
| local obj_id = tostring(obj) | |
| local result = results[obj_id] or {} | |
| if type(result) == "string" then -- there is a match both in an object's property, and its children | |
| result = { __match = result } | |
| end | |
| result[#result + 1] = prop | |
| local parent, obj = parents[obj], obj | |
| local match_path = { tostring(obj) } | |
| while parent do | |
| local parent_id = tostring(parent) | |
| results[parent_id] = results[parent_id] or tostring(obj) | |
| hidden[parent_id] = nil | |
| match_path[#match_path + 1] = parent_id | |
| obj = parent | |
| parent = parents[parent] | |
| end | |
| hidden[obj_id] = nil | |
| results[obj_id] = result | |
| results[#results + 1] = { prop = prop, path = table.reverse(match_path) } | |
| end | |
| end | |
| results.hidden = hidden | |
| return results | |
| end | |
| local function repopulate_cache(obj) | |
| PauseInfiniteLoopDetection("rfnPopulateSearchValuesCache") | |
| ValueSearchCache = { | |
| search_text = false, | |
| obj_parent = {}, -- obj -> parent | |
| matches = false, -- indexes in the tables below | |
| -- the following tables have one entry for each property text that was found recursively in 'root' | |
| objs = {}, | |
| texts = {}, | |
| props = {}, -- prop name where the text was found | |
| } | |
| populate_texts_cache(obj) | |
| ResumeInfiniteLoopDetection("rfnPopulateSearchValuesCache") | |
| end | |
| function GedGameSocket:rfnPopulateSearchValuesCache(obj_context) | |
| local root = self:ResolveObj(obj_context) | |
| if root then | |
| if ValueSearchCacheInProgress then return end | |
| ValueSearchCacheInProgress = true | |
| CreateRealTimeThread(function(obj) | |
| local success, err = sprocall(repopulate_cache, obj) | |
| if not success then | |
| assert(false, err) | |
| end | |
| ValueSearchCacheInProgress = false | |
| Msg("ValueSearchCacheUpdated") | |
| end, root) | |
| end | |
| end | |
| function GedGameSocket:rfnSearchValues(obj_context, text) | |
| local root = self:ResolveObj(obj_context) | |
| if root and text and text ~= "" then | |
| local results = {} | |
| PauseInfiniteLoopDetection("rfnSearchValues") | |
| if ValueSearchCacheInProgress then | |
| WaitMsg("ValueSearchCacheUpdated") | |
| elseif text == ValueSearchCache.search_text then | |
| repopulate_cache(root) -- refresh results button pressed | |
| end | |
| search_in_cache(root, text, results) | |
| ResumeInfiniteLoopDetection("rfnSearchValues") | |
| return results | |
| end | |
| end | |
| ----- Bookmarks | |
| if FirstLoad then | |
| g_Bookmarks = {} | |
| end | |
| local function GedSortBookmarks(bookmarks) | |
| table.sort(bookmarks, function(a, b) | |
| local id1 = IsKindOf(a, "Preset") and a.id or a[1].group | |
| local id2 = IsKindOf(b, "Preset") and b.id or b[1].group | |
| return id1 < id2 | |
| end) | |
| end | |
| -- Rebuild bookmarks from local storage | |
| function RebuildBookmarks() | |
| if LocalStorage.editor.bookmarks then | |
| local bookmarks = {} | |
| local loc_storage_bookmarks = {} | |
| -- Remove previous bookmarks of deleted template classes | |
| LocalStorage.editor.bookmarks["UnitAnimalTemplate"] = nil | |
| LocalStorage.editor.bookmarks["InventoryItemTemplate"] = nil | |
| for class, preset_arr in pairs(LocalStorage.editor.bookmarks) do | |
| if not bookmarks[class] then | |
| bookmarks[class] = {} | |
| loc_storage_bookmarks[class] = {} | |
| end | |
| for idx, preset_path in ipairs(preset_arr) do | |
| -- Find preset or group by class and path = { group, id } | |
| local bookmark = PresetOrGroupByUniquePath(class, preset_path) | |
| if bookmark then | |
| table.insert(bookmarks[class], bookmark) | |
| table.insert(loc_storage_bookmarks[class], preset_path) | |
| end | |
| end | |
| GedSortBookmarks(bookmarks[class]) | |
| -- Rebind new bookmarks object and update UI | |
| GedRebindRoot(g_Bookmarks[class], bookmarks[class], "bookmarks") | |
| end | |
| g_Bookmarks = bookmarks | |
| LocalStorage.editor.bookmarks = loc_storage_bookmarks | |
| SaveLocalStorageDelayed() | |
| else | |
| LocalStorage.editor.bookmarks = {} | |
| end | |
| end | |
| OnMsg.DataLoaded = RebuildBookmarks -- After Presets have been loaded initially | |
| OnMsg.DataReloadDone = RebuildBookmarks -- After Presets have been reloaded | |
| function OnMsg.GedPropertyEdited(ged_id, object, prop_id, old_value) | |
| if not IsKindOf(object, "Preset") then return end | |
| if not object.class or not LocalStorage.editor.bookmarks or not LocalStorage.editor.bookmarks[object.class] then return end | |
| if not table.find(g_Bookmarks[object.class], object) then return end | |
| local old_value_idx | |
| if prop_id == "Group" then | |
| old_value_idx = 1 | |
| elseif prop_id == "Id" then | |
| old_value_idx = 2 | |
| else | |
| return | |
| end | |
| -- Recreate the old path | |
| local old_path = GetPresetOrGroupUniquePath(object) | |
| old_path[old_value_idx] = old_value | |
| local change_idx | |
| for idx, path in ipairs(LocalStorage.editor.bookmarks[object.class]) do | |
| if path[1] == old_path[1] and path[2] == old_path[2] then | |
| change_idx = idx | |
| break | |
| end | |
| end | |
| if change_idx then | |
| -- Replace with the new path | |
| LocalStorage.editor.bookmarks[object.class][change_idx] = GetPresetOrGroupUniquePath(object) | |
| ObjModified(g_Bookmarks[object.class]) | |
| SaveLocalStorageDelayed() | |
| end | |
| end | |
| function GedGameSocket:rfnBindBookmarks(name, class) | |
| if not g_Bookmarks[class] then | |
| g_Bookmarks[class] = {} | |
| LocalStorage.editor.bookmarks[class] = {} | |
| end | |
| self:BindObj(name, g_Bookmarks[class]) | |
| table.insert_unique(self.root_names, name) | |
| end | |
| -- can bookmark a preset or a preset group | |
| function GedToggleBookmark(socket, bind_name, class) | |
| local bookmark = socket:ResolveObj(bind_name) | |
| local preset_root = socket:ResolveObj("root") | |
| if not bookmark or not IsKindOf(bookmark, "Preset") and not table.find(preset_root, bookmark) then | |
| return | |
| end | |
| if not GedAddBookmark(bookmark, class) then | |
| GedRemoveBookmark(bookmark, class) | |
| end | |
| end | |
| function GedAddBookmark(obj, class) | |
| local bookmarks = g_Bookmarks[class] | |
| if not table.find(bookmarks, obj) then | |
| if not bookmarks then | |
| bookmarks = {} | |
| g_Bookmarks[class] = bookmarks | |
| LocalStorage.editor.bookmarks[class] = {} | |
| end | |
| local bookmarks_size = #bookmarks | |
| bookmarks[bookmarks_size + 1] = obj | |
| GedSortBookmarks(bookmarks) | |
| ObjModified(bookmarks) | |
| LocalStorage.editor.bookmarks[class][bookmarks_size + 1] = GetPresetOrGroupUniquePath(obj) | |
| SaveLocalStorageDelayed() | |
| return true | |
| end | |
| return false | |
| end | |
| function GedRemoveBookmark(obj, class) | |
| local index = table.find(g_Bookmarks[class], obj) | |
| if index then | |
| local removed_path = GetPresetOrGroupUniquePath(g_Bookmarks[class][index]) | |
| local stored_bookmarks = LocalStorage.editor.bookmarks[class] | |
| local idx = table.findfirst(stored_bookmarks, function(idx, path, removed_path) | |
| return path[1] == removed_path[1] and path[2] == removed_path[2] | |
| end, removed_path) | |
| if idx then | |
| table.remove(stored_bookmarks, idx) | |
| end | |
| table.remove(g_Bookmarks[class], index) | |
| ObjModified(g_Bookmarks[class]) | |
| SaveLocalStorageDelayed() | |
| end | |
| end | |
| function GedBookmarksTree(obj, filter, format) | |
| local format = type(format) == "string" and T{format} or format | |
| local format_fn = function(obj) | |
| return IsKindOf(obj, "Preset") and GedTranslate(format, obj, not "check") or obj and obj[1] and obj[1].group or "[Invalid bookmark]" | |
| end | |
| local children_fn = function(obj) | |
| return not IsKindOf(obj, "Preset") and obj | |
| end | |
| return next(obj) and GedExpandNode(obj, nil, format_fn, children_fn) or "empty tree" | |
| end | |
| function GedPresetWarningsErrors(obj) | |
| -- find the preset class by the object that's selected (it could be a preset, a preset group, or GedMultiSelectAdapter) | |
| local preset_class | |
| if IsKindOf(obj, "Preset") then | |
| preset_class = obj.class | |
| elseif IsKindOf(obj, "GedMultiSelectAdapter") then | |
| preset_class = obj.__objects[1].class | |
| else -- preset group | |
| preset_class = obj[1] and obj[1].class | |
| end | |
| local warnings, errors = 0, 0 | |
| if preset_class then | |
| ForEachPreset(preset_class, function(preset) | |
| local msg = DiagnosticMessageCache[preset] | |
| if msg then | |
| if msg[#msg] == "warning" then | |
| warnings = warnings + 1 | |
| else | |
| errors = errors + 1 | |
| end | |
| end | |
| end) | |
| end | |
| return warnings + errors, warnings, errors | |
| end | |
| function GedModWarningsErrors(obj) | |
| local parent = obj | |
| if not IsKindOf(parent, "ModItem") and not IsKindOf(parent, "ModDef") then parent = GetParentTableOfKind(parent, "ModItem") end | |
| if IsKindOf(parent, "ModItem") then parent = parent.mod end | |
| local warnings, errors = 0, 0 | |
| local msg = DiagnosticMessageCache[parent] | |
| if msg then | |
| if msg[#msg] == "warning" then | |
| warnings = warnings + 1 | |
| else | |
| errors = errors + 1 | |
| end | |
| end | |
| assert(IsKindOf(parent, "ModDef")) | |
| parent:ForEachModItem(function(item) | |
| local msg = DiagnosticMessageCache[item] | |
| if msg then | |
| if msg[#msg] == "warning" then | |
| warnings = warnings + 1 | |
| else | |
| errors = errors + 1 | |
| end | |
| end | |
| end) | |
| return warnings + errors, warnings, errors | |
| end | |
| function GedGenericStatusText(obj, filter, format, warningErrorsFunction) | |
| local status = obj.class and obj:GetProperty("PresetStatusText") | |
| status = status and status ~= "" and string.format("<style GedHighlight>%s</style>", status) or "" | |
| local total, warnings, errors = warningErrorsFunction(obj) | |
| if total == 0 then return status end | |
| local texts = {} | |
| texts[#texts + 1] = errors > 0 and string.format("<color 255 0 0>%d error%s</color>" , errors , errors == 1 and "" or "s") or nil | |
| texts[#texts + 1] = warnings > 0 and string.format("<color 255 140 0>%d warning%s</color>", warnings, warnings == 1 and "" or "s") or nil | |
| return table.concat(texts, ", ") .. "\n" .. status | |
| end | |
| function GedPresetStatusText(obj, filter, format) | |
| if IsKindOf(obj, "GedMultiSelectAdapter") then | |
| obj = obj.__objects[1] | |
| end | |
| return GedGenericStatusText(obj, filter, format, GedPresetWarningsErrors) | |
| end | |
| function GedModStatusText(obj, filter, format) | |
| return GedGenericStatusText(obj, filter, format, GedModWarningsErrors) | |
| end | |
| -- Root panel and bookmarks panel set the selection in each other | |
| function OnMsg.GedOnEditorSelect(obj, selected, socket, panel_context) | |
| if not selected then return end | |
| if panel_context == "root" then | |
| local bookmark_idx = table.find(g_Bookmarks[obj.PresetClass or obj.class], obj) | |
| if bookmark_idx then | |
| socket:SetSelection("bookmarks", { bookmark_idx }, nil, not "notify") | |
| end | |
| elseif panel_context == "bookmarks" then | |
| local path | |
| if IsKindOf(obj, "Preset") then | |
| socket:SetSelection("root", PresetGetPath(obj), nil, not "notify") | |
| else -- preset group | |
| local presets = socket:ResolveObj("root") | |
| local idx = table.find(presets, obj) | |
| if idx then | |
| socket:SetSelection("root", { idx }, nil, not "notify") | |
| end | |
| end | |
| end | |
| end | |
| ----- Ask everywhere function (pops a message in-game and in all Ged windows) | |
| local function wait_any(functions) | |
| local thread = CurrentThread() | |
| local result = false | |
| for key, value in ipairs(functions) do | |
| CreateRealTimeThread(function() | |
| local worker_result = table.pack(value()) | |
| if not result then | |
| result = worker_result | |
| Wakeup(thread) | |
| end | |
| end) | |
| end | |
| return WaitWakeup() and table.unpack(result) | |
| end | |
| function GedAskEverywhere(title, question) | |
| local game_question = StdMessageDialog:new({}, terminal.desktop, { | |
| question = true, title = title, text = question, | |
| ok_text = "Yes", cancel_text = "No", | |
| }) | |
| game_question:Open() | |
| local questions = { function() return game_question:Wait() end } | |
| for id, ged in pairs(GedConnections) do | |
| if not ged.in_game then | |
| table.insert(questions, function() return ged:WaitQuestion(title, question, "Yes", "No") end) | |
| end | |
| end | |
| local result = wait_any(questions) | |
| -- close all dialogs | |
| if game_question.window_state ~= "destroying" then | |
| game_question:Close(false) | |
| end | |
| for id, ged in pairs(GedConnections) do | |
| if not ged.in_game then | |
| ged:DeleteQuestion() | |
| end | |
| end | |
| return result | |
| end | |