Skip to content

Commit

Permalink
Add advanced search, fix duplicated items in menu, auto-expand quick …
Browse files Browse the repository at this point in the history
…search results, add README (#202)

Co-authored-by: jonko0493 <[email protected]>
  • Loading branch information
WiIIiam278 and jonko0493 authored Jun 14, 2023
1 parent 8d79d3f commit 97c7201
Show file tree
Hide file tree
Showing 14 changed files with 576 additions and 223 deletions.
69 changes: 59 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,70 @@
Serial Loops is a level editor for the Nintendo DS game Suzumiya Haruhi no Chokuretsu (The Series of Haruhi Suzumiya).
<h1 align="center">
<img alt="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" src="src/SerialLoops/Icons/AppIcon.png" width="135px" />
<br/>
Serial Loops
</h1>
<p align="center">
<a href="https://dev.azure.com/jonko0493/haroohie-private/_apis/build/status%2FSerialLoops-Official?branchName=main">
<img alt="Azure Pipelines build status badge" src="https://dev.azure.com/jonko0493/haroohie-private/_apis/build/status%2FSerialLoops-Official?branchName=main" />
</a>
<a href="https://discord.gg/nesRSbpeFM">
<img alt="Haroohie Translation Club Discord Server badge " src="https://img.shields.io/discord/904791358609424436.svg?label=&logo=discord&logoColor=fff&color=7389D8&labelColor=6A7EC2" />
</a>
<a href="https://haroohie.club/chokuretsu/serial-loops/docs">
<img alt="Serial Loops documentation link badge" src="https://img.shields.io/badge/docs-haroohie.club-00C4F5?logo=github" />
</a>
</p>

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

## Screenshots
<p align="center">
<img width="325px" src="https://haroohie.club/images/chokuretsu/serial-loops/script-editor.png" alt="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." />
<img width="325px" src="https://haroohie.club/images/chokuretsu/serial-loops/map-editing.png" alt="Screenshot of the Serial Loops map editor, featuring the 'SLD1' map open with checkboxes to show/hide the camera position and collision grid" />
<img width="325px" src="https://haroohie.club/images/chokuretsu/serial-loops/sound-editing.png" alt="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." />
<img width="325px" src="https://haroohie.club/images/chokuretsu/serial-loops/home-screen.png" alt="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." />
</p>

## 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)
* 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!
1 change: 0 additions & 1 deletion src/SerialLoops.Lib/Items/ItemDescription.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
13 changes: 0 additions & 13 deletions src/SerialLoops.Lib/Items/ScriptItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ScriptSection, List<ScriptItemCommand>> GetScriptCommandTree(Project project, ILogger log)
Expand Down
104 changes: 92 additions & 12 deletions src/SerialLoops.Lib/Project.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using SerialLoops.Lib.Script.Parameters;

namespace SerialLoops.Lib
{
Expand Down Expand Up @@ -531,20 +532,99 @@ public void Save()
File.WriteAllText(Path.Combine(MainDirectory, $"{Name}.{PROJECT_FORMAT}"), JsonSerializer.Serialize<Project>(this, SERIALIZER_OPTIONS));
}

public List<ItemDescription> GetSearchResults(string searchTerm, bool titlesOnly = true)
public List<ItemDescription> 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<ItemDescription> 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;
}
}

Expand Down
32 changes: 32 additions & 0 deletions src/SerialLoops.Lib/SearchQuery.cs
Original file line number Diff line number Diff line change
@@ -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<DataHolder> Scopes { get; set; } = new() { DataHolder.Title };
public HashSet<ItemDescription.ItemType> Types { get; set; } = Enum.GetValues<ItemDescription.ItemType>().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 };
}
}
3 changes: 2 additions & 1 deletion src/SerialLoops/Controls/ItemExplorerPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions src/SerialLoops/Controls/ItemListPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,28 @@ public List<ItemDescription> Items
set
{
_items = value;
Viewer?.SetContents(GetSections(), _expandItems);
Viewer?.SetContents(GetSections(), ExpandItems);
}
}
public SectionListTreeGridView Viewer { get; private set; }

protected ILogger _log;
private readonly Size _size;
private List<ItemDescription> _items;
private readonly bool _expandItems;
protected bool ExpandItems { get; set; }

protected ItemListPanel(List<ItemDescription> 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);
Expand Down
4 changes: 2 additions & 2 deletions src/SerialLoops/Dialogs/GraphicSelectionDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class GraphicSelectionDialog : Dialog<IPreviewableGraphic>
private readonly Project _project;
private readonly ILogger _log;

private TextBox _filter;
private SearchBox _filter;
private ListBox _selector;
private Panel _preview;

Expand All @@ -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,
Expand Down
Loading

0 comments on commit 97c7201

Please sign in to comment.