-- Tacview ACMI - Universal Flight Analysis Tool 0.92 -- Export script for Lock On: Flaming Cliffs 1.1 -- Copyright (C) 2006-2008 - Stra Software -- See http://lomac.strasoftware.com/lomac-acmi.php for more info -- ACMI text files are exported to [/Lock On/Temp/] folder -- TO ENABLE THIS SCRIPT: -- Set [EnableExportScript = true] in [/Lock On/Config/export/config.lua] -- Add [dofile("./Config/Export/TacviewExportFlamingCliffs.lua")] at the end of [/Lock On/Config/Export/Export.lua] -- Required to get mission date dofile("./Config/World/World.lua") -- ACMI Log AcmiData= { -- Log parameters UpdatePeriod=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} }, -- Reduce ID size (to compact text log) -- OptimizedID=ID^0x1000000 (free in C, hard to do with this fucking LUA...) GetOptimizedID=function(ID) if math.mod(math.floor(ID/16777216),2)==1 then return ID-16777216; -- Clear bit end return ID+16777216; -- Set bit end, -- Add escape characters CleanupName=function(Name) if Name then Name=string.gsub(Name,"\\","\\\\"); Name=string.gsub(Name,",","\\,"); Name=string.gsub(Name,"=","\\="); return Name; end return ""; end, -- Convert 1251-Cyrillic to UTF-8 ToUTF8=function(StringToConvert) if not StringToConvert then return ""; end local StringLength=string.len(StringToConvert); local CharIndex=1; local ConvertedString=""; while(CharIndex<=StringLength) do local Char=string.byte(StringToConvert,CharIndex); if Char<128 then ConvertedString=ConvertedString..string.char(Char); else local ConvertedChar=AcmiData.UTF8LookupTable[Char]; if ConvertedChar then ConvertedString=ConvertedString..string.char(ConvertedChar[1]); if ConvertedChar[2] then ConvertedString=ConvertedString..string.char(ConvertedChar[2]); if ConvertedChar[3] then ConvertedString=ConvertedString..string.char(ConvertedChar[3]); end end end end CharIndex=CharIndex+1; end return ConvertedString; end, -- Trim Given String Trim=function(StringToTrim) return string.gsub(StringToTrim,"^%s*(.-)%s*$","%1"); end, -- Extract unit name from from name GetUnitName=function(Name) -- Purge '(Me)' tag from unit name Name=string.gsub(Name,"(%(Me%))",""); -- Purge Pilot Name Name=string.gsub(Name,"(%(.*%))",""); -- Trim Unit Name Name=AcmiData.Trim(Name); return Name; end, -- Extract unit name from from name GetPilotName=function(Name) -- Purge '(Me)' tag from unit name Name=string.gsub(Name,"(%(Me%))",""); -- Extract Pilot Name local NameFound=string.find(Name,"(%(.*%))"); if not NameFound then return ""; -- No pilot name end Name=string.sub(Name,NameFound); if not Name then return ""; -- No pilot name end return string.sub(Name,2,string.len(Name)-1); end, -- Begin to log ACMI data BeginLog=function(self) -- Reset objects list self.Objects={}; -- Reset tools self.NextUpdateTime=0; -- Create a new log file for each mission self.AcmiFile=io.open("./Temp/Tacview-"..os.date("%Y%m%d-%H%M%S")..".txt.acmi","wb"); -- Write header if self.AcmiFile then -- UTF-8 BOM {0xEF,0xBB,0xBF} self.AcmiFile:write(string.char(239,187,191)); -- Core Header self.AcmiFile:write("FileType=text/acmi/tacview\n"); self.AcmiFile:write("FileVersion=1.1\n"); self.AcmiFile:write("Source=Lock On: Flaming Cliffs 1.1\n"); self.AcmiFile:write("Recorder=Tacview 0.92\n"); self.AcmiFile:write("RecordingTime="..os.date("!%Y-%m-%dT%H:%M:%SZ",now).."\n"); local PlayerName=LoGetPilotName(); if PlayerName then self.AcmiFile:write("Author="..self.ToUTF8(self.CleanupName(PlayerName)).."\n"); end -- Declarations if MissionDate then -- Crimea Time Zone: Winter(UTC+2) Summer(DST=UTC+3) -- 3 hours time shift calculation (assume summer Crimea time shift) local LocalRefTime=os.time{year=2004,month=6,day=22,hour=0}; local UTCRefTime=os.time{year=2004,month=6,day=21,hour=21}; local TimeShift=LocalRefTime-UTCRefTime; -- DST bias required because nothing is simple with LUA local CurrentDate=os.date("*t",now); local IsDst; if CurrentDate and CurrentDate.isdst==true then IsDst=true; else IsDst=false; end -- Substract 3 hours from the current mission time (not easy with lua) local LocalMissionTime=os.time{year=MissionDate.Year,month=MissionDate.Month,day=MissionDate.Day,hour=0,isdst=IsDst}; local UTCMissionTime=LocalMissionTime-TimeShift+LoGetMissionStartTime(); local MissionTimeTable=os.date("*t",UTCMissionTime); self.AcmiFile:write("MissionTime="..string.format("%04u-%02u-%02uT%02u:%02u:%02uZ",MissionTimeTable.year,MissionTimeTable.month,MissionTimeTable.day,MissionTimeTable.hour,MissionTimeTable.min,MissionTimeTable.sec).."\n"); end self.AcmiFile:write("LatitudeOffset="..self.LatitudeOffset.."\n"); self.AcmiFile:write("LongitudeOffset="..self.LongitudeOffset.."\n"); self.AcmiFile:write("Coalition=Allies,Red\n"); self.AcmiFile:write("Coalition=Enemies,Blue\n"); self.AcmiFile:write("ProvidedEvents=Removed\n"); -- Additional Information local PlayerID=LoGetPlayerPlaneId(); if PlayerID then self.AcmiFile:write(string.format("MainAircraftID=%x\n",self.GetOptimizedID(PlayerID))); end end end, -- Update ACMI log UpdateLog=function(self) -- Check parameters if not AcmiData.AcmiFile then return; end -- Write frame time local CurrentTime=LoGetModelTime(); local FormatedTime=string.format("#%.2f\n",CurrentTime); local ShouldUpdateLog=false; if CurrentTime>=self.NextUpdateTime then ShouldUpdateLog=true; self.NextUpdateTime=CurrentTime+self.UpdatePeriod; end -- Get world objects local WorldObjects=LoGetWorldObjects(); if not WorldObjects then return; end -- ADD/UPDATE OBJECTS local IsNewObject; local OptimizedID; -- Player Plane Info local PlayerPlaneID=LoGetPlayerPlaneId(); local PlayerPitch,PlayerRoll,PlayerYaw=LoGetADIPitchBankYaw(); for ID,Object in pairs(WorldObjects) do -- Reduce ID size (to compact text log) OptimizedID=self.GetOptimizedID(ID); -- ADD NEW OBJECT if not self.Objects[ID] then if FormatedTime then self.AcmiFile:write(FormatedTime); FormatedTime=nil; end local ObjectName=self.CleanupName(Object.Name); local ObjectType=self.ObjectTypeLookupTable[Object.Subtype]; if not ObjectType then ObjectType=-Object.Subtype; -- This will help to debug new types end 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 local CountryCode; if Object.Country then CountryCode=self.CountryCodeLookupTable[Object.Country]; if not CountryCode then CountryCode="?"; end else CountryCode="?"; end self.AcmiFile:write(string.format("+%x,?,%x,%s,%s,%s,%s,?,?\n",OptimizedID,ObjectType,CoalitionID,CountryCode,self.ToUTF8(self.GetUnitName(ObjectName)),self.ToUTF8(self.GetPilotName(ObjectName)))); IsNewObject=true; else IsNewObject=false; end -- Log object dynamic properties local ObjectPrevValues=self.Objects[ID]; local Log=string.format("%x",OptimizedID); local ChangeDetected=false; -- Latitude if IsNewObject==true or Object.LatLongAlt.Lat~=ObjectPrevValues.LatLongAlt.Lat then Log=Log..string.format(",%.6f",Object.LatLongAlt.Lat-self.LatitudeOffset); ChangeDetected=true; else Log=Log..","; end -- Longitude if IsNewObject==true or Object.LatLongAlt.Long~=ObjectPrevValues.LatLongAlt.Long then Log=Log..string.format(",%.6f",Object.LatLongAlt.Long-self.LongitudeOffset); ChangeDetected=true; else Log=Log..","; end -- Altitude if IsNewObject==true or Object.LatLongAlt.Alt~=ObjectPrevValues.LatLongAlt.Alt then Log=Log..string.format(",%.2f",Object.LatLongAlt.Alt); ChangeDetected=true; else Log=Log..","; end -- Roll (available only for the main plane) local Roll=0; if PlayerPlaneID and ID==PlayerPlaneID then Roll=PlayerRoll; end if IsNewObject==true or Roll~=ObjectPrevValues.Roll then if IsNewObject==false or (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) local Pitch=0; if PlayerPlaneID and ID==PlayerPlaneID then Pitch=-PlayerPitch; end if IsNewObject==true or Pitch~=ObjectPrevValues.Pitch then if IsNewObject==false or (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 yet available, emulated by using heading for the main plane) local Yaw=Object.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 IsNewObject==true or Yaw~=ObjectPrevValues.Yaw then Log=Log..string.format(",%.1f\n",math.mod(math.deg(Yaw),360)); ChangeDetected=true; else Log=Log..",\n"; end -- Log data if IsNewObject==true or ( ChangeDetected==true and ShouldUpdateLog==true ) then if FormatedTime then self.AcmiFile:write(FormatedTime); FormatedTime=nil; end self.AcmiFile:write(Log); -- Remember objet properties self.Objects[ID]=Object; self.Objects[ID].Roll=Roll; self.Objects[ID].Pitch=Pitch; self.Objects[ID].Yaw=Yaw; end end -- REMOVE OBSOLETE OBJECTS for ID,Object in pairs(self.Objects) do if not WorldObjects[ID] then if FormatedTime then self.AcmiFile:write(FormatedTime); FormatedTime=nil; end self.AcmiFile:write(string.format("!20,%x\n",self.GetOptimizedID(ID))); self.Objects[ID]=nil; end end end, -- Stop ACMI logging EndLog=function(self) if self.AcmiFile then io.close(self.AcmiFile) end end, } -- Works once just before mission start. do local PrevLuaExportStart=LuaExportStart; LuaExportStart=function() AcmiData:BeginLog(); if PrevLuaExportStart then PrevLuaExportStart(); end end end -- Works just after every simulation frame. do local PrevLuaExportAfterNextFrame=LuaExportAfterNextFrame; LuaExportAfterNextFrame=function() AcmiData:UpdateLog(); if PrevLuaExportAfterNextFrame then PrevLuaExportAfterNextFrame(); end end end -- Works once just after mission stop. do local PrevLuaExportStop=LuaExportStop; LuaExportStop=function() AcmiData:EndLog(); if PrevLuaExportStop then PrevLuaExportStop(); end end end