----- 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, "") 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 = "\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 = { ""} docs[#docs+1] = "\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] = " " else docs[#docs + 1] = "" 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("", 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("%d error%s" , errors , errors == 1 and "" or "s") or nil texts[#texts + 1] = warnings > 0 and string.format("%d warning%s", 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