Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[New Feature] Zenless Zone Zero Game Setings #532

Merged
merged 46 commits into from
Jul 13, 2024
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
429cb92
Initial implementation for ZZZ GSP Backend
bagusnl Jul 3, 2024
a7016ac
Preliminary frontend for Zenless Settings
bagusnl Jul 3, 2024
ca169b6
Fix crashes on the initial ZZZ settings implementation
neon-nyan Jul 3, 2024
830b83d
Merge remote-tracking branch 'origin/main' into zzz-settings
bagusnl Jul 3, 2024
c6c7b69
Some fixes
bagusnl Jul 3, 2024
bc9bd65
Fixed recursive check
bagusnl Jul 3, 2024
965cd99
Fix decode/encode
shatyuka Jul 4, 2024
1f03aa8
Adjust proper deserialization on ZZZ's GeneralData
neon-nyan Jul 8, 2024
a20a3a9
Add enums
bagusnl Jul 8, 2024
b947fa1
Implement Node to Value APIs
neon-nyan Jul 8, 2024
d048271
Fix bool casting
bagusnl Jul 8, 2024
93c46f0
Add more properties to GeneralData
bagusnl Jul 8, 2024
460f61b
forgor to set default values
bagusnl Jul 8, 2024
9c198c6
Rearrange and Finalize GeneralData type
bagusnl Jul 9, 2024
c98931f
Ensure the non existed node to use default values
neon-nyan Jul 9, 2024
1e89356
Oopsie, forgor to remove test codes
neon-nyan Jul 9, 2024
cbc1242
Fix crash while saving values
neon-nyan Jul 9, 2024
982367a
Fix potential JSON string buffer went blank under Utf8JsonWriter
neon-nyan Jul 9, 2024
0917dcd
Implement resolution selector
bagusnl Jul 9, 2024
b1a09ac
Do not modify source acH
bagusnl Jul 9, 2024
64bd009
Fix log issues
neon-nyan Jul 10, 2024
18154bf
Implement ResolutionIndex property
neon-nyan Jul 10, 2024
e75b384
Properly switch values based on fullscreen state
neon-nyan Jul 10, 2024
d7e5977
Merge branch 'main' into zzz-settings
bagusnl Jul 11, 2024
f7cf545
Only append screen cmd when using custom res
bagusnl Jul 11, 2024
5e1da85
Add docs comments
bagusnl Jul 11, 2024
86e6957
Finalize ui backend
bagusnl Jul 11, 2024
1aa0133
Frontend!
bagusnl Jul 12, 2024
30cba60
Fix preset behavior
bagusnl Jul 12, 2024
0fb49bf
Implement audio control settings
bagusnl Jul 12, 2024
4836912
Adjust margins and style
bagusnl Jul 12, 2024
0772934
Localization
bagusnl Jul 12, 2024
a8e1cd8
CodeQA
bagusnl Jul 12, 2024
e8bf71e
CodeQA pt 2
bagusnl Jul 13, 2024
3769465
Remove method to disable slider popup
bagusnl Jul 13, 2024
de2e511
Avoid RequiresUnreferencedCode for JsonNode creation
neon-nyan Jul 13, 2024
39e0a9f
Kod Ki Ei :facepalm:
neon-nyan Jul 13, 2024
0fed906
Avoid re-init on creating default values
neon-nyan Jul 13, 2024
785e9dc
Move IGameSettingsValueMagic<T> to base class
neon-nyan Jul 13, 2024
fc9f086
Use ``JsonNode.DeepEquals`` to compare values
neon-nyan Jul 13, 2024
e2010db
Fix Borderless checkbox not disabled if fullscreen is used
neon-nyan Jul 13, 2024
f5cf481
Fix QA
neon-nyan Jul 13, 2024
cc3e513
Fix build
neon-nyan Jul 13, 2024
6291521
Adding more localization strings
neon-nyan Jul 13, 2024
3f6dc10
Fix missing in-code localization
neon-nyan Jul 13, 2024
c55c27b
Remove TODO comments
neon-nyan Jul 13, 2024
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
1 change: 1 addition & 0 deletions CollapseLauncher.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,5 @@
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Shcore/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=zenless/@EntryIndexedValue">True</s:Boolean>
</wpf:ResourceDictionary>
1 change: 0 additions & 1 deletion CollapseLauncher/Classes/EventsManagement/EventsHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.Networking.Connectivity;
using static CollapseLauncher.InnerLauncherConfig;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,333 @@
using CollapseLauncher.GameSettings.Zenless;
using Hi3Helper;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;

