From 3d366e65227c508e262a3b55f0a1a9a7674b1ac4 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Sun, 8 Mar 2015 16:20:10 -0500 Subject: [PATCH] Intial Commit of Version 0.1 --- .gitignore | 189 +++++++ Admin/4D1.ico | Bin 0 -> 370070 bytes Admin/Admin.cs | 29 ++ Admin/App.config | 6 + Admin/Bans.cs | 39 ++ Admin/Command.cs | 431 ++++++++++++++++ Admin/Connection.cs | 33 ++ Admin/Database.cs | 244 ++++++++++ Admin/Event.cs | 105 ++++ Admin/File.cs | 161 ++++++ Admin/IW4M ADMIN.csproj | 145 ++++++ Admin/Log.cs | 54 ++ Admin/Main.cs | 36 ++ Admin/Maps.cs | 18 + Admin/Player.cs | 134 +++++ Admin/Properties/AssemblyInfo.cs | 37 ++ Admin/Properties/Settings.settings | 6 + Admin/Properties/app.manifest | 52 ++ Admin/RCON.cs | 129 +++++ Admin/SQLiteDatabase.cs | 234 +++++++++ Admin/Server.cs | 759 +++++++++++++++++++++++++++++ Admin/Utilities.cs | 77 +++ IW4M Admin.sln | 22 + 23 files changed, 2940 insertions(+) create mode 100644 .gitignore create mode 100644 Admin/4D1.ico create mode 100644 Admin/Admin.cs create mode 100644 Admin/App.config create mode 100644 Admin/Bans.cs create mode 100644 Admin/Command.cs create mode 100644 Admin/Connection.cs create mode 100644 Admin/Database.cs create mode 100644 Admin/Event.cs create mode 100644 Admin/File.cs create mode 100644 Admin/IW4M ADMIN.csproj create mode 100644 Admin/Log.cs create mode 100644 Admin/Main.cs create mode 100644 Admin/Maps.cs create mode 100644 Admin/Player.cs create mode 100644 Admin/Properties/AssemblyInfo.cs create mode 100644 Admin/Properties/Settings.settings create mode 100644 Admin/Properties/app.manifest create mode 100644 Admin/RCON.cs create mode 100644 Admin/SQLiteDatabase.cs create mode 100644 Admin/Server.cs create mode 100644 Admin/Utilities.cs create mode 100644 IW4M Admin.sln diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..796453635 --- /dev/null +++ b/.gitignore @@ -0,0 +1,189 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +x64/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Roslyn cache directories +*.ide/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +#NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +## TODO: Comment the next line if you want to checkin your +## web deploy settings but do note that will include unencrypted +## passwords +#*.pubxml + +# NuGet Packages Directory +packages/* +## TODO: If the tool you use requires repositories.config +## uncomment the next line +#!packages/repositories.config + +# Enable "build/" folder in the NuGet Packages folder since +# NuGet packages use it for MSBuild targets. +# This line needs to be after the ignore of the build folder +# (and the packages folder if the line above has been uncommented) +!packages/build/ + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +sql/ +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# LightSwitch generated files +GeneratedArtifacts/ +_Pvt_Extensions/ +ModelManifest.xml \ No newline at end of file diff --git a/Admin/4D1.ico b/Admin/4D1.ico new file mode 100644 index 0000000000000000000000000000000000000000..36b041520a0813546294ebe091a63ce7f19f5fa2 GIT binary patch literal 370070 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 + + + + + diff --git a/Admin/Bans.cs b/Admin/Bans.cs new file mode 100644 index 000000000..3c2e4d98a --- /dev/null +++ b/Admin/Bans.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace IW4MAdmin +{ + class Ban + { + public Ban(String Reas, String TargID, String From) + { + Reason = Reas; + npID = TargID; + bannedByID = From; + When = DateTime.Now; + } + + public String getReason() + { + return Reason; + } + + public String getID() + { + return npID; + } + + public String getBanner() + { + return bannedByID; + } + + private String Reason; + private String npID; + private String bannedByID; + private DateTime When; + + } + +} diff --git a/Admin/Command.cs b/Admin/Command.cs new file mode 100644 index 000000000..6dc5214b7 --- /dev/null +++ b/Admin/Command.cs @@ -0,0 +1,431 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace IW4MAdmin +{ + abstract class Command + { + public Command(String N, String D, String U, Player.Permission P, int args, bool nT) + { + Name = N; + Description = D; + Usage = U; + Permission = P; + Arguments = args; + hasTarget = nT; + } + + //Get command name + public String getName() + { + return Name; + } + //Get description on command + public String getDescription() + { + return Description; + } + //Get the example usage of the command + public String getAlias() + { + return Usage; + } + //Get the required permission to execute the command + public Player.Permission getNeededPerm() + { + return Permission; + } + + public int getNumArgs() + { + return Arguments; + } + + public bool needsTarget() + { + return hasTarget; + } + + //Execute the command + abstract public void Execute(Event E); + + private String Name; + private String Description; + private String Usage; + private int Arguments; + private bool hasTarget; + + public Player.Permission Permission; + } + + class Owner : Command + { + public Owner(String N, String D, String U, Player.Permission P, int args, bool nT) : base(N, D, U, P, args, nT) { } + + public override void Execute(Event E) + { + if (E.Owner.owner == null) + { + E.Origin.setLevel(Player.Permission.Owner); + E.Origin.Tell("Congratulations, you have claimed ownership of this server!"); + E.Owner.owner = E.Origin; + E.Owner.DB.updatePlayer(E.Origin); + } + else + E.Origin.Tell("This server already has an owner!"); + } + + } + + + class Warn : Command + { + public Warn(String N, String D, String U, Player.Permission P, int args, bool nT) : base(N, D, U, P, args, nT) { } + + public override void Execute(Event E) + { + if (E.Origin.getLevel() <= E.Target.getLevel()) + E.Origin.Tell("You cannot warn " + E.Target.getName()); + else + { + E.Target.LastOffense = Utilities.removeWords(E.Data, 1); + E.Target.Warnings++; + String Message = String.Format("^1WARNING ^7[^3{0}^7]: ^3{1}^7, {2}", E.Target.Warnings, E.Target.getName(), E.Target.LastOffense); + E.Owner.Broadcast(Message); + if (E.Target.Warnings >= 4) + E.Target.Kick("You were kicked for too many warnings!"); + } + + } + + } + + class WarnClear : Command + { + public WarnClear(String N, String D, String U, Player.Permission P, int args, bool nT) : base(N, D, U, P, args, nT) { } + + public override void Execute(Event E) + { + E.Target.LastOffense = String.Empty; + E.Target.Warnings = 0; + String Message = String.Format("All warning cleared for {0}", E.Target.getName()); + E.Owner.Broadcast(Message); + } + + } + + class Kick : Command + { + public Kick(String N, String D, String U, Player.Permission P, int args, bool nT) : base(N, D, U, P, args, nT) { } + + public override void Execute(Event E) + { + E.Target.LastOffense = Utilities.removeWords(E.Data, 1); + String Message = "^1Player Kicked: ^5" + E.Target.LastOffense + " ^1Admin: ^5" + E.Origin.getName(); + if (E.Origin.getLevel() > E.Target.getLevel()) + E.Target.Kick(Message); + else + E.Origin.Tell("You cannot kick " + E.Target.getName()); + } + + } + + class Say : Command + { + public Say(String N, String D, String U, Player.Permission P, int args, bool nT) : base(N, D, U, P, args, nT) { } + + public override void Execute(Event E) + { + E.Owner.Broadcast("^1" + E.Origin.getName() + " - ^6" + E.Data + "^7"); + } + + } + + class TempBan : Command + { + public TempBan(String N, String D, String U, Player.Permission P, int args, bool nT) : base(N, D, U, P, args, nT) { } + + public override void Execute(Event E) + { + E.Target.LastOffense = Utilities.removeWords(E.Data, 1); + String Message = "^1Player Temporarily Banned: ^5" + E.Target.LastOffense + "^7 (1 hour)"; + if (E.Origin.getLevel() > E.Target.getLevel()) + E.Target.tempBan(Message); + else + E.Origin.Tell("You cannot temp ban " + E.Target.getName()); + + } + + } + + class SBan : Command + { + public SBan(String N, String D, String U, Player.Permission P, int args, bool nT) : base(N, D, U, P, args, nT) { } + + public override void Execute(Event E) + { + E.Target.LastOffense = Utilities.removeWords(E.Data, 1); + E.Target.lastEvent = E; // needs to be fixed + String Message = "^1Player Banned: ^5" + E.Target.LastOffense + "^7 (appeal at nbsclan.org)"; + if (E.Origin.getLevel() > E.Target.getLevel()) + E.Target.Ban(Message, E.Origin); + else + E.Origin.Tell("You cannot ban " + E.Target.getName()); + } + + } + + class Unban : Command + { + public Unban(String N, String D, String U, Player.Permission P, int args, bool nT) : base(N, D, U, P, args, nT) { } + + public override void Execute(Event E) + { + if (E.Owner.Unban(E.Data.Trim())) + E.Origin.Tell("Successfully unbanned " + E.Data.Trim()); + else + E.Origin.Tell("Unable to find a ban for that GUID"); + } + + } + + class WhoAmI : Command + { + public WhoAmI(String N, String D, String U, Player.Permission P, int args, bool nT) : base(N, D, U, P, args, nT) { } + + public override void Execute(Event E) + { + String You = String.Format("You are {0} at client spot {1} with xuid {2}. You have connected {3} times and are currently ranked {4}", E.Origin.getName(), E.Origin.getClientNum(), E.Origin.getID(), E.Origin.getConnections(), E.Origin.getLevel()); + E.Origin.Tell(You); + } + + } + + class List : Command + { + public List(String N, String D, String U, Player.Permission P, int args, bool nT) : base(N, D, U, P, args, nT) { } + + public override void Execute(Event E) + { + foreach (Player P in E.Owner.getPlayers()) + { + if (P == null) + continue; + + E.Origin.Tell(String.Format("[^3{0}^7]{3}[^3{1}^7] {2}", P.getLevel(), P.getClientNum(), P.getName(), Utilities.getSpaces(Player.Permission.SeniorAdmin.ToString().Length - P.getLevel().ToString().Length))); + } + } + + } + + class Help : Command + { + public Help(String N, String D, String U, Player.Permission P, int args, bool nT) : base(N, D, U, P, args, nT) { } + + public override void Execute(Event E) + { + String cmd = E.Data.Trim(); + + if (cmd.Length > 2) + { + bool found = false; + foreach (Command C in E.Owner.getCommands()) + { + if (C.getName().Contains(cmd) || C.getName() == cmd) + { + E.Origin.Tell(" [^3" + C.getName() + "^7] " + C.getDescription()); + found = true; + } + } + + if (!found) + E.Origin.Tell("Could not find that command"); + } + + else + { + foreach (Command C in E.Owner.getCommands()) + { + if (E.Origin.getLevel() >= C.getNeededPerm()) + { + E.Origin.Tell(" [^3" + C.getName() + "^7] "); + } + } + E.Origin.Tell("Type !help to get command usage example"); + } + } + + } + + class FastRestart : Command + { + public FastRestart(String N, String D, String U, Player.Permission P, int args, bool nT) : base(N, D, U, P, args, nT) { } + + public override void Execute(Event E) + { + E.Owner.Broadcast("Performing fast restart in 5 seconds..."); + E.Owner.fastRestart(5); + } + + } + + class MapRotate : Command + { + public MapRotate(String N, String D, String U, Player.Permission P, int args, bool nT) : base(N, D, U, P, args, nT) { } + + public override void Execute(Event E) + { + E.Owner.Broadcast("Performing map rotate in 5 seconds..."); + E.Owner.mapRotate(5); + } + + } + + class SetLevel : Command + { + public SetLevel(String N, String D, String U, Player.Permission P, int args, bool nT) : base(N, D, U, P, args, nT) { } + + public override void Execute(Event E) + { + if (E.Target == E.Origin) + { + E.Origin.Tell("You can't set your own level, silly."); + return; + } + + Player.Permission newPerm = Utilities.matchPermission(Utilities.removeWords(E.Data, 1)); + + if (newPerm > Player.Permission.Banned) + { + E.Target.setLevel(newPerm); + E.Target.Tell("Congratulations! You have been promoted to ^3" + newPerm); + E.Origin.Tell(E.Target.getName() + " was successfully promoted!"); + //NEEED TO mOVE + E.Owner.DB.updatePlayer(E.Target); + } + + else + E.Origin.Tell("Invalid group specified."); + } + + } + + class Usage : Command + { + public Usage(String N, String D, String U, Player.Permission P, int args, bool nT) : base(N, D, U, P, args, nT) { } + + public override void Execute(Event E) + { + E.Origin.Tell("IW4M Admin is using " + Math.Round(((System.Diagnostics.Process.GetCurrentProcess().PrivateMemorySize64 / 2048f) / 1200f), 1) + "MB"); + } + + } + + class Uptime : Command + { + public Uptime(String N, String D, String U, Player.Permission P, int args, bool nT) : base(N, D, U, P, args, nT) { } + + public override void Execute(Event E) + { + TimeSpan uptime = DateTime.Now - System.Diagnostics.Process.GetCurrentProcess().StartTime; + E.Origin.Tell(String.Format("IW4M Admin has been up for {0} days, {1} hours, and {2} minutes", uptime.Days, uptime.Hours, uptime.Minutes)); + } + + } + + class Admins : Command + { + public Admins(String N, String D, String U, Player.Permission P, int args, bool nT) : base(N, D, U, P, args, nT) { } + + public override void Execute(Event E) + { + foreach (Player P in E.Owner.getPlayers()) + { + if (P != null && P.getLevel() > Player.Permission.User) + { + E.Origin.Tell(String.Format("[^3{0}^7]{3}[^3{1}^7] {2}", P.getLevel(), P.getClientNum(), P.getName(), Utilities.getSpaces(Player.Permission.SeniorAdmin.ToString().Length - P.getLevel().ToString().Length))); + } + } + } + + } + + class Wisdom : Command + { + public Wisdom(String N, String D, String U, Player.Permission P, int args, bool nT) : base(N, D, U, P, args, nT) { } + + public override void Execute(Event E) + { + String Quote = new Connection("http://www.iheartquotes.com/api/v1/random?max_lines=1&max_characters=200").Read(); + E.Owner.Broadcast(Utilities.removeNastyChars(Quote)); + } + + } + + + class MapCMD : Command + { + public MapCMD(String N, String D, String U, Player.Permission P, int args, bool nT) : base(N, D, U, P, args, nT) { } + + public override void Execute(Event E) + { + string newMap = E.Data.Trim().ToLower(); + foreach (Map m in E.Owner.maps) + { + if (m.Name.ToLower() == newMap || m.Alias.ToLower() == newMap) + { + E.Owner.Broadcast("Changing to map ^2" + m.Alias); + Utilities.Wait(3); + E.Owner.Map(m.Name); + return; + } + } + + E.Owner.Broadcast("Attempting to change to unknown map ^1" + newMap); + Utilities.Wait(3); + E.Owner.Map(newMap); + } + + } + + class Find : Command + { + public Find(String N, String D, String U, Player.Permission P, int args, bool nT) : base(N, D, U, P, args, nT) { } + + public override void Execute(Event E) + { + var db_players = E.Owner.DB.findPlayers(E.Data.Trim()); + if (db_players == null) + { + E.Origin.Tell("No players found"); + return; + } + + foreach (Player P in db_players) + { + String mesg = String.Format("[^3{0}^7] [^3@{1}^7] - {2}", P.getName(), P.getDBID(), P.getID()); + E.Origin.Tell(mesg); + } + + } + + } + + class Rules : Command + { + public Rules(String N, String D, String U, Player.Permission P, int args, bool nT) : base(N, D, U, P, args, nT) { } + + public override void Execute(Event E) + { + if (E.Owner.rules.Count < 1) + E.Origin.Tell("This server has not set any rules."); + else + { + foreach (String r in E.Owner.rules) + E.Origin.Tell("- " + r); + } + } + + } +} diff --git a/Admin/Connection.cs b/Admin/Connection.cs new file mode 100644 index 000000000..fa1ce8df4 --- /dev/null +++ b/Admin/Connection.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Net; +using System.IO; + +namespace IW4MAdmin +{ + class Connection + { + public Connection(String Loc) + { + Location = Loc; + ConnectionHandle = WebRequest.Create(Location); + ConnectionHandle.Proxy = null; + } + + public String Read() + { + WebResponse Resp = ConnectionHandle.GetResponse(); + StreamReader data_in = new StreamReader(Resp.GetResponseStream()); + String result = data_in.ReadToEnd(); + + data_in.Close(); + Resp.Close(); + + return result; + } + + private String Location; + private WebRequest ConnectionHandle; + } +} diff --git a/Admin/Database.cs b/Admin/Database.cs new file mode 100644 index 000000000..1f78e5181 --- /dev/null +++ b/Admin/Database.cs @@ -0,0 +1,244 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Data.SQLite; +using System.Data; +using System.IO; +using System.Collections; + +namespace IW4MAdmin +{ + class Database + { + public Database(String FN) + { + FileName = FN; + DBCon = String.Format("Data Source={0}", FN); + Con = new SQLiteConnection(DBCon); + Init(); + } + + private void Init() + { + if(!File.Exists(FileName)) + { + String query = "CREATE TABLE [CLIENTS] ( [Name] TEXT NULL, [npID] TEXT NULL, [Number] INTEGER PRIMARY KEY AUTOINCREMENT, [Level] INT DEFAULT 0 NULL, [LastOffense] TEXT NULL, [Connections] INT DEFAULT 1 NULL);"; + ExecuteNonQuery(query); + query = "CREATE TABLE [BANS] ( [Reason] TEXT NULL, [npID] TEXT NULL, [bannedByID] Text NULL)"; + ExecuteNonQuery(query); + } + } + + public Player getPlayer(String ID, int cNum) + { + String Query = String.Format("SELECT * FROM CLIENTS WHERE npID = '{0}' LIMIT 1", ID); + DataTable Result = GetDataTable(Query); + + if (Result != null && Result.Rows.Count > 0) + { + DataRow ResponseRow = Result.Rows[0]; + return new Player(ResponseRow["Name"].ToString(), ResponseRow["npID"].ToString(), cNum, (Player.Permission)(ResponseRow["Level"]), Convert.ToInt32(ResponseRow["Number"]), ResponseRow["LastOffense"].ToString(), ((int)ResponseRow["Connections"] + 1)); + } + + else + return null; + } + + public List findPlayers(String name) + { + String Query = String.Format("SELECT * FROM CLIENTS WHERE Name LIKE '%{0}%' LIMIT 10", name); + DataTable Result = GetDataTable(Query); + + List Players = new List(); + + if (Result != null && Result.Rows.Count > 0) + { + foreach (DataRow p in Result.Rows) + { + Players.Add(new Player(p["Name"].ToString(), p["npID"].ToString(), -1, (Player.Permission)(p["Level"]), Convert.ToInt32(p["Number"]), p["LastOffense"].ToString(), ((int)p["Connections"]))); + } + return Players; + } + + else + return null; + } + + public Player findPlayers(int dbIndex) + { + String Query = String.Format("SELECT * FROM CLIENTS WHERE Number = '{0}' LIMIT 1", dbIndex); + DataTable Result = GetDataTable(Query); + + if (Result != null && Result.Rows.Count > 0) + { + foreach (DataRow p in Result.Rows) + return new Player(p["Name"].ToString(), p["npID"].ToString(), -1, (Player.Permission)(p["Level"]), Convert.ToInt32(p["Number"]), p["LastOffense"].ToString(), ((int)p["Connections"])); + } + + return null; + } + + public Player getOwner() + { + String Query = String.Format("SELECT * FROM CLIENTS WHERE Level = '{0}'", 4); + DataTable Result = GetDataTable(Query); + + if (Result != null && Result.Rows.Count > 0) + { + DataRow ResponseRow = Result.Rows[0]; + return new Player(ResponseRow["Name"].ToString(), ResponseRow["npID"].ToString(), -1, (Player.Permission)(ResponseRow["Level"]), Convert.ToInt32(ResponseRow["Number"]), null, 0); + } + + else + return null; + } + + public List getBans() + { + List Bans = new List(); + DataTable Result = GetDataTable("SELECT * FROM BANS"); + + foreach (DataRow Row in Result.Rows) + Bans.Add(new Ban(Row["Reason"].ToString(), Row["npID"].ToString(), Row["bannedByID"].ToString())); + + return Bans; + } + + public void removeBan(String GUID) + { + String Query = String.Format("DELETE FROM BANS WHERE npID = '{0}'", GUID); + ExecuteNonQuery(Query); + } + + public void addPlayer(Player P) + { + Dictionary newPlayer = new Dictionary(); + + newPlayer.Add("Name", Utilities.removeNastyChars(P.getName())); + newPlayer.Add("npID", P.getID()); + newPlayer.Add("Level", (int)P.getLevel()); + // newPlayer.Add("Number", P.getClientNum().ToString()); + newPlayer.Add("LastOffense", ""); + newPlayer.Add("Connections", 1); + + Insert("CLIENTS", newPlayer); + } + + public void updatePlayer(Player P) + { + Dictionary updatedPlayer = new Dictionary(); + + updatedPlayer.Add("Name", P.getName()); + updatedPlayer.Add("npID", P.getID()); + updatedPlayer.Add("Level", (int)P.getLevel()); + // updatedPlayer.Add("Number", P.getClientNum().ToString()); + updatedPlayer.Add("LastOffense", P.getLastO()); + updatedPlayer.Add("Connections", P.getConnections()); + + Update("CLIENTS", updatedPlayer, String.Format("npID = '{0}'", P.getID())); + } + + public void addBan(Ban B) + { + Dictionary newBan = new Dictionary(); + + newBan.Add("Reason", B.getReason()); + newBan.Add("npID", B.getID()); + newBan.Add("bannedByID", B.getBanner()); + + Insert("BANS", newBan); + } + + //HELPERS + + public bool Insert(String tableName, Dictionary data) + { + String columns = ""; + String values = ""; + Boolean returnCode = true; + foreach (KeyValuePair val in data) + { + columns += String.Format(" {0},", val.Key); + values += String.Format(" '{0}',", val.Value); + } + columns = columns.Substring(0, columns.Length - 1); + values = values.Substring(0, values.Length - 1); + try + { + this.ExecuteNonQuery(String.Format("insert into {0}({1}) values({2});", tableName, columns, values)); + } + catch (Exception fail) + { + Console.WriteLine(fail.Message); + returnCode = false; + } + return returnCode; + } + + public bool Update(String tableName, Dictionary data, String where) + { + String vals = ""; + Boolean returnCode = true; + if (data.Count >= 1) + { + foreach (KeyValuePair val in data) + { + vals += String.Format(" {0} = '{1}',", val.Key, val.Value); + } + vals = vals.Substring(0, vals.Length - 1); + } + try + { + this.ExecuteNonQuery(String.Format("update {0} set {1} where {2};", tableName, vals, where)); + } + catch (Exception fail) + { + Console.WriteLine(fail.Message); + returnCode = false; + } + return returnCode; + } + + public DataRow getDataRow(String Q) + { + DataRow Result = GetDataTable(Q).Rows[0]; + return Result; + } + + private int ExecuteNonQuery(String Request) + { + Con.Open(); + SQLiteCommand CMD = new SQLiteCommand(Con); + CMD.CommandText = Request; + int rowsUpdated = CMD.ExecuteNonQuery(); + Con.Close(); + return rowsUpdated; + } + + public DataTable GetDataTable(String sql) + { + DataTable dt = new DataTable(); + try + { + Con.Open(); + SQLiteCommand mycommand = new SQLiteCommand(Con); + mycommand.CommandText = sql; + SQLiteDataReader reader = mycommand.ExecuteReader(); + dt.Load(reader); + reader.Close(); + Con.Close(); + } + catch (Exception e) + { + Console.WriteLine(e.Message); + throw new Exception(e.Message); + } + return dt; + } + //END + + private String FileName; + private String DBCon; + private SQLiteConnection Con; + } +} diff --git a/Admin/Event.cs b/Admin/Event.cs new file mode 100644 index 000000000..f9e0c061b --- /dev/null +++ b/Admin/Event.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; + +namespace IW4MAdmin +{ + class Event + { + public enum GType + { + //FROM SERVER + Connect, + Disconnect, + Say, + Kill, + Death, + MapChange, + MapEnd, + + //FROM ADMIN + Broadcast, + Tell, + Kick, + Ban, + + Unknown, + } + + public Event(GType t, string d, Player O, Player T, Server S) + { + Type = t; + Data = d; + Origin = O; + Target = T; + Owner = S; + } + + //This needs to be here + public Command isValidCMD(List list) + { + if (this.Data.Substring(0, 1) == "!") + { + string[] cmd = this.Data.Substring(1, this.Data.Length - 1).Split(' '); + + foreach (Command C in list) + { + if (C.getName() == cmd[0].ToLower() || C.getAlias() == cmd[0].ToLower()) + return C; + } + + return null; + } + + else + return null; + } + + public static Event requestEvent(String[] line, Server SV) + { + try + { + String eventType = line[0].Substring(line[0].Length - 1); + eventType = eventType.Trim(); + + if (eventType == "J") + return new Event(GType.Connect, null, SV.clientFromLine(line, 3, true), null, SV); + + if (eventType == "Q") + return new Event(GType.Disconnect, null, SV.clientFromLine(line, 3, false), null, null); + + if (line[0].Substring(line[0].Length - 3).Trim() == "say") + { + Regex rgx = new Regex("[^a-zA-Z0-9 -! -_]"); + string message = rgx.Replace(line[4], ""); + if (message.Length < 2) + message = " "; + return new Event(GType.Say, Utilities.removeNastyChars(message), SV.clientFromLine(line, 3, false), null, null); + } + + if (eventType == "d") + return new Event(GType.MapEnd, null, null, null, null); + + if (line[0].Length > 400) // blaze it + return new Event(GType.MapChange, null, null, null, null); + + + return null; + } + catch (Exception E) + { + SV.Log.Write("Error requesting event " + E.Message, Log.Level.Debug); + return null; + } + } + + + public GType Type; + public string Data; // Data is usually the message sent by player + public Player Origin; + public Player Target; + public Server Owner; + + } +} diff --git a/Admin/File.cs b/Admin/File.cs new file mode 100644 index 000000000..fe953005b --- /dev/null +++ b/Admin/File.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; + +namespace IW4MAdmin +{ + class file + { + public file(String fileName) + { + //Not safe for directories with more than one folder but meh + _Directory = fileName.Split('\\')[0]; + Name = (fileName.Split('\\'))[fileName.Split('\\').Length-1]; + + if (!Directory.Exists(_Directory)) + Directory.CreateDirectory(_Directory); + + if (!File.Exists(fileName)) + { + FileStream penis = File.Create(fileName); + penis.Close(); + } + Handle = new StreamReader(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)); + sze = Handle.BaseStream.Length; + } + + public file(String file, bool write) + { + Name = file; + writeHandle = new StreamWriter(new FileStream(Name, FileMode.Create, FileAccess.Write, FileShare.ReadWrite)); + sze = 0; + } + + public long getSize() + { + sze = Handle.BaseStream.Length; + return sze; + } + + public void Write(String line) + { + writeHandle.WriteLine(line); + writeHandle.Flush(); + } + + public String[] getParameters(int num) + { + if (sze > 0) + { + String firstLine = Handle.ReadLine(); + String[] Parms = firstLine.Split(':'); + if (Parms.Length < num) + return null; + else + return Parms; + } + + return null; + } + + public int getNumLines() + { + return 0; + } + + //FROM http://stackoverflow.com/questions/398378/get-last-10-lines-of-very-large-text-file-10gb-c-sharp + public string ReadEndTokens() + { + Encoding encoding = Encoding.ASCII; + string tokenSeparator = "\n"; + int numberOfTokens = 2; + + int sizeOfChar = encoding.GetByteCount("\n"); + byte[] buffer = encoding.GetBytes(tokenSeparator); + + using (FileStream fs = new FileStream(this.Name, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + Int64 tokenCount = 0; + Int64 endPosition = fs.Length / sizeOfChar; + + for (Int64 position = sizeOfChar; position < endPosition; position += sizeOfChar) + { + fs.Seek(-position, SeekOrigin.End); + fs.Read(buffer, 0, buffer.Length); + + if (encoding.GetString(buffer) == tokenSeparator) + { + tokenCount++; + if (tokenCount == numberOfTokens) + { + byte[] returnBuffer = new byte[fs.Length - fs.Position]; + fs.Read(returnBuffer, 0, returnBuffer.Length); + return encoding.GetString(returnBuffer); + } + } + } + + // handle case where number of tokens in file is less than numberOfTokens + fs.Seek(0, SeekOrigin.Begin); + buffer = new byte[fs.Length]; + fs.Read(buffer, 0, buffer.Length); + return encoding.GetString(buffer); + } + } + public String[] readAll() + { + return Handle.ReadToEnd().Split('\n'); + } + + public String[] end(int neededLines) + { + var lines = new List(); + while (!Handle.EndOfStream) + { + String lins = Handle.ReadLine(); + lines.Add(lins.ToString()); + if (lines.Count > neededLines) + { + lines.RemoveAt(0); + } + } + + return lines.ToArray(); + } + + public String[] Tail(int lineCount) + { + var buffer = new List(lineCount); + string line; + for (int i = 0; i < lineCount; i++) + { + line = Handle.ReadLine(); + if (line == null) return buffer.ToArray(); + buffer.Add(line); + } + + int lastLine = lineCount - 1; //The index of the last line read from the buffer. Everything > this index was read earlier than everything <= this indes + + while (null != (line = Handle.ReadLine())) + { + lastLine++; + if (lastLine == lineCount) lastLine = 0; + buffer[lastLine] = line; + } + + if (lastLine == lineCount - 1) return buffer.ToArray(); + var retVal = new string[lineCount]; + buffer.CopyTo(lastLine + 1, retVal, 0, lineCount - lastLine - 1); + buffer.CopyTo(0, retVal, lineCount - lastLine - 1, lastLine + 1); + return retVal; + } + //END + + private long sze; + private String Name; + private String _Directory; + StreamReader Handle; + StreamWriter writeHandle; + } +} diff --git a/Admin/IW4M ADMIN.csproj b/Admin/IW4M ADMIN.csproj new file mode 100644 index 000000000..f51bdc649 --- /dev/null +++ b/Admin/IW4M ADMIN.csproj @@ -0,0 +1,145 @@ + + + + + Debug + AnyCPU + {DD5DCDA2-51DB-4B1A-922F-5705546E6115} + Exe + Properties + IW4MAdmin + IW4MAdmin + v2.0 + 512 + + false + C:\Users\Michael\Desktop\IW4MAdmin\ + true + Disk + false + Foreground + 7 + Days + false + false + true + IW4M Administration + RaidMax LLC + publish.htm + true + 2 + 0.1.0.%2a + false + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + x86 + pdbonly + true + bin\Release\ + TRACE + prompt + 0 + + + 0D02A7F5C6EA170625B5BF533E667AE6C3F93065 + + + IW4M ADMIN_TemporaryKey.pfx + + + true + + + LocalIntranet + + + Properties\app.manifest + + + true + + + 4D1.ico + + + IW4MAdmin.Program + + + + + + False + bin\Release\System.Data.SQLite.dll + + + + + + + + + + + + + + + + + + True + True + Settings.settings + + + + + + + + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + False + .NET Framework 3.5 SP1 Client Profile + false + + + False + .NET Framework 3.5 SP1 + true + + + + + + + + + + + \ No newline at end of file diff --git a/Admin/Log.cs b/Admin/Log.cs new file mode 100644 index 000000000..2471a3217 --- /dev/null +++ b/Admin/Log.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace IW4MAdmin +{ + class Log + { + public enum Level + { + All, + Debug, + Production, + None, + } + + public Log(file logf, Level mode) + { + logFile = logf; + logMode = mode; + } + + + public void Write(String line, Level lv) + { + String Line = String.Format("{1} - [{0}]: {2}", lv, getTime(), line); + switch(logMode) + { + case Level.All: + if (lv == Level.All || lv == Level.Debug || lv == Level.Production) + Console.WriteLine(Line); + break; + case Level.Debug: + if (lv == Level.All || lv == Level.Debug) + Console.WriteLine(Line); + break; + case Level.Production: + if (lv == Level.Production) + Console.WriteLine(Line); + break; + } + + logFile.Write(Line); + } + + private string getTime() + { + return DateTime.Now.ToString("HH:mm:ss"); + } + + private file logFile; + private Level logMode; + } +} diff --git a/Admin/Main.cs b/Admin/Main.cs new file mode 100644 index 000000000..2b6965ef6 --- /dev/null +++ b/Admin/Main.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace IW4MAdmin +{ + class Program + { + static void Main(string[] args) + { + + file Config = new file("config\\servers.cfg"); + String[] SV_CONF = Config.getParameters(3); + double Version = 0.1; + + if (Config.getSize() > 0 && SV_CONF != null) + { + Console.WriteLine("====================================================="); + Console.WriteLine(" IW4M ADMIN v" + Version); + Console.WriteLine(" by RaidMax "); + Console.WriteLine("====================================================="); + Console.WriteLine("Starting admin on port " + SV_CONF[1]); + + Server IW4M; + IW4M = new Server(SV_CONF[0], Convert.ToInt32(SV_CONF[1]), SV_CONF[2]); + IW4M.Monitor(); + } + else + { + Console.WriteLine("[FATAL] CONFIG FILE DOES NOT EXIST OR IS INCORRECT!"); + Utilities.Wait(5); + } + + } + } +} diff --git a/Admin/Maps.cs b/Admin/Maps.cs new file mode 100644 index 000000000..8914a664a --- /dev/null +++ b/Admin/Maps.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace IW4MAdmin +{ + class Map + { + public Map(String N, String A) + { + Name = N; + Alias = A; + } + + public String Name; + public String Alias; + } +} diff --git a/Admin/Player.cs b/Admin/Player.cs new file mode 100644 index 000000000..dd375d390 --- /dev/null +++ b/Admin/Player.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace IW4MAdmin +{ + class Player + { + public enum Permission + { + Banned = -1, + User = 0, + Moderator = 1, + Administrator = 2, + SeniorAdmin = 3, + Owner = 4, + Creator = 5, + } + + public Player(string n, string id, int num, int l) + { + Name = n; + npID = id; + Number = num; + Level = (Player.Permission)l; + LastOffense = null; + Connections = 0; + Warnings = 0; + } + + public Player(string n, string id, int num, Player.Permission l, int cind, String lo, int con) + { + Name = n; + npID = id; + Number = num; + Level = l; + dbID = cind; + LastOffense = lo; + Connections = con; + Warnings = 0; + } + + public String getName() + { + return Name; + } + + public String getID() + { + return npID; + } + + public String getDBID() + { + return Convert.ToString(dbID); + } + + public int getClientNum() + { + return Number; + } + + public Player.Permission getLevel() + { + return Level; + } + + public int getConnections() + { + return Connections; + } + + public String getLastO() + { + return LastOffense; + } + + // BECAUSE IT NEEDS TO BE CHANGED! + public void setLevel(Player.Permission Perm) + { + Level = Perm; + } + + public void Tell(String Message) + { + lastEvent.Owner.Tell(Message, this); + } + + public void Warn(String Message) + { + lastEvent.Owner.Broadcast(Message); + } + + public void Kick(String Message) + { + lastEvent.Owner.Kick(Message, this); + } + + public void tempBan(String Message) + { + lastEvent.Owner.tempBan(Message, this); + } + + public void Ban(String Message, Player Sender) + { + lastEvent.Owner.Ban(Message, this, Sender); + } + + //should be moved to utils + public Player findPlayer(String Nme) + { + foreach (Player P in lastEvent.Owner.getPlayers()) + { + if (P == null) + continue; + if (P.getName().ToLower().Contains(Name.ToLower())) + return P; + } + + return null; + } + + private string Name; + private string npID; + private int Number; + private Player.Permission Level; + private int dbID; + private int Connections; + + public Event lastEvent; + public String LastOffense; + public int Warnings; + } +} diff --git a/Admin/Properties/AssemblyInfo.cs b/Admin/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..7fb6d6e90 --- /dev/null +++ b/Admin/Properties/AssemblyInfo.cs @@ -0,0 +1,37 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Resources; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("IW4M Admin")] +[assembly: AssemblyDescription("Admin for IW4x projects")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("RaidMax LLC")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("© 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(true)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("62f47df4-c8eb-4b5d-bdd8-9de0ca6b570f")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.1.*")] +[assembly: NeutralResourcesLanguageAttribute("en")] diff --git a/Admin/Properties/Settings.settings b/Admin/Properties/Settings.settings new file mode 100644 index 000000000..049245f40 --- /dev/null +++ b/Admin/Properties/Settings.settings @@ -0,0 +1,6 @@ + + + + + + diff --git a/Admin/Properties/app.manifest b/Admin/Properties/app.manifest new file mode 100644 index 000000000..887fa2b9d --- /dev/null +++ b/Admin/Properties/app.manifest @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Admin/RCON.cs b/Admin/RCON.cs new file mode 100644 index 000000000..69188c3e7 --- /dev/null +++ b/Admin/RCON.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +//DILEMMA -- Seperate intance of RCON for each server or no? +namespace IW4MAdmin +{ + class RCON + { + enum Type + { + Query, + Execute, + } + + public RCON(Server I) + { + sv_connection = new UdpClient(); + sv_connection.Client.SendTimeout = 1000; + sv_connection.Client.ReceiveTimeout = 1000; + Instance = I; + toSend = new Queue(); + } + //When we don't care about a response + public bool sendRCON(String message) + { + try + { + String STR_REQUEST = String.Format("ÿÿÿÿrcon {0} {1}", Instance.getPassword(), message); + + Byte[] Request_ = Encoding.Unicode.GetBytes(STR_REQUEST); + Byte[] Request = new Byte[Request_.Length / 2]; + + int count = 0; //This is kinda hacky but Unicode -> ASCII doesn't seem to be working correctly for this. + foreach (Byte b in Request_) + { + if (b != 0) + Request[count / 2] = b; + count++; + } + + + System.Net.IPAddress IP = System.Net.IPAddress.Parse(Instance.getIP()); + IPEndPoint endPoint = new IPEndPoint(IP, Instance.getPort()); + + sv_connection.Connect(endPoint); + sv_connection.Send(Request, Request.Length); + } + + catch (SocketException) + { + Instance.Log.Write("Unable to reach server for sending RCON", Log.Level.Debug); + sv_connection.Close(); + return false; + } + + return true; + } + //We want to read the reponse + public String[] responseSendRCON(String message) + { + try + { + String STR_REQUEST; + if (message != "getstatus") + STR_REQUEST = String.Format("ÿÿÿÿrcon {0} {1}", Instance.getPassword(), message); + else + STR_REQUEST = String.Format("ÿÿÿÿ getstatus"); + + Byte[] Request_ = Encoding.Unicode.GetBytes(STR_REQUEST); + Byte[] Request = new Byte[Request_.Length/2]; + + int count = 0; //This is kinda hacky but Unicode -> ASCII doesn't seem to be working correctly for this. + foreach (Byte b in Request_) + { + if (b != 0) + Request[count/2] = b; + count++; + } + + + System.Net.IPAddress IP = System.Net.IPAddress.Parse(Instance.getIP()); + IPEndPoint endPoint = new IPEndPoint(IP, Instance.getPort()); + + sv_connection.Connect(endPoint); + sv_connection.Send(Request, Request.Length); + + + Byte[] receive = sv_connection.Receive(ref endPoint); + int num = int.Parse("0a", System.Globalization.NumberStyles.AllowHexSpecifier); + + return System.Text.Encoding.UTF8.GetString(receive).Split((char)num); + } + + catch (SocketException) + { + Instance.Log.Write("Unable to reach server for sending RCON", Log.Level.Debug); + sv_connection.Close(); + return null; + } + } + + public void addRCON(String Message, int delay) + { + toSend.Enqueue(Message); + } + + public void ManageRCONQueue() + { + while (true) + { + if (toSend.Count > 0) + { + sendRCON(toSend.Peek()); + toSend.Dequeue(); + } + Utilities.Wait(0.3); + } + } + + private UdpClient sv_connection; + private Server Instance; + private DateTime lastCMD; + private Queue toSend; + } +} diff --git a/Admin/SQLiteDatabase.cs b/Admin/SQLiteDatabase.cs new file mode 100644 index 000000000..2d6268bc0 --- /dev/null +++ b/Admin/SQLiteDatabase.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SQLite; + +namespace IW4MAdmin +{ + + + public class SQLiteDatabase + { + String dbConnection; + + /// + /// Default Constructor for SQLiteDatabase Class. + /// + public SQLiteDatabase(String file) + { + dbConnection = "Data Source=" + file; + } + + /// + /// Single Param Constructor for specifying the DB file. + /// + /// The File containing the DB + public SQLiteDatabase(String inputFile) + { + dbConnection = String.Format("Data Source={0}", inputFile); + } + + /// + /// Single Param Constructor for specifying advanced connection options. + /// + /// A dictionary containing all desired options and their values + public SQLiteDatabase(Dictionary connectionOpts) + { + String str = ""; + foreach (KeyValuePair row in connectionOpts) + { + str += String.Format("{0}={1}; ", row.Key, row.Value); + } + str = str.Trim().Substring(0, str.Length - 1); + dbConnection = str; + } + + /// + /// Allows the programmer to run a query against the Database. + /// + /// The SQL to run + /// A DataTable containing the result set. + public DataTable GetDataTable(string sql) + { + DataTable dt = new DataTable(); + try + { + SQLiteConnection cnn = new SQLiteConnection(dbConnection); + cnn.Open(); + SQLiteCommand mycommand = new SQLiteCommand(cnn); + mycommand.CommandText = sql; + SQLiteDataReader reader = mycommand.ExecuteReader(); + dt.Load(reader); + reader.Close(); + cnn.Close(); + } + catch (Exception e) + { + Console.WriteLine(e.Message); + throw new Exception(e.Message); + } + return dt; + } + + /// + /// Allows the programmer to interact with the database for purposes other than a query. + /// + /// The SQL to be run. + /// An Integer containing the number of rows updated. + public int ExecuteNonQuery(string sql) + { + SQLiteConnection cnn = new SQLiteConnection(dbConnection); + cnn.Open(); + SQLiteCommand mycommand = new SQLiteCommand(cnn); + mycommand.CommandText = sql; + int rowsUpdated = mycommand.ExecuteNonQuery(); + cnn.Close(); + return rowsUpdated; + } + + /// + /// Allows the programmer to retrieve single items from the DB. + /// + /// The query to run. + /// A string. + public string ExecuteScalar(string sql) + { + SQLiteConnection cnn = new SQLiteConnection(dbConnection); + cnn.Open(); + SQLiteCommand mycommand = new SQLiteCommand(cnn); + mycommand.CommandText = sql; + object value = mycommand.ExecuteScalar(); + cnn.Close(); + if (value != null) + { + return value.ToString(); + } + return ""; + } + + /// + /// Allows the programmer to easily update rows in the DB. + /// + /// The table to update. + /// A dictionary containing Column names and their new values. + /// The where clause for the update statement. + /// A boolean true or false to signify success or failure. + public bool Update(String tableName, Dictionary data, String where) + { + String vals = ""; + Boolean returnCode = true; + if (data.Count >= 1) + { + foreach (KeyValuePair val in data) + { + vals += String.Format(" {0} = '{1}',", val.Key.ToString(), val.Value.ToString()); + } + vals = vals.Substring(0, vals.Length - 1); + } + try + { + this.ExecuteNonQuery(String.Format("update {0} set {1} where {2};", tableName, vals, where)); + } + catch( Exception fail) + { + Console.WriteLine(fail.Message); + returnCode = false; + } + return returnCode; + } + + /// + /// Allows the programmer to easily delete rows from the DB. + /// + /// The table from which to delete. + /// The where clause for the delete. + /// A boolean true or false to signify success or failure. + public bool Delete(String tableName, String where) + { + Boolean returnCode = true; + try + { + this.ExecuteNonQuery(String.Format("delete from {0} where {1};", tableName, where)); + } + catch (Exception fail) + { + Console.WriteLine(fail.Message); + returnCode = false; + } + return returnCode; + } + + /// + /// Allows the programmer to easily insert into the DB + /// + /// The table into which we insert the data. + /// A dictionary containing the column names and data for the insert. + /// A boolean true or false to signify success or failure. + public bool Insert(String tableName, Dictionary data) + { + String columns = ""; + String values = ""; + Boolean returnCode = true; + foreach (KeyValuePair val in data) + { + columns += String.Format(" {0},", val.Key.ToString()); + values += String.Format(" '{0}',", val.Value); + } + columns = columns.Substring(0, columns.Length - 1); + values = values.Substring(0, values.Length - 1); + try + { + this.ExecuteNonQuery(String.Format("insert into {0}({1}) values({2});", tableName, columns, values)); + } + catch (Exception fail) + { + Console.WriteLine(fail.Message); + returnCode = false; + } + return returnCode; + } + + /// + /// Allows the programmer to easily delete all data from the DB. + /// + /// A boolean true or false to signify success or failure. + public bool ClearDB() + { + DataTable tables; + try + { + tables = this.GetDataTable("select NAME from SQLITE_MASTER where type='table' order by NAME;"); + foreach (DataRow table in tables.Rows) + { + this.ClearTable(table["NAME"].ToString()); + } + return true; + } + catch + { + return false; + } + } + + /// + /// Allows the user to easily clear all data from a specific table. + /// + /// The name of the table to clear. + /// A boolean true or false to signify success or failure. + public bool ClearTable(String table) + { + try + { + + this.ExecuteNonQuery(String.Format("delete from {0};", table)); + return true; + } + catch + { + return false; + } + } + } + + +} diff --git a/Admin/Server.cs b/Admin/Server.cs new file mode 100644 index 000000000..856e56596 --- /dev/null +++ b/Admin/Server.cs @@ -0,0 +1,759 @@ + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; //SLEEP +using System.IO; + + +namespace IW4MAdmin +{ + class Server + { + const int FLOOD_TIMEOUT = 300; + + public Server(string address, int port, string password) + { + IP = address; + Port = port; + rcon_pass = password; + clientnum = 0; + RCON = new RCON(this); + logFile = new file("admin_" + address + "_" + port + ".log", true); + Log = new Log(logFile, Log.Level.Production); + players = new List(new Player[18]); + DB = new Database(port + ".db"); + Bans = DB.getBans(); + owner = DB.getOwner(); + maps = new List(); + rules = new List(); + messages = new List(); + events = new Queue(); + nextMessage = 0; + initCommands(); + initMessages(); + initMaps(); + initRules(); + } + + //Returns the current server name -- *STRING* + public String getName() + { + return hostname; + } + + //Returns current server IP set by `net_ip` -- *STRING* + public String getIP() + { + return IP; + } + + //Returns current server port set by `net_port` -- *INT* + public int getPort() + { + return Port; + } + + //Returns number of active clients on server -- *INT* + public int getNumPlayers() + { + return clientnum; + } + + //Returns the list of commands + public List getCommands() + { + return commands; + } + + public List getPlayers() + { + return players; + } + + public List getBans() + { + return Bans; + } + + //Performs update on server statistics read from log. + public void Update() + { + } + + //Add player object p to `players` list + public bool addPlayer(Player P) + { + try + { + if (DB.getPlayer(P.getID(), P.getClientNum()) == null) + DB.addPlayer(P); + else + { + //messy way to prevent loss of last event + Player A; + A = DB.getPlayer(P.getID(), P.getClientNum()); + A.lastEvent = P.lastEvent; + P = A; + } + + players[P.getClientNum()] = null; + players[P.getClientNum()] = P; + + clientnum++; + + if (P.getLevel() == Player.Permission.Banned) + { + Log.Write("Banned client " + P.getName() + " trying to connect...", Log.Level.Production); + String Message = "^1Player Kicked: ^7Previously Banned for ^5" + isBanned(P).getReason(); + P.Kick(Message); + } + + else + Log.Write("Client " + P.getName() + " connecting...", Log.Level.Production); + + return true; + } + catch (Exception E) + { + Log.Write("Unable to add player - " + E.Message, Log.Level.Debug); + return false; + } + } + + //Remove player by CLIENT NUMBER + public bool removePlayer(int cNum) + { + Log.Write("Client at " + cNum + " disconnecting...", Log.Level.Production); + players[cNum] = null; + clientnum--; + return true; + } + + public Player clientFromLine(String[] line, int name_pos, bool create) + { + string Name = line[name_pos].ToString().Trim(); + if (create) + { + Player C = new Player(Name, line[1].ToString(), Convert.ToInt16(line[2]), 0); + return C; + } + + else + { + foreach (Player P in players) + { + if (P == null) + continue; + + if (line[1].Trim() == P.getID()) + return P; + } + + Log.Write("Could not find player but player is in server. Lets try to manually add (looks like you didn't start me on an empty server)", Log.Level.All); + addPlayer(new Player(Name, line[1].ToString(), Convert.ToInt16(line[2]), 0)); + return players[Convert.ToInt16(line[2])]; + } + } + + public Player clientFromLine(String Name) + { + foreach (Player P in players) + { + if (P == null) + continue; + if (P.getName().ToLower().Contains(Name.ToLower())) + return P; + } + + return null; + } + + public Ban isBanned(Player C) + { + if (C.getLevel() == Player.Permission.Banned) + { + foreach (Ban B in Bans) + { + if (B.getID() == C.getID()) + return B; + } + } + + return null; + } + + public Command processCommand(Event E, Command C) + { + E.Data = Utilities.removeWords(E.Data, 1); + String[] Args = E.Data.Trim().Split(' '); + + if (Args.Length < (C.getNumArgs())) + { + E.Origin.Tell("Not enough arguments supplied!"); + return null; + } + + if(E.Origin.getLevel() < C.getNeededPerm()) + { + E.Origin.Tell("You do not have access to that command!"); + return null; + } + + if (C.needsTarget()) + { + int cNum = -1; + int.TryParse(Args[0], out cNum); + + if (Args[0][0] == '@') + { + int dbID = -1; + int.TryParse(Args[0].Substring(1, Args[0].Length-1), out dbID); + Player found = E.Owner.DB.findPlayers(dbID); + if (found != null) + E.Target = found; + } + + else if(Args[0].Length < 3 && cNum > -1 && cNum < 18) + { + if (players[cNum] != null) + E.Target = players[cNum]; + } + + else + E.Target = clientFromLine(Args[0]); + + if (E.Target == null) + { + E.Origin.Tell("Unable to find specified player."); + return null; + } + } + return C; + } + + private void addEvent(Event E) + { + events.Enqueue(E); + } + + private void manageEventQueue() + { + while (true) + { + if (events.Count > 0) + { + processEvent(events.Peek()); + events.Dequeue(); + } + Utilities.Wait(0.1); + } + } + + //Starts the monitoring process + public void Monitor() + { + if (!intializeBasics()) + { + Log.Write("Shutting due to uncorrectable errors..." + logPath, Log.Level.Debug); + Utilities.Wait(10); + Environment.Exit(-1); + } + + //Handles new rcon requests in a fashionable manner + Thread RCONQueue = new Thread(new ThreadStart(RCON.ManageRCONQueue)); + RCONQueue.Start(); + + //Handles new events in a fashionable manner + Thread eventQueue = new Thread(new ThreadStart(manageEventQueue)); + eventQueue.Start(); + + long l_size = -1; + String[] lines = new String[8]; + String[] oldLines = new String[8]; + DateTime start = DateTime.Now; + + Utilities.Wait(1); + Broadcast("IW4M Admin is now ^2ONLINE"); + + while (errors <=5) + { + try + { + lastMessage = DateTime.Now - start; + if(lastMessage.TotalSeconds > messageTime && messages.Count > 0) + { + Broadcast(messages[nextMessage]); + if (nextMessage == (messages.Count - 1)) + nextMessage = 0; + else + nextMessage++; + start = DateTime.Now; + } + + if (l_size != logFile.getSize()) + { + lines = logFile.Tail(8); + if (lines != oldLines) + { + l_size = logFile.getSize(); + int end; + if (lines.Length == oldLines.Length) + end = lines.Length - 1; + else + end = Math.Abs((lines.Length - oldLines.Length)) - 1; + + for (int count = 0; count < lines.Length; count++) + { + if (lines.Length < 1 && oldLines.Length < 1) + continue; + + if (lines[count] == oldLines[oldLines.Length - 1]) + continue; + + if (lines[count].Length < 10) //Not a needed line + continue; + + else + { + string[] game_event = lines[count].Split(';'); + Event event_ = Event.requestEvent(game_event, this); + if (event_ != null) + { + if (event_.Origin == null) + event_.Origin = new Player("WORLD", "-1", -1, 0); + + event_.Origin.lastEvent = event_; + event_.Origin.lastEvent.Owner = this; + + addEvent(event_); + } + } + + } + } + } + oldLines = lines; + l_size = logFile.getSize(); + Thread.Sleep(1); + } + catch (Exception E) + { + Log.Write("Something unexpected occured. Hopefully we can ignore it - " + E.Message + " @" + Utilities.GetLineNumber(E), Log.Level.All); + errors++; + continue; + } + + } + + RCONQueue.Abort(); + eventQueue.Abort(); + + } + + private bool intializeBasics() + { + try + { + //GET fs_basepath + String[] p = RCON.responseSendRCON("fs_basepath"); + + if (p == null) + { + Log.Write("Could not obtain basepath!", Log.Level.All); + return false; + } + + p = p[1].Split('"'); + Basepath = p[3].Substring(0, p[3].Length - 2).Trim(); + p = null; + + Thread.Sleep(FLOOD_TIMEOUT); + //END + + //get fs_game + p = RCON.responseSendRCON("fs_game"); + + if (p == null) + { + Log.Write("Could not obtain mod path!", Log.Level.All); + return false; + } + + p = p[1].Split('"'); + Mod = p[3].Substring(0, p[3].Length - 2).Trim().Replace('/', '\\'); + p = null; + + Thread.Sleep(FLOOD_TIMEOUT); + //END + + //get g_log + p = RCON.responseSendRCON("g_log"); + + if (p == null) + { + Log.Write("Could not obtain log path!", Log.Level.All); + return false; + } + + if (p.Length < 4) + { + Thread.Sleep(FLOOD_TIMEOUT); + Log.Write("Server does not appear to have map loaded. Please map_rotate", Log.Level.All); + return false; + } + + p = p[1].Split('"'); + string log = p[3].Substring(0, p[3].Length - 2).Trim(); + p = null; + + Thread.Sleep(FLOOD_TIMEOUT); + //END + + //get g_logsync + p = RCON.responseSendRCON("g_logsync"); + + if (p == null) + { + Log.Write("Could not obtain log sync status!", Log.Level.All); + return false; + } + + + p = p[1].Split('"'); + int logsync = Convert.ToInt32(p[3].Substring(0, p[3].Length - 2).Trim()); + p = null; + + Thread.Sleep(FLOOD_TIMEOUT); + if (logsync != 1) + RCON.sendRCON("g_logsync 1"); + + Thread.Sleep(FLOOD_TIMEOUT); + //END + + //get iw4m_onelog + p = RCON.responseSendRCON("iw4m_onelog"); + + if (p[0] == String.Empty || p[1].Length < 15) + { + Log.Write("Could not obtain iw4m_onelog value!", Log.Level.All); + return false; + } + + p = p[1].Split('"'); + string onelog = p[3].Substring(0, p[3].Length - 2).Trim(); + p = null; + //END + + Thread.Sleep(FLOOD_TIMEOUT); + + //get sv_hostname + p = RCON.responseSendRCON("sv_hostname"); + + if (p == null) + { + Log.Write("Could not obtain server name!", Log.Level.All); + return false; + } + + p = p[1].Split('"'); + hostname = p[3].Substring(0, p[3].Length - 2).Trim(); + p = null; + //END + + if (Mod == String.Empty || onelog == "1") + logPath = Basepath + '\\' + "m2demo" + '\\' + log; + else + logPath = Basepath + '\\' + Mod + '\\' + log; + + if (!File.Exists(logPath)) + { + Log.Write("Gamelog does not exist!", Log.Level.All); + return false; + } + + logFile = new file(logPath); + Log.Write("Log file is " + logPath, Log.Level.Debug); + + return true; + } + catch (Exception E) + { + Log.Write("Error during initialization - " + E.Message, Log.Level.All); + return false; + } + } + + //Process any server event + public bool processEvent(Event E) + { + if (E.Type == Event.GType.Connect) + { + this.addPlayer(E.Origin); + return true; + } + + if (E.Type == Event.GType.Disconnect) + { + if (getNumPlayers() > 0) + removePlayer(E.Origin.getClientNum()); + return true; + } + + if (E.Type == Event.GType.Say) + { + Log.Write("Message from " + E.Origin.getName() + ": " + E.Data, Log.Level.Debug); + + if (E.Data.Substring(0, 1) != "!") + return true; + + Command C = E.isValidCMD(commands); + if (C != null) + { + C = processCommand(E, C); + if (C != null) + { + C.Execute(E); + return true; + } + else + { + Log.Write("Error processing command by " + E.Origin.getName(), Log.Level.Debug); + return true; + } + } + + else + E.Origin.Tell("You entered an invalid command!"); + + return true; + } + + if (E.Type == Event.GType.MapChange) + { + Log.Write("Map change detected..", Log.Level.Production); + return true; + //TODO here + } + + if (E.Type == Event.GType.MapEnd) + { + Log.Write("Game ending...", Log.Level.Production); + return true; + } + + return false; + } + + //THESE MAY NEED TO BE MOVED + public void Broadcast(String Message) + { + RCON.addRCON("sayraw " + Message, 0); + } + + public void Tell(String Message, Player Target) + { + RCON.addRCON("tell " + Target.getClientNum() + " " + Message + "^7", 0); + } + + public void Kick(String Message, Player Target) + { + RCON.addRCON("clientkick " + Target.getClientNum() + " \"" + Message + "^7\"", 0); + } + + public void Ban(String Message, Player Target, Player Origin) + { + RCON.addRCON("tempbanclient " + Target.getClientNum() + " \"" + Message + "^7\"", 0); + if (Origin != null) + { + Target.setLevel(Player.Permission.Banned); + Ban newBan = new Ban(Target.getLastO(), Target.getID(), Origin.getID()); + Bans.Add(newBan); + DB.addBan(newBan); + DB.updatePlayer(Target); + } + } + + public bool Unban(String GUID) + { + foreach (Ban B in Bans) + { + if (B.getID() == GUID) + { + DB.removeBan(GUID); + Bans.Remove(B); + Player P = DB.getPlayer(GUID, 0); + P.setLevel(Player.Permission.User); + DB.updatePlayer(P); + return true; + } + } + return false; + } + + + public void fastRestart(int delay) + { + Utilities.Wait(delay); + RCON.addRCON("fast_restart", 0); + } + + public void mapRotate(int delay) + { + Utilities.Wait(delay); + RCON.addRCON("map_rotate", 0); + } + + public void tempBan(String Message, Player Target) + { + RCON.addRCON("tempbanclient " + Target.getClientNum() + " \"" + Message + "\"", 0); + } + + public void mapRotate() + { + RCON.addRCON("map_rotate", 0); + } + + public void Map(String map) + { + RCON.addRCON("map " + map, 0); + } + //END + + //THIS IS BAD BECAUSE WE DON"T WANT EVERYONE TO HAVE ACCESS :/ + public String getPassword() + { + return rcon_pass; + } + + private void initMaps() + { + file mapfile = new file("config\\maps.cfg"); + String[] _maps = mapfile.readAll(); + if (_maps.Length > 2) // readAll returns minimum one empty string + { + foreach (String m in _maps) + { + String[] m2 = m.Split(':'); + if (m2.Length > 1) + { + Map map = new Map(m2[0].Trim(), m2[1].Trim()); + maps.Add(map); + } + } + } + else + Log.Write("Maps configuration appears to be empty - skipping...", Log.Level.All); + } + + private void initMessages() + { + file messageCFG = new file("config\\messages.cfg"); + String[] lines = messageCFG.readAll(); + + if (lines.Length < 2) //readAll returns minimum one empty string + { + Log.Write("Messages configuration appears empty - skipping...", Log.Level.All); + return; + } + + int mTime = -1; + int.TryParse(lines[0], out mTime); + + if (messageTime == -1) + messageTime = 60; + else + messageTime = mTime; + + foreach (String l in lines) + { + if (lines[0] != l && l.Length > 1) + messages.Add(l); + } + } + + private void initRules() + { + file ruleFile = new file("config\\rules.cfg"); + String[] _rules = ruleFile.readAll(); + if (_rules.Length > 2) // readAll returns minimum one empty string + { + foreach (String r in _rules) + { + if (r.Length > 1) + rules.Add(r); + } + } + else + Log.Write("Rules configuration appears empty - skipping...", Log.Level.All); + } + + private void initCommands() + { + // Something like *COMMAND* | NAME | HELP MSG | ALIAS | NEEDED PERMISSION | # OF REQUIRED ARGS | HAS TARGET | + + commands = new List(); + + if(owner == null) + commands.Add(new Owner("owner", "claim ownership of the server", "owner", Player.Permission.User, 0, false)); + + commands.Add(new Kick("kick", "kick a player by name. syntax: !kick .", "k", Player.Permission.Moderator, 2, true)); + commands.Add(new Say("say", "broadcast message to all players. syntax: !say .", "s", Player.Permission.Moderator, 1, false)); + commands.Add(new TempBan("tempban", "temporarily ban a player for 1 hour. syntax: !tempban .", "tb", Player.Permission.Moderator, 2, true)); + commands.Add(new SBan("ban", "permanently ban a player from the server. syntax: !ban ", "b", Player.Permission.SeniorAdmin, 2, true)); + commands.Add(new WhoAmI("whoami", "give information about yourself. syntax: !whoami.", "who", Player.Permission.User, 0, false)); + commands.Add(new List("list", "list active clients syntax: !list.", "l", Player.Permission.Moderator, 0, false)); + commands.Add(new Help("help", "list all available commands. syntax: !help.", "l", Player.Permission.User, 0, false)); + commands.Add(new FastRestart("fastrestart", "fast restart current map. syntax: !fastrestart.", "fr", Player.Permission.Moderator, 0, false)); + commands.Add(new MapRotate("maprotate", "cycle to the next map in rotation. syntax: !maprotate.", "mr", Player.Permission.Administrator, 0, false)); + commands.Add(new SetLevel("setlevel", "set player to specified administration level. syntax: !setlevel .", "sl", Player.Permission.Owner, 2, true)); + commands.Add(new Usage("usage", "get current application memory usage. syntax: !usage.", "u", Player.Permission.Moderator, 0, false)); + commands.Add(new Uptime("uptime", "get current application running time. syntax: !uptime.", "up", Player.Permission.Moderator, 0, false)); + commands.Add(new Warn("warn", "warn player for infringing rules syntax: !warn .", "w", Player.Permission.Moderator, 2, true)); + commands.Add(new WarnClear("warnclear", "remove all warning for a player syntax: !warnclear .", "wc", Player.Permission.Administrator, 1, true)); + commands.Add(new Unban("unban", "unban player by guid. syntax: !unban .", "ub", Player.Permission.Administrator, 1, false)); + commands.Add(new Admins("admins", "list currently connected admins. syntax: !admins.", "a", Player.Permission.User, 0, false)); + commands.Add(new Wisdom("wisdom", "get a random wisdom quote. syntax: !wisdom", "w", Player.Permission.Administrator, 0, false)); + commands.Add(new MapCMD("map", "change to specified map. syntax: !map", "m", Player.Permission.Administrator, 1, false)); + commands.Add(new Find("find", "find player in database. syntax: !find ", "f", Player.Permission.Administrator, 1, false)); + commands.Add(new Rules("rules", "list server rules. syntax: !rules", "r", Player.Permission.User, 0, false)); + + /* + commands.Add(new commands { command = "stats", desc = "view your server stats.", requiredPer = 0 }); + commands.Add(new commands { command = "speed", desc = "change player speed. syntax: !speed ", requiredPer = 3 }); + commands.Add(new commands { command = "gravity", desc = "change game gravity. syntax: !gravity ", requiredPer = 3 }); + + commands.Add(new commands { command = "version", desc = "view current app version.", requiredPer = 0 });*/ + } + + //Objects + public Log Log; + public RCON RCON; + public Database DB; + public List Bans; + public Player owner; + public List maps; + public List rules; + public Queue events; + + //Info + private String IP; + private int Port; + private String hostname; + private int clientnum; + private string rcon_pass; + private List players; + private List commands; + private List messages; + private int messageTime; + private TimeSpan lastMessage; + private int nextMessage; + private int errors = 0; + + //Log stuff + private String Basepath; + private String Mod; + private String logPath; + private file logFile; + } +} diff --git a/Admin/Utilities.cs b/Admin/Utilities.cs new file mode 100644 index 000000000..3c4aecb82 --- /dev/null +++ b/Admin/Utilities.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; + +namespace IW4MAdmin +{ + class Utilities + { + //Get string with specified number of spaces -- really only for visual output + public static String getSpaces(int Num) + { + String SpaceString = String.Empty; + while (Num > 0) + { + SpaceString += ' '; + Num--; + } + + return SpaceString; + } + + //Sleep for x amount of seconds + public static void Wait(double time) + { + Thread.Sleep((int)Math.Ceiling(time*1000)); + } + + //Remove words from a space delimited string + public static String removeWords(String str, int num) + { + String newStr = String.Empty; + String[] tmp = str.Split(' '); + + for (int i = 0; i < tmp.Length; i++) + { + if (i >= num) + newStr += tmp[i] + ' '; + } + + return newStr; + } + + public static Player.Permission matchPermission(String str) + { + String lookingFor = str.ToLower(); + + for (Player.Permission Perm = Player.Permission.User; Perm < Player.Permission.Owner; Perm++) + { + if (lookingFor.Contains(Perm.ToString().ToLower())) + return Perm; + } + + return Player.Permission.Banned; + } + + public static String removeNastyChars(String str) + { + return str.Replace("`", "").Replace("'", "").Replace("\\", "").Replace("\"", "").Replace("^", "").Replace(""", "''"); + } + + public static int GetLineNumber(Exception ex) + { + var lineNumber = 0; + const string lineSearch = ":line "; + var index = ex.StackTrace.LastIndexOf(lineSearch); + if (index != -1) + { + var lineNumberText = ex.StackTrace.Substring(index + lineSearch.Length); + if (int.TryParse(lineNumberText, out lineNumber)) + { + } + } + return lineNumber; + } + } +} diff --git a/IW4M Admin.sln b/IW4M Admin.sln new file mode 100644 index 000000000..a4117879e --- /dev/null +++ b/IW4M Admin.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.31101.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IW4M ADMIN", "Admin\IW4M ADMIN.csproj", "{DD5DCDA2-51DB-4B1A-922F-5705546E6115}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DD5DCDA2-51DB-4B1A-922F-5705546E6115}.Debug|Any CPU.ActiveCfg = Release|Any CPU + {DD5DCDA2-51DB-4B1A-922F-5705546E6115}.Debug|Any CPU.Build.0 = Release|Any CPU + {DD5DCDA2-51DB-4B1A-922F-5705546E6115}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DD5DCDA2-51DB-4B1A-922F-5705546E6115}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal