LANCommander/LANCommander.Server/UI/Components/ScriptEditorDialog.razor
Pat Hartl ca713f6d3f Rework .reg importing in script editor
Script editor now supports drag and drop for .ps1, .reg, and other text files. .ps1 files will be handled by replacing the contents of the text editor. .reg files will be converted to PowerShell commands and inserted into the script editor. Any other text file will be inserted as an escaped string.
2026-06-10 20:19:06 -05:00

368 lines
15 KiB
Text

@using LANCommander.SDK.Enums
@using OptionSchema = LANCommander.SDK.Models.OptionSchema
@using OptionChoiceYamlConverter = LANCommander.SDK.Models.OptionChoiceYamlConverter
@using LANCommander.Server.Extensions
@using YamlDotNet.Serialization
@using YamlDotNet.Serialization.NamingConventions
@inherits FeedbackComponent<ScriptEditorOptions, Script>
@inject ScriptService ScriptService
@inject ArchiveService ArchiveService
@inject ServerService ServerService
@inject RedistributableService RedistributableService
@inject ModalService ModalService
@inject IMessageService MessageService
@inject ILogger<ScriptEditorDialog> Logger
<Flex Justify="FlexJustify.SpaceBetween" Align="FlexAlign.Center" Style="margin-bottom: 16px;">
<Flex Gap="FlexGap.Small" Wrap="FlexWrap.Wrap" Align="FlexAlign.Center">
@foreach (var group in Snippets.Select(s => s.Group).Distinct())
{
<Dropdown>
<Overlay>
<Menu>
@foreach (var snippet in Snippets.Where(s => s.Group == group))
{
<MenuItem OnClick="() => InsertSnippet(snippet)">
@snippet.Name
</MenuItem>
}
</Menu>
</Overlay>
<ChildContent>
<Button Type="@ButtonType.Primary">@group</Button>
</ChildContent>
</Dropdown>
}
@if (_variables.Any())
{
<Dropdown>
<Overlay>
<Menu>
@foreach (var variable in _variables)
{
<MenuItem OnClick="() => InsertText(variable)">
@variable
</MenuItem>
}
</Menu>
</Overlay>
<ChildContent>
<Button Type="@ButtonType.Primary">Variables</Button>
</ChildContent>
</Dropdown>
}
@if (_optionKeys.Any())
{
<Dropdown>
<Overlay>
<Menu>
@foreach (var optionKey in _optionKeys)
{
<MenuItem OnClick="() => InsertOptionValue(optionKey)">
$Options.@optionKey
</MenuItem>
}
</Menu>
</Overlay>
<ChildContent>
<Button Type="@ButtonType.Primary">Options</Button>
</ChildContent>
</Dropdown>
}
@if (_archive != null)
{
<Button Icon="@IconType.Outline.FolderOpen" OnClick="BrowseForPath" Type="@ButtonType.Text">Browse</Button>
}
@if (IsDebuggable && HasDebugTarget)
{
<Tooltip Title="Debug">
<Button Icon="@IconType.Outline.CaretRight" Type="@ButtonType.Text" OnClick="Debug" Disabled="@_debugging" />
</Tooltip>
}
</Flex>
<Checkbox @bind-Checked="_script.RequiresAdmin">Requires Admin</Checkbox>
</Flex>
<CodeInput @ref="_codeInput" @bind-Value="_script.Contents" Language="Languages.PowerShell" OnSave="Save" ScriptType="@_script.Type.ToString()" OnReady="OnEditorReady" />
<PowerShellConsole @ref="_console" Id="_id" @bind-Active="_debugging"/>
<Form @ref="@_form" Model="@_script" Layout="@FormLayout.Vertical">
<FormItem Label="Name" Required Rules=@(new[] { new FormValidationRule { Required = true } })>
<Input @bind-Value="@context.Name" />
</FormItem>
<FormItem Label="Description">
<TextArea @bind-Value="context.Description" MaxLength=500 ShowCount />
</FormItem>
</Form>
@code {
Guid _id = Guid.NewGuid();
Form<Script>? _form;
CodeInput? _codeInput;
PowerShellConsole? _console;
IEnumerable<Snippet> Snippets { get; set; } = [];
List<string> _variables = new();
List<string> _optionKeys = new();
string _redistributableName = string.Empty;
bool _debugging;
bool _isNewScript;
Archive? _archive;
Script _script = new();
bool IsDebuggable =>
_script.Type == ScriptType.Package;
bool HasDebugTarget =>
(Options.GameId.HasValue && Options.GameId != Guid.Empty) ||
(Options.ToolId.HasValue && Options.ToolId != Guid.Empty) ||
(Options.RedistributableId.HasValue && Options.RedistributableId != Guid.Empty);
static readonly string[] GameClientScriptTypes = ["Install", "Uninstall", "BeforeStart", "AfterStop", "NameChange", "KeyChange", "SaveUpload", "SaveDownload", "DetectInstall", "RunWrapper"];
static readonly Dictionary<string, string[]> VariablesByScriptType = new()
{
["Install"] = ["$InstallDirectory", "$WorkingDirectory", "$ServerAddress", "$DefaultInstallDirectory", "$GameManifest", "$ToolManifest", "$RedistributableManifest", "$DisplayWidth", "$DisplayHeight", "$DisplayRefreshRate", "$DisplayBitDepth", "$IPXRelayHost", "$IPXRelayPort"],
["Uninstall"] = ["$InstallDirectory", "$WorkingDirectory", "$ServerAddress", "$DefaultInstallDirectory", "$GameManifest", "$DisplayWidth", "$DisplayHeight", "$DisplayRefreshRate", "$DisplayBitDepth", "$IPXRelayHost", "$IPXRelayPort"],
["BeforeStart"] = ["$InstallDirectory", "$WorkingDirectory", "$ServerAddress", "$DefaultInstallDirectory", "$GameManifest", "$PlayerAlias", "$Server", "$ServerId", "$ServerName", "$ServerHost", "$ServerPort", "$GameTitle", "$GameId", "$ToolManifest", "$RedistributableManifest", "$DisplayWidth", "$DisplayHeight", "$DisplayRefreshRate", "$DisplayBitDepth", "$IPXRelayHost", "$IPXRelayPort"],
["AfterStop"] = ["$InstallDirectory", "$WorkingDirectory", "$ServerAddress", "$DefaultInstallDirectory", "$GameManifest", "$PlayerAlias", "$Server", "$ServerId", "$ServerName", "$ServerHost", "$ServerPort", "$GameTitle", "$GameId", "$ToolManifest", "$RedistributableManifest", "$DisplayWidth", "$DisplayHeight", "$DisplayRefreshRate", "$DisplayBitDepth", "$IPXRelayHost", "$IPXRelayPort"],
["NameChange"] = ["$InstallDirectory", "$WorkingDirectory", "$ServerAddress", "$DefaultInstallDirectory", "$GameManifest", "$OldPlayerAlias", "$NewPlayerAlias", "$RedistributableManifest", "$DisplayWidth", "$DisplayHeight", "$DisplayRefreshRate", "$DisplayBitDepth", "$IPXRelayHost", "$IPXRelayPort"],
["KeyChange"] = ["$InstallDirectory", "$WorkingDirectory", "$ServerAddress", "$DefaultInstallDirectory", "$GameManifest", "$AllocatedKey", "$DisplayWidth", "$DisplayHeight", "$DisplayRefreshRate", "$DisplayBitDepth", "$IPXRelayHost", "$IPXRelayPort"],
["DetectInstall"] = ["$InstallDirectory", "$WorkingDirectory", "$ServerAddress", "$DefaultInstallDirectory", "$GameManifest", "$ToolManifest", "$RedistributableManifest", "$DisplayWidth", "$DisplayHeight", "$DisplayRefreshRate", "$DisplayBitDepth", "$IPXRelayHost", "$IPXRelayPort"],
["SaveUpload"] = ["$InstallDirectory", "$WorkingDirectory", "$ServerAddress", "$DefaultInstallDirectory", "$GameManifest", "$DisplayWidth", "$DisplayHeight", "$DisplayRefreshRate", "$DisplayBitDepth", "$IPXRelayHost", "$IPXRelayPort"],
["SaveDownload"] = ["$InstallDirectory", "$WorkingDirectory", "$ServerAddress", "$DefaultInstallDirectory", "$GameManifest", "$DisplayWidth", "$DisplayHeight", "$DisplayRefreshRate", "$DisplayBitDepth", "$IPXRelayHost", "$IPXRelayPort"],
["Package"] = ["$WorkingDirectory", "$Game", "$Tool", "$Redistributable"],
["GameStarted"] = ["$WorkingDirectory", "$Server", "$Game", "$User", "$ServerId", "$ServerName", "$ServerHost", "$ServerPort", "$GameTitle", "$GameId"],
["GameStopped"] = ["$WorkingDirectory", "$Server", "$Game", "$User", "$ServerId", "$ServerName", "$ServerHost", "$ServerPort", "$GameTitle", "$GameId"],
["UserRegistration"] = ["$WorkingDirectory", "$User"],
["UserLogin"] = ["$WorkingDirectory", "$User"],
["RunWrapper"] = ["$InstallDirectory", "$WorkingDirectory", "$ServerAddress", "$DefaultInstallDirectory", "$GameManifest", "$RedistributableManifest", "$ExecutablePath", "$Arguments"],
};
void UpdateVariables()
{
var key = _script.Type.ToString();
if (VariablesByScriptType.TryGetValue(key, out var vars))
_variables = vars.ToList();
else
_variables = ["$WorkingDirectory"];
}
protected override async Task OnInitializedAsync()
{
if (Options.ScriptId != Guid.Empty)
{
_script = await ScriptService.GetAsync(Options.ScriptId);
if (_script.RedistributableId.HasValue && _script.RedistributableId != Guid.Empty)
await LoadRedistributableOptions(_script.RedistributableId.Value);
}
else
{
_isNewScript = true;
var scriptType = Options.ScriptType ?? ScriptType.Install;
_script = new Script
{
Type = scriptType,
Name = scriptType.GetDisplayName(),
};
if (Options.GameId.HasValue && Options.GameId != Guid.Empty)
{
_script.GameId = Options.GameId;
_archive = await ArchiveService.GetLatestArchiveAsync(a => a.GameId == Options.GameId);
}
else if (Options.RedistributableId.HasValue && Options.RedistributableId != Guid.Empty)
{
_script.RedistributableId = Options.RedistributableId;
_archive = await ArchiveService.GetLatestArchiveAsync(a => a.RedistributableId == Options.RedistributableId);
await LoadRedistributableOptions(Options.RedistributableId.Value);
}
else if (Options.ServerId.HasValue && Options.ServerId != Guid.Empty)
{
_script.ServerId = Options.ServerId;
var server = await ServerService.GetAsync(Options.ServerId.Value);
_archive = await ArchiveService.GetLatestArchiveAsync(a => a.GameId == server.GameId);
}
else if (Options.ToolId.HasValue && Options.ToolId != Guid.Empty)
{
_script.ToolId = Options.ToolId;
_archive = await ArchiveService.GetLatestArchiveAsync(a => a.ToolId == Options.ToolId);
}
}
UpdateVariables();
Snippets = ScriptService.GetSnippets();
}
async Task OnEditorReady()
{
if (_isNewScript && _codeInput != null && Options.ScriptType.HasValue)
{
await _codeInput.InsertTemplateAsync(Options.ScriptType.Value.ToString());
}
}
public override async Task OnFeedbackOkAsync(ModalClosingEventArgs args)
{
var success = await Save();
if (success)
await base.OkCancelRefWithResult!.OnOk(_script);
else
args.Reject();
}
async Task<bool> Save()
{
try
{
if (_form.Validate())
{
if (_script.Id == Guid.Empty)
_script = await ScriptService.AddAsync(_script);
else
_script = await ScriptService.UpdateAsync(_script);
MessageService.Success("Script saved!");
return true;
}
return false;
}
catch (Exception ex)
{
MessageService.Error("Script could not be saved!");
Logger.LogError(ex, "Script could not be saved!");
return false;
}
}
async Task Debug()
{
if (!IsDebuggable)
return;
var saved = await Save();
if (!saved)
return;
if (Options.GameId.HasValue && Options.GameId != Guid.Empty)
await _console.DebugPackagingScript(Options.GameId.Value);
else if (Options.ToolId.HasValue && Options.ToolId != Guid.Empty)
await _console.DebugToolPackagingScript(Options.ToolId.Value);
else if (Options.RedistributableId.HasValue && Options.RedistributableId != Guid.Empty)
await _console.DebugRedistributablePackagingScript(Options.RedistributableId.Value);
}
async Task BrowseForPath()
{
var modalOptions = new ModalOptions()
{
Title = "Choose Reference",
Maximizable = false,
DefaultMaximized = true,
Closable = true,
OkText = "Insert File Path"
};
var browserOptions = new FilePickerOptions()
{
ArchiveId = _archive?.Id ?? Guid.Empty,
Select = true,
Multiple = false
};
var modalRef = await ModalService.CreateModalAsync<FilePickerDialog, FilePickerOptions, IEnumerable<IFileManagerEntry>>(modalOptions, browserOptions);
modalRef.OnOk = (results) =>
{
var path = results.FirstOrDefault().Path;
InsertText($"$InstallDirectory\\{path.Replace('/', '\\')}");
StateHasChanged();
return Task.CompletedTask;
};
}
async Task InsertText(string text)
{
if (_codeInput != null)
await _codeInput.InsertText(text);
}
async Task InsertSnippet(Snippet snippet)
{
if (_codeInput != null)
await _codeInput.InsertSnippet(snippet.Content);
}
async Task InsertOptionValue(string optionKey)
{
if (_codeInput == null)
return;
var text = $"$Options.{optionKey}";
if (string.IsNullOrEmpty(_script.Contents) || !_script.Contents.Contains("Get-RedistributableOptions"))
{
var boilerplate = $"$Options = Get-RedistributableOptions -Path $InstallDirectory -Id $GameManifest.Id -Name \"{_redistributableName}\"\n";
text = boilerplate + text;
}
await _codeInput.InsertText(text);
}
async Task LoadRedistributableOptions(Guid redistributableId)
{
try
{
var redistributable = await RedistributableService.GetAsync(redistributableId);
if (redistributable == null)
return;
_redistributableName = redistributable.Name ?? string.Empty;
if (!string.IsNullOrWhiteSpace(redistributable.OptionSchema))
{
var deserializer = new DeserializerBuilder()
.WithNamingConvention(PascalCaseNamingConvention.Instance)
.WithTypeConverter(new OptionChoiceYamlConverter())
.IgnoreUnmatchedProperties()
.Build();
var schema = deserializer.Deserialize<OptionSchema>(redistributable.OptionSchema);
if (schema != null)
{
_optionKeys = schema.GetFlattenedOptions().Keys.ToList();
}
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Failed to load redistributable option schema");
}
}
}