File size: 30,685 Bytes
b6a38d7 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 |
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 <ModLabel> 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 '<u(title)>' files will be copied to <u(path)>", mod, path = unpack_path})
if res ~= "ok" then
return
end
GedSetUiStatus("mod_unpack", "Copying...")
ModLog(T{348544010518, "Copying <ModLabel> to <u(path)>", 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 <u(folder)>: <u(err)>", 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 <u(file)>: <u(err)>", file = file, err = err})
end
end
else
ModLog(true, T{600384081290, "Error looking up files of <ModLabel>: <u(err)>", mod, err = err})
end
else
ModLog(true, T{836115199867, "Error looking up folders of <ModLabel>: <u(err)>", 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 <ModLabel>: <u(err)>", 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 <ModLabel> 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 '<u(name)>'!", name = items_name_string}
if #selection[2] ~= 1 then
confirm_text = T{621296865915, "Are you sure you want to delete the following <u(number_of_items)> selected items?\n<u(items)>", 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 = 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 <ModLabel> was not uploaded! Error: <u(err)>", 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 <ModLabel> 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 |