-- Tacview ACMI - Universal Flight Analysis Tool 0.95a -- Export script for Lock On: Flaming Cliffs 1.1 -- Copyright (C) 2006-2010 - 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 -- ACMI Log AcmiData= { -- 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} }, -- 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.LastObjectsStatus={}; -- Last frame object status self.LoggedObjectsStatus={}; -- Last logged object status (usually older than LastObjectsStatus) -- Reset tools self.NextDefaultObjectsUpdateTime=0; -- Create a new log file for each mission local DefaultTargetFolder="./Temp/"; local TargetFolder=os.getenv("TACVIEW_EXPORT_PATH"); local FileName="Tacview-"..os.date("%Y%m%d-%H%M%S")..".txt.acmi"; if not TargetFolder then TargetFolder=DefaultTargetFolder; end local LastCharacter=string.sub(TargetFolder,-1); if LastCharacter~="/" and LastCharacter~="\\" then TargetFolder=TargetFolder.."/"; end self.AcmiFile=io.open(TargetFolder..FileName,"wb"); if not self.AcmiFile then TargetFolder=DefaultTargetFolder; self.AcmiFile=io.open(TargetFolder..FileName,"wb"); end if not self.AcmiFile then return; end -- 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.4\n"); self.AcmiFile:write("Source=Lock On: Flaming Cliffs 1.1\n"); self.AcmiFile:write("Recorder=Tacview 0.95a\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=20}; -- Lock-On seems to be set at UTC+4 -- local UTCRefTime=os.time{year=2004,month=6,day=21,hour=21}; -- This is the good crimea UTC fix 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 -- Format current frame time local CurrentTime=LoGetModelTime(); local FormatedTime=string.format("#%.2f\n",CurrentTime); local ShouldUpdateLog=false; -- Add/Update default objects if CurrentTime>=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 -- Reduce ID size (to compact text log) local OptimizedID=self.GetOptimizedID(ID); -- 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 -- 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) local RefX,RefZ=getXYCoords(Latitude,Longitude); local ToNorthPosX,ToNorthPosZ=getXYCoords(Latitude+.001,Longitude); local ToNorthX=ToNorthPosZ-RefZ; local ToNorthY=ToNorthPosX-RefX; local ToNorthLength=math.sqrt(ToNorthX*ToNorthX+ToNorthY*ToNorthY); if ToNorthLength>0 then return Yaw-math.asin(ToNorthX/ToNorthLength); end return Yaw; 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(); -- 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(AcmiData.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(AcmiData.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 enougth 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 io.close(self.AcmiFile) self.AcmiFile=nil; 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