local function IsGedAppOpened(template_id) if not rawget(_G, "GedConnections") then return false end for key, conn in pairs(GedConnections) do if conn.app_template == template_id then return true end end return false end function IsModEditorOpened() return IsGedAppOpened("ModEditor") end function IsModManagerOpened() return IsGedAppOpened("ModManager") end ModEditorMapName = "ModEditor" function IsModEditorMap(map_name) map_name = map_name or GetMapName() return map_name == ModEditorMapName or (table.get(MapData, map_name, "ModEditor") or false) end function OnMsg.UnableToUnlockAchievementReasons(reasons, achievement) if AreModdingToolsActive() then reasons["modding tools active"] = true end end if not config.Mods then return end if FirstLoad then ModUploadThread = false LastEditedMod = false -- the last mod that was opened or edited in a Mod Editor Ged application end function OpenModEditor(mod) local editor = GedConnections[mod.mod_ged_id] if editor then local activated = editor:Call("rfnApp", "Activate") if activated ~= "disconnected" then return editor end end LoadLuaParticleSystemPresets() for _, presets in pairs(Presets) do if class ~= "ListItem" then PopulateParentTableCache(presets) end end local mod_path = ModConvertSlashes(mod:GetModRootPath()) local context = { mod_items = GedItemsMenu("ModItem"), dlcs = g_AvailableDlc or { }, mod_path = mod_path, mod_os_path = ConvertToOSPath(mod_path), mod_content_path = mod:GetModContentPath(), WarningsUpdateRoot = "root", suppress_property_buttons = { "GedOpPresetIdNewInstance", "GedRpcEditPreset", "OpenTagsEditor", }, } Msg("GatherModEditorLogins", context) local container = Container:new{ mod } UpdateParentTable(mod, container) editor = OpenGedApp("ModEditor", container, context) if editor then editor:Send("rfnApp", "SetSelection", "root", { 1 }) editor:Send("rfnApp", "SetTitle", string.format("Mod Editor - %s", mod.title)) mod.mod_ged_id = editor.ged_id end return editor end function OnMsg.GedOpened(ged_id) local conn = GedConnections[ged_id] if conn and conn.app_template == "ModEditor" then SetUnmuteSoundReason("ModEditor") -- disable mute when unfocused in order to be able to test sound mod items end if conn and (conn.app_template == "ModEditor" or conn.app_template == "ModManager") then ReloadShortcuts() end end function OnMsg.GedClosing(ged_id) local conn = GedConnections[ged_id] if conn and conn.app_template == "ModEditor" then ClearUnmuteSoundReason("ModEditor") -- disable mute when unfocused in order to be able to test sound mod items end end function OnMsg.GedClosed(ged) if ged and (ged.app_template == "ModEditor" or ged.app_template == "ModManager") then DelayedCall(0, ReloadShortcuts) end end function WaitModEditorOpen(mod) if not IsModEditorMap(CurrentMap) then ChangeMap(ModEditorMapName) CloseMenuDialogs() end if mod then OpenModEditor(mod) else if IsModManagerOpened() then return end local context = { dlcs = g_AvailableDlc or { }, } SortModsList() local ged = OpenGedApp("ModManager", ModsList, context) if ged then ged:BindObj("log", ModMessageLog) end if LocalStorage.OpenModdingDocs == nil or LocalStorage.OpenModdingDocs then if not Platform.developer then GedOpHelpMod() end end end end function ModEditorOpen(mod) CreateRealTimeThread(WaitModEditorOpen) end function GedModMessageLog(obj) return table.concat(obj, "\n") end function OnMsg.NewMapLoaded() if config.Mods then ReloadShortcuts() end end function OnMsg.ModsReloaded() if IsModManagerOpened() then SortModsList() end end function UpdateModEditorsPropPanels() for id, ged in pairs(GedConnections) do if ged.app_template == "ModEditor" then local selected_obj = ged:ResolveObj("SelectedObject") if selected_obj then ObjModified(selected_obj) end end end end ----- Ged Ops (Mods) function GedOpNewMod(socket, obj) local title = socket:WaitUserInput(T(200174645592, "Enter Mod Title"), "") if not title then return end title = title:trim_spaces() if #title == 0 then socket:ShowMessage(T(634182240966, "Error"), T(112659155240, "No name provided")) return end local err, mod = CreateMod(title) if err then socket:ShowMessage(GetErrorTitle(err, "mods", mod), GetErrorText(err, "mods")) return end return table.find(ModsList, mod) end function GedOpLoadMod(socket, obj, item_idx) local mod = ModsList[item_idx] if mod.items then return end table.insert_unique(AccountStorage.LoadMods, mod.id) Msg("OnGedLoadMod", mod.id) ModsReloadItems() ObjModified(ModsList) end function GedOpUnloadMod(socket, obj, item_idx) local mod = ModsList[item_idx] if not mod.items then return end table.remove_value(AccountStorage.LoadMods, mod.id) Msg("OnGedUnloadMod", mod.id) -- close Mod editor for that mod (mod-editing assumes that the mod is loaded) for id, ged in pairs(GedConnections) do if ged.app_template == "ModEditor" then local root = ged:ResolveObj("root") if root and root[1] == mod then ged:Close() end end end ModsReloadItems() ObjModified(ModsList) end function GedOpEditMod(socket, obj, item_idx) if not IsRealTimeThread() then return CreateRealTimeThread(GedOpEditMod, socket, obj, item_idx) end local mod = ModsList[item_idx] if not mod or IsValidThread(mod.mod_opening) then return end if not CanLoadUnpackedMods() then ModLog(true, T{970080750583, "Error opening for editing: cannot open unpacked mods", mod}) return end mod.mod_opening = CurrentThread() local force_reload -- copy if not in AppData or svnAssets if (mod.source ~= "appdata" and mod.source ~= "additional") or mod.packed then local mod_folder = mod.title:gsub('[/?<>\\:*|"]', "_") local unpack_path = string.format("AppData/Mods/%s/", mod_folder) unpack_path = string.gsub(ConvertToOSPath(unpack_path), "\\", "/") local base_unpack_path, i = string.sub(unpack_path, 1, -2), 0 while io.exists(unpack_path) do i = i + 1 unpack_path = base_unpack_path .. " " .. tostring(i) .. "/" end local res = socket:WaitQuestion(T(521819598348, "Confirm Copy"), T{814173350691, "Mod '' files will be copied to ", mod, path = unpack_path}) if res ~= "ok" then return end GedSetUiStatus("mod_unpack", "Copying...") ModLog(T{348544010518, "Copying to ", mod, path = unpack_path}) AsyncCreatePath(unpack_path) local err if mod.packed then local pack_path = mod.path .. ModsPackFileName err = AsyncUnpack(pack_path, unpack_path) else local folders err, folders = AsyncListFiles(mod.content_path, "*", "recursive,relative,folders") if not err then --create folder structure for _, folder in ipairs(folders) do local err = AsyncCreatePath(unpack_path .. folder) if err then ModLog(true, T{311163830130, "Error creating folder : ", folder = folder, err = err}) break end end --copy all files local files err, files = AsyncListFiles(mod.content_path, "*", "recursive,relative") if not err then for _,file in ipairs(files) do local err = AsyncCopyFile(mod.content_path .. file, unpack_path .. file, "raw") if err then ModLog(true, T{403285832388, "Error copying : ", file = file, err = err}) end end else ModLog(true, T{600384081290, "Error looking up files of : ", mod, err = err}) end else ModLog(true, T{836115199867, "Error looking up folders of : ", mod, err = err}) end end GedSetUiStatus("mod_unpack") if not err then mod:UnmountContent() mod:ChangePaths(unpack_path) mod.packed = false mod.source = "appdata" mod:MountContent() force_reload = true mod:SaveDef("serialize_only") else ModLog(true, T{578088043400, "Error copying : ", mod, err = err}) end end if force_reload or not mod:ItemsLoaded() then table.insert_unique(AccountStorage.LoadMods, mod.id) Msg("OnGedLoadMod", mod.id) mod.force_reload = true ModsReloadItems(nil, "force_reload") ObjModified(ModsList) end if mod:ItemsLoaded() then WaitModEditorOpen(mod) end mod.mod_opening = false end function GedOpRemoveMod(socket, obj, item_idx) local mod = ModsList[item_idx] local reasons = { } Msg("GatherModDeleteFailReasons", mod, reasons) if next(reasons) then socket:ShowMessage(T(634182240966, "Error"), table.concat(reasons, "\n")) else local res = socket:WaitQuestion(T(118482924523, "Are you sure?"), T{820846615088, "Do you want to delete all files?", mod}) if res == "cancel" then return end table.remove(ModsList, item_idx) local err = DeleteMod(mod) if err then socket:ShowMessage(GetErrorTitle(err, "mods"), GetErrorText(err, "mods", mod)) end return Clamp(item_idx, 1, #ModsList) end end function GedOpHelpMod(socket, obj, document) local help_file = string.format("%s", ConvertToOSPath(DocsRoot .. (document or "index.md.html"))) help_file = string.gsub(help_file, "[\n\r]", "") if io.exists(help_file) then help_file = string.gsub(help_file, " ", "%%20") OpenUrl("file:///" .. help_file, "force external browser") end end function GedOpDarkModeChange(socket, obj, choice) SetProperty(XEditorSettings, "DarkMode", choice) for id, dlg in pairs(Dialogs) do if IsKindOf(dlg, "XDarkModeAwareDialog") then dlg:SetDarkMode(GetDarkModeSetting()) end end for id, socket in pairs(GedConnections) do socket:Send("rfnApp", "SetDarkMode", GetDarkModeSetting()) end ReloadShortcuts() end function GedOpOpenDocsToggle(socket, obj, choice) if LocalStorage.OpenModdingDocs ~= nil then LocalStorage.OpenModdingDocs = not LocalStorage.OpenModdingDocs else LocalStorage.OpenModdingDocs = false end SaveLocalStorage() socket:Send("rfnApp", "SetActionToggled", "OpenModdingDocs", LocalStorage.OpenModdingDocs) end function OnMsg.GedActivated(ged, initial) if initial and ged.app_template == "ModManager" then ged:Send("rfnApp", "SetActionToggled", "OpenModdingDocs", LocalStorage.OpenModdingDocs == nil or LocalStorage.OpenModdingDocs) end end function GedOpTriggerCheat(socket, obj, cheat, ...) if string.starts_with(cheat, "Cheat") then local func = rawget(_G, cheat) if func then func(...) end end end function CreateMod(title) for _, mod in ipairs(ModsList) do if mod.title == title then return "exists" end end local path = string.format("AppData/Mods/%s/", title:gsub('[/?<>\\:*|"]', "_")) if io.exists(path .. "metadata.lua") then return "exists" end AsyncCreatePath(path) local authors = {} Msg("GatherModAuthorNames", authors) local author --choose from modding platform (except steam) for platform, name in pairs(authors) do if platform ~= "steam" then author = name break end end --fallback to steam name or default author = author or authors.steam or "unknown" local env = LuaModEnv() local id = ModDef:GenerateId() local mod = ModDef:new{ title = title, author = author, id = id, path = path, content_path = ModContentPath .. id .. "/", env = env, } Msg("ModDefCreated", mod) mod:SetupEnv() mod:MountContent() assert(Mods[mod.id] == nil) Mods[mod.id] = mod ModsList[#ModsList+1] = mod SortModsList() CacheModDependencyGraph() local items_err = AsyncStringToFile(path .. "items.lua", "return {}") local def_err = mod:SaveDef() return (def_err or items_err), mod end function DeleteMod(mod) local err = AsyncDeletePath(mod.path) if err then return err end Mods[mod.id] = nil table.remove_entry(ModsList, mod) table.remove_entry(ModsLoaded, mod) table.remove_entry(AccountStorage.LoadMods, mod.id) Msg("OnGedUnloadMod", mod.id) ObjModified(ModsList) mod:delete() end ----- Ged Ops (Mod Items) function GedOpNewModItem(socket, root, path, class_or_instance) if #path == 0 then path = { 1 } end if #path == 1 then table.insert(path, #root[1].items) end return GedOpTreeNewItem(socket, root, path, class_or_instance) end local function GetSelectionBaseClass(root, selection) return ParentNodeByPath(root, selection[1]).ContainerClass end function GedOpDuplicateModItem(socket, root, selection) local path = selection[1] if not path or #path < 2 then return "error" end assert(path[1] == 1) return GedOpTreeDuplicate(socket, root, selection, GetSelectionBaseClass(root, selection)) end function GedOpCutModItem(socket, root, selection) local path = selection[1] if not path or #path < 2 then return "error" end assert(path[1] == 1) return GedOpTreeCut(socket, root, selection, GetSelectionBaseClass(root, selection)) end function GedOpCopyModItem(socket, root, selection) local path = selection[1] if not path or #path < 2 then return "error" end assert(path[1] == 1) return GedOpTreeCopy(socket, root, selection, GetSelectionBaseClass(root, selection)) end function GedOpPasteModItem(socket, root, selection) -- simulate select ModDef/root if not selection[1] then selection[1] = { 1 } selection[2] = { 1 } selection.n = 2 end -- simulate select last element of ModDef/root if #selection[1] == 1 then table.insert(selection[1], #root[1].items) selection[2][1] = #root[1].items end return GedOpTreePaste(socket, root, selection) end function GedOpDeleteModItem(socket, root, selection) local path = selection[1] if not path or #path < 2 then return "error" end assert(path[1] == 1) local items_name_string = "" for idx = 1, #selection[2] do local leaf = selection[2][idx] local item = TreeNodeChildren(ParentNodeByPath(root, path))[leaf] local item_name = item.id or item.name or item.__class or item.EditorName or item.class items_name_string = idx == 1 and item_name or items_name_string .. "\n" .. item_name end local confirm_text = T{435161105463, "Please confirm the deletion of item ''!", name = items_name_string} if #selection[2] ~= 1 then confirm_text = T{621296865915, "Are you sure you want to delete the following selected items?\n", number_of_items = #selection[2], items = items_name_string} end if "ok" ~= socket:WaitQuestion(T(986829419084, "Confirmation"), confirm_text) then return end return GedOpTreeDeleteItem(socket, root, selection) end function GedSaveMod(ged) local old_root = ged:ResolveObj("root") local mod = old_root[1] if mod:CanSaveMod(ged) then mod:SaveWholeMod() end end -- reloads the mod to update function debug info, allowing the modder to debug their code after saving -- (TODO: unused for now, consider adding a button for that when the debugging support is ready) function GedReloadModItems(ged) local old_root = ged:ResolveObj("root") local mod = old_root[1] GedSetUiStatus("mod_reload_items", "Reloading items...") mod:UnloadItems() mod:LoadItems() local container = Container:new{ mod } UpdateParentTable(mod, container) GedRebindRoot(old_root, container) GedSetUiStatus("mod_reload_items") end function GedOpOpenModItemPresetEditor(socket, obj, selection, a, b, c) if obj and obj.ModdedPresetClass then OpenPresetEditor(obj.ModdedPresetClass) end end function GedGetModItemDockedActions(obj) local actions = {} Msg("GatherModItemDockedActions", obj, actions) -- use this msg to add more actions for mod item that are docked on the bottom right return actions end function OnMsg.GatherModItemDockedActions(obj, actions) if IsKindOf(obj, "Preset") then local preset_class = g_Classes[obj.ModdedPresetClass] local class = preset_class.PresetClass or preset_class.class actions["PresetEditor"] = { name = "Open in " .. (preset_class.EditorMenubarName ~= "" and preset_class.EditorMenubarName or (class .. " editor")), rolloverText = "Open the dedicated editor for this item,\nalongside the rest of the game content.", op = "GedOpOpenModItemPresetEditor" } end end function OnMsg.GatherModItemDockedActions(obj, actions) if IsKindOf(obj, "ModItem") and obj.TestModItem ~= ModItem.TestModItem then actions["TestModItem"] = { name = "Test mod item", rolloverText = obj.TestDescription, op = "GedOpTestModItem" } end end function GedGetEditableModsComboItems() if not ModsLoaded then return empty_table end local ret = {} for idx, mod in ipairs(ModsLoaded) do if mod and mod:ItemsLoaded() and not mod:IsPacked() then table.insert(ret, { text = mod.title or mod.id, value = mod.id }) end end return ret end -- Clones the selected Preset to the selected mod as a ModItemPreset so it can be modded function GedOpClonePresetInMod(socket, root, selection_path, item_class, mod_id) local mod = Mods and Mods[mod_id] if not mod or not mod.items then return "Invalid mod selected" end local selected_preset = socket:ResolveObj("SelectedPreset") local path = selection_path and selection_path[1] -- Check if the preset class has a corersponding mod item class local class_or_instance = "ModItem" .. item_class local mod_item_class = g_Classes[class_or_instance] if not g_Classes[item_class] or not mod_item_class then return "No ModItemPreset class exists for this Preset type" end -- Create the new ModItemPreset and add it to the tree of the calling Preset Editor local item_path, item_undo_fn = GedOpTreeNewItem(socket, root, path, class_or_instance, nil, mod_id) if type(item_path) ~= "table" or type(item_undo_fn) ~= "function" then return "Error creating the new mod item" end -- Copy all properties from the chosen preset using the __copy mod item property (see ModItemPreset:OnEditorSetProperty) local item = GetNodeByPath(root, item_path) item["__copy_group"] = selected_preset.group local prop_id = "__copy" local id_value = selected_preset.id GedSetProperty(socket, item, prop_id, id_value) -- Set the same group and id (unique one) like the selected preset and get the new path in the tree item:SetGroup(selected_preset.group) item:SetId(item:GenerateUniquePresetId(selected_preset.id)) item_path = RecursiveFindTreeItemPath(root, item) return item_path, item_undo_fn end function GedOpSetModdingBindings(socket) -- Bind the editable mods combo in Preset Editors, it should contain only loaded mods -- Note: Since all bindings require an "obj" whose reference can later be used to update the binding (with GedRebindRoot) -- and there's no suitable "obj" to pass here we use empty_table as a kind of dummy constant reference that we can use for updates later socket:BindObj("EditableModsCombo", empty_table, GedGetEditableModsComboItems) -- Don't bind LastEditedMod if that mod is currently not loaded or packed if LastEditedMod and Mods then local mod = Mods[LastEditedMod.id] if mod and mod:ItemsLoaded() and not mod:IsPacked() then socket:BindObj("LastEditedMod", mod.id, return_first) end end end function OnMsg.OnGedLoadMod(mod_id) GedRebindRoot(empty_table, empty_table, "EditableModsCombo", GedGetEditableModsComboItems, "dont_restore_app_state") end function OnMsg.OnGedUnloadMod(mod_id) GedRebindRoot(empty_table, empty_table, "EditableModsCombo", GedGetEditableModsComboItems, "dont_restore_app_state") end -- Utility function for updating the Mod Editor tree panel for a given mod that changed function ObjModifiedMod(mod) if not mod then return end local mod_container = ParentTableCache[mod] -- Check if the given ModDef instance is not the original one if not mod_container and mod.id and Mods and Mods[mod.id] then mod_container = ParentTableCache[Mods[mod.id]] end if mod_container then ObjModified(mod_container) end end local function CreatePackageForUpload(mod_def, params) local content_path = mod_def.content_path local temp_path = "TmpData/ModUpload/" local pack_path = temp_path .. "Pack/" local shots_path = temp_path .. "Screenshots/" --clean old files in ModUpload & recreate folder structure AsyncDeletePath(temp_path) AsyncCreatePath(pack_path) AsyncCreatePath(shots_path) --copy & rename mod screenshots params.screenshots = { } for i=1,5 do --copy & rename mod_def.screenshot1, mod_def.screenshot2, mod_def.screenshot3, mod_def.screenshot4, mod_def.screenshot5 local screenshot = mod_def["screenshot"..i] if io.exists(screenshot) then local path, name, ext = SplitPath(screenshot) local new_name = ModsScreenshotPrefix .. name .. ext local new_path = shots_path .. new_name local err = AsyncCopyFile(screenshot, new_path) if not err then local os_path = ConvertToOSPath(new_path) table.insert(params.screenshots, os_path) end end end local mod_entities = {} for _, entity in ipairs(mod_def.entities) do DelayedLoadEntity(mod_def, entity) mod_entities[entity] = true end WaitDelayedLoadEntities() ReloadLua() EngineBinAssetsPrints = {} local materials_seen, used_tex, textures_data = CollapseEntitiesTextures(mod_entities) if next(EngineBinAssetsPrints) then for _, log in ipairs(EngineBinAssetsPrints) do ModLogF(log) end end local dest_path = ConvertToOSPath(mod_def.content_path .. "BinAssets/") local res = SaveModMaterials(materials_seen, dest_path) --determine which files should to be packed and which ignored local files_to_pack = { } local substring_begin = #mod_def.content_path + 1 local err, all_files = AsyncListFiles(content_path, nil, "recursive") for i,file in ipairs(all_files) do local ignore for j,filter in ipairs(mod_def.ignore_files) do if MatchWildcard(file, filter) then ignore = true break end end local dir, filename, ext = SplitPath(file) if ext == ".dds" and not used_tex[filename .. ext] then ignore = true end ignore = ignore or ext == ".mtl" if not ignore then table.insert(files_to_pack, { src = file, dst = string.sub(file, substring_begin) }) end end --pack the mod content local err = AsyncPack(pack_path .. ModsPackFileName, content_path, files_to_pack) if err then return false, T{243097197797, --[[Mod upload error]] "Failed creating content package file ()", err = err} end params.os_pack_path = ConvertToOSPath(pack_path .. ModsPackFileName) return true, nil end function DbgPackMod(mod_def, show_file) local params = {} if mod_def:IsDirty() then mod_def:SaveWholeMod() end CreatePackageForUpload(mod_def, params) local dir = SplitPath(params.os_pack_path):gsub("/", "\\") if show_file then AsyncExec(string.format('explorer "%s"', dir)) end return dir end function PackModForBugReporter(mod) mod = IsKindOf(mod, "ModDef") and mod or (Mods and Mods[mod.id]) if not mod then return end local params = {} if mod:IsDirty() then mod:SaveWholeMod() end CreatePackageForUpload(mod, params) return params.os_pack_path end if FirstLoad then ModUploadDeveloperWarningShown = false end function UploadMod(ged_socket, mod, params, prepare_fn, upload_fn) ModUploadThread = CreateRealTimeThread(function(ged_socket, mod, params, prepare_fn, upload_fn) local function DoUpload() --uploading is done in three steps -- 1) the platform prepares the mod for uploading (generate IDs and others...) -- 2) the mod is packaged into a .hpk file -- 3) the mod is uploaded -- every function returns at least two parameters: `success` and `message` local function ReportError(ged_socket, message) ModLog(true, Untranslated{"Mod was not uploaded! Error: ", mod, err = message}) ged_socket:ShowMessage("Error", message) end local success, message success, message = prepare_fn(ged_socket, mod, params) if not success then ReportError(ged_socket, message) return end success, message = CreatePackageForUpload(mod, params) if not success then ReportError(ged_socket, message) return end success, message = upload_fn(ged_socket, mod, params) if not success then ReportError(ged_socket, message) else local msg = T{561889745203, "Mod was successfully uploaded!", mod} ModLog(msg) ged_socket:ShowMessage(T(898871916829, "Success"), msg) if insideHG() then if Platform.goldmaster then ged_socket:ShowMessage("Reminder", "After publishing a mod, make sure to copy it to svnAssets/Source/Mods/ and commit.") elseif Platform.developer and not ModUploadDeveloperWarningShown then ged_socket:ShowMessage("Reminder", "Publishing sample mods should be done using the target GoldMaster version of the game.") ModUploadDeveloperWarningShown = true end end end end PauseInfiniteLoopDetection("UploadMod") GedSetUiStatus("mod_upload", "Uploading...") DoUpload() GedSetUiStatus("mod_upload") ResumeInfiniteLoopDetection("UploadMod") ModUploadThread = false end, ged_socket, mod, params, prepare_fn, upload_fn) end function ValidateModBeforeUpload(ged_socket, mod) if IsValidThread(ModUploadThread) then ged_socket:ShowMessage("Error", "Another mod is currently uploading.\n\nPlease wait for the upload to finish.") return "upload in progress" end if mod.last_changes == "" then ged_socket:ShowMessage("Error", "Please fill in the 'Last Changes' field of your mod before uploading.") return "no 'last changes'" end if mod:IsDirty() then if "ok" ~= ged_socket:WaitQuestion("Mod Upload", "The mod needs to be saved before uploading.\n\nContinue?", "Yes", "No") or not mod:CanSaveMod(ged_socket) then return "mod saving failed" end mod:SaveWholeMod() end end function GedOpTestModItem(socket, root, path) local item = IsKindOf(root, "ModItem") and root or GetNodeByPath(root, path) if IsKindOf(item, "ModItem") then item:TestModItem(socket) end end function GedOpOpenModFolder(socket, root) local mod = root[1] local path = ConvertToOSPath(SlashTerminate(mod.path)) CreateRealTimeThread(function() AsyncExec(string.format('cmd /c start /D "%s" .', path)) end) end function GedOpPackMod(socket, root) local mod = root[1] if not mod then return end CreateRealTimeThread(function() if socket:WaitQuestion("Pack mod", "Packing the mod will take more time the bigger it is.\nAre you sure you want to continue?", "Yes", "No") == "ok" then GedSetUiStatus("mod_packing", "Packing mod...") DbgPackMod(mod, true) GedSetUiStatus("mod_packing") end end) end function GedOpModItemHelp(socket, root, path) local item = GetNodeByPath(root, path) if IsKindOf(item, "ModItem") then local filename = DocsRoot .. item.class .. ".md.html" if io.exists(filename) then local os_path = ConvertToOSPath(filename) OpenAddress(os_path) return end end local path_to_index = ConvertToOSPath(DocsRoot .. "index.md.html") if io.exists(path_to_index) then OpenAddress(path_to_index) end end function GedOpGenTTableMod(socket, root) local csv = {} local modDef = root[1] modDef:ForEachModItem(function(item) item:ForEachSubObject("PropertyObject", function(obj, parents) obj:GenerateLocalizationContext(obj) for _, propMeta in ipairs(obj.GetProperties and obj:GetProperties()) do local propVal = obj:GetProperty(propMeta.id) if propVal ~= "" and IsT(propVal) then local context, voice = match_and_remove(ContextCache[propVal], "voice:") if getmetatable(propVal) == TConcatMeta then for _, t in ipairs(propVal) do csv[#csv+1] = { id = TGetID(t), text = TDevModeGetEnglishText(t), context = context, voice = voice } end else csv[#csv+1] = { id = TGetID(propVal), text = TDevModeGetEnglishText(propVal), context = context, voice = voice } end end end end) end) local csv_filename = modDef.path .. "/ModTexts.csv" local fields = { "id", "text", "translation", "voice", "context" } -- translation is intentionally non-existent above, to create an empty column local field_captions = { "ID", "Text", "Translation", "VoiceActor", "Context" } local err = SaveCSV(csv_filename, csv, fields, field_captions, ",") if err then socket:ShowMessage("Error", "Failed to export a translation table to\n" .. ConvertToOSPath(csv_filename) .. "\nError: " .. err) else socket:ShowMessage("Success", "Successfully exported a translation table to\n" .. ConvertToOSPath(csv_filename)) end end local function GetDirSize(path) local err, files = AsyncListFiles(path) local size if not err then size = 0 for _, filename in ipairs(files) do size = size + io.getsize(filename) end end return size end local function GetModDetailsForBugReporter(modDef) local mod_content_path = modDef:GetModContentPath() local mod_root_path = modDef:GetModRootPath() local is_packed = modDef:IsPacked() local modSize = not is_packed and GetDirSize(ConvertToOSPath(mod_content_path)) or io.getsize(mod_root_path .. ModsPackFileName) local estPackSizeReduction = is_packed and 1 or 2 local maxSize = 100*1024*1024 --100mb local res = { id = modDef.id, title = modDef.title, mod_path = mod_content_path, mod_items_path = mod_root_path .. "items.lua", mod_metadata_path = mod_root_path .. "metadata.lua", mod_is_packed = is_packed and mod_root_path .. ModsPackFileName, mod_size_check = modSize and (modSize / estPackSizeReduction <= maxSize), mod_os_path = mod_root_path, } return res end function GedGetMod(socket) local mod = socket and socket.app_template == "ModEditor" and socket:ResolveObj("root") mod = mod and IsKindOf(mod[1], "ModDef") and mod[1] if not mod then return false end return GetModDetailsForBugReporter(mod) end function GedGetLastEditedMod(socket) return socket and GedGetMod(socket) or LastEditedMod end function GedAreModdingToolsActive(socket) return AreModdingToolsActive() end function GedPackModForBugReport(socket, mod) DebugPrint("Packing mod...") local modDef = Mods and Mods[mod.id] local packed_path if modDef then packed_path = PackModForBugReporter(modDef) end return packed_path end local function UpdateLastEditedMod(mod) local oldMod = LastEditedMod LastEditedMod = GetModDetailsForBugReporter(mod) if not oldMod or not LastEditedMod or oldMod.id ~= LastEditedMod.id then Msg("LastEditedModChanged", LastEditedMod) end end function OnMsg.ObjModified(obj) local mod = TryGetModDefFromObj(obj) if mod then UpdateLastEditedMod(mod) end end function OnMsg.GedOpened(app_id) local conn = GedConnections[app_id] if conn and conn.app_template == "ModEditor" then local root = conn and conn:ResolveObj("root") local mod = root and root[1] if mod then UpdateLastEditedMod(mod) end end end function GedGetSteamBetaName() local steam_beta, steam_branch if Platform.steam then steam_beta, steam_branch = SteamGetCurrentBetaName() end return steam_beta, steam_branch end