#nullable enable
namespace CollapseLauncher.GameSettings.Base
{
public enum JsonEnumStoreType
{
AsNumber,
AsString,
AsNumberString
}

internal static class MagicNodeBaseValuesExt
{
internal static JsonObject EnsureCreatedObject(this JsonNode? node, string keyName)
{
// If the node is empty, then create a new instance of it
if (node == null)
node = new JsonObject();

// Return
return node.EnsureCreatedInner<JsonObject>(keyName);
}

internal static JsonArray EnsureCreatedArray(this JsonNode? node, string keyName)
{
// If the node is empty, then create a new instance of it
if (node == null)
node = new JsonArray();

// Return
return node.EnsureCreatedInner<JsonArray>(keyName);
}

private static T EnsureCreatedInner<T>(this JsonNode? node, string keyName)
where T : JsonNode
{
// SANITATION: Avoid creation of JsonNode directly
if (typeof(T) == typeof(JsonNode))
throw new InvalidOperationException("You cannot initialize the parent JsonNode type. Only JsonObject or JsonArray is accepted!");

// Try get if the type is an array or object
bool isTryCreateArray = typeof(T) == typeof(JsonArray);

// Set parent node as object
JsonObject? parentNodeObj = node?.AsObject();

// If the value node does not exist, then create and add a new one
if (!(parentNodeObj?.TryGetPropertyValue(keyName, out var valueNode) ?? false))
{
// Otherwise, create a new empty one.
JsonNodeOptions options = new JsonNodeOptions
{
PropertyNameCaseInsensitive = true
};
JsonNode jsonValueNode = isTryCreateArray ?
new JsonArray(options) :
new JsonObject(options);
valueNode = jsonValueNode;
parentNodeObj?.Add(new KeyValuePair<string, JsonNode?>(keyName, jsonValueNode));
}

// If the value node keeps returning null, SCREW IT!!!
if (valueNode == null)
throw new TypeInitializationException(
nameof(T),
new NullReferenceException(
$"Failed to create the type of {nameof(T)} in the parent node as it is a null!"
));

// Return object node
return (T)valueNode;
}

public static string? GetNodeValue(this JsonNode? node, string keyName, string? defaultValue)
{
// Get node as object
JsonObject? jsonObject = node?.AsObject();

// If node is null, return the default value
if (jsonObject == null) return defaultValue;

// Try get node as struct value
if (jsonObject.TryGetPropertyValue(keyName, out JsonNode? jsonNodeValue) && jsonNodeValue != null)
{
string returnValue = jsonNodeValue.AsValue().GetValue<string>();
return returnValue;
}

return defaultValue;
}

public static TValue GetNodeValue<TValue>(this JsonNode? node, string keyName, TValue defaultValue)
where TValue : struct
{
// Get node as object
JsonObject? jsonObject = node?.AsObject();

// If node is null, return the default value
if (jsonObject == null) return defaultValue;

// Try get node as struct value
if (jsonObject.TryGetPropertyValue(keyName, out JsonNode? jsonNodeValue) && jsonNodeValue != null)
{
if (typeof(TValue) == typeof(bool) && jsonNodeValue.GetValueKind() == JsonValueKind.Number)
{
// Assuming 0 is false, and any non-zero number is true
int numValue = jsonNodeValue.AsValue().GetValue<int>();
bool boolValue = numValue != 0;
return (TValue)(object)boolValue; // Cast bool to TValue
}
else
{
return jsonNodeValue.AsValue().GetValue<TValue>();
}
}

return defaultValue;
}

public static TEnum GetNodeValueEnum<TEnum>(this JsonNode? node, string keyName, TEnum defaultValue)
where TEnum : struct
{
// Get node as object
JsonObject? jsonObject = node?.AsObject();

// If node is null, return the default value
if (jsonObject == null) return defaultValue;

// Try get node as struct value
if (jsonObject.TryGetPropertyValue(keyName, out JsonNode? jsonNodeValue) && jsonNodeValue != null)
{
// Get the JsonValue representative from the node and get the kind/type
JsonValue enumValueRaw = jsonNodeValue.AsValue();
JsonValueKind enumValueRawKind = enumValueRaw.GetValueKind();

// Decide the return value
switch (enumValueRawKind)
{
case JsonValueKind.Number: // If it's a number
int enumAsInt = (int)enumValueRaw; // Cast JsonValue as int
return EnumFromInt(enumAsInt); // Cast and return it as an enum
case JsonValueKind.String: // If it's a string
string? enumAsString = (string?)enumValueRaw; // Cast JsonValue as string

if (Enum.TryParse(enumAsString, true, out TEnum enumParsedFromString)) // Try parse as a named member
return enumParsedFromString; // If successful, return the returned value

// If the string is actually a number as a string, then try parse it as int
if (int.TryParse(enumAsString, null, out int enumAsIntFromString))
return EnumFromInt(enumAsIntFromString); // Cast and return it as an enum

// Throw if all the attempts were failed
throw new InvalidDataException($"String value: {enumAsString} at key: {keyName} is not a valid member of enum: {nameof(TEnum)}");
}
}

TEnum EnumFromInt(int value) => Unsafe.As<int, TEnum>(ref value); // Unsafe casting from int to TEnum

// Otherwise, return the default value instead
return defaultValue;
}

public static void SetNodeValue<TValue>(this JsonNode? node, string keyName, TValue value)
{
// If node is null, return and ignore
if (node == null) return;

// Get node as object
JsonObject jsonObject = node.AsObject();

// Create an instance of the JSON node value
JsonValue? jsonValue = JsonValue.Create(value);

Check warning on line 182 in CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/MagicNodeBaseValues.cs

View workflow job for this annotation

GitHub Actions / build (Release, x64, net8.0-windows10.0.22621.0)

Using member 'System.Text.Json.Nodes.JsonValue.Create<T>(T, JsonNodeOptions?)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Creating JsonValue instances with non-primitive types is not compatible with trimming. It can result in non-primitive types being serialized, which may have their members trimmed. Use the overload that takes a JsonTypeInfo, or make sure all of the required types are preserved.

// If the node has object, then assign the new value
if (jsonObject.ContainsKey(keyName))
node[keyName] = jsonValue;
// Otherwise, add it
else
jsonObject.Add(new KeyValuePair<string, JsonNode?>(keyName, jsonValue));
}

public static void SetNodeValueEnum<TEnum>(this JsonNode? node, string keyName, TEnum value, JsonEnumStoreType enumStoreType = JsonEnumStoreType.AsNumber)
where TEnum : struct, Enum
{
// If node is null, return and ignore
if (node == null) return;

// Get node as object
JsonObject jsonObject = node.AsObject();

// Create an instance of the JSON node value
JsonValue? jsonValue = enumStoreType switch
{
JsonEnumStoreType.AsNumber => AsEnumNumber(value),
JsonEnumStoreType.AsString => AsEnumString(value),
JsonEnumStoreType.AsNumberString => AsEnumNumberString(value),
_ => throw new NotSupportedException($"Enum store type: {enumStoreType} is not supported!")
};

// If the node has object, then assign the new value
if (jsonObject.ContainsKey(keyName))
node[keyName] = jsonValue;
// Otherwise, add it
else
jsonObject.Add(new KeyValuePair<string, JsonNode?>(keyName, jsonValue));

JsonValue AsEnumNumber(TEnum v)
{
int enumAsNumber = Unsafe.As<TEnum, int>(ref v);
return JsonValue.Create(enumAsNumber);
}

JsonValue? AsEnumString(TEnum v)
{
string? enumName = Enum.GetName(v);
return JsonValue.Create(enumName);
}

JsonValue AsEnumNumberString(TEnum v)
{
int enumAsNumber = Unsafe.As<TEnum, int>(ref v);
string enumAsNumberString = $"{enumAsNumber}";
return JsonValue.Create(enumAsNumberString);
}
}
}

internal class MagicNodeBaseValues<T>
where T : MagicNodeBaseValues<T>, new()
{
[JsonIgnore]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
public byte[] Magic { get; protected set; }

[JsonIgnore]
protected SettingsGameVersionManager GameVersionManager { get; set; }

[JsonIgnore]
public JsonNode? SettingsJsonNode { get; protected set; }

[JsonIgnore]
public JsonSerializerContext Context { get; protected set; }
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.


[Obsolete("Loading settings with Load() is not supported for IGameSettingsValueMagic<T> member. Use LoadWithMagic() instead!", true)]
public static T Load() => throw new NotSupportedException("Loading settings with Load() is not supported for IGameSettingsValueMagic<T> member. Use LoadWithMagic() instead!");

public static T LoadWithMagic(byte[] magic, SettingsGameVersionManager versionManager, JsonSerializerContext context)
{
if (magic == null || magic.Length == 0)
throw new NullReferenceException($"Magic cannot be an empty array!");

try
{
string? filePath = versionManager.ConfigFilePath;

Check warning on line 267 in CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/MagicNodeBaseValues.cs

View workflow job for this annotation

GitHub Actions / Qodana for .NET

Variable can be declared as non-nullable

'filePath' can be declared as non-nullable

Check warning

Code scanning / QDNET

Variable can be declared as non-nullable Warning

'filePath' can be declared as non-nullable

if (!File.Exists(filePath)) throw new FileNotFoundException("MagicNodeBaseValues config file not found!");
string raw = Sleepy.ReadString(filePath, magic);

#if DEBUG
Logger.LogWriteLine($"RAW MagicNodeBaseValues Settings: {filePath}\r\n" +
$"{raw}", LogType.Debug, true);
#endif
JsonNode? node = raw.DeserializeAsJsonNode();
T data = new T();
data.InjectNodeAndMagic(node, magic, versionManager, context);
return data;
}
catch (Exception ex)
{
Logger.LogWriteLine($"Failed to parse MagicNodeBaseValues settings\r\n{ex}", LogType.Error, true);
return new T().DefaultValue(magic, versionManager, context);
}
}

public T DefaultValue(byte[] magic, SettingsGameVersionManager versionManager, JsonSerializerContext context)
{
// Generate dummy data
T data = new T();

// Generate raw JSON string
string rawJson = data.Serialize(context, false, false);

// Deserialize it back to JSON Node and inject
// the node and magic
JsonNode? defaultJsonNode = rawJson.DeserializeAsJsonNode();
data.InjectNodeAndMagic(defaultJsonNode, magic, versionManager, context);

// Return
return data;
}

public void Save()
{
// Get the file and dir path
string? filePath = GameVersionManager.ConfigFilePath;

Check warning on line 308 in CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/MagicNodeBaseValues.cs

View workflow job for this annotation

GitHub Actions / Qodana for .NET

Variable can be declared as non-nullable

'filePath' can be declared as non-nullable

Check warning

Code scanning / QDNET

Variable can be declared as non-nullable Warning

'filePath' can be declared as non-nullable
string? fileDirPath = Path.GetDirectoryName(filePath);

// Create the dir if not exist
if (string.IsNullOrEmpty(fileDirPath) && !Directory.Exists(fileDirPath))
Directory.CreateDirectory(fileDirPath!);

// Write into the file
string jsonString = SettingsJsonNode.SerializeJsonNode(Context, false, false);
Sleepy.WriteString(filePath!, jsonString, Magic);

Check warning on line 317 in CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/MagicNodeBaseValues.cs

View workflow job for this annotation

GitHub Actions / Qodana for .NET

Redundant nullable warning suppression expression

The nullable warning suppression expression is redundant

Check warning

Code scanning / QDNET

Redundant nullable warning suppression expression Warning

The nullable warning suppression expression is redundant
}

public bool Equals(GeneralData? other)
{
return true;
}

protected virtual void InjectNodeAndMagic(JsonNode? jsonNode, byte[] magic, SettingsGameVersionManager versionManager, JsonSerializerContext context)
{
SettingsJsonNode = jsonNode;
GameVersionManager = versionManager;
Magic = magic;
Context = context;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using CollapseLauncher.Interfaces;
using System.IO;

#nullable enable
namespace CollapseLauncher.GameSettings.Base
{
internal sealed class SettingsGameVersionManager
{
/// <summary>
/// Create an instance of the IGameVersionCheck adapter for <see cref="MagicNodeBaseValues{T}"/> members.<br/>
/// Note: Use the formatting string with index 0 for executable and 1 for the config filename.<br/>
/// <br/>
/// For example:<br/>
/// "{0}_Data\Persistent\LocalStorage\{1}"
/// </summary>
/// <param name="gameVersionManager">The game version manager to be injected</param>
/// <param name="fileDirPathFormat">The formatted string for the file location</param>
/// <param name="fileName">Name of the config file</param>
/// <returns><see cref="SettingsGameVersionManager"/> to be used for <see cref="MagicNodeBaseValues{T}"/> members.</returns>
internal static SettingsGameVersionManager Create(IGameVersionCheck? gameVersionManager, string fileDirPathFormat, string fileName)
{
return new SettingsGameVersionManager
{
VersionManager = gameVersionManager,
ConfigFileLocationFormat = fileDirPathFormat,
ConfigFileName = fileName
};
}

internal IGameVersionCheck? VersionManager { get; set; }

internal string? GameFolder => VersionManager?.GameDirPath;
internal string GameExecutable => Path.GetFileNameWithoutExtension(VersionManager?.GamePreset.GameExecutableName!);
internal string? ConfigFileLocationFormat { get; set; }
internal string? ConfigFileName { get; set; }
internal string ConfigFilePath { get => Path.Combine(GameFolder ?? string.Empty, string.Format(ConfigFileLocationFormat ?? string.Empty, GameExecutable, ConfigFileName)); }
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using System.Text.Json.Serialization;

namespace CollapseLauncher.GameSettings.Zenless.Context;

[JsonSourceGenerationOptions(IncludeFields = false, GenerationMode = JsonSourceGenerationMode.Metadata, IgnoreReadOnlyFields = true)]
[JsonSerializable(typeof(GeneralData))]
internal sealed partial class ZenlessSettingsJSONContext : JsonSerializerContext {}
Loading
Loading