1. This site uses cookies. By continuing to use this site, you are agreeing to our use of cookies. Learn More.
  2. Two Factor Authentication is now available on BeyondUnreal Forums. To configure it, visit your Profile and look for the "Two Step Verification" option on the left side. We can send codes via email (may be slower) or you can set up any TOTP Authenticator app on your phone (Authy, Google Authenticator, etc) to deliver codes. It is highly recommended that you configure this to keep your account safe.

UE2 - UT2kX Linux based server crash when using FTP based code

Discussion in 'Programming' started by forrestmark9, May 5, 2014.

  1. forrestmark9

    forrestmark9 New Member

    Joined:
    Aug 6, 2009
    Messages:
    56
    Likes Received:
    0
    Yes and few people have been trying to correct a problem with a perk database for Killing Floor that connects to a FTP server and gets perk stats, but anyway the problem is on any Linux based OS the FTP code will cause a crash on every map change, spam the chat with FTP connection timed-out, and does not save the stats as it does not attempt a upload.

    Here is the code of the FTP classes, if you need more code I can provide is

    Code:
    Class FTPTcpLink extends TcpLink;
    
    var array<ServerStStats> PendingLoaders;
    var array<StatsObject> ToSave;
    var ServerPerksMut Mut;
    var IpAddr SiteAddress;
    var FTPDataConnection DataConnection;
    var transient float WelcomeTimer;
    var array<string> TotalList;
    var MessagingSpectator WebAdminController;
    var bool bConnectionBroken,bFullVerbose,bUploadAllStats,bTotalUpload,bFileInProgress,bLogAllCommands,bCheckedWeb;
    
    function BeginEvent()
    {
    	Mut.SaveAllStats = SaveAllStats;
    	Mut.RequestStats = RequestStats;
    	if( Mut.bDebugDatabase )
    	{
    		bLogAllCommands = true;
    		bFullVerbose = true;
    	}
    
    	LinkMode = MODE_Line;
    	ReceiveMode = RMODE_Event;
    	Resolve(Mut.RemoteDatabaseURL);
    }
    final function ReportError( string InEr )
    {
    	if( !bConnectionBroken )
    	{
    		Level.Game.Broadcast(Self,"FTP Error: "$InEr);
    		Log("FTP Error: "$InEr,Class.Name);
    	}
    	bConnectionBroken = true;
    	GoToState('ErrorState');
    }
    event Resolved( IpAddr Addr )
    {
    	SiteAddress = Addr;
    	SiteAddress.Port = Mut.RemotePort;
    	GoToState('Idle');
    }
    event ResolveFailed()
    {
    	ReportError("Couldn't resolve address, aborting...");
    }
    event Closed()
    {
    	ReportError("Connection was closed by FTP server!");
    }
    final function DebugLog( string Str )
    {
    	if( !bCheckedWeb )
    	{
    		bCheckedWeb = true;
    		foreach AllActors(class'MessagingSpectator',WebAdminController)
    			break;
    	}
    	if( WebAdminController!=None )
    		WebAdminController.ClientMessage(Str,'FTP');
    	Log(Str,'FTP');
    }
    event ReceivedLine( string Text )
    {
    	if( bLogAllCommands )
    		DebugLog("ReceiveFTP "$GetStateName()$":"@Text);
    	ProcessResponse(int(Left(Text,3)),Mid(Text,4));
    }
    final function SendFTPLine( string Text )
    {
    	if( bLogAllCommands )
    		DebugLog("SendFTP "$GetStateName()$":"@Text);
    	SendText(Text);
    }
    
    function SaveAllStats()
    {
    	local int i;
    
    	if( bTotalUpload )
    		return;
    	ToSave = Mut.ActiveStats;
    	for( i=0; i<ToSave.Length; ++i )
    	{
    		if( !ToSave[i].bStatsChanged )
    			ToSave.Remove(i--,1);
    	}
    	if( ToSave.Length>0 )
    		bUploadAllStats = true;
    }
    function RequestStats( ServerStStats Other )
    {
    	local int i;
    	
    	if( bTotalUpload )
    		return;
    	for( i=0; i<PendingLoaders.Length; ++i )
    	{
    		if( PendingLoaders[i]==None )
    			PendingLoaders.Remove(i--,1);
    		else if( PendingLoaders[i]==Other )
    			return;
    	}
    	PendingLoaders[PendingLoaders.Length] = Other;
    }
    final function FullUpload()
    {
    	TotalList = GetPerObjectNames("ServerPerksStat","StatsObject",9999999);
    	bTotalUpload = true;
    	bUploadAllStats = true;
    	bFullVerbose = true;
    	HasMoreStats();
    	SaveAllStats();
    }
    final function bool HasMoreStats()
    {
    	local byte i;
    	local int j;
    	
    	if( TotalList.Length==0 )
    		return false;
    	j = ToSave.Length;
    	for( i=0; i<Min(20,TotalList.Length); ++i )
    	{
    		ToSave.Length = j+1;
    		ToSave[j] = new(None,TotalList[i]) Class'StatsObject';
    		++j;
    	}
    	TotalList.Remove(0,20);
    	return true;
    }
    final function CheckNextCommand()
    {
    	while( PendingLoaders.Length>0 && PendingLoaders[0]==None )
    		PendingLoaders.Remove(0,1);
    
    	if( bUploadAllStats || (bTotalUpload && HasMoreStats()) )
    		GoToState('UploadStats','Begin');
    	else if( PendingLoaders.Length>0 )
    		GoToState('DownloadStats','Begin');
    	else
    	{
    		if( bFullVerbose )
    			Level.Game.Broadcast(Self,"FTP: All done!");
    		if( Mut.FTPKeepAliveSec>0 && !Level.Game.bGameEnded )
    			GoToState('KeepAlive');
    		else GoToState('EndConnection');
    	}
    }
    function ProcessResponse( int Code, string Line )
    {
    	switch( Code )
    	{
    	case 220: // Welcome
    		if( WelcomeTimer<Level.TimeSeconds )
    		{
    			SendFTPLine("USER "$Mut.RemoteFTPUser);
    			WelcomeTimer = Level.TimeSeconds+0.2;
    		}
    		break;
    	case 331: // Password required
    		SendFTPLine("PASS "$Mut.RemotePassword);
    		break;
    	case 230: // User logged in.
    		if( Mut.RemoteFTPDir!="" )
    			SendFTPLine("CWD "$Mut.RemoteFTPDir);
    		else SendFTPLine("TYPE A");
    		break;
    	case 250: // CWD command successful.
    		SendFTPLine("TYPE A");
    		break;
    	case 200: // Type set to A
    		CheckNextCommand();
    		break;
    	case 226: // File successfully transferred
    	case 150: // Opening ASCII mode data connection
    		break;
    	case 421: // No transfer timeout: closing control connection
    		if( bFullVerbose )
    			Level.Game.Broadcast(Self,"FTP: Connection timed out, reconnecting!");
    		GoToState('EndConnection');
    		break;
    	default:
    		if( bFullVerbose )
    			Level.Game.Broadcast(Self,"FTP: Unknown FTP code '"$Code$"': "$Line);
    		Log("Unknown FTP code '"$Code$"': "$Line,Class.Name);
    	}
    }
    function DataReceived();
    
    final function bool OpenDataConnection( string S, bool bUpload )
    {
    	local int i,j;
    	local IpAddr A;
    
    	A = SiteAddress;
    	
    	// Get destination port
    	S = Mid(S,InStr(S,"(")+1);
    	for( i=0; i<4; ++i ) // Skip IP address
    		S = Mid(S,InStr(S,",")+1);
    	i = InStr(S,",");
    	A.Port = int(Left(S,i))*256 + int(Mid(S,i+1));
    	
    	// Now attempt to bind port and open connection.
    	for( j=0; j<20; ++j )
    	{
    		if( DataConnection!=None )
    			DataConnection.Destroy();
    		DataConnection = Spawn(Class'FTPDataConnection',Self);
    		DataConnection.bUpload = bUpload;
    		DataConnection.BindPort(500+Rand(5000),true);
    		if( DataConnection.OpenNoSteam(A) )
    			return true;
    	}
    	DataConnection.Destroy();
    	DataConnection = None;
    	ReportError("Couldn't bind port for upload data connection!");
    	return false;
    }
    
    function Timer()
    {
    	ReportError("FTP connection timed out!");
    }
    
    state Idle
    {
    Ignores Timer;
    
    	final function StartConnection()
    	{
    		local int i;
    		
    		for( i=0; i<40; ++i )
    		{
    			BindPort(500+Rand(5000),true);
    			if( OpenNoSteam(SiteAddress) )
    			{
    				GoToState('InitConnection');
    				return;
    			}
    		}
    		ReportError("Port couldn't be bound or connection failed to open!");
    	}
    	function SaveAllStats()
    	{
    		Global.SaveAllStats();
    		if( bUploadAllStats )
    			StartConnection();
    	}
    	function RequestStats( ServerStStats Other )
    	{
    		Global.RequestStats(Other);
    		StartConnection();
    	}
    Begin:
    	Sleep(0.1f);
    	if( bUploadAllStats || PendingLoaders.Length>0 )
    		StartConnection();
    }
    state InitConnection
    {
    	function BeginState()
    	{
    		SetTimer(10,false);
    	}
    	event Closed()
    	{
    		ReportError("Connection was closed by FTP server!");
    	}
    }
    state ConnectionBase
    {
    	event Closed()
    	{
    		GoToState('Idle');
    	}
    Begin:
    	while( true )
    	{
    		if( bUploadAllStats && Level.bLevelChange ) // Delay mapchange until all stats are uploaded.
    			Level.NextSwitchCountdown = FMax(Level.NextSwitchCountdown,1.f);
    		Sleep(0.5);
    	}
    }
    state EndConnection extends ConnectionBase
    {
    	function BeginState()
    	{
    		SendFTPLine("QUIT");
    		Close();
    		SetTimer(4,false);
    	}
    }
    state KeepAlive extends ConnectionBase
    {
    Ignores Timer;
    
    	function SaveAllStats()
    	{
    		Global.SaveAllStats();
    		if( bUploadAllStats )
    			StartConnection();
    	}
    	function RequestStats( ServerStStats Other )
    	{
    		Global.RequestStats(Other);
    		StartConnection();
    	}
    	final function StartConnection()
    	{
    		CheckNextCommand();
    	}
    Begin:
    	while( true )
    	{
    		if( bUploadAllStats || PendingLoaders.Length>0 )
    			StartConnection();
    		Sleep(Mut.FTPKeepAliveSec);
    		SendFTPLine("NOOP");
    	}
    }
    state UploadStats extends ConnectionBase
    {
    	function BeginState()
    	{
    		bUploadAllStats = false;
    		SetTimer(10,false);
    	}
    	function SaveAllStats();
    	
    	final function InitDataConnection( string S )
    	{
    		if( bFullVerbose )
    			Level.Game.Broadcast(Self,"FTP: Upload stats for "$ToSave[0].PlayerName$" ("$(ToSave.Length-1+TotalList.Length)$" remains)");
    		if( OpenDataConnection(S,true) )
    		{
    			DataConnection.Data = ToSave[0].GetSaveData();
    			SendFTPLine("STOR "$ToSave[0].Name$".txt");
    			bFileInProgress = true;
    		}
    	}
    	final function NextPackage()
    	{
    		ToSave.Remove(0,1);
    		if( ToSave.Length==0 )
    			CheckNextCommand();
    		else SendFTPLine("PASV");
    	}
    	function ProcessResponse( int Code, string Line )
    	{
    		switch( Code )
    		{
    		case 200: // Type set to A
    			// SendFTPLine("PASV");
    			break;
    		case 227: // Entering passive mode
    			if( !bFileInProgress )
    				InitDataConnection(Line);
    			break;
    		case 150: // Opening ASCII mode data connection for file
    			SetTimer(60,false);
    			if( DataConnection!=None )
    				DataConnection.BeginUpload();
    			break;
    		case 226: // File transfer completed.
    			if( bFileInProgress )
    			{
    				SetTimer(10,false);
    				bFileInProgress = false;
    				NextPackage();
    			}
    			break;
    		default:
    			Global.ProcessResponse(Code,Line);
    		}
    	}
    Begin:
    	SendFTPLine("PASV");
    	while( true )
    	{
    		if( Level.bLevelChange ) // Delay mapchange until all stats are uploaded.
    		{
    			bFullVerbose = true;
    			Level.NextSwitchCountdown = FMax(Level.NextSwitchCountdown,1.f);
    		}
    		Sleep(0.5);
    	}
    }
    state DownloadStats extends ConnectionBase
    {
    	function BeginState()
    	{
    		SetTimer(10,false);
    	}
    	final function InitDataConnection( string S )
    	{
    		while( PendingLoaders.Length>0 && PendingLoaders[0]==None )
    			PendingLoaders.Remove(0,1);
    		if( PendingLoaders.Length==0 )
    		{
    			CheckNextCommand();
    			return;
    		}
    
    		if( bFullVerbose )
    			Level.Game.Broadcast(Self,"FTP: Download stats for "$PendingLoaders[0].MyStatsObject.PlayerName$" ("$(PendingLoaders.Length-1)$" remains)");
    
    		if( OpenDataConnection(S,false) )
    		{
    			DataConnection.OnCompleted = DataReceived;
    			SendFTPLine("RETR "$PendingLoaders[0].MyStatsObject.Name$".txt");
    			bFileInProgress = true;
    		}
    	}
    	function DataReceived()
    	{
    		bFileInProgress = false;
    		if( PendingLoaders[0]!=None )
    		{
    			if( DataConnection!=None )
    				PendingLoaders[0].GetData(DataConnection.Data);
    			else PendingLoaders[0].GetData("");
    		}
    		PendingLoaders.Remove(0,1);
    		while( PendingLoaders.Length>0 && PendingLoaders[0]==None )
    			PendingLoaders.Remove(0,1);
    
    		if( bUploadAllStats ) // Saving has higher priority.
    			GoToState('UploadStats');
    		else if( PendingLoaders.Length>0 )
    			SendFTPLine("PASV");
    		else CheckNextCommand();
    	}
    	function ProcessResponse( int Code, string Line )
    	{
    		switch( Code )
    		{
    		case 200: // Type set to A
    			// SendFTPLine("PASV");
    			break;
    		case 227: // Entering passive mode
    			if( !bFileInProgress )
    				InitDataConnection(Line);
    			break;
    		case 150: // Opening ASCII mode data connection for file
    			SetTimer(60,false);
    			break;
    		case 550: // No such file or directory
    			SetTimer(10,false);
    			if( bFileInProgress )
    			{
    				if( DataConnection!=None )
    					DataConnection.Destroy();
    				DataReceived();
    			}
    			break;
    		default:
    			Global.ProcessResponse(Code,Line);
    		}
    	}
    Begin:
    	SendFTPLine("PASV");
    	while( true )
    	{
    		if( bUploadAllStats && Level.bLevelChange ) // Delay mapchange until all stats are uploaded.
    		{
    			bFullVerbose = true;
    			Level.NextSwitchCountdown = FMax(Level.NextSwitchCountdown,1.f);
    		}
    		Sleep(0.5);
    	}
    }
    state ErrorState
    {
    Ignores SaveAllStats,RequestStats;
    Begin:
    	Sleep(1.f);
    	Mut.RespawnNetworkLink();
    }
    
    defaultproperties
    {
    }
    Code:
    Class FTPDataConnection extends TcpLink;
    
    var bool bUpload,bWasOpened;
    var string Data;
    
    delegate OnCompleted();
    
    function PostBeginPlay()
    {
    	LinkMode = MODE_Text;
    }
    event Opened()
    {
    	BeginUpload();
    }
    event Closed()
    {
    	OnCompleted();
    	Destroy();
    }
    event ReceivedText( string Text )
    {
    	if( bUpload )
    		Log(Text,'FTPD');
    	else Data $= Text;
    }
    function BeginUpload()
    {
    	if( bWasOpened )
    		GoToState('Uploading');
    	else bWasOpened = true;
    }
    
    state Uploading
    {
    	function BeginState()
    	{
    		Tick(0.f);
    	}
    	function Tick( float Delta )
    	{
    		if( Data!="" )
    		{
    			SendText(Left(Data,250));
    			Data = Mid(Data,250);
    		}
    		else Close();
    	}
    }
    
     
  2. WGH

    WGH New Member

    Joined:
    Jan 22, 2006
    Messages:
    237
    Likes Received:
    0
    Honestly, FTP seems to be a weird choice for this job.
     
    Last edited: May 5, 2014
  3. Wormbo

    Wormbo Administrator Staff Member

    Joined:
    Jun 4, 2001
    Messages:
    5,913
    Likes Received:
    36
    What WGH wrote. You might want to try elmuerte's LibHTTP4 instead and send the data via POST request.

    As for the crash: "Logs or it didn't happen." ;)

    BTW: Ports below 1024 are not available to non-root processes on Linux. Those various BindPort() calls not only ignore that, but also disregard the fact that passing true as second parameter already will try a range of available ports in case the one specified isn't available. But in any case, you should not even specify any port at all for a clientside of a connection, because the Unreal engine will do exactly what your code does a poor job of trying - pick a random port.
     
    Last edited: May 5, 2014
  4. forrestmark9

    forrestmark9 New Member

    Joined:
    Aug 6, 2009
    Messages:
    56
    Likes Received:
    0
    Hmm I see Marco over at TWI forums was the one who made this code, he made two options this FTP code here or a custom .exe database (Which uses UDPLink and sends 3 or 4 1-byte packets containing passwords and several other things) both of them have some bad problems such as the .exe overwriting peoples with anothers or blanking them if the server crashes on a map change, and the problems with FTP I listed.

    Also I can get a log as soon as TWI finishes updating there forums so I can get to the thread I posted about this
     
    Last edited by a moderator: May 5, 2014
  5. forrestmark9

    forrestmark9 New Member

    Joined:
    Aug 6, 2009
    Messages:
    56
    Likes Received:
    0
    Here is the crash log from TWI forums I posted about, not much is shown but it always crashes when it says "Unknown FTP code '221': Goodbye."

     
  6. Wormbo

    Wormbo Administrator Staff Member

    Joined:
    Jun 4, 2001
    Messages:
    5,913
    Likes Received:
    36
    The crash is not related to the FTP protocol. It's caused by a corrupted package file, either the KF-WestLondon map file or any of the packages it depends on.
     
  7. forrestmark9

    forrestmark9 New Member

    Joined:
    Aug 6, 2009
    Messages:
    56
    Likes Received:
    0
    That's odd cause this crash only happened when the FTP database was enabled
     
  8. forrestmark9

    forrestmark9 New Member

    Joined:
    Aug 6, 2009
    Messages:
    56
    Likes Received:
    0
    Marco doesn't seem to want to change things to use libHTTP4 and says the port issue will most likely not fix things. He thinks that Tripwire may have broken something in IpDrv.

    I've decided to make my own custom method using libHTTP4 but sadly I do not know where to start except I need to spawn HttpSock. I've never had experience using UDPLink or TCPLink
     
    Last edited by a moderator: May 12, 2014
  9. Wormbo

    Wormbo Administrator Staff Member

    Joined:
    Jun 4, 2001
    Messages:
    5,913
    Likes Received:
    36
    LibHTTP4 is quite easy to use for basic stuff. To make an HTTP request, you simply call the get(), head(), post() or postex() function on the HttpSock instance you spawned. The get() and head() functions make GET and HEAD requests, respectively. The difference is that a HEAD request expects only HTTP headers to be returned, but is otherwise identical to the GET request.

    LibHTTP comes complete with support for authentication, cookies and proxies. It can also evaluate HTTP redirects via 3xx status codes. The content interpretation for returned data is left entirely up to you, though. For example for the UTAN Ban Manager v104 I created a ban database update protocol that allows the server to send ban data in a custom line-based format that contains instructions which bans to add or remove. Of course you could also use standard formats like JSON or XML to wrap your response or reply data. You will have to implement appropriate parsing, though.

    If you want to send data, you usually pick from post() and postex(), although it is also possible to send data with the other request types. The difference between post() and postex() is that the former takes an optional string parameter for POST content, while the latter takes an optional string array parameter for the same purpose.

    If you want to use the HTTP response, you should assign a callback function to the HttpSock's OnComplete delegate like in this example. The function will be called when the HTTP request completes successfully.

    If the returned data is too large for single-pass processing, you might consider processing it as it comes in via the OnResponseBody delegate, which is called for each line of received response body (not headers) as they come in. If you need even more control, you could even extend from the HttpSock class. You will have to do that e.g. if you want to modify the HTTP UserAgent string.
     
  10. forrestmark9

    forrestmark9 New Member

    Joined:
    Aug 6, 2009
    Messages:
    56
    Likes Received:
    0
    Hmm I see that is quite interesting but sadly I would not know to setup things such as authentication and file types as I've never really worked with HTTP or XML

    Such as an example here from the .ini from the .exe Database
    Code:
    [76561197997881512]
    [WPC]_Forrest_Mark_X=SRVetScientist,10072068,10145656,56081791,10003592,10000033,10267987,10563821,91783779,10001152,10000020,10972567,10000002,9999999,10000185,10000647,10029994,10000637,10000830,10047318,30143945,10047147,10412619,10000092,10000166,'ForrestCharacters.Tyrael',((N="FMXAch",V="DF"),(N="AchMaps",V="00000300000F3000000F01000000000000F000000000"),(N="VIPInfo",V="0201"),(N="BenelliDamageProgress",V="13227489"),(N="AutoShotgunDamageProgress",V="10521200"),(N="KatanaDamageProgress",V="10117652"),(N="MachineGunDamageProgress",V="95792892"),(N="MedicGunDamageProgress",V="995230981"),(N="VirusKillsProgress",V="10001849"),(N="ScrnPistolDamageProgress",V="90388926"),(N="LAWDamageProgress",V="15470964"),(N="ScrnPistolKillProgress",V="9999999"),(N="TurretDamageProgress",V="91019897"),(N="Ach",V="F2DBEDF6813C19193801B02D1057840080076091F033005166F8BD80C43E00147D80BD0F78FC3E5C57E43980C40C72007001"),(N="D3Ach",V="9A2ADE1B1F1CFE8007F40F"))
    The first is the playername, his current selected perk, the rest before ForrestCharacters.Tyrael is stats value for default KF perks, the name parameter is the players selected character, everything after that are custom-stat values added via a special function

    I'd need to find the player by there SteamID then load there stats after that send the loaded stats to the StatsObject or the Mutator. I'm not sure how Marco has everything setup

    The FTP database does something similiar but it saves seperate .txt files the name of the file is the players SteamID and the contents are the rest. Marco set it up in a way that I would need as it saves the file in a .tmp to keep players stats from being overwritten if say the server crashed

    So far this is all I got
    Code:
    function RespawnNetworkLink()
    {
    	if( Link!=None )
    		Link.Destroy();
    	if( HttpLink!=None )
    		HttpLink.Destroy();
    	if( bUseHTTPLink )
    	{
    		HttpLink = Spawn(Class'SRHttpSock');
    		SRHttpSock(HttpLink).Mut = Self;
    	}
    	else if( !bUseFTPLink )
    	{
    		Link = Spawn(Class'DatabaseUdpLink');
    		DatabaseUdpLink(Link).Mut = Self;
    	}
    	else
    	{
    		Link = Spawn(Class'FMXFTPTcpLink');
    		FMXFTPTcpLink(Link).Mut = Self;
    	}
    	if( SRHttpSock(HttpLink) != none )
    		SRHttpSock(HttpLink).BeginEvent();
    	else
    		Link.BeginEvent();
    }
    Code:
    class SRHttpSock extends HttpSock;
    
    var ServerPerksMut Mut;
    
    function BeginEvent()
    {
    	Get(Mut.RemoteDatabaseURL);
    }
    
    defaultproperties
    {
    }
    Well looking at Marcos code I need to add a SaveAllStats() function as this is called by the mutator when stats are saved, I would also seem to need to add a array of "current stats" (ToSave)
    I do know that if I need to get someones stats I should do something like Get("http://"$Mut.RemoteDatabaseURL$FMXServerPerksMut(Mut).HTTPFolder$ToSave[0].Name$".txt")

    There also seems to be a function I have to call called GetSaveData() which is in StatsObject, this returns the stat values of the person perks. If I'm correct I would use POST for this? like this? Post("http://"$Mut.RemoteDatabaseURL$FMXServerPerksMut(Mut).HTTPFolder$ToSave[0].Name$".txt.tmp", ToSave[0].GetSaveData());

    Here is the code
    Code:
    final function string GetSaveData()
    {
    	local string Result;
    
    	Result = SelectedVeterancy$","$DamageHealedStat$","$WeldingPointsStat$","$ShotgunDamageStat$","$HeadshotKillsStat$","$StalkerKillsStat;
    	Result = Result$","$BullpupDamageStat$","$MeleeDamageStat$","$FlameThrowerDamageStat$","$SelfHealsStat$","$SoleSurvivorWavesStat;
    	Result = Result$","$CashDonatedStat$","$FeedingKillsStat$","$BurningCrossbowKillsStat$","$GibbedFleshpoundsStat$","$StalkersKilledWithExplosivesStat;
    	Result = Result$","$GibbedEnemiesStat$","$BloatKillsStat$","$SirenKillsStat$","$KillsStat$","$ExplosivesDamageStat;
    	Result = Result$","$int(TotalZedTimeStat)$","$TotalPlayTime$","$WinsCount$","$LostsCount$",'"$SelectedChar$"'";
    	Result = Result$","$GetPropertyText("CC");
    	return Result;
    }
     
    Last edited by a moderator: May 13, 2014
  11. forrestmark9

    forrestmark9 New Member

    Joined:
    Aug 6, 2009
    Messages:
    56
    Likes Received:
    0
    Apparently this is how the FTP does everything as said by someone on the TWI forums

     

Share This Page