diff --git a/Minecraft Server Starter.sln b/Minecraft Server Starter.sln new file mode 100644 index 0000000..7f2a264 --- /dev/null +++ b/Minecraft Server Starter.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.24720.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Minecraft Server Starter", "Minecraft Server Starter\Minecraft Server Starter.csproj", "{22E6F114-52F5-447E-82A3-D951E5E82E5F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {22E6F114-52F5-447E-82A3-D951E5E82E5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {22E6F114-52F5-447E-82A3-D951E5E82E5F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {22E6F114-52F5-447E-82A3-D951E5E82E5F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {22E6F114-52F5-447E-82A3-D951E5E82E5F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/Minecraft Server Starter/App.config b/Minecraft Server Starter/App.config new file mode 100644 index 0000000..37830cc --- /dev/null +++ b/Minecraft Server Starter/App.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/Minecraft Server Starter/App.xaml b/Minecraft Server Starter/App.xaml new file mode 100644 index 0000000..2261259 --- /dev/null +++ b/Minecraft Server Starter/App.xaml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Minecraft Server Starter/App.xaml.cs b/Minecraft Server Starter/App.xaml.cs new file mode 100644 index 0000000..6446100 --- /dev/null +++ b/Minecraft Server Starter/App.xaml.cs @@ -0,0 +1,94 @@ +// Made by Lonami Exo (C) LonamiWebs +// Creation date: february 2016 +// Modifications: +// - No modifications made +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Windows; + +namespace Minecraft_Server_Starter +{ + public partial class App : Application + { + public App() + { + InitializeComponent(); + + SelectCulture(Thread.CurrentThread.CurrentUICulture.ToString()); + + Settings.Init("LonamiWebs\\Minecraft Server Starter", new Dictionary + { + { "eulaAccepted", false }, + { "minRam", 512 }, + { "maxRam", 1024 }, + { "javaPath", Java.FindJavaPath() }, + { "priority", (int)ProcessPriorityClass.Normal }, + { "notificationEnabled", true }, + { "notificationLoc", (int)Toast.Location.TopLeft }, + { "mssFolder", Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "LonamiWebs\\Minecraft Server Starter")}, + { "ignoreCommandsBlock", true } + }); + } + + public static void SelectCulture(string culture) + { + // List all our resources + List dictionaryList = new List(); + foreach (ResourceDictionary dictionary in Current.Resources.MergedDictionaries) + dictionaryList.Add(dictionary); + + // We want our specific culture + string requestedCulture = string.Format("Strings.{0}.xaml", culture); + ResourceDictionary resourceDictionary = dictionaryList.FirstOrDefault(d => d.Source.OriginalString == requestedCulture); + if (resourceDictionary == null) + { + requestedCulture = "Strings.xaml"; + resourceDictionary = dictionaryList.FirstOrDefault(d => d.Source.OriginalString == requestedCulture); + } + + // If we have the requested resource, remove it from the list and place at the end.\ + // Then this language will be our string table to use. + if (resourceDictionary != null) + { + Application.Current.Resources.MergedDictionaries.Remove(resourceDictionary); + Application.Current.Resources.MergedDictionaries.Add(resourceDictionary); + } + // Inform the threads of the new culture + Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(culture); + Thread.CurrentThread.CurrentUICulture = new CultureInfo(culture); + } + } +} + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Minecraft Server Starter/Classes/MinecraftClasses/Backup.cs b/Minecraft Server Starter/Classes/MinecraftClasses/Backup.cs new file mode 100644 index 0000000..00866ab --- /dev/null +++ b/Minecraft Server Starter/Classes/MinecraftClasses/Backup.cs @@ -0,0 +1,427 @@ +/// +/// Copyright (c) 2016 All Rights Reserved +/// +/// Lonami Exo +/// February 2016 +/// Class representing a backup + +using System; +using System.IO; +using System.IO.Compression; +using System.Runtime.Serialization; +using System.Threading.Tasks; +using ExtensionMethods; +using System.Collections.Generic; + +using SProperties = Minecraft_Server_Starter.ServerProperties; + +namespace Minecraft_Server_Starter +{ + [DataContract] + public class Backup + { + #region Constant fields + + // the extension used to save Backup files + const string fileExtension = ".info"; + + // where the backups are stored + static string backupsFolder => + Path.Combine(Settings.GetValue("mssFolder"), "Backups"); + + // the base location for this backup + string baseLocation => Path.Combine(backupsFolder, ID); + + #endregion + + #region Public properties + + [DataMember] + public Server Server { get; set; } + + public string ID { get { return creationDate.ToString(); } } + public string DisplayName + { + get + { + return Server.Name + " " + + CreationDate.ToShortDateString() + " " + + CreationDate.ToLongTimeString(); + } + } + + public string Size + => new FileInfo(baseLocation + ".zip").Length.ToFileSizeString(); + + [DataMember] + public bool Worlds { get; set; } + [DataMember] + public bool ServerProperties { get; set; } + [DataMember] + public bool WhiteList { get; set; } + [DataMember] + public bool Ops { get; set; } + [DataMember] + public bool Banned { get; set; } + [DataMember] + public bool Logs { get; set; } + + [DataMember] + public bool Everything { get; set; } + + public DateTime CreationDate { get { return UnixDate.UnixTimeToDateTime(creationDate); } } + [DataMember] + long creationDate { get; set; } + + [DataMember] + string worldsName { get; set; } + + #endregion + + #region Constructors + + public Backup(Server server) : this(server, + true, true, true, true, true, true, true) + { } + + public Backup(Server server, bool worlds, bool serverProperties, bool whiteList, + bool ops, bool banned, bool logs, bool everything) + { + Server = server; + + Worlds = worlds; + ServerProperties = serverProperties; + WhiteList = whiteList; + Ops = ops; + Banned = banned; + Logs = logs; + + Everything = everything; + } + + #endregion + + #region Public methods + + /// + /// Sets the creation date to now + /// + public void SetCreation() + { + creationDate = UnixDate.DateTimeToUnixTime(DateTime.Now); + } + + /// + /// Generates a new backup from an old backup + /// + /// The old backup without the creation date set + /// The backup with the creation dae set + public static Backup Generate(Backup backup) + { + backup.creationDate = UnixDate.DateTimeToUnixTime(DateTime.Now); + return backup; + } + + #endregion + + #region Private methods + + // what files do we need to copy from the given folder with the current settings? + IEnumerable GetRequiredCopyFiles(string folder) + { + var propertiesPath = Path.Combine(folder, SProperties.PropertiesName); + string tmpFile; + + if (ServerProperties) + { + yield return propertiesPath; + } + if (WhiteList) + { + // new server old server + tmpFile = GetIfExistsFile(folder, "whitelist.json", "white-list.txt"); + if (!string.IsNullOrEmpty(tmpFile)) + yield return tmpFile; + } + if (Ops) + { + // new server old server + tmpFile = GetIfExistsFile(folder, "ops.json", "ops.txt"); + if (!string.IsNullOrEmpty(tmpFile)) + yield return tmpFile; + } + if (Banned) + { + // new server old server + tmpFile = GetIfExistsFile(folder, "banned-ips.json", "banned-ips.txt"); + if (!string.IsNullOrEmpty(tmpFile)) + yield return tmpFile; + + // new server old server + tmpFile = GetIfExistsFile(folder, "banned-players.json", "banned-players.txt"); + if (!string.IsNullOrEmpty(tmpFile)) + yield return tmpFile; + } + if (Logs) + { + // old server + tmpFile = GetIfExistsFile(folder, "server.log"); + if (!string.IsNullOrEmpty(tmpFile)) + yield return tmpFile; + } + } + + // what directories do we need to copy from the given folder with the current settings? + IEnumerable GetRequiredCopyDirectories(string folder) + { + var propertiesPath = Path.Combine(folder, SProperties.PropertiesName); + var properties = SProperties.FromFile(propertiesPath); + + if (Worlds) + yield return Path.Combine(folder, (worldsName = properties["level-name"].Value)); + + if (Logs) + { + // new server + var dir = Path.Combine(folder, "logs"); + if (Directory.Exists(dir)) + yield return dir; + } + } + + // gets the first existing file from one of the given possibilities + static string GetIfExistsFile(string folder, params string[] possibilities) + { + foreach (var possibility in possibilities) + if (File.Exists(Path.Combine(folder, possibility))) + return Path.Combine(folder, possibility); + + return null; + } + + // saves the current backup to the specified file + void Save(string file) => Serializer.Serialize(this, file); + + // loads a backup from the specified file + static Backup Load(string file) => Serializer.Deserialize(file); + + #endregion + + #region Saving + + /// + /// Saves the backup, determining whether the server is running (to backup a copy instead the original) or not + /// + /// If the server is running, a temporary copy of itself will be created so no errors are thrown + /// True if the operation was successful + public async Task Save(bool isServerRunning) + { + SetCreation(); + bool success = true; + + await Task.Factory.StartNew(() => + { + var zipLocation = baseLocation + ".zip"; + var bakLocation = baseLocation + fileExtension; + + try + { + if (!Directory.Exists(Path.GetDirectoryName(baseLocation))) + Directory.CreateDirectory(Path.GetDirectoryName(baseLocation)); + + var folder = Server.Location; + var folderName = Path.GetFileName(folder); + + // so we can perform the backup even with server running + var tmpFolder = Path.Combine(Path.GetTempPath(), + Path.GetFileNameWithoutExtension(Path.GetTempFileName()), folderName); + + if (Everything) + { + if (isServerRunning) // if the server is open, copy the server and then perform the backup + { + new DirectoryInfo(folder).CopyDirectory(new DirectoryInfo(tmpFolder)); + + ZipFile.CreateFromDirectory(tmpFolder, zipLocation); + + try { Directory.Delete(tmpFolder, true); } + catch { }; + } + else // otherwise perform the backup directly + { + ZipFile.CreateFromDirectory(folder, zipLocation); + } + } + else + { + using (ZipArchive zip = ZipFile.Open(zipLocation, ZipArchiveMode.Create)) + { + if (isServerRunning) + { + Directory.CreateDirectory(tmpFolder); + + // copy all the files and directories to a temporary location before adding them to the zip file + foreach (var cfile in GetRequiredCopyFiles(folder)) + { + var tmpFile = Path.Combine(tmpFolder, Path.GetFileName(cfile)); + File.Copy(cfile, tmpFile); + zip.AddFile(tmpFile); + } + + foreach (var cdir in GetRequiredCopyDirectories(folder)) + { + var tmpDir = Path.Combine(tmpFolder, Path.GetFileName(cdir)); + new DirectoryInfo(cdir).CopyDirectory(new DirectoryInfo(tmpDir)); + zip.AddDirectory(tmpDir); + } + + // clear the temporary directory + try { Directory.Delete(tmpFolder, true); } + catch { }; + } + else + { + // if the server is closed, directly add all the files and directories to the zip + foreach (var cfile in GetRequiredCopyFiles(folder)) + zip.AddFile(cfile); + + foreach (var cdir in GetRequiredCopyDirectories(folder)) + zip.AddDirectory(cdir); + } + } + } + + // if we got this far, save the backup information + Save(baseLocation + fileExtension); + } + catch // perhaps the server log is being used, or it doesn't exist; clear failed files + { + if (File.Exists(baseLocation + ".zip")) + try { File.Delete(baseLocation + ".zip"); } + catch { } + + success = false; + } + }); + + return success; + } + + #endregion + + #region Loading + + /// + /// Loads a backup + /// + /// For which server? + /// Should the worlds be loaded? + /// Should the server properties be loaded? + /// Should the white list be loaded? + /// Should the op list be loaded? + /// Should the banned players list be loaded? + /// Should the logs be loaded? + /// Should just everything be loaded? + public void Load(Server server, bool worlds, bool serverProperties, bool whiteList, + bool ops, bool banned, bool logs, bool everything) + { + // if all the options match, we want to extract everything from the backup + bool all = Worlds == worlds && + ServerProperties == serverProperties && + WhiteList == whiteList && + Ops == ops && + Banned == banned && + Logs == logs; + + using (ZipArchive zip = ZipFile.Open(baseLocation + ".zip", ZipArchiveMode.Read)) + { + if (all) + zip.ExtractToDirectory(server.Location, true); + + // else, extract only those options which we want + else + { + foreach (var entry in zip.Entries) + { + var entryName = entry.FullName.Contains("/") ? + entry.FullName.Substring(0, entry.FullName.IndexOf('/') + 1) : + entry.FullName; + + if ( + (worlds && entryName == worldsName + "/") || + (logs && (entryName == "logs/" || entryName == "server.log")) || + (serverProperties && entryName == "server.properties") || + (whiteList && (entryName == "whitelist.json" || entryName == "white-list.txt")) || + (ops && (entryName == "ops.json" || entryName == "ops.txt")) || + (banned && (entryName == "banned-ips.json" || entryName == "banned-ips.txt" || + entryName == "banned-players.json" || entryName == "banned-players.txt"))) + { + entry.ExtractToFileSafe(Path.Combine(server.Location, entry.FullName), true); + } + } + } + } + } + + /// + /// Retrieves a backup given its id + /// + /// The id of the backup to retrieve + /// The retrieved backup + public static Backup GetBackup(string id) + { + var file = Path.Combine(backupsFolder, id + fileExtension); + if (File.Exists(file)) + return Load(file); + + return null; + } + + /// + /// Gets all the available backups + /// + /// What backup name should be shown first? + /// All the saved backups + public static List GetBackups(string prioritySortingName) + { + try + { + if (Directory.Exists(backupsFolder)) + { + var files = Directory.GetFiles(backupsFolder, "*" + fileExtension); + var backups = new List(files.Length); + foreach (var file in files) + backups.Add(Load(file)); + + backups.Sort(new BackupSort(prioritySortingName)); + + return backups; + } + + Directory.CreateDirectory(backupsFolder); + } + catch { } + + return new List(); + } + + #endregion + + #region Deleting + + public bool Delete() + { + if (File.Exists(baseLocation + fileExtension)) + try { File.Delete(baseLocation + fileExtension); } + catch { return false; } + + if (File.Exists(baseLocation + ".zip")) + try { File.Delete(baseLocation + ".zip"); } + catch { return false; } + + return true; + } + + #endregion + } +} diff --git a/Minecraft Server Starter/Classes/MinecraftClasses/EULA.cs b/Minecraft Server Starter/Classes/MinecraftClasses/EULA.cs new file mode 100644 index 0000000..b420a95 --- /dev/null +++ b/Minecraft Server Starter/Classes/MinecraftClasses/EULA.cs @@ -0,0 +1,40 @@ +/// +/// Copyright (c) 2016 All Rights Reserved +/// +/// Lonami Exo +/// February 2016 +/// Class used to prompt Mojang's EULA message + +using System.IO; + +namespace Minecraft_Server_Starter +{ + /// + /// Class to interop with Minecraft EULA + /// + public static class EULA + { + // a string representing the Minecraft eula + const string eulaString = +@"#By changing the setting below to TRUE you are indicating your agreement to our EULA (https://account.mojang.com/documents/Minecraft_eula). +#{0} +eula=true +"; + + /// + /// Create an EULA file if it's set to false or it doesn't exist + /// + public static void CreateEULA(string dir) + { + if (string.IsNullOrEmpty(dir)) + return;// + + if (!Directory.Exists(dir)) + Directory.CreateDirectory(dir); + + string eula = Path.Combine(dir, "eula.txt"); + if (!File.Exists(eula) || File.ReadAllText(eula).Contains("eula=false")) + File.WriteAllText(eula, string.Format(eulaString, UnixDate.GetUniversalDate())); + } + } +} diff --git a/Minecraft Server Starter/Classes/MinecraftClasses/Heads.cs b/Minecraft Server Starter/Classes/MinecraftClasses/Heads.cs new file mode 100644 index 0000000..6a608dc --- /dev/null +++ b/Minecraft Server Starter/Classes/MinecraftClasses/Heads.cs @@ -0,0 +1,66 @@ +/// +/// Copyright (c) 2016 All Rights Reserved +/// +/// Lonami Exo +/// February 2016 +/// A simple class to get hum... Minecraft players heads from their skins + +using ExtensionMethods; +using System; +using System.IO; +using System.Threading.Tasks; +using System.Windows.Media.Imaging; + +namespace Minecraft_Server_Starter +{ + class Heads + { + #region Constant fields + + // where the cached heads are stored + static string headsFolder => + Path.Combine(Settings.GetValue("mssFolder"), "Heads"); + + #endregion + + #region Public methods + + /// + /// Return a player's head, and caches it in disk if it didn't exist yet + /// + /// The username to get the head from + /// The player head + public static async Task GetPlayerHead(string player) + { + var file = Path.Combine(headsFolder, player + ".png"); + if (File.Exists(file)) + return new BitmapImage(new Uri(file)); + + if (!Directory.Exists(headsFolder)) + Directory.CreateDirectory(headsFolder); + + var skin = await SkinDownloader.GetSkinHead(player); + skin.Save(file); + return skin; + } + + /// + /// Clears the player heads from disk. Returns false if some couldn't be deleted + /// + /// + public static bool ClearPlayerHeads() + { + bool success = true; + if (!Directory.Exists(headsFolder)) return success; + + foreach (var head in Directory.GetFiles(headsFolder, "*.png")) + { + try { File.Delete(head); } + catch { success = false; } + } + return success; + } + + #endregion + } +} diff --git a/Minecraft Server Starter/Classes/MinecraftClasses/MinecraftServer.cs b/Minecraft Server Starter/Classes/MinecraftClasses/MinecraftServer.cs new file mode 100644 index 0000000..e650722 --- /dev/null +++ b/Minecraft Server Starter/Classes/MinecraftClasses/MinecraftServer.cs @@ -0,0 +1,394 @@ +/// +/// Copyright (c) 2016 All Rights Reserved +/// +/// Lonami Exo +/// February 2016 +/// Class used to interop with a Minecraft server + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; + +namespace Minecraft_Server_Starter +{ + public enum Status { Opening, Open, Closing, Closed } + + public class MinecraftServer + { + #region Private fields + + #region String analysis regexs + + // two possible formats for time: + // yyyy-MM-dd hh:mm:ss + // [hh:mm:ss] + Regex timeRegex = new Regex(@"(?:\[\d{2}:\d{2}:\d{2}\])|(?:\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})", RegexOptions.Compiled); + // three possible formats for type: + // [(INFO|WARN|ERROR)] + // [Server thread/(INFO|WARN|ERROR)] + // [Server Shutdown Thread/INFO] + Regex typeRegex = new Regex(@"\[(?:Server(?: Shutdown)? (?:t|T)hread\/)?(INFO|WARN|WARNING|ERROR)\]", RegexOptions.Compiled); + + // only one format: Done (d... + Regex loadedRegex = new Regex(@"Done \(\d", RegexOptions.Compiled); + + Regex commandBlockRegex = new Regex(@"\[(.+?)\]", RegexOptions.Compiled); + const string serverName = "Server"; // avoid these on command blocks + + Regex playerJoinedRegex = new Regex(@"(\w+) ?\[\/", RegexOptions.Compiled); + Regex playerLeftRegex = new Regex(@"(\w+) lost connection", RegexOptions.Compiled); + + // spigot format + Regex spigotTimeTypeRegex = new Regex(@"\[(\d{2}:\d{2}:\d{2}) (INFO|WARN|ERROR)\]: ", RegexOptions.Compiled); + + #endregion + + #region Holders + + // status + Status _ServerStatus = Status.Closed; + + // the process holding the server + Process server; + // the streamwriter to the server process + StreamWriter input; + + List onlinePlayers = new List(); + + #endregion + + #endregion + + #region Public properties + + // returns the current server status + public Status ServerStatus + { + get { return _ServerStatus; } + set + { + var lstValue = _ServerStatus; + _ServerStatus = value; + + if (value == Status.Opening && value != lstValue) + onServerMessage(Res.GetStr("initializingServer")); + + if (value == Status.Closed && value != lstValue) + onServerMessage(Res.GetStr("serverHasClosed")); + + onServerStatusChanged(value); + } + } + + // selected server + public Server Server { get; private set; } + + // online players + public List OnlinePlayers { get { return new List(onlinePlayers); } } + + // ignore commands block + public bool IgnoreCommandBlocks { + get { return Settings.GetValue("ignoreCommandsBlock"); } + set { Settings.SetValue("ignoreCommandsBlock", value); } + } + + #endregion + + #region Events + + // occurs when the server changes it's status + public delegate void ServerStatusChangedEventHandler(Status status); + public event ServerStatusChangedEventHandler ServerStatusChanged; + void onServerStatusChanged(Status status) + { + if (ServerStatusChanged != null) + ServerStatusChanged(status); + } + + // occurs when the server's output receives content + public delegate void ServerMessageEventHandler(string time, string type, string typeName, string message); + public event ServerMessageEventHandler ServerMessage; + void onServerMessage(string msg) + { + if (ServerMessage != null) + { + var tuple = analyseMessage(msg); + if (tuple != null) // might be invalid input, such as command blocks when they're disabled + ServerMessage(tuple.Item1, tuple.Item2, tuple.Item3, tuple.Item4); + } + } + + // occurs when an analysed message represents a player joining/leaving + public delegate void PlayerEventHandler(bool joined, string player); + public event PlayerEventHandler Player; + void onPlayer(bool joined, string player) + { + if (joined) onlinePlayers.Add(player); + else onlinePlayers.Remove(player); + + if (Player != null) + Player(joined, player); + } + + #endregion + + #region Constructors + + public MinecraftServer(Server server) + { + Server = server; + EULA.CreateEULA(Server.Location); + } + + #endregion + + #region Current server management + + // start the server + public void Start() + { + if (ServerStatus != Status.Closed) + return; // if it's not closed, we can't "unclose" it because it already is + + // notify server is opening + ServerStatus = Status.Opening; + + Server.Use(); // update last use date + + // create a new server process + server = Process.Start(getPsi()); + + // attach event handlers + server.OutputDataReceived += server_OutputDataReceived; + server.ErrorDataReceived += server_OutputDataReceived; + server.Exited += server_Exited; + + // set streamwriter to it's stdin + input = server.StandardInput; + + // begin read async + server.BeginOutputReadLine(); + server.BeginErrorReadLine(); + } + + // kill the server + public void Kill() + { + if (ServerStatus == Status.Closed) + return; // already closed, return + + // stop receiving poop + server.OutputDataReceived -= server_OutputDataReceived; + server.ErrorDataReceived -= server_OutputDataReceived; + server.Exited -= server_Exited; + + // DIE, DIE!! MWAHAHAHA... right + server.Kill(); + ServerStatus = Status.Closed; + } + + #endregion + + #region Commands + + // send a command + public void SendCommand(string command) { input.WriteLine(command); } + + // stop (saving first) + public void Stop() { SendCommand("stop"); } + + // say something! + public void Say(string msg) { SendCommand("say " + msg); } + + // kick a noob + public void Kick(string player) { SendCommand("kick " + player); } + + // ban a very noob + public void Ban(string player) { SendCommand("ban " + player); } + + // pardon (unban) an innocent noob + public void Pardon(string player) { SendCommand("pardon " + player); } + + // op a pro player + public void Op(string player) { SendCommand("op " + player); } + + // deop a traitor + public void Deop(string player) { SendCommand("deop " + player); } + + // toggle rain + public void ToggleRain() { SendCommand("toggledownfall"); } + + // invoke the sun + public void SetDay() { SendCommand("time set 0"); } + + // invoke the moon + public void SetNight() { SendCommand("time set 14000"); } + + // save the world + public void Save() { SendCommand("save-all"); } + + #endregion + + #region Server output handling + + // the server wrote something to it's stdout + void server_OutputDataReceived(object sender, DataReceivedEventArgs e) + { + if (e.Data == null) + { + ServerStatus = Status.Closed; + return; + } + else + onServerMessage(e.Data); + } + + // oops, someone died, not me (._ . ") + void server_Exited(object sender, EventArgs e) + { + ServerStatus = Status.Closed; + } + + #endregion + + #region Server messages analysis + + // return a tuple formed by + Tuple analyseMessage(string msg) + { + // initialize result variables + string time, type, typeName, message; + time = type = typeName = string.Empty; + + // find the most basic matches + var timeMatch = timeRegex.Match(msg); + var typeMatch = typeRegex.Match(msg); + + // check index to ensure no player is trolling us + if (timeMatch.Success && timeMatch.Index == 0) + { + time = timeMatch.Value + " "; + + // if typeMatch is right after timeMatch, success + if (typeMatch.Success && typeMatch.Index == timeMatch.Length + 1) + { + type = typeMatch.Value; + typeName = typeMatch.Groups[1].Value; + message = msg.Replace(timeMatch.Value, "").Replace(typeMatch.Value, "").Trim(); + + var addLength = 2; // ": ".Length + if (!message.StartsWith(": ")) + { + addLength = 1; // the server message is closer than expected (old Minecraft version) + message = ": " + message; + } + + // to ensure it isn't a player trolling us! + var matchIndex = typeMatch.Index + typeMatch.Length + addLength; + + if (!checkCommonRegexs(msg, matchIndex)) + return null; + } + else // if no match found, only set a plain message + message = msg.Replace(timeMatch.Value, "").Trim(); + } + else // if absolutely no match found, it might be spigot + { + var spigotTimeTypeMatch = spigotTimeTypeRegex.Match(msg); + if (spigotTimeTypeMatch.Success && spigotTimeTypeMatch.Index == 0) // yes! spigot! + { + time = "[" + spigotTimeTypeMatch.Groups[1].Value + "]"; // time + typeName = spigotTimeTypeMatch.Groups[2].Value; // type + type = " [" + typeName + "]: "; + message = msg.Replace(spigotTimeTypeMatch.Value, ""); + + if (!checkCommonRegexs(msg, spigotTimeTypeMatch.Length)) + return null; + } + else + { + // not even spigot, only set a plain message + message = msg; + } + } + + return new Tuple(time, type, typeName, message); + } + + // check common (normal server or spigot) matches, return true if output is ok + bool checkCommonRegexs(string msg, int matchIndex) + { + // if server isn't open yet... + if (ServerStatus != Status.Open) + { + // check if it's a "Done!" message (server is loaded) + var loadedMatch = loadedRegex.Match(msg, matchIndex); + if (loadedMatch.Success && loadedMatch.Index == matchIndex) + { + ServerStatus = Status.Open; + server.PriorityClass = (ProcessPriorityClass)Settings.GetValue("priority"); + } + } + else + { + if (IgnoreCommandBlocks) + { + // check if it's a "[@: ...]" message (command block output) + var commandBlockMatch = commandBlockRegex.Match(msg, matchIndex); + if (commandBlockMatch.Success && + commandBlockMatch.Index == matchIndex && + commandBlockMatch.Groups[1].Value != serverName) + { + return false; // output is not ok + } + } + + // match for player joining/leaving + var playerJoinedMatch = playerJoinedRegex.Match(msg, matchIndex); + var playerLeftMatch = playerLeftRegex.Match(msg, matchIndex); + + // we may need to fire a player status + if (playerJoinedMatch.Success && playerJoinedMatch.Index == matchIndex) + onPlayer(true, playerJoinedMatch.Groups[1].Value); + + else if (playerLeftMatch.Success && playerLeftMatch.Index == matchIndex) + onPlayer(false, playerLeftMatch.Groups[1].Value); + } + return true; // output is ok! + } + + #endregion + + #region Private methods + + // get the process start info to start the selected server + ProcessStartInfo getPsi() + { + var arguments = new StringBuilder(); + arguments.Append("-Xms"); + arguments.Append(Settings.GetValue("minRam")); + arguments.Append("M -Xmx"); + arguments.Append(Settings.GetValue("maxRam")); + arguments.Append("M -jar \""); + arguments.Append(Server.ServerJar); + arguments.Append("\" nogui"); + + return new ProcessStartInfo + { + FileName = Settings.GetValue("javaPath"), + Arguments = arguments.ToString(), + CreateNoWindow = true, + UseShellExecute = false, + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + WorkingDirectory = Server.Location + }; + } + + #endregion + } +} diff --git a/Minecraft Server Starter/Classes/MinecraftClasses/MinecraftVersions.cs b/Minecraft Server Starter/Classes/MinecraftClasses/MinecraftVersions.cs new file mode 100644 index 0000000..cf1c778 --- /dev/null +++ b/Minecraft Server Starter/Classes/MinecraftClasses/MinecraftVersions.cs @@ -0,0 +1,398 @@ +/// +/// Copyright (c) $year$ All Rights Reserved +/// +/// Lonami Exo +/// february 2016 +/// Classes determining the Minecraft version number plus the download URL + +using System; +using System.IO; +using System.Net; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Json; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Minecraft_Server_Starter +{ + /// + /// Minecraft versions, latest and all the available + /// + [DataContract] + public class MinecraftVersions + { + #region Constant fields + + // "__comment": "This URL is being phased out! Please update your scripts to check https://launchermeta.mojang.com/mc/game/version_manifest.json instead. Thanks <3 —Dinnerbone", + //const string versionsUrl = "https://www.npmjs.com/package/minecraft-versions"; + + const string versionsUrl = "https://launchermeta.mojang.com/mc/game/version_manifest.json"; + + #endregion + + #region Properties + + /// + /// Latest versions IDs (names) + /// + [DataMember(Name = "latest")] + public Latest Latest { get; set; } + + /// + /// All available versions + /// + [DataMember(Name = "versions")] + public MinecraftVersion[] Versions { get; set; } + + #endregion + + #region JSON + + public string ToJSON() + { + using (var ms = new MemoryStream()) + using (var sr = new StreamReader(ms)) + { + var serializer = new DataContractJsonSerializer(typeof(MinecraftVersions)); + serializer.WriteObject(ms, this); + + ms.Position = 0; + return sr.ReadToEnd(); + } + } + + public static MinecraftVersions FromJSON(string json) + { + using (Stream stream = new MemoryStream()) + { + byte[] data = Encoding.UTF8.GetBytes(json); + stream.Write(data, 0, data.Length); + stream.Position = 0; + var deserializer = new DataContractJsonSerializer(typeof(MinecraftVersions)); + + return (MinecraftVersions)deserializer.ReadObject(stream); + } + } + + #endregion + + #region Get versions + + /// + /// Gets all the available Minecraft versions. Requires an internet connection + /// + /// All the Minecraft versions + public static async Task GetLatests() + { + using (var wc = new WebClient()) + return FromJSON(await wc.DownloadStringTaskAsync(versionsUrl)); + } + + #endregion + + #region Overrides + + public override string ToString() + { + return $"{Versions.Length} version(s); {Latest}"; + } + + #endregion + + #region Comparision + + public static async Task Compare(string versionId1, string versionId2) + { + return Compare(versionId1, versionId2, await GetLatests()); + } + + /// + /// Determines which version is greater, if left (-1) or right (+1). Returns 0 if unknown or equal + /// + public static int Compare(string leftId, string rightId, MinecraftVersions versions) + { + bool leftEquals = false; + bool rightEquals = false; + + for (int i = 0; i < versions.Versions.Length; i++) + { + leftEquals = versions.Versions[i].ID.Equals(leftId); + rightEquals = versions.Versions[i].ID.Equals(rightId); + + if (leftEquals && rightEquals) + return 0; + + if (leftEquals) + return -1; + + if (rightEquals) + return 1; + } + + return 0; + } + + #endregion + } + + /// + /// Latest Minecraft version snapshot and release + /// + [DataContract] + public class Latest + { + /// + /// Latest snapshot version + /// + [DataMember(Name = "snapshot")] + public string Snapshot { get; set; } + + /// + /// Latest release version + /// + [DataMember(Name = "release")] + public string Release { get; set; } + + public override string ToString() + { + return $"Latest snapshot: {Snapshot}; Latest release: {Release}"; + } + } + + /// + /// Minecraft version + /// + [DataContract] + public class MinecraftVersion + { + #region Constant fields + + // base minecraft server urls + const string baseServerUrl = "https://s3.amazonaws.com/Minecraft.Download/versions/{0}/minecraft_server.{0}.jar"; + const string baseClientUrl = "https://s3.amazonaws.com/Minecraft.Download/versions/{0}/{0}.jar"; + + + // old minecraft server urls + const string old12Url = "http://assets.minecraft.net/1_2/minecraft_server.jar"; + const string old10Url = "https://s3.amazonaws.com/MinecraftDownload/launcher/minecraft_server.jar"; + + #endregion + + #region Properties + + string _ID; + /// + /// The ID of the Minecraft version (a.k.a. version name) + /// + [DataMember(Name = "id")] + public string ID + { + get { return _ID; } + set + { + _ID = value; + // server + switch (_ID) + { + case "1.2.4": + case "1.2.3": + case "1.2.2": + case "1.2.1": + ServerUrl = old12Url; break; + + case "1.0": + ServerUrl = old10Url; break; + + default: + if ( // if id starts with any of these + _ID[0] == 'a' || // alpha + _ID[0] == 'b' || // beta + _ID[0] == 'c' || // old_alpha + _ID[0] == 'r' || // old_alpha + _ID[0] == 'i') // old_alpha + break; // do nothing + + ServerUrl = string.Format(baseServerUrl, _ID); break; + } + // client + switch (_ID) + { + default: // always works + ClientUrl = string.Format(baseClientUrl, _ID); + break; + } + } + } + + /// + /// When was this version released? + /// + [DataMember(Name = "releaseTime")] + public string ReleaseTime { get; set; } + + /// + /// Version time + /// + [DataMember(Name = "time")] + public string Time { get; set; } + + /// + /// Version type: "release", "snapshot", "old_beta" or "old_alpha" + /// + [DataMember(Name = "type")] + public string Type { get; set; } + + /// + /// Url pointing to a .json file containing more information of this version + /// + [DataMember(Name = "url")] + public string Url { get; set; } + + /// + /// Url pointing to the minecraft_server.jar file corresponding to this version. It can be null + /// + public string ServerUrl { get; set; } + + /// + /// Url pointing to the client .jar file corresponding to this version + /// + public string ClientUrl { get; set; } + + #endregion + + #region Constructors + + public MinecraftVersion() { } + + public MinecraftVersion(string version) + { + ID = version; + + switch(ID[0]) + { + // alpha + case 'a': Type = "alpha"; break; + + // beta or old_beta + case 'b': Type = "beta"; break; + + // old_alpha + case 'c': + case 'r': + case 'i': Type = "old_alpha"; break; + + // release or snapshot + default: Type = ID.Contains("pre") || ID.Contains("w") ? "snapshot" : "release"; + break; + } + } + + #endregion + + #region Server downloading and caching + + const string serverName = "minecraft_server.{0}.jar"; + static string cacheFolder => Path.Combine(Settings.GetValue("mssFolder"), "Cache"); + + /// + /// Copy a cached minecraft_server.jar file to the desired location. + /// If it doesn't exist, it will be downloaded + /// + /// Where the minecraft_server.jar will be copied + /// A progress to keep track of the current download + /// A cancellation token to cancel the file from being downloaded + /// True if the operation was successful + public async Task CopyServer(string copyTo, + IProgress progress = null, CancellationTokenSource cts = null) + { + var cached = Path.Combine(cacheFolder, string.Format(serverName, ID)); + + try + { + if (!File.Exists(cached)) + { + if (!Directory.Exists(cacheFolder)) + Directory.CreateDirectory(cacheFolder); + + using (var wc = new WebClient()) + { + if (progress != null) // update progress + wc.DownloadProgressChanged += (s, e) => progress.Report(e); + + if (cts != null) // set cancellation token + cts.Token.Register(wc.CancelAsync); + + await wc.DownloadFileTaskAsync(ServerUrl, cached); + } + } + + CopyServer(cached, copyTo); + return true; + } + catch + { + if (cts.IsCancellationRequested) + try { File.Delete(cached); } catch { } + + return false; + } + } + + /// + /// Copies a minecraft_server.jar file to the desired location, performing all the required checks + /// + /// The original server file + /// The destination + public static void CopyServer(string file, string copyTo) + { + //var cache = Path.Combine(cacheFolder, Path.GetFileNameWithoutExtension(file)) + + // "_" + UnixDate.DateTimeToUnixTime(DateTime.Now) + ".jar"; + + if (!Directory.Exists(Path.GetDirectoryName(copyTo))) + Directory.CreateDirectory(Path.GetDirectoryName(copyTo)); + + File.Copy(file, copyTo, true); + } + + #endregion + + #region Cache management + + public static string GetSizeUsedByCache() + { + var sizes = new string[] { "B", "KB", "MB", "GB", "TB" }; + + double size = 0d; + foreach (var file in Directory.EnumerateFiles(cacheFolder)) + size += new FileInfo(file).Length; + + int sizeIndex = 0; + while (size > 1024) + { + ++sizeIndex; + size /= 1024; + } + + return $"{size.ToString("0.##")}{sizes[sizeIndex]}"; + } + + public static void ClearCache() + { + foreach (var file in Directory.EnumerateFiles(cacheFolder)) + try { File.Delete(file); } + catch { } + } + + #endregion + + #region Overrides + + public override string ToString() + { + return $"{ID} {Type}"; + } + + #endregion + } +} diff --git a/Minecraft Server Starter/Classes/MinecraftClasses/MinecraftWorld.cs b/Minecraft Server Starter/Classes/MinecraftClasses/MinecraftWorld.cs new file mode 100644 index 0000000..15365eb --- /dev/null +++ b/Minecraft Server Starter/Classes/MinecraftClasses/MinecraftWorld.cs @@ -0,0 +1,129 @@ +/// +/// Copyright (c) 2016 All Rights Reserved +/// +/// Lonami Exo +/// February 2016 +/// Class used to interop with Minecraft worlds + +using ExtensionMethods; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Threading.Tasks; + +namespace Minecraft_Server_Starter +{ + public class MinecraftWorld + { + /// + /// Full path of the folder containing the Minecraft world + /// + public string FullPath { get; set; } + + /// + /// The name of the Minecraft world + /// + public string Name { get; set; } + + public MinecraftWorld(string path) + { + FullPath = path.TrimEnd(Path.DirectorySeparatorChar); + Name = Path.GetFileName(FullPath); + } + + /// + /// Determines whether a .zip file or a folder is a valid Minecraft world + /// + /// The path to the world + /// True if it's valid + public static bool IsValidWorld(string path) + { + if (string.IsNullOrEmpty(path)) + return false; + + if (Directory.Exists(path)) + { + return File.Exists(Path.Combine(path, "level.dat")); + } + else if (File.Exists(path) && Path.GetExtension(path) + .Equals(".zip", StringComparison.OrdinalIgnoreCase)) + { + return !string.IsNullOrEmpty(findLevelData(path)); + } + + return false; + } + + /// + /// Extracts a .zip file containing a world to the desired base path + /// + /// The world .zip file + /// The base path + /// The world name + public static string ExtractWorldZip(string worldZip, string basePath) + { + string worldName = null; + using (ZipArchive zip = ZipFile.Open(worldZip, ZipArchiveMode.Read)) + { + var worldRoot = Path.GetDirectoryName(findLevelData(worldZip)); + if (string.IsNullOrEmpty(worldRoot)) // level.dat is directly in the root of the .zip >.< + worldName = Path.GetFileNameWithoutExtension(worldZip); + else + worldName = Path.GetFileName(worldRoot); + + basePath = Path.Combine(basePath, worldName); + + // every world file must start by the world root path + "/" + var mustStartWith = string.IsNullOrEmpty(worldRoot) ? string.Empty : + worldRoot.Replace(Path.DirectorySeparatorChar, '/') + "/"; + foreach (var entry in zip.Entries) + if (!entry.FullName.EndsWith("/") && // if it's not dir and it starts with, + entry.FullName.StartsWith(mustStartWith)) + { + // the path is equal to the desired path + the relative path EXCEPT the start + var newpath = string.IsNullOrEmpty(mustStartWith) ? + Path.Combine(basePath, entry.FullName).Replace('/', Path.DirectorySeparatorChar) : + Path.Combine(basePath, entry.FullName.Replace(mustStartWith, string.Empty)) + .Replace('/', Path.DirectorySeparatorChar); + + entry.ExtractToFileSafe(newpath, true); + } + } + return worldName; + } + + static string findLevelData(string zipFile) + { + using (var zip = ZipFile.OpenRead(zipFile)) + foreach (var entry in zip.Entries) + if (entry.Name == "level.dat") + return entry.FullName; + + return null; + } + + /// + /// List all available worlds in a base folder + /// + /// The base folder path containing the worlds + /// All available worlds + public static async Task> ListWorlds(string basePath) + { + var worlds = new List(); + + await Task.Factory.StartNew(() => + { + foreach (var file in Directory.EnumerateFiles(basePath, "level.dat", SearchOption.AllDirectories)) + worlds.Add(new MinecraftWorld(Path.GetDirectoryName(file))); + }); + + return worlds; + } + + public override string ToString() + { + return Name; + } + } +} diff --git a/Minecraft Server Starter/Classes/MinecraftClasses/Server.cs b/Minecraft Server Starter/Classes/MinecraftClasses/Server.cs new file mode 100644 index 0000000..8ecd88a --- /dev/null +++ b/Minecraft Server Starter/Classes/MinecraftClasses/Server.cs @@ -0,0 +1,355 @@ +/// +/// Copyright (c) 2016 All Rights Reserved +/// +/// Lonami Exo +/// February 2016 +/// +/// A class representing a Minecraft server file. +/// This class stores the name, description and .jar location of a Minecraft server +/// + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; + +namespace Minecraft_Server_Starter +{ + [DataContract] + public class Server + { + #region Constant fields + + // the base name for all the Minecraft server jar files + const string jarBaseName = "minecraft_server.jar"; + + // the extension used to save Server files + const string fileExtension = ".sv"; + + // where the servers are saved + static string serversFolder => + Path.Combine(Settings.GetValue("mssFolder"), "Servers"); + + #endregion + + #region Public properties + + /// + /// The server name + /// + [DataMember] + public string Name { get; set; } + /// + /// The server description + /// + [DataMember] + public string Description { get; set; } + /// + /// The server .jar location + /// + [DataMember] + public string ServerJar { get; set; } + + /// + /// The folder containing the server + /// + public string Location => Path.GetDirectoryName(ServerJar); + + /// + /// The server-icon.png path + /// + public string IconPath => Path.Combine(Location, "server-icon.png"); // TODO USE EVERYWHERE + + /// + /// The server.properties path + /// + public string PropertiesPath => Path.Combine(Location, "server.properties"); + + /// + /// The server properties + /// + public ServerProperties Properties + { + get + { + ServerProperties result = null; + + try + { + var propertiesPath = PropertiesPath; + if (File.Exists(propertiesPath)) + result = ServerProperties.FromFile(propertiesPath); + else + File.WriteAllText(propertiesPath, (result = ServerProperties.Empty).Encode()); + } + catch { } + + return result ?? ServerProperties.Empty; + } + set + { + File.WriteAllText(PropertiesPath, value.Encode()); + } + } + + /// + /// The creation date of the server + /// + public DateTime CreationDate { get { return UnixDate.UnixTimeToDateTime(creationDate); } } + /// + /// The date of when the server was last used + /// + public DateTime LastUseDate { get { return UnixDate.UnixTimeToDateTime(lastUseDate); } } + + /// + /// An unique ID identifying this server + /// + public string ID { get { return creationDate.ToString(); } } + + #endregion + + #region Private fields + + // creation and last use date (in unix time) + [DataMember] + long creationDate; + [DataMember] + long lastUseDate; + + string serverFileLoc + => Path.Combine(serversFolder, ID + fileExtension); + + #endregion + + #region Constructors + + /// + /// Create a new instance of a Server + /// + public Server() + { + creationDate = lastUseDate = UnixDate.DateTimeToUnixTime(DateTime.Now); + } + + /// + /// Creates a new instance of a Server given a name and a description. The minecraft_server.jar location is set automatically + /// + /// The name of the server + /// The description of the server + public Server(string name, string description) : this() + { + Name = name; + Description = description; + ServerJar = Path.Combine(serversFolder, ID, jarBaseName); + } + + /// + /// Creates a new instance of a Server given a name, a description and a minecraft_server.jar file + /// + /// The name of the server + /// The description of the server + /// The minecraft_server.jar location + public Server(string name, string description, string jar) : this(name, description) + { + ServerJar = jar; + } + + /// + /// Create a new instance of a Server given an encoded file + /// + /// The encoded Server file + /// The decoded Server + public static Server FromFile(string encodedFile) + { + return Decode(File.ReadAllText(encodedFile, Encoding.UTF8)); + } + + /// + /// Create a new instance of a Server given an encoded file + /// + /// The encoded Server file + /// The decoded Server + public static Server FromServerID(string serverId) + { + var file = Path.Combine(serversFolder, serverId + fileExtension); + if (File.Exists(file)) + return Server.FromFile(file); + + return null; + } + + #endregion + + #region Use and delete + + /// + /// Use this server, updating it's last use date + /// + public void Use() + { + lastUseDate = UnixDate.DateTimeToUnixTime(DateTime.Now); + Save(); + } + + /// + /// Delete this server whilst keeping all the other files + /// + public bool Delete() + { + try + { + File.Delete(serverFileLoc); + return true; + } + catch { return false; } + } + + /// + /// Delete all the files related to this folder + /// + public bool DeleteAll() + { + if (!Delete()) + return false; + + try + { + Directory.Delete(Location, true); + return true; + } + catch { return false; } + } + + #endregion + + #region Saving + + /// + /// Save the server info + /// + public void Save() + { + if (!Directory.Exists(Path.GetDirectoryName(serverFileLoc))) + Directory.CreateDirectory(Path.GetDirectoryName(serverFileLoc)); + + File.WriteAllText(serverFileLoc, Encode(), Encoding.UTF8); + } + + #endregion + + #region Server properties + + /// + /// Updates a server property in the server.properties file + /// + /// The property name + /// The property value + public void UpdateServersProperty(string name, string value) + { + var properties = Properties; + properties[name].Value = value; + File.WriteAllText(PropertiesPath, properties.Encode()); + } + + #endregion + + #region Servers listing + + /// + /// Returns a list of all the saved servers + /// + /// The saved servers + public static List GetServers() + { + if (!Directory.Exists(serversFolder)) + { + Directory.CreateDirectory(serversFolder); + return new List(); + } + var files = Directory.GetFiles(serversFolder, "*" + fileExtension); + var servers = new List(files.Length); + foreach (var file in files) + servers.Add(FromFile(file)); + + return servers.OrderByDescending(sv => sv.LastUseDate).ToList(); + } + + #endregion + + #region Encode and decode + + // TOD MAYBE I CAN TO FILE DIRECTLY OR STH + /// + /// Encode the current server and return an encoded string of it + /// + /// The encoded server string + public string Encode() + { + return Serializer.SerializeToString(this); + } + + /// + /// Decode the current server from it's encoded string + /// + /// The encoded server string + /// The decoded Server + public static Server Decode(string str) + { + return Serializer.DeserializeFromString(str); + } + + #endregion + + #region Overrides + + /// + /// Checks equality against another object + /// + /// The object against which the server will be checked + /// True if they're equal + public override bool Equals(object obj) + { + if (!(obj is Server)) return false; + return this == (Server)obj; + } + + /// + /// Get the hash code of the server + /// + /// The hash code of the server + public override int GetHashCode() + { + return creationDate.GetHashCode(); + } + + /// + /// Checks equality against two servers + /// + /// First server + /// Second server + /// True if they're equal + public static bool operator ==(Server a, Server b) + { + if (ReferenceEquals(a, b)) return true; + + if (((object)a == null) || ((object)b == null)) return false; + + return a.creationDate == b.creationDate; + } + + /// + /// Checks non equality against two servers + /// + /// First server + /// Second server + /// True if they're not equal + public static bool operator !=(Server a, Server b) + { + return !(a == b); + } + + #endregion + } +} diff --git a/Minecraft Server Starter/Classes/MinecraftClasses/ServerJar.cs b/Minecraft Server Starter/Classes/MinecraftClasses/ServerJar.cs new file mode 100644 index 0000000..5e46ff0 --- /dev/null +++ b/Minecraft Server Starter/Classes/MinecraftClasses/ServerJar.cs @@ -0,0 +1,101 @@ +/// +/// Copyright (c) 2016 All Rights Reserved +/// +/// Lonami Exo +/// 01/04/2016 +/// Class used to determine a Minecraft server jar version + +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading.Tasks; + +namespace Minecraft_Server_Starter +{ + public static class ServerJar + { + /// + /// Determines the minecraft_server.jar version. Returns null if the version could not be determined + /// + /// The minecraft_server.jar location + /// The version of the server + public static async Task GetServerVersion(string serverJar) + { + string version = null; + + if (!File.Exists(serverJar)) + return "Unexisting"; + + await Task.Run(() => + { + using (var zip = ZipFile.Open(serverJar, ZipArchiveMode.Read)) + { + foreach (var entry in zip.Entries) + { + // skip these files, as they don't contain the version string + if (entry.Name.Contains('$') || !entry.Name.EndsWith(".class")) + continue; + + // open the compressed stream + using (var stream = entry.Open()) + { + // find this string in the stream + var idx = (int)indexOfInStream(stream, "Starting minecraft server version "); + if (idx < 0) + continue; + + using (var sr = new StreamReader(stream)) + { + // read the following characters to get the actual version characters + var buffer = new char[32]; + sr.Read(buffer, 0, buffer.Length); + + int i = 0; + for (; i < buffer.Length; i++) + { + // if character is less than the first readable character (space), stop + if (buffer[i] < 32) + break; + } + + // return a string with the version + version = new string(buffer, 0, i); + } + } + + if (!string.IsNullOrEmpty(version)) + break; + } + } + }); + + return version ?? "Unknown"; + } + + // keep in mind this moves the stream position + static long indexOfInStream(Stream stream, string value) + { + // start here (since we want the start of the string, instead of substracting it after we can do it now) + int index = -value.Length; + for (int i = 0; i < value.Length;) + { + // try reading the next byte + var b = stream.ReadByte(); + ++index; + + if (b < 0) // end of stream + return -1; + + // convert the byte to a character + var c = (char)b; + + if (c == value[i]) // if the character equals to the next character in the string to search for + ++i; // increment i to match the next character + else // else, start again + i = 0; + } + + return index; + } + } +} diff --git a/Minecraft Server Starter/Classes/MinecraftClasses/ServerProperties.cs b/Minecraft Server Starter/Classes/MinecraftClasses/ServerProperties.cs new file mode 100644 index 0000000..6cf8d39 --- /dev/null +++ b/Minecraft Server Starter/Classes/MinecraftClasses/ServerProperties.cs @@ -0,0 +1,266 @@ +/// +/// Copyright (c) 2016 All Rights Reserved +/// +/// Lonami Exo +/// February 2016 +/// Class representing a server.properties file + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Minecraft_Server_Starter +{ + public class ServerProperties + { + #region Indexer + + /// + /// Gets or sets the server property with a given a name + /// + /// The name of the property + /// The server property with that name + public ServerProperty this[string name] + { + get + { + return Properties.FirstOrDefault(p => p.MatchsName(name)) + ?? new ServerProperty(name, string.Empty); + } + set + { + bool done = false; + for (int i = 0; i < Properties.Count; i++) + { + if (Properties[i].MatchsName(name)) + { + Properties[i] = value; + break; + } + } + if (!done) + { + Properties.Add(value); + } + } + } + + #endregion + + #region Constant fields + + // the default name of the server.properties file + public const string PropertiesName = "server.properties"; + + // the header of the server.properties + const string header = "#Minecraft server properties"; + // a base template of the server.properties + const string baseTemplate = @"#Minecraft server properties +#Day Mon dd hh:mm:ss CET year +allow-flight=false +allow-nether=true +announce-player-achievements=true +broadcast-console-to-ops=true +difficulty=1 +enable-command-block=false +enable-query=false +enable-rcon=false +force-gamemode=false +gamemode=0 +generate-structures=true +generator-settings= +hardcore=false +level-name=world +level-seed= +level-type=DEFAULT +max-build-height=256 +max-players=20 +max-tick-time=60000 +max-world-size=29999984 +motd=A Minecraft Server +network-compression-threshold=256 +online-mode=true +op-permission-level=4 +player-idle-timeout=0 +pvp=true +resource-pack-hash= +resource-pack-sha1= +resource-pack= +server-ip= +server-port=25565 +snooper-enabled=true +spawn-animals=true +spawn-monsters=true +spawn-npcs=true +view-distance=10 +white-list=false +"; + + #endregion + + #region Public fields + + /// + /// Properties in the server.properties + /// + public List Properties = new List(); + + #endregion + + #region Constructors + + // create a new instance + ServerProperties() { } + + /// + /// Gets a default (empty) server properties + /// + public static ServerProperties Empty => FromString(baseTemplate); + + /// + /// Gets a server properties from a given file + /// + /// The server.properties file + /// The server properties representing the file + public static ServerProperties FromFile(string file) + { + return FromString(File.ReadAllText(file)); + } + + /// + /// Gets a server properties from a given server.properties string + /// + /// The server.properties contents + /// The server properties representing the contents of the server.properties file + public static ServerProperties FromString(string str) + { + var svProperties = new ServerProperties(); + + var lines = str.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + if (line.StartsWith("#") || !line.Contains("=")) + continue; // ignore comment + + var spl = line.Split(new char[] { '=' }, StringSplitOptions.None); + if (spl.Length < 2) continue; // ignore invalid line + + // add property + svProperties.Properties.Add(new ServerProperty(spl[0], spl[1])); + } + + return svProperties; + } + + #endregion + + #region Defaults + + public static bool IsMultiChoice(string propertyName) + { + switch (propertyName) + { + case "difficulty": + case "gamemode": + return true; + + default: + return false; + } + } + + public static bool IsInteger(string propertyName) + { + switch (propertyName) + { + case "max-build-height": + case "max-players": + case "max-tick-time": + case "max-world-size": + case "network-compression-threshold": + case "op-permission-level": + case "player-idle-timeout": + case "server-port": + case "view-distance": + return true; + + default: + return false; + } + } + + public static bool IsTrueFalse(string propertyName) + { + switch (propertyName) + { + case "allow-flight": + case "allow-nether": + case "announce-player-achievements": + case "broadcast-console-to-ops": + case "enable-command-block": + case "enable-query": + case "enable-rcon": + case "force-gamemode": + case "generate-structures": + case "hardcore": + case "online-mode": + case "pvp": + case "snooper-enabled": + case "spawn-animals": + case "spawn-monsters": + case "spawn-npcs": + case "white-list": + return true; + + default: + return false; + } + } + + public static string[] GetMultiChoices(string propertyName) + { + switch (propertyName) + { + case "difficulty": + return new string[] { "Peaceful", "Easy", "Normal", "Hard" }; + + case "gamemode": + return new string[] { "Survival", "Creative", "Adventure", "Spectator" }; + + default: + throw new InvalidOperationException("There are no multiple choices for this property"); + } + } + + #endregion + + #region Encode + + /// + /// Encodes the current properties as a server.properties file + /// + /// The encoded value + public string Encode() + { + var sb = new StringBuilder(); + + // append header and date + sb.AppendLine(header); + sb.Append("#"); + sb.AppendLine(UnixDate.GetUniversalDate()); + + // append every property + foreach (var property in Properties) + sb.AppendLine(property.ToString()); + + // extra line + sb.AppendLine(); + + return sb.ToString(); + } + + #endregion + } +} diff --git a/Minecraft Server Starter/Classes/MinecraftClasses/ServerProperty.cs b/Minecraft Server Starter/Classes/MinecraftClasses/ServerProperty.cs new file mode 100644 index 0000000..5442f1b --- /dev/null +++ b/Minecraft Server Starter/Classes/MinecraftClasses/ServerProperty.cs @@ -0,0 +1,243 @@ + +using ExtensionMethods; +/// +/// Copyright (c) 2016 All Rights Reserved +/// +/// Lonami Exo +/// February 2016 +/// Class representing a server.properties property +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace Minecraft_Server_Starter +{ + public class ServerProperty + { + #region Properties + + /// + /// The original name of the property + /// + public string OriginalName { get; set; } + + /// + /// The name of the property + /// + public string Name { get; set; } + + /// + /// The value of the property + /// + public string Value { get; set; } + + /// + /// The description, if available, of the property + /// + public string Description + { + get + { + string description; + if (!descriptions.TryGetValue(OriginalName, out description)) + description = Res.GetStr("noDescriptionAvailable"); + + return description; + } + } + + /// + /// Is the property a boolean value? + /// + public bool IsBoolean => Value == "true" || Value == "false"; + + #endregion + + #region Constructor + + /// + /// Initialize a new instance of this class + /// + /// The original name of the property + /// The default value of the property + public ServerProperty(string oriName, string value) + { + OriginalName = oriName; + + // "beautify" the name + Name = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(oriName.Replace("-", " ").Replace("_", " ")); + Value = value; + } + + #endregion + + #region Comparing + + /// + /// Does the given value match with the name of the property? + /// + public bool MatchsName(string value) + { + return Name.Equals(value, StringComparison.OrdinalIgnoreCase) || + OriginalName.Equals(value, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Determines whether the property passes a search or not + /// + public bool PassesSearch(string search) + { + if (string.IsNullOrEmpty(search)) + return true; + + return + Name.Contains(search, StringComparison.OrdinalIgnoreCase) || + OriginalName.Contains(search, StringComparison.OrdinalIgnoreCase); + } + + #endregion + + #region Constant fields + + // took from http://Minecraft.gamepedia.com/Server.properties + // TODO and from http://Minecraft-es.gamepedia.com/Server.properties + static readonly Dictionary descriptions = + new Dictionary + { + // allow-flight + { "allow-flight", Res.GetStr("descAllowFlight") }, + + // allow-nether + { "allow-nether", Res.GetStr("descAllowNether") }, + + //announce-player-achievements + { "announce-player-achievements", Res.GetStr("descAnnounceAchievements") }, + + //difficulty + { "difficulty", Res.GetStr("descDifficulty") }, + + //enable-query + { "enable-query", Res.GetStr("descEnableQuery") }, + + //enable-rcon + { "enable-rcon", Res.GetStr("descEnableRcon") }, + + //enable-command-block + { "enable-command-block", Res.GetStr("descEnableCommandBlock") }, + + //force-gamemode + { "force-gamemode", Res.GetStr("descForceGamemode") }, + + //gamemode + { "gamemode", Res.GetStr("descGamemode") }, + + //generate-structures + { "generate-structures", Res.GetStr("descGenerateStructures") }, + + //generator-settings + { "generator-settings", Res.GetStr("descGeneratorSettings") }, + + //hardcore + { "hardcore", Res.GetStr("descHardcore") }, + + //level-name + { "level-name", Res.GetStr("descLevelName") }, + + //level-seed + { "level-seed", Res.GetStr("descLevelSeed") }, + + //level-type + { "level-type", Res.GetStr("descLevelType") }, + + //max-build-height + { "max-build-height", Res.GetStr("descMaxBuildHeight") }, + + //max-players + { "max-players", Res.GetStr("descMaxPlayers") }, + + //max-tick-time + { "max-tick-time", Res.GetStr("descMaxTickTime") }, + + //max-world-size + { "max-world-size", Res.GetStr("descMaxWorldSize") }, + + //motd + { "motd", Res.GetStr("descMotd") }, + + //network-compression-threshold + { "network-compression-threshold", Res.GetStr("descNetworkCompression") }, + + //online-mode + { "online-mode", Res.GetStr("descOnlineMode") }, + + //op-permission-level + { "op-permission-level", Res.GetStr("descOpPermissionLevel") }, + + //player-idle-timeout + { "player-idle-timeout", Res.GetStr("descPlayerIdleTimeout") }, + + //pvp + { "pvp", Res.GetStr("descPvp") }, + + //query.port + { "query.port", Res.GetStr("descQueryPort") }, + + //rcon.password + { "rcon.password", Res.GetStr("descRconPassword") }, + + //rcon.port + { "rcon.port", Res.GetStr("descRconPort") }, + + //resource-pack + { "resource-pack", Res.GetStr("descResourcePack") }, + + //resource-pack-hash + { "resource-pack-hash", Res.GetStr("descResourcePackHash") }, + + //server-ip + { "server-ip", Res.GetStr("descServerIp") }, + + //server-port + { "server-port", Res.GetStr("descServerPort") }, + + //snooper-enabled + { "snooper-enabled", Res.GetStr("descSnooper") }, + + //spawn-animals + { "spawn-animals", Res.GetStr("descSpawnAnimals") }, + + //spawn-monsters + { "spawn-monsters", Res.GetStr("descSpawnMonsters") }, + + //spawn-npcs + { "spawn-npcs", Res.GetStr("descSpawnNpcs") }, + + //spawn-protection + { "spawn-protection", Res.GetStr("descSpawnProtection") }, + + //use-native-transport + { "use-native-transport", Res.GetStr("descUseNativeSupport") }, + + //view-distance + { "view-distance", Res.GetStr("descViewDistance") }, + + //white-list + { "white-list", Res.GetStr("descWhiteList") } }; + + #endregion + + #region Overrides + + /// + /// Encode the property ready to be saved in a server.properties file + /// + /// The encoded value + public override string ToString() + { + return OriginalName + "=" + Value; + } + + #endregion + + } +} diff --git a/Minecraft Server Starter/Classes/MinecraftClasses/SkinDownloader.cs b/Minecraft Server Starter/Classes/MinecraftClasses/SkinDownloader.cs new file mode 100644 index 0000000..29c307e --- /dev/null +++ b/Minecraft Server Starter/Classes/MinecraftClasses/SkinDownloader.cs @@ -0,0 +1,83 @@ +/// +/// Copyright (c) 2016 All Rights Reserved +/// +/// Lonami Exo +/// February 2016 +/// Class used to download Minecraft skins given an username + +using System.IO; +using System.Net; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media.Imaging; + +namespace Minecraft_Server_Starter +{ + static class SkinDownloader + { + #region Constant fields + + const string baseUrl = "http://skins.Minecraft.net/MinecraftSkins/{0}.png"; + + #endregion + + #region Public methods + + /// + /// Returns the head of the specified username skin + /// + /// The username to chop their head off + /// Their chopped head + public static async Task GetSkinHead(string username) + { + var skin = await GetSkin(username); + if (skin == null) return null; + + // head_location(8, 8); head_size(8, 8) + return new CroppedBitmap(skin, new Int32Rect(8, 8, 8, 8)); + } + + /// + /// Returns the skin of the specified username + /// + /// The username you wish to extract the skin from + /// Their skin + public static async Task GetSkin(string username) + { + using (var wc = new WebClient()) + try { return loadImage(await wc.DownloadDataTaskAsync(getUrl(username))); } + catch (WebException) { return null; } // probably 404 + } + + #endregion + + #region Private methods + + // username -> url + static string getUrl(string username) + { + return string.Format(baseUrl, username); + } + + // byte[] -> BitmapImage + static BitmapImage loadImage(byte[] imageData) + { + if (imageData == null || imageData.Length == 0) return null; + var image = new BitmapImage(); + using (var mem = new MemoryStream(imageData)) + { + mem.Position = 0; + image.BeginInit(); + image.CreateOptions = BitmapCreateOptions.PreservePixelFormat; + image.CacheOption = BitmapCacheOption.OnLoad; + image.UriSource = null; + image.StreamSource = mem; + image.EndInit(); + } + image.Freeze(); + return image; + } + + #endregion + } +} \ No newline at end of file diff --git a/Minecraft Server Starter/Classes/Utils/BackupSort.cs b/Minecraft Server Starter/Classes/Utils/BackupSort.cs new file mode 100644 index 0000000..bcdc67d --- /dev/null +++ b/Minecraft Server Starter/Classes/Utils/BackupSort.cs @@ -0,0 +1,45 @@ +/// +/// Copyright (c) 2016 All Rights Reserved +/// +/// Lonami Exo +/// February 2016 +/// Class used to sort backups + +using System.Collections.Generic; + +namespace Minecraft_Server_Starter +{ + class BackupSort : IComparer + { + string priorityName; + + public BackupSort(string priorityName = null) + { + this.priorityName = priorityName; + } + + public int Compare(Backup x, Backup y) + { + // if a priority name exists + if (!string.IsNullOrEmpty(priorityName)) + { + // and they have different names + if (x.Server.Name != y.Server.Name) + { + // and one is equal to the priority name, return that first + if (x.Server.Name.Equals(priorityName)) + return -1; + + if (y.Server.Name.Equals(priorityName)) + return +1; + } + } + + // return the most recent one + if (x.CreationDate > y.CreationDate) + return -1; + else + return +1; + } + } +} diff --git a/Minecraft Server Starter/Classes/Utils/ExtensionMethods.cs b/Minecraft Server Starter/Classes/Utils/ExtensionMethods.cs new file mode 100644 index 0000000..c266800 --- /dev/null +++ b/Minecraft Server Starter/Classes/Utils/ExtensionMethods.cs @@ -0,0 +1,196 @@ +/// +/// Copyright (c) 2016 All Rights Reserved +/// +/// Lonami Exo +/// February 2016 +/// Some extension methods + +using System; +using System.IO; +using System.IO.Compression; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Animation; +using System.Windows.Media.Imaging; + +namespace ExtensionMethods +{ + public static class Images + { + /// + /// Resizes a bitmap to the new desired size + /// + /// Bitmap to resize + /// New bitmap width + /// New bitmap height + /// The resized bitmap + public static BitmapSource Resize(this BitmapSource bitmapSource, double newWidth, double newHeight) + { + if (bitmapSource == null) return null; + + return new TransformedBitmap(bitmapSource, new ScaleTransform( + newWidth / bitmapSource.PixelWidth, newHeight / bitmapSource.PixelHeight)); + } + + /// + /// Saves a BitmapSource to disk + /// + /// Bitmap to save + /// Location where the bitmap wil be saved + public static bool Save(this BitmapSource bitmapSource, string fileName) + { + if (string.IsNullOrEmpty(fileName) || bitmapSource == null) + return false; + + var encoder = new PngBitmapEncoder(); + encoder.Frames.Add(BitmapFrame.Create(bitmapSource)); + try + { + if (!Directory.Exists(Path.GetDirectoryName(fileName))) + Directory.CreateDirectory(Path.GetDirectoryName(fileName)); + + using (var filestream = new FileStream(fileName, FileMode.Create)) + encoder.Save(filestream); + } + catch { return false; } + return true; + } + } + + public static class Zips + { + public static void AddDirectory(this ZipArchive zip, string dir, string basePath = null) + { + var zipPath = string.IsNullOrEmpty(basePath) ? + Path.GetFileName(dir) : + basePath + "/" + Path.GetFileName(dir); + + foreach (var subFile in Directory.GetFiles(dir)) + zip.AddFile(subFile, zipPath); + + foreach (var subDir in Directory.GetDirectories(dir)) + zip.AddDirectory(subDir, zipPath); + } + + public static void AddFile(this ZipArchive zip, string file, string basePath = null) + { + var zipPath = string.IsNullOrEmpty(basePath) ? + Path.GetFileName(file) : + basePath + "/" + Path.GetFileName(file); + + zip.CreateEntryFromFile(file, zipPath); + } + + public static bool ExtractToDirectory(this ZipArchive zip, string folder, bool overwrite) + { + try + { + foreach (var entry in zip.Entries) + entry.ExtractToFileSafe(Path.Combine(folder, entry.FullName), overwrite); + + return true; + } + catch { return false; } + } + + public static bool ExtractToFileSafe(this ZipArchiveEntry entry, string file, bool overwrite) + { + try + { + var dir = Path.GetDirectoryName(file); + if (!Directory.Exists(Path.GetDirectoryName(file))) + Directory.CreateDirectory(Path.GetDirectoryName(file)); + + entry.ExtractToFile(file, overwrite); + return true; + } + catch { return false; } + } + } + + public static class Longs + { + static readonly string[] fileSizes = new string[] { + "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" + }; + + public static string ToFileSizeString(this long value) + { + double newValue = value; + int fsIndex = 0; // file size index + + while (newValue > 1024d) + { + ++fsIndex; + newValue /= 1024d; + } + + return newValue.ToString("0.##") + " " + fileSizes[fsIndex]; + } + } + + public static class Directories + { + public static void CopyDirectory(this DirectoryInfo src, DirectoryInfo dst) + { + if (!Directory.Exists(dst.FullName)) + Directory.CreateDirectory(dst.FullName); + + foreach (FileInfo fi in src.GetFiles()) + fi.CopyTo(Path.Combine(dst.FullName, fi.Name), true); + + foreach (DirectoryInfo diSourceSubDir in src.GetDirectories()) + diSourceSubDir.CopyDirectory(dst.CreateSubdirectory(diSourceSubDir.Name)); + } + } + + public static class Uris + { + public static BitmapSource LoadImage(this Uri uri) + { + var image = new BitmapImage(); + using (FileStream stream = File.OpenRead(uri.LocalPath)) + { + image.BeginInit(); + image.StreamSource = stream; + image.CacheOption = BitmapCacheOption.OnLoad; + image.EndInit(); // load the image from the stream + } // close the stream + return image; + } + + public static BitmapSource LoadImage(this Uri uri, double width, double height) + => uri.LoadImage().Resize(width, height); + } + + public static class Animations + { + #region Fade + + public static void Fade(this UIElement element, bool? fadeIn, double duration = 500) + { + if (!fadeIn.HasValue) // if null, fade in will be true if opacity is 0 + fadeIn = element.Opacity == 0; + + var anim = new DoubleAnimation() + { + From = fadeIn.Value ? 0 : 1, + To = fadeIn.Value ? 1 : 0, + Duration = new Duration(TimeSpan.FromMilliseconds(duration)), + EasingFunction = new CubicEase() + }; + + element.BeginAnimation(UIElement.OpacityProperty, anim); + } + + #endregion + } + + public static class Strings + { + public static bool Contains(this string str, string value, StringComparison comparision) + { + return str.IndexOf(value, comparision) > -1; + } + } +} diff --git a/Minecraft Server Starter/Classes/Utils/Java.cs b/Minecraft Server Starter/Classes/Utils/Java.cs new file mode 100644 index 0000000..1cf0800 --- /dev/null +++ b/Minecraft Server Starter/Classes/Utils/Java.cs @@ -0,0 +1,68 @@ +/// +/// Copyright (c) 2016 All Rights Reserved +/// +/// Lonami Exo +/// February 2016 +/// Class used to interop with Java + +using System; +using System.IO; + +namespace Minecraft_Server_Starter +{ + public static class Java + { + // returns null if the path wasn't found + public static string FindJavaPath() + { + var path = GetFullPath("java.exe"); + if (!string.IsNullOrEmpty(path)) + return path; + + var javaFolder = Path.Combine(Environment.GetFolderPath + (Environment.SpecialFolder.ProgramFiles), "Java"); + + if (!Directory.Exists(javaFolder)) + return string.Empty; + + string greatestDir = null; + Version greatestVersion = null; + foreach (var dir in Directory.EnumerateDirectories(javaFolder)) + { + if (dir.StartsWith("jre")) + { + var version = new Version(dir.Substring(3).Replace('_', '.')); + if (greatestVersion == null || version > greatestVersion) + { + greatestDir = dir; + greatestVersion = version; + } + } + } + if (string.IsNullOrEmpty(greatestDir)) return string.Empty; + + var bin = Path.Combine(javaFolder, greatestDir, "bin"); + if (!Directory.Exists(bin)) return string.Empty; + + var java = Path.Combine(bin, "java.exe"); + if (!File.Exists(java)) return string.Empty; + + return java; + } + + static string GetFullPath(string fileName) + { + if (File.Exists(fileName)) + return Path.GetFullPath(fileName); + + var values = Environment.GetEnvironmentVariable("PATH"); + foreach (var path in values.Split(';')) + { + var fullPath = Path.Combine(path, fileName); + if (File.Exists(fullPath)) + return fullPath; + } + return null; + } + } +} diff --git a/Minecraft Server Starter/Classes/Utils/PortManager.cs b/Minecraft Server Starter/Classes/Utils/PortManager.cs new file mode 100644 index 0000000..e342c77 --- /dev/null +++ b/Minecraft Server Starter/Classes/Utils/PortManager.cs @@ -0,0 +1,74 @@ +/// +/// Copyright (c) 2016 All Rights Reserved +/// +/// Lonami Exo +/// February 2016 +/// Class used to manage ports + +using Open.Nat; +using System.Collections.Generic; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace Minecraft_Server_Starter +{ + // thanks to http://www.codeproject.com/Articles/807861/Open-NAT-A-NAT-Traversal-library-for-NET-and-Mono + public static class PortManager + { + static NatDevice device; + + public static async Task Init() + { + if (device != null) return true; + + var nat = new NatDiscoverer(); + var cts = new CancellationTokenSource(5000); + try + { + device = await nat.DiscoverDeviceAsync(PortMapper.Upnp, cts); + return true; + } + catch + { + try + { + device = await nat.DiscoverDeviceAsync(PortMapper.Pmp, cts); + return true; + } + catch { } + } + + return false; + } + + public static async Task GetExternalIP() + { + return await device.GetExternalIPAsync(); + } + + public static async Task OpenPort(ushort port) + { + await device.CreatePortMapAsync(new Mapping(Protocol.Tcp, port, port, "Minecraft Server Starter")); + } + + + + //var device = await nat.DiscoverDeviceAsync(PortMapper.Upnp, cts); + + //await device.CreatePortMapAsync(new Mapping(Protocol.Tcp, 1600, 1700, "Open.Nat (temporary)")); + //await device.CreatePortMapAsync(new Mapping(Protocol.Tcp, 1601, 1701, "Open.Nat (Session lifetime)")); + //await device.CreatePortMapAsync(new Mapping(Protocol.Tcp, 1602, 1702, 0, "Open.Nat (Permanent lifetime)")); + //await device.CreatePortMapAsync(new Mapping(Protocol.Tcp, 1603, 1703, 20, "Open.Nat (Manual lifetime)")); + + public static async Task ClosePort(ushort port) + { + await device.DeletePortMapAsync(new Mapping(Protocol.Tcp, port, port, "Minecraft Server Starter")); + } + + public static async Task> GetOpenPorts() + { + return await device.GetAllMappingsAsync(); + } + } +} diff --git a/Minecraft Server Starter/Classes/Utils/Serializer.cs b/Minecraft Server Starter/Classes/Utils/Serializer.cs new file mode 100644 index 0000000..43093b9 --- /dev/null +++ b/Minecraft Server Starter/Classes/Utils/Serializer.cs @@ -0,0 +1,97 @@ +// Made by Lonami Exo (C) LonamiWebs +// Creation date: february 2016 +// Modifications: +// - 21-03-2016. Added methods to manage files +using System.IO; +using System.Runtime.Serialization; +using System.Text; +using System.Xml; + +/* Requires System.Runtime.Serialization.dll +By default, any class will serialize it's public properties. +If you want to explicitely specify what will be serialized, see below example: + +[DataContract] +class MyClass +{ + // will serialize + [DataMember] + ; + + [DataMember] + { get {...} set {...} } + + [XmlElement(ElementName = "")] + + + // will not serialize + ; + { get {...} set {...} } +} +*/ + +public static class Serializer +{ + /// + /// Determines whether the generated XML should be indented or not + /// + public static bool Indent = true; + + /// + /// Serializes a given object into the desired file + /// + /// The type of the object to serialize + /// The object to serialize + /// The file where the object will be serialized + public static void Serialize(T obj, string file) => + File.WriteAllText(file, SerializeToString(obj), Encoding.UTF8); + + /// + /// Serializes a given object into a string + /// + /// The type of the object to serialize + /// The object to serialize + /// The serialized object + public static string SerializeToString(T obj) + { + var settings = new XmlWriterSettings { Indent = Indent }; + + using (var ms = new MemoryStream()) + using (var sr = new StreamReader(ms)) + { + var serializer = new DataContractSerializer(typeof(T)); + using (var writer = XmlWriter.Create(ms, settings)) + serializer.WriteObject(writer, obj); + + ms.Position = 0; + return sr.ReadToEnd(); + } + } + + /// + /// Deserializes an object from the given file + /// + /// The type of the object to deserialize + /// The file from where the object will be deserialized + /// The deserialized object + public static T Deserialize(string file) => + DeserializeFromString(File.ReadAllText(file, Encoding.UTF8)); + + /// + /// Deserializes an object from a given XML string + /// + /// The type of the object to deserialize + /// The serialized object + /// The deserialized object + public static T DeserializeFromString(string xml) + { + using (var ms = new MemoryStream()) + { + byte[] data = Encoding.UTF8.GetBytes(xml); + ms.Write(data, 0, data.Length); + ms.Position = 0; + var deserializer = new DataContractSerializer(typeof(T)); + return (T)deserializer.ReadObject(ms); + } + } +} diff --git a/Minecraft Server Starter/Classes/Utils/Settings.cs b/Minecraft Server Starter/Classes/Utils/Settings.cs new file mode 100644 index 0000000..048d3bf --- /dev/null +++ b/Minecraft Server Starter/Classes/Utils/Settings.cs @@ -0,0 +1,200 @@ +/** Made by Lonami Exo +* (C) LonamiWebs 2015 +* Created: 24/05/2015 +* LastMod: 03/06/2015 +* Requires: Serializer.cs +*/ +using System; +using System.Collections.Generic; +using System.IO; + +/// +/// An improved (both on performance and error handling) Settings class +/// +public static class Settings +{ + #region Private variables and consts + + // The application name. It can be "Domain\\Name" + static string appName; + // Where the settings file is located + static string settingsFile; + + // The stored values for the current settings + static Dictionary values = new Dictionary(); + // The default values to be used + static Dictionary defaultValues; + + // Has been Settings initialized? + static bool initialized; + + // Error messages + const string errorNotInitialized = "You must call Settings.Init() before using any other method call"; + + #endregion + + #region Public variables and consts + + /// + /// Should the settings be saved automatically? + /// + public static bool Autosave { get; set; } = true; + + /// + /// Determines whether the .SetValue(...) should be disabled or not. + /// Useful when loading a settings window + /// + public static bool SetSettingDisabled { get; set; } = false; + + /// + /// Retrieves the application folder + /// + public static string ApplicationFolder => Path.GetDirectoryName(settingsFile); + + #endregion + + #region Initialize + + /// + /// Initializes the Settings instance. This must be called once + /// + /// The name of the current application. You may use "Domain\\AppName" + public static void Init(string appName) + { Init(appName, new Dictionary()); } + + /// + /// Initializes the Settings instance. This must be called once + /// + /// The name of the current application. You may use "Domain\\AppName" + /// The value pairs for the default values if they're not found or corrupted + public static void Init(string appName, Dictionary defaultValues) + { + if (initialized) + return; + initialized = true; + + Settings.appName = appName; + Settings.defaultValues = defaultValues; + + var dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), Settings.appName); + settingsFile = Path.Combine(dir, "settings"); + + if (!Directory.Exists(dir)) + Directory.CreateDirectory(dir); + + load(false); + } + + #endregion + + #region Load save and reset + + /// + /// Loads the settings. Default values will be used if they're corrupted, and the method will return false. + /// This is automatically called once after calling Settings.Init() + /// + /// False if the settings file was corrupted + public static bool Load() => load(false); + + /// + /// Saves the settings. + /// This is automatically called if Autosave is enabled + /// + public static void Save() + { + checkInit(); + Serializer.Serialize(values, settingsFile); + } + + /// + /// Resets the current settings to it's default values, and saves them if Autosave is true + /// + public static void Reset() + { + checkInit(); + + load(true); + if (Autosave) + File.Delete(settingsFile); + } + + #endregion + + #region Get and set value + + /// + /// Gets a value for the given setting name. If it doesn't exist, an exception will be thrown + /// + /// The name of the setting + /// The value of the setting + public static T GetValue(string settingName) => (T)values[settingName]; + + /// + /// Tries to get a value for the given setting name. No exception will be thrown if the setting doesn't exist + /// + /// The name of the setting + /// Where the result value will be stored + /// True if the setting exists + public static bool TryGetValue(string settingName, out T value) + { + object result; + if (values.TryGetValue(settingName, out result)) + { + value = (T)result; + return true; + } + value = default(T); + return false; + } + + /// + /// Sets a value for the given setting name + /// + /// The name of the setting + /// The value of the setting + public static void SetValue(string settingName, T value) + { + if (SetSettingDisabled) + return; + + values[settingName] = value; + if (Autosave) + Save(); + } + + #endregion + + #region Private methods + + // Load + static bool load(bool onlyDefault) + { + checkInit(); + + values = new Dictionary(defaultValues); + if (onlyDefault || !File.Exists(settingsFile)) + return true; + + try + { + var result = Serializer.Deserialize>(settingsFile); + + // don't set the dictionary directly, as there might be some + // default values which don't exist in the loaded settings file + foreach (var kvp in result) + values[kvp.Key] = kvp.Value; + + return true; + } + catch { return false; } + } + + // Checks + static void checkInit() + { + if (!initialized) + throw new NullReferenceException(errorNotInitialized); + } + + #endregion +} diff --git a/Minecraft Server Starter/Classes/Utils/UnixDate.cs b/Minecraft Server Starter/Classes/Utils/UnixDate.cs new file mode 100644 index 0000000..ef98183 --- /dev/null +++ b/Minecraft Server Starter/Classes/Utils/UnixDate.cs @@ -0,0 +1,36 @@ +/// +/// Copyright (c) 2016 All Rights Reserved +/// +/// Lonami Exo +/// February 2016 +/// Class used to convert between DateTime to unix time and back + +using System; +using System.Globalization; + +namespace Minecraft_Server_Starter +{ + public static class UnixDate + { + // epoch datetime + static readonly DateTime epoch = new DateTime(1970, 1, 1); + + // unix time utils + public static long DateTimeToUnixTime(DateTime dt) + { return Convert.ToInt64((dt - epoch).TotalMilliseconds); } + + public static DateTime UnixTimeToDateTime(long ut) + { return epoch.Add(TimeSpan.FromMilliseconds(ut)); } + + public static string GetUniversalDate() + { + CultureInfo tmp = CultureInfo.DefaultThreadCurrentCulture; + + CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture; + var result = DateTime.Now.ToString("ddd MMM dd HH:mm:ss CET yyyy"); + CultureInfo.DefaultThreadCurrentCulture = tmp; + + return result; + } + } +} diff --git a/Minecraft Server Starter/Classes/Utils/WaitCursor.cs b/Minecraft Server Starter/Classes/Utils/WaitCursor.cs new file mode 100644 index 0000000..61d9753 --- /dev/null +++ b/Minecraft Server Starter/Classes/Utils/WaitCursor.cs @@ -0,0 +1,27 @@ +/// +/// Copyright (c) 2016 All Rights Reserved +/// +/// Nicholas Butler +/// 02/03/2004 +/// Class used to use a wait cursor easily (using (WaitCursor.New) ...) + +using System; +using System.Windows.Input; + +public class WaitCursor : IDisposable +{ + Cursor _previousCursor; + + public WaitCursor() + { + _previousCursor = Mouse.OverrideCursor; + Mouse.OverrideCursor = Cursors.Wait; + } + + public void Dispose() + { + Mouse.OverrideCursor = _previousCursor; + } + + public static WaitCursor New => new WaitCursor(); +} \ No newline at end of file diff --git a/Minecraft Server Starter/Controls/ColoredTextBox.cs b/Minecraft Server Starter/Controls/ColoredTextBox.cs new file mode 100644 index 0000000..8343b66 --- /dev/null +++ b/Minecraft Server Starter/Controls/ColoredTextBox.cs @@ -0,0 +1,76 @@ +/// +/// Copyright (c) 2016 All Rights Reserved +/// +/// Lonami Exo +/// February 2016 +/// Simple class to generated colored text + +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Media; + +namespace Minecraft_Server_Starter +{ + class ColoredTextBox : RichTextBox + { + Paragraph paragraph; + + #region Public properties + + // this isn't exactly how it should work for a final product but... serves our purpose + public string Text + { + get + { + if (Selection.IsEmpty) + return new TextRange(Document.ContentStart, Document.ContentEnd).Text.Trim(); + + return Selection.Text.Trim(); + } + } + + #endregion + + #region Constructor + + public ColoredTextBox() + { + Clear(); + } + + #endregion + + #region Public methods + + // append text + public new void AppendText(string text) + { + paragraph.Inlines.Add(new Run(text)); + } + public void AppendText(string text, Brush color) + { + paragraph.Inlines.Add(new Run(text) { Foreground = color }); + } + + // append lines + public void AppendLine(string text) + { + AppendText(text); + paragraph.Inlines.Add(new LineBreak()); + } + public void AppendLine(string text, Brush color) + { + AppendText(text, color); + paragraph.Inlines.Add(new LineBreak()); + } + + // clear the paragraph and add a new clean one + public void Clear() + { + paragraph = new Paragraph(); + Document = new FlowDocument(paragraph); + } + + #endregion + } +} diff --git a/Minecraft Server Starter/Controls/MOTDTextBox.xaml b/Minecraft Server Starter/Controls/MOTDTextBox.xaml new file mode 100644 index 0000000..a890d4d --- /dev/null +++ b/Minecraft Server Starter/Controls/MOTDTextBox.xaml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Minecraft Server Starter/Windows/MainWindow.xaml.cs b/Minecraft Server Starter/Windows/MainWindow.xaml.cs new file mode 100644 index 0000000..a018a16 --- /dev/null +++ b/Minecraft Server Starter/Windows/MainWindow.xaml.cs @@ -0,0 +1,616 @@ +/// +/// Copyright (c) 2016 All Rights Reserved +/// +/// Lonami Exo +/// February 2016 +/// The main program window + +using ExtensionMethods; +using Microsoft.Win32; +using System; +using System.ComponentModel; +using System.IO; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +namespace Minecraft_Server_Starter +{ + public partial class MainWindow : Window + { + #region Private fields + + MinecraftServer ms; + + // current server status + Status currentStatus = Status.Closed; + + // can the ports be opened? + bool canOpenPorts; + + // are the ports open at the moment? + bool arePortsOpen; + + // don't open multiple settings instances + SettingsWindow settingsWindow; + + #endregion + + #region Constructor and loading + + public MainWindow() + { + InitializeComponent(); + } + + // load window + async void Window_Loaded(object sender, RoutedEventArgs e) + { + if (!Settings.GetValue("eulaAccepted")) + { + if (MessageBox.Show(Res.GetStr("smcAgreeEula"), + Res.GetStr("smtAgreeEula"), MessageBoxButton.YesNo, MessageBoxImage.Information) + != MessageBoxResult.Yes) + { + Close(); + return; + } + Settings.SetValue("eulaAccepted", true); + } + reloadServers(); + canOpenPorts = await PortManager.Init(); + } + + #endregion + + #region Server related + + #region Server operations + + // delete server + void deleteServerClick(object sender, RoutedEventArgs e) + { + // check if running + if (CheckServerRunning()) + return; + + // else show a new window, and if the srever was deleted, reload the servers + var dsw = new DeleteServerWindow((Server)serverList.SelectedItem); + dsw.ShowDialog(); + + if (dsw.Deleted) + reloadServers(); + } + + // manage backups + void manageBackupsClick(object sender, RoutedEventArgs e) + { + if (ms == null) // if no server is open, open a backup window with false + new BackupWindow(isServerOpen: false, server: (Server)serverList.SelectedItem).ShowDialog(); + + else // else, if the currently selected server is equal to the servers, and it's open, isServerOpen will be true + new BackupWindow(ms.Server == ((Server)serverList.SelectedItem) + && currentStatus == Status.Open, (Server)serverList.SelectedItem).ShowDialog(); + } + + // edit the current server + void editServerClick(object sender, RoutedEventArgs e) + { + new EditServerWindow((Server)serverList.SelectedItem).Show(); + } + + // upgrade (or downgrade) the current server + void upgradeServerClick(object sender, RoutedEventArgs e) + { + // check if running + if (CheckServerRunning()) // TODO WELL, USE DIFFERENT TEXT HAHA! + return; + + // else we can upgrde + new UpgradeWindow((Server)serverList.SelectedItem).ShowDialog(); + } + + bool CheckServerRunning() + { + // if the currently selected server is equal to the running one, and it's not stopped, show error + var server = (Server)serverList.SelectedItem; + if (ms != null && (server == ms.Server) && + currentStatus != Status.Closed) + { + MessageBox.Show(Res.GetStr("smcCannotDeleteRunning"), + Res.GetStr("smcCannotDeleteRunning"), MessageBoxButton.OK, MessageBoxImage.Error); + + return true; + } + + return false; + } + + // add server + void addServerClick(object sender, RoutedEventArgs e) + { + showAddServer(); + } + + void showAddServer(string worldPath = null) + { + var asw = new AddServerWindow(worldPath); + if (asw.ShowDialog() ?? false) + { + asw.Result.Save(); + reloadServers(); + } + } + + #endregion + + #region Server management + + // reload servers list + void reloadServers() + { + serverList.Items.Clear(); + foreach (var sv in Server.GetServers()) + serverList.Items.Add(sv); + + if (serverList.Items.Count > 0) // if there's any, select it + { + serverList.SelectedIndex = 0; + serverSelectedOptionsPanel.Visibility = + startStopPanel.Visibility = serverList.Visibility = Visibility.Visible; + } + else + serverSelectedOptionsPanel.Visibility = + startStopPanel.Visibility = serverList.Visibility = Visibility.Collapsed; + } + + // show server tooltip + void serverListMouseEnter(object sender, MouseEventArgs e) + { + if (serverList.Items.Count == 0) return; + + var sv = (Server)serverList.SelectedItem; + serverList.ToolTip = Res.GetStr("sCreatedThe", sv.CreationDate.ToLongDateString()); + if (!string.IsNullOrEmpty(sv.Description)) + serverList.ToolTip += Environment.NewLine + sv.Description; + } + + // start/stop the server + void startStopClick(object sender, RoutedEventArgs e) + { + switch (currentStatus) + { + case Status.Closed: + + if (!File.Exists(Settings.GetValue("javaPath"))) + { + MessageBox.Show(Res.GetStr("smcJavaNotFound"), + Res.GetStr("smtJavaNotFound"), MessageBoxButton.OK, MessageBoxImage.Error); + + break; + } + + ms = new MinecraftServer((Server)serverList.SelectedItem); + ms.ServerMessage += log; + ms.ServerStatusChanged += serverStatusChanged; + ms.Player += playerEvent; + + serverStatusChanged(Status.Opening); + logBox.Clear(); + + ms.Start(); + break; + case Status.Opening: + serverStatusChanged(Status.Closed); + ms.Kill(); + break; + case Status.Open: + serverStatusChanged(Status.Closing); + + ms.Stop(); + break; + case Status.Closing: + serverStatusChanged(Status.Closed); + ms.Kill(); + break; + } + } + + #endregion + + #region Server events + + void playerEvent(bool joined, string player) + { + Dispatcher.Invoke(() => + { + Toast.makeText(Res.GetStr(joined ? "sPlayerJoined" : "sPlayerLeft", player), + Res.GetStr(joined ? "sPlayerJoinedFull" : "sPlayerLeftFull", player), player); + + if (joined) + { + playerList.Items.Add(player); + playerList.SelectedItem = player; + } + else + playerList.Items.Remove(player); + + playersGrid.Visibility = playerList.Items.Count > 0 ? + Visibility.Visible : Visibility.Collapsed; + }); + } + + #endregion + + #region Server status + + async void serverStatusChanged(Status status) + { + bool mustClosePorts = false; + currentStatus = status; + + Dispatcher.Invoke(() => + { + Title = Res.GetStr("appTitle"); + + switch (status) + { + case Status.Opening: + case Status.Closing: + Title += Res.GetStr("sServerRunning", ms.Server.Name); + startStopBlock.Text = Res.GetStr("sForceClose"); + startStopImage.Source = Res.GetRes("StopImg"); + + commandGrid.Visibility = Visibility.Visible; + + sayGrid.Visibility = moreServerAction.Visibility = + moreServerActionMenu.Visibility = Visibility.Collapsed; + + portButton.Visibility = canOpenPorts ? Visibility.Visible : Visibility.Collapsed; + + break; + case Status.Open: + Title += Res.GetStr("sServerRunning", ms.Server.Name); + startStopBlock.Text = Res.GetStr("sCloseSaving"); + startStopImage.Source = Res.GetRes("StopImg"); + + commandGrid.Visibility = sayGrid.Visibility = + moreServerAction.Visibility = moreServerActionMenu.Visibility = Visibility.Visible; + + portButton.Visibility = canOpenPorts ? Visibility.Visible : Visibility.Collapsed; + + break; + case Status.Closed: + startStopBlock.Text = Res.GetStr("sOpenServer"); + startStopImage.Source = Res.GetRes("PlayImg"); + + commandGrid.Visibility = sayGrid.Visibility = + moreServerAction.Visibility = moreServerActionMenu.Visibility = + portButton.Visibility = Visibility.Collapsed; + + if (arePortsOpen) + mustClosePorts = true; + + break; + } + }); + + if (mustClosePorts) + await togglePorts(); + } + + #endregion + + #region Server commands + + // commands + void commandBox_KeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Enter) + { + e.Handled = true; + doSendCommand(); + } + } + void sendCommandClick(object sender, RoutedEventArgs e) => doSendCommand(); + void doSendCommand() + { + if (!string.IsNullOrWhiteSpace(commandBox.Text)) + { + ms.SendCommand(commandBox.Text.Trim()); + commandBox.Clear(); + } + } + + // say + void sayBox_KeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Enter) + { + e.Handled = true; + doSay(); + } + } + void sendMessageClick(object sender, RoutedEventArgs e) => doSay(); + void doSay() + { + if (!string.IsNullOrWhiteSpace(sayBox.Text)) + { + ms.Say(sayBox.Text.Trim()); + sayBox.Clear(); + } + } + + // more actions menu + void moreServerActionClick(object sender, RoutedEventArgs e) + { + moreServerActionMenu.IsOpen = true; + } + + // -> save + void saveWorldClick(object sender, RoutedEventArgs e) + { + moreServerActionMenu.IsOpen = false; + ms.Save(); + moreServerActionMenu.IsOpen = false; + } + + // -> kill + void closeNoSaveClick(object sender, RoutedEventArgs e) + { + moreServerActionMenu.IsOpen = false; + ms.Kill(); + moreServerActionMenu.IsOpen = false; + } + + #endregion + + #endregion + + #region Player management + + // op + void opClick(object sender, RoutedEventArgs e) + => ms.Op((string)playerList.SelectedItem); + + // deop + void deopClick(object sender, RoutedEventArgs e) + => ms.Deop((string)playerList.SelectedItem); + + // ban + void banClick(object sender, RoutedEventArgs e) + => ms.Ban((string)playerList.SelectedItem); + + // pardon + void pardonClick(object sender, RoutedEventArgs e) + => ms.Pardon((string)playerList.SelectedItem); + + // kick + void kickClick(object sender, RoutedEventArgs e) + => ms.Kick((string)playerList.SelectedItem); + + // tell + void tellPlayerClick(object sender, RoutedEventArgs e) + { + commandBox.Focus(); + commandBox.SelectAll(); + commandBox.Text = $"tell {(string)playerList.SelectedItem} "; + commandBox.SelectionStart = commandBox.Text.Length; + } + + // reload the player head + async void playerSelectionChanged(object sender, SelectionChangedEventArgs e) + { + playerHead.Source = playerList.SelectedIndex < 0 ? null : + await Heads.GetPlayerHead((string)playerList.SelectedItem); + } + + #endregion + + #region Log + + void log(string time, string type, string typeName, string msg) + { + Dispatcher.Invoke(() => + { + logBox.AppendText(time, Brushes.LightBlue); + logBox.AppendText(type, GetColorForType(typeName)); + logBox.AppendLine(msg); + logBox.ScrollToEnd(); + }); + } + + void copyLogClick(object sender, RoutedEventArgs e) + { + logBoxMenu.IsOpen = false; + + if (logBox.Selection.IsEmpty) + Clipboard.SetText(logBox.Text); + else + Clipboard.SetText(logBox.Selection.Text); + } + + void saveLogClick(object sender, RoutedEventArgs e) + { + logBoxMenu.IsOpen = false; + var sfd = new SaveFileDialog() + { + FileName = "mss_log_" + DateTime.Now.ToShortDateString(), + Filter = Res.GetStr("sTextFileFilter") + }; + if (sfd.ShowDialog() ?? false) + { + if (logBox.Selection.IsEmpty) + File.WriteAllText(sfd.FileName, logBox.Text); + else + File.WriteAllText(sfd.FileName, logBox.Selection.Text); + } + } + + void clearLogClick(object sender, RoutedEventArgs e) + { + logBoxMenu.IsOpen = false; + logBox.Clear(); + } + + Brush GetColorForType(string typeName) + { + switch (typeName) + { + case "INFO": return Brushes.Blue; + case "WARNING": case "WARN": return Brushes.DarkOrange; + case "ERROR": return Brushes.Red; + default: return Brushes.Violet; + } + } + + #endregion + + #region Drag and drop worlds + + // drag enter + void dragEnter(object sender, DragEventArgs e) + { + Console.WriteLine("enter"); + var paths = (string[])e.Data.GetData(DataFormats.FileDrop); + if (paths != null && paths.Length == 1 && MinecraftWorld.IsValidWorld(paths[0])) + { + e.Effects = DragDropEffects.Copy; + toggleDragDropGrid(true); + } + else + { + e.Effects = DragDropEffects.None; + toggleDragDropGrid(false, true); + } + } + + // drag leave + void dragLeave(object sender, DragEventArgs e) + { + Console.WriteLine("leave"); + toggleDragDropGrid(false); + } + + void dragMouseMove(object sender, MouseEventArgs e) + { + Console.WriteLine("move"); + // handle awkward cases! + if (Mouse.LeftButton == MouseButtonState.Released && dragDropGrid.Opacity > 0) + toggleDragDropGrid(false); + } + + // drag drop + void dragDrop(object sender, DragEventArgs e) + { + Console.WriteLine("drop"); + var a = ((string[])e.Data.GetData(DataFormats.FileDrop))[0]; + toggleDragDropGrid(false); + if ((e.Effects & DragDropEffects.Copy) != 0) + showAddServer(((string[])e.Data.GetData(DataFormats.FileDrop))[0]); + } + + // toggle grid + void toggleDragDropGrid(bool shown, bool quick = false) + { + // TODO fix this, after toggling it hit test is gone! :( + Console.WriteLine("toggle"); + if (quick) + dragDropGrid.Opacity = shown ? 1 : 0; + else + dragDropGrid.Fade(shown); + } + + #endregion + + #region Settings + + void settingsClick(object sender, RoutedEventArgs e) + { + if (settingsWindow == null) + { + settingsWindow = new SettingsWindow(); + settingsWindow.Closed += (ss, se) => settingsWindow = null; + settingsWindow.Show(); + } + else + settingsWindow.Activate(); + } + + #endregion + + #region Ports + + async void portClick(object sender, RoutedEventArgs e) + { + await togglePorts(); + } + + async Task togglePorts() + { + ushort port; + ushort.TryParse(ms.Server.Properties["server-port"].Value, out port); + if (port == 0) + port = 25565; + + if (arePortsOpen) // close them + { + await PortManager.ClosePort(port); + portsText.Text = "Open port"; + } + else // open them + { + await PortManager.OpenPort(port); + portsText.Text = "Close port"; + } + + arePortsOpen = !arePortsOpen; + } + + #endregion + + #region Handle forced closing + + void Window_Closing(object sender, CancelEventArgs e) + { + switch (currentStatus) + { + case Status.Opening: + case Status.Closing: + + switch (MessageBox.Show(Res.GetStr("smcServerBusy"), Res.GetStr("smtServerBusy"), + MessageBoxButton.OKCancel, MessageBoxImage.Question)) + { + case MessageBoxResult.OK: + ms.Kill(); + return; + + default: + e.Cancel = true; + return; + } + + case Status.Open: + + switch (MessageBox.Show(Res.GetStr("smcWorldUnsaved"), Res.GetStr("smtWorldUnsaved"), + MessageBoxButton.YesNoCancel, MessageBoxImage.Question)) + { + case MessageBoxResult.Yes: + ms.Stop(); + return; + case MessageBoxResult.No: + ms.Kill(); + return; + default: + e.Cancel = true; + return; + } + } + } + + #endregion + } +} diff --git a/Minecraft Server Starter/Windows/Pages/GenerateBackupPage.xaml b/Minecraft Server Starter/Windows/Pages/GenerateBackupPage.xaml new file mode 100644 index 0000000..a360ede --- /dev/null +++ b/Minecraft Server Starter/Windows/Pages/GenerateBackupPage.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + +