Skip to content
Open
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
2 changes: 1 addition & 1 deletion CommandLine/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public int OnExecute()

if (Flags == null)
{
Flags = configuration.Flags;
Flags = configuration.SerializeFlags();
}
logger.Info($"Flags: {Flags}");
logger.Info($"Rom: {Rom}");
Expand Down
1 change: 1 addition & 0 deletions CommandLine/Sample.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
"ShuffleDripperEnemy": false,
"ShuffleEncounters": false,
"ShuffleEnemyHP": false,
"ShuffleBossHP": "VANILLA",
"ShuffleGP": false,
"ShuffleItemDropFrequency": true,
"ShuffleLifeExperience": false,
Expand Down
2 changes: 1 addition & 1 deletion CoreSourceGenerator/CoreSourceGenerator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
<PackageReference Include="PolySharp" Version="*">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
2 changes: 1 addition & 1 deletion CoreSourceGenerator/FlagsSerializeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ private static void GenerateReactiveProperty(StringBuilder sb, ReactiveFieldInfo
sb.AppendLine($"{indent} {{");
sb.AppendLine($"{indent} {field.FieldName} = value;");
sb.AppendLine($"{indent} OnPropertyChanged(nameof({field.PropertyName}));");
sb.AppendLine($"{indent} OnPropertyChanged(nameof(Flags));");
sb.AppendLine($"{indent} OnPropertyChanged(\"Flags\");");
sb.AppendLine($"{indent} }}");
sb.AppendLine($"{indent} }}");
sb.AppendLine($"{indent} }}{defaultValue}");
Expand Down
12 changes: 5 additions & 7 deletions CrossPlatformUI.Desktop/LocalFilePersistenceService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ namespace CrossPlatformUI.Desktop;
[RequiresUnreferencedCode("Newtonsoft.Json uses reflection")]
public class LocalFilePersistenceService : ISuspendSyncService // : ISuspensionDriver
{
// TODO put this in appdata
public const string SettingsFilename = "Settings.json";
public string? SettingsPath;

public LocalFilePersistenceService()
Expand All @@ -36,7 +34,7 @@ public LocalFilePersistenceService()

public object? LoadState()
{
var data = File.ReadAllText(SettingsFilename);
var data = File.ReadAllText(App.SETTINGS_FILENAME);
return JsonConvert.DeserializeObject<object>(data, serializerSettings);
}

Expand All @@ -46,7 +44,7 @@ public void SaveState(object state)
var next = JObject.Parse(json);
try
{
var settings = File.ReadAllText(SettingsFilename);
var settings = File.ReadAllText(App.SETTINGS_FILENAME);
var orig = JObject.Parse(settings);
orig.Merge(next, new JsonMergeSettings
{
Expand All @@ -57,7 +55,7 @@ public void SaveState(object state)
}
catch (Exception e) when (e is JsonException or IOException) { }

using var file = File.CreateText(SettingsFilename);
using var file = File.CreateText(App.SETTINGS_FILENAME);
using var writer = new JsonTextWriter(file);
next.WriteTo(writer);
}
Expand All @@ -66,12 +64,12 @@ public void InvalidateState()
{
try
{
File.Delete(SettingsFilename);
File.Delete(App.SETTINGS_FILENAME);
} catch (IOException) {}
}

public Task<IEnumerable<string>> ListLocalFiles(string path)
{
return Task.FromResult(Directory.GetFiles(path).AsEnumerable());
}
}
}
128 changes: 83 additions & 45 deletions CrossPlatformUI/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Avalonia.Markup.Xaml;
using Material.Styles.Assists;
using Microsoft.Extensions.DependencyInjection;
using Z2Randomizer.RandomizerCore;
using CrossPlatformUI.Services;
using CrossPlatformUI.ViewModels;
using CrossPlatformUI.Views;
Expand Down Expand Up @@ -57,11 +58,10 @@ public override void Initialize()

public static string Version = "";
public static string Title = "";
public const string SETTINGS_FILENAME = "Settings_v5_1.json";

public static TopLevel? TopLevel { get; private set; }

// public static MainViewModel? Main { get; set; }

private MainViewModel? main;

public override void OnFrameworkInitializationCompleted()
Expand All @@ -73,7 +73,7 @@ public override void OnFrameworkInitializationCompleted()
var files = FileSystemService!;
try
{
var json = files.OpenFileSync(IFileSystemService.RandomizerPath.Settings, "Settings.json");
var json = files.OpenFileSync(IFileSystemService.RandomizerPath.Settings, SETTINGS_FILENAME);
main = JsonSerializer.Deserialize(json, new SerializationContext(true).MainViewModel)!;
}
catch (System.IO.FileNotFoundException) { /* No settings file exists */ }
Expand Down Expand Up @@ -153,7 +153,7 @@ private Task PersistStateInternal()
{
var files = Current?.Services?.GetService<IFileSystemService>()!;
var json = JsonSerializer.Serialize(main!, SerializationContext.Default.MainViewModel);
await files.SaveFile(IFileSystemService.RandomizerPath.Settings, "Settings.json", json);
await files.SaveFile(IFileSystemService.RandomizerPath.Settings, SETTINGS_FILENAME, json);
});
}

Expand All @@ -177,78 +177,116 @@ private Task PersistStateInternal()
PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate
)]
[JsonSerializable(typeof(MainViewModel))]
[JsonSerializable(typeof(RandomizerConfiguration))]
public partial class SerializationContext : JsonSerializerContext
{
public SerializationContext(bool _) // added an argument to avoid constructors colliding
: base(CreateOptions())
: base(InitSafeOptions())
{
}

private static JsonSerializerOptions CreateOptions()
/// modifies original context - can't be called after init
private static JsonSerializerOptions InitSafeOptions()
{
var options = Default.GeneratedSerializerOptions!;
options.Converters.Add(new SafeStringEnumConverterFactory());
return options;
}

var enumNamespacePrefix = "Z2Randomizer";
/// returns a new options copy with safe serialization
public static JsonSerializerOptions CreateSafeOptions()
{
var options = new JsonSerializerOptions
{
TypeInfoResolver = SerializationContext.Default
};
options.Converters.Add(new SafeStringEnumConverterFactory());
return options;
}
}

foreach (var enumType in AppDomain.CurrentDomain.GetAssemblies()
.Where(a => !a.IsDynamic)
.SelectMany(a =>
{
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
try { return a.GetTypes(); }
#pragma warning restore IL2026
catch { return Enumerable.Empty<Type>(); }
})
.Where(t =>
t.IsEnum &&
t.IsPublic &&
!t.IsGenericTypeDefinition &&
t.Namespace != null && t.Namespace.StartsWith(enumNamespacePrefix)
))
public sealed class SafeStringEnumConverterFactory : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert)
=> typeToConvert.IsEnum;

[RequiresUnreferencedCode("Uses reflection to construct generic enum converters at runtime.")]
[SuppressMessage("Trimming", "IL2046:'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = " Justification = \"Enum converters are created dynamically; enum metadata is preserved.\")]")]
public override JsonConverter CreateConverter(
Type typeToConvert,
JsonSerializerOptions options)
{
var converterType = typeof(SafeStringEnumConverter<>)
.MakeGenericType(typeToConvert);

return (JsonConverter)Activator.CreateInstance(converterType)!;
}
}

/// Custom StringEnumConverter that returns default instead of failing for unknown values
public sealed class SafeStringEnumConverter<T> : JsonConverter<T> where T : struct, Enum
{
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
try
{
try
if (reader.TokenType == JsonTokenType.String)
{
#pragma warning disable IL2076 // 'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.
var converterType = typeof(SafeStringEnumConverter<>).MakeGenericType(enumType);
#pragma warning restore IL2076
var converter = (JsonConverter)Activator.CreateInstance(converterType)!;
options.Converters.Add(converter);
var value = reader.GetString();

if (!string.IsNullOrEmpty(value) &&
Enum.TryParse<T>(value, ignoreCase: true, out var parsed))
{
return parsed;
}
}
catch
else if (reader.TokenType == JsonTokenType.Number)
{
// we try our best to add the custom parsing, but proceed regardless
if (reader.TryGetInt32(out var raw) && Enum.IsDefined(typeof(T), raw))
{
return (T)Enum.ToObject(typeof(T), raw);
}
}
}
catch // we prefer returning the default over failing
{
}

return options;
return default;
}
}

