using System; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Windows.Forms; using System.Xml; using Tesla.Net; namespace TeslaConsole; /// Role a launch entry fills; only meaningful for products that prompt with the start-params dialog. public enum AppHostType { None, GameClient, LiveCamera, MissionReview } /// One launchable application a product registers on a pod. public class LaunchEntryDefinition { public Guid LaunchKey; public string DisplayName = ""; public string Exe = ""; /// Arguments; the token "{res}" expands to " -res W H" when a custom resolution is chosen, else "". public string Args = ""; /// Optional; when null/empty the working directory is derived from . public string WorkingDirectory; public bool AutoRestart = true; public AppHostType HostType; /// Builds the wire for this entry, expanding {res} from the optional resolution. public LaunchData ToLaunchData(Size? resolution) { string resArg = resolution.HasValue ? $" -res {resolution.Value.Width} {resolution.Value.Height}" : ""; LaunchData data = default(LaunchData); data.LaunchPair.DisplayName = DisplayName; data.LaunchPair.LaunchKey = LaunchKey; data.ExeFile = Exe; data.Arguments = (Args ?? "").Replace("{res}", resArg); data.WorkingDirectory = string.IsNullOrEmpty(WorkingDirectory) ? Path.GetDirectoryName(Exe) : WorkingDirectory; data.AutoRestart = AutoRestart; return data; } } /// An installable product: an install/package key plus the launch entries it registers. public class ProductDefinition { public Guid Id; public string Name = ""; public string MenuText = ""; /// When true, installing prompts for resolution + roles and only selected-role entries are registered. public bool HostTypeDialog; public List Entries = new List(); } /// /// Data-driven catalog of installable products, loaded from RedPlanet\Apps.xml (next to the exe). /// Replaces the products that were hardcoded in . /// public static class AppRegistry { private static readonly List mProducts = new List(); private static readonly Dictionary mById = new Dictionary(); // IList, not IReadOnlyList: the read-only interfaces are net45+ and the // XP11 console targets net40. Callers only enumerate/index it. public static IList Products => mProducts; public static string CatalogPath => Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), "RedPlanet\\Apps.xml"); static AppRegistry() { Reload(); } public static ProductDefinition GetProduct(Guid id) { return mById.TryGetValue(id, out var product) ? product : null; } /// (Re)loads the catalog from . Safe if the file is missing — yields an empty catalog. public static void Reload() { LoadFromFile(CatalogPath); } /// Loads the catalog from an explicit path. Safe if the file is missing — yields an empty catalog. public static void LoadFromFile(string path) { mProducts.Clear(); mById.Clear(); if (!File.Exists(path)) { return; } XmlDocument doc = new XmlDocument(); doc.Load(path); foreach (XmlNode node in doc.DocumentElement.ChildNodes) { if (node.NodeType == XmlNodeType.Element && node.Name == "Product") { ProductDefinition product = ParseProduct(node); mProducts.Add(product); mById[product.Id] = product; } } } /// Adds a product to the catalog and persists the whole catalog to disk. public static void AddProduct(ProductDefinition product) { mProducts.Add(product); mById[product.Id] = product; Save(); } /// Writes the catalog back to . public static void Save() { XmlDocument doc = new XmlDocument(); doc.AppendChild(doc.CreateXmlDeclaration("1.0", "utf-8", null)); doc.AppendChild(doc.CreateComment( " Tesla Console product catalog. Managed by Site Management (Add Product...). ")); XmlElement root = doc.CreateElement("AppCatalog"); doc.AppendChild(root); foreach (ProductDefinition product in mProducts) { XmlElement pe = doc.CreateElement("Product"); pe.SetAttribute("id", product.Id.ToString("D")); pe.SetAttribute("name", product.Name); pe.SetAttribute("menuText", product.MenuText); pe.SetAttribute("hostTypeDialog", product.HostTypeDialog ? "true" : "false"); foreach (LaunchEntryDefinition entry in product.Entries) { XmlElement le = doc.CreateElement("Launch"); le.SetAttribute("key", entry.LaunchKey.ToString("D")); le.SetAttribute("displayName", entry.DisplayName); le.SetAttribute("exe", entry.Exe); le.SetAttribute("args", entry.Args ?? ""); if (!string.IsNullOrEmpty(entry.WorkingDirectory)) { le.SetAttribute("workingDirectory", entry.WorkingDirectory); } le.SetAttribute("autoRestart", entry.AutoRestart ? "true" : "false"); le.SetAttribute("hostType", entry.HostType.ToString()); pe.AppendChild(le); } root.AppendChild(pe); } Directory.CreateDirectory(Path.GetDirectoryName(CatalogPath)); doc.Save(CatalogPath); } private static ProductDefinition ParseProduct(XmlNode node) { ProductDefinition product = new ProductDefinition { Id = ParseGuid(Attr(node, "id")), Name = Attr(node, "name"), MenuText = Attr(node, "menuText"), HostTypeDialog = ParseBool(Attr(node, "hostTypeDialog")) }; if (string.IsNullOrEmpty(product.MenuText)) { product.MenuText = product.Name + "..."; } foreach (XmlNode child in node.ChildNodes) { if (child.NodeType == XmlNodeType.Element && child.Name == "Launch") { product.Entries.Add(ParseLaunch(child)); } } return product; } private static LaunchEntryDefinition ParseLaunch(XmlNode node) { return new LaunchEntryDefinition { LaunchKey = ParseGuid(Attr(node, "key")), DisplayName = Attr(node, "displayName"), Exe = Attr(node, "exe"), Args = Attr(node, "args"), WorkingDirectory = HasAttr(node, "workingDirectory") ? Attr(node, "workingDirectory") : null, AutoRestart = !HasAttr(node, "autoRestart") || ParseBool(Attr(node, "autoRestart")), HostType = ParseHostType(Attr(node, "hostType")) }; } private static bool HasAttr(XmlNode node, string name) { return node.Attributes?[name] != null; } private static string Attr(XmlNode node, string name) { return node.Attributes?[name]?.InnerText ?? ""; } private static Guid ParseGuid(string value) { return Guid.TryParse(value, out var guid) ? guid : Guid.Empty; } private static bool ParseBool(string value) { return string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) || value == "1"; } private static AppHostType ParseHostType(string value) { return Enum.TryParse(value, ignoreCase: true, out var result) ? result : AppHostType.None; } }