------------------------------------------------------------------------------------------------------------------------------------------------------------- -- VEAF spawnable aircrafts editor tool for DCS World -- By Zip (2021) -- -- Features: -- --------- -- * This tool processes a mission and update flight plans. -- * The flight plans templates can be customized -- -- Prerequisite: -- ------------ -- * The mission file archive must already be exploded ; the script only works on the mission files, not directly on the .miz archive -- -- Basic Usage: -- ------------ -- Call the script by running it in a lua environment ; it needs the veafMissionEditor library, so the script working directory must contain the veafMissionEditor.lua file -- -- veafSpawnableAircraftsEditor.lua [-debug|-trace] -- -- Command line options: -- * the path to the exploded mission files (no trailing backslash) -- * the path to the settings file -- * -debug if set, the script will output some information ; useful to find out which units were edited -- * -trace if set, the script will output a lot of information : useful to understand what went wrong -- * -import if set, the script will import data from the .miz file (the exploded mission folder) instead of injecting data. Useful to update the settings file -- * -dontclean, if set, the script will not delete all the injected groups (starting with "veafSpawn-" or matching ".*[tT]emplate.*") from the mission -- * -nameFilter, a regex that will be used to filter the groups that are processed (in either direction) by matching their names. Default to nil, all groups processed ------------------------------------------------------------------------------------------------------------------------------------------------------------- --require("src/scripts/veaf/veafMissionEditor") require("veafMissionEditor") veafSpawnableAircraftsEditor = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in the log will start with this. veafSpawnableAircraftsEditor.Id = "SPAWN_AC - " --- Version. veafSpawnableAircraftsEditor.Version = "1.3.0" -- trace level, specific to this module veafSpawnableAircraftsEditor.Trace = false veafSpawnableAircraftsEditor.Debug = false -- default position for all flights that are processed (imported from .miz to the configuration file) in meters, DCS model local DEFAULT_POSITION_X = 0 local DEFAULT_POSITION_Y = 0 -- default callsign by id local DEFAULT_CALLSIGNS_BY_ID = { [1] = "Enfield", [2] = "Springfield", [3] = "Uzi", [4] = "Colt", [5] = "Dodge", [6] = "Ford", [7] = "Chevy", [8] = "Pontiac" } ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- local CATEGORIES = {"plane", "helicopter"} local countriesById = { [0] = { iso = "RUS", name = "Russia", shortname = "Russia", id=0 }, [1] = { iso = "UKR", name = "Ukraine", shortname = "Ukraine", id=1 }, [2] = { iso = "USA", name = "USA", shortname = "USA", id=2 }, [3] = { iso = "TUR", name = "Turkey", shortname = "Turkey", id=3 }, [4] = { iso = "UK", name = "UK", shortname = "UK", id=4 }, [5] = { iso = "FRA", name = "France", shortname = "France", id=5 }, [6] = { iso = "GER", name = "Germany", shortname = "Germany", id=6 }, [7] = { iso = "AUSAF", name = "USAF Aggressors", shortname = "USAF Aggressors", id=7 }, [8] = { iso = "CAN", name = "Canada", shortname = "Canada", id=8 }, [9] = { iso = "SPN", name = "Spain", shortname = "Spain", id=9 }, [10] = { iso = "NETH", name = "The Netherlands", shortname = "The Netherlands", id=10 }, [11] = { iso = "BEL", name = "Belgium", shortname = "Belgium", id=11 }, [12] = { iso = "NOR", name = "Norway", shortname = "Norway", id=12 }, [13] = { iso = "DEN", name = "Denmark", shortname = "Denmark", id=13 }, [15] = { iso = "ISR", name = "Israel", shortname = "Israel", id=15 }, [16] = { iso = "GRG", name = "Georgia", shortname = "Georgia", id=16 }, [17] = { iso = "INS", name = "Insurgents", shortname = "Insurgents", id=17 }, [18] = { iso = "ABH", name = "Abkhazia", shortname = "Abkhazia", id=18 }, [19] = { iso = "RSO", name = "South Ossetia", shortname = "South Ossetia", id=19 }, [20] = { iso = "ITA", name = "Italy", shortname = "Italy", id=20 }, [21] = { iso = "AUS", name = "Australia", shortname = "Australia", id=21 }, [22] = { iso = "SUI", name = "Switzerland", shortname = "Switzerland", id=22 }, [23] = { iso = "AUT", name = "Austria", shortname = "Austria", id=23 }, [24] = { iso = "BLR", name = "Belarus", shortname = "Belarus", id=24 }, [25] = { iso = "BGR", name = "Bulgaria", shortname = "Bulgaria", id=25 }, [26] = { iso = "CZE", name = "Czech Republic", shortname = "Czech Republic", id=26 }, [27] = { iso = "CHN", name = "China", shortname = "China", id=27 }, [28] = { iso = "HRV", name = "Croatia", shortname = "Croatia", id=28 }, [29] = { iso = "EGY", name = "Egypt", shortname = "Egypt", id=29 }, [30] = { iso = "FIN", name = "Finland", shortname = "Finland", id=30 }, [31] = { iso = "GRC", name = "Greece", shortname = "Greece", id=31 }, [32] = { iso = "HUN", name = "Hungary", shortname = "Hungary", id=32 }, [33] = { iso = "IND", name = "India", shortname = "India", id=33 }, [34] = { iso = "IRN", name = "Iran", shortname = "Iran", id=34 }, [35] = { iso = "IRQ", name = "Iraq", shortname = "Iraq", id=35 }, [36] = { iso = "JPN", name = "Japan", shortname = "Japan", id=36 }, [37] = { iso = "KAZ", name = "Kazakhstan", shortname = "Kazakhstan", id=37 }, [38] = { iso = "PRK", name = "North Korea", shortname = "North Korea", id=38 }, [39] = { iso = "PAK", name = "Pakistan", shortname = "Pakistan", id=39 }, [40] = { iso = "POL", name = "Poland", shortname = "Poland", id=40 }, [41] = { iso = "ROU", name = "Romania", shortname = "Romania", id=41 }, [42] = { iso = "SAU", name = "Saudi Arabia", shortname = "Saudi Arabia", id=42 }, [43] = { iso = "SRB", name = "Serbia", shortname = "Serbia", id=43 }, [44] = { iso = "SVK", name = "Slovakia", shortname = "Slovakia", id=44 }, [45] = { iso = "KOR", name = "South Korea", shortname = "South Korea", id=45 }, [46] = { iso = "SWE", name = "Sweden", shortname = "Sweden", id=46 }, [47] = { iso = "SYR", name = "Syria", shortname = "Syria", id=47 }, [48] = { iso = "YEM", name = "Yemen", shortname = "Yemen", id=48 }, [49] = { iso = "VNM", name = "Vietnam", shortname = "Vietnam", id=49 }, [50] = { iso = "VEN", name = "Venezuela", shortname = "Venezuela", id=50 }, [51] = { iso = "TUN", name = "Tunisia", shortname = "Tunisia", id=51 }, [52] = { iso = "THA", name = "Thailand", shortname = "Thailand", id=52 }, [53] = { iso = "SDN", name = "Sudan", shortname = "Sudan", id=53 }, [54] = { iso = "PHL", name = "Philippines", shortname = "Philippines", id=54 }, [55] = { iso = "MAR", name = "Morocco", shortname = "Morocco", id=55 }, [56] = { iso = "MEX", name = "Mexico", shortname = "Mexico", id=56 }, [57] = { iso = "MYS", name = "Malaysia", shortname = "Malaysia", id=57 }, [58] = { iso = "LBY", name = "Libya", shortname = "Libya", id=58 }, [59] = { iso = "JOR", name = "Jordan", shortname = "Jordan", id=59 }, [60] = { iso = "IDN", name = "Indonesia", shortname = "Indonesia", id=60 }, [61] = { iso = "HND", name = "Honduras", shortname = "Honduras", id=61 }, [62] = { iso = "ETH", name = "Ethiopia", shortname = "Ethiopia", id=62 }, [63] = { iso = "CHL", name = "Chile", shortname = "Chile", id=63 }, [64] = { iso = "BRA", name = "Brazil", shortname = "Brazil", id=64 }, [65] = { iso = "BHR", name = "Bahrain", shortname = "Bahrain", id=65 }, [66] = { iso = "NZG", name = "Third Reich", shortname = "Third Reich", id=66 }, [67] = { iso = "YUG", name = "Yugoslavia", shortname = "Yugoslavia", id=67 }, [68] = { iso = "SUN", name = "USSR", shortname = "USSR", id=68 }, [69] = { iso = "RSI", name = "Italian Social Republic", shortname = "Italian Social Republic", id=69 }, [70] = { iso = "DZA", name = "Algeria", shortname = "Algeria", id=70 }, [71] = { iso = "KWT", name = "Kuwait", shortname = "Kuwait", id=71 }, [72] = { iso = "QAT", name = "Qatar", shortname = "Qatar", id=72 }, [73] = { iso = "OMN", name = "Oman", shortname = "Oman", id=73 }, [74] = { iso = "ARE", name = "United Arab Emirates", shortname = "United Arab Emirates", id=74 }, [75] = { iso = "RSA", name = "South Africa", shortname = "South Africa", id=75 }, [76] = { iso = "CUB", name = "Cuba", shortname = "Cuba", id=76 }, [77] = { iso = "PRT", name = "Portugal", shortname = "Portugal", id=77 }, [78] = { iso = "GDR", name = "GDR", shortname = "GDR", id=78 }, [79] = { iso = "LBN", name = "Lebanon", shortname = "Lebanon", id=79 }, [80] = { iso = "BLUE", name = "Combined Joint Task Forces Blue", shortname = "CJTF Blue", id=80 }, [81] = { iso = "RED", name = "Combined Joint Task Forces Red", shortname = "CJTF Red", id=81 }, [82] = { iso = "UN", name = "United Nations Peacekeepers", shortname = "UN", id=82 }, [83] = { iso = "ARG", name = "Argentina", shortname = "Argentina", id=83 }, [84] = { iso = "CYP", name = "Cyprus", shortname = "Cyprus", id=84 }, [85] = { iso = "SVN", name = "Slovenia", shortname = "Slovenia", id=85 }, [86] = { iso = "BOL", name = "Bolivia", shortname = "Bolivia", id=86 }, [87] = { iso = "GHA", name = "Ghana", shortname = "Ghana", id=87 }, [88] = { iso = "NGA", name = "Nigeria", shortname = "Nigeria", id=88 }, [89] = { iso = "PER", name = "Peru", shortname = "Peru", id=89 }, [90] = { iso = "ECU", name = "Ecuador", shortname = "Ecuador, id=90" } } local countriesByName = {} local countriesByShortname = {} for id, country in pairs(countriesById) do countriesByName[country.name] = country countriesByName[country.name:lower()] = country countriesByShortname[country.shortname] = country countriesByShortname[country.shortname:lower()] = country end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafSpawnableAircraftsEditor.logError(message) print(veafSpawnableAircraftsEditor.Id .. message) end function veafSpawnableAircraftsEditor.logInfo(message) print(veafSpawnableAircraftsEditor.Id .. message) end function veafSpawnableAircraftsEditor.logDebug(message) if message and veafSpawnableAircraftsEditor.Debug or veafSpawnableAircraftsEditor.Trace then print(veafSpawnableAircraftsEditor.Id .. message) end end function veafSpawnableAircraftsEditor.logTrace(message) if message and veafSpawnableAircraftsEditor.Trace then print(veafSpawnableAircraftsEditor.Id .. message) end end local p = nil local function _p(o, level) local MAX_LEVEL = 20 if level == nil then level = 0 end if level > MAX_LEVEL then logError("max depth reached in p : "..tostring(MAX_LEVEL)) return "" end local text = "" if (type(o) == "table") then text = "\n" for key,value in pairs(o) do for i=0, level do text = text .. " " end text = text .. "." .. key .. "=" .. p(value, level+1) .. "\n" end elseif (type(o) == "function") then text = "[function]" elseif (type(o) == "boolean") then if o == true then text = "[true]" else text = "[false]" end else if o == nil then text = "[nil]" else text = tostring(o) end end return text end p = function(o, level) if o and type(o) == "table" and (o.x and o.z and o.y and #o == 3) then return string.format("{x=%s, z=%s, y=%s}", p(o.x), p(o.z), p(o.y)) elseif o and type(o) == "table" and (o.x and o.y and #o == 2) then return string.format("{x=%s, y=%s}", p(o.x), p(o.y)) end return _p(o, level) end ---checks if a string starts with a prefix ---@param aString any ---@param aPrefix any ---@param caseSensitive? boolean ; if true, case sensitive search ---@return boolean local function startsWith(aString, aPrefix, caseSensitive) local aString = aString if not aString then return false elseif not caseSensitive then aString = aString:upper() end local aPrefix = aPrefix if not aPrefix then return false elseif not caseSensitive then aPrefix = aPrefix:upper() end return string.sub(aString,1,string.len(aPrefix))==aPrefix end local function deepcopy(obj, seen) -- Handle non-tables and previously-seen tables. if type(obj) ~= 'table' then return obj end if seen and seen[obj] then return seen[obj] end -- New table; mark it as seen and copy recursively. local s = seen or {} local res = {} s[obj] = res for k, v in pairs(obj) do res[deepcopy(k, s)] = deepcopy(v, s) end return setmetatable(res, getmetatable(obj)) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Core methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- ---Loads the .lua settings file containing all the spawnable aircrafts ---@param settingsPath string the complete path to the settings file ---@param nameFilter string? a regex that will filter out the group names that we want to load; defaults to nil -> all groups are loaded ---@return table veafSpawnableAircrafts a table containing all the loaded spawnable aircraft groups, by lowercased name local function loadSpawnableAircraftSettings(settingsPath, nameFilter) veafSpawnableAircraftsEditor.logDebug(string.format("Loading settings from [%s]",settingsPath)) local file, err = loadfile(settingsPath) if file then file() else veafSpawnableAircraftsEditor.logInfo(string.format("File does not exist: [%s]",settingsPath)) end local veafSpawnableAircrafts = {} local function addGroupIfFilterMatches(categoryName, coalitionName, countryName, groupData) local groupName = groupData.name or "" if (nameFilter == nil) or groupName:match(nameFilter) then groupData.veafSpawnableAircraftsEditorData = {} groupData.veafSpawnableAircraftsEditorData.categoryName = categoryName groupData.veafSpawnableAircraftsEditorData.coalitionName = coalitionName groupData.veafSpawnableAircraftsEditorData.countryName = countryName veafSpawnableAircrafts[groupData.name:lower()] = groupData end end if settings then -- old syntax --[[ settings = { ["FR planes"] = { ["category"] = "plane", ["coalition"] = "blue", ["country"] = "france", ["groups"] = { [1] = { ]] for _, settingData in pairs(settings) do local groups_t = settingData.groups if groups_t then for _, groupData in pairs(groups_t) do addGroupIfFilterMatches(settingData.category, settingData.coalition, settingData.country, groupData) end end end -- new syntax --[[ settings = { ["categories"] = { ["plane"] = { ["coalitions"] = { ["blue"] = { ["countries"] = { ["france"] = { ["groups"] = { ["veafSpawn-m2000-fox1"] = { ]] local categories_t = settings.categories if categories_t then for categoryName, categoryData in pairs(categories_t) do local coalitions_t = categoryData.coalitions if coalitions_t then for coalitionName, coalitionData in pairs(coalitions_t) do local countries_t = coalitionData.countries if countries_t then for countryName, countryData in pairs(countries_t) do local groups_t = countryData.groups if groups_t then for _, groupData in pairs(groups_t) do addGroupIfFilterMatches(categoryName, coalitionName, countryName, groupData) end end end end end end end end veafSpawnableAircraftsEditor.logDebug("Settings loaded") end return veafSpawnableAircrafts end local function processGroup(group) local rGroup = nil if group ~= nil then rGroup = deepcopy(group) rGroup.hidden = true rGroup.lateActivation = true rGroup.x = DEFAULT_POSITION_X rGroup.y = DEFAULT_POSITION_Y local route = rGroup.route if route then local points = route.points if points then for index, point in pairs(points) do if index ~= 1 then points[index] = nil -- remove all navigation points after the first one else points[index].x = DEFAULT_POSITION_X points[index].y = DEFAULT_POSITION_Y end end end end local units = rGroup.units if units then local unitNumber = 0 for _, unit in pairs(units) do unitNumber = unitNumber + 1 unit.name = string.format("%s #%02d", group.name, unitNumber) unit.x = DEFAULT_POSITION_X unit.y = DEFAULT_POSITION_Y end end end return rGroup end ---Reads a mission file and appends the resulting groups (after processing) into the spawnable aircrafts settings file ---@param missionPath string the mission file path (folder where the `mission` file resides) ---@param settingsPath string the spawnable aircrafts settings file path ---@param nameFilter string? a regex that will filter out the group names that we want to get from the mission; defaults to nil -> all groups are loaded function veafSpawnableAircraftsEditor.retrieveFromMission(missionPath, settingsPath, nameFilter) -- load the mission file veafSpawnableAircraftsEditor.logDebug(string.format("Loading mission from [%s]",missionPath)) local filePath = missionPath .. "\\mission" local file = assert(loadfile(filePath)) if not file then veafMissionEditor.logError(string.format("Error while loading mission file [%s]",missionPath)) return end file() ---@diagnostic disable-next-line: undefined-global -- "mission" is defined by the file() function local mission = mission veafSpawnableAircraftsEditor.logDebug("Mission loaded") local function processCategory(missionGroups, coalitionName, countryName, countryData, categoryName) local category_t = countryData[categoryName] if category_t then veafSpawnableAircraftsEditor.logDebug(string.format(" checking category [%s]", categoryName)) local group_t = category_t.group for _, groupData in pairs(group_t) do local groupName = groupData.name veafSpawnableAircraftsEditor.logDebug(string.format(" groupName=%s",p(groupName))) if groupName then if not(nameFilter) or groupName:match(nameFilter) then veafSpawnableAircraftsEditor.logDebug(" Group name checked") local missionGroup = groupData missionGroup.veafSpawnableAircraftsEditorData = {} missionGroup.veafSpawnableAircraftsEditorData.coalitionName = coalitionName missionGroup.veafSpawnableAircraftsEditorData.countryName = countryName missionGroup.veafSpawnableAircraftsEditorData.categoryName = categoryName table.insert(missionGroups, missionGroup) end end end end end -- parse the mission and get the aircrafts groups local missionGroups = {} if mission then local coalition_t = mission.coalition if coalition_t then for coalitionName, coalitionData in pairs(coalition_t) do veafSpawnableAircraftsEditor.logDebug(string.format(" coalitionName=%s",p(coalitionName))) local country_t = coalitionData.country if country_t then for _, countryData in pairs(country_t) do local countryName = countryData.name if countryName then veafSpawnableAircraftsEditor.logDebug(string.format(" countryName=%s",p(countryName))) -- process categories for _, categoryName in pairs(CATEGORIES) do processCategory(missionGroups, coalitionName, countryName, countryData, categoryName) end end end end end end end -- load the spawnable aircrafts settings file local veafSpawnableAircrafts = loadSpawnableAircraftSettings(settingsPath) -- load all groups, do not apply nameFilter! -- inject the mission groups into the spawnable aircrafts groups if missionGroups then for _, groupData in pairs(missionGroups) do local groupName = groupData.name if veafSpawnableAircrafts[groupName:lower()] then veafSpawnableAircraftsEditor.logInfo(string.format("REPLACING group [%s]", groupName)) else veafSpawnableAircraftsEditor.logInfo(string.format("ADDING group [%s]", groupName)) end veafSpawnableAircrafts[groupName:lower()] = groupData -- replace or insert end end -- save the spawnable aircrafts settings file local rSettings = { categories = {}} local rCategories = rSettings.categories for _, groupData in pairs(veafSpawnableAircrafts) do local rGroupData = processGroup(groupData) or {} local categoryName = rGroupData.veafSpawnableAircraftsEditorData.categoryName:lower() if not rCategories[categoryName] then rCategories[categoryName] = { coalitions = {}} end local rCoalitions = rCategories[categoryName].coalitions local coalitionName = rGroupData.veafSpawnableAircraftsEditorData.coalitionName:lower() if not rCoalitions[coalitionName] then rCoalitions[coalitionName] = { countries = {}} end local rCountries = rCoalitions[coalitionName].countries local countryName = rGroupData.veafSpawnableAircraftsEditorData.countryName if not rCountries[countryName] then rCountries[countryName] = { groups = {}} end local rGroups = rCountries[countryName].groups rGroupData.veafSpawnableAircraftsEditorData = nil local groupName = rGroupData.name rGroups[groupName] = rGroupData end local tableAsLua = veafMissionEditor.serialize("settings", rSettings, nil, false) veafMissionEditor.writeMissionFile(settingsPath, tableAsLua) end function veafSpawnableAircraftsEditor.injectInMission(filePath, settingsPath, nameFilter) -- load the spawnable aircrafts settings file local veafSpawnableAircrafts = loadSpawnableAircraftSettings(settingsPath, nameFilter) -- only load groups that match the nameFilter parameter, if any -- map the spawnable aircrafts groups by country and category local veafSpawnableAircraftsByCountryAndCategory = {} for groupName, groupData in pairs(veafSpawnableAircrafts) do local countryName = groupData.veafSpawnableAircraftsEditorData.countryName if countryName then if veafSpawnableAircraftsByCountryAndCategory[countryName] == nil then veafSpawnableAircraftsByCountryAndCategory[countryName] = {} end local countryCategoriesTable = veafSpawnableAircraftsByCountryAndCategory[countryName] local categoryName = groupData.veafSpawnableAircraftsEditorData.categoryName if countryCategoriesTable[categoryName] == nil then countryCategoriesTable[categoryName] = {} end countryCategoriesTable[categoryName][groupName] = groupData end end local availableCallsigns = {} for digit1 = 1, 8, 1 do for digit2 = 1, 9, 1 do for digit3 = 1, 9, 1 do local callsignId = digit1*100+digit2*10+digit3 availableCallsigns[callsignId] = true end end end local availableGroupIds = {} for i = 1, 30000, 1 do availableGroupIds[i] = true end local availableUnitIds = {} for i = 1, 50000, 1 do availableUnitIds[i] = true end local function getNextAvailableId(idLibrary, maxId) for i = 1, maxId, 1 do if idLibrary[i] then idLibrary[i] = false return i end end end local function getNextAvailableCallsign() return getNextAvailableId(availableCallsigns, 999) end local function getNextAvailableGroupId() return getNextAvailableId(availableGroupIds, 30000) end local function getNextAvailableUnitId() return getNextAvailableId(availableUnitIds, 50000) end local nbCallsignIds = 0 local function findAvailableIds(o) if (type(o) == "table") then for key,value in pairs(o) do --veafSpawnableAircraftsEditor.logTrace(string.format("parseTable %s", p(key))) if tostring(key):lower() == "groupid" then local groupId = tonumber(value) if groupId then availableGroupIds[groupId] = false veafSpawnableAircraftsEditor.logTrace(string.format("groupId [%s] is not available", p(groupId))) end elseif tostring(key):lower() == "unitid" then local unitId = tonumber(value) if unitId then availableUnitIds[unitId] = false veafSpawnableAircraftsEditor.logTrace(string.format("unitId [%s] is not available", p(unitId))) end elseif tostring(key):lower() == "callsign" then if type(value) == "table" then local digit1 = value[1] or 0 local digit2 = value[2] or 0 local digit3 = value[3] or 0 local callsignId = digit1*100+digit2*10+digit3 availableCallsigns[callsignId] = false nbCallsignIds = nbCallsignIds + 1 veafSpawnableAircraftsEditor.logTrace(string.format("callsignId [%s] is not available", p(callsignId))) end end findAvailableIds(value) end end end ---This function is called by the mission editor to process the lua mission table before it's written back to disk ---@param missionTable table a LUA mission table as read from the `mission` file ---@return table missionTable the same LUA mission table, processed and ready to be written back to the `mission` file local function processMissionTable(missionTable) -- find all available ids (groups, units, callsigns) findAvailableIds(missionTable) -- this will populate the local availableCallsigns, availableGroupIds, availableUnitIds variables veafSpawnableAircraftsEditor.logTrace(string.format("nbCallsignIds = [%s]", nbCallsignIds)) -- find the country tables in the mission table local missionCountryTablesByName = {} local missionCoalitionTablesByName = {} -- browse coalitions for _, missionCoalition_t in pairs(missionTable["coalition"]) do local missionCoalitionName = missionCoalition_t["name"] veafSpawnableAircraftsEditor.logTrace(string.format("found coalition [%s]",missionCoalitionName)) if missionCoalitionTablesByName[missionCoalitionName] == nil then missionCoalitionTablesByName[missionCoalitionName] = missionCoalition_t end -- browse countries for _, missionCountry_t in pairs(missionCoalition_t["country"]) do local missionCountryName = missionCountry_t["name"] if missionCountryName then veafSpawnableAircraftsEditor.logTrace(string.format("found country [%s]",missionCountryName)) missionCountryTablesByName[missionCountryName:lower()] = missionCountry_t end -- remove all the existing veafSpawn aircrafts groups if not veafSpawnableAircraftsEditor.leaveExistingGroupsInPlace then for _, categoryName in pairs(CATEGORIES) do local missionCategory_t = missionCountry_t[categoryName] if missionCategory_t then local missionCategoryGroup_t = missionCategory_t.group if missionCategoryGroup_t then local newMissionCategoryGroup_t = {} for _, missionGroup_t in pairs(missionCategoryGroup_t) do local groupName = missionGroup_t.name if groupName then if not (startsWith(groupName, "veafSpawn-", false) or groupName:match(".*[tT]emplate.*")) then -- skip spawnable groups and template groups (i.e. re-add the groups that are not managed by this script) table.insert(newMissionCategoryGroup_t, missionGroup_t) else veafSpawnableAircraftsEditor.logDebug(string.format("removing existing group from mission: [%s]",groupName)) end end end missionCategory_t.group = newMissionCategoryGroup_t end end end end end end -- process all the spawnable aircrafts groups by country for spawnCountryName, spawnCategoriesTable in pairs(veafSpawnableAircraftsByCountryAndCategory) do local missionCountry_t = missionCountryTablesByName[spawnCountryName:lower()] -- create the country in the mission if missing if not missionCountry_t then local spawnCoalitionName = nil for _, spawnCategoryTable in pairs(spawnCategoriesTable) do for _, spawnGroupTable in pairs(spawnCategoryTable) do spawnCoalitionName = spawnGroupTable.veafSpawnableAircraftsEditorData.coalitionName break end end local missionCoalition_t = missionCoalitionTablesByName[spawnCoalitionName] if missionCoalition_t then local countryData = countriesByName[spawnCountryName] if not countryData then countryData = countriesByShortname[spawnCountryName] end if countryData then veafSpawnableAircraftsEditor.logInfo(string.format("Missing country %s in the mission; adding it automatically", spawnCountryName)) missionCountry_t = { id = countryData.id, name = countryData.shortname} table.insert(missionCoalition_t["country"], missionCountry_t) end end end if not missionCountry_t then else veafSpawnableAircraftsEditor.logTrace(string.format("Processing country [%s]", spawnCountryName)) -- process categories for spawnCategoryName, spawnGroupsTable in pairs(spawnCategoriesTable) do local missionCategory_t = missionCountry_t[spawnCategoryName] -- create the category if it doesn't exist if not missionCategory_t then veafSpawnableAircraftsEditor.logInfo(string.format("Missing category %s for country %s in the mission; adding it automatically", spawnCategoryName, spawnCountryName)) missionCountry_t[spawnCategoryName] = { group = {}} end for spawnGroupName, spawnGroupData in pairs(spawnGroupsTable) do -- find the group in the category groups table local missionGroups_t = missionCountry_t[spawnCategoryName].group local foundIndex = nil for index, group_t in pairs(missionGroups_t) do local missionGroupName = group_t.name if missionGroupName:upper() == spawnGroupName:upper() then foundIndex = index break end end local newGroupData = deepcopy(spawnGroupData) -- change the group and units ids newGroupData.groupId = getNextAvailableGroupId() for _, newUnitData in pairs(newGroupData.units) do newUnitData.unitId = getNextAvailableUnitId() local callsignData = newUnitData.callsign if callsignData and type(callsignData) == "table" then local oldCallsign = callsignData.name local availableCallsignId = getNextAvailableCallsign() if not availableCallsignId then veafSpawnableAircraftsEditor.logError(string.format("Cannot find any available callsignId in mission for: [%s]", spawnGroupName)) else veafSpawnableAircraftsEditor.logTrace(string.format("availableCallsignId = [%s]", availableCallsignId)) local digit1 = math.floor(availableCallsignId / 100) local digit2 = math.floor((availableCallsignId - (digit1*100)) / 10) local digit3 = availableCallsignId - (digit1*100) - (digit2*10) callsignData.name = DEFAULT_CALLSIGNS_BY_ID[digit1] .. digit2 .. digit3 callsignData[1] = digit1 callsignData[2] = digit2 callsignData[3] = digit3 end veafSpawnableAircraftsEditor.logTrace(string.format("unitName=[%s] unitId=[%s], callsign changed from [%s] to [%s]", newUnitData.name, newUnitData.unitId, oldCallsign, callsignData.name)) end end -- reset work data newGroupData.veafSpawnableAircraftsEditorData = nil -- force late activation newGroupData.lateActivation = true -- force hidden on map newGroupData.hidden = true if foundIndex then -- found it, let's replace the group data veafSpawnableAircraftsEditor.logDebug(string.format("replacing existing group in mission: [%s]", spawnGroupName)) missionGroups_t[foundIndex] = newGroupData else -- inject the group as new veafSpawnableAircraftsEditor.logDebug(string.format("adding new group in mission: [%s]", spawnGroupName)) table.insert(missionGroups_t, newGroupData) end end end end end return missionTable end -- edit the "mission" file veafSpawnableAircraftsEditor.logDebug(string.format("Processing mission at [%s]",filePath)) local _filePath = filePath .. "\\mission" veafMissionEditor.editMission(_filePath, _filePath, "mission", processMissionTable) veafSpawnableAircraftsEditor.logDebug("Mission edited") end veafSpawnableAircraftsEditor.logDebug(string.format("#arg=%d",#arg)) for i=0, #arg do veafSpawnableAircraftsEditor.logDebug(string.format("arg[%d]=%s",i,arg[i])) end if #arg < 2 then veafSpawnableAircraftsEditor.logError("USAGE : veafSpawnableAircraftsEditor.lua [-debug|-trace] [-import] [-namefilter ]") return end local debug = veafSpawnableAircraftsEditor.Debug or false local trace = veafSpawnableAircraftsEditor.Trace or false local import = false local nameFilter = nil local filePath = arg[1] local settingsPath = arg[2] for i = 3, #arg, 1 do -- processing an argument veafSpawnableAircraftsEditor.logDebug(string.format("arg[%s]:lower()=%s", i, arg[i]:lower())) if arg[i]:lower() == "-debug" then debug = true elseif arg[i]:lower() == "-trace" then trace = true elseif arg[i]:lower() == "-import" then import = true elseif arg[i]:lower() == "-namefilter" then nameFilter = arg[i+1] elseif arg[i]:lower() == "-dontclean" then veafSpawnableAircraftsEditor.leaveExistingGroupsInPlace = true end end if debug or trace then veafSpawnableAircraftsEditor.Debug = true veafMissionEditor.Debug = true if trace then veafSpawnableAircraftsEditor.Trace = true veafMissionEditor.Trace = true end else veafSpawnableAircraftsEditor.Debug = false veafMissionEditor.Debug = false veafSpawnableAircraftsEditor.Trace = false veafMissionEditor.Trace = false end veafSpawnableAircraftsEditor.logDebug(string.format("nameFilter=%s", nameFilter or "")) if import then veafSpawnableAircraftsEditor.logInfo("Importing data from the mission and/or reprocessing the settings file)") veafSpawnableAircraftsEditor.retrieveFromMission(filePath, settingsPath, nameFilter) else veafSpawnableAircraftsEditor.logInfo("Injecting data into the mission") veafSpawnableAircraftsEditor.injectInMission(filePath, settingsPath, nameFilter) end