diff --git a/README.md b/README.md index dcb9a2a..911a731 100644 --- a/README.md +++ b/README.md @@ -52,9 +52,24 @@ Before installing the CPDLC Plugin, ensure you have the following: ### Configuring Labels and Strips -The plugin provides custom label and strip items that must be added to your profile's `Labels.xml` and `Strips.xml` files. See [Labels.xml.diff](Labels.xml.diff) and [Strips.xml.diff](Strips.xml.diff) for complete examples. +The plugin provides custom label and strip items that must be added to your profile's `Labels.xml` and `Strips.xml` files. -#### Labels.xml +#### Automatic (recommended) + +After launching vatSys with the plugin installed, use the `CPDLC > Install label & strip items` menu option to add the custom label and strip items. Restart vatSys to apply the changes. + +To revert, use `CPDLC > Uninstall label & strip items`, which restores the original files from a backup. + +If your `Labels.xml` or `Strips.xml` files have been customised, the automatic installer will fail. Apply the changes manually using the diffs below. + +> [!NOTE] +> When bundling the plugin in a vatSys profile, you can hide the install and uninstall menu items by setting `ShowInstallationMenuItems` to `false` in `CPDLC.json`. + +#### Manual + +See [Labels.xml.diff](Labels.xml.diff) and [Strips.xml.diff](Strips.xml.diff) for complete examples. + +##### Labels.xml Replace each occurrence of: ```xml @@ -72,7 +87,7 @@ With: ``` -#### Strips.xml +##### Strips.xml Replace: ```xml diff --git a/docs/docs/vatsys-plugin/configuration.md b/docs/docs/vatsys-plugin/configuration.md index f739683..75947b3 100644 --- a/docs/docs/vatsys-plugin/configuration.md +++ b/docs/docs/vatsys-plugin/configuration.md @@ -117,6 +117,17 @@ Default: `1` "NdaRecalculationIntervalMinutes": 1 ``` +### ShowInstallationMenuItems + +Controls whether the `Install label & strip items` and `Uninstall label & strip items` entries appear under the `CPDLC` menu. Set to `false` when shipping the plugin with pre-modified profiles to prevent controllers from accidentally reverting the changes. + +Default: `true` + +**Example:** +```json +"ShowInstallationMenuItems": false +``` + ## Uplink Messages The `UplinkMessages` object defines the available CPDLC messages for the controller to send. diff --git a/docs/docs/vatsys-plugin/installation.md b/docs/docs/vatsys-plugin/installation.md index 7d32d9e..613fb2b 100644 --- a/docs/docs/vatsys-plugin/installation.md +++ b/docs/docs/vatsys-plugin/installation.md @@ -49,6 +49,20 @@ vatSys requires separate items for foreground content and background color. The vatSys uses the Eurocat CPDLC symbols (`.`, `-`, `+`) for voice capabilities on VATSIM. The plugin repurposes these symbols for their intended CPDLC function, and provides `CPDLCPLUGIN_TEXTSTATUS` as a replacement voice capability indicator. +#### Automatic + +After launching vatSys with the plugin installed, use the `CPDLC > Install label & strip items` menu option to add the custom label and strip items. Restart vatSys to apply the changes. + +To revert, use `CPDLC > Uninstall label & strip items`, which restores the original files from a backup. + +If your `Labels.xml` or `Strips.xml` files have been customised, the automatic installer will fail. Apply the changes manually using the sections below. + +:::note +When bundling the plugin in a vatSys profile, you can hide the install and uninstall menu items by setting `ShowInstallationMenuItems` to `false` in `CPDLC.json`. +::: + +#### Manual + **Labels.xml:** Replace `LABEL_ITEM_CPDLC` with: ```xml diff --git a/source/CPDLCPlugin/Configuration/ConfigurationLoader.cs b/source/CPDLCPlugin/Configuration/ConfigurationLoader.cs index fb58b9e..9ab925c 100644 --- a/source/CPDLCPlugin/Configuration/ConfigurationLoader.cs +++ b/source/CPDLCPlugin/Configuration/ConfigurationLoader.cs @@ -2,7 +2,6 @@ using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; -using vatsys; namespace CPDLCPlugin.Configuration; @@ -15,7 +14,7 @@ public static PluginConfiguration Load() var searchDirectories = new List(); // Search the profile first - if (TryFindProfileDirectory(out var profileDirectory)) + if (ProfileDirectoryResolver.TryGetProfileDirectory(out var profileDirectory)) { searchDirectories.AddRange([ Path.Combine(profileDirectory.FullName, "Plugins", "Configs", "CPDLC Plugin"), @@ -56,18 +55,4 @@ public static PluginConfiguration Load() return configuration; } - - // Thanks Max! - static bool TryFindProfileDirectory(out DirectoryInfo? directoryInfo) - { - directoryInfo = null; - if (!Profile.Loaded) - return false; - - var shortNameObject = typeof(Profile).GetField("shortName", BindingFlags.Static | BindingFlags.NonPublic); - var shortName = (string)shortNameObject.GetValue(shortNameObject); - - directoryInfo = new DirectoryInfo(Path.Combine(Helpers.GetFilesFolder(), "Profiles", shortName)); - return true; - } } diff --git a/source/CPDLCPlugin/Configuration/PluginConfiguration.cs b/source/CPDLCPlugin/Configuration/PluginConfiguration.cs index 9bca693..878d695 100644 --- a/source/CPDLCPlugin/Configuration/PluginConfiguration.cs +++ b/source/CPDLCPlugin/Configuration/PluginConfiguration.cs @@ -21,4 +21,9 @@ public class PluginConfiguration // How often (in minutes) to recalculate the NDA for all tracked aircraft. // NDA is also recalculated on every FDR update, so this is a catch-all interval. public int NdaRecalculationIntervalMinutes { get; init; } = 1; + + // When false, the Install/Uninstall label & strip items menu entries are hidden. + // ACCs shipping the plugin with pre-modified profiles can disable these to prevent + // controllers from accidentally reverting the changes. + public bool ShowInstallationMenuItems { get; init; } = true; } diff --git a/source/CPDLCPlugin/Configuration/ProfileDirectoryResolver.cs b/source/CPDLCPlugin/Configuration/ProfileDirectoryResolver.cs new file mode 100644 index 0000000..b5fcd1c --- /dev/null +++ b/source/CPDLCPlugin/Configuration/ProfileDirectoryResolver.cs @@ -0,0 +1,21 @@ +using System.IO; +using System.Reflection; +using vatsys; + +namespace CPDLCPlugin.Configuration; + +public static class ProfileDirectoryResolver +{ + public static bool TryGetProfileDirectory(out DirectoryInfo? directoryInfo) + { + directoryInfo = null; + if (!Profile.Loaded) + return false; + + var shortNameObject = typeof(Profile).GetField("shortName", BindingFlags.Static | BindingFlags.NonPublic); + var shortName = (string)shortNameObject.GetValue(shortNameObject); + + directoryInfo = new DirectoryInfo(Path.Combine(Helpers.GetFilesFolder(), "Profiles", shortName)); + return true; + } +} diff --git a/source/CPDLCPlugin/Configuration/XmlInstaller.cs b/source/CPDLCPlugin/Configuration/XmlInstaller.cs new file mode 100644 index 0000000..a39f894 --- /dev/null +++ b/source/CPDLCPlugin/Configuration/XmlInstaller.cs @@ -0,0 +1,168 @@ +using System.IO; + +namespace CPDLCPlugin.Configuration; + +public static class XmlInstaller +{ + const string BackupSuffix = ".cpdlcplugin.bak"; + const string InstalledMarker = "CPDLCPLUGIN_CPDLCSTATUS"; + + // Replace the CPDLC/WAKETURBCAT/FIELD18RMK block because the new layout inserts CPDLCSTATUS + // items before WAKETURBCAT and appends TEXTSTATUS items after FIELD18RMK. + const string LabelsOldBlock = + " \r\n \r\n "; + + const string LabelsNewBlock = + " \r\n \r\n \r\n\r\n \r\n \r\n\r\n \r\n \r\n "; + + const string StripsOldItem = + " "; + + const string StripsNewItem = + " \r\n \r\n\r\n \r\n "; + + public static InstallationStatus GetStatus() + { + if (!TryFindConfigFiles(out var labels, out var strips, out var error)) + return InstallationStatus.Error(error!); + + var labelsInstalled = File.Exists(labels) && File.ReadAllText(labels).Contains(InstalledMarker); + var stripsInstalled = File.Exists(strips) && File.ReadAllText(strips).Contains(InstalledMarker); + + if (labelsInstalled && stripsInstalled) + return InstallationStatus.Installed(); + + return InstallationStatus.NotInstalled(); + } + + public static InstallResult Install() + { + if (!TryFindConfigFiles(out var labels, out var strips, out var error)) + return InstallResult.Failed(error!); + + try + { + PatchFile(labels, LabelsOldBlock, LabelsNewBlock); + PatchFile(strips, StripsOldItem, StripsNewItem); + return InstallResult.Success(); + } + catch (InstallException ex) + { + return InstallResult.Failed(ex.Message); + } + } + + public static InstallResult Uninstall() + { + if (!TryFindConfigFiles(out var labels, out var strips, out var error)) + return InstallResult.Failed(error!); + + try + { + RestoreFile(labels); + RestoreFile(strips); + return InstallResult.Success(); + } + catch (InstallException ex) + { + return InstallResult.Failed(ex.Message); + } + } + + static void PatchFile(string filePath, string oldText, string newText) + { + if (!File.Exists(filePath)) + throw new InstallException($"File not found: {filePath}"); + + var backupPath = filePath + BackupSuffix; + + // If a backup exists, restore from it as the base to avoid stacking patches. + // Otherwise, create a backup from the current file. + string content; + if (File.Exists(backupPath)) + { + content = File.ReadAllText(backupPath); + } + else + { + content = File.ReadAllText(filePath); + File.WriteAllText(backupPath, content); + } + + if (!content.Contains(oldText)) + { + throw new InstallException( + $"Could not find the expected content in {Path.GetFileName(filePath)}. " + + "The file may have been customised. Please apply the changes manually."); + } + + // Replace all occurrences (Labels.xml has both a Normal and Extended label block). + var patched = content.Replace(oldText, newText); + + File.WriteAllText(filePath, patched); + } + + static void RestoreFile(string filePath) + { + var backupPath = filePath + BackupSuffix; + if (!File.Exists(backupPath)) + { + throw new InstallException( + $"No backup found for {Path.GetFileName(filePath)}. " + + "The plugin's label & strip items must be removed manually."); + } + + File.Copy(backupPath, filePath, overwrite: true); + File.Delete(backupPath); + } + + static bool TryFindConfigFiles(out string labelsPath, out string stripsPath, out string? error) + { + labelsPath = string.Empty; + stripsPath = string.Empty; + error = null; + + if (!ProfileDirectoryResolver.TryGetProfileDirectory(out var profileDir) || profileDir is null) + { + error = "Profile directory could not be resolved."; + return false; + } + + labelsPath = Path.Combine(profileDir.FullName, "Labels.xml"); + stripsPath = Path.Combine(profileDir.FullName, "Strips.xml"); + return true; + } + + class InstallException(string message) : Exception(message); +} + +public readonly struct InstallationStatus +{ + public bool IsInstalled { get; } + public string? ErrorMessage { get; } + + InstallationStatus(bool isInstalled, string? errorMessage) + { + IsInstalled = isInstalled; + ErrorMessage = errorMessage; + } + + public static InstallationStatus Installed() => new(true, null); + public static InstallationStatus NotInstalled() => new(false, null); + public static InstallationStatus Error(string errorMessage) => new(false, errorMessage); +} + +public readonly struct InstallResult +{ + public bool Succeeded { get; } + public string? ErrorMessage { get; } + + InstallResult(bool succeeded, string? errorMessage) + { + Succeeded = succeeded; + ErrorMessage = errorMessage; + } + + public static InstallResult Success() => new(true, null); + public static InstallResult Failed(string errorMessage) => new(false, errorMessage); +} diff --git a/source/CPDLCPlugin/Plugin.cs b/source/CPDLCPlugin/Plugin.cs index e2fed03..6bc66ac 100644 --- a/source/CPDLCPlugin/Plugin.cs +++ b/source/CPDLCPlugin/Plugin.cs @@ -44,6 +44,12 @@ public class Plugin : ILabelPlugin, IStripPlugin, IRecipient Name; IServiceProvider ServiceProvider { get; set; } @@ -65,6 +71,8 @@ public Plugin() AddToolbarItems(); + CheckXmlInstallation(); + Network.Connected += NetworkConnected; Network.Disconnected += NetworkDisconnected; @@ -326,6 +334,119 @@ void AddToolbarItems() }; MMI.AddCustomMenuItem(historyMenuItem); + + if (_configuration?.ShowInstallationMenuItems ?? true) + { + var status = XmlInstaller.GetStatus(); + if (!string.IsNullOrEmpty(status.ErrorMessage)) + return; + + _labelsInstalled = status.IsInstalled; + + var toolStripItem = new ToolStripMenuItem(_labelsInstalled ? UninstallMenuLabel : InstallMenuLabel); + toolStripItem.Click += (_, _) => + { + if (_labelsInstalled) + RunUninstall(); + else + RunInstall(); + }; + + _installationMenuItem = toolStripItem; + + var menuItem = new CustomToolStripMenuItem( + CustomToolStripMenuItemWindowType.Main, + menuItemCategory, + toolStripItem); + MMI.AddCustomMenuItem(menuItem); + } + } + + void CheckXmlInstallation() + { + try + { + var status = XmlInstaller.GetStatus(); + if (!string.IsNullOrEmpty(status.ErrorMessage)) + { + Log.Warning("Could not verify Labels.xml/Strips.xml installation: {Error}", status.ErrorMessage); + return; + } + + if (!status.IsInstalled) + { + AddError(new InvalidOperationException("CPDLC Label and Strip items must be installed. Please install them from the CPDLC menu.")); + } + } + catch (Exception ex) + { + AddError(ex, "Failed to verify Labels.xml/Strips.xml installation"); + } + } + + void RunInstall() + { + try + { + var result = XmlInstaller.Install(); + if (result.Succeeded) + { + SetInstallationMenuState(installed: true); + System.Windows.Forms.MessageBox.Show( + "CPDLC label & strip items installed successfully.\n\nPlease restart vatSys to apply the changes.", + Name, + MessageBoxButtons.OK, + MessageBoxIcon.Information); + } + else + { + System.Windows.Forms.MessageBox.Show( + $"Failed to install CPDLC label & strip items.\n\n{result.ErrorMessage}", + Name, + MessageBoxButtons.OK, + MessageBoxIcon.Error); + } + } + catch (Exception ex) + { + AddError(ex, "Failed to install label & strip items"); + } + } + + void SetInstallationMenuState(bool installed) + { + _labelsInstalled = installed; + if (_installationMenuItem is not null) + _installationMenuItem.Text = installed ? UninstallMenuLabel : InstallMenuLabel; + } + + void RunUninstall() + { + try + { + var result = XmlInstaller.Uninstall(); + if (result.Succeeded) + { + SetInstallationMenuState(installed: false); + System.Windows.Forms.MessageBox.Show( + "CPDLC label & strip items uninstalled successfully.\n\nPlease restart vatSys to apply the changes.", + Name, + MessageBoxButtons.OK, + MessageBoxIcon.Information); + } + else + { + System.Windows.Forms.MessageBox.Show( + $"Failed to uninstall CPDLC label & strip items.\n\n{result.ErrorMessage}", + Name, + MessageBoxButtons.OK, + MessageBoxIcon.Error); + } + } + catch (Exception ex) + { + AddError(ex, "Failed to uninstall label & strip items"); + } } public void OnFDRUpdate(FDP2.FDR updated)