Adds a new section "Scripting" in the server UI where PowerShell modules can be defined. These can be used for defining a library of functions that can be used in any script. These modules are automatically synced to the launcher and imported upon script execution.
374 lines
15 KiB
Text
374 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 ModuleService ModuleService
|
|
@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()" ModuleFunctions="@_moduleFunctions" 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<ModuleFunctionCompletion> _moduleFunctions = new();
|
|
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();
|
|
|
|
_moduleFunctions = ModuleService.GetPublicFunctionCompletions()
|
|
.Select(f => new ModuleFunctionCompletion(f.Name, string.IsNullOrWhiteSpace(f.Synopsis) ? null : f.Synopsis, f.Module))
|
|
.ToList();
|
|
}
|
|
|
|
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");
|
|
}
|
|
}
|
|
}
|