From 8a3ccaafe3a9737aeaa6b812b01eb6b3be759b72 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Sun, 27 Jul 2025 01:09:59 -0500 Subject: [PATCH] basic starscript support + a textbox that provides the starscript executed result & compiled script as well as suggestions --- Directory.Packages.props | 1 + src/Ryujinx/Ryujinx.csproj | 1 + .../Systems/Starscript/RyujinxStarscript.cs | 29 +++++ .../Systems/Starscript/StarscriptHelper.cs | 68 ++++++++++ .../Starscript/StarscriptTextBox.axaml | 21 +++ .../Starscript/StarscriptTextBox.axaml.cs | 95 ++++++++++++++ .../Starscript/StarscriptTextBoxViewModel.cs | 123 ++++++++++++++++++ .../UI/Views/Main/MainMenuBarView.axaml | 4 + .../UI/Views/Main/MainMenuBarView.axaml.cs | 3 + 9 files changed, 345 insertions(+) create mode 100644 src/Ryujinx/Systems/Starscript/RyujinxStarscript.cs create mode 100644 src/Ryujinx/Systems/Starscript/StarscriptHelper.cs create mode 100644 src/Ryujinx/Systems/Starscript/StarscriptTextBox.axaml create mode 100644 src/Ryujinx/Systems/Starscript/StarscriptTextBox.axaml.cs create mode 100644 src/Ryujinx/Systems/Starscript/StarscriptTextBoxViewModel.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 5eb7eda3a..1310a8ae4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -55,6 +55,7 @@ + diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index 480d14781..881414fb6 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -73,6 +73,7 @@ + diff --git a/src/Ryujinx/Systems/Starscript/RyujinxStarscript.cs b/src/Ryujinx/Systems/Starscript/RyujinxStarscript.cs new file mode 100644 index 000000000..9efa16178 --- /dev/null +++ b/src/Ryujinx/Systems/Starscript/RyujinxStarscript.cs @@ -0,0 +1,29 @@ +using Ryujinx.Ava.Systems.AppLibrary; +using Ryujinx.Common; +using Starscript; + +namespace Ryujinx.Ava.Systems.Starscript +{ + public static class RyujinxStarscript + { + public static readonly StarscriptHypervisor Hypervisor = StarscriptHypervisor.Create().WithStandardLibrary(true); + + static RyujinxStarscript() + { + Hypervisor.Set("ryujinx.releaseChannel", + ReleaseInformation.IsCanaryBuild + ? "Canary" + : ReleaseInformation.IsReleaseBuild + ? "Stable" + : "Custom"); + Hypervisor.Set("ryujinx.version", Program.Version); + Hypervisor.Set("appLibrary", StarscriptHelper.Wrap(RyujinxApp.MainWindow.ApplicationLibrary)); + Hypervisor.Set("currentApplication", () => + RyujinxApp.MainWindow.ApplicationLibrary.FindApplication( + RyujinxApp.MainWindow.ViewModel.AppHost?.ApplicationId ?? 0, + out ApplicationData appData) + ? StarscriptHelper.Wrap(appData) + : Value.Null); + } + } +} diff --git a/src/Ryujinx/Systems/Starscript/StarscriptHelper.cs b/src/Ryujinx/Systems/Starscript/StarscriptHelper.cs new file mode 100644 index 000000000..184b8486f --- /dev/null +++ b/src/Ryujinx/Systems/Starscript/StarscriptHelper.cs @@ -0,0 +1,68 @@ +using Gommon; +using Ryujinx.Ava.Systems.AppLibrary; +using Starscript; +using System; + +namespace Ryujinx.Ava.Systems.Starscript +{ + public static class StarscriptHelper + { + public static ValueMap Wrap(ApplicationLibrary appLib) + { + ValueMap lMap = new(); + lMap.Set("appCount", () => appLib.Applications.Count); + lMap.Set("dlcCount", () => appLib.DownloadableContents.Count); + lMap.Set("updateCount", () => appLib.TitleUpdates.Count); + lMap.Set("has", ctx => + { + ulong titleId; + + try + { + titleId = ctx.Constrain(Constraint.ExactlyOneArgument).NextString(1).ToULong(); + } + catch (FormatException) + { + throw ctx.Error( + $"Invalid input to {ctx.FormattedName}; input must be a hexadecimal number in a string."); + } + + return appLib.FindApplication(titleId, out _); + }); + lMap.Set("get", ctx => + { + ulong titleId; + + try + { + titleId = ctx.Constrain(Constraint.ExactlyOneArgument).NextString(1).ToULong(); + } + catch (FormatException) + { + throw ctx.Error( + $"Invalid input to {ctx.FormattedName}; input must be a hexadecimal number in a string."); + } + + return appLib.FindApplication(titleId, + out ApplicationData applicationData) + ? Wrap(applicationData) + : null; + }); + return lMap; + } + + public static ValueMap Wrap(ApplicationData appData) + { + ValueMap aMap = new(); + aMap.Set("name", appData.Name); + aMap.Set("version", appData.Version); + aMap.Set("developer", appData.Developer); + aMap.Set("fileExtension", appData.FileExtension); + aMap.Set("fileSize", appData.FileSizeString); + aMap.Set("hasLdnGames", appData.HasLdnGames); + aMap.Set("timePlayed", appData.TimePlayedString); + aMap.Set("isFavorite", appData.Favorite); + return aMap; + } + } +} diff --git a/src/Ryujinx/Systems/Starscript/StarscriptTextBox.axaml b/src/Ryujinx/Systems/Starscript/StarscriptTextBox.axaml new file mode 100644 index 000000000..b30dc3eff --- /dev/null +++ b/src/Ryujinx/Systems/Starscript/StarscriptTextBox.axaml @@ -0,0 +1,21 @@ + + + + + + + + + diff --git a/src/Ryujinx/Systems/Starscript/StarscriptTextBox.axaml.cs b/src/Ryujinx/Systems/Starscript/StarscriptTextBox.axaml.cs new file mode 100644 index 000000000..8a772d134 --- /dev/null +++ b/src/Ryujinx/Systems/Starscript/StarscriptTextBox.axaml.cs @@ -0,0 +1,95 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Styling; +using FluentAvalonia.UI.Controls; +using Humanizer; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Controls; +using Ryujinx.Ava.UI.Helpers; +using Starscript; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.Systems.Starscript +{ + public partial class StarscriptTextBox : RyujinxControl + { + public IReadOnlyList CurrentSuggestions => ViewModel.CurrentSuggestions; + + public string CurrentScriptSource => ViewModel.CurrentScriptSource; + public Exception Exception => ViewModel.Exception; + public Script CurrentScript => ViewModel.CurrentScript; + public StringSegment CurrentScriptResult => ViewModel.CurrentScriptResult; + + public StarscriptTextBox() + { + InitializeComponent(); + + InputBox.TextInput += HandleTextChanged; + + InputBox.AsyncPopulator = GetSuggestionsAsync; + InputBox.MinimumPopulateDelay = 0.Seconds(); + InputBox.TextFilter = (_, _) => true; + InputBox.TextSelector = (text, suggestion) => + { + if (text is not null && suggestion is null) + return text; + if (text is null && suggestion is not null) + return suggestion; + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (text is null && suggestion is null) + return string.Empty; + + var sb = new StringBuilder(text.Length + suggestion.Length + 1); + sb.Append(text); + + for (int i = 0; i < suggestion.Length - 1; i++) + { + if (text.EndsWith(suggestion[..(suggestion.Length - i - 1)])) + { + suggestion = suggestion[(suggestion.Length - i - 1)..]; + break; + } + } + + sb.Append(suggestion); + + return sb.ToString(); + }; + + Style textStyle = new(x => x.OfType().Descendant().OfType()); + textStyle.Setters.Add(new Setter(MarginProperty, new Thickness(0, 0))); + + Styles.Add(textStyle); + } + + private Task> GetSuggestionsAsync(string input, CancellationToken token) + => Task.FromResult(ViewModel.GetSuggestions(input, token)); + + private void HandleTextChanged(object sender, TextInputEventArgs eventArgs) + { + if (sender is AutoCompleteBox) + ViewModel.CurrentScriptSource = eventArgs.Text; + } + + public static StarscriptTextBox Create(StarscriptHypervisor hv) + => new() { ViewModel = new StarscriptTextBoxViewModel(hv) }; + + public static async Task Show() + { + ContentDialog contentDialog = new() + { + PrimaryButtonText = string.Empty, + SecondaryButtonText = string.Empty, + CloseButtonText = LocaleManager.Instance[LocaleKeys.UserProfilesClose], + Content = new StarscriptTextBox { ViewModel = new() } + }; + + await ContentDialogHelper.ShowAsync(contentDialog.ApplyStyles()); + } + } +} diff --git a/src/Ryujinx/Systems/Starscript/StarscriptTextBoxViewModel.cs b/src/Ryujinx/Systems/Starscript/StarscriptTextBoxViewModel.cs new file mode 100644 index 000000000..3a2cd591c --- /dev/null +++ b/src/Ryujinx/Systems/Starscript/StarscriptTextBoxViewModel.cs @@ -0,0 +1,123 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using Ryujinx.Ava.UI.ViewModels; +using Starscript; +using Starscript.Internal; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Threading; + +namespace Ryujinx.Ava.Systems.Starscript +{ + public partial class StarscriptTextBoxViewModel : BaseModel + { + private readonly StarscriptHypervisor _hv; + + public StarscriptTextBoxViewModel(StarscriptHypervisor hv = null) + { + _hv = hv ?? RyujinxStarscript.Hypervisor; + } + + public ObservableCollection CurrentSuggestions { get; } = []; + + [ObservableProperty] private bool _hasError; + [ObservableProperty] private StringSegment _currentScriptResult; + [ObservableProperty] private string _errorMessage; + private Exception _exception; + private string _currentScriptSource; + private Script _currentScript; + + public Exception Exception + { + get => _exception; + set + { + ErrorMessage = (_exception = value) switch + { + ParseException pe => pe.Error.ToString(), + StarscriptException se => se.Message, + _ => string.Empty + }; + + OnPropertyChanged(); + } + } + + public string CurrentScriptSource + { + get => _currentScriptSource; + set + { + _currentScriptSource = value; + + if (value is null) + { + CurrentScript = null; + CurrentScriptResult = null; + Exception = null; + HasError = false; + return; + } + + try + { + CurrentScript = Compiler.DirectCompile(CurrentScriptSource); + Exception = null; + HasError = false; + } + catch (ParseException pe) + { + CurrentScript = null; + CurrentScriptResult = null; + Exception = pe; + HasError = true; + } + + OnPropertyChanged(); + } + } + + public Script CurrentScript + { + get => _currentScript; + private set + { + try + { + CurrentScriptResult = value?.Execute(_hv)!; + _currentScript = value; + Exception = null; + HasError = false; + } + catch (StarscriptException se) + { + _currentScript = null; + CurrentScriptResult = null; + Exception = se; + HasError = true; + } + + OnPropertyChanged(); + } + } + + public IEnumerable GetSuggestions(string input, CancellationToken token) + { + CurrentScriptSource = input; + + _hv.GetCompletions(CurrentScriptSource, CurrentScriptSource.Length, CreateCallback(), token); + + OnPropertyChanged(nameof(CurrentSuggestions)); + + return CurrentSuggestions; + } + + private CompletionCallback CreateCallback() + { + CurrentSuggestions.Clear(); + + return (result, isFunction) => + CurrentSuggestions.Add(isFunction ? $"{result}(" : result); + } + } +} diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml index 1bce31f6d..162997430 100755 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml @@ -253,6 +253,10 @@ Header="{ext:Locale MenuBarHelpAbout}" Icon="{ext:Icon fa-solid fa-circle-info}" ToolTip.Tip="{ext:Locale OpenAboutTooltip}" /> + CompatibilityListWindow.Show()); UpdateMenuItem.Command = MainWindowViewModel.UpdateCommand; + + StarscriptDebugMenuItem.Command = Commands.Create(StarscriptTextBox.Show); FaqMenuItem.Command = SetupGuideMenuItem.Command =