Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -72,7 +87,7 @@ With:
<Item Type="CPDLCPLUGIN_TEXTSTATUS_BG" BackgroundColour="Custom" />
```

#### Strips.xml
##### Strips.xml

Replace:
```xml
Expand Down
11 changes: 11 additions & 0 deletions docs/docs/vatsys-plugin/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 14 additions & 0 deletions docs/docs/vatsys-plugin/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 1 addition & 16 deletions source/CPDLCPlugin/Configuration/ConfigurationLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using vatsys;

namespace CPDLCPlugin.Configuration;

Expand All @@ -15,7 +14,7 @@ public static PluginConfiguration Load()
var searchDirectories = new List<string>();

// 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"),
Expand Down Expand Up @@ -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;
}
}
5 changes: 5 additions & 0 deletions source/CPDLCPlugin/Configuration/PluginConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
21 changes: 21 additions & 0 deletions source/CPDLCPlugin/Configuration/ProfileDirectoryResolver.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
168 changes: 168 additions & 0 deletions source/CPDLCPlugin/Configuration/XmlInstaller.cs
Original file line number Diff line number Diff line change
@@ -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 =
" <Item Type=\"LABEL_ITEM_CPDLC\" Colour=\"\" BackgroundColour=\"CPDLCDownlink\" LeftClick=\"Label_CPDLC_Menu\" MiddleClick=\"Label_CPDLC_Message_Toggle\" RightClick=\"Label_CPDLC_Editor\" />\r\n <Item Type=\"LABEL_ITEM_WAKETURBCAT\" Colour=\"\" LeftClick=\"\" MiddleClick=\"\" RightClick=\"\" />\r\n <Item Type=\"LABEL_ITEM_FIELD18RMK\" Colour=\"\" LeftClick=\"Label_Field18_Popup\" MiddleClick=\"\" RightClick=\"\" />";

const string LabelsNewBlock =
" <!-- CPDLC Plugin: CPDLC Status -->\r\n <Item Type=\"CPDLCPLUGIN_CPDLCSTATUS\" />\r\n <Item Type=\"CPDLCPLUGIN_CPDLCSTATUS_BG\" BackgroundColour=\"Custom\" />\r\n\r\n <Item Type=\"LABEL_ITEM_WAKETURBCAT\" Colour=\"\" LeftClick=\"\" MiddleClick=\"\" RightClick=\"\" />\r\n <Item Type=\"LABEL_ITEM_FIELD18RMK\" Colour=\"\" LeftClick=\"Label_Field18_Popup\" MiddleClick=\"\" RightClick=\"\" />\r\n\r\n <!-- CPDLC Plugin: Text Status -->\r\n <Item Type=\"CPDLCPLUGIN_TEXTSTATUS\" />\r\n <Item Type=\"CPDLCPLUGIN_TEXTSTATUS_BG\" BackgroundColour=\"Custom\" />";

const string StripsOldItem =
" <StripItem Type=\"CPDLCStatus\" LeftClick=\"Label_CPDLC_Editor\" MinLength=\"1\" />";

const string StripsNewItem =
" <!-- CPDLC Plugin: CPDLC Status -->\r\n <StripItem Type=\"CPDLCPLUGIN_CPDLCSTATUS\" MinLength=\"1\" />\r\n\r\n <!-- CPDLC Plugin: Text Status -->\r\n <StripItem Type=\"CPDLCPLUGIN_TEXTSTATUS\" />";

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);
}
Loading
Loading