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** is a fully-fledged editor for the Nintendo DS game, _Suzumiya Haruhi no Chokuretsu_ (The Series of Haruhi Suzumiya).
+
+## Screenshots
+
+
+
+
+
+
## 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