diff --git a/resources/plugins/base/mist_4_4_90.lua b/resources/plugins/base/mist_4_5_107.lua similarity index 73% rename from resources/plugins/base/mist_4_4_90.lua rename to resources/plugins/base/mist_4_5_107.lua index fbf4f6ac..431848d8 100644 --- a/resources/plugins/base/mist_4_4_90.lua +++ b/resources/plugins/base/mist_4_5_107.lua @@ -34,17 +34,19 @@ mist = {} -- don't change these mist.majorVersion = 4 -mist.minorVersion = 4 -mist.build = 90 +mist.minorVersion = 5 +mist.build = 107 -- forward declaration of log shorthand local log - +local dbLog + local mistSettings = { errorPopup = false, -- errors printed by mist logger will create popup warning you warnPopup = false, infoPopup = false, logLevel = 'warn', + dbLog = 'warn', } do -- the main scope @@ -73,11 +75,11 @@ do -- the main scope mist.nextGroupId = 1 mist.nextUnitId = 1 - local dbLog + local function initDBs() -- mist.DBs scope mist.DBs = {} - + mist.DBs.markList = {} mist.DBs.missionData = {} if env.mission then @@ -106,6 +108,25 @@ do -- the main scope zone.point.x = zone_data.x zone.point.y = 0 zone.point.z = zone_data.y + zone.properties = {} + if zone_data.properties then + for propInd, prop in pairs(zone_data.properties) do + if prop.value and type(prop.value) == 'string' and prop.value ~= "" then + zone.properties[prop.key] = prop.value + end + end + end + if zone.verticies then -- trust but verify + local r = 0 + for i = 1, #zone.verticies do + local dist = mist.utils.get2DDist(zone.point, zone.verticies[i]) + if dist > r then + r = mist.utils.deepCopy(dist) + end + end + zone.radius = r + + end mist.DBs.zonesByName[zone_data.name] = zone mist.DBs.zonesByNum[#mist.DBs.zonesByNum + 1] = mist.utils.deepCopy(zone) --[[deepcopy so that the zone in zones_by_name and the zone in @@ -113,6 +134,85 @@ do -- the main scope end end end + + mist.DBs.drawingByName = {} + mist.DBs.drawingIndexed = {} + + if env.mission.drawings and env.mission.drawings.layers then + for i = 1, #env.mission.drawings.layers do + local l = env.mission.drawings.layers[i] + + for j = 1, #l.objects do + local copy = mist.utils.deepCopy(l.objects[j]) + --log:warn(copy) + local doOffset = false + copy.layer = l.name + + local theta = copy.angle or 0 + theta = math.rad(theta) + if copy.primitiveType == "Polygon" then + + if copy.polygonMode == 'rect' then + local h, w = copy.height, copy.width + copy.points = {} + copy.points[1] = {x = h/2, y = w/2} + copy.points[2] = {x = -h/2, y = w/2} + copy.points[3] = {x = -h/2, y = -w/2} + copy.points[4] = {x = h/2, y = -w/2} + doOffset = true + elseif copy.polygonMode == "circle" then + copy.points = {x = copy.mapX, y = copy.mapY} + elseif copy.polygonMode == 'oval' then + copy.points = {} + local numPoints = 24 + local angleStep = (math.pi*2)/numPoints + doOffset = true + for v = 1, numPoints do + local pointAngle = v * angleStep + local x = copy.r1 * math.cos(pointAngle) + local y = copy.r2 * math.sin(pointAngle) + + table.insert(copy.points,{x=x,y=y}) + + end + elseif copy.polygonMode == "arrow" then + doOffset = true + end + + + if theta ~= 0 and copy.points and doOffset == true then + + --log:warn('offsetting Values') + for p = 1, #copy.points do + local offset = mist.vec.rotateVec2(copy.points[p], theta) + copy.points[p] = offset + end + --log:warn(copy.points[1]) + end + + elseif copy.primitiveType == "Line" and copy.closed == true then + table.insert(copy.points, mist.utils.deepCopy(copy.points[1])) + end + if copy.points and #copy.points > 1 then + for u = 1, #copy.points do + copy.points[u].x = mist.utils.round(copy.points[u].x + copy.mapX, 2) + copy.points[u].y = mist.utils.round(copy.points[u].y + copy.mapY, 2) + end + + end + if mist.DBs.drawingByName[copy.name] then + log:warn("Drawing by the name of [ $1 ] already exists in DB. Failed to add to mist.DBs.drawingByName.", copy.name) + else + + mist.DBs.drawingByName[copy.name] = copy + end + table.insert(mist.DBs.drawingIndexed, copy) + end + + end + + end + mist.DBs.navPoints = {} mist.DBs.units = {} @@ -151,28 +251,31 @@ do -- the main scope for cntry_id, cntry_data in pairs(coa_data.country) do local countryName = string.lower(cntry_data.name) + if cntry_data.id and country.names[cntry_data.id] then + countryName = string.lower(country.names[cntry_data.id]) + end mist.DBs.units[coa_name][countryName] = {} mist.DBs.units[coa_name][countryName].countryId = cntry_data.id if type(cntry_data) == 'table' then --just making sure - for obj_type_name, obj_type_data in pairs(cntry_data) do + for obj_cat_name, obj_cat_data in pairs(cntry_data) do - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then --should be an unncessary check + if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" or obj_cat_name == "static" then --should be an unncessary check - local category = obj_type_name + local category = obj_cat_name - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! + if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then --there's a group! mist.DBs.units[coa_name][countryName][category] = {} - for group_num, group_data in pairs(obj_type_data.group) do + for group_num, group_data in pairs(obj_cat_data.group) do if group_data and group_data.units and type(group_data.units) == 'table' then --making sure again- this is a valid group mist.DBs.units[coa_name][countryName][category][group_num] = {} local groupName = group_data.name - if env.mission.version > 7 then + if env.mission.version > 7 and env.mission.version < 19 then groupName = env.getValueDictByKey(groupName) end mist.DBs.units[coa_name][countryName][category][group_num].groupName = groupName @@ -196,7 +299,7 @@ do -- the main scope local units_tbl = mist.DBs.units[coa_name][countryName][category][group_num].units --pointer to the units table for this group units_tbl[unit_num] = {} - if env.mission.version > 7 then + if env.mission.version > 7 and env.mission.version < 19 then units_tbl[unit_num].unitName = env.getValueDictByKey(unit_data.name) else units_tbl[unit_num].unitName = unit_data.name @@ -240,6 +343,7 @@ do -- the main scope if category == 'static' then units_tbl[unit_num].categoryStatic = unit_data.category units_tbl[unit_num].shape_name = unit_data.shape_name + units_tbl[unit_num].linkUnit = unit_data.linkUnit if unit_data.mass then units_tbl[unit_num].mass = unit_data.mass end @@ -251,10 +355,10 @@ do -- the main scope end --for unit_num, unit_data in pairs(group_data.units) do end --if group_data and group_data.units then - end --for group_num, group_data in pairs(obj_type_data.group) do - end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do + end --for group_num, group_data in pairs(obj_cat_data.group) do + end --if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then + end --if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" or obj_cat_name == "static" then + end --for obj_cat_name, obj_cat_data in pairs(cntry_data) do end --if type(cntry_data) == 'table' then end --for cntry_id, cntry_data in pairs(coa_data.country) do end --if coa_data.country then --there is a country table @@ -305,6 +409,11 @@ do -- the main scope ['Arco'] = 2, ['Shell'] = 3, }, + ['TRANSPORT'] = { + ['Heavy'] = 9, + ['Trash'] = 10, + ['Cargo'] = 11, + ['Ascot'] = 12, ['JTAC'] = { ['Axeman'] = 1, ['Darknight'] = 2, @@ -346,14 +455,105 @@ do -- the main scope ['rules'] = { ['canUseAircraft'] = true, ['appliesTo'] = { - 'A-10C', + 'A-10C_2', + 'A-10C', 'A-10A', }, }, - }, - }, - }, - } + }, + ['f16'] = { + Viper = 9, + Venom = 10, + Lobo = 11, + Cowboy = 12, + Python = 13, + Rattler =14, + Panther = 15, + Wolf = 16, + Weasel = 17, + Wild = 18, + Ninja = 19, + Jedi = 20, + rules = { + ['canUseAircraft'] = true, + ['appliesTo'] = { + 'F-16C_50', + 'F-16C bl.52d', + 'F-16C bl.50', + 'F-16A MLU', + 'F-16A', + }, + }, + + }, + ['f18'] = { + ['Hornet'] = 9, + ['Squid'] = 10, + ['Ragin'] = 11, + ['Roman'] = 12, + Sting = 13, + Jury =14, + Jokey = 15, + Ram = 16, + Hawk = 17, + Devil = 18, + Check = 19, + Snake = 20, + ['rules'] = { + ['canUseAircraft'] = true, + ['appliesTo'] = { + + "FA-18C_hornet", + 'F/A-18C', + }, + }, + }, + ['b1'] = { + ['Bone'] = 9, + ['Dark'] = 10, + ['Vader'] = 11, + ['rules'] = { + ['canUseAircraft'] = true, + ['appliesTo'] = { + 'B-1B', + }, + }, + }, + ['b52'] = { + ['Buff'] = 9, + ['Dump'] = 10, + ['Kenworth'] = 11, + ['rules'] = { + ['canUseAircraft'] = true, + ['appliesTo'] = { + 'B-52H', + }, + }, + }, + ['f15e'] = { + ['Dude'] = 9, + ['Thud'] = 10, + ['Gunny'] = 11, + ['Trek'] = 12, + Sniper = 13, + Sled =14, + Best = 15, + Jazz = 16, + Rage = 17, + Tahoe = 18, + ['rules'] = { + ['canUseAircraft'] = true, + ['appliesTo'] = { + 'F-15E', + --'F-15ERAZBAM', + }, + }, + }, + + }, + }, + }, + } mist.DBs.const.shapeNames = { ["Landmine"] = "landmine", ["FARP CP Blindage"] = "kp_ug", @@ -443,6 +643,42 @@ do -- the main scope ["Small house 1A area"] = "domik1a-all", ["White_Flag"] = "H-Flag_W", ["Airshow_Cone"] = "Comp_cone", + ["Bulk Cargo Ship Ivanov"] = "barge-1", + ["Bulk Cargo Ship Yakushev"] = "barge-2", + ["Outpost"]="block", + ["Road outpost"]="block-onroad", + ["Container camo"] = "bw_container_cargo", + ["Tech Hangar A"] = "ceh_ang_a", + ["Bunker 1"] = "dot", + ["Bunker 2"] = "dot2", + ["Tanker Elnya 160"] = "elnya", + ["F-shape barrier"] = "f_bar_cargo", + ["Helipad Single"] = "farp", + ["FARP"] = "farps", + ["Fueltank"] = "fueltank_cargo", + ["Gate"] = "gate", + ["FARP Fuel Depot"] = "gsm rus", + ["Armed house"] = "home1_a", + ["FARP Command Post"] = "kp-ug", + ["Watch Tower Armed"] = "ohr-vyshka", + ["Oiltank"] = "oiltank_cargo", + ["Pipes small"] = "pipes_small_cargo", + ["Pipes big"] = "pipes_big_cargo", + ["Oil platform"] = "plavbaza", + ["Tetrapod"] = "tetrapod_cargo", + ["Fuel tank"] = "toplivo", + ["Trunks long"] = "trunks_long_cargo", + ["Trunks small"] = "trunks_small_cargo", + ["Passenger liner"] = "yastrebow", + ["Passenger boat"] = "zwezdny", + ["Oil rig"] = "oil_platform", + ["Gas platform"] = "gas_platform", + ["Container 20ft"] = "container_20ft", + ["Container 40ft"] = "container_40ft", + ["Downed pilot"] = "cadaver", + ["Parachute"] = "parash", + ["Pilot F15 Parachute"] = "pilot_f15_parachute", + ["Pilot standing"] = "pilot_parashut", } @@ -504,8 +740,8 @@ do -- the main scope mist.DBs.MEgroupsById = mist.utils.deepCopy(mist.DBs.groupsById) mist.DBs.deadObjects = {} - - do + + do local mt = {} function mt.__newindex(t, key, val) @@ -649,9 +885,14 @@ do -- the main scope newTable.category = 'static' else unitOneRef = newObject:getUnits() - newTable.countryId = tonumber(unitOneRef[1]:getCountry()) - newTable.coalitionId = tonumber(unitOneRef[1]:getCoalition()) - newTable.category = tonumber(newObject:getCategory()) + if #unitOneRef > 0 and unitOneRef[1] and type(unitOneRef[1]) == 'table' then + newTable.countryId = tonumber(unitOneRef[1]:getCountry()) + newTable.coalitionId = tonumber(unitOneRef[1]:getCoalition()) + newTable.category = tonumber(newObject:getCategory()) + else + log:warn('getUnits failed to return on $1 ; Built Data: $2.', event, newTable) + return false + end end for countryData, countryId in pairs(country.id) do if newTable.country and string.upper(countryData) == string.upper(newTable.country) or countryId == newTable.countryId then @@ -782,6 +1023,8 @@ do -- the main scope newTable.units[1].canCargo = data.canCargo newTable.units[1].categoryStatic = data.categoryStatic newTable.units[1].type = data.type + newTable.units[1].linkUnit = data.linkUnit + mistAddedObjects[index] = nil break end @@ -818,6 +1061,7 @@ do -- the main scope --dbLog:info('iterate') for name, gData in pairs(tempSpawnedGroups) do --env.info(name) + --dbLog:info(gData) local updated = false local stillExists = false if not gData.checked then @@ -825,9 +1069,9 @@ do -- the main scope local _g = gData.gp or Group.getByName(name) if mist.DBs.groupsByName[name] then -- first check group level properties, groupId, countryId, coalition - -- dbLog:info('Found in DBs, check if updated') + --dbLog:info('Found in DBs, check if updated') local dbTable = mist.DBs.groupsByName[name] - -- dbLog:info(dbTable) + --dbLog:info(dbTable) if gData.type ~= 'static' then -- dbLog:info('Not static') @@ -848,11 +1092,11 @@ do -- the main scope end --dbLog:info('Updated: $1', updated) if updated == false and gData.type ~= 'static' then -- time to check units - --dbLog:info('No Group Mismatch, Check Units') + --dbLog:info('No Group Mismatch, Check Units') if _g and _g:isExist() == true then stillExists = true for index, uObject in pairs(_g:getUnits()) do - --dbLog:info(index) + --dbLog:info(index) if mist.DBs.unitsByName[uObject:getName()] then --dbLog:info('UnitByName table exists') local uTable = mist.DBs.unitsByName[uObject:getName()] @@ -870,8 +1114,10 @@ do -- the main scope if stillExists == true and (updated == true or not mist.DBs.groupsByName[name]) then --dbLog:info('Get Table') - writeGroups[#writeGroups+1] = {data = dbUpdate(name, gData.type), isUpdated = updated} - + local dbData = dbUpdate(name, gData.type) + if dbData and type(dbData) == 'table' then + writeGroups[#writeGroups+1] = {data = dbData, isUpdated = updated} + end end -- Work done, so remove end @@ -998,19 +1244,20 @@ do -- the main scope local function groupSpawned(event) -- dont need to add units spawned in at the start of the mission if mist is loaded in init line if event.id == world.event.S_EVENT_BIRTH and timer.getTime0() < timer.getAbsTime() then - --dbLog:info('unitSpawnEvent') - + --log:info('unitSpawnEvent') + --log:info(event) + --log:info(event.initiator:getTypeName()) --table.insert(tempSpawnedUnits,(event.initiator)) ------- -- New functionality below. ------- if Object.getCategory(event.initiator) == 1 and not Unit.getPlayerName(event.initiator) then -- simple player check, will need to later check to see if unit was spawned with a player in a flight - --dbLog:info('Object is a Unit') + --log:info('Object is a Unit') if Unit.getGroup(event.initiator) then - --dbLog:info(Unit.getGroup(event.initiator):getName()) + -- log:info(Unit.getGroup(event.initiator):getName()) local g = Unit.getGroup(event.initiator) if not tempSpawnedGroups[g:getName()] then - --dbLog:info('added') + --log:info('added') tempSpawnedGroups[g:getName()] = {type = 'group', gp = g} tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1 end @@ -1018,7 +1265,7 @@ do -- the main scope log:error('Group not accessible by unit in event handler. This is a DCS bug') end elseif Object.getCategory(event.initiator) == 3 or Object.getCategory(event.initiator) == 6 then - --dbLog:info('Object is Static') + --log:info('Object is Static') tempSpawnedGroups[StaticObject.getName(event.initiator)] = {type = 'static'} tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1 end @@ -1169,10 +1416,13 @@ do -- the main scope for i = 1, #st do local s = st[i] if StaticObject.isExist(s) then - if not mist.DBs.unitsByName[s:getName()] then - --env.info(StaticObject.getID(s) .. ' Not found in DB yet') - tempSpawnedGroups[s:getName()] = {type = 'static'} - tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1 + local name = s:getName() + if not mist.DBs.unitsByName[name] then + dbLog:warn('$1 Not found in DB yet. ID: $2', name, StaticObject.getID(s)) + if string.len(name) > 0 then -- because in this mission someone sent the name was returning as an empty string. Gotta be careful. + tempSpawnedGroups[s:getName()] = {type = 'static'} + tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1 + end end end end @@ -1180,6 +1430,7 @@ do -- the main scope end end + --- init function. -- creates logger, adds default event handler @@ -1284,8 +1535,9 @@ do -- the main scope --- Spawns a static object to the game world. -- @todo write good docs -- @tparam table staticObj table containing data needed for the object creation - function mist.dynAddStatic(newObj) - log:info(newObj) + function mist.dynAddStatic(n) + --log:info(newObj) + local newObj = mist.utils.deepCopy(n) if newObj.units and newObj.units[1] then -- if its mist format for entry, val in pairs(newObj.units[1]) do if newObj[entry] and newObj[entry] ~= val or not newObj[entry] then @@ -1366,7 +1618,7 @@ do -- the main scope mistAddedObjects[#mistAddedObjects + 1] = mist.utils.deepCopy(newObj) if newObj.x and newObj.y and newObj.type and type(newObj.x) == 'number' and type(newObj.y) == 'number' and type(newObj.type) == 'string' then - log:info(newObj) + --log:warn(newObj) coalition.addStaticObject(country.id[newCountry], newObj) return newObj @@ -1379,8 +1631,10 @@ do -- the main scope -- Same as coalition.add function in SSE. checks the passed data to see if its valid. -- Will generate groupId, groupName, unitId, and unitName if needed -- @tparam table newGroup table containting values needed for spawning a group. - function mist.dynAdd(newGroup) - + function mist.dynAdd(ng) + + local newGroup = mist.utils.deepCopy(ng) + --log:warn(newGroup) --mist.debug.writeData(mist.utils.serialize,{'msg', newGroup}, 'newGroupOrig.lua') local cntry = newGroup.country if newGroup.countryId then @@ -1452,7 +1706,11 @@ do -- the main scope end if newGroup.clone and mist.DBs.groupsByName[newGroup.name] or not newGroup.name then - newGroup.name = tostring(newCountry .. tostring(typeName) .. mistDynAddIndex[typeName]) + --if newGroup.baseName then + -- idea of later. So custmozed naming can be created + -- else + newGroup.name = tostring(newCountry .. tostring(typeName) .. mistDynAddIndex[typeName]) + --end end if not newGroup.hidden then @@ -1537,16 +1795,32 @@ do -- the main scope end end else -- if aircraft and no route assigned. make a quick and stupid route so AI doesnt RTB immediately - if newCat == 'AIRPLANE' or newCat == 'HELICOPTER' then + --if newCat == 'AIRPLANE' or newCat == 'HELICOPTER' then newGroup.route = {} newGroup.route.points = {} newGroup.route.points[1] = {} - end + --end end newGroup.country = newCountry - - --mist.debug.writeData(mist.utils.serialize,{'msg', newGroup}, 'newGroup.lua') + -- update and verify any self tasks + if newGroup.route and newGroup.route.points then + for i, pData in pairs(newGroup.route.points) do + if pData.task and pData.task.params and pData.task.params.tasks and #pData.task.params.tasks > 0 then + for tIndex, tData in pairs(pData.task.params.tasks) do + if tData.params and tData.params.action then + if tData.params.action.id == "EPLRS" then + tData.params.action.params.groupId = newGroup.groupId + elseif tData.params.action.id == "ActivateBeacon" or tData.params.action.id == "ActivateICLS" then + tData.params.action.params.unitId = newGroup.units[1].unitId + end + end + end + end + + end + end + --mist.debug.writeData(mist.utils.serialize,{'msg', newGroup}, 'newGroupPushedToAddGroup.lua') --log:warn(newGroup) -- sanitize table newGroup.groupName = nil @@ -1559,7 +1833,7 @@ do -- the main scope for unitIndex, unitData in pairs(newGroup.units) do newGroup.units[unitIndex].unitName = nil end - + coalition.addGroup(country.id[newCountry], Unit.Category[newCat], newGroup) return newGroup @@ -1816,10 +2090,10 @@ do if type(coa_data) == 'table' then if coa_data.country then --there is a country table for cntry_id, cntry_data in pairs(coa_data.country) do - for obj_type_name, obj_type_data in pairs(cntry_data) do - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - for group_num, group_data in pairs(obj_type_data.group) do + for obj_cat_name, obj_cat_data in pairs(cntry_data) do + if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" then -- only these types have points + if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then --there's a group! + for group_num, group_data in pairs(obj_cat_data.group) do if group_data and group_data.groupId == gpId then -- this is the group we are looking for if group_data.route and group_data.route.points and #group_data.route.points > 0 then local points = {} @@ -1834,10 +2108,10 @@ do end return end --if group_data and group_data.name and group_data.name == 'groupname' - end --for group_num, group_data in pairs(obj_type_data.group) do - end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do + end --for group_num, group_data in pairs(obj_cat_data.group) do + end --if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then + end --if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" or obj_cat_name == "static" then + end --for obj_cat_name, obj_cat_data in pairs(cntry_data) do end --for cntry_id, cntry_data in pairs(coa_data.country) do end --if coa_data.country then --there is a country table end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then @@ -2214,9 +2488,25 @@ do --- Returns a table containing unit names. -- @tparam table tbl sequential strings -- @treturn table @{UnitNameTable} - function mist.makeUnitTable(tbl) + function mist.makeUnitTable(tbl, exclude) --Assumption: will be passed a table of strings, sequential --log:info(tbl) + + + local excludeType = {} + if exclude then + if type(exclude) == 'table' then + for x, y in pairs(exclude) do + excludeType[x] = true + excludeType[y] = true + end + else + excludeType[exclude] = true + end + + end + + local units_by_name = {} local l_munits = mist.DBs.units --local reference for faster execution @@ -2277,12 +2567,15 @@ do elseif unit:sub(4,12) == '[vehicle]' then category = 'vehicle' country_start = 13 + elseif unit:sub(4, 11) == '[static]' then + category = 'static' + country_start = 12 end for coa, coa_tbl in pairs(l_munits) do for country, country_table in pairs(coa_tbl) do if country == string.lower(unit:sub(country_start)) then -- match for unit_type, unit_type_tbl in pairs(country_table) do - if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then + if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then for group_ind, group_tbl in pairs(unit_type_tbl) do if type(group_tbl) == 'table' then for unit_ind, unit in pairs(group_tbl.units) do @@ -2310,12 +2603,15 @@ do elseif unit:sub(5,13) == '[vehicle]' then category = 'vehicle' country_start = 14 + elseif unit:sub(5, 12) == '[static]' then + category = 'static' + country_start = 13 end for coa, coa_tbl in pairs(l_munits) do for country, country_table in pairs(coa_tbl) do if country == string.lower(unit:sub(country_start)) then -- match for unit_type, unit_type_tbl in pairs(country_table) do - if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then + if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then for group_ind, group_tbl in pairs(unit_type_tbl) do if type(group_tbl) == 'table' then for unit_ind, unit in pairs(group_tbl.units) do @@ -2340,12 +2636,14 @@ do category = 'ship' elseif unit:sub(7) == '[vehicle]' then category = 'vehicle' - end + elseif unit:sub(7) == '[static]' then + category = 'static' + end for coa, coa_tbl in pairs(l_munits) do if coa == 'blue' then for country, country_table in pairs(coa_tbl) do for unit_type, unit_type_tbl in pairs(country_table) do - if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then + if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then for group_ind, group_tbl in pairs(unit_type_tbl) do if type(group_tbl) == 'table' then for unit_ind, unit in pairs(group_tbl.units) do @@ -2368,12 +2666,14 @@ do category = 'ship' elseif unit:sub(8) == '[vehicle]' then category = 'vehicle' + elseif unit:sub(8) == '[static]' then + category = 'static' end for coa, coa_tbl in pairs(l_munits) do if coa == 'blue' then for country, country_table in pairs(coa_tbl) do for unit_type, unit_type_tbl in pairs(country_table) do - if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then + if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then for group_ind, group_tbl in pairs(unit_type_tbl) do if type(group_tbl) == 'table' then for unit_ind, unit in pairs(group_tbl.units) do @@ -2398,12 +2698,14 @@ do category = 'ship' elseif unit:sub(6) == '[vehicle]' then category = 'vehicle' + elseif unit:sub(6) == '[static]' then + category = 'static' end for coa, coa_tbl in pairs(l_munits) do if coa == 'red' then for country, country_table in pairs(coa_tbl) do for unit_type, unit_type_tbl in pairs(country_table) do - if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then + if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then for group_ind, group_tbl in pairs(unit_type_tbl) do if type(group_tbl) == 'table' then for unit_ind, unit in pairs(group_tbl.units) do @@ -2426,12 +2728,14 @@ do category = 'ship' elseif unit:sub(7) == '[vehicle]' then category = 'vehicle' + elseif unit:sub(7) == '[static]' then + category = 'static' end for coa, coa_tbl in pairs(l_munits) do if coa == 'red' then for country, country_table in pairs(coa_tbl) do for unit_type, unit_type_tbl in pairs(country_table) do - if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then + if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then for group_ind, group_tbl in pairs(unit_type_tbl) do if type(group_tbl) == 'table' then for unit_ind, unit in pairs(group_tbl.units) do @@ -2456,11 +2760,13 @@ do category = 'ship' elseif unit:sub(6) == '[vehicle]' then category = 'vehicle' + elseif unit:sub(6) == '[static]' then + category = 'static' end for coa, coa_tbl in pairs(l_munits) do for country, country_table in pairs(coa_tbl) do for unit_type, unit_type_tbl in pairs(country_table) do - if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then + if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then for group_ind, group_tbl in pairs(unit_type_tbl) do if type(group_tbl) == 'table' then for unit_ind, unit in pairs(group_tbl.units) do @@ -2482,11 +2788,13 @@ do category = 'ship' elseif unit:sub(7) == '[vehicle]' then category = 'vehicle' + elseif unit:sub(7) == '[static]' then + category = 'static' end for coa, coa_tbl in pairs(l_munits) do for country, country_table in pairs(coa_tbl) do for unit_type, unit_type_tbl in pairs(country_table) do - if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then + if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then for group_ind, group_tbl in pairs(unit_type_tbl) do if type(group_tbl) == 'table' then for unit_ind, unit in pairs(group_tbl.units) do @@ -2517,6 +2825,126 @@ do return units_tbl end +function mist.getUnitsByAttribute(att, rnum, id) + local cEntry = {} + cEntry.typeName = att.type or att.typeName or att.typename + cEntry.country = att.country + cEntry.coalition = att.coalition + cEntry.skill = att.skill + cEntry.categry = att.category + + local num = rnum or 1 + + if cEntry.skill == 'human' then + cEntry.skill = {'Client', 'Player'} + end + + + local checkedVal = {} + local units = {} + for uName, uData in pairs(mist.DBs.unitsByName) do + local matched = 0 + for cName, cVal in pairs(cEntry) do + if type(cVal) == 'table' then + for sName, sVal in pairs(cVal) do + if (uData[cName] and uData[cName] == sVal) or (uData[cName] and uData[cName] == sName) then + matched = matched + 1 + end + end + else + if uData[cName] and uData[cName] == cVal then + matched = matched + 1 + end + end + end + if matched >= num then + if id then + units[uData.unitId] = true + else + + units[uName] = true + end + end + end + + local rtn = {} + for name, _ in pairs(units) do + table.insert(rtn, name) + end + return rtn + +end + +function mist.getGroupsByAttribute(att, rnum, id) + local cEntry = {} + cEntry.typeName = att.type or att.typeName or att.typename + cEntry.country = att.country + cEntry.coalition = att.coalition + cEntry.skill = att.skill + cEntry.categry = att.category + + local num = rnum or 1 + + if cEntry.skill == 'human' then + cEntry.skill = {'Client', 'Player'} + end + local groups = {} + for gName, gData in pairs(mist.DBs.groupsByName) do + local matched = 0 + for cName, cVal in pairs(cEntry) do + if type(cVal) == 'table' then + for sName, sVal in pairs(cVal) do + if cName == 'skill' or cName == 'typeName' then + local lMatch = 0 + for uId, uData in pairs(gData.units) do + if (uData[cName] and uData[cName] == sVal) or (gData[cName] and gData[cName] == sName) then + lMatch = lMatch + 1 + break + end + end + if lMatch > 0 then + matched = matched + 1 + end + end + if (gData[cName] and gData[cName] == sVal) or (gData[cName] and gData[cName] == sName) then + matched = matched + 1 + break + end + end + else + if cName == 'skill' or cName == 'typeName' then + local lMatch = 0 + for uId, uData in pairs(gData.units) do + if (uData[cName] and uData[cName] == sVal) then + lMatch = lMatch + 1 + break + end + end + if lMatch > 0 then + matched = matched + 1 + end + end + if gData[cName] and gData[cName] == cVal then + matched = matched + 1 + end + end + end + if matched >= num then + if id then + groups[gData.groupid] = true + else + groups[gName] = true + end + end + end + local rtn = {} + for name, _ in pairs(groups) do + table.insert(rtn, name) + end + return rtn + +end + function mist.getDeadMapObjsInZones(zone_names) -- zone_names: table of zone names -- returns: table of dead map objects (indexed numerically) @@ -2552,6 +2980,160 @@ function mist.getDeadMapObjsInPolygonZone(zone) end return map_objs end +mist.shape = {} +function mist.shape.insideShape(shape1, shape2, full) + if shape1.radius then -- probably a circle + if shape2.radius then + return mist.shape.circleInCircle(shape1, shape2, full) + elseif shape2[1] then + return mist.shape.circleInPoly(shape1, shape2, full) + end + + elseif shape1[1] then -- shape1 is probably a polygon + if shape2.radius then + return mist.shape.polyInCircle(shape1, shape2, full) + elseif shape2[1] then + return mist.shape.polyInPoly(shape1, shape2, full) + end + end + return false +end + +function mist.shape.circleInCircle(c1, c2, full) + if not full then -- quick partial check + if mist.utils.get2DDist(c1.point, c2.point) <= c2.radius then + return true + end + end + local theta = mist.utils.getHeadingPoints(c2.point, c1.point) -- heading from + if full then + return mist.utils.get2DDist(mist.projectPoint(c1.point, c1.radius, theta), c2.point) <= c2.radius + else + return mist.utils.get2DDist(mist.projectPoint(c1.point, c1.radius, theta + math.pi), c2.point) <= c2.radius + end + return false +end + + +function mist.shape.circleInPoly(circle, poly, full) + + if poly and type(poly) == 'table' and circle and type(circle) == 'table' and circle.radius and circle.point then + if not full then + for i = 1, #poly do + if mist.utils.get2DDist(circle.point, poly[i]) <= circle.radius then + return true + end + end + end + -- no point is inside of the zone, now check if any part is + local count = 0 + for i = 1, #poly do + local theta -- heading of each set of points + if i == #poly then + theta = mist.utils.getHeadingPoints(poly[i],poly[1]) + else + theta = mist.utils.getHeadingPoints(poly[i],poly[i+1]) + end + -- offset + local pPoint = mist.projectPoint(circle.point, circle.radius, theta - (math.pi/180)) + local oPoint = mist.projectPoint(circle.point, circle.radius, theta + (math.pi/180)) + + + if mist.pointInPolygon(pPoint, poly) == true then + if (full and mist.pointInPolygon(oPoint, poly) == true) or not full then + return true + + end + + end + end + + end + return false +end + + +function mist.shape.polyInPoly(p1, p2, full) + local count = 0 + for i = 1, #p1 do + + if mist.pointInPolygon(p1[i], p2) then + count = count + 1 + end + if (not full) and count > 0 then + return true + end + end + if count == #p1 then + return true + end + + return false +end + +function mist.shape.polyInCircle(poly, circle, full) + local count = 0 + for i = 1, #poly do + if mist.utils.get2DDist(circle.point, poly[i]) <= circle.radius then + if full then + count = count + 1 + else + return true + end + end + end + if count == #poly then + return true + end + + return false +end + +function mist.shape.getPointOnSegment(point, seg, isSeg) + local p = mist.utils.makeVec2(point) + local s1 = mist.utils.makeVec2(seg[1]) + local s2 = mist.utils.makeVec2(seg[2]) + + + local cx, cy = p.x - s1.x, p.y - s1.y + local dx, dy = s2.x - s1.x, s2.x - s1.y + local d = (dx*dx + dy*dy) + + if d == 0 then + return {x = s1.x, y = s1.y} + end + local u = (cx*dx + cy*dy)/d + if isSeg then + if u < 0 then + u = 0 + elseif u > 1 then + u = 1 + end + end + return {x = s1.x + u*dx, y = s1.y + u*dy} +end + + +function mist.shape.segmentIntersect(segA, segB) + local dx1, dy1 = segA[2].x - segA[1].x, segA[2] - segA[1].y + local dx2, dy2 = segB[2].x - segB[1].x, segB[2] - segB[1].y + local dx3, dy3 = segA[1].x - segB[1].x, segA[1].y - segB[1].y + local d = dx1*dy2 - dy1*dx2 + if d == 0 then + return false + end + local t1 = (dx2*dy3 - dy2*dx3)/d + if t1 < 0 or t1 > 1 then + return false + end + local t2 = (dx1*dy3 - dy1*dx3)/d + if t2 < 0 or t2 > 1 then + return false + end + -- point of intersection + return true, segA[1].x + t1*dx1, segA[1].y + t1*dy1 +end + function mist.pointInPolygon(point, poly, maxalt) --raycasting point in polygon. Code from http://softsurfer.com/Archive/algorithm_0103/algorithm_0103.htm --[[local type_tbl = { @@ -2591,17 +3173,23 @@ function mist.pointInPolygon(point, poly, maxalt) --raycasting point in polygon. end end +function mist.mapValue(val, inMin, inMax, outMin, outMax) + return (val - inMin) * (outMax - outMin) / (inMax - inMin) + outMin +end + function mist.getUnitsInPolygon(unit_names, polyZone, max_alt) local units = {} for i = 1, #unit_names do - units[#units + 1] = Unit.getByName(unit_names[i]) + units[#units + 1] = Unit.getByName(unit_names[i]) or StaticObject.getByName(unit_names[i]) end local inZoneUnits = {} for i =1, #units do - if units[i]:isActive() and mist.pointInPolygon(units[i]:getPosition().p, polyZone, max_alt) then - inZoneUnits[#inZoneUnits + 1] = units[i] + local lUnit = units[i] + local lCat = lUnit:getCategory() + if ((lCat == 1 and lUnit:isActive()) or lCat ~= 1) and mist.pointInPolygon(lUnit:getPosition().p, polyZone, max_alt) then + inZoneUnits[#inZoneUnits + 1] = lUnit end end @@ -2609,8 +3197,7 @@ function mist.getUnitsInPolygon(unit_names, polyZone, max_alt) end function mist.getUnitsInZones(unit_names, zone_names, zone_type) - - zone_type = zone_type or 'cylinder' + zone_type = zone_type or 'cylinder' if zone_type == 'c' or zone_type == 'cylindrical' or zone_type == 'C' then zone_type = 'cylinder' end @@ -2622,9 +3209,13 @@ function mist.getUnitsInZones(unit_names, zone_names, zone_type) local units = {} local zones = {} - + + if zone_names and type(zone_names) == 'string' then + zone_names = {zone_names} + end for k = 1, #unit_names do - local unit = Unit.getByName(unit_names[k]) + + local unit = Unit.getByName(unit_names[k]) or StaticObject.getByName(unit_names[k]) if unit then units[#units + 1] = unit end @@ -2632,31 +3223,40 @@ function mist.getUnitsInZones(unit_names, zone_names, zone_type) for k = 1, #zone_names do - local zone = trigger.misc.getZone(zone_names[k]) + local zone = mist.DBs.zonesByName[zone_names[k]] if zone then - zones[#zones + 1] = {radius = zone.radius, x = zone.point.x, y = zone.point.y, z = zone.point.z} + zones[#zones + 1] = {radius = zone.radius, x = zone.point.x, y = zone.point.y, z = zone.point.z, verts = zone.verticies} end end local in_zone_units = {} - for units_ind = 1, #units do - for zones_ind = 1, #zones do + local lUnit = units[units_ind] + local unit_pos = lUnit:getPosition().p + local lCat = lUnit:getCategory() + for zones_ind = 1, #zones do if zone_type == 'sphere' then --add land height value for sphere zone type local alt = land.getHeight({x = zones[zones_ind].x, y = zones[zones_ind].z}) if alt then zones[zones_ind].y = alt end end - local unit_pos = units[units_ind]:getPosition().p - if unit_pos and units[units_ind]:isActive() == true then - if zone_type == 'cylinder' and (((unit_pos.x - zones[zones_ind].x)^2 + (unit_pos.z - zones[zones_ind].z)^2)^0.5 <= zones[zones_ind].radius) then - in_zone_units[#in_zone_units + 1] = units[units_ind] - break - elseif zone_type == 'sphere' and (((unit_pos.x - zones[zones_ind].x)^2 + (unit_pos.y - zones[zones_ind].y)^2 + (unit_pos.z - zones[zones_ind].z)^2)^0.5 <= zones[zones_ind].radius) then - in_zone_units[#in_zone_units + 1] = units[units_ind] - break - end + + if unit_pos and ((lCat == 1 and lUnit:isActive() == true) or lCat ~= 1) then -- it is a unit and is active or it is not a unit + if zones[zones_ind].verts then + if mist.pointInPolygon(unit_pos, zones[zones_ind].verts) then + in_zone_units[#in_zone_units + 1] = lUnit + end + + else + if zone_type == 'cylinder' and (((unit_pos.x - zones[zones_ind].x)^2 + (unit_pos.z - zones[zones_ind].z)^2)^0.5 <= zones[zones_ind].radius) then + in_zone_units[#in_zone_units + 1] = lUnit + break + elseif zone_type == 'sphere' and (((unit_pos.x - zones[zones_ind].x)^2 + (unit_pos.y - zones[zones_ind].y)^2 + (unit_pos.z - zones[zones_ind].z)^2)^0.5 <= zones[zones_ind].radius) then + in_zone_units[#in_zone_units + 1] = lUnit + break + end + end end end end @@ -2679,14 +3279,14 @@ function mist.getUnitsInMovingZones(unit_names, zone_unit_names, radius, zone_ty local zone_units = {} for k = 1, #unit_names do - local unit = Unit.getByName(unit_names[k]) + local unit = Unit.getByName(unit_names[k]) or StaticObject.getByName(unit_names[k]) if unit then units[#units + 1] = unit end end for k = 1, #zone_unit_names do - local unit = Unit.getByName(zone_unit_names[k]) + local unit = Unit.getByName(zone_unit_names[k]) or StaticObject.getByName(zone_unit_names[k]) if unit then zone_units[#zone_units + 1] = unit end @@ -2695,15 +3295,18 @@ function mist.getUnitsInMovingZones(unit_names, zone_unit_names, radius, zone_ty local in_zone_units = {} for units_ind = 1, #units do + local lUnit = units[units_ind] + local lCat = lUnit:getCategory() + local unit_pos = lUnit:getPosition().p for zone_units_ind = 1, #zone_units do - local unit_pos = units[units_ind]:getPosition().p + local zone_unit_pos = zone_units[zone_units_ind]:getPosition().p - if unit_pos and zone_unit_pos and units[units_ind]:isActive() == true then + if unit_pos and zone_unit_pos and ((lCat == 1 and lUnit:isActive()) or lCat ~= 1) then if zone_type == 'cylinder' and (((unit_pos.x - zone_unit_pos.x)^2 + (unit_pos.z - zone_unit_pos.z)^2)^0.5 <= radius) then - in_zone_units[#in_zone_units + 1] = units[units_ind] + in_zone_units[#in_zone_units + 1] = lUnit break elseif zone_type == 'sphere' and (((unit_pos.x - zone_unit_pos.x)^2 + (unit_pos.y - zone_unit_pos.y)^2 + (unit_pos.z - zone_unit_pos.z)^2)^0.5 <= radius) then - in_zone_units[#in_zone_units + 1] = units[units_ind] + in_zone_units[#in_zone_units + 1] = lUnit break end end @@ -2721,7 +3324,8 @@ function mist.getUnitsLOS(unitset1, altoffset1, unitset2, altoffset2, radius) -- get the positions all in one step, saves execution time. for unitset1_ind = 1, #unitset1 do local unit1 = Unit.getByName(unitset1[unitset1_ind]) - if unit1 and unit1:isActive() == true then + local lCat = unit1:getCategory() + if unit1 and ((lCat == 1 and unit1:isActive()) or lCat ~= 1) then unit_info1[#unit_info1 + 1] = {} unit_info1[#unit_info1].unit = unit1 unit_info1[#unit_info1].pos = unit1:getPosition().p @@ -2730,7 +3334,8 @@ function mist.getUnitsLOS(unitset1, altoffset1, unitset2, altoffset2, radius) for unitset2_ind = 1, #unitset2 do local unit2 = Unit.getByName(unitset2[unitset2_ind]) - if unit2 and unit2:isActive() == true then + local lCat = unit2:getCategory() + if unit2 and ((lCat == 1 and unit2:isActive()) or lCat ~= 1) then unit_info2[#unit_info2 + 1] = {} unit_info2[#unit_info2].unit = unit2 unit_info2[#unit_info2].pos = unit2:getPosition().p @@ -2766,7 +3371,8 @@ end function mist.getAvgPoint(points) local avgX, avgY, avgZ, totNum = 0, 0, 0, 0 for i = 1, #points do - local nPoint = mist.utils.makeVec3(points[i]) + --log:warn(points[i]) + local nPoint = mist.utils.makeVec3(points[i]) if nPoint.z then avgX = avgX + nPoint.x avgY = avgY + nPoint.y @@ -2859,13 +3465,13 @@ function mist.getBRString(vars) local metric = vars.metric local avgPos = mist.getAvgPos(units) if avgPos then - local vec = {x = avgPos.x - ref.x, y = avgPos.y - ref.y, z = avgPos.z - ref.z} - local dir = mist.utils.getDir(vec, ref) - local dist = mist.utils.get2DDist(avgPos, ref) - if alt then - alt = avgPos.y - end - return mist.tostringBR(dir, dist, alt, metric) + local vec = {x = avgPos.x - ref.x, y = avgPos.y - ref.y, z = avgPos.z - ref.z} + local dir = mist.utils.getDir(vec, ref) + local dist = mist.utils.get2DDist(avgPos, ref) + if alt then + alt = avgPos.y + end + return mist.tostringBR(dir, dist, alt, metric) end end @@ -2891,10 +3497,11 @@ function mist.getLeadingPos(vars) unitPosTbl[#unitPosTbl + 1] = unit:getPosition().p end end + if #unitPosTbl > 0 then -- one more more units found. -- first, find the unit most in the heading direction local maxPos = -math.huge - + heading = heading * -1 -- rotated value appears to be opposite of what was expected local maxPosInd -- maxPos - the furthest in direction defined by heading; maxPosInd = for i = 1, #unitPosTbl do local rotatedVec2 = mist.vec.rotateVec2(mist.utils.makeVec2(unitPosTbl[i]), heading) @@ -2985,8 +3592,147 @@ function mist.getLeadingBRString(vars) end end +--[[getPathLength from GSH +-- Returns the length between the defined set of points. Can also return the point index before the cutoff was achieved +p - table of path points, vec2 or vec3 +cutoff - number distance after which to stop at +topo - boolean for if it should get the topographical distance + +]] + +function mist.getPathLength(p, cutoff, topo) + local l = 0 + local cut = 0 or cutOff + local path = {} + + for i = 1, #p do + if topo then + table.insert(path, mist.utils.makeVec3GL(p[i])) + else + table.insert(path, mist.utils.makeVec3(p[i])) + end + end + + for i = 1, #path do + if i + 1 <= #path then + if topo then + l = mist.utils.get3DDist(path[i], path[i+1]) + l + else + l = mist.utils.get2DDist(path[i], path[i+1]) + l + end + end + if cut ~= 0 and l > cut then + return l, i + end + end + return l end +--[[ +Return a series of points to simplify the input table. Best used in conjunction with findPathOnRoads to turn the massive table into a list of X points. +p - table of path points, can be vec2 or vec3 +num - number of segments. +exact - boolean for whether or not it returns the exact distance or uses the first WP to that distance. + + +]] + +function mist.getPathInSegments(p, num, exact) + local tot = mist.getPathLength(p) + local checkDist = tot/num + local typeUsed = 'vec2' + + local points = {[1] = p[1]} + local curDist = 0 + for i = 1, #p do + if i + 1 <= #p then + curDist = mist.utils.get2DDist(p[i], p[i+1]) + curDist + if curDist > checkDist then + curDist = 0 + if exact then + -- get avg point between the two + -- insert into point table + -- need to be accurate... maybe reassign the point for the value it is checking? + -- insert into p table? + else + table.insert(points, p[i]) + end + end + + end + + end + return points + +end + + +function mist.getPointAtDistanceOnPath(p, dist, r, rtn) + log:info('find distance: $1', dist) + local rType = r or 'roads' + local point = {x= 0, y = 0, z = 0} + local path = {} + local ret = rtn or 'vec2' + local l = 0 + if p[1] and #p == 2 then + path = land.findPathOnRoads(rType, p[1].x, p[1].y, p[2].x, p[2].y) + else + path = p + end + for i = 1, #path do + if i + 1 <= #path then + nextPoint = path[i+1] + if topo then + l = mist.utils.get3DDist(path[i], path[i+1]) + l + else + l = mist.utils.get2DDist(path[i], path[i+1]) + l + end + end + if l > dist then + local diff = dist + if i ~= 1 then -- get difference + diff = l - dist + end + local dir = mist.utils.getHeadingPoints(mist.utils.makeVec3(path[i]), mist.utils.makeVec3(path[i+1])) + local x, y + if r then + x, y = land.getClosestPointOnRoads(rType, mist.utils.round((math.cos(dir) * diff) + path[i].x,1), mist.utils.round((math.sin(dir) * diff) + path[i].y,1)) + else + x, y = mist.utils.round((math.cos(dir) * diff) + path[i].x,1), mist.utils.round((math.sin(dir) * diff) + path[i].y,1) + end + + if ret == 'vec2' then + return {x = x, y = y}, dir + elseif ret == 'vec3' then + return {x = x, y = 0, z = y}, dir + end + + return {x = x, y = y}, dir + end + end + log:warn('Find point at distance: $1, path distance $2', dist, l) + return false +end + + +function mist.projectPoint(point, dist, theta) + local newPoint = {} + if point.z then + newPoint.z = mist.utils.round(math.sin(theta) * dist + point.z, 3) + newPoint.y = mist.utils.deepCopy(point.y) + else + newPoint.y = mist.utils.round(math.sin(theta) * dist + point.y, 3) + end + newPoint.x = mist.utils.round(math.cos(theta) * dist + point.x, 3) + + return newPoint +end + +end + + + + --- Group functions. -- @section groups do -- group functions scope @@ -3037,6 +3783,9 @@ do -- group functions scope newData.units = {} local newUnits = newGroup:getUnits() + if #newUnits == 0 then + log:warn('getCurrentGroupData has returned no units for: $1', gpName) + end for unitNum, unitData in pairs(newGroup:getUnits()) do newData.units[unitNum] = {} local uName = unitData:getName() @@ -3074,7 +3823,7 @@ do -- group functions scope end - function mist.getGroupData(gpName) + function mist.getGroupData(gpName, route) local found = false local newData = {} if mist.DBs.groupsByName[gpName] then @@ -3115,14 +3864,17 @@ do -- group functions scope newData.units[unitNum].unitName = unitData.unitName newData.units[unitNum].heading = unitData.heading -- added to DBs newData.units[unitNum].playerCanDrive = unitData.playerCanDrive -- added to DBs - + newData.units[unitNum].livery_id = unitData.livery_id + newData.units[unitNum].AddPropAircraft = unitData.AddPropAircraft + newData.units[unitNum].AddPropVehicle = unitData.AddPropVehicle + if newData.category == 'plane' or newData.category == 'helicopter' then newData.units[unitNum].payload = payloads[unitNum] - newData.units[unitNum].livery_id = unitData.livery_id + newData.units[unitNum].onboard_num = unitData.onboard_num newData.units[unitNum].callsign = unitData.callsign - newData.units[unitNum].AddPropAircraft = unitData.AddPropAircraft + end if newData.category == 'static' then newData.units[unitNum].categoryStatic = unitData.categoryStatic @@ -3132,6 +3884,10 @@ do -- group functions scope end end --log:info(newData) + if route then + newData.route = mist.getGroupRoute(gpName, true) + end + return newData else log:error('$1 not found in MIST database', gpName) @@ -3156,10 +3912,10 @@ do -- group functions scope if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then if coa_data.country then --there is a country table for cntry_id, cntry_data in pairs(coa_data.country) do - for obj_type_name, obj_type_data in pairs(cntry_data) do - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - for group_num, group_data in pairs(obj_type_data.group) do + for obj_cat_name, obj_cat_data in pairs(cntry_data) do + if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" then -- only these types have points + if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then --there's a group! + for group_num, group_data in pairs(obj_cat_data.group) do if group_data and group_data.groupId == gpId then for unitIndex, unitData in pairs(group_data.units) do --group index if unitData.unitId == unitId then @@ -3198,10 +3954,10 @@ do -- group functions scope if type(coa_data) == 'table' then if coa_data.country then --there is a country table for cntry_id, cntry_data in pairs(coa_data.country) do - for obj_type_name, obj_type_data in pairs(cntry_data) do - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - for group_num, group_data in pairs(obj_type_data.group) do + for obj_cat_name, obj_cat_data in pairs(cntry_data) do + if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" then -- only these types have points + if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then --there's a group! + for group_num, group_data in pairs(obj_cat_data.group) do if group_data and group_data.groupId == gpId then local payloads = {} for unitIndex, unitData in pairs(group_data.units) do --group index @@ -3223,16 +3979,53 @@ do -- group functions scope end log:warn("Couldn't find payload for group: $1", groupIdent) return - end + function mist.getGroupTable(groupIdent) + local gpId = groupIdent + if type(groupIdent) == 'string' and not tonumber(groupIdent) then + if mist.DBs.MEgroupsByName[groupIdent] then + gpId = mist.DBs.MEgroupsByName[groupIdent].groupId + else + log:error('$1 not found in mist.DBs.MEgroupsByName', groupIdent) + end + end + + if gpId then + for coa_name, coa_data in pairs(env.mission.coalition) do + if type(coa_data) == 'table' then + if coa_data.country then --there is a country table + for cntry_id, cntry_data in pairs(coa_data.country) do + for obj_cat_name, obj_cat_data in pairs(cntry_data) do + if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" then -- only these types have points + if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then --there's a group! + for group_num, group_data in pairs(obj_cat_data.group) do + if group_data and group_data.groupId == gpId then + return group_data + end + end + end + end + end + end + end + end + end + else + log:error('Need string or number. Got: $1', type(groupIdent)) + return false + end + log:warn("Couldn't find table for group: $1", groupIdent) + + end + function mist.getValidRandomPoint(vars) end function mist.teleportToPoint(vars) -- main teleport function that all of teleport/respawn functions call - --log:info(vars) + --log:warn(vars) local point = vars.point local gpName if vars.gpName then @@ -3243,6 +4036,14 @@ do -- group functions scope log:error('Missing field groupName or gpName in variable table') end + --[[New vars to add, mostly for when called via inZone functions + anyTerrain + offsetWP1 + offsetRoute + initTasks + + ]] + local action = vars.action local disperse = vars.disperse or false @@ -3250,8 +4051,9 @@ do -- group functions scope local radius = vars.radius or 0 local innerRadius = vars.innerRadius - local route = vars.route local dbData = false + + local newGroupData if gpName and not vars.groupData then @@ -3272,18 +4074,33 @@ do -- group functions scope action = 'tele' newGroupData = vars.groupData end + + if vars.newGroupName then + newGroupData.groupName = vars.newGroupName + end + if #newGroupData.units == 0 then + log:warn('$1 has no units in group table', gpName) + return + end + --log:info('get Randomized Point') local diff = {x = 0, y = 0} local newCoord, origCoord local validTerrain = {'LAND', 'ROAD', 'SHALLOW_WATER', 'WATER', 'RUNWAY'} - if string.lower(newGroupData.category) == 'ship' then - validTerrain = {'SHALLOW_WATER' , 'WATER'} - elseif string.lower(newGroupData.category) == 'vehicle' then - validTerrain = {'LAND', 'ROAD'} + if vars.anyTerrain then + -- do nothing + elseif vars.validTerrain then + validTerrain = vars.validTerrain + else + if string.lower(newGroupData.category) == 'ship' then + validTerrain = {'SHALLOW_WATER' , 'WATER'} + elseif string.lower(newGroupData.category) == 'vehicle' then + validTerrain = {'LAND', 'ROAD'} + end end - local offsets = {} + if point and radius >= 0 then local valid = false -- new thoughts @@ -3303,7 +4120,7 @@ do -- group functions scope ---- old for i = 1, 100 do newCoord = mist.getRandPointInCircle(point, radius, innerRadius) - if mist.isTerrainValid(newCoord, validTerrain) then + if vars.anyTerrain or mist.isTerrainValid(newCoord, validTerrain) then origCoord = mist.utils.deepCopy(newCoord) diff = {x = (newCoord.x - newGroupData.units[1].x), y = (newCoord.y - newGroupData.units[1].y)} valid = true @@ -3381,20 +4198,46 @@ do -- group functions scope end - if route then - newGroupData.route = route - end - --log:info(newGroupData) + + local tempRoute + + if mist.DBs.MEgroupsByName[gpName] and not vars.route then + -- log:warn('getRoute') + tempRoute = mist.getGroupRoute(gpName, true) + elseif vars.route then + -- log:warn('routeExist') + tempRoute = mist.utils.deepCopy(vars.route) + end + -- log:warn(tempRoute) + if tempRoute then + if (vars.offsetRoute or vars.offsetWP1 or vars.initTasks) then + for i = 1, #tempRoute do + -- log:warn(i) + if (vars.offsetRoute) or (i == 1 and vars.offsetWP1) or (i == 1 and vars.initTasks) then + -- log:warn('update offset') + tempRoute[i].x = tempRoute[i].x + diff.x + tempRoute[i].y = tempRoute[i].y + diff.y + elseif vars.initTasks and i > 1 then + --log:warn('deleteWP') + tempRoute[i] = nil + end + end + end + newGroupData.route = tempRoute + end + + + --log:warn(newGroupData) --mist.debug.writeData(mist.utils.serialize,{'teleportToPoint', newGroupData}, 'newGroupData.lua') if string.lower(newGroupData.category) == 'static' then - --log:info(newGroupData) + --log:warn(newGroupData) return mist.dynAddStatic(newGroupData) end return mist.dynAdd(newGroupData) end - function mist.respawnInZone(gpName, zone, disperse, maxDisp) + function mist.respawnInZone(gpName, zone, disperse, maxDisp, v) if type(gpName) == 'table' and gpName:getName() then gpName = gpName:getName() @@ -3405,9 +4248,9 @@ do -- group functions scope end if type(zone) == 'string' then - zone = trigger.misc.getZone(zone) - elseif type(zone) == 'table' and not zone.radius then - zone = trigger.misc.getZone(zone[math.random(1, #zone)]) + zone = mist.DBs.zonesByName[zone] + elseif type(zone) == 'table' and not zone.radius then + zone = mist.DBs.zonesByName[zone[math.random(1, #zone)]] end local vars = {} vars.gpName = gpName @@ -3416,10 +4259,17 @@ do -- group functions scope vars.radius = zone.radius vars.disperse = disperse vars.maxDisp = maxDisp + + if v and type(v) == 'table' then + for index, val in pairs(v) do + vars[index] = val + end + end + return mist.teleportToPoint(vars) end - function mist.cloneInZone(gpName, zone, disperse, maxDisp) + function mist.cloneInZone(gpName, zone, disperse, maxDisp, v) --log:info('cloneInZone') if type(gpName) == 'table' then gpName = gpName:getName() @@ -3428,9 +4278,9 @@ do -- group functions scope end if type(zone) == 'string' then - zone = trigger.misc.getZone(zone) - elseif type(zone) == 'table' and not zone.radius then - zone = trigger.misc.getZone(zone[math.random(1, #zone)]) + zone = mist.DBs.zonesByName[zone] + elseif type(zone) == 'table' and not zone.radius then + zone = mist.DBs.zonesByName[zone[math.random(1, #zone)]] end local vars = {} vars.gpName = gpName @@ -3440,10 +4290,15 @@ do -- group functions scope vars.disperse = disperse vars.maxDisp = maxDisp --log:info('do teleport') + if v and type(v) == 'table' then + for index, val in pairs(v) do + vars[index] = val + end + end return mist.teleportToPoint(vars) end - function mist.teleportInZone(gpName, zone, disperse, maxDisp) -- groupName, zoneName or table of Zone Names, keepForm is a boolean + function mist.teleportInZone(gpName, zone, disperse, maxDisp, v) -- groupName, zoneName or table of Zone Names, keepForm is a boolean if type(gpName) == 'table' and gpName:getName() then gpName = gpName:getName() else @@ -3451,9 +4306,9 @@ do -- group functions scope end if type(zone) == 'string' then - zone = trigger.misc.getZone(zone) - elseif type(zone) == 'table' and not zone.radius then - zone = trigger.misc.getZone(zone[math.random(1, #zone)]) + zone = mist.DBs.zonesByName[zone] + elseif type(zone) == 'table' and not zone.radius then + zone = mist.DBs.zonesByName[zone[math.random(1, #zone)]] end local vars = {} @@ -3463,6 +4318,11 @@ do -- group functions scope vars.radius = zone.radius vars.disperse = disperse vars.maxDisp = maxDisp + if v and type(v) == 'table' then + for index, val in pairs(v) do + vars[index] = val + end + end return mist.teleportToPoint(vars) end @@ -3688,14 +4548,20 @@ do -- group functions scope end return choices[rtnVal] end + + function mist.stringCondense(s) + local exclude = {'%-', '%(', '%)', '%_', '%[', '%]', '%.', '%#', '% ', '%{', '%}', '%$', '%%', '%?', '%+', '%^'} + for i , str in pairs(exclude) do + s = string.gsub(s, str, '') + end + return s + end function mist.stringMatch(s1, s2, bool) - local exclude = {'%-', '%(', '%)', '%_', '%[', '%]', '%.', '%#', '% ', '%{', '%}', '%$', '%%', '%?', '%+', '%^'} + if type(s1) == 'string' and type(s2) == 'string' then - for i , str in pairs(exclude) do - s1 = string.gsub(s1, str, '') - s2 = string.gsub(s2, str, '') - end + s1 = mist.stringCondense(s1) + s2 = mist.stringCondense(s2) if not bool then s1 = string.lower(s1) s2 = string.lower(s2) @@ -3823,6 +4689,25 @@ do -- mist.util scope function mist.utils.celsiusToFahrenheit(c) return c*(9/5)+32 end + + function mist.utils.hexToRGB(hex, l) -- because for some reason the draw tools use hex when everything is rgba 0 - 1 + local int = 255 + if l then + int = 1 + end + if hex and type(hex) == 'string' then + local val = {} + hex = string.gsub(hex, '0x', '') + if string.len(hex) == 8 then + val[1] = tonumber("0x"..hex:sub(1,2)) / int + val[2] = tonumber("0x"..hex:sub(3,4)) / int + val[3] = tonumber("0x"..hex:sub(5,6)) / int + val[4] = tonumber("0x"..hex:sub(7,8)) / int + + return val + end + end + end function mist.utils.converter(t1, t2, val) if type(t1) == 'string' then @@ -4024,7 +4909,7 @@ do -- mist.util scope --- Returns the center of a zone as Vec3. -- @tparam string|table zone trigger zone name or table -- @treturn Vec3 center of the zone - function mist.utils.zoneToVec3(zone) + function mist.utils.zoneToVec3(zone, gl) local new = {} if type(zone) == 'table' then if zone.point then @@ -4032,7 +4917,7 @@ do -- mist.util scope new.y = zone.point.y new.z = zone.point.z elseif zone.x and zone.y and zone.z then - return zone + new = mist.utils.deepCopy(zone) end return new elseif type(zone) == 'string' then @@ -4041,11 +4926,21 @@ do -- mist.util scope new.x = zone.point.x new.y = zone.point.y new.z = zone.point.z - return new end end + if new.x and gl then + new.y = land.getHeight({x = new.x, y = new.z}) + end + return new end + function mist.utils.getHeadingPoints(point1, point2, north) -- sick of writing this out. + if north then + return mist.utils.getDir(mist.vec.sub(mist.utils.makeVec3(point2), mist.utils.makeVec3(point1)), (mist.utils.makeVec3(point1))) + else + return mist.utils.getDir(mist.vec.sub(mist.utils.makeVec3(point2), mist.utils.makeVec3(point1))) + end + end --- Returns heading-error corrected direction. -- True-north corrected direction from point along vector vec. -- @tparam Vec3 vec @@ -4067,6 +4962,12 @@ do -- mist.util scope -- @tparam Vec2|Vec3 point2 second point -- @treturn number distance between given points. function mist.utils.get2DDist(point1, point2) + if not point1 then + log:warn("mist.utils.get2DDist 1st input value is nil") + end + if not point2 then + log:warn("mist.utils.get2DDist 2nd input value is nil") + end point1 = mist.utils.makeVec3(point1) point2 = mist.utils.makeVec3(point2) return mist.vec.mag({x = point1.x - point2.x, y = 0, z = point1.z - point2.z}) @@ -4077,6 +4978,12 @@ do -- mist.util scope -- @tparam Vec3 point2 second point -- @treturn number distancen between given points in 3D space. function mist.utils.get3DDist(point1, point2) + if not point1 then + log:warn("mist.utils.get2DDist 1st input value is nil") + end + if not point2 then + log:warn("mist.utils.get2DDist 2nd input value is nil") + end return mist.vec.mag({x = point1.x - point2.x, y = point1.y - point2.y, z = point1.z - point2.z}) end @@ -4507,6 +5414,23 @@ end do -- mist.debug scope mist.debug = {} + function mist.debug.changeSetting(s) + if type(s) == 'table' then + for sName, sVal in pairs(s) do + if type(sVal) == 'string' or type(sVal) == 'number' then + if sName == 'log' then + mistSettings[sName] = sVal + mist.log:setLevel(sVal) + elseif sName == 'dbLog' then + mistSettings[sName] = sVal + dblog:setLevel(sVal) + end + else + mistSettings[sName] = sVal + end + end + end + end --- Dumps the global table _G. -- This dumps the global table _G to a file in -- the DCS\Logs directory. @@ -4514,11 +5438,24 @@ do -- mist.debug scope -- in $DCS_ROOT\Scripts\MissionScripting.lua to access lfs and io -- libraries. -- @param fname - function mist.debug.dump_G(fname) + function mist.debug.dump_G(fname, simp) if lfs and io then local fdir = lfs.writedir() .. [[Logs\]] .. fname local f = io.open(fdir, 'w') - f:write(mist.utils.tableShow(_G)) + if simp then + local g = mist.utils.deepCopy(_G) + g.mist = nil + g.slmod = nil + g.env.mission = nil + g.env.warehouses = nil + g.country.by_idx = nil + g.country.by_country = nil + + f:write(mist.utils.tableShow(g)) + else + + f:write(mist.utils.tableShow(_G)) + end f:close() log:info('Wrote debug data to $1', fdir) --trigger.action.outText(errmsg, 10) @@ -4562,6 +5499,141 @@ do -- mist.debug scope end end end + + -- write group table + function mist.debug.writeGroup(gName, data) + if gName and mist.DBs.groupsByName[gName] then + local dat + if data then + dat = mist.getGroupData(gName) + else + dat = mist.getGroupTable(gName) + end + if dat then + dat.route = {points = mist.getGroupRoute(gName, true)} + end + + if io and lfs and dat then + mist.debug.writeData(mist.utils.serialize, {gName, dat}, gName .. '_table.lua') + else + if dat then + trigger.action.outText('Error: insufficient libraries to run mist.debug.writeGroup, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua \nGroup table written to DCS.log file instead.', 10) + log:warn('$1 dataTable: $2', gName, dat) + else + trigger.action.outText('Unable to write group table for: ' .. gName .. '\n Error: insufficient libraries to run mist.debug.writeGroup, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua', 10) + end + end + end + end + + + + -- write all object types in mission. + function mist.debug.writeTypes(fName) + local wt = 'mistDebugWriteTypes.lua' + if fName and type(fName) == 'string' and string.find(fName, '.lua') then + wt = fName + end + local output = {units = {}, countries = {}} + for coa_name_miz, coa_data in pairs(env.mission.coalition) do + if type(coa_data) == 'table' then + if coa_data.country then --there is a country table + for cntry_id, cntry_data in pairs(coa_data.country) do + local countryName = string.lower(cntry_data.name) + if cntry_data.id and country.names[cntry_data.id] then + countryName = string.lower(country.names[cntry_data.id]) + end + output.countries[countryName] = {} + if type(cntry_data) == 'table' then --just making sure + for obj_cat_name, obj_cat_data in pairs(cntry_data) do + if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" or obj_cat_name == "static" then --should be an unncessary check + local category = obj_cat_name + if not output.countries[countryName][category] then + -- log:warn('Create: $1', category) + output.countries[countryName][category] = {} + end + if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then --there's a group! + for group_num, group_data in pairs(obj_cat_data.group) do + if group_data and group_data.units and type(group_data.units) == 'table' then --making sure again- this is a valid group + for i = 1, #group_data.units do + if group_data.units[i] then + local u = group_data.units[i] + local liv = u.livery_id or 'default' + if not output.units[u.type] then -- create unit table + -- log:warn('Create: $1', u.type) + output.units[u.type] = {count = 0, livery_id = {}} + end + + if not output.countries[countryName][category][u.type] then + -- log:warn('Create country, category, unit: $1', u.type) + output.countries[countryName][category][u.type] = 0 + end + -- add to count + output.countries[countryName][category][u.type] = output.countries[countryName][category][u.type] + 1 + output.units[u.type].count = output.units[u.type].count + 1 + + if liv and not output.units[u.type].livery_id[countryName] then + -- log:warn('Create livery country: $1', countryName) + output.units[u.type].livery_id[countryName] = {} + end + if liv and not output.units[u.type].livery_id[countryName][liv] then + --log:warn('Create Livery: $1', liv) + output.units[u.type].livery_id[countryName][liv] = 0 + end + if liv then + output.units[u.type].livery_id[countryName][liv] = output.units[u.type].livery_id[countryName][liv] + 1 + end + if u.payload and u.payload.pylons then + if not output.units[u.type].CLSID then + output.units[u.type].CLSID = {} + output.units[u.type].pylons = {} + end + + for pyIndex, pData in pairs(u.payload.pylons) do + if not output.units[u.type].CLSID[pData.CLSID] then + output.units[u.type].CLSID[pData.CLSID] = 0 + end + output.units[u.type].CLSID[pData.CLSID] = output.units[u.type].CLSID[pData.CLSID] + 1 + + if not output.units[u.type].pylons[pyIndex] then + output.units[u.type].pylons[pyIndex] = {} + end + if not output.units[u.type].pylons[pyIndex][pData.CLSID] then + output.units[u.type].pylons[pyIndex][pData.CLSID] = 0 + end + output.units[u.type].pylons[pyIndex][pData.CLSID] = output.units[u.type].pylons[pyIndex][pData.CLSID] + 1 + end + + end + end + end + end + end + end + end + end + end + end + end + end + end + if io and lfs then + mist.debug.writeData(mist.utils.serialize, {'mistDebugWriteTypes', output}, wt) + else + trigger.action.outText('Error: insufficient libraries to run mist.debug.writeTypes, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua \n writeTypes table written to DCS.log file instead.', 10) + log:warn('mist.debug.writeTypes: $1', output) + end + return output + end + function mist.debug.writeWeapons(unit) + + end + + function mist.debug.mark(msg, coord) + + mist.marker.add({point = coord, text = msg}) + log:warn('debug.mark: $1 $2', msg, coord) + end end --- 3D Vector functions @@ -4633,6 +5705,13 @@ do -- mist.vec scope function mist.vec.rotateVec2(vec2, theta) return { x = vec2.x*math.cos(theta) - vec2.y*math.sin(theta), y = vec2.x*math.sin(theta) + vec2.y*math.cos(theta)} end + + function mist.vec.normalize(vec3) + local mag = mist.vec.mag(vec3) + if mag ~= 0 then + return mist.vec.scalar_mult(vec3, 1.0 / mag) + end + end end --- Flag functions. @@ -4826,7 +5905,7 @@ unitTableDef = table or nil if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == 0) then local num_in_zone = 0 for i = 1, #units do - local unit = Unit.getByName(units[i]) + local unit = Unit.getByName(units[i]) or StaticObject.getByName(units[i]) if unit then local pos = unit:getPosition().p if mist.pointInPolygon(pos, zone, maxalt) then @@ -4913,7 +5992,13 @@ unitTableDef = table or nil end end - + --[[ + function mist.flagFunc.weapon_in_zones(vars) + -- borrow from suchoi surprise. While running enabled event handler that checks for weapons in zone. + -- Choice is weapon category or weapon strings. + + end +]] --- Sets a flag if unit(s) is/are inside a moving zone. -- @todo document function mist.flagFunc.units_in_moving_zones(vars) @@ -5282,13 +6367,15 @@ do -- mist.msg scope local caSlots = false local caMSGtoGroup = false + local anyUpdate = false + local lastMessageTime = nil if env.mission.groundControl then -- just to be sure? for index, value in pairs(env.mission.groundControl) do if type(value) == 'table' then for roleName, roleVal in pairs(value) do for rIndex, rVal in pairs(roleVal) do - if env.mission.groundControl[index][roleName][rIndex] > 0 then + if type(rVal) == 'number' and rVal > 0 then caSlots = true break end @@ -5303,11 +6390,124 @@ do -- mist.msg scope end local function mistdisplayV5() - --[[thoughts to improve upon - event handler based activeClients table. - display messages only when there is an update - possibly co-routine it. - ]] + --log:warn("mistdisplayV5: $1", timer.getTime()) + + local clearView = true + if #messageList > 0 then + --log:warn('Updates: $1', anyUpdate) + if anyUpdate == true then + local activeClients = {} + + for clientId, clientData in pairs(mist.DBs.humansById) do + if Unit.getByName(clientData.unitName) and Unit.getByName(clientData.unitName):isExist() == true then + activeClients[clientData.groupId] = clientData.groupName + end + end + anyUpdate = false + if displayActive == false then + displayActive = true + end + --mist.debug.writeData(mist.utils.serialize,{'msg', messageList}, 'messageList.lua') + local msgTableText = {} + local msgTableSound = {} + local curTime = timer.getTime() + for mInd, messageData in pairs(messageList) do + --log:warn(messageData) + if messageData.displayTill < curTime then + messageData:remove() -- now using the remove/destroy function. + else + if messageData.displayedFor then + messageData.displayedFor = curTime - messageData.addedAt + end + local nextSound = 1000 + local soundIndex = 0 + + if messageData.multSound and #messageData.multSound > 0 then + for index, sData in pairs(messageData.multSound) do + if sData.time <= messageData.displayedFor and sData.played == false and sData.time < nextSound then -- find index of the next sound to be played + nextSound = sData.time + soundIndex = index + end + end + if soundIndex ~= 0 then + messageData.multSound[soundIndex].played = true + end + end + + for recIndex, recData in pairs(messageData.msgFor) do -- iterate recipiants + if recData == 'RED' or recData == 'BLUE' or activeClients[recData] then -- rec exists + if messageData.text then -- text + if not msgTableText[recData] then -- create table entry for text + msgTableText[recData] = {} + msgTableText[recData].text = {} + if recData == 'RED' or recData == 'BLUE' then + msgTableText[recData].text[1] = '-------Combined Arms Message-------- \n' + end + msgTableText[recData].text[#msgTableText[recData].text + 1] = messageData.text + msgTableText[recData].displayTime = messageData.displayTime - messageData.displayedFor + else -- add to table entry and adjust display time if needed + if recData == 'RED' or recData == 'BLUE' then + msgTableText[recData].text[#msgTableText[recData].text + 1] = '\n ---------------- Combined Arms Message: \n' + else + msgTableText[recData].text[#msgTableText[recData].text + 1] = '\n ---------------- \n' + end + table.insert(msgTableText[recData].text, messageData.text) + if msgTableText[recData].displayTime < messageData.displayTime - messageData.displayedFor then + msgTableText[recData].displayTime = messageData.displayTime - messageData.displayedFor + else + --msgTableText[recData].displayTime = 10 + end + end + end + if soundIndex ~= 0 then + msgTableSound[recData] = messageData.multSound[soundIndex].file + end + end + + end + messageData.update = nil + + end + + end + ------- new display + + if caSlots == true and caMSGtoGroup == false then + if msgTableText.RED then + trigger.action.outTextForCoalition(coalition.side.RED, table.concat(msgTableText.RED.text), msgTableText.RED.displayTime, clearView) + + end + if msgTableText.BLUE then + trigger.action.outTextForCoalition(coalition.side.BLUE, table.concat(msgTableText.BLUE.text), msgTableText.BLUE.displayTime, clearView) + end + end + + for index, msgData in pairs(msgTableText) do + if type(index) == 'number' then -- its a groupNumber + trigger.action.outTextForGroup(index, table.concat(msgData.text), msgData.displayTime, clearView) + end + end + --- new audio + if msgTableSound.RED then + trigger.action.outSoundForCoalition(coalition.side.RED, msgTableSound.RED) + end + if msgTableSound.BLUE then + trigger.action.outSoundForCoalition(coalition.side.BLUE, msgTableSound.BLUE) + end + + + for index, file in pairs(msgTableSound) do + if type(index) == 'number' then -- its a groupNumber + trigger.action.outSoundForGroup(index, file) + end + end + + end + + else + mist.removeFunction(displayFuncId) + displayActive = false + end end local function mistdisplayV4() @@ -5468,14 +6668,15 @@ end]] ]] - + local new = {} new.text = vars.text -- The actual message new.displayTime = vars.displayTime -- How long will the message appear for new.displayedFor = 0 -- how long the message has been displayed so far + new.displayTill = timer.getTime() + vars.displayTime new.name = vars.name -- ID to overwrite the older message (if it exists) Basically it replaces a message that is displayed with new text. new.addedAt = timer.getTime() - new.update = true + --log:warn('New Message: $1', new.text) if vars.multSound and vars.multSound[1] then new.multSound = vars.multSound @@ -5560,19 +6761,21 @@ end]] if messageList[i].name then if messageList[i].name == vars.name then --log:info('updateMessage') + messageList[i].displayTill = timer.getTime() + messageList[i].displayTime messageList[i].displayedFor = 0 messageList[i].addedAt = timer.getTime() messageList[i].sound = new.sound messageList[i].text = new.text messageList[i].msgFor = new.msgFor messageList[i].multSound = new.multSound - messageList[i].update = true + anyUpdate = true + --log:warn('Message updated: $1', new.messageID) return messageList[i].messageID end end end end - + anyUpdate = true messageID = messageID + 1 new.messageID = messageID @@ -5586,7 +6789,7 @@ end]] if displayActive == false then displayActive = true - displayFuncId = mist.scheduleFunction(mistdisplayV4, {}, timer.getTime() + messageDisplayRate, messageDisplayRate) + displayFuncId = mist.scheduleFunction(mistdisplayV5, {}, timer.getTime() + messageDisplayRate, messageDisplayRate) end return messageID @@ -5597,6 +6800,7 @@ end]] for i, msgData in pairs(messageList) do if messageList[i] == self then table.remove(messageList, i) + anyUpdate = true return true --removal successful end end @@ -5607,6 +6811,7 @@ end]] for i, msgData in pairs(messageList) do if messageList[i].messageID == id then table.remove(messageList, i) + anyUpdate = true return true --removal successful end end @@ -6007,6 +7212,9 @@ do -- mist.demos scope end end + + + do --[[ stuff for marker panels marker.add() add marker. Point of these functions is to simplify process and to store all mark panels added. @@ -6026,98 +7234,382 @@ do If mark added to a group before a client joins slot is synced. Mark made for cliet A in Slot A. Client A leaves, Client B joins in slot A. What do they see? + May need to automate process... + + + Could release this. But things I might need to add/change before doing so. + - removing marks and re-adding in same sequence doesn't appear to work. May need to schedule adding mark if updating an entry. + - I really dont like the old message style code for which groups get the message. Perhaps change to unitsTable and create function for getting humanUnitsTable. + = Event Handler, and check it, for marks added via script or user to deconflict Ids. + - Full validation of passed values for a specific shape type. ]] - --[[ - local typeBase = { - ['Mi-8MT'] = {'Mi-8MTV2', 'Mi-8MTV', 'Mi-8'}, - ['MiG-21Bis'] = {'Mig-21'}, - ['MiG-15bis'] = {'Mig-15'}, - ['FW-190D9'] = {'FW-190'}, - ['Bf-109K-4'] = {'Bf-109'}, - } - - - local mId = 1337 + + local usedMarks = {} + + local mDefs = { + coa = { + ['red'] = {fillColor = {.8, 0 , 0, .5}, color = {.8, 0 , 0, .5}, lineType = 2, fontSize = 16}, + ['blue'] = {fillColor = {0, 0 , 0.8, .5}, color = {0, 0 , 0.8, .5}, lineType = 2, fontSize = 16}, + ['all'] = {fillColor = {.1, .1 , .1, .5}, color = {.9, .9 , .9, .5}, lineType = 2, fontSize = 16}, + ['neutral'] = {fillColor = {.1, .1 , .1, .5}, color = {.2, .2 , .2, .5}, lineType = 2, fontSize = 16}, + }, + } + + local userDefs = {['red'] = {},['blue'] = {},['all'] = {},['neutral'] = {}} + local mId = 1000 + + local tNames = {'line', 'circle','rect', 'arrow', 'text', 'quad', 'freeform'} + local tLines = {[0] = 'no line', [1] = 'solid', [2] = 'dashed',[3] = 'dotted', [4] = 'dot dash' ,[5] = 'long dash', [6] = 'two dash'} + local coas = {[-1] = 'all', [0] = 'neutral', [1] = 'red', [2] = 'blue'} + + local altNames = {['poly'] = 7, ['lines'] = 1, ['polygon'] = 7 } + + local function draw(s) + --log:warn(s) + if type(s) == 'table' then + local mType = s.markType + if mType == 'panel' then + if markScope == 'coa' then + trigger.action.markToCoalition(s.markId, s.text, s.pos, s.markFor, s.readOnly) + elseif markScope == 'group' then + trigger.action.markToGroup(s.markId, s.text, s.pos, s.markFor, s.readOnly) + else + trigger.action.markToAll(s.markId, s.text, s.pos, s.readOnly) + end + elseif mType == 'line' then + trigger.action.lineToAll(s.coa, s.markId, s.pos[1], s.pos[2], s.color, s.fillColor, s.lineType, s.readOnly, s.message) + elseif mType == 'circle' then + trigger.action.circleToAll(s.coa, s.markId, s.pos[1], s.radius, s.color, s.fillColor, s.lineType, s.readOnly, s.message) + elseif mType == 'rect' then + trigger.action.rectToAll(s.coa, s.markId, s.pos[1], s.pos[2], s.color, s.fillColor, s.lineType, s.readOnly, s.message) + elseif mType == 'arrow' then + trigger.action.arrowToAll(s.coa, s.markId, s.pos[1], s.pos[2], s.color, s.fillColor, s.lineType, s.readOnly, s.message) + elseif mType == 'text' then + trigger.action.textToAll(s.coa, s.markId, s.pos[1], s.color, s.fillColor, s.fontSize, s.readOnly, s.text) + elseif mType == 'quad' then + trigger.action.quadToAll(s.coa, s.markId, s.pos[1], s.pos[2], s.pos[3], s.pos[4], s.color, s.fillColor, s.lineType, s.readOnly, s.message) + end + if s.name and not usedMarks[s.name] then + usedMarks[s.name] = s.markId + end + elseif type(s) == 'string' then + --log:warn(s) + mist.utils.dostring(s) + end + end + mist.marker = {} - mist.marker.list = {} + local function markSpamFilter(recList, spamBlockOn) for id, name in pairs(recList) do if name == spamBlockOn then - log:info('already on recList') + --log:info('already on recList') return recList end end - log:info('add to recList') + --log:info('add to recList') table.insert(recList, spamBlockOn) return recList end local function iterate() - mId = mId + 1 - return mId + while mId < 10000000 do + if usedMarks[mId] then + mId = mId + 1 + else + return mist.utils.deepCopy(mId) + end + end + return mist.utils.deepCopy(mId) end + + local function validateColor(val) + if type(val) == 'table' then + for i = 1, #val do + if type(val[i]) == 'number' and val[i] > 1 then + val[i] = val[i]/255 -- convert RGB values from 0-255 to 0-1 equivilent. + end + end + elseif type(val) == 'string' then + val = mist.utils.hexToRGB(val) + + end + return val + end + + local function checkDefs(vName, coa) + --log:warn('CheckDefs: $1 $2', vName, coa) + local coaName + if type(coa) == 'number' then + if coas[coa] then + coaName = coas[coa] + end + elseif type(coa) == 'string' then + coaName = coa + end + + -- log:warn(coaName) + if userDefs[coaName] and userDefs[coaName][vName] then + return userDefs[coaName][vName] + elseif mDefs.coa[coaName] and mDefs.coa[coaName][vName] then + return mDefs.coa[coaName][vName] + end + + end + + function mist.marker.getNextId() + return iterate() + end + + local handle = {} + function handle:onEvent(e) + if world.event.S_EVENT_MARK_ADDED == e.id and e.idx then + usedMarks[e.idx] = e.idx + if not mist.DBs.markList[e.idx] then + --log:info('create maker DB: $1', e.idx) + mist.DBs.markList[e.idx] = {time = e.time, pos = e.pos, groupId = e.groupId, mType = 'panel', text = e.text, markId = e.idx, coalition = e.coalition} + if e.unit then + mist.DBs.markList[e.idx].unit = e.initiaor:getName() + end + --log:info(mist.marker.list[e.idx]) + end + + elseif world.event.S_EVENT_MARK_CHANGE == e.id and e.idx then + if mist.DBs.markList[e.idx] then + mist.DBs.markList[e.idx].text = e.text + end + elseif world.event.S_EVENT_MARK_REMOVE == e.id and e.idx then + if mist.DBs.markList[e.idx] then + mist.DBs.markList[e.idx] = nil + end + end + + end + + local function getMarkId(id) + if mist.DBs.markList[id] then + return id + else + for mEntry, mData in pairs(mist.DBs.markList) do + if id == mData.name or id == mData.id then + return mData.id + end + end + end + + + end + + + local function removeMark(id) + --log:info("Removing Mark: $1", id + local removed = false + if type(id) == 'table' then + for ind, val in pairs(id) do + local r = getMarkId(val) + if r then + trigger.action.removeMark(r) + mist.DBs.markList[r] = nil + removed = true + end + end + + else + local r = getMarkId(id) + trigger.action.removeMark(r) + mist.DBs.markList[r] = nil + removed = true + end + return removed + end + + world.addEventHandler(handle) + function mist.marker.setDefault(vars) + local anyChange = false + if vars and type(vars) == 'table' then + for l1, l1Data in pairs(vars) do + if type(l1Data) == 'table' then + if not userDefs[l1] then + userDefs[l1] = {} + end + + for l2, l2Data in pairs(l1Data) do + userDefs[l1][l2] = l2Data + anyChange = true + end + else + userDefs[l1] = l1Data + anyChange = true + end + end + + end + return anyChange + end - function mist.marker.add(pos, text, markFor, id) - log:warn('markerFunc') - log:info('Pos: $1, Text: $2, markFor: $3, id: $4', pos, text, markFor, id) - if not id then - - else + function mist.marker.add(vars) + --log:warn('markerFunc') + --log:warn(vars) + local pos = vars.point or vars.points or vars.pos + local text = vars.text or '' + local markFor = vars.markFor + local markForCoa = vars.markForCoa or vars.coa -- optional, can be used if you just want to mark to a specific coa/all + local id = vars.id or vars.markId or vars.markid + local mType = vars.mType or vars.markType or vars.type or 0 + local color = vars.color + local fillColor = vars.fillColor + local lineType = vars.lineType or 2 + local readOnly = vars.readOnly or true + local message = vars.message + local fontSize = vars.fontSize + local name = vars.name + local radius = vars.radius or 500 + + local coa = -1 + local usedId = 0 + + + if id then + if type(id) ~= 'number' then + name = id + usedId = iterate() + end + --log:info('checkIfIdExist: $1', id) + --[[ + Maybe it should treat id or name as the same thing/single value. + + If passed number it will use that as the first Id used and will delete/update any marks associated with that same value. + + + ]] + + local lId = id or name + if mist.DBs.markList[id] then ---------- NEED A BETTER WAY TO ASSOCIATE THE ID VALUE. CUrrnetly deleting from table and checking if that deleted entry exists which it wont. + --log:warn('active mark to be removed: $1', id) + name = mist.DBs.markList[id].name or id + removeMark(id) + elseif usedMarks[id] then + --log:info('exists in usedMarks: $1', id) + removeMark(usedMarks[id]) + elseif name and usedMarks[name] then + --log:info('exists in usedMarks: $1', name) + removeMark(usedMarks[name]) + end + usedId = iterate() + usedMarks[id] = usedId -- redefine the value used end - local markType = 'all' + if name then + usedMarks[name] = usedId + end + + if usedId == 0 then + usedId = iterate() + end + if mType then + if type(mType) == 'string' then + for i = 1, #tNames do + --log:warn(tNames[i]) + if mist.stringMatch(mType, tNames[i]) then + mType = i + break + end + end + elseif type(mType) == 'number' and mType > #tNames then + mType = 0 + end + end + --log:warn(mType) + local markScope = 'all' local markForTable = {} - if pos then - pos = mist.utils.makeVec3(pos) + + if pos then + if pos[1] then + for i = 1, #pos do + pos[i] = mist.utils.makeVec3(pos[i]) + end + + else + pos[1] = mist.utils.makeVec3(pos) + end + end if text and type(text) ~= string then text = tostring(text) - else - text = '' end - - if markFor then + + if markForCoa then + if type(markForCoa) == 'string' then + if tonumber(markForCoa) then + coa = coas[tonumber(markForCoa)] + markScope = 'coa' + else + for ind, cName in pairs(coas) do + if mist.stringMatch(cName, markForCoa) then + coa = ind + markScope = 'coa' + break + end + end + end + elseif type(markForCoa) == 'number' and markForCoa >=-1 and markForCoa <= #coas then + coa = markForCoa + markScore = 'coa' + end + + + + elseif markFor then if type(markFor) == 'number' then -- groupId if mist.DBs.groupsById[markFor] then - markType = 'group' + markScope = 'group' end elseif type(markFor) == 'string' then -- groupName if mist.DBs.groupsByName[markFor] then - markType = 'group' + markScope = 'group' markFor = mist.DBs.groupsByName[markFor].groupId end elseif type(markFor) == 'table' then -- multiple groupName, country, coalition, all - markType = 'table' - log:info(markFor) + markScope = 'table' + --log:warn(markFor) for forIndex, forData in pairs(markFor) do -- need to rethink this part and organization. Gotta be a more logical way to send messages to coa, groups, or all. - log:info(forIndex) - log:info(forData) for list, listData in pairs(forData) do - log:info(listData) + --log:warn(listData) forIndex = string.lower(forIndex) if type(listData) == 'string' then listData = string.lower(listData) end if listData == 'all' then - markType = 'all' + markScope = 'all' break elseif (forIndex == 'coa' or forIndex == 'ca') then -- mark for coa or CA. - for name, index in pairs (coalition.side) do + local matches = 0 + for name, index in pairs (coalition.side) do if listData == string.lower(name) then - markType = 'coalition' + markScope = 'coa' + markFor = index + coa = index + matches = matches + 1 end end - elseif (forIndex == 'countries' and string.lower(clientData.country) == listData) or (forIndex == 'units' and string.lower(clientData.unitName) == listData) then - markForTable = markSpamFilter(markForTable, clientData.groupId) + if matches > 1 then + markScope = 'all' + end + elseif forIndex == 'countries' then + for clienId, clientData in pairs(mist.DBs.humansById) do + if (string.lower(clientData.country) == listData) or (forIndex == 'units' and string.lower(clientData.unitName) == listData) then + markForTable = markSpamFilter(markForTable, clientData.groupId) + end + end elseif forIndex == 'unittypes' then -- mark to group -- iterate play units for clientId, clientData in pairs(mist.DBs.humansById) do for typeId, typeData in pairs(listData) do - log:info(typeData) + --log:warn(typeData) local found = false if list == 'all' or clientData.coalition and type(clientData.coalition) == 'string' and mist.stringMatch(clientData.coalition, list) then if mist.matchString(typeData, clientData.type) then @@ -6160,61 +7652,268 @@ do end end else - markType = 'all' + markScope = 'all' end + if mType == 0 then + local data = {markId = usedId, text = text, pos = pos[1], markScope = markScope, markFor = markFor, markType = 'panel', name = name, time = timer.getTime()} + if markScope ~= 'table' then + -- create marks + + mist.DBs.markList[usedId] = data-- add to the DB + + else + if #markForTable > 0 then + --log:info('iterate') + local list = {} + if id and not name then + name = id + end + for i = 1, #markForTable do + local newId = iterate() + local data = {markId = newId, text = text, pos = pos[i], markFor = markForTable[i], markType = 'panel', name = name, readOnly = readOnly, time = timer.getTime()} + mist.DBs.markList[newId] = data + table.insert(list, data) - + draw(data) + + end + return list + end + end - - - if markType ~= 'table' then - local newId = iterate() - local data = {markId = newId, text = text, pos = pos, markType = markType, markFor = markFor} + draw(data) + + return data + elseif mType > 0 then + local newId = iterate() + local fCal = {} + fCal[#fCal+1] = mType + fCal[#fCal+1] = coa + fCal[#fCal+1] = usedId + + local likeARainCoat = false + if mType == 7 then + local score = 0 + for i = 1, #pos do + if i < #pos then + local val = ((pos[i+1].x - pos[i].x)*(pos[i+1].z + pos[i].z)) + --log:warn("$1 index score is: $2", i, val) + score = score + val + else + score = score + ((pos[1].x - pos[i].x)*(pos[1].z + pos[i].z)) + end + end + --log:warn(score) + if score > 0 then -- it is anti-clockwise. Due to DCS bug make it clockwise. + likeARainCoat = true + --log:warn('flip') + + for i = #pos, 1, -1 do + fCal[#fCal+1] = pos[i] + end + end + end + if likeARainCoat == false then + for i = 1, #pos do + fCal[#fCal+1] = pos[i] + end + end + if radius and mType == 2 then + fCal[#fCal+1] = radius + end + + if not color then + color = checkDefs('color', coa) + else + color = validateColor(color) + end + fCal[#fCal+1] = color + + + if not fillColor then + fillColor = checkDefs('fillColor', coa) + else + fillColor = validateColor(fillColor) + end + fCal[#fCal+1] = fillColor + + if mType == 5 then -- text to all + if not fontSize then + fontSize = checkDefs('fontSize', coa) or 16 + end + fCal[#fCal+1] = fontSize + else + if not lineType then + lineType = checkDefs('lineType', coa) or 2 + end + end + fCal[#fCal+1] = lineType + if not readOnly then + readOnly = true + end + fCal[#fCal+1] = readOnly + if mType == 5 then + fCal[#fCal+1] = text + else + + fCal[#fCal+1] = message + end + local data = {coa = coa, markId = usedId, pos = pos, markFor = markFor, color = color, readOnly = readOnly, message = message, fillColor = fillColor, lineType = lineType, markType = tNames[mType], name = name, radius = radius, text = text, fontSize = fontSize, time = timer.getTime()} + mist.DBs.markList[usedId] = data + + if mType == 7 or mType == 1 then + local s = "trigger.action.markupToAll(" - -- create marks - if markType == 'coa' then - trigger.action.markToCoalition(newId, text, pos, markFor) - elseif markType == 'group' then - trigger.action.markToGroup(newId, text, pos, markFor) - else - trigger.action.markToAll(iterate(), text, pos) - end - table.insert(mist.marker.list, data) -- add to the DB - else - if #markForTable > 0 then - log:info('iterate') - for i = 1, #markForTable do - local newId = iterate() - local data = {markId = newId, text = text, pos = pos, markFor = markFor} - log:info(data) - table.insert(mist.marker.list, data) - trigger.action.markToGroup(newId, text, pos, markForTable[i]) - end - end - end - + for i = 1, #fCal do + --log:warn(fCal[i]) + if type(fCal[i]) == 'table' or type(fCal[i]) == 'boolean' then + s = s .. mist.utils.oneLineSerialize(fCal[i]) + else + s = s .. fCal[i] + end + if i < #fCal then + s = s .. ',' + end + end + + s = s .. ')' + if name then + usedMarks[name] = usedId + end + draw(s) + + else + + draw(data) + + end + return data + end end function mist.marker.remove(id) - for i, data in pairs(mist.marker.list) do - if id == data.markId then - trigger.action.removeMark(id) - end - end + return removeMark(id) end function mist.marker.get(id) - + if mist.DBs.markList[id] then + return mist.DBs.markList[id] + end + local names = {} + for markId, data in pairs(mist.DBs.markList) do + if data.name and data.name == id then + table.insert(names, data) + end + end + if #names >= 1 then + return names + end end - function mist.marker.coords(pos, cType, markFor, id) -- wrapper function to just display coordinates of a specific format at location - - - end - ]] + function mist.marker.drawZone(name, v) + if mist.DBs.zonesByName[name] then + --log:warn(mist.DBs.zonesByName[name]) + local vars = v or {} + local ref = mist.utils.deepCopy(mist.DBs.zonesByName[name]) + + if ref.type == 2 then -- it is a quad, but use freeform cause it isnt as bugged + vars.mType = 6 + vars.point = ref.verticies + else + vars.mType = 2 + vars.radius = ref.radius + vars.point = ref.point + end + + + if not (vars.ignoreColor and vars.ignoreColor == true) and not vars.fillColor then + vars.fillColor = ref.color + end + + --log:warn(vars) + return mist.marker.add(vars) + end + end + + function mist.marker.drawShape(name, v) + if mist.DBs.drawingByName[name] then + + local d = v or {} + local o = mist.utils.deepCopy(mist.DBs.drawingByName[name]) + --mist.marker.add({point = {x = o.mapX, z = o.mapY}, text = name}) + --log:warn(o) + d.points = o.points or {} + if o.primitiveType == "Polygon" then + d.mType = 7 + + if o.polygonMode == "rect" then + d.mType = 6 + elseif o.polygonMode == "circle" then + d.mType = 2 + d.points = {x = o.mapX, y = o.mapY} + d.radius = o.radius + end + elseif o.primitiveType == "TextBox" then + d.mType = 5 + d.points = {x = o.mapX, y = o.mapY} + d.text = o.text or d.text + d.fontSize = d.fontSize or o.fontSize + end + -- NOTE TO SELF. FIGURE OUT WHICH SHAPES NEED TO BE OFFSET. OVAL YES. + + if o.fillColorString and not d.fillColor then + d.fillColor = mist.utils.hexToRGB(o.fillColorString) + end + if o.colorString then + d.color = mist.utils.hexToRGB(o.colorString) + end + + + if o.thickness == 0 then + d.lineType = 0 + elseif o.style == 'solid' then + d.lineType = 1 + elseif o.style == 'dot' then + d.lineType = 2 + elseif o.style == 'dash' then + d.lineType = 3 + else + d.lineType = 1 + end + + + if o.primitiveType == "Line" and #d.points >= 2 then + d.mType = 1 + local rtn = {} + for i = 1, #d.points -1 do + local var = mist.utils.deepCopy(d) + var.points = {} + var.points[1] = d.points[i] + var.points[2] = d.points[i+1] + table.insert(rtn, mist.marker.add(var)) + end + return rtn + else + if d.mType then + --log:warn(d) + return mist.marker.add(d) + end + end + end + + + end + + + --[[ + function mist.marker.circle(v) + + + end +]] end --- Time conversion functions. -- @section mist.time @@ -6430,6 +8129,7 @@ do -- group tasks scope mist.air = {} mist.air.fixedWing = {} mist.air.heli = {} + mist.ship = {} --- Tasks group to follow a route. -- This sets the mission task for the given group. @@ -6453,7 +8153,7 @@ do -- group tasks scope if group then local groupCon = group:getController() if groupCon then - log:warn(misTask) + --log:warn(misTask) groupCon:setTask(misTask) return true end @@ -6475,17 +8175,17 @@ do -- group tasks scope if type(coa_data) == 'table' then if coa_data.country then --there is a country table for cntry_id, cntry_data in pairs(coa_data.country) do - for obj_type_name, obj_type_data in pairs(cntry_data) do - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - for group_num, group_data in pairs(obj_type_data.group) do + for obj_cat_name, obj_cat_data in pairs(cntry_data) do + if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" then -- only these types have points + if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then --there's a group! + for group_num, group_data in pairs(obj_cat_data.group) do if group_data and group_data.groupId == gpId then -- this is the group we are looking for if group_data.route and group_data.route.points and #group_data.route.points > 0 then local points = {} for point_num, point in pairs(group_data.route.points) do local routeData = {} - if env.mission.version > 7 then + if env.mission.version > 7 and env.mission.version < 19 then routeData.name = env.getValueDictByKey(point.name) else routeData.name = point.name @@ -6515,10 +8215,10 @@ do -- group tasks scope log:error('Group route not defined in mission editor for groupId: $1', gpId) return end --if group_data and group_data.name and group_data.name == 'groupname' - end --for group_num, group_data in pairs(obj_type_data.group) do - end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do + end --for group_num, group_data in pairs(obj_cat_data.group) do + end --if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then + end --if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" or obj_cat_name == "static" then + end --for obj_cat_name, obj_cat_data in pairs(cntry_data) do end --for cntry_id, cntry_data in pairs(coa_data.country) do end --if coa_data.country then --there is a country table end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then @@ -6822,14 +8522,19 @@ do -- group tasks scope end -- need to return a Vec3 or Vec2? - function mist.getRandPointInCircle(p, radius, innerRadius, maxA, minA) + function mist.getRandPointInCircle(p, r, innerRadius, maxA, minA) local point = mist.utils.makeVec3(p) local theta = 2*math.pi*math.random() + local radius = r or 1000 local minR = innerRadius or 0 if maxA and not minA then theta = math.rad(math.random(0, maxA - math.random())) - elseif maxA and minA and minA < maxA then - theta = math.rad(math.random(minA, maxA) - math.random()) + elseif maxA and minA then + if minA < maxA then + theta = math.rad(math.random(minA, maxA) - math.random()) + else + theta = math.rad(math.random(maxA, minA) - math.random()) + end end local rad = math.random() + math.random() if rad > 1 then @@ -6854,14 +8559,21 @@ do -- group tasks scope end function mist.getRandomPointInZone(zoneName, innerRadius, maxA, minA) - if type(zoneName) == 'string' and type(trigger.misc.getZone(zoneName)) == 'table' then - return mist.getRandPointInCircle(trigger.misc.getZone(zoneName).point, trigger.misc.getZone(zoneName).radius, innerRadius, maxA, minA) - end + if type(zoneName) == 'string' then + local zone = mist.DBs.zonesByName[zoneName] + if zone.type and zone.type == 2 then + return mist.getRandomPointInPoly(zone.verticies) + else + return mist.getRandPointInCircle(zone.point, zone.radius, innerRadius, maxA, minA) + end + end return false end function mist.getRandomPointInPoly(zone) - local avg = mist.getAvgPoint(zone) + --env.info('Zone Size: '.. #zone) + local avg = mist.getAvgPoint(zone) + --log:warn(avg) local radius = 0 local minR = math.huge local newCoord = {} @@ -6873,6 +8585,8 @@ do -- group tasks scope minR = mist.utils.get2DDist(avg, zone[i]) end end + --log:warn('Radius: $1', radius) + --log:warn('minR: $1', minR) local lSpawnPos = {} for j = 1, 100 do newCoord = mist.getRandPointInCircle(avg, radius) @@ -6886,6 +8600,19 @@ do -- group tasks scope end return newCoord end + + function mist.getWindBearingAndVel(p) + local point = mist.utils.makeVec3(o) + local gLevel = land.getHeight({x = point.x, y = point.z}) + if point.y <= gLevel then + point.y = gLevel + 10 + end + local t = atmosphere.getWind(point) + local bearing = math.tan(t.z/t.x) + local vel = math.sqrt(t.x^2 + t.z^2) + return bearing, vel + + end function mist.groupToRandomPoint(vars) local group = vars.group --Required @@ -6941,25 +8668,25 @@ do -- group tasks scope return end - function mist.groupRandomDistSelf(gpData, dist, form, heading, speed) + function mist.groupRandomDistSelf(gpData, dist, form, heading, speed, disableRoads) local pos = mist.getLeadPos(gpData) local fakeZone = {} fakeZone.radius = dist or math.random(300, 1000) fakeZone.point = {x = pos.x, y = pos.y, z = pos.z} - mist.groupToRandomZone(gpData, fakeZone, form, heading, speed) + mist.groupToRandomZone(gpData, fakeZone, form, heading, speed, disableRoads) return end - function mist.groupToRandomZone(gpData, zone, form, heading, speed) + function mist.groupToRandomZone(gpData, zone, form, heading, speed, disableRoads) if type(gpData) == 'string' then gpData = Group.getByName(gpData) end if type(zone) == 'string' then - zone = trigger.misc.getZone(zone) + zone = mist.DBs.zonesByName[zone] elseif type(zone) == 'table' and not zone.radius then - zone = trigger.misc.getZone(zone[math.random(1, #zone)]) + zone = mist.DBs.zonesByName[zone[math.random(1, #zone)]] end if speed then @@ -6973,7 +8700,7 @@ do -- group tasks scope vars.headingDegrees = heading vars.speed = speed vars.point = mist.utils.zoneToVec3(zone) - + vars.disableRoads = disableRoads mist.groupToRandomPoint(vars) return @@ -7042,7 +8769,7 @@ do -- group tasks scope function mist.groupToPoint(gpData, point, form, heading, speed, useRoads) if type(point) == 'string' then - point = trigger.misc.getZone(point) + point = mist.DBs.zonesByName[point] end if speed then speed = mist.utils.kmphToMps(speed) @@ -7081,6 +8808,16 @@ do -- group tasks scope return leader:getPosition().p end end + + function mist.groupIsDead(groupName) -- copy more or less from on station + if Group.getByName(groupName) then + local gp = Group.getByName(groupName) + if #gp:getUnits() > 0 or gp:isExist() == true then + return false + end + end + return true + end end @@ -7200,7 +8937,7 @@ do -- mist.Logger scope -- @usage -- log everything --myLogger:setLevel(3) function mist.Logger:setLevel(level) - if not level then + if not level then self.level = 2 else if type(level) == 'string' then diff --git a/resources/plugins/base/plugin.json b/resources/plugins/base/plugin.json index 8d0c50b4..3f39e2de 100644 --- a/resources/plugins/base/plugin.json +++ b/resources/plugins/base/plugin.json @@ -5,7 +5,7 @@ "specificOptions": [], "scriptsWorkOrders": [ { - "file": "mist_4_4_90.lua", + "file": "mist_4_5_107.lua", "mnemonic": "mist" }, { diff --git a/resources/plugins/skynetiads/skynet-iads-compiled.lua b/resources/plugins/skynetiads/skynet-iads-compiled.lua index fe5b4a3c..a1e1d2f9 100644 --- a/resources/plugins/skynetiads/skynet-iads-compiled.lua +++ b/resources/plugins/skynetiads/skynet-iads-compiled.lua @@ -1,40 +1,39 @@ -env.info("--- SKYNET VERSION: 2.4.0 | BUILD TIME: 05.11.2021 1737Z ---") +env.info("--- SKYNET VERSION: 3.0.0-develop | BUILD TIME: 03.04.2022 1630Z ---") do --this file contains the required units per sam type samTypesDB = { - ['S-200'] = { - ['type'] = 'complex', - ['searchRadar'] = { - ['RLS_19J6'] = { - ['name'] = { - ['NATO'] = 'Tin Shield', - }, - }, - ['p-19 s-125 sr'] = { - ['name'] = { - ['NATO'] = 'Flat Face', + ['type'] = 'complex', + ['searchRadar'] = { + ['RLS_19J6'] = { + ['name'] = { + ['NATO'] = 'Tin Shield', + }, }, - }, - }, - ['EWR P-37 BAR LOCK'] = { - ['Name'] = { - ['NATO'] = "Bar lock", - }, - }, - ['trackingRadar'] = { - ['RPC_5N62V'] = { - }, - }, - ['launchers'] = { - ['S-200_Launcher'] = { - }, - }, - ['name'] = { - ['NATO'] = 'SA-5 Gammon', - }, - ['harm_detection_chance'] = 60 - }, + ['p-19 s-125 sr'] = { + ['name'] = { + ['NATO'] = 'Flat Face', + }, + }, + }, + ['EWR P-37 BAR LOCK'] = { + ['Name'] = { + ['NATO'] = "Bar lock", + }, + }, + ['trackingRadar'] = { + ['RPC_5N62V'] = { + }, + }, + ['launchers'] = { + ['S-200_Launcher'] = { + }, + }, + ['name'] = { + ['NATO'] = 'SA-5 Gammon', + }, + ['harm_detection_chance'] = 60 + }, ['S-300'] = { ['type'] = 'complex', ['searchRadar'] = { @@ -67,7 +66,8 @@ samTypesDB = { ['name'] = { ['NATO'] = 'SA-10 Grumble', }, - ['harm_detection_chance'] = 90 + ['harm_detection_chance'] = 90, + ['canEngageHARM'] = true }, ['Buk'] = { ['type'] = 'complex', @@ -181,12 +181,11 @@ samTypesDB = { ['required'] = false, }, }, - - ['name'] = { ['NATO'] = 'Patriot', }, - ['harm_detection_chance'] = 90 + ['harm_detection_chance'] = 90, + ['canEngageHARM'] = true }, ['Hawk'] = { ['type'] = 'complex', @@ -228,8 +227,7 @@ samTypesDB = { }, ['harm_detection_chance'] = 60 }, - - ['NASAM'] = { + ['NASAMS'] = { ['type'] = 'complex', ['searchRadar'] = { ['NASAMS_Radar_MPQ64F1'] = { @@ -243,15 +241,16 @@ samTypesDB = { }, ['name'] = { - ['NATO'] = 'NASAM', + ['NATO'] = 'NASAMS', }, ['misc'] = { ['NASAMS_Command_Post'] = { ['required'] = false, }, }, + ['canEngageHARM'] = true, + ['harm_detection_chance'] = 90 }, - ['2S6 Tunguska'] = { ['type'] = 'single', ['searchRadar'] = { @@ -325,6 +324,9 @@ samTypesDB = { ['name'] = { ['NATO'] = 'SA-15 Gauntlet', }, + ['harm_detection_chance'] = 90, + ['canEngageHARM'] = true + }, ['Gepard'] = { ['type'] = 'single', @@ -775,6 +777,10 @@ samTypesDB['S-300PMU2'] = { }, ['harm_detection_chance'] = 90 } + +--[[ + +--]] end @@ -1143,17 +1149,16 @@ function SkynetIADS:create(name) iads.samSites = {} iads.commandCenters = {} iads.ewRadarScanMistTaskID = nil - iads.samSetupMistTaskID = nil iads.coalition = nil iads.contacts = {} iads.maxTargetAge = 32 iads.name = name + iads.harmDetection = SkynetIADSHARMDetection:create(iads) iads.logger = SkynetIADSLogger:create(iads) if iads.name == nil then iads.name = "" end iads.contactUpdateInterval = 5 - iads.samSetupTime = 60 return iads end @@ -1319,6 +1324,7 @@ function SkynetIADS:addSAMSite(samSiteName) self:setCoalition(samSiteDCS) local samSite = SkynetIADSSamSite:create(samSiteDCS, self) samSite:setupElements() + samSite:setCanEngageAirWeapons(true) samSite:goLive() -- for performance improvement, if iads is not scanning no update coverage update needs to be done, will be executed once when iads activates if self.ewRadarScanMistTaskID ~= nil then @@ -1482,6 +1488,9 @@ function SkynetIADS.evaluateContacts(self) samSite:targetCycleUpdateEnd() end + self.harmDetection:setContacts(self:getContacts()) + self.harmDetection:evaluateContacts() + self.logger:printSystemStatus() end @@ -1618,6 +1627,11 @@ function SkynetIADS:mergeContact(contact) local iadsContact = self.contacts[i] if iadsContact:getName() == contact:getName() then iadsContact:refresh() + local radars = contact:getAbstractRadarElementsDetected() + for j = 1, #radars do + local radar = radars[j] + iadsContact:addAbstractRadarElementDetected(radar) + end existingContact = true end end @@ -1646,28 +1660,13 @@ end -- will start going through the Early Warning Radars and SAM sites to check what targets they have detected function SkynetIADS.activate(self) mist.removeFunction(self.ewRadarScanMistTaskID) - mist.removeFunction(self.samSetupMistTaskID) self.ewRadarScanMistTaskID = mist.scheduleFunction(SkynetIADS.evaluateContacts, {self}, 1, self.contactUpdateInterval) self:buildRadarCoverage() end function SkynetIADS:setupSAMSitesAndThenActivate(setupTime) - if setupTime then - self.samSetupTime = setupTime - end - local samSites = self:getSAMSites() - for i = 1, #samSites do - local sam = samSites[i] - sam:goLive() - --point defences will go dark after sam:goLive() call on the SAM they are protecting, so we load them by calling a separate goLive call here, point defence SAMs will therefore receive 2 goLive calls - -- this should not have a negative impact on performance - local pointDefences = sam:getPointDefences() - for j = 1, #pointDefences do - pointDefence = pointDefences[j] - pointDefence:goLive() - end - end - self.samSetupMistTaskID = mist.scheduleFunction(SkynetIADS.activate, {self}, timer.getTime() + self.samSetupTime) + self:activate() + self.iads:printOutputToLog("DEPRECATED: setupSAMSitesAndThenActivate, no longer needed since using enableEmission instead of AI on / off allows for the Ground units to setup with their radars turned off") end function SkynetIADS:deactivate() @@ -1846,18 +1845,32 @@ do SkynetIADSAbstractDCSObjectWrapper = {} -function SkynetIADSAbstractDCSObjectWrapper:create(dcsObject) +function SkynetIADSAbstractDCSObjectWrapper:create(dcsRepresentation) local instance = {} setmetatable(instance, self) self.__index = self - instance.dcsObject = dcsObject - instance.name = dcsObject:getName() - instance.typeName = dcsObject:getTypeName() + instance.dcsName = "" + instance.typeName = "" + instance:setDCSRepresentation(dcsRepresentation) + if getmetatable(dcsRepresentation) ~= Group then + instance.typeName = dcsRepresentation:getTypeName() + end return instance end +function SkynetIADSAbstractDCSObjectWrapper:setDCSRepresentation(representation) + self.dcsRepresentation = representation + if self.dcsRepresentation then + self.dcsName = self:getDCSRepresentation():getName() + end +end + +function SkynetIADSAbstractDCSObjectWrapper:getDCSRepresentation() + return self.dcsRepresentation +end + function SkynetIADSAbstractDCSObjectWrapper:getName() - return self.name + return self.dcsName end function SkynetIADSAbstractDCSObjectWrapper:getTypeName() @@ -1865,37 +1878,95 @@ function SkynetIADSAbstractDCSObjectWrapper:getTypeName() end function SkynetIADSAbstractDCSObjectWrapper:getPosition() - return self.dcsObject:getPosition() + return self.dcsRepresentation:getPosition() end function SkynetIADSAbstractDCSObjectWrapper:isExist() - if self.dcsObject then - return self.dcsObject:isExist() + if self.dcsRepresentation then + return self.dcsRepresentation:isExist() else return false end end -function SkynetIADSAbstractDCSObjectWrapper:getDCSRepresentation() - return self.dcsObject +function SkynetIADSAbstractDCSObjectWrapper:insertToTableIfNotAlreadyAdded(tbl, object) + local isAdded = false + for i = 1, #tbl do + local child = tbl[i] + if child == object then + isAdded = true + end + end + if isAdded == false then + table.insert(tbl, object) + end + return not isAdded end +-- helper code for class inheritance +function inheritsFrom( baseClass ) + + local new_class = {} + local class_mt = { __index = new_class } + + function new_class:create() + local newinst = {} + setmetatable( newinst, class_mt ) + return newinst + end + + if nil ~= baseClass then + setmetatable( new_class, { __index = baseClass } ) + end + + -- Implementation of additional OO properties starts here -- + + -- Return the class object of the instance + function new_class:class() + return new_class + end + + -- Return the super class object of the instance + function new_class:superClass() + return baseClass + end + + -- Return true if the caller is an instance of theClass + function new_class:isa( theClass ) + local b_isa = false + + local cur_class = new_class + + while ( nil ~= cur_class ) and ( false == b_isa ) do + if cur_class == theClass then + b_isa = true + else + cur_class = cur_class:superClass() + end + end + + return b_isa + end + + return new_class +end + + end do SkynetIADSAbstractElement = {} +SkynetIADSAbstractElement = inheritsFrom(SkynetIADSAbstractDCSObjectWrapper) function SkynetIADSAbstractElement:create(dcsRepresentation, iads) - local instance = {} + local instance = self:superClass():create(dcsRepresentation) setmetatable(instance, self) self.__index = self instance.connectionNodes = {} instance.powerSources = {} instance.iads = iads instance.natoName = "UNKNOWN" - instance.dcsName = "" - instance:setDCSRepresentation(dcsRepresentation) world.addEventHandler(instance) return instance end @@ -1966,17 +2037,6 @@ function SkynetIADSAbstractElement:genericCheckOneObjectIsAlive(objects) return isAlive end -function SkynetIADSAbstractElement:setDCSRepresentation(representation) - self.dcsRepresentation = representation - if self.dcsRepresentation then - self.dcsName = self:getDCSRepresentation():getName() - end -end - -function SkynetIADSAbstractElement:getDCSRepresentation() - return self.dcsRepresentation -end - function SkynetIADSAbstractElement:getNatoName() return self.natoName end @@ -2026,54 +2086,6 @@ function SkynetIADSAbstractElement:informChildrenOfStateChange() end --- helper code for class inheritance -function inheritsFrom( baseClass ) - - local new_class = {} - local class_mt = { __index = new_class } - - function new_class:create() - local newinst = {} - setmetatable( newinst, class_mt ) - return newinst - end - - if nil ~= baseClass then - setmetatable( new_class, { __index = baseClass } ) - end - - -- Implementation of additional OO properties starts here -- - - -- Return the class object of the instance - function new_class:class() - return new_class - end - - -- Return the super class object of the instance - function new_class:superClass() - return baseClass - end - - -- Return true if the caller is an instance of theClass - function new_class:isa( theClass ) - local b_isa = false - - local cur_class = new_class - - while ( nil ~= cur_class ) and ( false == b_isa ) do - if cur_class == theClass then - b_isa = true - else - cur_class = cur_class:superClass() - end - end - - return b_isa - end - - return new_class -end - end do @@ -2086,6 +2098,9 @@ SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK = 2 SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE = 1 SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE = 2 +SkynetIADSAbstractRadarElement.HARM_TO_SAM_ASPECT = 30 +SkynetIADSAbstractRadarElement.HARM_LOOKAHEAD_NM = 20 + function SkynetIADSAbstractRadarElement:create(dcsElementWithRadar, iads) local instance = self:superClass():create(dcsElementWithRadar, iads) setmetatable(instance, self) @@ -2103,6 +2118,7 @@ function SkynetIADSAbstractRadarElement:create(dcsElementWithRadar, iads) instance.childRadars = {} instance.missilesInFlight = {} instance.pointDefences = {} + instance.harmDecoys = {} instance.ingnoreHARMSWhilePointDefencesHaveAmmo = false instance.autonomousBehaviour = SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI instance.goLiveRange = SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE @@ -2112,12 +2128,16 @@ function SkynetIADSAbstractRadarElement:create(dcsElementWithRadar, iads) instance.maxHarmShutDownTime = 0 instance.minHarmPresetShutdownTime = 30 instance.maxHarmPresetShutdownTime = 180 + instance.harmShutdownTime = 0 instance.firingRangePercent = 100 instance.actAsEW = false instance.cachedTargets = {} instance.cachedTargetsMaxAge = 1 instance.cachedTargetsCurrentAge = 0 instance.goLiveTime = 0 + instance.engageAirWeapons = false + instance.isAPointDefence = false + instance.canEngageHARM = false -- 5 seconds seems to be a good value for the sam site to find the target with its organic radar instance.noCacheActiveForSecondsAfterGoLive = 5 return instance @@ -2152,8 +2172,19 @@ function SkynetIADSAbstractRadarElement:cleanUp() self:removeEventHandlers() end +function SkynetIADSAbstractRadarElement:setIsAPointDefence(state) + if (state == true or state == false) then + self.isAPointDefence = state + end +end + +function SkynetIADSAbstractRadarElement:getIsAPointDefence() + return self.isAPointDefence +end + function SkynetIADSAbstractRadarElement:addPointDefence(pointDefence) table.insert(self.pointDefences, pointDefence) + pointDefence:setIsAPointDefence(true) return self end @@ -2161,8 +2192,12 @@ function SkynetIADSAbstractRadarElement:getPointDefences() return self.pointDefences end +function SkynetIADSAbstractRadarElement:addHARMDecoy(harmDecoy) + table.insert(self.harmDecoys, harmDecoy) +end + function SkynetIADSAbstractRadarElement:addParentRadar(parentRadar) - self:abstractAddRadar(parentRadar, self.parentRadars) + self:insertToTableIfNotAlreadyAdded(self.parentRadars, parentRadar) self:informChildrenOfStateChange() end @@ -2174,21 +2209,8 @@ function SkynetIADSAbstractRadarElement:clearParentRadars() self.parentRadars = {} end -function SkynetIADSAbstractRadarElement:abstractAddRadar(radar, tbl) -local isAdded = false - for i = 1, #tbl do - local child = tbl[i] - if child == radar then - isAdded = true - end - end - if isAdded == false then - table.insert(tbl, radar) - end -end - function SkynetIADSAbstractRadarElement:addChildRadar(childRadar) - self:abstractAddRadar(childRadar, self.childRadars) + self:insertToTableIfNotAlreadyAdded(self.childRadars, childRadar) end function SkynetIADSAbstractRadarElement:getChildRadars() @@ -2271,6 +2293,10 @@ function SkynetIADSAbstractRadarElement:pointDefencesHaveRemainingAmmo(minNumber local pointDefence = self.pointDefences[i] remainingMissiles = remainingMissiles + pointDefence:getRemainingNumberOfMissiles() end + return self:hasRequiredNumberOfMissiles(minNumberOfMissiles, remainingMissiles) +end + +function SkynetIADSAbstractRadarElement:hasRequiredNumberOfMissiles(minNumberOfMissiles, remainingMissiles) local returnValue = false if ( remainingMissiles > 0 and remainingMissiles >= minNumberOfMissiles ) then returnValue = true @@ -2278,23 +2304,33 @@ function SkynetIADSAbstractRadarElement:pointDefencesHaveRemainingAmmo(minNumber return returnValue end +function SkynetIADSAbstractRadarElement:hasRemainingAmmoToEngageMissiles(minNumberOfMissiles) + local remainingMissiles = self:getRemainingNumberOfMissiles() + return self:hasRequiredNumberOfMissiles(minNumberOfMissiles, remainingMissiles) +end + +-- this method needs to be refactored so that it works for ew radars that don't have launchers, or that it is only called by sam sites +function SkynetIADSAbstractRadarElement:hasEnoughLaunchersToEngageMissiles(minNumberOfLaunchers) + local launchers = self:getLaunchers() + if(launchers ~= nil) then + launchers = #self:getLaunchers() + else + launchers = 0 + end + return self:hasRequiredNumberOfMissiles(minNumberOfLaunchers, launchers) +end + function SkynetIADSAbstractRadarElement:pointDefencesHaveEnoughLaunchers(minNumberOfLaunchers) local numOfLaunchers = 0 for i = 1, #self.pointDefences do local pointDefence = self.pointDefences[i] numOfLaunchers = numOfLaunchers + #pointDefence:getLaunchers() end - local returnValue = false - if ( numOfLaunchers > 0 and numOfLaunchers >= minNumberOfLaunchers ) then - returnValue = true - end - return returnValue + return self:hasRequiredNumberOfMissiles(minNumberOfLaunchers, numOfLaunchers) end function SkynetIADSAbstractRadarElement:setIgnoreHARMSWhilePointDefencesHaveAmmo(state) - if state == true or state == false then - self.ingnoreHARMSWhilePointDefencesHaveAmmo = state - end + self.iads:printOutputToLog("DEPRECATED: setIgnoreHARMSWhilePointDefencesHaveAmmo SAM Site will stay live automaticall as long as itself or it's point defences can defend against a HARM") return self end @@ -2409,6 +2445,20 @@ function SkynetIADSAbstractRadarElement:setHARMDetectionChance(chance) return self end +function SkynetIADSAbstractRadarElement:setCanEngageHARM(canEngage) + if canEngage == true or canEngage == false then + self.canEngageHARM = canEngage + if canEngage == true then + self:setCanEngageAirWeapons(true) + end + end + return self +end + +function SkynetIADSAbstractRadarElement:getCanEngageHARM() + return self.canEngageHARM +end + function SkynetIADSAbstractRadarElement:setupElements() local numUnits = #self:getUnitsToAnalyse() for typeName, dataType in pairs(SkynetIADS.database) do @@ -2436,8 +2486,8 @@ function SkynetIADSAbstractRadarElement:setupElements() --this check ensures a unit or group has all required elements for the specific sam or ew type: if (hasLauncher and hasSearchRadar and hasTrackingRadar and #self.launchers > 0 and #self.searchRadars > 0 and #self.trackingRadars > 0 ) or (hasSearchRadar and hasLauncher and #self.searchRadars > 0 and #self.launchers > 0) then - local harmDetection = dataType['harm_detection_chance'] - self:setHARMDetectionChance(harmDetection) + self:setHARMDetectionChance(dataType['harm_detection_chance']) + self:setCanEngageHARM(dataType['canEngageHARM']) local natoName = dataType['name']['NATO'] self:buildNatoName(natoName) break @@ -2445,6 +2495,24 @@ function SkynetIADSAbstractRadarElement:setupElements() end end +function SkynetIADSAbstractRadarElement:setCanEngageAirWeapons(engageAirWeapons) + if self:isDestroyed() == false then + local controller = self:getDCSRepresentation():getController() + if ( engageAirWeapons == true ) then + self.engageAirWeapons = true + controller:setOption(AI.Option.Ground.id.ENGAGE_AIR_WEAPONS, true) + else + self.engageAirWeapons = false + controller:setOption(AI.Option.Ground.id.ENGAGE_AIR_WEAPONS, false) + end + end + return self +end + +function SkynetIADSAbstractRadarElement:getCanEngageAirWeapons() + return self.engageAirWeapons +end + function SkynetIADSAbstractRadarElement:buildNatoName(natoName) --we shorten the SA-XX names and don't return their code names eg goa, gainful.. local pos = natoName:find(" ") @@ -2545,9 +2613,9 @@ function SkynetIADSAbstractRadarElement:goLive() then if self:isDestroyed() == false then local cont = self:getController() - cont:setOnOff(true) cont:setOption(AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.RED) cont:setOption(AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_FREE) + self:getDCSRepresentation():enableEmission(true) self.goLiveTime = timer.getTime() self.aiState = true end @@ -2572,16 +2640,7 @@ function SkynetIADSAbstractRadarElement:goDark() and (self.harmSilenceID ~= nil or ( self.harmSilenceID == nil and #self:getDetectedTargets() == 0 and self:hasMissilesInFlight() == false) or ( self.harmSilenceID == nil and #self:getDetectedTargets() > 0 and self:hasMissilesInFlight() == false and self:hasRemainingAmmo() == false ) ) then if self:isDestroyed() == false then - local controller = self:getController() - -- if the SAM site still has ammo we turn off the controller, this prevents rearming, however this way the SAM site is frozen in a red state, on the next actication it will be up and running much faster, therefore it will instantaneously engage targets - -- also this is a better way to get the HARM to miss the target, if not set to false the HARM often sticks to the target - if self:hasRemainingAmmo() then - controller:setOnOff(false) - --if the SAM is out of ammo we set the state to green, and ROE to weapon hold, this way it will shut down its radar and it can be rearmed - else - controller:setOption(AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.GREEN) - controller:setOption(AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD) - end + self:getDCSRepresentation():enableEmission(false) end -- point defence will only go live if the Radar Emitting site it is protecting goes dark and this is due to a it defending against a HARM if (self.harmSilenceID ~= nil) then @@ -2596,10 +2655,15 @@ function SkynetIADSAbstractRadarElement:goDark() end function SkynetIADSAbstractRadarElement:pointDefencesGoLive() + local setActive = false for i = 1, #self.pointDefences do local pointDefence = self.pointDefences[i] - pointDefence:setActAsEW(true) + if ( pointDefence:getActAsEW() == false ) then + setActive = true + pointDefence:setActAsEW(true) + end end + return setActive end function SkynetIADSAbstractRadarElement:isActive() @@ -2722,16 +2786,26 @@ end function SkynetIADSAbstractRadarElement:goSilentToEvadeHARM(timeToImpact) self:finishHarmDefence(self) - --self.objectsIdentifiedAsHarms = {} - local harmTime = self:getHarmShutDownTime() - if self.iads:getDebugSettings().harmDefence then - self.iads:printOutputToLog("HARM DEFENCE SHUTTING DOWN: "..self:getDCSName().." | FOR: "..harmTime.." seconds | TTI: "..timeToImpact) + if ( timeToImpact == nil ) then + timeToImpact = 0 end - self.harmSilenceID = mist.scheduleFunction(SkynetIADSAbstractRadarElement.finishHarmDefence, {self}, timer.getTime() + harmTime, 1) + + self.minHarmShutdownTime = self:calculateMinimalShutdownTimeInSeconds(timeToImpact) + self.maxHarmShutDownTime = self:calculateMaximalShutdownTimeInSeconds(self.minHarmShutdownTime) + + self.harmShutdownTime = self:calculateHARMShutdownTime() + if self.iads:getDebugSettings().harmDefence then + self.iads:printOutputToLog("HARM DEFENCE SHUTTING DOWN: "..self:getDCSName().." | FOR: "..self.harmShutdownTime.." seconds | TTI: "..timeToImpact) + end + self.harmSilenceID = mist.scheduleFunction(SkynetIADSAbstractRadarElement.finishHarmDefence, {self}, timer.getTime() + self.harmShutdownTime, 1) self:goDark() end -function SkynetIADSAbstractRadarElement:getHarmShutDownTime() +function SkynetIADSAbstractRadarElement:getHARMShutdownTime() + return self.harmShutdownTime +end + +function SkynetIADSAbstractRadarElement:calculateHARMShutdownTime() local shutDownTime = math.random(self.minHarmShutdownTime, self.maxHarmShutDownTime) return shutDownTime end @@ -2739,6 +2813,7 @@ end function SkynetIADSAbstractRadarElement.finishHarmDefence(self) mist.removeFunction(self.harmSilenceID) self.harmSilenceID = nil + self.harmShutdownTime = 0 if ( self:getAutonomousState() == true ) then self:goAutonomous() @@ -2755,7 +2830,7 @@ function SkynetIADSAbstractRadarElement:getDetectedTargets() local target = targets[i] -- there are cases when a destroyed object is still visible as a target to the radar, don't add it, will cause errors everywhere the dcs object is accessed if target.object then - local iadsTarget = SkynetIADSContact:create(target) + local iadsTarget = SkynetIADSContact:create(target, self) iadsTarget:refresh() if self:isTargetInRange(iadsTarget) then table.insert(self.cachedTargets, iadsTarget) @@ -2802,37 +2877,74 @@ end -- will only check for missiles, if DCS ads AAA than can engage HARMs then this code must be updated: function SkynetIADSAbstractRadarElement:shallIgnoreHARMShutdown() local numOfHarms = self:getNumberOfObjectsItentifiedAsHARMS() - return ( self:pointDefencesHaveRemainingAmmo(numOfHarms) and self:pointDefencesHaveEnoughLaunchers(numOfHarms) and self.ingnoreHARMSWhilePointDefencesHaveAmmo == true) + --[[ + self.iads:printOutputToLog("Self enough launchers: "..tostring(self:hasEnoughLaunchersToEngageMissiles(numOfHarms))) + self.iads:printOutputToLog("Self enough missiles: "..tostring(self:hasRemainingAmmoToEngageMissiles(numOfHarms))) + self.iads:printOutputToLog("PD enough missiles: "..tostring(self:pointDefencesHaveRemainingAmmo(numOfHarms))) + self.iads:printOutputToLog("PD enough launchers: "..tostring(self:pointDefencesHaveEnoughLaunchers(numOfHarms))) + --]] + return ( ((self:hasEnoughLaunchersToEngageMissiles(numOfHarms) and self:hasRemainingAmmoToEngageMissiles(numOfHarms) and self:getCanEngageHARM()) or (self:pointDefencesHaveRemainingAmmo(numOfHarms) and self:pointDefencesHaveEnoughLaunchers(numOfHarms)))) end +function SkynetIADSAbstractRadarElement:informOfHARM(harmContact) + local radars = self:getRadars() + for j = 1, #radars do + local radar = radars[j] + local distanceNM = mist.utils.metersToNM(self:getDistanceInMetersToContact(radar, harmContact:getPosition().p)) + local harmToSAMHeading = mist.utils.toDegree(mist.utils.getHeadingPoints(harmContact:getPosition().p, radar:getPosition().p)) + local harmToSAMAspect = self:calculateAspectInDegrees(harmContact:getMagneticHeading(), harmToSAMHeading) + local speedKT = harmContact:getGroundSpeedInKnots(0) + local secondsToImpact = self:getSecondsToImpact(distanceNM, speedKT) + --TODO: Make constant out of aspect and distance --> use tti instead of distanceNM? + -- when iterating through the radars, store shortest tti and work with that value?? + if ( harmToSAMAspect < SkynetIADSAbstractRadarElement.HARM_TO_SAM_ASPECT and distanceNM < SkynetIADSAbstractRadarElement.HARM_LOOKAHEAD_NM ) then + self:addObjectIdentifiedAsHARM(harmContact) + if ( #self:getPointDefences() > 0 and self:pointDefencesGoLive() == true and self.iads:getDebugSettings().harmDefence ) then + self.iads:printOutputToLog("POINT DEFENCES GOING LIVE FOR: "..self:getDCSName().." | TTI: "..secondsToImpact) + end + --self.iads:printOutputToLog("Ignore HARM shutdown: "..tostring(self:shallIgnoreHARMShutdown())) + if ( self:getIsAPointDefence() == false and ( self:isDefendingHARM() == false or ( self:getHARMShutdownTime() < secondsToImpact ) ) and self:shallIgnoreHARMShutdown() == false) then + self:goSilentToEvadeHARM(secondsToImpact) + break + end + end + end +end + +function SkynetIADSAbstractElement:addObjectIdentifiedAsHARM(harmContact) + self:insertToTableIfNotAlreadyAdded(self.objectsIdentifiedAsHarms, harmContact) +end + +function SkynetIADSAbstractRadarElement:calculateAspectInDegrees(harmHeading, harmToSAMHeading) + local aspect = harmHeading - harmToSAMHeading + if ( aspect < 0 ) then + aspect = -1 * aspect + end + if aspect > 180 then + aspect = 360 - aspect + end + return mist.utils.round(aspect) +end function SkynetIADSAbstractRadarElement:getNumberOfObjectsItentifiedAsHARMS() - local numFound = 0 - for unitName, unit in pairs(self.objectsIdentifiedAsHarms) do - numFound = numFound + 1 - end - return numFound + return #self.objectsIdentifiedAsHarms end function SkynetIADSAbstractRadarElement:cleanUpOldObjectsIdentifiedAsHARMS() - local validObjects = {} - local validCount = 0 - for unitName, unit in pairs(self.objectsIdentifiedAsHarms) do - local harm = unit['target'] - if harm:getAge() <= self.objectsIdentifiedAsHarmsMaxTargetAge then - validObjects[harm:getName()] = {} - validObjects[harm:getName()]['target'] = harm - validObjects[harm:getName()]['count'] = unit['count'] - validCount = validCount + 1 + local newHARMS = {} + for i = 1, #self.objectsIdentifiedAsHarms do + local harmContact = self.objectsIdentifiedAsHarms[i] + if harmContact:getAge() < self.objectsIdentifiedAsHarmsMaxTargetAge then + table.insert(newHARMS, harmContact) end - end - --stop point defences acting as ew (always on), will occur if activated via shallIgnoreHARMShutdown() in evaluateIfTargetsContainHARMs + end + --stop point defences acting as ew (always on), will occur if activated via evaluateIfTargetsContainHARMs() --if in this iteration all harms where cleared we turn of the point defence. But in any other cases we dont turn of point defences, that interferes with other parts of the iads -- when setting up the iads (letting pds go to read state) - if validCount == 0 and self:getNumberOfObjectsItentifiedAsHARMS() > 0 then + if (#newHARMS == 0 and self:getNumberOfObjectsItentifiedAsHARMS() > 0 ) then self:pointDefencesStopActingAsEW() end - self.objectsIdentifiedAsHarms = validObjects + self.objectsIdentifiedAsHarms = newHARMS end @@ -2847,58 +2959,6 @@ function SkynetIADSAbstractRadarElement.evaluateIfTargetsContainHARMs(self) --we use the regular interval of this method to update to other states: self:updateMissilesInFlight() self:cleanUpOldObjectsIdentifiedAsHARMS() - - - local targets = self:getDetectedTargets() - for i = 1, #targets do - local target = targets[i] - local radars = self:getRadars() - for j = 1, #radars do - local radar = radars[j] - if radar:isExist() == true then - local distance = self:getDistanceInMetersToContact(radar, target:getPosition().p) - local impactPoint = self:calculateImpactPoint(target, distance) - if impactPoint then - local harmImpactPointDistanceToSAM = self:getDistanceInMetersToContact(radar, impactPoint) - if harmImpactPointDistanceToSAM <= 100 then - if self.objectsIdentifiedAsHarms[target:getName()] then - self.objectsIdentifiedAsHarms[target:getName()]['count'] = self.objectsIdentifiedAsHarms[target:getName()]['count'] + 1 - else - self.objectsIdentifiedAsHarms[target:getName()] = {} - self.objectsIdentifiedAsHarms[target:getName()]['target'] = target - self.objectsIdentifiedAsHarms[target:getName()]['count'] = 1 - end - local savedTarget = self.objectsIdentifiedAsHarms[target:getName()]['target'] - savedTarget:refresh() - local numDetections = self.objectsIdentifiedAsHarms[target:getName()]['count'] - local speed = savedTarget:getGroundSpeedInKnots() - local timeToImpact = self:getSecondsToImpact(mist.utils.metersToNM(distance), speed) - local shallReactToHarm = self:shallReactToHARM() - - -- if self:getNumberOfObjectsItentifiedAsHARMS() > 0 then - -- env.info("detect as HARM: "..self:getDCSName().." "..self:getNumberOfObjectsItentifiedAsHARMS()) - -- end - - -- we use 2 detection cycles so a random object in the air pointing at the SAM site for a spilt second will not trigger a shutdown. shallReactToHarm adds some salt otherwise the SAM will always shut down 100% of the time. - if numDetections == 2 and shallReactToHarm then - if self:shallIgnoreHARMShutdown() == false then - self.minHarmShutdownTime = self:calculateMinimalShutdownTimeInSeconds(timeToImpact) - self.maxHarmShutDownTime = self:calculateMaximalShutdownTimeInSeconds(self.minHarmShutdownTime) - self:goSilentToEvadeHARM(timeToImpact) - else - self:pointDefencesGoLive() - end - end - if numDetections == 2 and shallReactToHarm == false then - if self.iads:getDebugSettings().harmDefence then - self.iads:printOutputToLog("HARM DEFENCE NO REACTION: "..self:getDCSName()) - end - end - end - end - end - end - end end end @@ -2983,19 +3043,57 @@ do SkynetIADSContact = {} SkynetIADSContact = inheritsFrom(SkynetIADSAbstractDCSObjectWrapper) -function SkynetIADSContact:create(dcsRadarTarget) +SkynetIADSContact.CLIMB = "CLIMB" +SkynetIADSContact.DESCEND = "DESCEND" + +SkynetIADSContact.HARM = "HARM" +SkynetIADSContact.NOT_HARM = "NOT_HARM" +SkynetIADSContact.HARM_UNKNOWN = "HARM_UNKNOWN" + +function SkynetIADSContact:create(dcsRadarTarget, abstractRadarElementDetected) local instance = self:superClass():create(dcsRadarTarget.object) setmetatable(instance, self) self.__index = self + instance.abstractRadarElementsDetected = {} + table.insert(instance.abstractRadarElementsDetected, abstractRadarElementDetected) instance.firstContactTime = timer.getAbsTime() instance.lastTimeSeen = 0 instance.dcsRadarTarget = dcsRadarTarget - instance.position = instance.dcsObject:getPosition() + instance.position = instance:getDCSRepresentation():getPosition() instance.numOfTimesRefreshed = 0 instance.speed = 0 + instance.harmState = SkynetIADSContact.HARM_UNKNOWN + instance.simpleAltitudeProfile = {} return instance end +function SkynetIADSContact:setHARMState(state) + self.harmState = state +end + +function SkynetIADSContact:isIdentifiedAsHARM() + return self.harmState == SkynetIADSContact.HARM +end + +function SkynetIADSContact:isHARMStateUnknown() + return self.harmState == SkynetIADSContact.HARM_UNKNOWN +end + +function SkynetIADSContact:getMagneticHeading() + if ( self:isExist() ) then + return mist.utils.round(mist.utils.toDegree(mist.getHeading(self:getDCSRepresentation()))) + else + return -1 + end +end + +function SkynetIADSContact:getAbstractRadarElementsDetected() + return self.abstractRadarElementsDetected +end + +function SkynetIADSContact:addAbstractRadarElementDetected(radar) + self:insertToTableIfNotAlreadyAdded(self.abstractRadarElementsDetected, radar) +end function SkynetIADSContact:isTypeKnown() return self.dcsRadarTarget.type @@ -3005,6 +3103,17 @@ function SkynetIADSContact:isDistanceKnown() return self.dcsRadarTarget.distance end +function SkynetIADSContact:getTypeName() + if self:isIdentifiedAsHARM() then + return SkynetIADSContact.HARM + end + local category = self:getDCSRepresentation():getCategory() + if category == Object.Category.UNIT then + return self.typeName + end + return "UNKNOWN" +end + function SkynetIADSContact:getPosition() return self.position end @@ -3016,9 +3125,17 @@ function SkynetIADSContact:getGroundSpeedInKnots(decimals) return mist.utils.round(self.speed, decimals) end +function SkynetIADSContact:getHeightInFeetMSL() + if self:isExist() then + return mist.utils.round(mist.utils.metersToFeet(self:getDCSRepresentation():getPosition().p.y), 0) + else + return 0 + end +end + function SkynetIADSContact:getDesc() - if self.dcsObject:isExist() then - return self.dcsObject:getDesc() + if self:isExist() then + return self:getDCSRepresentation():getDesc() else return {} end @@ -3029,19 +3146,39 @@ function SkynetIADSContact:getNumberOfTimesHitByRadar() end function SkynetIADSContact:refresh() - self.numOfTimesRefreshed = self.numOfTimesRefreshed + 1 - if self.dcsObject and self.dcsObject:isExist() then - local distance = mist.utils.metersToNM(mist.utils.get2DDist(self.position.p, self.dcsObject:getPosition().p)) + if self:isExist() then local timeDelta = (timer.getAbsTime() - self.lastTimeSeen) if timeDelta > 0 then + self.numOfTimesRefreshed = self.numOfTimesRefreshed + 1 + local distance = mist.utils.metersToNM(mist.utils.get2DDist(self.position.p, self:getDCSRepresentation():getPosition().p)) local hours = timeDelta / 3600 self.speed = (distance / hours) + self:updateSimpleAltitudeProfile() + self.position = self:getDCSRepresentation():getPosition() end - self.position = self.dcsObject:getPosition() end self.lastTimeSeen = timer.getAbsTime() end +function SkynetIADSContact:updateSimpleAltitudeProfile() + local currentAltitude = self:getDCSRepresentation():getPosition().p.y + + local previousPath = "" + if #self.simpleAltitudeProfile > 0 then + previousPath = self.simpleAltitudeProfile[#self.simpleAltitudeProfile] + end + + if self.position.p.y > currentAltitude and previousPath ~= SkynetIADSContact.DESCEND then + table.insert(self.simpleAltitudeProfile, SkynetIADSContact.DESCEND) + elseif self.position.p.y < currentAltitude and previousPath ~= SkynetIADSContact.CLIMB then + table.insert(self.simpleAltitudeProfile, SkynetIADSContact.CLIMB) + end +end + +function SkynetIADSContact:getSimpleAltitudeProfile() + return self.simpleAltitudeProfile +end + function SkynetIADSContact:getAge() return mist.utils.round(timer.getAbsTime() - self.lastTimeSeen) end @@ -3316,7 +3453,7 @@ function SkynetIADSSAMSearchRadar:setFiringRangePercent(percent) end function SkynetIADSSAMSearchRadar:getDistance(target) - return mist.utils.get2DDist(target:getPosition().p, self.dcsObject:getPosition().p) + return mist.utils.get2DDist(target:getPosition().p, self:getDCSRepresentation():getPosition().p) end function SkynetIADSSAMSearchRadar:getHeight(target) @@ -3381,7 +3518,7 @@ end function SkynetIADSSamSite:informOfContact(contact) -- we make sure isTargetInRange (expensive call) is only triggered if no previous calls to this method resulted in targets in range - if self.targetsInRange == false and self:isTargetInRange(contact) then + if ( self.targetsInRange == false and self:isTargetInRange(contact) and ( contact:isIdentifiedAsHARM() == false or ( contact:isIdentifiedAsHARM() == true and self:getCanEngageHARM() == true ) ) ) then self:goLive() self.targetsInRange = true end @@ -3535,3 +3672,141 @@ SA-2 Launcher: } } --]] +do + +SkynetIADSHARMDetection = {} +SkynetIADSHARMDetection.__index = SkynetIADSHARMDetection + +SkynetIADSHARMDetection.HARM_THRESHOLD_SPEED_KTS = 800 + +function SkynetIADSHARMDetection:create(iads) + local harmDetection = {} + setmetatable(harmDetection, self) + harmDetection.contacts = {} + harmDetection.iads = iads + harmDetection.contactRadarsEvaluated = {} + return harmDetection +end + +function SkynetIADSHARMDetection:setContacts(contacts) + self.contacts = contacts +end + +function SkynetIADSHARMDetection:evaluateContacts() + self:cleanAgedContacts() + for i = 1, #self.contacts do + local contact = self.contacts[i] + local groundSpeed = contact:getGroundSpeedInKnots(0) + --if a contact has only been hit by a radar once it's speed is 0 + if groundSpeed == 0 then + return + end + local simpleAltitudeProfile = contact:getSimpleAltitudeProfile() + local newRadarsToEvaluate = self:getNewRadarsThatHaveDetectedContact(contact) + --self.iads:printOutputToLog(contact:getName().." new Radars to evaluate: "..#newRadarsToEvaluate) + --self.iads:printOutputToLog(contact:getName().." ground speed: "..groundSpeed) + if ( #newRadarsToEvaluate > 0 and contact:isIdentifiedAsHARM() == false and ( groundSpeed > SkynetIADSHARMDetection.HARM_THRESHOLD_SPEED_KTS and #simpleAltitudeProfile <= 2 ) ) then + local detectionProbability = self:getDetectionProbability(newRadarsToEvaluate) + --self.iads:printOutputToLog("DETECTION PROB: "..detectionProbability) + if ( self:shallReactToHARM(detectionProbability) ) then + contact:setHARMState(SkynetIADSContact.HARM) + if (self.iads:getDebugSettings().harmDefence ) then + self.iads:printOutputToLog("HARM IDENTIFIED: "..contact:getTypeName().." | DETECTION PROBABILITY WAS: "..detectionProbability.."%") + end + else + contact:setHARMState(SkynetIADSContact.NOT_HARM) + if (self.iads:getDebugSettings().harmDefence ) then + self.iads:printOutputToLog("HARM NOT IDENTIFIED: "..contact:getTypeName().." | DETECTION PROBABILITY WAS: "..detectionProbability.."%") + end + end + end + + if ( #simpleAltitudeProfile > 2 and contact:isIdentifiedAsHARM() ) then + contact:setHARMState(SkynetIADSContact.HARM_UNKNOWN) + if (self.iads:getDebugSettings().harmDefence ) then + self.iads:printOutputToLog("CORRECTING HARM STATE: CONTACT IS NOT A HARM: "..contact:getName()) + end + end + + if ( contact:isIdentifiedAsHARM() ) then + self:informRadarsOfHARM(contact) + end + end +end + +function SkynetIADSHARMDetection:cleanAgedContacts() + local activeContactRadars = {} + for contact, radars in pairs (self.contactRadarsEvaluated) do + if contact:getAge() < 32 then + activeContactRadars[contact] = radars + end + end + self.contactRadarsEvaluated = activeContactRadars +end + +function SkynetIADSHARMDetection:getNewRadarsThatHaveDetectedContact(contact) + local newRadars = contact:getAbstractRadarElementsDetected() + local radars = self.contactRadarsEvaluated[contact] + if radars then + newRadars = {} + local contactRadars = contact:getAbstractRadarElementsDetected() + for i = 1, #contactRadars do + local contactRadar = contactRadars[i] + local newRadar = self:isElementInTable(radars, contactRadar) + if newRadar ~= nil then + table.insert(newRadars, newRadar) + end + end + end + self.contactRadarsEvaluated[contact] = contact:getAbstractRadarElementsDetected() + return newRadars +end + +function SkynetIADSHARMDetection:isElementInTable(tbl, element) + for i = 1, #tbl do + tblElement = tbl[i] + if tblElement == element then + return nil + end + end + return element +end + +function SkynetIADSHARMDetection:informRadarsOfHARM(contact) + local samSites = self.iads:getUsableSAMSites() + self:updateRadarsOfSites(samSites, contact) + + local ewRadars = self.iads:getUsableEarlyWarningRadars() + self:updateRadarsOfSites(ewRadars, contact) +end + +function SkynetIADSHARMDetection:updateRadarsOfSites(sites, contact) + for i = 1, #sites do + local site = sites[i] + site:informOfHARM(contact) + end +end + +function SkynetIADSHARMDetection:shallReactToHARM(chance) + return chance >= math.random(1, 100) +end + +function SkynetIADSHARMDetection:getDetectionProbability(radars) + local detectionChance = 0 + local missChance = 100 + local detection = 0 + for i = 1, #radars do + detection = radars[i]:getHARMDetectionChance() + if ( detectionChance == 0 ) then + detectionChance = detection + else + detectionChance = detectionChance + (detection * (missChance / 100)) + end + missChance = 100 - detection + end + return detectionChance +end + +end + + diff --git a/resources/plugins/skynetiads/skynetiads-config.lua b/resources/plugins/skynetiads/skynetiads-config.lua index dc7f1982..38365c80 100644 --- a/resources/plugins/skynetiads/skynetiads-config.lua +++ b/resources/plugins/skynetiads/skynetiads-config.lua @@ -58,8 +58,10 @@ if dcsLiberation and SkynetIADS then for i,pd in pairs(element.PD) do env.info(string.format("DCSLiberation|Skynet-IADS plugin - adding IADS Point Defence %s", pd)) local point_defence = iads:addSAMSite(pd) - iads_unit:addPointDefence(point_defence) - iads_unit:setIgnoreHARMSWhilePointDefencesHaveAmmo(true) + if point_defence ~= nil then + -- only add as point defence if skynet can handle the PD unit + iads_unit:addPointDefence(point_defence) + end end end end @@ -87,6 +89,7 @@ if dcsLiberation and SkynetIADS then iadsDebug.harmDefence = true iadsDebug.samSiteStatusEnvOutput = true iadsDebug.earlyWarningRadarStatusEnvOutput = true + iadsDebug.commandCenterStatusEnvOutput = true end -- add the AWACS @@ -149,7 +152,7 @@ if dcsLiberation and SkynetIADS then end --activate the IADS - iads:setupSAMSitesAndThenActivate() + iads:activate() end ------------------------------------------------------------------------------------------------------------------------------------------------------------