/// Custom StringEnumConverter that returns default instead of failing for unknown values
public class SafeStringEnumConverter<T> : JsonConverter<T> where T : struct, Enum
{
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}

public override T ReadAsPropertyName(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
try
{
var value = reader.GetString();
if (Enum.TryParse<T>(value, ignoreCase: true, out var result))

if (!string.IsNullOrEmpty(value) &&
Enum.TryParse<T>(value, ignoreCase: true, out var parsed))
{
return result;
return parsed;
}
}
else if (reader.TokenType == JsonTokenType.Number &&
reader.TryGetInt32(out var intValue) &&
Enum.IsDefined(typeof(T), intValue))
catch // we prefer returning the default over failing
{
return (T)Enum.ToObject(typeof(T), intValue);
}

return default;
}

public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
public override void WriteAsPropertyName(
Utf8JsonWriter writer,
T value,
JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
writer.WritePropertyName(value.ToString());
}
}
19 changes: 8 additions & 11 deletions CrossPlatformUI/CrossPlatformUI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@
<PackageReference Include="DialogHost.Avalonia" Version="$(DialogHostAvaloniaVersion)" />
<PackageReference Include="Material.Avalonia" Version="$(MaterialAvaloniaVersion)" />
<PackageReference Include="Material.Icons.Avalonia" Version="$(MaterialIconsAvaloniaVersion)" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
<PackageReference Include="ReactiveUI.Avalonia" Version="11.3.8" />
<PackageReference Include="ReactiveUI.Validation" Version="6.0.5" />
<PackageReference Include="ReactiveUI.Validation" Version="6.0.18" />
<PackageReference Include="Aigamo.ResXGenerator" Version="4.3.0">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Xaml.Behaviors.Interactions" Version="11.3.6.6" />
<PackageReference Include="Xaml.Behaviors.Interactions" Version="11.3.9.3" />
</ItemGroup>

<ItemGroup>
Expand All @@ -51,10 +51,10 @@

<Target Name="GenerateGitInfo" BeforeTargets="BeforeBuild">
<MakeDir Directories="$(IntermediateOutputPath)" />
<Exec Command="git rev-parse --short HEAD > $(IntermediateOutputPath)git-hash.txt" />
<Exec Command="git rev-parse --abbrev-ref HEAD > $(IntermediateOutputPath)git-branch.txt" />
<Exec Command="git tag --points-at HEAD > $(IntermediateOutputPath)git-tag.txt" />
<Exec Command="git diff --quiet || echo dirty > $(IntermediateOutputPath)git-dirty.txt" IgnoreExitCode="true" />
<Exec Command="git rev-parse --short HEAD &gt; $(IntermediateOutputPath)git-hash.txt" />
<Exec Command="git rev-parse --abbrev-ref HEAD &gt; $(IntermediateOutputPath)git-branch.txt" />
<Exec Command="git tag --points-at HEAD &gt; $(IntermediateOutputPath)git-tag.txt" />
<Exec Command="git diff --quiet || echo dirty &gt; $(IntermediateOutputPath)git-dirty.txt" IgnoreExitCode="true" />

<ReadLinesFromFile File="$(IntermediateOutputPath)git-hash.txt"><Output TaskParameter="Lines" PropertyName="GitHash" /></ReadLinesFromFile>
<ReadLinesFromFile File="$(IntermediateOutputPath)git-branch.txt"><Output TaskParameter="Lines" PropertyName="GitBranch" /></ReadLinesFromFile>
Expand Down Expand Up @@ -84,10 +84,7 @@
</GitInfoText>
</PropertyGroup>

<WriteLinesToFile
File="$(IntermediateOutputPath)GitInfo.cs"
Overwrite="true"
Lines="$([System.Text.RegularExpressions.Regex]::Split('$(GitInfoText)', '\n'))" />
<WriteLinesToFile File="$(IntermediateOutputPath)GitInfo.cs" Overwrite="true" Lines="$([System.Text.RegularExpressions.Regex]::Split('$(GitInfoText)', '\n'))" />

