Skip to content

Commit e2b91f2

Browse files
authored
Add prototype "set quick save here" feature (#537)
1 parent 49f6a5c commit e2b91f2

21 files changed

+247
-81
lines changed

src/SerialLoops.Lib/Flags.cs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,53 +15,53 @@ public class Flags
1515

1616
public static string GetFlagNickname(int flag, Project project)
1717
{
18-
if (_names.TryGetValue(flag, out string value))
18+
if (_names.TryGetValue(flag + 1, out string value))
1919
{
2020
return $"{value} (F{flag:D2})";
2121
}
2222

23-
TopicItem topic = (TopicItem)project.Items.FirstOrDefault(i => i.Type == ItemDescription.ItemType.Topic && ((TopicItem)i).TopicEntry.Id == flag);
23+
TopicItem topic = (TopicItem)project.Items.FirstOrDefault(i => i.Type == ItemDescription.ItemType.Topic && ((TopicItem)i).TopicEntry.Id == flag + 1);
2424
if (topic is not null)
2525
{
2626
return string.Format(project.Localize("{0} Obtained (F{1:D2})"), topic.DisplayName, flag);
2727
}
2828

2929
TopicItem readTopic = (TopicItem)project.Items.FirstOrDefault(i => i.Type == ItemDescription.ItemType.Topic &&
30-
((((TopicItem)i).TopicEntry.Type == TopicType.Main && ((TopicItem)i).HiddenMainTopic is not null && ((TopicItem)i).TopicEntry.Id + 3463 == flag) ||
31-
(((TopicItem)i).TopicEntry.Type != TopicType.Main && ((TopicItem)i).TopicEntry.Id + 3451 == flag)));
30+
((((TopicItem)i).TopicEntry.Type == TopicType.Main && ((TopicItem)i).HiddenMainTopic is not null && ((TopicItem)i).TopicEntry.Id + 3463 == flag + 1) ||
31+
(((TopicItem)i).TopicEntry.Type != TopicType.Main && ((TopicItem)i).TopicEntry.Id + 3451 == flag + 1)));
3232
if (readTopic is not null)
3333
{
3434
return string.Format(project.Localize("{0} Watched in Extras (F{1:D2})"), readTopic.DisplayName, flag);
3535
}
3636

37-
BackgroundMusicItem bgm = (BackgroundMusicItem)project.Items.FirstOrDefault(i => i.Type == ItemDescription.ItemType.BGM && ((BackgroundMusicItem)i).Flag == flag);
37+
BackgroundMusicItem bgm = (BackgroundMusicItem)project.Items.FirstOrDefault(i => i.Type == ItemDescription.ItemType.BGM && ((BackgroundMusicItem)i).Flag == flag + 1);
3838
if (bgm is not null)
3939
{
4040
return string.Format(project.Localize("Listened to {0} (F{1:D2})"), bgm.DisplayName, flag);
4141
}
4242

43-
BackgroundItem bg = (BackgroundItem)project.Items.FirstOrDefault(i => i.Type == ItemDescription.ItemType.Background && ((BackgroundItem)i).Flag == flag);
44-
if (bg is not null && flag > 0)
43+
BackgroundItem bg = (BackgroundItem)project.Items.FirstOrDefault(i => i.Type == ItemDescription.ItemType.Background && ((BackgroundItem)i).Flag == flag + 1);
44+
if (bg is not null && flag + 1 > 0)
4545
{
4646
return string.Format(project.Localize("{0} ({1}) Seen (F{2:D2})"), bg.CgName, bg.DisplayName, flag);
4747
}
4848

4949
GroupSelectionItem groupSelection = (GroupSelectionItem)project.Items.FirstOrDefault(i => i.Type == ItemDescription.ItemType.Group_Selection &&
50-
((GroupSelectionItem)i).Selection.Activities.Any(a => a?.Routes.Any(r => r?.Flag == flag) ?? false));
50+
((GroupSelectionItem)i).Selection.Activities.Any(a => a?.Routes.Any(r => r?.Flag == flag + 1) ?? false));
5151
if (groupSelection is not null)
5252
{
53-
ScenarioRoute route = groupSelection.Selection.Activities.First(a => a?.Routes.Any(r => r.Flag == flag) ?? false).Routes.First(r => r.Flag == flag);
53+
ScenarioRoute route = groupSelection.Selection.Activities.First(a => a?.Routes.Any(r => r.Flag == flag + 1) ?? false).Routes.First(r => r.Flag == flag + 1);
5454
return string.Format(project.Localize("Route \"{0}\" Completed (F{1:D2})"), route.Title.GetSubstitutedString(project), flag);
5555
}
5656

