diff --git a/README.md b/README.md index 53405c2c..d158a511 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,70 @@ -Serial Loops is a level editor for the Nintendo DS game Suzumiya Haruhi no Chokuretsu (The Series of Haruhi Suzumiya). +

+ Serial Loops app icon; the letters 'SL' emblazoned above four translucent gray rings within a rounded square box colored with a blue-to-green gradient along the negative X-Z axis +
+ Serial Loops +

+

+ + Azure Pipelines build status badge + + + Haroohie Translation Club Discord Server badge + + + Serial Loops documentation link badge + +

+ +**Serial Loops** is a fully-fledged editor for the Nintendo DS game, _Suzumiya Haruhi no Chokuretsu_ (The Series of Haruhi Suzumiya). + +## Screenshots +

+ Screenshot of the Serial Loops script editor, featuring the 'EV2_029' script being edited. A list of commands is displayed in a list view panel, with buttons to add, remove and clear commands, with information about the currently selected command displayed on the right. Haruhi and Tsuruya are displayed on a preview of the Nintendo DS screen. + Screenshot of the Serial Loops map editor, featuring the 'SLD1' map open with checkboxes to show/hide the camera position and collision grid + Screenshot of the Serial Loops sound editor, featuring a modal widget with a sound wave graph. Buttons to start and stop playback are present, as are sliders and a checkbox to enable looping and adjust the track loop start and end points. + Screenshot of the Serial Loops home screen. The Serial Loops logo and title sits at the top of the menu. Below that, under 'Start' on the left hand side, options to create a project, open an existing project, and modify preferences are present. An empty list of 'Recents' is visible on the right hand side, where recent projects would appear. +