<ItemGroup>
<Compile Include="$(IntermediateOutputPath)GitInfo.cs" />
Expand Down
21 changes: 16 additions & 5 deletions CrossPlatformUI/Lang/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,11 @@ taking damage. Play at your own risk.</value>
</data>
<data name="ShuffleEnemyHPToolTip" xml:space="preserve">
<value>If selected, each enemy type will have between 50% and 150% of its vanilla HP value.</value>
</data>
<data name="ShuffleBossHPToolTip" xml:space="preserve">
<value>Choose how boss HP is randomized. HP will always be capped at 255.

Great Palace bosses are not included.</value>
</data>
<data name="ShuffleWhichEnemiesStealEXPToolTip" xml:space="preserve">
<value>Randomly reassigns which enemies steal EXP. The number of enemies that steal EXP in each
Expand All @@ -235,13 +240,19 @@ immune in each group is the same as the number in that group that are immune in
in the dark to change.</value>
</data>
<data name="EnemyExperienceDropsToolTip" xml:space="preserve">
<value>Controls how much experience enemies drop. The possible experience values for an enemy are
<value>Controls how much experience enemies give.

The possible experience values for an enemy are:

0, 2, 3, 5, 10, 15, 20, 30, 50, 70, 100, 150, 200, 300, 500, 700, 1000.

• Vanilla: Same as the base game
• Low: Each enemy gives a random XP level between -3 and +1 levels of vanilla.
• Average: Each enemy gives a random XP level between -2 and +2 levels of vanilla.
• High: Each enemy gives a random XP level between -1 and +3 levels of vanilla.</value>
When XP is randomized, the enemy's XP value shifts left or right in this list by the rolled number of steps.

For example, a Blue Stalfos gives 50 XP in the vanilla game. If the XP is rolled -2 to +2 levels,
the minimum XP it could give is 20 and the maximum is 100.

Also note that XP values are randomized per enemy table and can differ in a single seed. For example
Red Iron Knuckle XP in P1/P2/P5 could roll down while Red Iron Knuckle XP in P3/P4/P6 rolls up.</value>
</data>
<data name="EnableHelpfulHintsToolTip" xml:space="preserve">
<value>When enabled, helpful hints are scattered throughout the world. These hints are provided
Expand Down
1 change: 1 addition & 0 deletions CrossPlatformUI/Presets/FullShufflePreset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ public static class FullShufflePreset
GeneratorsAlwaysMatch = true,

ShuffleEnemyHP = true,
ShuffleBossHP = EnemyLifeOption.MEDIUM,
ShuffleXPStealers = true,
ShuffleXPStolenAmount = true,
ShuffleSwordImmunity = true,
Expand Down
1 change: 1 addition & 0 deletions CrossPlatformUI/Presets/HardmodePreset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ public static class HardmodePreset
GeneratorsAlwaysMatch = true,

ShuffleEnemyHP = true,
ShuffleBossHP = EnemyLifeOption.VANILLA,
ShuffleXPStealers = true,
ShuffleXPStolenAmount = true,
ShuffleSwordImmunity = true,
Expand Down
1 change: 1 addition & 0 deletions CrossPlatformUI/Presets/MaxRandoPreset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ public static class MaxRandoPreset
GeneratorsAlwaysMatch = true,

ShuffleEnemyHP = true,
ShuffleBossHP = EnemyLifeOption.MEDIUM,
ShuffleXPStealers = true,
ShuffleXPStolenAmount = true,
ShuffleSwordImmunity = true,
Expand Down
1 change: 1 addition & 0 deletions CrossPlatformUI/Presets/NormalPreset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ public static class NormalPreset
GeneratorsAlwaysMatch = true,

ShuffleEnemyHP = true,
ShuffleBossHP = EnemyLifeOption.MEDIUM,
ShuffleXPStealers = true,
ShuffleXPStolenAmount = true,
ShuffleSwordImmunity = true,
Expand Down
Loading
Loading