5757
ScriptItem script = (ScriptItem)project.Items.FirstOrDefault(i => i.Type == ItemDescription.ItemType.Script && ((ScriptItem)i).StartReadFlag > 0 &&
58-
flag >= ((ScriptItem)i).StartReadFlag && flag < ((ScriptItem)i).StartReadFlag + ((ScriptItem)i).Event.ScriptSections.Count);
58+
flag + 1 >= ((ScriptItem)i).StartReadFlag && flag + 1 < ((ScriptItem)i).StartReadFlag + ((ScriptItem)i).Event.ScriptSections.Count);
5959
if (script is not null)
6060
{
61-
return string.Format(project.Localize("Script {0} Section {1} Completed (F{2:D2})"), script.DisplayName, script.Event.ScriptSections[flag - script.StartReadFlag].Name, flag);
61+
return string.Format(project.Localize("Script {0} Section {1} Completed (F{2:D2})"), script.DisplayName, script.Event.ScriptSections[flag + 1 - script.StartReadFlag].Name, flag);
6262
}
6363

64-
Tutorial tutorial = project.TutorialFile.Tutorials.FirstOrDefault(t => t.Id != 0 && t.Id == flag);
64+
Tutorial tutorial = project.TutorialFile.Tutorials.FirstOrDefault(t => t.Id != 0 && t.Id == flag + 1);
6565
if (tutorial is not null)
6666
{
6767
return string.Format(project.Localize("Tutorial {0} Completed (F{1:D2})"),

src/SerialLoops.Lib/Items/ScriptItem.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,13 @@ public ScriptPreview GetScriptPreview(OrderedDictionary<ScriptSection, List<Scri
228228
preview.ChessMode = true;
229229
}
230230

231+
// Get the BGM first
232+
ScriptItemCommand bgmCommand = commands.LastOrDefault(c => c.Verb == CommandVerb.BGM_PLAY);
233+
if (bgmCommand is not null && ((BgmModeScriptParameter)bgmCommand.Parameters[1]).Mode == BgmModeScriptParameter.BgmMode.Start)
234+
{
235+
preview.Bgm = ((BgmScriptParameter)bgmCommand.Parameters[0]).Bgm;
236+
}
237+
231238
// If we're in chess mode, we don't need to draw any of the top screen stuff as the screens are flipped
232239
if (!preview.ChessMode)
233240
{

src/SerialLoops.Lib/Items/TopicItem.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public class TopicItem : Item
1212

1313
public TopicItem(Topic topic, Project project) : base($"{topic.Id}", ItemType.Topic)
1414
{
15-
DisplayName = $"{topic.Id} - {topic.Title.GetSubstitutedString(project)}";
15+
DisplayName = $"{topic.Id - 1} - {topic.Title.GetSubstitutedString(project)}";
1616
CanRename = false;
1717
TopicEntry = topic;
1818
}

src/SerialLoops.Lib/Project.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using HaruhiChokuretsuLib.Util;
2020
using HaruhiChokuretsuLib.Util.Exceptions;
2121
using SerialLoops.Lib.Items;
22+
using SerialLoops.Lib.SaveFile;
2223
using SerialLoops.Lib.Script.Parameters;
2324
using SerialLoops.Lib.Util;
2425
using SkiaSharp;
@@ -111,6 +112,8 @@ public partial class Project
111112
public SKColor[] DialogueColors { get; set; } = new SKColor[16];
112113
[JsonIgnore]
113114
public SKPaint[] DialogueColorFilters { get; set; } = new SKPaint[16];
115+
[JsonIgnore]
116+
public SaveItem ProjectSaveFile { get; set; }
114117

115118
public float AverageBgmMaxAmplitude { get; set; }
116119

@@ -1319,6 +1322,18 @@ public CharacterItem GetCharacterBySpeaker(Speaker speaker)
13191322
return (CharacterItem)Items.First(i => i.Type == ItemType.Character && i.DisplayName == $"CHR_{Characters[(int)speaker].Name}");
13201323
}
13211324

1325+
public bool LoadProjectSave()
1326+
{
1327+
string savPath = Path.Combine(MainDirectory, $"{Name}.sav");
1328+
if (!File.Exists(savPath))
1329+
{
1330+
return false;
1331+
}
1332+
1333+
ProjectSaveFile = new(savPath, $"{Name}.sav");
1334+
return ProjectSaveFile is not null;
1335+
}
1336+
13221337
private bool ItemMatches(ItemDescription item, string term, SearchQuery.DataHolder scope, ILogger logger)
13231338
{
13241339
switch (scope)

src/SerialLoops.Lib/Script/ScriptItemCommand.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public class ScriptItemCommand : ReactiveObject
2525
public EventFile Script { get; set; }
2626
public int Index { get; set; }
2727
public Project Project { get; set; }
28+
public ScriptPreview CurrentPreview { get; set; }
2829

2930
public static ScriptItemCommand FromInvocation(ScriptCommandInvocation invocation, ScriptSection section, int index, EventFile eventFile, Project project, Func<string, string> localize, ILogger log)
3031
{

src/SerialLoops.Lib/Script/ScriptPreview.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public class ScriptPreview
88
{
99
public short EpisodeHeader { get; set; }
1010
public BackgroundItem Kbg { get; set; }
11+
public BackgroundMusicItem Bgm { get; set; }
1112
public PlaceItem Place { get; set; }
1213
public List<(ChibiItem Chibi, int X, int Y)> TopScreenChibis { get; set; } = [];
1314
public (int InternalYOffset, int ExternalXOffset, ChibiItem EmotingChibi) ChibiEmote { get; set; }

src/SerialLoops.Lib/Util/Extensions.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
using HaruhiChokuretsuLib.Archive.Event;
1111
using HaruhiChokuretsuLib.Archive.Graphics;
1212
using HaruhiChokuretsuLib.Font;
13+
using HaruhiChokuretsuLib.Save;
1314
using HaruhiChokuretsuLib.Util;
1415
using NAudio.Wave;
1516
using SerialLoops.Lib.Items;
17+
using SerialLoops.Lib.Script;
1618
using SerialLoops.Lib.Script.Parameters;
1719
using SkiaSharp;
1820
using SoftCircuits.Collections;
@@ -264,6 +266,47 @@ public static ScriptCommandInvocation Clone(this ScriptCommandInvocation invocat
264266
return clonedInvocation;
265267
}
266268

269+
public static void ApplyScriptPreview(this QuickSaveSlotData quickSave, ScriptPreview scriptPreview, ScriptItem script, int commandIndex, Project project, ILogger log)
270+
{
271+
quickSave.KbgIndex = (short)(scriptPreview.Kbg?.Id ?? ((BackgroundItem)project.Items.First(i => i.Type == ItemDescription.ItemType.Background && ((BackgroundItem)i).BackgroundType == BgType.KINETIC_SCREEN)).Id);
272+
quickSave.BgmIndex = (short)(scriptPreview.Bgm?.Index ?? -1);
273+
quickSave.Place = (short)(scriptPreview.Place?.Index ?? 0);
274+
if (scriptPreview.Background.BackgroundType == BgType.TEX_BG)
275+
{
276+
quickSave.BgIndex = (short)scriptPreview.Background.Id;
277+
quickSave.CgIndex = 0;
278+
}
279+
else
280+
{
281+
OrderedDictionary<ScriptSection, List<ScriptItemCommand>> commandTree = script.GetScriptCommandTree(project, log);
282+
ScriptItemCommand currentCommand = commandTree[script.Event.ScriptSections[quickSave.CurrentScriptBlock]][commandIndex];
283+
List<ScriptItemCommand> commands = currentCommand.WalkCommandGraph(commandTree, script.Graph);
284+
for (int i = commands.Count - 1; i >= 0; i--)
285+
{
286+
if (commands[i].Verb == CommandVerb.BG_DISP || commands[i].Verb == CommandVerb.BG_DISP2 || (commands[i].Verb == CommandVerb.BG_FADE && ((BgScriptParameter)commands[i].Parameters[0]).Background is not null))
287+
{
288+
quickSave.BgIndex = (short)((BgScriptParameter)commands[i].Parameters[0]).Background.Id;
289+
}
290+
}
291+
quickSave.CgIndex = (short)scriptPreview.Background.Id;
292+
}
293+
quickSave.BgPalEffect = (short)scriptPreview.BgPalEffect;
294+
quickSave.EpisodeHeader = scriptPreview.EpisodeHeader;
295+
for (int i = 1; i <= 5; i++)
296+
{
297+
if (scriptPreview.TopScreenChibis.Any(c => c.Chibi.TopScreenIndex == i))
298+
{
299+
quickSave.TopScreenChibis |= (CharacterMask)(1 << i);
300+
}
301+
}
302+
quickSave.FirstCharacterSprite = scriptPreview.Sprites.ElementAtOrDefault(0).Sprite?.Index ?? 0;
303+
quickSave.SecondCharacterSprite = scriptPreview.Sprites.ElementAtOrDefault(1).Sprite?.Index ?? 0;
304+
quickSave.ThirdCharacterSprite = scriptPreview.Sprites.ElementAtOrDefault(2).Sprite?.Index ?? 0;
305+
quickSave.Sprite1XOffset = (short)(scriptPreview.Sprites.ElementAtOrDefault(0).Positioning?.X ?? 0);
306+
quickSave.Sprite2XOffset = (short)(scriptPreview.Sprites.ElementAtOrDefault(1).Positioning?.X ?? 0);
307+
quickSave.Sprite3XOffset = (short)(scriptPreview.Sprites.ElementAtOrDefault(2).Positioning?.X ?? 0);
308+
}
309+
267310
public static void CollectGarbage(this EventFile evt)
268311
{
269312
// Collect conditional garbage

src/SerialLoops/Assets/Strings.Designer.cs

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/SerialLoops/Assets/Strings.resx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3639,6 +3639,12 @@ item in the dropdown!</value>
36393639
<data name="FramesToSecondsHelp" xml:space="preserve">
36403640
<value>The game runs at 60 frames per second, so 60 frames is equivalent to 1 second.</value>
36413641
</data>
3642+
<data name="ScriptCommandSetQuickSaveText" xml:space="preserve">
3643+
<value>Set Quick Save Here</value>
3644+
</data>
3645+
<data name="LoadSaveErrorMessage" xml:space="preserve">
3646+
<value>Unable to load project save file. Please ensure you have set your emulator to melonDS and have built and run at least once.</value>
3647+
</data>
36423648
<data name="CharacterSpriteEditorSelectBase" xml:space="preserve">
36433649
<value>Select sprite base</value>
36443650
</data>
@@ -3648,6 +3654,9 @@ item in the dropdown!</value>
36483654
<data name="CharacterSpriteEditorSelectMouthFrames" xml:space="preserve">
36493655
<value>Select mouth animation frames</value>
36503656
</data>
3657+
<data name="ScriptCommandSetQuickSaveAdvancedText" xml:space="preserve">
3658+
<value>Set Quick Save Here (Advanced)</value>
3659+
</data>
36513660
<data name="ItemLocationLeft" xml:space="preserve">
36523661
<value>Left</value>
36533662
</data>

src/SerialLoops/Models/ScriptCommandTreeItem.cs

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,25 @@
1-
using System.Collections.ObjectModel;
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.ObjectModel;
4+
using System.IO;
5+
using System.Linq;
6+
using System.Threading.Tasks;
27
using Avalonia.Controls;
38
using Avalonia.Layout;
49
using HaruhiChokuretsuLib.Archive.Event;
10+
using HaruhiChokuretsuLib.Save;
11+
using HaruhiChokuretsuLib.Util;
512
using ReactiveUI;
13+
using SerialLoops.Assets;
14+
using SerialLoops.Lib.Items;
615
using SerialLoops.Lib.Script;
16+
using SerialLoops.Lib.Script.Parameters;
17+
using SerialLoops.Lib.Util;
718
using SerialLoops.Utility;
19+
using SerialLoops.ViewModels.Dialogs;
20+
using SerialLoops.Views;
21+
using SerialLoops.Views.Dialogs;
22+
using SoftCircuits.Collections;
823

924
namespace SerialLoops.Models;
1025

@@ -23,13 +38,83 @@ public class ScriptCommandTreeItem : ITreeItem, IViewFor<ScriptItemCommand>
2338
public ObservableCollection<ITreeItem> Children { get; set; } = null;
2439
public bool IsExpanded { get; set; } = false;
2540

26-
public ScriptCommandTreeItem(ScriptItemCommand command)
41+
public ScriptCommandTreeItem(ScriptItemCommand command, ILogger log, MainWindow window)
2742
{
2843
ViewModel = command;
2944
this.OneWayBind(ViewModel, vm => vm.Display, v => v._textBlock.Text);
3045
_textBlock.VerticalAlignment = VerticalAlignment.Center;
3146
_panel.Children.Add(_textBlock);
3247
_panel[ToolTip.TipProperty] = Shared.GetScriptVerbHelp(ViewModel?.Verb ?? EventFile.CommandVerb.NOOP1);
48+
_panel.ContextMenu = new();
49+
_panel.ContextMenu.Items.Add(new MenuItem
50+
{
51+
Header = Strings.ScriptCommandSetQuickSaveText,
52+
Command = ReactiveCommand.Create(SetQuickSaveHere),
53+
});
54+
_panel.ContextMenu.Items.Add(new MenuItem
55+
{
56+
Header = Strings.ScriptCommandSetQuickSaveAdvancedText,
57+
Command = ReactiveCommand.CreateFromTask(async Task () =>
58+
{
59+
if (!SetQuickSaveHere())
60+
{
61+
return;
62+
}
63+
SaveSlotEditorDialogViewModel quickSaveEditorDialogVm = new(ViewModel.Project.ProjectSaveFile,
64+
ViewModel.Project.ProjectSaveFile!.Save.QuickSaveSlot,
65+
ViewModel.Project.ProjectSaveFile.Name, Strings.Quick_Save, ViewModel.Project, log, dontTreatAsQuickSave: true);
66+
await new SaveSlotEditorDialog { DataContext = quickSaveEditorDialogVm }.ShowDialog(window);
67+
}),
68+
});
69+
70+
return;
71+
72+
bool SetQuickSaveHere()
73+
{
74+
if (ViewModel?.Project.ProjectSaveFile is null)
75+
{
76+
if (!(ViewModel?.Project.LoadProjectSave() ?? false))
77+
{
78+
log.LogError("");
79+
return false;
80+
}
81+
}
82+
83+
ScriptItem script = (ScriptItem)ViewModel.Project.Items.First(i =>
84+
i.Type == ItemDescription.ItemType.Script && ((ScriptItem)i).Event.Index == ViewModel.Script.Index);
85+
List<ItemDescription> references = script.GetReferencesTo(ViewModel.Project);
86+
ScenarioItem scenarioRef = (ScenarioItem)references.FirstOrDefault(i => i.Type == ItemDescription.ItemType.Scenario);
87+
GroupSelectionItem groupSelectionRef = (GroupSelectionItem)references.FirstOrDefault(i => i.Type == ItemDescription.ItemType.Group_Selection);
88+
89+
QuickSaveSlotData quickSave = ViewModel.Project.ProjectSaveFile!.Save.QuickSaveSlot;
90+
quickSave.SaveTime = DateTimeOffset.Now;
91+
92+
if (scenarioRef is not null)
93+
{
94+
quickSave.ScenarioPosition = (short)(scenarioRef.Scenario.Commands.FindIndex(c =>
95+
c.Verb == ScenarioCommand.ScenarioVerb.LOAD_SCENE && c.Parameter == ViewModel.Script.Index) + 1);
96+
}
97+
else if (groupSelectionRef is not null)
98+
{
99+
quickSave.ScenarioPosition = (short)(ViewModel.Project.Scenario.Commands.FindIndex(c =>
100+
c.Verb == ScenarioCommand.ScenarioVerb.ROUTE_SELECT && c.Parameter == groupSelectionRef.Index) + 1);
101+
}
102+
else
103+
{
104+
log.LogWarning($"Unable to find references to current script '{ViewModel.Script.Name}' (0x{ViewModel.Script.Index:X3}' within other scenario. Could be dangerous!");
105+
quickSave.ScenarioPosition = 1;
106+
}
107+
quickSave.EpisodeNumber = 1;
108+
quickSave.CurrentScript = ViewModel.Script.Index;
109+
quickSave.CurrentScriptBlock = ViewModel.Script.ScriptSections.IndexOf(ViewModel.Section);
110+
quickSave.CurrentScriptCommand = ViewModel.Index;
111+
112+
quickSave.ApplyScriptPreview(ViewModel.CurrentPreview, script, ViewModel.Index, ViewModel.Project, log);
113+
114+
File.WriteAllBytes(ViewModel.Project.ProjectSaveFile.SaveLoc, ViewModel.Project.ProjectSaveFile.Save.GetBytes());
115+
116+
return true;
117+
}
33118
}
34119

35120
public Control GetDisplay()

0 commit comments

Comments
 (0)