-- Tacview ACMI - Universal Flight Analysis Tool 1.1.1 -- Export script for Lock On: Flaming Cliffs 1.1 -- Copyright (C) 2006-2011 - Stra Software -- http://tacview.strasoftware.com/ -- TO ENABLE THIS SCRIPT: -- Set [EnableExportScript = true] in [./Config/export/config.lua] -- Add [dofile("./Config/Export/TacviewExportFlamingCliffs.lua")] at the end of [./Config/Export/Export.lua] -- ACMI text files are exported to %TACVIEW_EXPORT_PATH% folder -- if this environment variable is not valid, files are exported to [./Temp/] folder -- Headers dofile("./Config/World/World.lua") -- Required to get mission date -- 2D map tools (Official code from ED) (because Lock-On maps are not on a true 3D sphere) --data for recalculation zeroX = 5000000 zeroZ = 6600000 centerX = 11465000 - zeroX --circle center centerZ = 6500000 - zeroZ pnSxW_X = 4468608.57 - zeroX -- point 40dgN : 24dgE pnSxW_Z = 5730893.72 - zeroZ pnNxW_X = 5357858.31 - zeroX -- point 48dgN : 24dgE pnNxW_Z = 5828649.53 - zeroZ pnSxE_X = 4468608.57 - zeroX -- point 40dgN : 42dgE pnSxE_Z = 7269106.20 - zeroZ pnNxE_X = 5357858.31 - zeroX -- point 48dgN : 42dgE pnNxE_Z = 7171350.00 - zeroZ lenNorth = math.sqrt((pnNxW_X-centerX)*(pnNxW_X-centerX) + (pnNxW_Z-centerZ)*(pnNxW_Z-centerZ)) lenSouth = math.sqrt((pnSxW_X-centerX)*(pnSxW_X-centerX) + (pnSxW_Z-centerZ)*(pnSxW_Z-centerZ)) lenN_S = lenSouth - lenNorth RealAngleMaxLongitude = math.atan ((pnSxW_Z - centerZ)/(pnSxW_X - centerX)) * 180/math.pi -- borders EndWest = 24 EndEast = 42 EndNorth = 48 EndSouth = 40 MiddleLongitude = (EndWest + EndEast) / 2 ToLengthN_S = ((EndNorth - EndSouth) / lenN_S) ToAngleW_E = (MiddleLongitude - EndWest) / RealAngleMaxLongitude ToDegree = 180/math.pi function getLongitude(x, z) -- degrees , (x (meters) - to North, z (meters) - to East) ang = - math.atan (((z - centerZ)) / (x - centerX)) * ToDegree return ang * ToAngleW_E + MiddleLongitude end function getLatitude(x, z) --degrees (x (meters) - to North, z (meters) - to East) len = lenSouth - math.sqrt((x-centerX)*(x-centerX) + (z-centerZ)*(z-centerZ)) return len * ToLengthN_S + EndSouth end function getXYCoords(inLatitudeDegrees, inLongitudeDegrees) -- args: 2 numbers // Return two value in order: X, Y -- Lo coordinates system realAng = (inLongitudeDegrees - MiddleLongitude) / ToAngleW_E / ToDegree realLen = lenSouth - (inLatitudeDegrees - EndSouth) / ToLengthN_S outX = centerX - realLen * math.cos (realAng) outZ = centerZ + realLen * math.sin (realAng) return outX, outZ end -- Tacview Exporter Tacview= { -- Log parameters DefaultObjectsUpdatePeriod=1/10, -- Delay between two log updates (in seconds), use [0] to force update at each frame LatitudeOffset=42, -- To improve log resolution LongitudeOffset=32, -- To improve log resolution -- Convert Lock-On Object_Type To Tacview Object_Type ObjectTypeLookupTable= { [ 1] = 16, -- 0x10 Airplane [ 2] = 24, -- 0x18 Helicopter [ 3] = 80, -- 0x50 Chaff (Flare) [ 4] = 64, -- 0x40 Missile [ 5] = 76, -- 0x4c Bomb [ 6] = 72, -- 0x48 Shell [ 7] = 68, -- 0x44 Rocket [ 8] = 40, -- 0x28 Ground Moving [ 9] = 136, -- 0x88 Ground Standing [12] = 48, -- 0x30 Ship [13] = 128, -- 0x80 Aerodrome [16] = 32, -- 0x20 SAM [17] = 36, -- 0x24 Tank }, -- Convert Lock-On Coalition_ID To Tacview Coalition_ID CoalitionLookupTable= { ["Allies"] = 0, ["Enemies"] = 1, }, -- Convert Lock-On Country_Name To ISO Country_Code CountryCodeLookupTable= { ["Belgium"] = "be", ["Belgique"] = "be", ["Belgien"] = "be", ["Áåëüãèÿ"] = "be", ["Canada"] = "ca", ["Kanada"] = "ca", ["Êàíàäà"] = "ca", ["Denmark"] = "dk", ["Danemark"] = "dk", ["Äàíèÿ"] = "dk", ["France"] = "fr", ["Frankreich"] = "fr", ["Ôðàíöèÿ"] = "fr", ["Georgia"] = "ge", ["Géorgie"] = "ge", ["Georgie"] = "ge", ["Georgien"] = "ge", ["Ãðóçèÿ"] = "ge", ["Germany"] = "de", ["Allemagne"] = "de", ["Deutschland"] = "de", ["Ãåðìàíèÿ"] = "de", ["Israel"] = "il", ["Israël"] = "il", ["Èçðàèëü"] = "il", ["Netherlands"] = "nl", ["Pays-Bas"] = "nl", ["Niederlande"] = "nl", ["Íèäåðëàíäû"] = "nl", ["Norway"] = "no", ["Norvège"] = "no", ["Norvege"] = "no", ["Norwegen"] = "no", ["Íîðâåãèÿ"] = "no", ["Russia"] = "ru", ["Russie"] = "ru", ["Russland"] = "ru", ["Ðîññèÿ"] = "ru", ["Spain"] = "es", ["Espagne"] = "es", ["Spanien"] = "es", ["Èñïàíèÿ"] = "es", ["Turkey"] = "tr", ["Turquie"] = "tr", ["Turkei"] = "tr", ["Òóðöèÿ"] = "tr", ["UK"] = "uk", ["Angleterre"] = "uk", ["Royaume Uni"] = "uk", ["Royaume-Uni"] = "uk", ["Großbritannien"] = "uk", ["United Kingdom"] = "uk", ["Âåëèêîáðèòàíèÿ"] = "uk", ["Ukraine"] = "ua", ["Óêðàèíà"] = "ua", ["USA"] = "us", ["United States"] = "us", ["Etats Unis"] = "us", ["Etats-Unis"] = "us", ["ÑØÀ"] = "us", ["Iran"] = "ir", ["Iraq"] = "iq", ["Greece"] = "gr", ["Italy"] = "it", }, -- 1251-Cyrillic to UTF-8 Table UTF8LookupTable= { [128]={208,130},[129]={208,131},[130]={226,128,154},[131]={209,147},[132]={226,128,158},[133]={226,128,166},[134]={226,128,160},[135]={226,128,161}, [136]={226,130,172},[137]={226,128,176},[138]={208,137},[139]={226,128,185},[140]={208,138},[141]={208,140},[142]={208,139},[143]={208,143}, [144]={209,146},[145]={226,128,152},[146]={226,128,153},[147]={226,128,156},[148]={226,128,157},[149]={226,128,162},[150]={226,128,147},[151]={226,128,148}, [152]={194,152},[153]={226,132,162},[154]={209,153},[155]={226,128,186},[156]={209,154},[157]={209,156},[158]={209,155},[159]={209,159}, [160]={194,160},[161]={208,142},[162]={209,158},[163]={208,136},[164]={194,164},[165]={210,144},[166]={194,166},[167]={194,167}, [168]={208,129},[169]={194,169},[170]={208,132},[171]={194,171},[172]={194,172},[173]={194,173},[174]={194,174},[175]={208,135}, [176]={194,176},[177]={194,177},[178]={208,134},[179]={209,150},[180]={210,145},[181]={194,181},[182]={194,182},[183]={194,183}, [184]={209,145},[185]={226,132,150},[186]={209,148},[187]={194,187},[188]={209,152},[189]={208,133},[190]={209,149},[191]={209,151}, [192]={208,144},[193]={208,145},[194]={208,146},[195]={208,147},[196]={208,148},[197]={208,149},[198]={208,150},[199]={208,151}, [200]={208,152},[201]={208,153},[202]={208,154},[203]={208,155},[204]={208,156},[205]={208,157},[206]={208,158},[207]={208,159}, [208]={208,160},[209]={208,161},[210]={208,162},[211]={208,163},[212]={208,164},[213]={208,165},[214]={208,166},[215]={208,167}, [216]={208,168},[217]={208,169},[218]={208,170},[219]={208,171},[220]={208,172},[221]={208,173},[222]={208,174},[223]={208,175}, [224]={208,176},[225]={208,177},[226]={208,178},[227]={208,179},[228]={208,180},[229]={208,181},[230]={208,182},[231]={208,183}, [232]={208,184},[233]={208,185},[234]={208,186},[235]={208,187},[236]={208,188},[237]={208,189},[238]={208,190},[239]={208,191}, [240]={209,128},[241]={209,129},[242]={209,130},[243]={209,131},[244]={209,132},[245]={209,133},[246]={209,134},[247]={209,135}, [248]={209,136},[249]={209,137},[250]={209,138},[251]={209,139},[252]={209,140},[253]={209,141},[254]={209,142},[255]={209,143} }, -- Fast File Class (lazy write cache) FastFile= { -- Constants MaxCacheSize=64*1024, -- Maximum data in cache before automatic flush -- Create file Create=function(self,FileName) -- Check Parameters if self.File then self:Close() end -- Create a new file self.File=io.open(FileName,"wb"); if not self.File then return false; -- Operation failed end -- Complete return true; end, -- Inject data into file (lazy write) Write=function(self,Data) -- Check Parameters if not self.File then return false; -- File not open end -- Update Cache if self.CacheSize+string.len(Data)>=self.MaxCacheSize then -- Add Part Of Data To Cache if self.CacheSize=self.NextDefaultObjectsUpdateTime then ShouldUpdateLog=true; self.NextDefaultObjectsUpdateTime=CurrentTime+self.DefaultObjectsUpdatePeriod; end local DefaultObjectsList=LoGetWorldObjects(); if self:AddUpdateObjects(DefaultObjectsList,FormatedTime,ShouldUpdateLog)==true then FormatedTime=nil; end -- Remove destroyed objects self:RemoveObjects(DefaultObjectsList,nil,FormatedTime); end, -- Add/Update objects AddUpdateObjects=function(self,CurrentObjectsList,FormatedTime,ShouldUpdateLog) -- Check parameters if not CurrentObjectsList then return false; end -- Dump objects local LogWasUpdated=false; for ID,Object in pairs(CurrentObjectsList) do -- Add new object if not self.LastObjectsStatus[ID] then local LogLine; -- Time prefix if FormatedTime then LogLine=FormatedTime; FormatedTime=nil; else LogLine=""; end -- Object Name local ObjectName=self.CleanupName(Object.Name); -- Object Type local ObjectType=self.ObjectTypeLookupTable[Object.Subtype]; if not ObjectType then ObjectType=-Object.Subtype; -- This will help to debug new types end -- Coalition local CoalitionID; if ObjectType==80 then CoalitionID="?"; -- Chaff/Flare coalition is not reliable in Lock-On else if Object.Coalition then CoalitionID=self.CoalitionLookupTable[Object.Coalition]; if not CoalitionID then CoalitionID="?"; end else CoalitionID="?"; end end -- Country local CountryCode; if Object.Country then CountryCode=self.CountryCodeLookupTable[Object.Country]; if not CountryCode then CountryCode="?"; end else CountryCode="?"; end -- Reduce ID size (to compact text log) local OptimizedID=self.GetOptimizedID(ID); -- Declare new object self.AcmiFile:Write(LogLine..string.format("+%x,?,%x,%s,%s,%s,%s,?,?\n",OptimizedID,ObjectType,CoalitionID,CountryCode,self.ToUTF8(self.GetUnitName(ObjectName)),self.ToUTF8(self.GetPilotName(ObjectName)))); -- Done LogWasUpdated=true; end -- Update object data as required if self:UpdateObject(ID,Object,self.LoggedObjectsStatus[ID],FormatedTime,ShouldUpdateLog)==true then FormatedTime=nil; LogWasUpdated=true; end end -- Completed return LogWasUpdated; end, -- Fix Yaw (because FC/BS map is a 2D projection, not a true 3D sphere) FixedYaw=function(Yaw,Latitude,Longitude) -- Calculate vector to north in Lock-On coordinates local RefX,RefZ=getXYCoords(Latitude,Longitude); local ToNorthPosX,ToNorthPosZ=getXYCoords(Latitude+1,Longitude); local ToNorthX=ToNorthPosZ-RefZ; local ToNorthY=ToNorthPosX-RefX; local ToNorthLength=math.sqrt(ToNorthX*ToNorthX+ToNorthY*ToNorthY); -- Normalize vector if ToNorthLength>0 then ToNorthX=ToNorthX/ToNorthLength; ToNorthY=ToNorthY/ToNorthLength; end -- Calculate Yaw Error return Yaw+math.atan2(ToNorthY,ToNorthX)-math.pi/2; end, -- Update one object UpdateObject=function(self,ID,CurrentObjectData,PrevObjectData,FormatedTime,ShouldUpdateLog) -- Log object dynamic properties local ChangeDetected=false; local LogWasUpdated=false; local ObjectType=self.ObjectTypeLookupTable[CurrentObjectData.Subtype]; local Log=""; local Roll=0; local Pitch=0; local Yaw=0; -- Latitude if not PrevObjectData or CurrentObjectData.LatLongAlt.Lat~=PrevObjectData.LatLongAlt.Lat then Log=Log..string.format(",%.6f",CurrentObjectData.LatLongAlt.Lat-self.LatitudeOffset); ChangeDetected=true; else Log=Log..","; end -- Longitude if not PrevObjectData or CurrentObjectData.LatLongAlt.Long~=PrevObjectData.LatLongAlt.Long then Log=Log..string.format(",%.6f",CurrentObjectData.LatLongAlt.Long-self.LongitudeOffset); ChangeDetected=true; else Log=Log..","; end -- Altitude if not PrevObjectData or CurrentObjectData.LatLongAlt.Alt~=PrevObjectData.LatLongAlt.Alt then Log=Log..string.format(",%.2f",CurrentObjectData.LatLongAlt.Alt); ChangeDetected=true; else Log=Log..","; end -- Roll/Pitch/Yaw if ObjectType==80 or ObjectType==84 or ObjectType==72 then -- flare/chaff/shell -- Do not log roll/pitch/yaw to reduce recording size if not PrevObjectData then Log=Log..",0,0,0\n"; else Log=Log..",,,\n"; end else if ObjectType==76 or ObjectType==68 then -- bomb/rockets -- Do not log roll/pitch/yaw to reduce recording size if not PrevObjectData then Log=Log..",?,?,?\n"; else Log=Log..",,,\n"; end else -- Player Plane Info local PlayerPlaneID=LoGetPlayerPlaneId(); local PlayerPitch,PlayerRoll,PlayerYaw=LoGetADIPitchBankYaw(); if not PlayerPlaneID then PlayerPlaneID=self.PlayerPlaneID; -- Use last player ID (to avoid bug on last record when object/player is destroyed) PlayerPitch=self.PlayerPitch; PlayerRoll=self.PlayerRoll; PlayerYaw=self.PlayerYaw; else self.PlayerPlaneID=PlayerPlaneID; -- Remember current player ID self.PlayerPitch=PlayerPitch; self.PlayerRoll=PlayerRoll; self.PlayerYaw=PlayerYaw; end -- Roll (available only for the main plane) if PlayerPlaneID and ID==PlayerPlaneID then Roll=PlayerRoll; end if not PrevObjectData or Roll~=PrevObjectData.Roll then if PlayerPlaneID and ID==PlayerPlaneID then Log=Log..string.format(",%.1f",math.mod(math.deg(Roll),360)); else Log=Log..",?"; end ChangeDetected=true; else Log=Log..","; end -- Pitch (available only for the main plane) if PlayerPlaneID and ID==PlayerPlaneID then Pitch=-PlayerPitch; end if not PrevObjectData or Pitch~=PrevObjectData.Pitch then if PlayerPlaneID and ID==PlayerPlaneID then Log=Log..string.format(",%.1f",-math.mod(math.deg(Pitch),360)); else Log=Log..",?"; end ChangeDetected=true; else Log=Log..","; end -- Yaw (not available in FC, emulated by using heading) Yaw=CurrentObjectData.Heading; --Player's plane Yaw is not reliable enough for an ACMI --if PlayerPlaneID and ID==PlayerPlaneID then -- Yaw=math.rad(360)-PlayerYaw; --end if not PrevObjectData or Yaw~=PrevObjectData.Yaw then local NewFormatedYaw=string.format(",%.1f\n",math.mod(math.deg(Tacview.FixedYaw(Yaw,CurrentObjectData.LatLongAlt.Lat,CurrentObjectData.LatLongAlt.Long)),360)); local YawIsDifferent=true; if PrevObjectData then local OldFormatedYaw=string.format(",%.1f\n",math.mod(math.deg(Tacview.FixedYaw(PrevObjectData.Yaw,PrevObjectData.LatLongAlt.Lat,PrevObjectData.LatLongAlt.Long)),360)); if NewFormatedYaw==OldFormatedYaw then YawIsDifferent=false; end end if YawIsDifferent==true then Log=Log..NewFormatedYaw; ChangeDetected=true; else Log=Log..",\n"; -- Yaw is not different enough to be dumped end else Log=Log..",\n"; end end end -- Log data if not PrevObjectData or ( ChangeDetected==true and ShouldUpdateLog==true ) then local OptimizedID=self.GetOptimizedID(ID); if FormatedTime then self.AcmiFile:Write(FormatedTime..string.format("%x",OptimizedID)..Log); else self.AcmiFile:Write(string.format("%x",OptimizedID)..Log); end LogWasUpdated=true; -- Remember Last Logged Object Properties self.LoggedObjectsStatus[ID]=CurrentObjectData; self.LoggedObjectsStatus[ID].Roll=Roll; self.LoggedObjectsStatus[ID].Pitch=Pitch; self.LoggedObjectsStatus[ID].Yaw=Yaw; end -- Remember Current Frame Object Properties self.LastObjectsStatus[ID]=CurrentObjectData; self.LastObjectsStatus[ID].Roll=Roll; self.LastObjectsStatus[ID].Pitch=Pitch; self.LastObjectsStatus[ID].Yaw=Yaw; -- Complete return LogWasUpdated; end, -- Remove any destroyed objects RemoveObjects=function(self,DefaultObjectsList,BallisticObjectsList,FormatedTime) local Log; for ID,Object in pairs(self.LastObjectsStatus) do if (not DefaultObjectsList or not DefaultObjectsList[ID]) and (not BallisticObjectsList or not BallisticObjectsList[ID]) then -- Log last position if required if self:UpdateObject(ID,Object,self.LoggedObjectsStatus[ID],FormatedTime,true)==true then FormatedTime=nil; end -- Prefix event with time if required if FormatedTime then Log=FormatedTime; FormatedTime=nil; else Log=""; end -- Log Event self.AcmiFile:Write(Log..string.format("!20,%x\n",self.GetOptimizedID(ID))); -- Remove object from lists self.LoggedObjectsStatus[ID]=nil; self.LastObjectsStatus[ID]=nil; end end end, -- Stop ACMI logging EndLog=function(self) if self.AcmiFile then self.AcmiFile:Close(); self.AcmiFile=nil; end end, } -- Works once just before mission start. do local PrevLuaExportStart=LuaExportStart; LuaExportStart=function() Tacview:BeginLog(); if PrevLuaExportStart then PrevLuaExportStart(); end end end -- Works just after every simulation frame. do local PrevLuaExportAfterNextFrame=LuaExportAfterNextFrame; LuaExportAfterNextFrame=function() Tacview:UpdateLog(); if PrevLuaExportAfterNextFrame then PrevLuaExportAfterNextFrame(); end end end -- Works once just after mission stop. do local PrevLuaExportStop=LuaExportStop; LuaExportStop=function() Tacview:EndLog(); if PrevLuaExportStop then PrevLuaExportStop(); end end end