using System; using System.Collections.Generic; using System.Globalization; using System.Net; using System.Net.Sockets; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Timers; using Microsoft.Win32; using Springie.SpringNamespace; using Timer=System.Timers.Timer; namespace Springie.Client { public partial class TasClient { // server info #region SayPlace enum public enum SayPlace { Channel, Battle, User } ; #endregion public const int MaxRank = 4; public const int MinRank = 0; private Battle battle = null; private int battleID = 0; private ServerConnection con = null; private Dictionary existingChannels = new Dictionary(); private Dictionary existingUsers = new Dictionary(); private bool isChanScanning = false; private bool isConnected = false; private bool isLoggedIn = false; private Dictionary joinedChannels = new Dictionary(); private int lastSpectatorCount; private int lastUdpSourcePort = 0; private bool lockToChangeTo; private MapInfo mapToChangeTo; private int pingInterval = 10; // how often to ping server (in seconds) private Timer pingTimer; private string serverHost = ""; private IPAddress serverIp; private int serverPort = 0; private int serverUdpHolePunchingPort = 0; private string serverVersion = ""; private bool startingAfterUdpPunch = false; private Timer udpPunchingTimer = new Timer(400); // user info private string username = ""; public TasClient() { pingTimer = new Timer(pingInterval*1000); pingTimer.AutoReset = true; pingTimer.Elapsed += OnPingTimer; pingTimer.Start(); udpPunchingTimer.Elapsed += new ElapsedEventHandler(udpPunchingTimer_Elapsed); udpPunchingTimer.AutoReset = true; ConnectionLost += RaiseFailure; LoginDenied += RaiseFailure; ChannelJoinFailed += RaiseFailure; BattleOpenFailed += RaiseFailure; } public string UserName { get { return username; } } public bool IsLoggedIn { get { return isLoggedIn; } } public int PingInterval { get { return pingInterval; } set { pingInterval = value; pingTimer.Interval = pingInterval*1000; } } public bool IsConnected { get { return isConnected; } } // group events public event EventHandler Failure; //this event is fired whenever any failure events fire public event EventHandler Said; // this is fired when any kind of say message is recieved // invidivudal events public event EventHandler Connected; public event EventHandler ConnectionLost; public event EventHandler LoginAccepted; public event EventHandler LoginDenied; public event EventHandler ChannelJoined; public event EventHandler ChannelLeft; public event EventHandler ChannelJoinFailed; public event EventHandler ChannelListDone; public event EventHandler UserAdded; public event EventHandler UserRemoved; public event EventHandler BattleOpenFailed; public event EventHandler BattleOpened; public event EventHandler BattleClosed; public event EventHandler BattleUserJoined; public event EventHandler BattleUserLeft; public event EventHandler BattleUserStatusChanged; public event EventHandler BattleUserIpRecieved; public event EventHandler BattleMapChanged; public event EventHandler BattleLockChanged; public event EventHandler BattleDisabledUnitsChanged; public event EventHandler BattleDetailsChanged; public event EventHandler BattleFound; public event EventHandler ChannelUserAdded; public event EventHandler ChannelUserRemoved; public event EventHandler ChannelTopicChanged; public event EventHandler UserStatusChanged; public event EventHandler MyStatusChangedToInGame; public Dictionary GetJoinedChannels() { return new Dictionary(joinedChannels); } public bool GetExistingUser(string name, out User u) { return existingUsers.TryGetValue(name, out u); } public void Connect(string host, int port) { serverHost = host; serverIp = Dns.GetHostAddresses(host)[0]; serverPort = port; battle = null; battleID = 0; existingUsers = new Dictionary(); existingChannels = new Dictionary(); joinedChannels = new Dictionary(); isChanScanning = false; isLoggedIn = false; isConnected = false; username = ""; try { con = new ServerConnection(); con.Connect(host, port); con.ConnectionClosed += OnConnectionClosed; con.CommandRecieved += OnCommandRecieved; } catch { con = null; if (ConnectionLost != null) ConnectionLost(this, new TasEventArgs("Cannot connect to remote machine")); } } public void Disconnect() { if (con != null && isConnected) con.Close(); //con.CommandRecieved existingUsers = new Dictionary(); existingChannels = new Dictionary(); joinedChannels = new Dictionary(); battle = null; battleID = 0; username = ""; isLoggedIn = false; isConnected = false; isChanScanning = false; } private void OnPingTimer(object sender, EventArgs args) { if (isConnected && con != null) con.SendCommand(0, "PING"); } public void Login(string login, string password, string appinfo) { if (con == null) throw new TasClientException("Not connected"); string localIp = Dns.GetHostAddresses(Dns.GetHostName())[0].ToString(); string mhz = Registry.GetValue("HKEY_LOCAL_MACHINE\\HARDWARE\\DESCRIPTION\\SYSTEM\\CentralProcessor\\0", "~MHz", 0).ToString(); string mhz2 = ""; if (Registry.GetValue("HKEY_LOCAL_MACHINE\\HARDWARE\\DESCRIPTION\\SYSTEM\\CentralProcessor\\0", "VendorIdentifier", 0).ToString().Contains("AMD")) { Match m = Regex.Match(Registry.GetValue("HKEY_LOCAL_MACHINE\\HARDWARE\\DESCRIPTION\\SYSTEM\\CentralProcessor\\0", "ProcessorNameString", 0).ToString(), "[^0-9]*([0-9]*)"); if (m.Groups.Count > 1) mhz2 = m.Groups[1].Value; int i, o; int.TryParse(mhz2, out o); int.TryParse(mhz, out i); if (o > i) mhz = o.ToString(); } con.SendCommand(0, "LOGIN", login, HashPassword(password), mhz, localIp, appinfo); } public void ListChannels() { isChanScanning = true; existingChannels.Clear(); con.SendCommand(0, "CHANNELS"); } public Battle GetBattle() { if (battle != null) return (Battle)battle.Clone(); else return null; } public Dictionary GetExistingChannels() { if (isChanScanning) throw new TasClientException("Channel scan operation in progress"); return new Dictionary(existingChannels); } public void JoinChannel(string channelName) { JoinChannel(channelName, null); } public void JoinChannel(string channelName, string key) { if (con == null) throw new TasClientException("Not connected"); if (!String.IsNullOrEmpty(key)) con.SendCommand(0, "JOIN", channelName, HashPassword(key)); else con.SendCommand(0, "JOIN", channelName); } public void LeaveChannel(string channelName) { con.SendCommand(0, "LEAVE", channelName); joinedChannels.Remove(channelName); if (ChannelLeft != null) ChannelLeft(this, new TasEventArgs(channelName)); } public void ChangeMyStatus(bool isAway, bool isInGame) { User u = new User(); u.isAway = isAway; u.isInGame = isInGame; con.SendCommand(0, "MYSTATUS", u.ToInt()); } /// /// Starts game and automatically does hole punching if necessary /// public void StartGame() { if (battle != null) { if (battle.Nat == Battle.NatMode.HolePunching) { startingAfterUdpPunch = true; SendUdpPacket(0, serverIp, serverUdpHolePunchingPort); udpPunchingTimer.Start(); } else ChangeMyStatus(false, true); } } private void udpPunchingTimer_Elapsed(object sender, ElapsedEventArgs e) { SendUdpPacket(lastUdpSourcePort, serverIp, serverUdpHolePunchingPort); } private void SendUdpPacket(int sourcePort, IPAddress targetIp, int targetPort) { IPAddress[] ret = Dns.GetHostAddresses(serverHost); Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); IPEndPoint local = new IPEndPoint(IPAddress.Any, sourcePort); s.Bind(local); //s.ExclusiveAddressUse = false; lastUdpSourcePort = ((IPEndPoint)s.LocalEndPoint).Port; s.Connect(targetIp, targetPort); s.Send(ASCIIEncoding.ASCII.GetBytes(UserName)); s.Close(); } /// /// Say something through chat system /// /// Pick user (private message) channel or battle /// Channel or User name /// chat text /// is message emote? (channel or battle only) public void Say(SayPlace place, string channel, string text, bool isEmote) { if (String.IsNullOrEmpty(text)) return; switch (place) { case SayPlace.Channel: if (isEmote) con.SendCommand(0, "SAYEX", channel, text); else con.SendCommand(0, "SAY", channel, text); break; case SayPlace.User: con.SendCommand(0, "SAYPRIVATE", channel, text); break; case SayPlace.Battle: if (isEmote) con.SendCommand(0, "SAYBATTLEEX", text); else con.SendCommand(0, "SAYBATTLE", text); break; } } public void GameSaid(string username, string text) { if (Said != null) Said(this, new TasSayEventArgs(TasSayEventArgs.Origins.Player, TasSayEventArgs.Places.Game, "", username, text, false)); } public void OpenBattle(Battle nbattle) { LeaveBattle(); // leave current battle battleID = -1; battle = (Battle)nbattle.Clone(); List objList = new List(); objList.Add(0); // type = normal objList.Add((int)battle.Nat); objList.Add(battle.Password); objList.Add(battle.HostPort); objList.Add(battle.MaxPlayers); ; //battle.Details.AddToParamList(objList); objList.Add(battle.Mod.Checksum); objList.Add(battle.Rank); objList.Add(battle.Map.Checksum); objList.Add(battle.Map.Name); objList.Add('\t' + battle.Title); objList.Add('\t' + battle.Mod.Name); mapToChangeTo = battle.Map; lockToChangeTo = false; con.SendCommand(0, "OPENBATTLE", objList.ToArray()); lastSpectatorCount = -1; // send predefined starting rectangles foreach (KeyValuePair v in battle.Rectangles) con.SendCommand(0, "ADDSTARTRECT", v.Key, v.Value.Left, v.Value.Top, v.Value.Right, v.Value.Bottom); } public void LeaveBattle() { if (battle != null) { con.SendCommand(0, "LEAVEBATTLE"); battle = null; battleID = 0; if (BattleClosed != null) BattleClosed(this, new TasEventArgs()); } } public void AddBattleRectangle(int allyno, BattleRect rect) { if (battle != null) { if (allyno < Spring.MaxAllies && allyno >= 0) { RemoveBattleRectangle(allyno); battle.Rectangles.Add(allyno, rect); con.SendCommand(0, "ADDSTARTRECT", allyno, rect.Left, rect.Top, rect.Right, rect.Bottom); } } } public void RemoveBattleRectangle(int allyno) { if (battle != null) { if (battle.Rectangles.ContainsKey(allyno)) { battle.Rectangles.Remove(allyno); con.SendCommand(0, "REMOVESTARTRECT", allyno); } } } public void DisableUnits(params string[] units) { if (battle != null) { List tmp = new List(units); foreach (string s in tmp) { if (!battle.DisabledUnits.Contains(s)) { UnitInfo u; if (battle.Mod.GetUnitInfo(s, out u)) battle.DisabledUnits.Add(s); } } if (battle.DisabledUnits.Count > 0) { con.SendCommand(0, "DISABLEUNITS", battle.DisabledUnits.ToArray()); if (BattleDisabledUnitsChanged != null) BattleDisabledUnitsChanged(this, new TasEventArgs(battle.DisabledUnits.ToArray())); } } } public void EnableAllUnits() { if (battle != null) { battle.DisabledUnits.Clear(); con.SendCommand(0, "ENABLEALLUNITS"); if (BattleDisabledUnitsChanged != null) BattleDisabledUnitsChanged(this, new TasEventArgs(battle.DisabledUnits.ToArray())); } } private void UpdateBattleInfo(bool lck, MapInfo mapname) { if (battle != null) { lockToChangeTo = lck; con.SendCommand(0, "UPDATEBATTLEINFO", battle.CountSpectators(), (int)(lck ? 1 : 0), mapname.Checksum, mapname.Name); } } public void ChangeLock(bool lck) { if (battle != null && lck != lockToChangeTo) UpdateBattleInfo(lck, mapToChangeTo); } private void UpdateSpectators() { if (battle != null) { int n = battle.CountSpectators(); if (n != lastSpectatorCount) { lastSpectatorCount = n; con.SendCommand(0, "UPDATEBATTLEINFO", n, (int)(battle.IsLocked ? 1 : 0), battle.Map.Checksum, battle.Map.Name); } } } public void ChangeMap(MapInfo nmap) { if (battle != null) { mapToChangeTo = nmap; UpdateBattleInfo(lockToChangeTo, nmap); } } public void UpdateBattleDetails(BattleDetails bd) { if (battle != null) { List objList = new List(); con.SendCommand(0, "SETSCRIPTTAGS", bd.GetParamList()); } } public void SetScriptTag(string data) { con.SendCommand(0, "SETSCRIPTTAGS", data); } private void OnConnectionClosed(object sender, EventArgs args) { if (sender == con) { isConnected = false; if (ConnectionLost != null) ConnectionLost(this, new TasEventArgs("Connection was closed")); Disconnect(); } } private void OnCommandRecieved(object sender, ServerConnectionEventArgs args) { if (sender == con) DispatchServerCommand(args.Command, (string[])args.Parameters); } /// /// purpose of this event handler is to redirect "fail" events to failure event too /// /// /// private void RaiseFailure(object sender, TasEventArgs args) { if (Failure != null) Failure(this, args); } /// /// Hash password with default hash used by remote server /// /// string with password /// hash string public static string HashPassword(string pass) { MD5 md5 = (MD5)HashAlgorithm.Create("MD5"); md5.Initialize(); byte[] hashed = md5.ComputeHash(ASCIIEncoding.ASCII.GetBytes(pass)); return Convert.ToBase64String(hashed); } public void Ring(string username) { con.SendCommand(0, "RING", username); } public void Kick(string username) { con.SendCommand(0, "KICKFROMBATTLE", username); } public void ForceTeam(string username, int team) { con.SendCommand(0, "FORCETEAMNO", username, team); } public void ForceAlly(string username, int ally) { con.SendCommand(0, "FORCEALLYNO", username, ally); } public void ForceSpectator(string username) { con.SendCommand(0, "FORCESPECTATORMODE", username); } public void ForceColor(string username, int color) { con.SendCommand(0, "FORCETEAMCOLOR", username, color); } /// /// Primary method - processes commands from server /// /// command name /// command arguments private void DispatchServerCommand(string command, string[] args) { Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; try { switch (command) { case "TASServer": // happens after connecting to server serverVersion = args[0]; int.TryParse(args[2], out serverUdpHolePunchingPort); isConnected = true; if (Connected != null) Connected(this, new TasEventArgs()); break; case "ACCEPTED": // Login accepted username = args[0]; isLoggedIn = true; if (LoginAccepted != null) LoginAccepted(this, new TasEventArgs()); break; case "DENIED": // login denied isLoggedIn = false; if (LoginDenied != null) LoginDenied(this, new TasEventArgs(Utils.Glue(args))); break; case "JOIN": // channel joined if (!joinedChannels.ContainsKey(args[0])) joinedChannels.Add(args[0], Channel.Create(args[0])); if (ChannelJoined != null) ChannelJoined(this, new TasEventArgs(args)); break; case "JOINFAILED": // channel join failed if (ChannelJoinFailed != null) ChannelJoinFailed(this, new TasEventArgs(Utils.Glue(args))); break; case "CHANNEL": // iterating channels { ExistingChannel c = new ExistingChannel(); c.name = args[0]; int.TryParse(args[1], out c.userCount); if (args.Length >= 3) c.topic = Utils.Glue(args, 2); existingChannels.Add(c.name, c); } break; case "ENDOFCHANNELS": // end of channel list iteration isChanScanning = false; if (ChannelListDone != null) ChannelListDone(this, new TasEventArgs()); break; case "ADDUSER": // new user joined ta server { User u = User.Create(args[0]); u.country = args[1]; int.TryParse(args[2], out u.cpu); //IPAddress.TryParse(args[3], out u.ip); existingUsers.Add(u.name, u); if (UserAdded != null) UserAdded(this, new TasEventArgs(args)); } break; case "REMOVEUSER": // user left ta server existingUsers.Remove(args[0]); if (UserRemoved != null) UserRemoved(this, new TasEventArgs(args)); break; case "MOTD": // server motd if (Said != null && args.Length > 0) Said(this, new TasSayEventArgs(TasSayEventArgs.Origins.Server, TasSayEventArgs.Places.Motd, "", "", Utils.Glue(args, 0), false)); break; case "SERVERMSG": // server message if (Said != null) Said(this, new TasSayEventArgs(TasSayEventArgs.Origins.Server, TasSayEventArgs.Places.Normal, "", "", Utils.Glue(args, 0), false)); break; case "SERVERMSGBOX": // server messagebox if (Said != null) Said(this, new TasSayEventArgs(TasSayEventArgs.Origins.Server, TasSayEventArgs.Places.MessageBox, "", "", Utils.Glue(args, 0), false)); break; case "CHANNELMESSAGE": // server broadcast to channel if (Said != null) Said(this, new TasSayEventArgs(TasSayEventArgs.Origins.Server, TasSayEventArgs.Places.Channel, args[0], "", Utils.Glue(args, 1), false)); break; case "SAID": // someone said something in channel if (Said != null) Said(this, new TasSayEventArgs(TasSayEventArgs.Origins.Player, TasSayEventArgs.Places.Channel, args[0], args[1], Utils.Glue(args, 2), false)); break; case "SAIDEX": // someone said something with emote in channel if (Said != null) Said(this, new TasSayEventArgs(TasSayEventArgs.Origins.Player, TasSayEventArgs.Places.Channel, args[0], args[1], Utils.Glue(args, 2), true)); break; case "SAYPRIVATE": // sent back from sever when user sends private message if (Said != null) Said(this, new TasSayEventArgs(TasSayEventArgs.Origins.Player, TasSayEventArgs.Places.Normal, args[0], username, Utils.Glue(args, 1), false)); // channel = char partner name break; case "SAIDPRIVATE": // someone said something to me if (Said != null) Said(this, new TasSayEventArgs(TasSayEventArgs.Origins.Player, TasSayEventArgs.Places.Normal, args[0], args[0], Utils.Glue(args, 1), false)); break; case "SAIDBATTLE": // someone said something in battle if (Said != null) Said(this, new TasSayEventArgs(TasSayEventArgs.Origins.Player, TasSayEventArgs.Places.Battle, "", args[0], Utils.Glue(args, 1), false)); break; case "SAIDBATTLEEX": // someone said in battle with emote if (Said != null) Said(this, new TasSayEventArgs(TasSayEventArgs.Origins.Player, TasSayEventArgs.Places.Battle, "", args[0], Utils.Glue(args, 1), true)); break; case "BROADCAST": // server sends urgent broadcast if (Said != null) Said(this, new TasSayEventArgs(TasSayEventArgs.Origins.Server, TasSayEventArgs.Places.Broadcast, "", "", Utils.Glue(args, 0), false)); break; case "REDIRECT": // server sends backup IP // removed due to bugs break; case "CLIENTSTATUS": // client's status changed { int status = 0; int.TryParse(args[1], out status); User u = existingUsers[args[0]]; u.FromInt(status); if (u.name == UserName && (u.isInGame == true && existingUsers[args[0]].isInGame == false)) { existingUsers[args[0]] = u; if (MyStatusChangedToInGame != null) MyStatusChangedToInGame(this, new TasEventArgs()); } existingUsers[args[0]] = u; if (UserStatusChanged != null) UserStatusChanged(this, new TasEventArgs(args)); } break; case "CLIENTS": // client list sent after channel join string[] usrs = Utils.Glue(args, 1).Split(' '); foreach (string s in usrs) joinedChannels[args[0]].channelUsers.Add(s); if (ChannelUserAdded != null) ChannelUserAdded(this, new TasEventArgs(args[0])); break; case "JOINED": // user joined one of my channels joinedChannels[args[0]].channelUsers.Add(args[1]); if (ChannelUserAdded != null) ChannelUserAdded(this, new TasEventArgs(args[0])); break; case "LEFT": // user left one of my channels joinedChannels[args[0]].channelUsers.Remove(args[1]); if (ChannelUserRemoved != null) ChannelUserRemoved(this, new TasEventArgs(args[0], args[1], Utils.Glue(args, 2))); break; case "CHANNELTOPIC": // channel topic update (after joining a channel) { Channel c = joinedChannels[args[0]]; c.topicSetBy = args[1]; c.topicSetDate = ConvertMilisecondTime(args[2]); c.topic = Utils.Glue(args, 3); if (ChannelTopicChanged != null) ChannelTopicChanged(this, new TasEventArgs(args[0])); } break; case "OPENBATTLEFAILED": // opening new battle has failed if (BattleOpenFailed != null) BattleOpenFailed(this, new TasEventArgs(Utils.Glue(args))); break; case "OPENBATTLE": // openbattle ok { battleID = int.Parse(args[0]); UserBattleStatus self = new UserBattleStatus(username); self.IsSpectator = true; battle.Users.Add(self); // add self UpdateBattleDetails(battle.Details); if (BattleOpened != null) BattleOpened(this, new TasEventArgs(args[0])); } break; case "REQUESTBATTLESTATUS": // server asks us to update our status con.SendCommand(0, "MYBATTLESTATUS", 1 << 22, 0); // tell server that we are synchronized spectators break; case "JOINEDBATTLE": // user joined the battle if (battle != null && int.Parse(args[0]) == battleID) { battle.Users.Add(new UserBattleStatus(args[1])); if (BattleUserJoined != null) BattleUserJoined(this, new TasEventArgs(args[1])); } break; case "LEFTBATTLE": // user left the battle if (battle != null && int.Parse(args[0]) == battleID) { battle.RemoveUser(args[1]); UpdateSpectators(); if (BattleUserLeft != null) BattleUserLeft(this, new TasEventArgs(args[1])); if (args[1] == username) { battle = null; battleID = 0; if (BattleClosed != null) BattleClosed(this, new TasEventArgs()); } } break; case "CLIENTBATTLESTATUS": // player battle status has changed if (battle != null) { int uindex = battle.GetUserIndex(args[0]); if (uindex != -1) { UserBattleStatus bs = battle.Users[uindex]; bs.SetFrom(int.Parse(args[1]), int.Parse(args[2])); battle.Users[uindex] = bs; UpdateSpectators(); if (BattleUserStatusChanged != null) BattleUserStatusChanged(this, new TasEventArgs(args[0])); } } break; case "UPDATEBATTLEINFO": // update external battle info (lock and map) if (battle != null && int.Parse(args[0]) == battleID) { string mapname = Utils.Glue(args, 4); if (battle.Map.Name != mapname) { if (mapToChangeTo != null && mapToChangeTo.Name == mapname) { // if we changed to known requested map, use it battle.Map = mapToChangeTo; } else battle.Map = Program.main.Spring.UnitSync.MapList[mapname]; //otherwise find this map using unitsync if (BattleMapChanged != null) BattleMapChanged(this, new TasEventArgs(mapname)); } if (battle.IsLocked != int.Parse(args[2]) > 0) { battle.IsLocked = int.Parse(args[2]) > 0; if (BattleLockChanged != null) BattleLockChanged(this, new TasEventArgs(args[2])); } } break; case "BATTLEOPENED": { if (BattleFound != null) BattleFound(this, new TasEventArgs(args)); break; } case "CLIENTIPPORT": if (battle != null) { int idx = battle.GetUserIndex(args[0]); if (idx != -1) { UserBattleStatus bs = battle.Users[idx]; bs.ip = IPAddress.Parse(args[1]); bs.port = int.Parse(args[2]); battle.Users[idx] = bs; if (BattleUserIpRecieved != null) BattleUserIpRecieved(this, new TasEventArgs(args)); } } break; case "SETSCRIPTTAGS": // updates internal battle details if (battle != null) { BattleDetails bd = new BattleDetails(); bd.Parse(Utils.Glue(args), battle.ModOptions); battle.Details = bd; if (BattleDetailsChanged != null) BattleDetailsChanged(this, new TasEventArgs(args)); } break; case "UDPSOURCEPORT": udpPunchingTimer.Stop(); if (startingAfterUdpPunch) { startingAfterUdpPunch = false; if (battle != null) { // send UDP packets to client (2x to be sure) foreach (UserBattleStatus ubs in battle.Users) if (ubs.ip != IPAddress.None && ubs.port != 0) SendUdpPacket(lastUdpSourcePort, ubs.ip, ubs.port); foreach (UserBattleStatus ubs in battle.Users) if (ubs.ip != IPAddress.None && ubs.port != 0) SendUdpPacket(lastUdpSourcePort, ubs.ip, ubs.port); battle.HostPort = lastUdpSourcePort; // update source port for hosting and start it ChangeMyStatus(false, true); } } break; } } catch (Exception e) { if (!ErrorHandling.HandleException(e, "Exception while dispatching " + command + " " + Utils.Glue(args))) throw e; } ; } public static DateTime ConvertMilisecondTime(string arg) { return (new DateTime(1970, 1, 1, 0, 0, 0)).AddMilliseconds(long.Parse(arg)); } } }