Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions CrossPlatformUI/ViewModels/GenerateRomViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Subjects;
using System.IO;
using System.Reflection;
using System.Text.Json.Serialization;
using System.Threading;
Expand Down Expand Up @@ -96,8 +97,14 @@ async void GenerateSeed()
if(!tokenSource.IsCancellationRequested && output.success)
{
var flags = config.SerializeFlags();
var basename = $"Z2_{config.Seed}_{flags}";
var filename = basename + ".nes";
var version = Assembly.GetEntryAssembly()!.GetName().Version!;
var versionstr = $"{version.Major}.{version.Minor}.{version.Build}";
var filename = OutputFilenameFormatter.Format(config.OutputFilenameTemplate, flags, config.Seed, randomizer.Hash, version: versionstr);
var basename = Path.GetFileNameWithoutExtension(filename);
if (string.IsNullOrEmpty(basename))
{
basename = filename;
}
await files.SaveGeneratedBinaryFile(filename, output.romdata!, Main.OutputFilePath);
#if DEBUG
var debugfile = basename + ".mlb";
Expand Down
28 changes: 17 additions & 11 deletions CrossPlatformUI/ViewModels/RandomizerViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,28 @@ public class RandomizerViewModel : ReactiveValidationObject, IRoutableViewModel,
[JsonIgnore]
public BehaviorSubject<bool> FlagsValidSubject = new(true);

private bool IsFlagStringValid(string flags)
private static bool IsFlagStringValid(string flags) => FlagPasteParser.IsValidFlagString(flags);

private string flagInput = "";

[JsonIgnore]
public string FlagInput
{
try
{
_ = new RandomizerConfiguration(flags);
return true;
}
catch
get => flagInput;
set
{
return false;
var trimmedValue = value?.Trim() ?? "";
var (extractedFlags, extractedSeed) = FlagPasteParser.Parse(trimmedValue);

if (Main is not null && !string.IsNullOrEmpty(extractedSeed))
{
Main.Config.Seed = extractedSeed;
}

this.RaiseAndSetIfChanged(ref flagInput, extractedFlags ?? trimmedValue);
}
}

[JsonIgnore]
public string FlagInput { get; set { field = value.Trim(); this.RaisePropertyChanged(); } } = "";

[JsonIgnore]
public string Seed
{
Expand Down
6 changes: 6 additions & 0 deletions CrossPlatformUI/ViewModels/Tabs/SpritePreviewViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ public BeamSprites BeamSprite
set { Main.Config.BeamSprite = value; this.RaisePropertyChanged(); }
}

public string OutputFilenameTemplate
{
get => Main.Config.OutputFilenameTemplate;
set { Main.Config.OutputFilenameTemplate = value; this.RaisePropertyChanged(); }
}

public byte spriteTunicColor { get; private set; }
public byte spriteSkinTone { get; private set; }
public byte spriteOutlineColor { get; private set; }
Expand Down
2 changes: 1 addition & 1 deletion CrossPlatformUI/Views/RandomizerView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<StackPanel Orientation="Vertical">
<Grid ColumnDefinitions="2*, *, 196" RowDefinitions="Auto" Margin="0">
<StackPanel Grid.Column="0" Margin="4 8 0 8">
<TextBox UseFloatingPlaceholder="True" PlaceholderText="Paste Flags Here" p1:TextFieldAssist.Label="Flag String" Text="{Binding FlagInput}">
<TextBox Name="FlagInputTextBox" UseFloatingPlaceholder="True" PlaceholderText="Paste Flags Here" p1:TextFieldAssist.Label="Flag String" Text="{Binding FlagInput}">
<TextBox.InnerRightContent>
<Button Name="PresetsButton" ToolTip.Tip="Presets" Classes="Flat" Padding="4" Width="{Binding $self.Bounds.Height}">
<Button.Content>
Expand Down
24 changes: 24 additions & 0 deletions CrossPlatformUI/Views/RandomizerView.axaml.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Reactive.Disposables;
using System.Reactive.Disposables.Fluent;
using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using ReactiveUI;
using ReactiveUI.Avalonia;
using CrossPlatformUI.ViewModels;
Expand All @@ -20,6 +23,27 @@ public RandomizerView()
this.WhenActivated(disposables => {
if (DataContext is RandomizerViewModel vm)
{
var flagInputTextBox = this.FindControl<TextBox>("FlagInputTextBox");
if (flagInputTextBox is not null)
{
EventHandler<RoutedEventArgs> pasteHandler = (_, _) =>
{
Dispatcher.UIThread.Post(() =>
{
vm.FlagInput = flagInputTextBox.Text ?? "";
if (flagInputTextBox.Text != vm.FlagInput)
{
flagInputTextBox.Text = vm.FlagInput;
flagInputTextBox.CaretIndex = vm.FlagInput.Length;
}
}, DispatcherPriority.Background);
};

flagInputTextBox.PastingFromClipboard += pasteHandler;
Disposable.Create(() => flagInputTextBox.PastingFromClipboard -= pasteHandler)
.DisposeWith(disposables);
}

vm.SaveAsPreset.Subscribe(_ =>
{
Button presetsButton = this.FindControl<Button>("PresetsButton") ?? throw new System.Exception("Missing Required Validation Element");
Expand Down
12 changes: 12 additions & 0 deletions CrossPlatformUI/Views/Tabs/SpritePreviewView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@
SelectedItem="{Binding BeamSprite, Converter={x:Static ui:Util.EnumConvert}}">
<ToolTip.Tip><TextBlock Text="{x:Static lang:Resources.BeamSpriteToolTip}"/></ToolTip.Tip>
</ComboBox>

<Separator Margin="0 8 0 0"/>

<TextBox
Margin="0 8 0 0"
UseFloatingPlaceholder="True"
PlaceholderText="Z2-%f-%s-%h.nes"
assists:TextFieldAssist.Label="Output Filename"
Text="{Binding OutputFilenameTemplate}"
>
<ToolTip.Tip><TextBlock Text="%f = flags, %s = seed, %h = hash, %d = date/time, %v = version"/></ToolTip.Tip>
</TextBox>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Vertical">
<TextBlock>Sprite Preview</TextBlock>
Expand Down
85 changes: 85 additions & 0 deletions RandomizerCore/FlagPasteParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Z2Randomizer.RandomizerCore.Flags;

namespace Z2Randomizer.RandomizerCore;

public static class FlagPasteParser
{
private static readonly HashSet<char> ValidFlagCharacters = [.. FlagBuilder.ENCODING_TABLE, '$'];

public static string? ExtractFlags(string input) => Parse(input).Flags;

public static string? ExtractSeed(string input) => Parse(input).Seed;

public static bool IsValidFlagString(string flags) => TryNormalizeFlagString(flags, out _);

// Replace anything that isn't a valid flag string character with a space,
// tokenize on the resulting whitespace, then pick out the flags and seed.
public static (string? Flags, string? Seed) Parse(string input)
{
if (string.IsNullOrWhiteSpace(input))
{
return (null, null);
}

var cleaned = string.Concat(
input.Select(character => ValidFlagCharacters.Contains(character) ? character : ' '));
var tokens = cleaned.Split(' ', StringSplitOptions.RemoveEmptyEntries);

string? flags = null;
var flagIndex = -1;
for (var i = 0; i < tokens.Length; i++)
{
if (TryNormalizeFlagString(tokens[i], out var normalizedFlags))
{
flags = normalizedFlags;
flagIndex = i;
break;
}
}

// A seed is only meaningful alongside flags, so don't look for one until
// the flags have been found.
if (flags is null)
{
return (null, null);
}

for (var i = 0; i < tokens.Length; i++)
{
if (i != flagIndex && tokens[i].Length >= 6 && tokens[i].All(char.IsDigit))
{
return (flags, tokens[i]);
}
}

return (flags, null);
}

private static bool TryNormalizeFlagString(string flags, out string normalizedFlags)
{
normalizedFlags = string.Empty;
if (string.IsNullOrWhiteSpace(flags))
{
return false;
}

var trimmedFlags = flags.Trim();
if (trimmedFlags.Any(character => !ValidFlagCharacters.Contains(character)))
{
return false;
}

try
{
normalizedFlags = new RandomizerConfiguration(trimmedFlags).SerializeFlags();
return normalizedFlags == trimmedFlags;
}
catch (Exception)
{
return false;
}
}
}
59 changes: 59 additions & 0 deletions RandomizerCore/OutputFilenameFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;

namespace Z2Randomizer.RandomizerCore;

public static class OutputFilenameFormatter
{
public const string DefaultTemplate = "Z2-%f-%s-%h.nes";

// The characters Windows forbids in a filename. Linux and macOS only
// disallow '/' (and NUL), so honoring the Windows set produces a name that
// is valid on all three platforms.
private static readonly char[] InvalidFileNameChars =
['<', '>', ':', '"', '/', '\\', '|', '?', '*'];

public static string Format(string? template, string flags, string? seed, string hash, DateTime? timestamp = null, string? version = null)
{
var resolvedTemplate = string.IsNullOrWhiteSpace(template) ? DefaultTemplate : template;
var resolvedTimestamp = (timestamp ?? DateTime.Now).ToString("yyyy-MM-dd-HHmm", CultureInfo.InvariantCulture);
var normalizedHash = string.Concat((hash ?? "").Where(c => !char.IsWhiteSpace(c)));

var formatted = resolvedTemplate
.Replace("%d", resolvedTimestamp, StringComparison.Ordinal)
.Replace("%f", flags, StringComparison.Ordinal)
.Replace("%s", seed ?? "", StringComparison.Ordinal)
.Replace("%h", normalizedHash, StringComparison.Ordinal)
.Replace("%v", version ?? "", StringComparison.Ordinal);

return SanitizeFileName(formatted);
}

// Replaces any character that wouldn't be a valid filename on Windows,
// Linux, or macOS so a user-supplied template can never produce a name the
// OS rejects.
public static string SanitizeFileName(string fileName)
{
var builder = new StringBuilder(fileName.Length);
foreach (var character in fileName)
{
var invalid = character < ' ' || Array.IndexOf(InvalidFileNameChars, character) >= 0;
builder.Append(invalid ? '_' : character);
}

// Windows rejects names that end in a space or a dot.
var sanitized = builder.ToString().TrimEnd(' ', '.');
if (sanitized.Length == 0)
{
return "_";
}

var dot = sanitized.IndexOf('.');
var baseName = dot >= 0 ? sanitized[..dot] : sanitized;

return sanitized;
}
}
4 changes: 4 additions & 0 deletions RandomizerCore/RandomizerConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,10 @@ private bool palaceStylesAnyMetastyleSelected()
[IgnoreInFlags]
private string spriteName;

[Reactive]
[IgnoreInFlags]
private string outputFilenameTemplate = OutputFilenameFormatter.DefaultTemplate;


[Reactive]
[IgnoreInFlags]
Expand Down
Loading
Loading