## Installation -The following prerequisites need to be installed in order to use Serial Loops: +### Prerequisites +#### Installing devKitARM +[devkitARM](https://devkitpro.org/wiki/Getting_Started) is required to use Serial Loops on all platforms. -* The [.NET 6.0 runtime](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) -* [devkitARM](https://devkitpro.org/wiki/Getting_Started) - - Using the Windows graphical installer, you can simply select the devkitARM (Nintendo DS) workloads - - On macOS and Linux, run `sudo dkp-pacman -S nds-dev` from the terminal after installing the devkitPro pacman distribution. +* Using the Windows graphical installer, you can simply select the devkitARM (Nintendo DS) workloads +* On macOS and Linux, run `sudo dkp-pacman -S nds-dev` from the terminal after installing the devkitPro pacman distribution. -Additionally, on Linux, you will need to install OpenAL. On Ubuntu/Debian (which are the distros we test on), it can be installed in a single command: -``` +#### Installing OpenAL (Linux) +If you're running on Linux, you will also need to install OpenAL, needed for audio processing. On Ubuntu/Debian (which are the distros we test on), it can be installed in a single command: +```bash sudo apt install libopenal-dev ``` +### Download & Install +Once you have installed any necessary prerequisites, to install Serial Loops, download the latest release for your platform from the [Releases tab](https://github.com/haroohie-club/SerialLoops/releases). + +Be sure to [read the Serial Loops documentation](https://haroohie.club/chokuretsu/serial-loops/docs) for instructions on how to use it! + ## Bugs Please file bugs in the Issues tab in this repository. Please include the following information: * The platform you are running Serial Loops on -* The version of the Chokuretsu ROM you are using (Japanese, patched English ROM, etc.) +* The version of the _Chokuretsu_ ROM you are using (Japanese, patched English ROM, etc.) * A description of the steps required to reproduce the issue -* The relevant logs for the issue (can be found in ~/SerialLoops/Logs) \ No newline at end of file +* The relevant logs for the issue (can be found in ~/SerialLoops/Logs) + +## Development +### License +Serial Loops is licensed under the GPLv3. See [LICENSE](LICENSE) for more information. + +### Building +Serial Loops requires the .NET 6.0 SDK to build. You can download it [here](https://dotnet.microsoft.com/download/dotnet/6.0). To build Serial Loops for your platform, run: + +```bash +dotnet build src/PLATFORM +``` + +Remember to replace `PLATFORM` with the platform you're on: +* `SerialLoops.Gtk` for Linux +* `SerialLoops.Mac` for macOS +* `SerialLoops.Wpf` for Windows + +We recommend Visual Studio 2022 for development. If you'd like to contribute new features or fixes, we recommend [getting in touch on Discord first](https://discord.gg/nesRSbpeFM) before submitting a Pull Request! \ No newline at end of file diff --git a/src/SerialLoops.Lib/Items/ItemDescription.cs b/src/SerialLoops.Lib/Items/ItemDescription.cs index 40b45f98..c832a9f1 100644 --- a/src/SerialLoops.Lib/Items/ItemDescription.cs +++ b/src/SerialLoops.Lib/Items/ItemDescription.cs @@ -11,7 +11,6 @@ public class ItemDescription public bool CanRename { get; set; } public string DisplayName { get; protected set; } public string DisplayNameWithStatus => UnsavedChanges ? $"{DisplayName} *" : DisplayName; - public string SearchableText { get; set; } public ItemType Type { get; private set; } public bool UnsavedChanges { get; set; } = false; diff --git a/src/SerialLoops.Lib/Items/ScriptItem.cs b/src/SerialLoops.Lib/Items/ScriptItem.cs index a7d29ddf..7ec20ce9 100644 --- a/src/SerialLoops.Lib/Items/ScriptItem.cs +++ b/src/SerialLoops.Lib/Items/ScriptItem.cs @@ -24,19 +24,6 @@ public ScriptItem(EventFile evt, ILogger log) : base(evt.Name[0..^1], ItemType.S Event = evt; Graph.AddVertexRange(Event.ScriptSections); - - try - { - SearchableText = string.Join('\n', evt.ScriptSections.SelectMany(s => s.Objects.Select(c => c.Command.Mnemonic)) - .Concat(evt.ConditionalsSection.Objects) - .Concat(evt.LabelsSection.Objects.Select(l => l.Name))); - //.Concat(evt.DialogueLines.Select(l => l.Text))); - } - catch (Exception ex) - { - log.LogError($"Exception encountered while creating searchable text for script {Name}: {ex.Message}"); - log.Log(ex.StackTrace); - } } public Dictionary> GetScriptCommandTree(Project project, ILogger log) diff --git a/src/SerialLoops.Lib/Project.cs b/src/SerialLoops.Lib/Project.cs index 98152ec9..c52e01b4 100644 --- a/src/SerialLoops.Lib/Project.cs +++ b/src/SerialLoops.Lib/Project.cs @@ -16,6 +16,7 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; +using SerialLoops.Lib.Script.Parameters; namespace SerialLoops.Lib { @@ -531,20 +532,99 @@ public void Save() File.WriteAllText(Path.Combine(MainDirectory, $"{Name}.{PROJECT_FORMAT}"), JsonSerializer.Serialize(this, SERIALIZER_OPTIONS)); } - public List GetSearchResults(string searchTerm, bool titlesOnly = true) + public List GetSearchResults(string query, ILogger logger) { - if (titlesOnly) - { - return Items.Where(item => - item.DisplayName.Contains(searchTerm.Trim(), StringComparison.OrdinalIgnoreCase)).ToList(); - } - else + return GetSearchResults(SearchQuery.Create(query), logger); + } + + public List GetSearchResults(SearchQuery query, ILogger logger, IProgressTracker? tracker = null) + { + var term = query.Term.Trim(); + var searchable = Items.Where(i => query.Types.Contains(i.Type)).ToList(); + tracker?.Focus($"{searchable.Count} Items", searchable.Count); + + return searchable.Where(item => + { + bool hit = query.Scopes.Aggregate( + false, + (current, scope) => current || ItemMatches(item, term, scope, logger) + ); + if (tracker is not null) tracker.Finished++; + return hit; + }) + .ToList(); + } + + private bool ItemMatches(ItemDescription item, string term, SearchQuery.DataHolder scope, ILogger logger) + { + switch (scope) { - return Items.Where(item => - item.Name.Contains(searchTerm.Trim(), StringComparison.OrdinalIgnoreCase) || - item.DisplayName.Contains(searchTerm.Trim(), StringComparison.OrdinalIgnoreCase) || - (!string.IsNullOrEmpty(item.SearchableText) && - item.SearchableText.Contains(searchTerm.Trim(), StringComparison.OrdinalIgnoreCase))).ToList(); + case SearchQuery.DataHolder.Title: + return item.Name.Contains(term, StringComparison.OrdinalIgnoreCase) || + item.DisplayName.Contains(term, StringComparison.OrdinalIgnoreCase); + + case SearchQuery.DataHolder.Dialogue_Text: + if (item is ScriptItem dialogueScript) + { + if (LangCode.Equals("ja", StringComparison.OrdinalIgnoreCase)) + { + return dialogueScript.GetScriptCommandTree(this, logger) + .Any(s => s.Value.Any(c => c.Parameters + .Where(p => p.Type == ScriptParameter.ParameterType.DIALOGUE) + .Any(p => ((DialogueScriptParameter)p).Line.Text + .Contains(term, StringComparison.OrdinalIgnoreCase)))); + } + else + { + return dialogueScript.GetScriptCommandTree(this, logger) + .Any(s => s.Value.Any(c => c.Parameters + .Where(p => p.Type == ScriptParameter.ParameterType.DIALOGUE) + .Any(p => ((DialogueScriptParameter)p).Line.Text + .GetSubstitutedString(this).Contains(term, StringComparison.OrdinalIgnoreCase)))); + } + } + return false; + + case SearchQuery.DataHolder.Script_Flag: + if (item is ScriptItem flagScript) + { + return flagScript.GetScriptCommandTree(this, logger) + .Any(s => s.Value.Any(c => c.Parameters + .Where(p => p.Type == ScriptParameter.ParameterType.FLAG) + .Any(p => ((FlagScriptParameter)p).FlagName + .Contains(term, StringComparison.OrdinalIgnoreCase)))); + } + return false; + + case SearchQuery.DataHolder.Conditional: + if (item is ScriptItem conditionalScript) + { + return conditionalScript.Event.ConditionalsSection?.Objects? + .Any(c => !string.IsNullOrEmpty(c) && c.Contains(term, StringComparison.OrdinalIgnoreCase)) ?? false; + } + return false; + + case SearchQuery.DataHolder.Speaker_Name: + if (item is ScriptItem speakerScript) + { + return speakerScript.GetScriptCommandTree(this, logger) + .Any(s => s.Value.Any(c => c.Parameters + .Where(p => p.Type == ScriptParameter.ParameterType.DIALOGUE) + .Any(p => Characters[(int)((DialogueScriptParameter)p).Line.Speaker].Name + .Contains(term, StringComparison.OrdinalIgnoreCase)))); + } + return false; + + case SearchQuery.DataHolder.Background_Type: + if (item is BackgroundItem bg) + { + return bg.BackgroundType.ToString().Contains(term, StringComparison.OrdinalIgnoreCase); + } + return false; + + default: + logger.LogError($"Unimplemented search scope: {scope}"); + return false; } } diff --git a/src/SerialLoops.Lib/SearchQuery.cs b/src/SerialLoops.Lib/SearchQuery.cs new file mode 100644 index 00000000..e96ce691 --- /dev/null +++ b/src/SerialLoops.Lib/SearchQuery.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SerialLoops.Lib.Items; + +namespace SerialLoops.Lib; + +public class SearchQuery +{ + public string Term { get; set; } + public HashSet Scopes { get; set; } = new() { DataHolder.Title }; + public HashSet Types { get; set; } = Enum.GetValues().ToHashSet(); + public bool QuickSearch => !Scopes.Any(scope => (int)scope > 10); + + public enum DataHolder + { + // Quick search filters + Title = 1, + + // Deep search filters + Dialogue_Text = 11, + Script_Flag = 12, + Speaker_Name = 13, + Conditional = 14, + Background_Type = 15 + } + + public static SearchQuery Create(string text) + { + return new() { Term = text }; + } +} \ No newline at end of file diff --git a/src/SerialLoops/Controls/ItemExplorerPanel.cs b/src/SerialLoops/Controls/ItemExplorerPanel.cs index 4db5699a..c80b6bcc 100644 --- a/src/SerialLoops/Controls/ItemExplorerPanel.cs +++ b/src/SerialLoops/Controls/ItemExplorerPanel.cs @@ -30,7 +30,8 @@ public class ItemExplorerPanel : ItemListPanel private void SearchBox_TextChanged(object sender, EventArgs e) { var searchTerm = _searchBox.Text; - Items = !string.IsNullOrWhiteSpace(searchTerm) ? _project.GetSearchResults(searchTerm) : _project.Items; + ExpandItems = !string.IsNullOrEmpty(searchTerm); + Items = !string.IsNullOrEmpty(searchTerm) ? _project.GetSearchResults(searchTerm, _log) : _project.Items; } private void Viewer_SelectedItemChanged(object sender, EventArgs e) diff --git a/src/SerialLoops/Controls/ItemListPanel.cs b/src/SerialLoops/Controls/ItemListPanel.cs index 16d5557e..23a92301 100644 --- a/src/SerialLoops/Controls/ItemListPanel.cs +++ b/src/SerialLoops/Controls/ItemListPanel.cs @@ -17,7 +17,7 @@ public List Items set { _items = value; - Viewer?.SetContents(GetSections(), _expandItems); + Viewer?.SetContents(GetSections(), ExpandItems); } } public SectionListTreeGridView Viewer { get; private set; } @@ -25,20 +25,20 @@ public List Items protected ILogger _log; private readonly Size _size; private List _items; - private readonly bool _expandItems; + protected bool ExpandItems { get; set; } protected ItemListPanel(List items, Size size, bool expandItems, ILogger log) { Items = items; _log = log; _size = size; - _expandItems = expandItems; + ExpandItems = expandItems; InitializeComponent(); } void InitializeComponent() { - Viewer = new SectionListTreeGridView(GetSections(), _size, _expandItems); + Viewer = new SectionListTreeGridView(GetSections(), _size, ExpandItems); MinimumSize = _size; Padding = 0; Content = new TableLayout(Viewer.Control); diff --git a/src/SerialLoops/Dialogs/GraphicSelectionDialog.cs b/src/SerialLoops/Dialogs/GraphicSelectionDialog.cs index af34df99..6217ec55 100644 --- a/src/SerialLoops/Dialogs/GraphicSelectionDialog.cs +++ b/src/SerialLoops/Dialogs/GraphicSelectionDialog.cs @@ -18,7 +18,7 @@ public class GraphicSelectionDialog : Dialog private readonly Project _project; private readonly ILogger _log; - private TextBox _filter; + private SearchBox _filter; private ListBox _selector; private Panel _preview; @@ -37,7 +37,7 @@ private void InitializeComponent() MinimumSize = new Size(450, 400); Padding = 10; - _filter = new TextBox + _filter = new SearchBox { PlaceholderText = "Filter by name", Width = 150, diff --git a/src/SerialLoops/Dialogs/PreferencesDialog.cs b/src/SerialLoops/Dialogs/PreferencesDialog.cs index d649657e..62a5359a 100644 --- a/src/SerialLoops/Dialogs/PreferencesDialog.cs +++ b/src/SerialLoops/Dialogs/PreferencesDialog.cs @@ -2,10 +2,8 @@ using Eto.Forms; using HaruhiChokuretsuLib.Util; using SerialLoops.Lib; -using SerialLoops.Utility; using System; -using System.Collections.Generic; -using System.Linq; +using SerialLoops.Utility; namespace SerialLoops.Dialogs { @@ -18,6 +16,7 @@ public PreferencesDialog(Config config, ILogger log) { Title = "Preferences"; MinimumSize = new Size(450, 300); + Size = new Size(450, 300); Configuration = config; _log = log; @@ -124,116 +123,5 @@ private void SaveButton_Click(object sender, EventArgs e) Configuration.Save(_log); Close(); } - - internal class OptionsGroup : GroupBox - { - public OptionsGroup(string name, List