diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..bf1bd73
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,115 @@
+root = true
+
+# All Files
+[*]
+charset = utf-8-bom
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = true
+
+# XML Project Files
+[*.{csproj,slnx,props}]
+indent_style = space
+indent_size = 2
+
+# Code Files
+[*.cs]
+trim_trailing_whitespace = true
+indent_style = space
+indent_size = 4
+tab_width = 4
+end_of_line = crlf
+csharp_prefer_braces = when_multiline:warning
+dotnet_diagnostic.IDE0047.severity = none
+dotnet_diagnostic.IDE0048.severity = none
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggest
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggest
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggest
+dotnet_style_parentheses_in_other_operators = always_for_clarity:suggest
+csharp_indent_labels = one_less_than_current
+csharp_using_directive_placement = outside_namespace:silent
+csharp_prefer_simple_using_statement = true:suggestion
+csharp_style_namespace_declarations = block_scoped:silent
+csharp_style_prefer_method_group_conversion = true:silent
+csharp_style_prefer_top_level_statements = true:silent
+csharp_style_prefer_primary_constructors = true:suggestion
+csharp_prefer_system_threading_lock = true:suggestion
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_constructors = false:silent
+csharp_style_expression_bodied_operators = false:silent
+csharp_style_expression_bodied_properties = true:silent
+csharp_style_expression_bodied_indexers = true:silent
+csharp_style_expression_bodied_accessors = true:silent
+csharp_style_expression_bodied_lambdas = true:silent
+csharp_style_expression_bodied_local_functions = false:silent
+dotnet_diagnostic.WFO1000.severity = none
+
+[*.{cs,vb}]
+#### Naming styles ####
+
+# Naming styles
+
+dotnet_naming_style.begins_with_i.required_prefix = I
+dotnet_naming_style.begins_with_i.capitalization = pascal_case
+
+[*.{cs,vb}]
+#### Naming styles ####
+
+# Naming rules
+
+dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
+dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
+dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
+
+dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.types_should_be_pascal_case.symbols = types
+dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
+
+# Symbol specifications
+
+dotnet_naming_symbols.interface.applicable_kinds = interface
+dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interface.required_modifiers =
+
+dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
+dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types.required_modifiers =
+
+dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
+dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_field_members.required_modifiers =
+
+# Naming styles
+
+dotnet_naming_style.begins_with_i.required_prefix = I
+dotnet_naming_style.begins_with_i.required_suffix =
+dotnet_naming_style.begins_with_i.word_separator =
+dotnet_naming_style.begins_with_i.capitalization = pascal_case
+
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
+dotnet_style_prefer_auto_properties = true:silent
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+dotnet_style_explicit_tuple_names = true:suggestion
+
+# IDE0130: Namespace does not match folder structure
+dotnet_diagnostic.IDE0130.severity = none
diff --git a/.github/README-es.md b/.github/README-es.md
index e756418..04bb50a 100644
--- a/.github/README-es.md
+++ b/.github/README-es.md
@@ -11,11 +11,6 @@ Editor de partidas guardadas para Animal Crossing: New Horizons
Permite editar partidas guardadas extraídas del Nintendo Switch.
* Debes extraer las partidas guardadas de la consola tu mismo, este programa no las extrae por ti.
-## Véase también
-
-[MyHorizons](https://github.com/Cuyler36/MyHorizons) de [Cuyler36](https://github.com/Cuyler36/)
-* Algunas partes del código fueron adaptadas del proyecto MyHorizons de Cuyler36 (arriba)
-
## Otro
Dirígete a la [Wiki](https://github.com/kwsch/NHSE/wiki) para más información.
diff --git a/.github/README-fr.md b/.github/README-fr.md
index 13f9423..1611c45 100644
--- a/.github/README-fr.md
+++ b/.github/README-fr.md
@@ -10,11 +10,6 @@ NHSE
Modifiez les données de sauvegarde extraites de votre Nintendo Switch.
* Veuillez préparer vos propres données de sauvegarde. Ce programme n'extrait pas les données de sauvegarde de la console.
-## Voir également
-
-[MyHorizons](https://github.com/Cuyler36/MyHorizons) par [Cuyler36](https://github.com/Cuyler36/)
-* Une partie du code est fortement adaptée du projet de Cuyler36 ci-dessus.
-
## Autre
Consultez le [Wiki](https://github.com/kwsch/NHSE/wiki) pour plus d'informations.
diff --git a/.github/README-it.md b/.github/README-it.md
index 0fb79c3..b84098a 100644
--- a/.github/README-it.md
+++ b/.github/README-it.md
@@ -11,11 +11,6 @@ Modificatore di salvataggi per Animal Crossing: New Horizons
Puoi modificare i salvataggi esportati dalla tua Nintendo Switch.
* Esporta i tuoi salvataggi; Questo programma non può esportare direttamente i salvataggi dalla tua console.
-## Guarda Anche
-
-[MyHorizons](https://github.com/Cuyler36/MyHorizons) di [Cuyler36](https://github.com/Cuyler36/)
-* Alcune porzioni del codice sono completamente adattate dal progetto di Cuyler36.
-
## Altro
Per ulteriori informazioni, consultare la nostra [Wiki](https://github.com/kwsch/NHSE/wiki) (in inglese).
diff --git a/.github/README-jp.md b/.github/README-jp.md
index 2659547..4c52b57 100644
--- a/.github/README-jp.md
+++ b/.github/README-jp.md
@@ -11,11 +11,6 @@ NHSE
Nintendo Switch から抽出したセーブデータを編集します。
* 自分のセーブデータを用意してください。このプログラムはコンソールからセーブデータを抽出しません。
-## 参考
-
-[MyHorizons](https://github.com/Cuyler36/MyHorizons) by [Cuyler36](https://github.com/Cuyler36/)
-* コードの一部は上記の Cuyler36 のプロジェクトから大いに翻案されています。
-
## その他
詳しくは [Wiki](https://github.com/kwsch/NHSE/wiki) を参照してください。
diff --git a/.github/README-zh-CN.md b/.github/README-zh-CN.md
index f9cc385..c610e74 100644
--- a/.github/README-zh-CN.md
+++ b/.github/README-zh-CN.md
@@ -10,10 +10,6 @@ NHSE
可以编辑你从Nintendo Switch中导出的存档。
* 请自行解决存档导出问题,本程序并不能直接从你的Switch中导出存档。
-## 参见
-
-[MyHorizons](https://github.com/Cuyler36/MyHorizons) by [Cuyler36](https://github.com/Cuyler36/)
-* 代码的某些部分完全改编自Cuyler36的项目。
## 其他
diff --git a/Directory.Build.props b/Directory.Build.props
index d33a2f3..081ad4a 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,7 +1,9 @@
- 10
+ 14
+ net10.0
enable
NHSE
+ true
diff --git a/NHSE.Core/Drawing/AcreTileColor.cs b/NHSE.Core/Drawing/AcreTileColor.cs
index 2c0e10e..b422edb 100644
--- a/NHSE.Core/Drawing/AcreTileColor.cs
+++ b/NHSE.Core/Drawing/AcreTileColor.cs
@@ -1,22 +1,21 @@
using System.Drawing;
-namespace NHSE.Core
+namespace NHSE.Core;
+
+public static class AcreTileColor
{
- public static class AcreTileColor
+ public static readonly byte[] AcreTiles = ResourceUtil.GetBinaryResource("outside.bin");
+
+ public static int GetAcreTileColor(ushort acre, int x, int y)
{
- public static readonly byte[] AcreTiles = ResourceUtil.GetBinaryResource("outside.bin");
+ if (acre > (ushort)OutsideAcre.FldOutNGardenRFront00)
+ return Color.Transparent.ToArgb();
+ var baseOfs = acre * 32 * 32 * 4;
- public static int GetAcreTileColor(ushort acre, int x, int y)
- {
- if (acre > (ushort)OutsideAcre.FldOutNGardenRFront00)
- return Color.Transparent.ToArgb();
- var baseOfs = acre * 32 * 32 * 4;
-
- // 64x64
- var shift = (4 * ((y * 64) + x));
- var ofs = baseOfs + shift;
- var tile = AcreTiles[ofs];
- return CollisionUtil.Dict[tile].ToArgb();
- }
+ // 64x64
+ var shift = (4 * ((y * 64) + x));
+ var ofs = baseOfs + shift;
+ var tile = AcreTiles[ofs];
+ return CollisionUtil.Dict[tile].ToArgb();
}
-}
+}
\ No newline at end of file
diff --git a/NHSE.Core/Drawing/ColorUtil.cs b/NHSE.Core/Drawing/ColorUtil.cs
index 9e16f86..ce0e1b3 100644
--- a/NHSE.Core/Drawing/ColorUtil.cs
+++ b/NHSE.Core/Drawing/ColorUtil.cs
@@ -1,54 +1,53 @@
using System.Drawing;
using System.Linq;
-namespace NHSE.Core
+namespace NHSE.Core;
+
+public static class ColorUtil
{
- public static class ColorUtil
+ public static Color GetColor(int index)
{
- public static Color GetColor(int index)
- {
- var arr = Colors;
- if ((uint) index < arr.Length)
- return arr[index];
+ var arr = Colors;
+ if ((uint) index < arr.Length)
+ return arr[index];
- // loop back and blend with something else
- index %= arr.Length;
- var c = arr[index];
- return Blend(Color.Red, c, 0.2f);
- }
-
- private static readonly Color[] Colors = new[]
- {
- 0xD9D9D9, 0xCCD9E8, 0x7F7F7F, 0xD5D5D5, 0xF7F7F7, 0xCFCFCF, 0xB4B4B4, 0xF1F1F1,
- 0xFFFFFF, 0x7F7F7F, 0x7F7F7F, 0xB6B6B6, 0x7FBBEB, 0xFFFFFF, 0x7FB2E5, 0xF9FBFD,
- 0xDFE6ED, 0x7F7F7F, 0xFFFFF0, 0x7F7F7F, 0xF7F7F7, 0x7F7F7F, 0xE3E3E3, 0xFFFFFF,
- 0xB1B1B1, 0x7F7F7F, 0xFFFFFF, 0xF7FBFF, 0xFCF5EB, 0x7FFFFF, 0xBFFFE9, 0xF7FFFF,
- 0xFAFAED, 0xFFF1E1, 0x7F7F7F, 0xFFF5E6, 0x7F7FFF, 0xC495F0, 0xD29494, 0xEEDBC3,
- 0xAFCECF, 0xBFFF7F, 0xE8B48E, 0xFFBFA7, 0xB1CAF6, 0xFFFBED, 0xED899D, 0x7FFFFF,
- 0x7F7FC5, 0x7FC5C5, 0xDBC285, 0xD4D4D4, 0x7FB17F, 0xDEDBB5, 0xC57FC5, 0xAAB597,
- 0xFFC57F, 0xCC98E5, 0xC57F7F, 0xF4CABC, 0xC7DDC5, 0xA39EC5, 0x97A7A7, 0x7FE6E8,
- 0xC97FE9, 0xFF89C9, 0x7FDFFF, 0xB4B4B4, 0x8EC7FF, 0xD89090, 0xFFFCF7, 0x90C590,
- 0xFF7FFF, 0xEDEDED, 0xFBFBFF, 0xFFEB7F, 0xECD28F, 0xBFBFBF, 0x7FBF7F, 0xD6FF97,
- 0xF7FFF7, 0xFFB4D9, 0xE6ADAD, 0xA57FC0, 0xFFFFF7, 0xF7F2C5, 0xF2F2FC, 0xFFF7FA,
- 0xBDFD7F, 0xFFFCE6, 0xD6EBF2, 0xF7BFBF, 0xEFFFFF, 0xFCFCE8, 0xE9E9E9, 0xC7F6C7,
- 0xFFDAE0, 0xFFCFBC, 0x8FD8D4, 0xC3E6FC, 0xBBC3CC, 0xD7E1EE, 0xFFFFEF, 0x7FFF7F,
- 0x98E698, 0xFCF7F2, 0xFF7FFF, 0xBF7F7F, 0xB2E6D4, 0x7F7FE6, 0xDCAAE9, 0xC9B7ED,
- 0x9DD9B8, 0xBDB3F6, 0x7FFCCC, 0xA3E8E5, 0xE38AC2, 0x8C8CB7, 0xFAFFFC, 0xFFF1F0,
- 0xFFF1DA, 0xFFEED6, 0x7F7FBF, 0xFEFAF2, 0xBFBF7F, 0xB5C691, 0xFFD27F, 0xFFA27F,
- 0xECB7EA, 0xF6F3D4, 0xCBFDCB, 0xD7F6F6, 0xEDB7C9, 0xFFF7EA, 0xFFECDC, 0xE6C29F,
- 0xFFDFE5, 0xEECFEE, 0xD7EFF2, 0xBF7FBF, 0xFF7F7F, 0xDDC7C7, 0xA0B4F0, 0xC5A289,
- 0xFCBFB8, 0xF9D1AF, 0x96C5AB, 0xFFFAF6, 0xCFA896, 0xDFDFDF, 0xC3E6F5, 0xB4ACE6,
- 0xB7BFC7, 0xFFFCFC, 0x7FFFBF, 0xA2C0D9, 0xE8D9C5, 0x7FBFBF, 0xEBDFEB, 0xFFB1A3,
- 0x9FEFE7, 0xF6C0F6, 0xFAEED9, 0xFFFFFF, 0xFAFAFA, 0xFFFF7F, 0xCCE698, 0xF7F7F7,
- 0xFFFFFF, 0xCFCFCF, 0xDCE8F4, 0xEBF1F8, 0xF7F7F7, 0x99CCFF,
- }.Select(z => Color.FromArgb(z | -0x1000000)).ToArray();
-
- public static Color Blend(Color color, Color backColor, double amount)
- {
- byte r = (byte)((color.R * amount) + (backColor.R * (1 - amount)));
- byte g = (byte)((color.G * amount) + (backColor.G * (1 - amount)));
- byte b = (byte)((color.B * amount) + (backColor.B * (1 - amount)));
- return Color.FromArgb(r, g, b);
- }
+ // loop back and blend with something else
+ index %= arr.Length;
+ var c = arr[index];
+ return Blend(Color.Red, c, 0.2f);
}
-}
+
+ private static readonly Color[] Colors = new[]
+ {
+ 0xD9D9D9, 0xCCD9E8, 0x7F7F7F, 0xD5D5D5, 0xF7F7F7, 0xCFCFCF, 0xB4B4B4, 0xF1F1F1,
+ 0xFFFFFF, 0x7F7F7F, 0x7F7F7F, 0xB6B6B6, 0x7FBBEB, 0xFFFFFF, 0x7FB2E5, 0xF9FBFD,
+ 0xDFE6ED, 0x7F7F7F, 0xFFFFF0, 0x7F7F7F, 0xF7F7F7, 0x7F7F7F, 0xE3E3E3, 0xFFFFFF,
+ 0xB1B1B1, 0x7F7F7F, 0xFFFFFF, 0xF7FBFF, 0xFCF5EB, 0x7FFFFF, 0xBFFFE9, 0xF7FFFF,
+ 0xFAFAED, 0xFFF1E1, 0x7F7F7F, 0xFFF5E6, 0x7F7FFF, 0xC495F0, 0xD29494, 0xEEDBC3,
+ 0xAFCECF, 0xBFFF7F, 0xE8B48E, 0xFFBFA7, 0xB1CAF6, 0xFFFBED, 0xED899D, 0x7FFFFF,
+ 0x7F7FC5, 0x7FC5C5, 0xDBC285, 0xD4D4D4, 0x7FB17F, 0xDEDBB5, 0xC57FC5, 0xAAB597,
+ 0xFFC57F, 0xCC98E5, 0xC57F7F, 0xF4CABC, 0xC7DDC5, 0xA39EC5, 0x97A7A7, 0x7FE6E8,
+ 0xC97FE9, 0xFF89C9, 0x7FDFFF, 0xB4B4B4, 0x8EC7FF, 0xD89090, 0xFFFCF7, 0x90C590,
+ 0xFF7FFF, 0xEDEDED, 0xFBFBFF, 0xFFEB7F, 0xECD28F, 0xBFBFBF, 0x7FBF7F, 0xD6FF97,
+ 0xF7FFF7, 0xFFB4D9, 0xE6ADAD, 0xA57FC0, 0xFFFFF7, 0xF7F2C5, 0xF2F2FC, 0xFFF7FA,
+ 0xBDFD7F, 0xFFFCE6, 0xD6EBF2, 0xF7BFBF, 0xEFFFFF, 0xFCFCE8, 0xE9E9E9, 0xC7F6C7,
+ 0xFFDAE0, 0xFFCFBC, 0x8FD8D4, 0xC3E6FC, 0xBBC3CC, 0xD7E1EE, 0xFFFFEF, 0x7FFF7F,
+ 0x98E698, 0xFCF7F2, 0xFF7FFF, 0xBF7F7F, 0xB2E6D4, 0x7F7FE6, 0xDCAAE9, 0xC9B7ED,
+ 0x9DD9B8, 0xBDB3F6, 0x7FFCCC, 0xA3E8E5, 0xE38AC2, 0x8C8CB7, 0xFAFFFC, 0xFFF1F0,
+ 0xFFF1DA, 0xFFEED6, 0x7F7FBF, 0xFEFAF2, 0xBFBF7F, 0xB5C691, 0xFFD27F, 0xFFA27F,
+ 0xECB7EA, 0xF6F3D4, 0xCBFDCB, 0xD7F6F6, 0xEDB7C9, 0xFFF7EA, 0xFFECDC, 0xE6C29F,
+ 0xFFDFE5, 0xEECFEE, 0xD7EFF2, 0xBF7FBF, 0xFF7F7F, 0xDDC7C7, 0xA0B4F0, 0xC5A289,
+ 0xFCBFB8, 0xF9D1AF, 0x96C5AB, 0xFFFAF6, 0xCFA896, 0xDFDFDF, 0xC3E6F5, 0xB4ACE6,
+ 0xB7BFC7, 0xFFFCFC, 0x7FFFBF, 0xA2C0D9, 0xE8D9C5, 0x7FBFBF, 0xEBDFEB, 0xFFB1A3,
+ 0x9FEFE7, 0xF6C0F6, 0xFAEED9, 0xFFFFFF, 0xFAFAFA, 0xFFFF7F, 0xCCE698, 0xF7F7F7,
+ 0xFFFFFF, 0xCFCFCF, 0xDCE8F4, 0xEBF1F8, 0xF7F7F7, 0x99CCFF,
+ }.Select(z => Color.FromArgb(z | -0x1000000)).ToArray();
+
+ public static Color Blend(Color color, Color backColor, double amount)
+ {
+ byte r = (byte)((color.R * amount) + (backColor.R * (1 - amount)));
+ byte g = (byte)((color.G * amount) + (backColor.G * (1 - amount)));
+ byte b = (byte)((color.B * amount) + (backColor.B * (1 - amount)));
+ return Color.FromArgb(r, g, b);
+ }
+}
\ No newline at end of file
diff --git a/NHSE.Core/Drawing/FieldItemColor.cs b/NHSE.Core/Drawing/FieldItemColor.cs
index 1633a2a..484a3f5 100644
--- a/NHSE.Core/Drawing/FieldItemColor.cs
+++ b/NHSE.Core/Drawing/FieldItemColor.cs
@@ -1,79 +1,78 @@
using System.Drawing;
-namespace NHSE.Core
+namespace NHSE.Core;
+
+public static class FieldItemColor
{
- public static class FieldItemColor
+ public static Color GetItemColor(Item item)
{
- public static Color GetItemColor(Item item)
- {
- if (item.DisplayItemId >= Item.FieldItemMin)
- return GetItemColor60000(item);
- var kind = ItemInfo.GetItemKind(item);
- return ColorUtil.GetColor((int)kind);
- }
-
- private static Color GetItemColor60000(Item item)
- {
- var id = item.DisplayItemId;
- if (id == Item.NONE)
- return Color.Transparent;
-
- if (!FieldItemList.Items.TryGetValue(id, out var def))
- return Color.DarkGreen;
-
- var kind = def.Kind;
- if (kind.IsTree())
- return GetTreeColor(id);
- if (kind.IsFlower())
- return Color.HotPink;
- if (kind.IsWeed())
- return Color.DarkOliveGreen;
- if (kind.IsFence())
- return Color.LightCoral;
- if (kind == FieldItemKind.UnitIconHole)
- return Color.Black;
- if (kind.IsBush())
- return Color.LightGreen;
- if (kind.IsStone())
- return Color.LightGray;
-
- return Color.DarkGreen; // shouldn't reach here, but ok
- }
-
- private static Color GetTreeColor(ushort id)
- {
- if (0xEC9C <= id && id <= 0xECA0) // money tree
- return Color.Gold;
-
- return id switch
- {
- // Fruit
- 0xEA61 => Color.Red, // "PltTreeApple"
- 0xEA62 => Color.Orange, // "PltTreeOrange"
- 0xEAC8 => Color.Lime, // "PltTreePear"
- 0xEAC9 => Color.DarkRed, // "PltTreeCherry"
- 0xEACA => Color.PeachPuff, // "PltTreePeach"
-
- // Cedar
- 0xEA69 => Color.SaddleBrown, // "PltTreeCedar4"
- 0xEAB6 => Color.SaddleBrown, // "PltTreeCedar2"
- 0xEAB7 => Color.SaddleBrown, // "PltTreeCedar1"
- 0xEAB8 => Color.SaddleBrown, // "PltTreeCedar3"
-
- // Palm
- 0xEA77 => Color.LightGoldenrodYellow, // "PltTreePalm4"
- 0xEAC0 => Color.LightGoldenrodYellow, // "PltTreePalm2"
- 0xEAC1 => Color.LightGoldenrodYellow, // "PltTreePalm1"
- 0xEAC2 => Color.LightGoldenrodYellow, // "PltTreePalm3"
-
- 0xEA76 => Color.MediumSeaGreen, // "PltTreeBamboo4"
- 0xEAC4 => Color.MediumSeaGreen, // "PltTreeBamboo0"
- 0xEAC5 => Color.MediumSeaGreen, // "PltTreeBamboo2"
- 0xEAC6 => Color.MediumSeaGreen, // "PltTreeBamboo1"
- 0xEAC7 => Color.MediumSeaGreen, // "PltTreeBamboo3"
-
- _ => Color.SandyBrown,
- };
- }
+ if (item.DisplayItemId >= Item.FieldItemMin)
+ return GetItemColor60000(item);
+ var kind = ItemInfo.GetItemKind(item);
+ return ColorUtil.GetColor((int)kind);
}
-}
+
+ private static Color GetItemColor60000(Item item)
+ {
+ var id = item.DisplayItemId;
+ if (id == Item.NONE)
+ return Color.Transparent;
+
+ if (!FieldItemList.Items.TryGetValue(id, out var def))
+ return Color.DarkGreen;
+
+ var kind = def.Kind;
+ if (kind.IsTree)
+ return GetTreeColor(id);
+ if (kind.IsFlower)
+ return Color.HotPink;
+ if (kind.IsWeed)
+ return Color.DarkOliveGreen;
+ if (kind.IsFence)
+ return Color.LightCoral;
+ if (kind == FieldItemKind.UnitIconHole)
+ return Color.Black;
+ if (kind.IsBush)
+ return Color.LightGreen;
+ if (kind.IsStone)
+ return Color.LightGray;
+
+ return Color.DarkGreen; // shouldn't reach here, but ok
+ }
+
+ private static Color GetTreeColor(ushort id)
+ {
+ if (id is >= 0xEC9C and <= 0xECA0) // money tree
+ return Color.Gold;
+
+ return id switch
+ {
+ // Fruit
+ 0xEA61 => Color.Red, // "PltTreeApple"
+ 0xEA62 => Color.Orange, // "PltTreeOrange"
+ 0xEAC8 => Color.Lime, // "PltTreePear"
+ 0xEAC9 => Color.DarkRed, // "PltTreeCherry"
+ 0xEACA => Color.PeachPuff, // "PltTreePeach"
+
+ // Cedar
+ 0xEA69 => Color.SaddleBrown, // "PltTreeCedar4"
+ 0xEAB6 => Color.SaddleBrown, // "PltTreeCedar2"
+ 0xEAB7 => Color.SaddleBrown, // "PltTreeCedar1"
+ 0xEAB8 => Color.SaddleBrown, // "PltTreeCedar3"
+
+ // Palm
+ 0xEA77 => Color.LightGoldenrodYellow, // "PltTreePalm4"
+ 0xEAC0 => Color.LightGoldenrodYellow, // "PltTreePalm2"
+ 0xEAC1 => Color.LightGoldenrodYellow, // "PltTreePalm1"
+ 0xEAC2 => Color.LightGoldenrodYellow, // "PltTreePalm3"
+
+ 0xEA76 => Color.MediumSeaGreen, // "PltTreeBamboo4"
+ 0xEAC4 => Color.MediumSeaGreen, // "PltTreeBamboo0"
+ 0xEAC5 => Color.MediumSeaGreen, // "PltTreeBamboo2"
+ 0xEAC6 => Color.MediumSeaGreen, // "PltTreeBamboo1"
+ 0xEAC7 => Color.MediumSeaGreen, // "PltTreeBamboo3"
+
+ _ => Color.SandyBrown,
+ };
+ }
+}
\ No newline at end of file
diff --git a/NHSE.Core/Drawing/ItemColor.cs b/NHSE.Core/Drawing/ItemColor.cs
index 031e1da..c4c07ab 100644
--- a/NHSE.Core/Drawing/ItemColor.cs
+++ b/NHSE.Core/Drawing/ItemColor.cs
@@ -1,27 +1,26 @@
using System.Drawing;
-namespace NHSE.Core
-{
- public static class ItemColor
- {
- public static Color GetItemColor(Item item)
- {
- if (item.ItemId == Item.NONE)
- return Color.Transparent;
- var kind = ItemInfo.GetItemKind(item);
- if (kind == ItemKind.Unknown)
- return Color.LimeGreen;
- return ColorUtil.GetColor((int)kind);
- }
+namespace NHSE.Core;
- public static Color GetItemColor(ushort item)
- {
- if (item == Item.NONE)
- return Color.Transparent;
- var kind = ItemInfo.GetItemKind(item);
- if (kind == ItemKind.Unknown)
- return Color.LimeGreen;
- return ColorUtil.GetColor((int)kind);
- }
+public static class ItemColor
+{
+ public static Color GetItemColor(Item item)
+ {
+ if (item.ItemId == Item.NONE)
+ return Color.Transparent;
+ var kind = ItemInfo.GetItemKind(item);
+ if (kind == ItemKind.Unknown)
+ return Color.LimeGreen;
+ return ColorUtil.GetColor((int)kind);
}
-}
+
+ public static Color GetItemColor(ushort item)
+ {
+ if (item == Item.NONE)
+ return Color.Transparent;
+ var kind = ItemInfo.GetItemKind(item);
+ if (kind == ItemKind.Unknown)
+ return Color.LimeGreen;
+ return ColorUtil.GetColor((int)kind);
+ }
+}
\ No newline at end of file
diff --git a/NHSE.Core/Drawing/TerrainTileColor.cs b/NHSE.Core/Drawing/TerrainTileColor.cs
index c4b0fec..78a2453 100644
--- a/NHSE.Core/Drawing/TerrainTileColor.cs
+++ b/NHSE.Core/Drawing/TerrainTileColor.cs
@@ -3,240 +3,237 @@
using static NHSE.Core.TerrainUnitModel;
using static NHSE.Core.LandAngles;
-namespace NHSE.Core
+namespace NHSE.Core;
+
+public static class TerrainTileColor
{
- public static class TerrainTileColor
+ private static readonly Color River = Color.FromArgb(128, 215, 195);
+ private static readonly Color Grass = Color.ForestGreen;
+
+ public static Color GetTileColor(TerrainTile tile, int relativeX, int relativeY)
{
- private static readonly Color River = Color.FromArgb(128, 215, 195);
- private static readonly Color Grass = Color.ForestGreen;
+ if (tile.UnitModelRoad.IsRoad)
+ return GetRoadColor(tile.UnitModelRoad);
+ var baseColor = GetTileDefaultColor(tile.UnitModel, tile.LandMakingAngle, relativeX, relativeY);
+ if (tile.Elevation == 0)
+ return baseColor;
- public static Color GetTileColor(TerrainTile tile, int relativeX, int relativeY)
- {
- if (tile.UnitModelRoad.IsRoad())
- return GetRoadColor(tile.UnitModelRoad);
- var baseColor = GetTileDefaultColor(tile.UnitModel, tile.LandMakingAngle, relativeX, relativeY);
- if (tile.Elevation == 0)
- return baseColor;
-
- return ColorUtil.Blend(baseColor, Color.White, 1.4d / (tile.Elevation + 1));
- }
-
- private static Color GetRoadColor(TerrainUnitModel mdl)
- {
- if (mdl.IsRoadBrick())
- return Color.Firebrick;
- if (mdl.IsRoadDarkSoil())
- return Color.SaddleBrown;
- if (mdl.IsRoadSoil())
- return Color.Peru;
- if (mdl.IsRoadStone())
- return Color.DarkGray;
- if (mdl.IsRoadPattern())
- return Color.Ivory;
- if (mdl.IsRoadTile())
- return Color.SteelBlue;
- if (mdl.IsRoadSand())
- return Color.SandyBrown;
- return Color.BurlyWood;
- }
-
- /// Notes about rivers the number is how many sides / diagonals are water.
- private static Color GetRiverColor(TerrainUnitModel mdl, LandAngles landAngle, int relativeX, int relativeY)
- {
- return mdl switch
- {
- // River0A single "hole" of water land all sides. Rotation does nothing
- River0A when (relativeX < 4 || relativeX >= 12 || relativeY < 4 || relativeY >= 12) =>
- Grass,
- // River1A narrow channel end opening on bottom, land on other sides
- River1A => landAngle switch
- {
- Default when relativeX < 4 || relativeX >= 12 || relativeY < 4 => Grass,
- Rotate90ClockAnverse when relativeX < 4 || relativeY < 4 || relativeY >= 12 => Grass,
- Rotate180ClockAnverse when relativeX < 4 || relativeX >= 12 || relativeY >= 12 => Grass,
- Rotate270ClockAnverse when relativeY < 4 || relativeY >= 12 || relativeX >= 12 => Grass,
- _ => River
- },
- // River2A narrow water channel opening on top and bottom, land left and right
- River2A => landAngle switch
- {
- Default when relativeX is < 4 or >= 12 => Grass,
- Rotate90ClockAnverse when relativeY is >= 12 or < 4 => Grass,
- Rotate180ClockAnverse when relativeX is < 4 or >= 12 => Grass,
- Rotate270ClockAnverse when relativeY is < 4 or >= 12 => Grass,
- _ => River
- },
- // River2B narrow 45 channel angled land top left with nub bottom right
- River2B => landAngle switch
- {
- Default when IsPointInMultiTriangle(relativeX, relativeY, new(4, 15), new(0, 0), new(15, 4), new(0, 15), new(15, 0)) || IsNubOnBottomRight(relativeX, relativeY) || relativeX < 4 || relativeY < 4 => Grass,
- Rotate90ClockAnverse when IsPointInMultiTriangle(relativeX, relativeY, new(4, 0), new(0, 15), new(15, 12), new(0, 0), new(15, 15)) || IsNubOnTopRight(relativeX, relativeY) || relativeX < 4 || relativeY >= 12 => Grass,
- Rotate180ClockAnverse when IsPointInMultiTriangle(relativeX, relativeY, new(0, 12), new(15, 15), new(12, 0), new(0, 15), new(15, 0)) || IsNubOnTopLeft(relativeX, relativeY) || relativeX >= 12 || relativeY >= 12 => Grass,
- Rotate270ClockAnverse when IsPointInMultiTriangle(relativeX, relativeY, new(0, 4), new(15, 0), new(12, 15), new(0, 0), new(15, 15)) || IsNubOnBottomLeft(relativeX, relativeY) || relativeX >= 12 || relativeY < 4 => Grass,
- _ => River
- },
- // River2C narrow 90 channel corner land top left with nub bottom right
- River2C => landAngle switch
- {
- Default when relativeX < 4 || relativeY < 4 || IsNubOnBottomRight(relativeX, relativeY) => Grass,
- Rotate90ClockAnverse when relativeX < 4 || relativeY >= 12 || IsNubOnTopRight(relativeX, relativeY) => Grass,
- Rotate180ClockAnverse when relativeX >= 12 || relativeY >= 12 || IsNubOnTopLeft(relativeX, relativeY) => Grass,
- Rotate270ClockAnverse when relativeX >= 12 || relativeY < 4 || IsNubOnBottomLeft(relativeX, relativeY) => Grass,
- _ => River
- },
- // River3A narrow 3 way land left side, nub top right and bottom right
- River3A => landAngle switch
- {
- Default when relativeX < 4 || IsNubOnTopRight(relativeX, relativeY) || IsNubOnBottomRight(relativeX, relativeY) => Grass,
- Rotate90ClockAnverse when relativeY >= 12 || IsNubOnTopLeft(relativeX, relativeY) || IsNubOnTopRight(relativeX, relativeY) => Grass,
- Rotate180ClockAnverse when relativeX >= 12 || IsNubOnBottomLeft(relativeX, relativeY) || IsNubOnTopLeft(relativeX, relativeY) => Grass,
- Rotate270ClockAnverse when relativeY < 4 || IsNubOnBottomRight(relativeX, relativeY) || IsNubOnBottomLeft(relativeX, relativeY) => Grass,
- _ => River
- },
- // River3B river 45 corner angled land top left, no nub
- River3B => landAngle switch
- {
- Default when IsPointInMultiTriangle(relativeX, relativeY, new(4, 15), new(0, 0), new(15, 4), new(0, 15), new(15, 0)) => Grass,
- Rotate90ClockAnverse when IsPointInMultiTriangle(relativeX, relativeY, new(4, 0), new(0, 15), new(15, 12), new(0, 0), new(15, 15)) => Grass,
- Rotate180ClockAnverse when IsPointInMultiTriangle(relativeX, relativeY, new(0, 12), new(15, 15), new(12, 0), new(0, 15), new(15, 0)) => Grass,
- Rotate270ClockAnverse when IsPointInMultiTriangle(relativeX, relativeY, new(0, 4), new(15, 0), new(12, 15), new(0, 0), new(15, 15)) => Grass,
- _ => River
- },
- // River3C river 90 corner corner land top left, no nub
- River3C => landAngle switch
- {
- Default when relativeX < 4 || relativeY < 4 => Grass,
- Rotate90ClockAnverse when relativeX < 4 || relativeY >= 12 => Grass,
- Rotate180ClockAnverse when relativeX >= 12 || relativeY >= 12 => Grass,
- Rotate270ClockAnverse when relativeX >= 12 || relativeY < 4 => Grass,
- _ => River
- },
- // River4A river side with nub top land left side with nub top right only
- River4A => landAngle switch
- {
- Default when relativeX < 4 || IsNubOnTopRight(relativeX, relativeY) => Grass,
- Rotate90ClockAnverse when relativeY >= 12 || IsNubOnTopLeft(relativeX, relativeY) => Grass,
- Rotate180ClockAnverse when relativeX >= 12 || IsNubOnBottomLeft(relativeX, relativeY) => Grass,
- Rotate270ClockAnverse when relativeY < 4 || IsNubOnBottomRight(relativeX, relativeY) => Grass,
- _ => River
- },
- // River4B river side with nub bottom land left side with nub bottom right only
- River4B => landAngle switch
- {
- Default when relativeX < 4 || IsNubOnBottomRight(relativeX, relativeY) => Grass,
- Rotate90ClockAnverse when relativeY >= 12 || IsNubOnTopRight(relativeX, relativeY) => Grass,
- Rotate180ClockAnverse when relativeX >= 12 || IsNubOnTopLeft(relativeX, relativeY) => Grass,
- Rotate270ClockAnverse when relativeY < 4 || IsNubOnBottomLeft(relativeX, relativeY) => Grass,
- _ => River
- },
- // River4C narrow 4 way nub on all 4 corners, 4 sides water. rotation does nothing
- River4C when (IsNubOnBottomLeft(relativeX, relativeY) || IsNubOnBottomRight(relativeX, relativeY) || IsNubOnTopLeft(relativeX, relativeY) || IsNubOnTopRight(relativeX, relativeY)) => Grass,
- // River5A river corner to 2 narrow Nub on top left, top right, and bottom right. 2 narrows meet a river
- River5A => landAngle switch
- {
- Default when IsNubOnTopLeft(relativeX, relativeY) || IsNubOnTopRight(relativeX, relativeY) || IsNubOnBottomRight(relativeX, relativeY) => Grass,
- Rotate90ClockAnverse when IsNubOnBottomLeft(relativeX, relativeY) || IsNubOnTopLeft(relativeX, relativeY) || IsNubOnTopRight(relativeX, relativeY) => Grass,
- Rotate180ClockAnverse when IsNubOnBottomLeft(relativeX, relativeY) || IsNubOnBottomRight(relativeX, relativeY) || IsNubOnTopLeft(relativeX, relativeY) => Grass,
- Rotate270ClockAnverse when IsNubOnBottomLeft(relativeX, relativeY) || IsNubOnBottomRight(relativeX, relativeY) || IsNubOnTopRight(relativeX, relativeY) => Grass,
- _ => River
- },
- // River5B river side land on left side
- River5B => landAngle switch
- {
- Default when relativeX < 4 => Grass,
- Rotate90ClockAnverse when relativeY >= 12 => Grass,
- Rotate180ClockAnverse when relativeX >= 12 => Grass,
- Rotate270ClockAnverse when relativeY < 4 => Grass,
- _ => River
- },
- // River6A river 2 opposing nubs nub on top left and bottom right
- River6A => landAngle switch
- {
- Default when IsNubOnTopLeft(relativeX, relativeY) || IsNubOnBottomRight(relativeX, relativeY) => Grass,
- Rotate90ClockAnverse when IsNubOnBottomLeft(relativeX, relativeY) || IsNubOnTopRight(relativeX, relativeY) => Grass,
- Rotate180ClockAnverse when IsNubOnTopLeft(relativeX, relativeY) || IsNubOnBottomRight(relativeX, relativeY) => Grass,
- Rotate270ClockAnverse when IsNubOnBottomLeft(relativeX, relativeY) || IsNubOnTopRight(relativeX, relativeY) => Grass,
- _ => River
- },
- // River6B river 2 nubs same side nub on bottom left and bottom right corner, where 1 narrow meets river bottom side
- River6B => landAngle switch
- {
- Default when IsNubOnBottomLeft(relativeX, relativeY) || IsNubOnBottomRight(relativeX, relativeY) => Grass,
- Rotate90ClockAnverse when IsNubOnBottomRight(relativeX, relativeY) || IsNubOnTopRight(relativeX, relativeY) => Grass,
- Rotate180ClockAnverse when IsNubOnTopRight(relativeX, relativeY) || IsNubOnTopLeft(relativeX, relativeY) => Grass,
- Rotate270ClockAnverse when IsNubOnTopLeft(relativeX, relativeY) || IsNubOnBottomLeft(relativeX, relativeY) => Grass,
- _ => River
- },
- // River7A river 1 nub nub on bottom left corner, fills gaps of diagonal bank
- River7A => landAngle switch
- {
- Default when IsNubOnBottomLeft(relativeX, relativeY) => Grass,
- Rotate90ClockAnverse when IsNubOnBottomRight(relativeX, relativeY) => Grass,
- Rotate180ClockAnverse when IsNubOnTopRight(relativeX, relativeY) => Grass,
- Rotate270ClockAnverse when IsNubOnTopLeft(relativeX, relativeY) => Grass,
- _ => River
- },
- // River8A river is no land, just water. Rotation doesn't matter
- River8A => River,
- _ => River
- };
- }
-
- private static bool IsNubOnTopLeft(int relativeX, int relativeY) => IsPointInTriangle(relativeX, relativeY, new(0, 4), new(0, 0), new(4, 0));
- private static bool IsNubOnTopRight(int relativeX, int relativeY) => IsPointInTriangle(relativeX, relativeY, new(12, 0), new(15, 0), new(15, 4));
- private static bool IsNubOnBottomLeft(int relativeX, int relativeY) => IsPointInTriangle(relativeX, relativeY, new(0, 12), new(0, 15), new(4, 15));
- private static bool IsNubOnBottomRight(int relativeX, int relativeY) => IsPointInTriangle(relativeX, relativeY, new(12, 15), new(15, 15), new(15, 12));
-
- private static bool IsPointInMultiTriangle(int px, int py, Coordinate a, Coordinate b, Coordinate c, Coordinate vortexA, Coordinate vortexB)
- {
- return IsPointInTriangle(px, py, a, vortexA, b)
- || IsPointInTriangle(px, py, a, b, c)
- || IsPointInTriangle(px, py, c, b, vortexB);
- }
-
- private static bool IsPointInTriangle(int px, int py, Coordinate a, Coordinate b, Coordinate c)
- {
- Coordinate p = new(px, py);
- float areaTotal = GetTriangleArea(a, b, c);
- float area1 = GetTriangleArea(p, b, c);
- float area2 = GetTriangleArea(a, p, c);
- float area3 = GetTriangleArea(a, b, p);
-
- return Math.Abs(areaTotal - (area1 + area2 + area3)) < 0.0001f;
- }
-
- private static float GetTriangleArea(Coordinate A, Coordinate B, Coordinate C)
- {
- return Math.Abs((A.X * (B.Y - C.Y) +
- B.X * (C.Y - A.Y) +
- C.X * (A.Y - B.Y)) / 2.0f);
- }
-
- private readonly record struct Coordinate(int X, int Y);
-
- private static readonly Color CliffBase = ColorUtil.Blend(Grass, Color.Black, 0.6d);
-
- private static Color GetTileDefaultColor(TerrainUnitModel mdl, ushort landAngle, int relativeX, int relativeY)
- {
- var angle = (LandAngles)landAngle;
- if (mdl.IsRiver())
- return GetRiverColor(mdl, angle, relativeX, relativeY);
- if (mdl.IsFall())
- return Color.DeepSkyBlue;
- if (mdl.IsCliff())
- return CliffBase;
- return Grass;
- }
-
- private static readonly char[] Numbers = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
-
- public static string GetTileName(TerrainTile tile)
- {
- var name = tile.UnitModel.ToString();
- var num = name.IndexOfAny(Numbers);
- if (num < 0)
- return name;
- return name.Substring(0, num) + Environment.NewLine + name.Substring(num);
- }
+ return ColorUtil.Blend(baseColor, Color.White, 1.4d / (tile.Elevation + 1));
}
-}
+
+ private static Color GetRoadColor(TerrainUnitModel mdl)
+ {
+ if (mdl.IsRoadBrick)
+ return Color.Firebrick;
+ if (mdl.IsRoadDarkSoil)
+ return Color.SaddleBrown;
+ if (mdl.IsRoadSoil)
+ return Color.Peru;
+ if (mdl.IsRoadStone)
+ return Color.DarkGray;
+ if (mdl.IsRoadPattern)
+ return Color.Ivory;
+ if (mdl.IsRoadTile)
+ return Color.SteelBlue;
+ if (mdl.IsRoadSand)
+ return Color.SandyBrown;
+ return Color.BurlyWood;
+ }
+
+ /// Notes about rivers the number is how many sides / diagonals are water.
+ private static Color GetRiverColor(TerrainUnitModel mdl, LandAngles landAngle, int relativeX, int relativeY)
+ {
+ return mdl switch
+ {
+ // River0A single "hole" of water land all sides. Rotation does nothing
+ River0A when (relativeX < 4 || relativeX >= 12 || relativeY < 4 || relativeY >= 12) =>
+ Grass,
+ // River1A narrow channel end opening on bottom, land on other sides
+ River1A => landAngle switch
+ {
+ Default when relativeX < 4 || relativeX >= 12 || relativeY < 4 => Grass,
+ Rotate90ClockAnverse when relativeX < 4 || relativeY < 4 || relativeY >= 12 => Grass,
+ Rotate180ClockAnverse when relativeX < 4 || relativeX >= 12 || relativeY >= 12 => Grass,
+ Rotate270ClockAnverse when relativeY < 4 || relativeY >= 12 || relativeX >= 12 => Grass,
+ _ => River
+ },
+ // River2A narrow water channel opening on top and bottom, land left and right
+ River2A => landAngle switch
+ {
+ Default when relativeX is < 4 or >= 12 => Grass,
+ Rotate90ClockAnverse when relativeY is >= 12 or < 4 => Grass,
+ Rotate180ClockAnverse when relativeX is < 4 or >= 12 => Grass,
+ Rotate270ClockAnverse when relativeY is < 4 or >= 12 => Grass,
+ _ => River
+ },
+ // River2B narrow 45 channel angled land top left with nub bottom right
+ River2B => landAngle switch
+ {
+ Default when IsPointInMultiTriangle(relativeX, relativeY, new(4, 15), new(0, 0), new(15, 4), new(0, 15), new(15, 0)) || IsNubOnBottomRight(relativeX, relativeY) || relativeX < 4 || relativeY < 4 => Grass,
+ Rotate90ClockAnverse when IsPointInMultiTriangle(relativeX, relativeY, new(4, 0), new(0, 15), new(15, 12), new(0, 0), new(15, 15)) || IsNubOnTopRight(relativeX, relativeY) || relativeX < 4 || relativeY >= 12 => Grass,
+ Rotate180ClockAnverse when IsPointInMultiTriangle(relativeX, relativeY, new(0, 12), new(15, 15), new(12, 0), new(0, 15), new(15, 0)) || IsNubOnTopLeft(relativeX, relativeY) || relativeX >= 12 || relativeY >= 12 => Grass,
+ Rotate270ClockAnverse when IsPointInMultiTriangle(relativeX, relativeY, new(0, 4), new(15, 0), new(12, 15), new(0, 0), new(15, 15)) || IsNubOnBottomLeft(relativeX, relativeY) || relativeX >= 12 || relativeY < 4 => Grass,
+ _ => River
+ },
+ // River2C narrow 90 channel corner land top left with nub bottom right
+ River2C => landAngle switch
+ {
+ Default when relativeX < 4 || relativeY < 4 || IsNubOnBottomRight(relativeX, relativeY) => Grass,
+ Rotate90ClockAnverse when relativeX < 4 || relativeY >= 12 || IsNubOnTopRight(relativeX, relativeY) => Grass,
+ Rotate180ClockAnverse when relativeX >= 12 || relativeY >= 12 || IsNubOnTopLeft(relativeX, relativeY) => Grass,
+ Rotate270ClockAnverse when relativeX >= 12 || relativeY < 4 || IsNubOnBottomLeft(relativeX, relativeY) => Grass,
+ _ => River
+ },
+ // River3A narrow 3 way land left side, nub top right and bottom right
+ River3A => landAngle switch
+ {
+ Default when relativeX < 4 || IsNubOnTopRight(relativeX, relativeY) || IsNubOnBottomRight(relativeX, relativeY) => Grass,
+ Rotate90ClockAnverse when relativeY >= 12 || IsNubOnTopLeft(relativeX, relativeY) || IsNubOnTopRight(relativeX, relativeY) => Grass,
+ Rotate180ClockAnverse when relativeX >= 12 || IsNubOnBottomLeft(relativeX, relativeY) || IsNubOnTopLeft(relativeX, relativeY) => Grass,
+ Rotate270ClockAnverse when relativeY < 4 || IsNubOnBottomRight(relativeX, relativeY) || IsNubOnBottomLeft(relativeX, relativeY) => Grass,
+ _ => River
+ },
+ // River3B river 45 corner angled land top left, no nub
+ River3B => landAngle switch
+ {
+ Default when IsPointInMultiTriangle(relativeX, relativeY, new(4, 15), new(0, 0), new(15, 4), new(0, 15), new(15, 0)) => Grass,
+ Rotate90ClockAnverse when IsPointInMultiTriangle(relativeX, relativeY, new(4, 0), new(0, 15), new(15, 12), new(0, 0), new(15, 15)) => Grass,
+ Rotate180ClockAnverse when IsPointInMultiTriangle(relativeX, relativeY, new(0, 12), new(15, 15), new(12, 0), new(0, 15), new(15, 0)) => Grass,
+ Rotate270ClockAnverse when IsPointInMultiTriangle(relativeX, relativeY, new(0, 4), new(15, 0), new(12, 15), new(0, 0), new(15, 15)) => Grass,
+ _ => River
+ },
+ // River3C river 90 corner corner land top left, no nub
+ River3C => landAngle switch
+ {
+ Default when relativeX < 4 || relativeY < 4 => Grass,
+ Rotate90ClockAnverse when relativeX < 4 || relativeY >= 12 => Grass,
+ Rotate180ClockAnverse when relativeX >= 12 || relativeY >= 12 => Grass,
+ Rotate270ClockAnverse when relativeX >= 12 || relativeY < 4 => Grass,
+ _ => River
+ },
+ // River4A river side with nub top land left side with nub top right only
+ River4A => landAngle switch
+ {
+ Default when relativeX < 4 || IsNubOnTopRight(relativeX, relativeY) => Grass,
+ Rotate90ClockAnverse when relativeY >= 12 || IsNubOnTopLeft(relativeX, relativeY) => Grass,
+ Rotate180ClockAnverse when relativeX >= 12 || IsNubOnBottomLeft(relativeX, relativeY) => Grass,
+ Rotate270ClockAnverse when relativeY < 4 || IsNubOnBottomRight(relativeX, relativeY) => Grass,
+ _ => River
+ },
+ // River4B river side with nub bottom land left side with nub bottom right only
+ River4B => landAngle switch
+ {
+ Default when relativeX < 4 || IsNubOnBottomRight(relativeX, relativeY) => Grass,
+ Rotate90ClockAnverse when relativeY >= 12 || IsNubOnTopRight(relativeX, relativeY) => Grass,
+ Rotate180ClockAnverse when relativeX >= 12 || IsNubOnTopLeft(relativeX, relativeY) => Grass,
+ Rotate270ClockAnverse when relativeY < 4 || IsNubOnBottomLeft(relativeX, relativeY) => Grass,
+ _ => River
+ },
+ // River4C narrow 4 way nub on all 4 corners, 4 sides water. rotation does nothing
+ River4C when (IsNubOnBottomLeft(relativeX, relativeY) || IsNubOnBottomRight(relativeX, relativeY) || IsNubOnTopLeft(relativeX, relativeY) || IsNubOnTopRight(relativeX, relativeY)) => Grass,
+ // River5A river corner to 2 narrow Nub on top left, top right, and bottom right. 2 narrows meet a river
+ River5A => landAngle switch
+ {
+ Default when IsNubOnTopLeft(relativeX, relativeY) || IsNubOnTopRight(relativeX, relativeY) || IsNubOnBottomRight(relativeX, relativeY) => Grass,
+ Rotate90ClockAnverse when IsNubOnBottomLeft(relativeX, relativeY) || IsNubOnTopLeft(relativeX, relativeY) || IsNubOnTopRight(relativeX, relativeY) => Grass,
+ Rotate180ClockAnverse when IsNubOnBottomLeft(relativeX, relativeY) || IsNubOnBottomRight(relativeX, relativeY) || IsNubOnTopLeft(relativeX, relativeY) => Grass,
+ Rotate270ClockAnverse when IsNubOnBottomLeft(relativeX, relativeY) || IsNubOnBottomRight(relativeX, relativeY) || IsNubOnTopRight(relativeX, relativeY) => Grass,
+ _ => River
+ },
+ // River5B river side land on left side
+ River5B => landAngle switch
+ {
+ Default when relativeX < 4 => Grass,
+ Rotate90ClockAnverse when relativeY >= 12 => Grass,
+ Rotate180ClockAnverse when relativeX >= 12 => Grass,
+ Rotate270ClockAnverse when relativeY < 4 => Grass,
+ _ => River
+ },
+ // River6A river 2 opposing nubs nub on top left and bottom right
+ River6A => landAngle switch
+ {
+ Default when IsNubOnTopLeft(relativeX, relativeY) || IsNubOnBottomRight(relativeX, relativeY) => Grass,
+ Rotate90ClockAnverse when IsNubOnBottomLeft(relativeX, relativeY) || IsNubOnTopRight(relativeX, relativeY) => Grass,
+ Rotate180ClockAnverse when IsNubOnTopLeft(relativeX, relativeY) || IsNubOnBottomRight(relativeX, relativeY) => Grass,
+ Rotate270ClockAnverse when IsNubOnBottomLeft(relativeX, relativeY) || IsNubOnTopRight(relativeX, relativeY) => Grass,
+ _ => River
+ },
+ // River6B river 2 nubs same side nub on bottom left and bottom right corner, where 1 narrow meets river bottom side
+ River6B => landAngle switch
+ {
+ Default when IsNubOnBottomLeft(relativeX, relativeY) || IsNubOnBottomRight(relativeX, relativeY) => Grass,
+ Rotate90ClockAnverse when IsNubOnBottomRight(relativeX, relativeY) || IsNubOnTopRight(relativeX, relativeY) => Grass,
+ Rotate180ClockAnverse when IsNubOnTopRight(relativeX, relativeY) || IsNubOnTopLeft(relativeX, relativeY) => Grass,
+ Rotate270ClockAnverse when IsNubOnTopLeft(relativeX, relativeY) || IsNubOnBottomLeft(relativeX, relativeY) => Grass,
+ _ => River
+ },
+ // River7A river 1 nub nub on bottom left corner, fills gaps of diagonal bank
+ River7A => landAngle switch
+ {
+ Default when IsNubOnBottomLeft(relativeX, relativeY) => Grass,
+ Rotate90ClockAnverse when IsNubOnBottomRight(relativeX, relativeY) => Grass,
+ Rotate180ClockAnverse when IsNubOnTopRight(relativeX, relativeY) => Grass,
+ Rotate270ClockAnverse when IsNubOnTopLeft(relativeX, relativeY) => Grass,
+ _ => River
+ },
+ // River8A river is no land, just water. Rotation doesn't matter
+ River8A => River,
+ _ => River
+ };
+ }
+
+ private static bool IsNubOnTopLeft(int relativeX, int relativeY) => IsPointInTriangle(relativeX, relativeY, new(0, 4), new(0, 0), new(4, 0));
+ private static bool IsNubOnTopRight(int relativeX, int relativeY) => IsPointInTriangle(relativeX, relativeY, new(12, 0), new(15, 0), new(15, 4));
+ private static bool IsNubOnBottomLeft(int relativeX, int relativeY) => IsPointInTriangle(relativeX, relativeY, new(0, 12), new(0, 15), new(4, 15));
+ private static bool IsNubOnBottomRight(int relativeX, int relativeY) => IsPointInTriangle(relativeX, relativeY, new(12, 15), new(15, 15), new(15, 12));
+
+ private static bool IsPointInMultiTriangle(int px, int py, Coordinate a, Coordinate b, Coordinate c, Coordinate vortexA, Coordinate vortexB)
+ {
+ return IsPointInTriangle(px, py, a, vortexA, b)
+ || IsPointInTriangle(px, py, a, b, c)
+ || IsPointInTriangle(px, py, c, b, vortexB);
+ }
+
+ private static bool IsPointInTriangle(int px, int py, Coordinate a, Coordinate b, Coordinate c)
+ {
+ Coordinate p = new(px, py);
+ float areaTotal = GetTriangleArea(a, b, c);
+ float area1 = GetTriangleArea(p, b, c);
+ float area2 = GetTriangleArea(a, p, c);
+ float area3 = GetTriangleArea(a, b, p);
+
+ return Math.Abs(areaTotal - (area1 + area2 + area3)) < 0.0001f;
+ }
+
+ private static float GetTriangleArea(Coordinate a, Coordinate b, Coordinate c)
+ {
+ return Math.Abs(((a.X * (b.Y - c.Y)) +
+ (b.X * (c.Y - a.Y)) +
+ (c.X * (a.Y - b.Y))) / 2.0f);
+ }
+
+ private readonly record struct Coordinate(int X, int Y);
+
+ private static readonly Color CliffBase = ColorUtil.Blend(Grass, Color.Black, 0.6d);
+
+ private static Color GetTileDefaultColor(TerrainUnitModel mdl, ushort landAngle, int relativeX, int relativeY)
+ {
+ var angle = (LandAngles)landAngle;
+ if (mdl.IsRiver)
+ return GetRiverColor(mdl, angle, relativeX, relativeY);
+ if (mdl.IsFall)
+ return Color.DeepSkyBlue;
+ if (mdl.IsCliff)
+ return CliffBase;
+ return Grass;
+ }
+
+ public static string GetTileName(TerrainTile tile)
+ {
+ var name = tile.UnitModel.ToString();
+ var num = name.IndexOfAnyInRange('0', '9');
+ if (num < 0)
+ return name;
+ return name[..num] + Environment.NewLine + name[num..];
+ }
+}
\ No newline at end of file
diff --git a/NHSE.Core/Editing/Batch/BatchMutator.cs b/NHSE.Core/Editing/Batch/BatchMutator.cs
index 9b47429..e6e3ef0 100644
--- a/NHSE.Core/Editing/Batch/BatchMutator.cs
+++ b/NHSE.Core/Editing/Batch/BatchMutator.cs
@@ -1,11 +1,10 @@
using System.Collections.Generic;
-namespace NHSE.Core
-{
- public abstract class BatchMutator where T : class
- {
- protected const string CONST_RAND = "$rand";
+namespace NHSE.Core;
- public abstract ModifyResult Modify(T item, IEnumerable filters, IEnumerable modifications);
- }
-}
+public abstract class BatchMutator where T : class
+{
+ protected const string CONST_RAND = "$rand";
+
+ public abstract ModifyResult Modify(T item, IEnumerable filters, IEnumerable modifications);
+}
\ No newline at end of file
diff --git a/NHSE.Core/Editing/Batch/BatchProcessor.cs b/NHSE.Core/Editing/Batch/BatchProcessor.cs
index a69d460..24168c4 100644
--- a/NHSE.Core/Editing/Batch/BatchProcessor.cs
+++ b/NHSE.Core/Editing/Batch/BatchProcessor.cs
@@ -2,86 +2,82 @@
using System.Collections.Generic;
using System.Linq;
-namespace NHSE.Core
+namespace NHSE.Core;
+
+///
+/// Carries out a batch edit and contains information summarizing the results.
+///
+public abstract class BatchProcessor(BatchMutator Mutator) where T : class
{
+ private int Modified { get; set; }
+ private int Iterated { get; set; }
+ private int Failed { get; set; }
+
+ protected abstract bool CanModify(T item);
+ protected abstract bool Finalize(T item);
+
///
- /// Carries out a batch edit and contains information summarizing the results.
+ /// Tries to modify the .
///
- public abstract class BatchProcessor where T : class
+ /// Object to modify.
+ /// Filters which must be satisfied prior to any modifications being made.
+ /// Modifications to perform on the item.
+ /// Result of the attempted modification.
+ public bool Process(T item, IEnumerable filters, IEnumerable modifications)
{
- private int Modified { get; set; }
- private int Iterated { get; set; }
- private int Failed { get; set; }
+ if (!CanModify(item))
+ return false;
- protected readonly BatchMutator Mutator;
- protected BatchProcessor(BatchMutator mut) => Mutator = mut;
+ var result = Mutator.Modify(item, filters, modifications);
+ if (result != ModifyResult.Invalid)
+ Iterated++;
+ if (result == ModifyResult.Error)
+ Failed++;
+ if (result != ModifyResult.Modified)
+ return false;
- protected abstract bool CanModify(T item);
- protected abstract bool Finalize(T item);
+ Finalize(item);
+ Modified++;
+ return true;
+ }
- ///
- /// Tries to modify the .
- ///
- /// Object to modify.
- /// Filters which must be satisfied prior to any modifications being made.
- /// Modifications to perform on the item.
- /// Result of the attempted modification.
- public bool Process(T item, IEnumerable filters, IEnumerable modifications)
+ ///
+ /// Gets a message indicating the overall result of all modifications performed across multiple Batch Edit jobs.
+ ///
+ /// Collection of modifications.
+ /// Friendly (multi-line) string indicating the result of the batch edits.
+ public string GetEditorResults(ICollection sets)
+ {
+ if (sets.Count == 0)
+ return "No instructions present.";
+ int ctr = Modified / sets.Count;
+ int len = Iterated / sets.Count;
+ string maybe = sets.Count == 1 ? string.Empty : "~";
+ string result = $"Success: {maybe}{ctr}/{len}";
+ if (Failed > 0)
+ result += Environment.NewLine + maybe + $"Failed: {Failed} not processed.";
+ return result;
+ }
+
+ public void Execute(ReadOnlySpan lines, IEnumerable data)
+ {
+ var sets = StringInstructionSet.GetBatchSets(lines).ToArray();
+ foreach (var pk in data)
{
- if (!CanModify(item))
- return false;
-
- var result = Mutator.Modify(item, filters, modifications);
- if (result != ModifyResult.Invalid)
- Iterated++;
- if (result == ModifyResult.Error)
- Failed++;
- if (result != ModifyResult.Modified)
- return false;
-
- Finalize(item);
- Modified++;
- return true;
- }
-
- ///
- /// Gets a message indicating the overall result of all modifications performed across multiple Batch Edit jobs.
- ///
- /// Collection of modifications.
- /// Friendly (multi-line) string indicating the result of the batch edits.
- public string GetEditorResults(ICollection sets)
- {
- if (sets.Count == 0)
- return "No instructions present.";
- int ctr = Modified / sets.Count;
- int len = Iterated / sets.Count;
- string maybe = sets.Count == 1 ? string.Empty : "~";
- string result = $"Success: {maybe}{ctr}/{len}";
- if (Failed > 0)
- result += Environment.NewLine + maybe + $"Failed: {Failed} not processed.";
- return result;
- }
-
- public void Execute(IList lines, IEnumerable data)
- {
- var sets = StringInstructionSet.GetBatchSets(lines).ToArray();
- foreach (var pk in data)
- {
- foreach (var set in sets)
- Process(pk, set.Filters, set.Instructions);
- }
- }
-
- protected abstract void Initialize(StringInstructionSet[] sets);
-
- public void Process(StringInstructionSet[] sets, IReadOnlyList items)
- {
- Initialize(sets);
- foreach (var s in sets)
- {
- foreach (var i in items)
- Process(i, s.Filters, s.Instructions);
- }
+ foreach (var set in sets)
+ Process(pk, set.Filters, set.Instructions);
}
}
-}
+
+ protected abstract void Initialize(ReadOnlySpan sets);
+
+ public void Process(ReadOnlySpan sets, IReadOnlyList items)
+ {
+ Initialize(sets);
+ foreach (var s in sets)
+ {
+ foreach (var i in items)
+ Process(i, s.Filters, s.Instructions);
+ }
+ }
+}
\ No newline at end of file
diff --git a/NHSE.Core/Editing/Batch/ItemMutator.cs b/NHSE.Core/Editing/Batch/ItemMutator.cs
index 58a6c4b..bfa3c35 100644
--- a/NHSE.Core/Editing/Batch/ItemMutator.cs
+++ b/NHSE.Core/Editing/Batch/ItemMutator.cs
@@ -5,205 +5,214 @@
using System.Linq;
using System.Reflection;
-namespace NHSE.Core
+namespace NHSE.Core;
+
+public class ItemMutator : BatchMutator-
{
- public class ItemMutator : BatchMutator
-
+ public readonly ItemReflection Reflect = ItemReflection.Default;
+ private const char CONST_POINTER = '*';
+
+ public override ModifyResult Modify(Item item, IEnumerable filters, IEnumerable modifications)
{
- public readonly ItemReflection Reflect = ItemReflection.Default;
-
- public override ModifyResult Modify(Item item, IEnumerable filters, IEnumerable modifications)
+ var pi = Reflect.Props[Array.IndexOf(Reflect.Types, item.GetType())];
+ foreach (var cmd in filters)
{
- var pi = Reflect.Props[Array.IndexOf(Reflect.Types, item.GetType())];
- foreach (var cmd in filters)
+ try
{
- try
- {
- if (!IsFilterMatch(cmd, item, pi))
- return ModifyResult.Filtered;
- }
- // Swallow any error because this can be malformed user input.
- catch (Exception ex)
- {
- Debug.WriteLine($"Failed to compare: {ex.Message} - {cmd.PropertyName} {cmd.PropertyValue}");
- return ModifyResult.Error;
- }
+ if (!IsFilterMatch(cmd, item, pi))
+ return ModifyResult.Filtered;
}
-
- ModifyResult result = ModifyResult.Modified;
- foreach (var cmd in modifications)
+ // Swallow any error because this can be malformed user input.
+ catch (Exception ex)
{
- try
- {
- var tmp = SetProperty(cmd, item, pi);
- if (tmp != ModifyResult.Modified)
- result = tmp;
- }
- // Swallow any error because this can be malformed user input.
- catch (Exception ex)
- {
- Debug.WriteLine($"Failed to modify: {ex.Message} - {cmd.PropertyName} {cmd.PropertyValue}");
- }
+ Debug.WriteLine($"Failed to compare: {ex.Message} - {cmd.PropertyName} {cmd.PropertyValue}");
+ return ModifyResult.Error;
}
- return result;
}
- ///
- /// Sets the if the should be filtered due to the provided.
- ///
- /// Command Filter
- /// Pokémon to check.
- /// PropertyInfo cache (optional)
- /// True if filtered, else false.
- private static ModifyResult SetProperty(StringInstruction cmd, Item item, IReadOnlyDictionary props)
+ ModifyResult result = ModifyResult.Modified;
+ foreach (var cmd in modifications)
{
- if (SetComplexProperty(item, cmd))
- return ModifyResult.Modified;
+ try
+ {
+ var tmp = SetProperty(cmd, item, pi);
+ if (tmp != ModifyResult.Modified)
+ result = tmp;
+ }
+ // Swallow any error because this can be malformed user input.
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Failed to modify: {ex.Message} - {cmd.PropertyName} {cmd.PropertyValue}");
+ }
+ }
+ return result;
+ }
- if (!props.TryGetValue(cmd.PropertyName, out var pi))
- return ModifyResult.Error;
-
- if (!pi.CanWrite)
- return ModifyResult.Error;
-
- object val = cmd.Random ? (object)cmd.RandomValue : cmd.PropertyValue;
- ReflectUtil.SetValue(pi, item, val);
+ ///
+ /// Sets if the should be filtered due to the provided.
+ ///
+ /// Command Filter
+ /// Pokémon to check.
+ /// PropertyInfo cache (optional)
+ /// True if filtered, else false.
+ private static ModifyResult SetProperty(StringInstruction cmd, Item item, Dictionary.AlternateLookup> props)
+ {
+ if (SetComplexProperty(item, cmd))
return ModifyResult.Modified;
+
+ if (!props.TryGetValue(cmd.PropertyName, out var pi))
+ return ModifyResult.Error;
+
+ if (!pi.CanWrite)
+ return ModifyResult.Error;
+
+ if (cmd.Random)
+ ReflectUtil.SetValue(pi, item, cmd.RandomValue);
+ else
+ ReflectUtil.SetValue(pi, item, cmd.PropertyValue);
+ return ModifyResult.Modified;
+ }
+
+ private static bool SetComplexProperty(Item item, StringInstruction cmd)
+ {
+ // Zeroed out item?
+ if (cmd.PropertyName == nameof(Item.ItemId))
+ {
+ if (!int.TryParse(cmd.PropertyValue, out var val))
+ return false;
+ if (val is not (0 or 0xFFFE))
+ return false;
+ item.Delete();
+ return true;
+ }
+ return false;
+ }
+
+ ///
+ /// Tries to fetch the property from the cache of available properties.
+ ///
+ /// Pokémon to check
+ /// Property Name to check
+ /// Property Info retrieved (if any).
+ /// True if has property, false if does not.
+ public bool TryGetHasProperty(Item item, string name, [NotNullWhen(true)] out PropertyInfo? pi)
+ {
+ var type = item.GetType();
+ return TryGetHasProperty(type, name, out pi);
+ }
+
+ ///
+ /// Tries to fetch the property from the cache of available properties.
+ ///
+ /// Type to check
+ /// Property Name to check
+ /// Property Info retrieved (if any).
+ /// True if has property, false if does not.
+ public bool TryGetHasProperty(Type type, string name, [NotNullWhen(true)] out PropertyInfo? pi)
+ {
+ var index = Array.IndexOf(Reflect.Types, type);
+ if (index < 0)
+ {
+ pi = null;
+ return false;
+ }
+ var props = Reflect.Props[index];
+ return props.TryGetValue(name, out pi);
+ }
+
+ ///
+ /// Gets the type of the property using the saved cache of properties.
+ ///
+ /// Property Name to fetch the type for
+ /// Type index. Leave empty (0) for a nonspecific format.
+ /// Short name of the property's type.
+ public string? GetPropertyType(string propertyName, int typeIndex = 0)
+ {
+ if (typeIndex == 0) // Any
+ {
+ foreach (var p in Reflect.Props)
+ {
+ if (p.TryGetValue(propertyName, out var pi))
+ return pi.PropertyType.Name;
+ }
+ return null;
}
- private static bool SetComplexProperty(Item item, StringInstruction cmd)
+ int index = typeIndex - 1 >= Reflect.Props.Length ? 0 : typeIndex - 1; // All vs Specific
+ var pr = Reflect.Props[index];
+ if (!pr.TryGetValue(propertyName, out var info))
+ return null;
+ return info.PropertyType.Name;
+ }
+
+ ///
+ /// Checks if the object is filtered by the provided .
+ ///
+ /// Filters which must be satisfied.
+ /// Object to check.
+ /// True if matches all filters.
+ public bool IsFilterMatch(IEnumerable filters, Item item) => filters.All(z => IsFilterMatch(z, item, Reflect.Props[Array.IndexOf(Reflect.Types, item.GetType())]));
+
+ ///
+ /// Checks if the should be filtered due to the provided.
+ ///
+ /// Command Filter
+ /// Pokémon to check.
+ /// PropertyInfo cache (optional)
+ /// True if filter matches, else false.
+ private static bool IsFilterMatch(StringInstruction cmd, Item item, Dictionary.AlternateLookup> props)
+ {
+ return IsPropertyFiltered(cmd, item, props);
+ }
+
+ ///
+ /// Checks if the should be filtered due to the provided.
+ ///
+ /// Command Filter
+ /// Pokémon to check.
+ /// PropertyInfo cache
+ /// True if filtered, else false.
+ private static bool IsPropertyFiltered(StringInstruction cmd, Item item, Dictionary.AlternateLookup> props)
+ {
+ if (!props.TryGetValue(cmd.PropertyName, out var pi))
+ return false;
+ if (!pi.CanRead)
+ return false;
+
+ var val = cmd.PropertyValue;
+ if (val.StartsWith(CONST_POINTER) && props.TryGetValue(val.AsSpan(1), out var opi))
{
- // Zeroed out item?
- if (cmd.PropertyName == nameof(Item.ItemId))
+ var result = opi.GetValue(item) ?? throw new NullReferenceException();
+ return cmd.Comparer.IsCompareOperator(pi.CompareTo(item, result));
+ }
+ return cmd.Comparer.IsCompareOperator(pi.CompareTo(item, val));
+ }
+
+ ///
+ /// Checks if the object is filtered by the provided .
+ ///
+ /// Filters which must be satisfied.
+ /// Object to check.
+ /// True if matches all filters.
+ public static bool IsFilterMatch(IEnumerable filters, object obj)
+ {
+ foreach (var cmd in filters)
+ {
+ if (!ReflectUtil.HasProperty(obj, cmd.PropertyName, out var pi))
+ return false;
+ try
{
- if (!int.TryParse(cmd.PropertyValue, out var val))
- return false;
- if (val is not 0 or 0xFFFE)
- return false;
- item.Delete();
- return true;
+ if (cmd.Comparer.IsCompareOperator(pi.CompareTo(obj, cmd.PropertyValue)))
+ continue;
+ }
+ // User provided inputs can mismatch the type's required value format, and fail to be compared.
+ catch (Exception e)
+ {
+ Debug.WriteLine($"Unable to compare {cmd.PropertyName} to {cmd.PropertyValue}.");
+ Debug.WriteLine(e.Message);
}
return false;
}
-
- ///
- /// Tries to fetch the property from the cache of available properties.
- ///
- /// Pokémon to check
- /// Property Name to check
- /// Property Info retrieved (if any).
- /// True if has property, false if does not.
- public bool TryGetHasProperty(Item item, string name, [NotNullWhen(true)] out PropertyInfo? pi)
- {
- var type = item.GetType();
- return TryGetHasProperty(type, name, out pi);
- }
-
- ///
- /// Tries to fetch the property from the cache of available properties.
- ///
- /// Type to check
- /// Property Name to check
- /// Property Info retrieved (if any).
- /// True if has property, false if does not.
- public bool TryGetHasProperty(Type type, string name, [NotNullWhen(true)] out PropertyInfo? pi)
- {
- var index = Array.IndexOf(Reflect.Types, type);
- if (index < 0)
- {
- pi = null;
- return false;
- }
- var props = Reflect.Props[index];
- return props.TryGetValue(name, out pi);
- }
-
- ///
- /// Gets the type of the property using the saved cache of properties.
- ///
- /// Property Name to fetch the type for
- /// Type index. Leave empty (0) for a nonspecific format.
- /// Short name of the property's type.
- public string? GetPropertyType(string propertyName, int typeIndex = 0)
- {
- if (typeIndex == 0) // Any
- {
- foreach (var p in Reflect.Props)
- {
- if (p.TryGetValue(propertyName, out var pi))
- return pi.PropertyType.Name;
- }
- return null;
- }
-
- int index = typeIndex - 1 >= Reflect.Props.Length ? 0 : typeIndex - 1; // All vs Specific
- var pr = Reflect.Props[index];
- if (!pr.TryGetValue(propertyName, out var info))
- return null;
- return info.PropertyType.Name;
- }
-
- ///
- /// Checks if the object is filtered by the provided .
- ///
- /// Filters which must be satisfied.
- /// Object to check.
- /// True if matches all filters.
- public bool IsFilterMatch(IEnumerable filters, Item item) => filters.All(z => IsFilterMatch(z, item, Reflect.Props[Array.IndexOf(Reflect.Types, item.GetType())]));
-
- ///
- /// Checks if the should be filtered due to the provided.
- ///
- /// Command Filter
- /// Pokémon to check.
- /// PropertyInfo cache (optional)
- /// True if filter matches, else false.
- private static bool IsFilterMatch(StringInstruction cmd, Item item, IReadOnlyDictionary props)
- {
- return IsPropertyFiltered(cmd, item, props);
- }
-
- ///
- /// Checks if the should be filtered due to the provided.
- ///
- /// Command Filter
- /// Pokémon to check.
- /// PropertyInfo cache
- /// True if filtered, else false.
- private static bool IsPropertyFiltered(StringInstruction cmd, Item item, IReadOnlyDictionary props)
- {
- if (!props.TryGetValue(cmd.PropertyName, out var pi))
- return false;
- if (!pi.CanRead)
- return false;
- return pi.IsValueEqual(item, cmd.PropertyValue) == cmd.Evaluator;
- }
-
- ///
- /// Checks if the object is filtered by the provided .
- ///
- /// Filters which must be satisfied.
- /// Object to check.
- /// True if matches all filters.
- public static bool IsFilterMatch(IEnumerable filters, object obj)
- {
- foreach (var cmd in filters)
- {
- if (!ReflectUtil.HasProperty(obj, cmd.PropertyName, out var pi))
- return false;
- try
- {
- if (pi.IsValueEqual(obj, cmd.PropertyValue) == cmd.Evaluator)
- continue;
- }
- // User provided inputs can mismatch the type's required value format, and fail to be compared.
- catch (Exception e)
- {
- Debug.WriteLine($"Unable to compare {cmd.PropertyName} to {cmd.PropertyValue}.");
- Debug.WriteLine(e.Message);
- }
- return false;
- }
- return true;
- }
+ return true;
}
-}
+}
\ No newline at end of file
diff --git a/NHSE.Core/Editing/Batch/ItemProcessor.cs b/NHSE.Core/Editing/Batch/ItemProcessor.cs
index c9d7b50..fb8dcae 100644
--- a/NHSE.Core/Editing/Batch/ItemProcessor.cs
+++ b/NHSE.Core/Editing/Batch/ItemProcessor.cs
@@ -1,52 +1,48 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Linq;
-namespace NHSE.Core
+namespace NHSE.Core;
+
+public class ItemProcessor(BatchMutator
- mut) : BatchProcessor
- (mut)
{
- public class ItemProcessor : BatchProcessor
-
+ protected override bool CanModify(Item item) => true;
+ protected override bool Finalize(Item item) => true;
+
+ ///
+ /// Initializes the list with a context-sensitive value. If the provided value is a string, it will attempt to convert that string to its corresponding index.
+ ///
+ /// Instructions to initialize.
+ public static void ScreenStrings(IEnumerable il)
{
- public ItemProcessor(BatchMutator
- mut) : base(mut)
+ foreach (var i in il.Where(i => !i.PropertyValue.All(char.IsDigit)))
{
- }
+ string pv = i.PropertyValue;
+ if (pv.StartsWith('$') && pv.Contains(','))
+ i.SetRandomRange(pv);
- protected override bool CanModify(Item item) => true;
- protected override bool Finalize(Item item) => true;
-
- ///
- /// Initializes the list with a context-sensitive value. If the provided value is a string, it will attempt to convert that string to its corresponding index.
- ///
- /// Instructions to initialize.
- public void ScreenStrings(IEnumerable il)
- {
- foreach (var i in il.Where(i => !i.PropertyValue.All(char.IsDigit)))
- {
- string pv = i.PropertyValue;
- if (pv.StartsWith("$") && pv.Contains(","))
- i.SetRandRange(pv);
-
- SetInstructionScreenedValue(i);
- }
- }
-
- ///
- /// Initializes the with a context-sensitive value. If the provided value is a string, it will attempt to convert that string to its corresponding index.
- ///
- /// Instruction to initialize.
- private static void SetInstructionScreenedValue(StringInstruction i)
- {
- switch (i.PropertyName)
- {
- case nameof(Item.ItemId) or nameof(Item.ExtensionItemId): i.SetScreenedValue(GameInfo.Strings.itemlistdisplay); return;
- }
- }
-
- protected override void Initialize(StringInstructionSet[] sets)
- {
- foreach (var set in sets)
- {
- ScreenStrings(set.Filters);
- ScreenStrings(set.Instructions);
- }
+ SetInstructionScreenedValue(i);
}
}
-}
+
+ ///
+ /// Initializes the with a context-sensitive value. If the provided value is a string, it will attempt to convert that string to its corresponding index.
+ ///
+ /// Instruction to initialize.
+ private static void SetInstructionScreenedValue(StringInstruction i)
+ {
+ switch (i.PropertyName)
+ {
+ case nameof(Item.ItemId) or nameof(Item.ExtensionItemId): i.SetScreenedValue(GameInfo.Strings.itemlistdisplay); return;
+ }
+ }
+
+ protected override void Initialize(ReadOnlySpan sets)
+ {
+ foreach (var set in sets)
+ {
+ ScreenStrings(set.Filters);
+ ScreenStrings(set.Instructions);
+ }
+ }
+}
\ No newline at end of file
diff --git a/NHSE.Core/Editing/Batch/ItemReflection.cs b/NHSE.Core/Editing/Batch/ItemReflection.cs
index 64cbfb3..24aecc6 100644
--- a/NHSE.Core/Editing/Batch/ItemReflection.cs
+++ b/NHSE.Core/Editing/Batch/ItemReflection.cs
@@ -3,48 +3,82 @@
using System.Linq;
using System.Reflection;
-namespace NHSE.Core
+namespace NHSE.Core;
+
+public class ItemReflection
{
- public class ItemReflection
+ public static ItemReflection Default { get; } = new();
+
+ public readonly Type[] Types = [typeof(Item), typeof(VillagerItem)];
+ public string[][] Properties => GetProperties.Value;
+
+ ///
+ /// Extra properties to show in the list of selectable properties (GUI)
+ ///
+ private static readonly string[] CustomProperties =
+ [
+ ];
+
+ public readonly Dictionary.AlternateLookup>[] Props;
+ private readonly Lazy GetProperties;
+
+ public ItemReflection()
{
- public static ItemReflection Default { get; } = new();
-
- public readonly Type[] Types = { typeof(Item), typeof(VillagerItem) };
- public readonly Dictionary[] Props;
- public readonly string[][] Properties;
-
- public ItemReflection()
- {
- Props = Types
- .Select(z => ReflectUtil.GetAllPropertyInfoPublic(z)
- .GroupBy(p => p.Name)
- .Select(g => g.First())
- .ToDictionary(p => p.Name))
- .ToArray();
-
- Properties = GetPropArray();
- }
-
- public string[][] GetPropArray()
- {
- var p = new string[Types.Length][];
- for (int i = 0; i < p.Length; i++)
- {
- var pz = ReflectUtil.GetPropertiesPublic(Types[i]);
- p[i] = pz.OrderBy(a => a).ToArray();
- }
-
- // Properties for any
- var any = ReflectUtil.GetPropertiesPublic(typeof(Item)).Union(p.SelectMany(a => a)).OrderBy(a => a).ToArray();
- // Properties shared by all
- var all = p.Aggregate(new HashSet(p[0]), (h, e) => { h.IntersectWith(e); return h; }).OrderBy(a => a).ToArray();
-
- var p1 = new string[Types.Length + 2][];
- Array.Copy(p, 0, p1, 1, p.Length);
- p1[0] = any;
- p1[p1.Length - 1] = all;
-
- return p1;
- }
+ Props = GetPropertyDictionaries(Types);
+ GetProperties = new Lazy(() => GetPropArray(Props, CustomProperties));
}
-}
+
+ private static Dictionary.AlternateLookup>[] GetPropertyDictionaries(ReadOnlySpan types)
+ {
+ var result = new Dictionary.AlternateLookup>[types.Length];
+ for (int i = 0; i < types.Length; i++)
+ result[i] = GetPropertyDictionary(types[i], ReflectUtil.GetAllPropertyInfoPublic).GetAlternateLookup>();
+ return result;
+ }
+
+ private static Dictionary GetPropertyDictionary(Type type, Func> selector)
+ {
+ const int expectedMax = 8;
+ var dict = new Dictionary(expectedMax);
+ var props = selector(type);
+ foreach (var p in props)
+ dict.TryAdd(p.Name, p);
+ return dict;
+ }
+
+ private static string[][] GetPropArray(Dictionary.AlternateLookup>[] types, ReadOnlySpan extra)
+ {
+ // Create a list for all types, [inAny, ..types, inAll]
+ var result = new string[types.Length + 2][];
+ var p = result.AsSpan(1, types.Length);
+
+ for (int i = 0; i < p.Length; i++)
+ {
+ var type = types[i].Dictionary;
+ string[] combine = [.. type.Keys, .. extra];
+ Array.Sort(combine);
+ p[i] = combine;
+ }
+
+ // Properties for any PKM
+ // Properties shared by all PKM
+ var first = p[0];
+ var any = new HashSet(first);
+ var all = new HashSet(first);
+ foreach (var set in p[1..])
+ {
+ any.UnionWith(set);
+ all.IntersectWith(set);
+ }
+
+ var arrAny = any.ToArray();
+ Array.Sort(arrAny);
+ result[0] = arrAny;
+
+ var arrAll = all.ToArray();
+ Array.Sort(arrAll);
+ result[^1] = arrAll;
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/NHSE.Core/Editing/Batch/ModifyResult.cs b/NHSE.Core/Editing/Batch/ModifyResult.cs
index e3988e2..411503c 100644
--- a/NHSE.Core/Editing/Batch/ModifyResult.cs
+++ b/NHSE.Core/Editing/Batch/ModifyResult.cs
@@ -1,28 +1,27 @@
-namespace NHSE.Core
+namespace NHSE.Core;
+
+///
+/// Batch Editor Modification result for an individual item.
+///
+public enum ModifyResult
{
///
- /// Batch Editor Modification result for an individual item.
+ /// The data has invalid data and is not a suitable candidate for modification.
///
- public enum ModifyResult
- {
- ///
- /// The data has invalid data and is not a suitable candidate for modification.
- ///
- Invalid,
+ Invalid,
- ///
- /// An error was occurred while iterating modifications for this data.
- ///
- Error,
+ ///
+ /// An error was occurred while iterating modifications for this data.
+ ///
+ Error,
- ///
- /// The data was skipped due to a matching Filter.
- ///
- Filtered,
+ ///
+ /// The data was skipped due to a matching Filter.
+ ///
+ Filtered,
- ///
- /// The data was modified.
- ///
- Modified,
- }
-}
+ ///
+ /// The data was modified.
+ ///
+ Modified,
+}
\ No newline at end of file
diff --git a/NHSE.Core/Editing/Batch/StringInstruction.cs b/NHSE.Core/Editing/Batch/StringInstruction.cs
index e82b368..de95aa6 100644
--- a/NHSE.Core/Editing/Batch/StringInstruction.cs
+++ b/NHSE.Core/Editing/Batch/StringInstruction.cs
@@ -1,101 +1,375 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
-using System.Linq;
+using System.Diagnostics.CodeAnalysis;
+using System.Text;
+using static NHSE.Core.InstructionComparer;
-namespace NHSE.Core
+namespace NHSE.Core;
+
+///
+/// Batch Editing instruction
+///
+///
+/// Can be a filter (skip), or a modification instruction (modify)
+///
+///
+///
+///
+/// Property to modify.
+/// Value to set to the property.
+/// Filter Comparison Type
+public sealed record StringInstruction(string PropertyName, string PropertyValue, InstructionComparer Comparer)
{
+ public string PropertyValue { get; private set; } = PropertyValue;
+
///
- /// Batch Editing instruction
+ /// Sets the to the index of the value in the input , if it exists.
+ ///
+ /// List of values to search for the .
+ /// True if the value was found and set, false otherwise.
+ public bool SetScreenedValue(ReadOnlySpan arr)
+ {
+ int index = arr.IndexOf(PropertyValue);
+ if ((uint)index >= arr.Length)
+ return false;
+ PropertyValue = index.ToString();
+ return true;
+ }
+
+ ///
+ /// Valid prefixes that are recognized for value comparison types.
+ ///
+ public static ReadOnlySpan Prefixes =>
+ [
+ Apply,
+ FilterEqual, FilterNotEqual, FilterGreaterThan, FilterGreaterThanOrEqual, FilterLessThan, FilterLessThanOrEqual,
+ ];
+
+ private const char Apply = '.';
+ private const char SplitRange = ',';
+
+ private const char FilterEqual = '=';
+ private const char FilterNotEqual = '!';
+ private const char FilterGreaterThan = '>';
+ private const char FilterLessThan = '<';
+ private const char FilterGreaterThanOrEqual = '≥';
+ private const char FilterLessThanOrEqual = '≤';
+
+ ///
+ /// Character which divides a property and a value.
///
///
- /// Can be a filter (skip), or a modification instruction (modify)
+ /// Example:
+ /// =Species=1
+ /// The second = is the split.
///
- ///
- ///
- ///
- public sealed class StringInstruction
+ public const char SplitInstruction = '=';
+
+ // Extra Functionality
+ private int RandomMinimum, RandomMaximum;
+
+ ///
+ /// Apply a instead of fixed value, based on the and values.
+ ///
+ public bool Random { get; private set; }
+
+ ///
+ /// Gets a value, based on the and values.
+ ///
+ public int RandomValue => RandUtil.Rand.Next(RandomMinimum, RandomMaximum + 1);
+
+ ///
+ /// Checks if the input is a valid "random range" specification.
+ ///
+ public static bool IsRandomRange(ReadOnlySpan str)
{
- public string PropertyName { get; }
- public string PropertyValue { get; private set; }
- public bool Evaluator { get; private init; }
+ // Need at least one character on either side of the splitter char.
+ int index = str.IndexOf(SplitRange);
+ return index > 0 && index < str.Length - 1;
+ }
- public StringInstruction(string name, string value)
+ ///
+ /// Sets a "random range" specification to the instruction.
+ ///
+ /// When the splitter is not present.
+ public void SetRandomRange(ReadOnlySpan str)
+ {
+ var index = str.IndexOf(SplitRange);
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(index);
+
+ var min = str[..index];
+ var max = str[(index + 1)..];
+ _ = int.TryParse(min, out RandomMinimum);
+ _ = int.TryParse(max, out RandomMaximum);
+
+ if (RandomMinimum == RandomMaximum)
{
- PropertyName = name;
- PropertyValue = value;
+ PropertyValue = RandomMinimum.ToString();
+ Debug.WriteLine($"{PropertyName} randomization range Min/Max same?");
}
-
- public void SetScreenedValue(string[] arr)
+ else
{
- int index = Array.IndexOf(arr, PropertyValue);
- PropertyValue = index > -1 ? index.ToString() : PropertyValue;
- }
-
- public static readonly IReadOnlyList Prefixes = new[] { Apply, Require, Exclude };
- private const char Exclude = '!';
- private const char Require = '=';
- private const char Apply = '.';
- private const char SplitRange = ',';
-
- ///
- /// Character which divides a property and a value.
- ///
- ///
- /// Example:
- /// =Species=1
- /// The second = is the split.
- ///
- public const char SplitInstruction = '=';
-
- // Extra Functionality
- private int RandomMinimum, RandomMaximum;
- public bool Random { get; private set; }
- public int RandomValue => RandUtil.Rand.Next(RandomMinimum, RandomMaximum + 1);
-
- public void SetRandRange(string pv)
- {
- string str = pv.Substring(1);
- var split = str.Split(SplitRange);
- int.TryParse(split[0], out RandomMinimum);
- int.TryParse(split[1], out RandomMaximum);
-
- if (RandomMinimum == RandomMaximum)
- {
- PropertyValue = RandomMinimum.ToString();
- Debug.WriteLine(PropertyName + " randomization range Min/Max same?");
- }
- else
- {
- Random = true;
- }
- }
-
- public static IEnumerable GetFilters(IEnumerable lines)
- {
- var raw = GetRelevantStrings(lines, Exclude, Require);
- return from line in raw
- let eval = line[0] == Require
- let split = line.Substring(1).Split(SplitInstruction)
- where split.Length == 2 && !string.IsNullOrWhiteSpace(split[0])
- select new StringInstruction(split[0], split[1]) { Evaluator = eval };
- }
-
- public static IEnumerable GetInstructions(IEnumerable lines)
- {
- var raw = GetRelevantStrings(lines, Apply).Select(line => line.Substring(1));
- return from line in raw
- select line.Split(SplitInstruction) into split
- where split.Length == 2
- select new StringInstruction(split[0], split[1]);
- }
-
- ///
- /// Weeds out invalid lines and only returns those with a valid first character.
- ///
- private static IEnumerable GetRelevantStrings(IEnumerable lines, params char[] pieces)
- {
- return lines.Where(line => !string.IsNullOrEmpty(line) && pieces.Any(z => z == line[0]));
+ Random = true;
}
}
+
+ ///
+ /// Gets a list of s from the input .
+ ///
+ public static List GetFilters(ReadOnlySpan text) => GetFilters(text.EnumerateLines());
+
+ ///
+ /// Gets a list of filters from the input .
+ ///
+ public static List GetFilters(ReadOnlySpan lines)
+ {
+ var result = new List(lines.Length);
+ foreach (var line in lines)
+ {
+ if (TryParseFilter(line, out var entry))
+ result.Add(entry);
+ }
+ return result;
+ }
+
+ ///
+ /// Gets a list of filters from the input .
+ ///
+ public static List GetFilters(SpanLineEnumerator lines)
+ {
+ var result = new List();
+ foreach (var line in lines)
+ {
+ if (TryParseFilter(line, out var entry))
+ result.Add(entry);
+ }
+ return result;
+ }
+
+ ///
+ /// Gets a list of filters from the input .
+ ///
+ public static List GetFilters(IReadOnlyList lines)
+ {
+ var result = new List(lines.Count);
+ foreach (var line in lines)
+ {
+ if (TryParseFilter(line, out var entry))
+ result.Add(entry);
+ }
+ return result;
+ }
+
+ ///
+ /// Gets a list of filters from the input .
+ ///
+ public static List GetFilters(IEnumerable lines)
+ {
+ var result = new List();
+ foreach (var line in lines)
+ {
+ if (TryParseFilter(line, out var entry))
+ result.Add(entry);
+ }
+ return result;
+ }
+
+ ///
+ /// Gets a list of instructions from the input .
+ ///
+ public static List GetInstructions(ReadOnlySpan text) => GetInstructions(text.EnumerateLines());
+
+ ///
+ /// Gets a list of instructions from the input .
+ ///
+ public static List GetInstructions(ReadOnlySpan lines)
+ {
+ var result = new List(lines.Length);
+ foreach (var line in lines)
+ {
+ if (TryParseInstruction(line, out var entry))
+ result.Add(entry);
+ }
+ return result;
+ }
+
+ ///
+ /// Gets a list of instructions from the input .
+ ///
+ public static List GetInstructions(SpanLineEnumerator lines)
+ {
+ var result = new List();
+ foreach (var line in lines)
+ {
+ if (TryParseInstruction(line, out var entry))
+ result.Add(entry);
+ }
+ return result;
+ }
+
+ ///
+ /// Gets a list of instructions from the input .
+ ///
+ public static List GetInstructions(IReadOnlyList lines)
+ {
+ var result = new List(lines.Count);
+ foreach (var line in lines)
+ {
+ if (TryParseInstruction(line, out var entry))
+ result.Add(entry);
+ }
+ return result;
+ }
+
+ ///
+ /// Gets a list of instructions from the input .
+ ///
+ public static List GetInstructions(IEnumerable lines)
+ {
+ var result = new List();
+ foreach (var line in lines)
+ {
+ if (TryParseInstruction(line, out var entry))
+ result.Add(entry);
+ }
+ return result;
+ }
+
+ ///
+ /// Tries to parse a filter from the input .
+ ///
+ public static bool TryParseFilter(ReadOnlySpan line, [NotNullWhen(true)] out StringInstruction? entry)
+ {
+ entry = null;
+ if (line.Length is 0)
+ return false;
+ var comparer = GetComparer(line[0]);
+ if (!comparer.IsSupported)
+ return false;
+ return TryParseSplitTuple(line[1..], ref entry, comparer);
+ }
+
+ ///
+ /// Tries to parse a instruction from the input .
+ ///
+ public static bool TryParseInstruction(ReadOnlySpan line, [NotNullWhen(true)] out StringInstruction? entry)
+ {
+ entry = null;
+ if (!line.StartsWith(Apply))
+ return false;
+ return TryParseSplitTuple(line[1..], ref entry);
+ }
+
+ ///
+ /// Tries to split a tuple from the input .
+ ///
+ public static bool TryParseSplitTuple(ReadOnlySpan tuple, [NotNullWhen(true)] ref StringInstruction? entry, InstructionComparer eval = default)
+ {
+ if (!TryParseSplitTuple(tuple, out var name, out var value))
+ return false;
+ entry = new StringInstruction(name.ToString(), value.ToString(), eval);
+ return true;
+ }
+
+ ///
+ /// Tries to split a tuple from the input .
+ ///
+ public static bool TryParseSplitTuple(ReadOnlySpan tuple, out ReadOnlySpan name, out ReadOnlySpan value)
+ {
+ name = default;
+ value = default;
+ var splitIndex = tuple.IndexOf(SplitInstruction);
+ if (splitIndex <= 0)
+ return false;
+
+ name = tuple[..splitIndex];
+ if (name.IsWhiteSpace())
+ return false;
+
+ value = tuple[(splitIndex + 1)..];
+ var noExtra = value.IndexOf(SplitInstruction);
+ return noExtra == -1;
+ }
+
+ ///
+ /// Gets the from the input .
+ ///
+ public static InstructionComparer GetComparer(char opCode) => opCode switch
+ {
+ FilterEqual => IsEqual,
+ FilterNotEqual => IsNotEqual,
+ FilterGreaterThan => IsGreaterThan,
+ FilterLessThan => IsLessThan,
+ FilterGreaterThanOrEqual => IsGreaterThanOrEqual,
+ FilterLessThanOrEqual => IsLessThanOrEqual,
+ _ => None,
+ };
+}
+
+///
+/// Value comparison type
+///
+public enum InstructionComparer : byte
+{
+ None,
+ IsEqual,
+ IsNotEqual,
+ IsGreaterThan,
+ IsGreaterThanOrEqual,
+ IsLessThan,
+ IsLessThanOrEqual,
+}
+
+///
+/// Extension methods for
+///
+public static class InstructionComparerExtensions
+{
+ extension(InstructionComparer comparer)
+ {
+ ///
+ /// Indicates if the is supported by the logic.
+ ///
+ /// True if supported, false if unsupported.
+ public bool IsSupported => comparer switch
+ {
+ IsEqual => true,
+ IsNotEqual => true,
+ IsGreaterThan => true,
+ IsGreaterThanOrEqual => true,
+ IsLessThan => true,
+ IsLessThanOrEqual => true,
+ _ => false,
+ };
+
+ ///
+ /// Checks if the compare operator is satisfied by a boolean comparison result.
+ ///
+ /// Result from Equals comparison
+ /// True if satisfied
+ /// Only use this method if the comparison is boolean only. Use the otherwise.
+ public bool IsCompareEquivalence(bool compareResult) => comparer switch
+ {
+ IsEqual => compareResult,
+ IsNotEqual => !compareResult,
+ _ => false,
+ };
+
+ ///
+ /// Checks if the compare operator is satisfied by the result.
+ ///
+ /// Result from CompareTo
+ /// True if satisfied
+ public bool IsCompareOperator(int compareResult) => comparer switch
+ {
+ IsEqual => compareResult is 0,
+ IsNotEqual => compareResult is not 0,
+ IsGreaterThan => compareResult > 0,
+ IsGreaterThanOrEqual => compareResult >= 0,
+ IsLessThan => compareResult < 0,
+ IsLessThanOrEqual => compareResult <= 0,
+ _ => false,
+ };
+ }
}
diff --git a/NHSE.Core/Editing/Batch/StringInstructionSet.cs b/NHSE.Core/Editing/Batch/StringInstructionSet.cs
index f903e41..46ea193 100644
--- a/NHSE.Core/Editing/Batch/StringInstructionSet.cs
+++ b/NHSE.Core/Editing/Batch/StringInstructionSet.cs
@@ -1,38 +1,143 @@
-using System.Collections.Generic;
-using System.Linq;
+using System;
+using System.Collections.Generic;
+using System.Text;
-namespace NHSE.Core
+namespace NHSE.Core;
+
+///
+/// Processes input of strings into a list of valid Filters and Instructions.
+///
+public sealed class StringInstructionSet
{
///
- /// Processes input of strings into a list of valid Filters and Instructions.
+ /// Filters to check if the object should be modified.
///
- public sealed class StringInstructionSet
+ public readonly IReadOnlyList Filters;
+
+ ///
+ /// Instructions to modify the object.
+ ///
+ public readonly IReadOnlyList Instructions;
+
+ private const char SetSeparatorChar = ';';
+
+ public StringInstructionSet(IReadOnlyList filters, IReadOnlyList instructions)
{
- public readonly IReadOnlyList Filters;
- public readonly IReadOnlyList Instructions;
-
- private const string SetSeparator = ";";
-
- public StringInstructionSet(IReadOnlyList filters, IReadOnlyList instructions)
- {
- Filters = filters;
- Instructions = instructions;
- }
-
- public StringInstructionSet(ICollection set)
- {
- Filters = StringInstruction.GetFilters(set).ToList();
- Instructions = StringInstruction.GetInstructions(set).ToList();
- }
-
- public static IEnumerable GetBatchSets(IList lines)
- {
- int start = 0;
- while (start < lines.Count)
- {
- var list = lines.Skip(start).TakeWhile(_ => !lines[start++].StartsWith(SetSeparator)).ToList();
- yield return new StringInstructionSet(list);
- }
- }
+ Filters = filters;
+ Instructions = instructions;
}
-}
+
+ public StringInstructionSet(ReadOnlySpan text)
+ {
+ var set = text.EnumerateLines();
+ Filters = StringInstruction.GetFilters(set);
+ Instructions = StringInstruction.GetInstructions(set);
+ }
+
+ public StringInstructionSet(SpanLineEnumerator set)
+ {
+ Filters = StringInstruction.GetFilters(set);
+ Instructions = StringInstruction.GetInstructions(set);
+ }
+
+ public StringInstructionSet(ReadOnlySpan set)
+ {
+ Filters = StringInstruction.GetFilters(set);
+ Instructions = StringInstruction.GetInstructions(set);
+ }
+
+ ///
+ /// Gets a list of s from the input .
+ ///
+ public static StringInstructionSet[] GetBatchSets(ReadOnlySpan lines)
+ {
+ int ctr = 0;
+ int start = 0;
+ while (start < lines.Length)
+ {
+ var slice = lines[start..];
+ var count = GetInstructionSetLength(slice);
+ ctr++;
+ start += count + 1;
+ }
+
+ var result = new StringInstructionSet[ctr];
+ ctr = 0;
+ start = 0;
+ while (start < lines.Length)
+ {
+ var slice = lines[start..];
+ var count = GetInstructionSetLength(slice);
+ var set = slice[..count];
+ result[ctr++] = new StringInstructionSet(set);
+ start += count + 1;
+ }
+ return result;
+ }
+
+ ///
+ /// Gets a list of s from the input .
+ ///
+ public static StringInstructionSet[] GetBatchSets(ReadOnlySpan text)
+ {
+ int ctr = 0;
+ int start = 0;
+ while (start < text.Length)
+ {
+ var slice = text[start..];
+ var count = GetInstructionSetLength(slice);
+ ctr++;
+ start += count + 1;
+ }
+
+ var result = new StringInstructionSet[ctr];
+ ctr = 0;
+ start = 0;
+ while (start < text.Length)
+ {
+ var slice = text[start..];
+ var count = GetInstructionSetLength(slice);
+ var set = slice[..count];
+ result[ctr++] = new StringInstructionSet(set);
+ start += count + 1;
+ }
+ return result;
+ }
+
+ ///
+ /// Scans through the to count the amount of characters to consume.
+ ///
+ /// Multi line string
+ /// Amount of characters comprising a set of instructions
+ public static int GetInstructionSetLength(ReadOnlySpan text)
+ {
+ int start = 0;
+ while (start < text.Length)
+ {
+ var line = text[start..];
+ if (line.Length != 0 && line[0] == SetSeparatorChar)
+ return start;
+ var next = line.IndexOf('\n');
+ if (next == -1)
+ return text.Length;
+ start += next + 1;
+ }
+ return start;
+ }
+
+ ///
+ /// Scans through the to count the amount of valid lines to consume.
+ ///
+ /// Amount of lines comprising a set of instructions.
+ public static int GetInstructionSetLength(ReadOnlySpan lines)
+ {
+ int start = 0;
+ while (start < lines.Length)
+ {
+ var line = lines[start++];
+ if (line.StartsWith(SetSeparatorChar))
+ return start;
+ }
+ return start;
+ }
+}
\ No newline at end of file
diff --git a/NHSE.Core/Editing/FieldItem/FieldItemColumn.cs b/NHSE.Core/Editing/FieldItem/FieldItemColumn.cs
index ae5fe7d..c45f77f 100644
--- a/NHSE.Core/Editing/FieldItem/FieldItemColumn.cs
+++ b/NHSE.Core/Editing/FieldItem/FieldItemColumn.cs
@@ -1,24 +1,23 @@
-namespace NHSE.Core
+namespace NHSE.Core;
+
+public sealed class FieldItemColumn
{
- public sealed class FieldItemColumn
+ /// X Coordinate within the Field Item Layer
+ public readonly int X;
+
+ /// Y Coordinate within the Field Item Layer
+ public readonly int Y;
+
+ /// Offset relative to the start of the Field Item Layer
+ public readonly int Offset;
+
+ public readonly byte[] Data;
+
+ public FieldItemColumn(int x, int y, int offset, byte[] data)
{
- /// X Coordinate within the Field Item Layer
- public readonly int X;
-
- /// Y Coordinate within the Field Item Layer
- public readonly int Y;
-
- /// Offset relative to the start of the Field Item Layer
- public readonly int Offset;
-
- public readonly byte[] Data;
-
- public FieldItemColumn(int x, int y, int offset, byte[] data)
- {
- X = x;
- Y = y;
- Offset = offset;
- Data = data;
- }
+ X = x;
+ Y = y;
+ Offset = offset;
+ Data = data;
}
-}
+}
\ No newline at end of file
diff --git a/NHSE.Core/Editing/FieldItem/FieldItemDropper.cs b/NHSE.Core/Editing/FieldItem/FieldItemDropper.cs
index a7004b4..021dd9c 100644
--- a/NHSE.Core/Editing/FieldItem/FieldItemDropper.cs
+++ b/NHSE.Core/Editing/FieldItem/FieldItemDropper.cs
@@ -1,126 +1,126 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Linq;
-namespace NHSE.Core
+namespace NHSE.Core;
+
+///
+/// Converts into columns of writable Item tiles.
+///
+public static class FieldItemDropper
{
- ///
- /// Converts into columns of writable Item tiles.
- ///
- public static class FieldItemDropper
+ private const int MapHeight = FieldItemLayer.FieldItemHeight;
+ private const int MapWidth = FieldItemLayer.FieldItemWidth;
+
+ // Each dropped item is a 2x2 square, with the top left tile being the root node, and the other 3 being extensions pointing back to the root.
+
+ ///
+ /// Raw X coordinate for the top-left item root tile.
+ /// Raw Y coordinate for the top-left item root tile.
+ /// Total count of items to be dropped (not tiles).
+ /// Count of items tall the overall spawn-rectangle is.
+ /// Excluded outer tile count. Useful for enforcing that beach acre tiles are skipped.
+ /// Excluded outer tile count. Useful for enforcing that beach acre tiles are skipped.
+ public static bool CanFitDropped(int x, int y, int totalCount, int yCount, int borderX, int borderY)
{
- private const int MapHeight = FieldItemLayer.FieldItemHeight;
- private const int MapWidth = FieldItemLayer.FieldItemWidth;
-
- // Each dropped item is a 2x2 square, with the top left tile being the root node, and the other 3 being extensions pointing back to the root.
-
- ///
- /// Raw X coordinate for the top-left item root tile.
- /// Raw Y coordinate for the top-left item root tile.
- /// Total count of items to be dropped (not tiles).
- /// Count of items tall the overall spawn-rectangle is.
- /// Excluded outer tile count. Useful for enforcing that beach acre tiles are skipped.
- /// Excluded outer tile count. Useful for enforcing that beach acre tiles are skipped.
- public static bool CanFitDropped(int x, int y, int totalCount, int yCount, int borderX, int borderY)
- {
- return CanFitDropped(x, y, totalCount, yCount, borderX, borderX, borderY, borderY);
- }
-
- ///
- /// Checks if the requested of items can be dropped on the field item layer. Does not check terrain or existing items.
- ///
- /// Coordinates should be 32x32 style instead of 16x16.
- /// Raw X coordinate for the top-left item root tile.
- /// Raw Y coordinate for the top-left item root tile.
- /// Total count of items to be dropped (not tiles).
- /// Count of items tall the overall spawn-rectangle is.
- /// Excluded outer tile count. Useful for enforcing that beach acre tiles are skipped.
- /// Excluded outer tile count. Useful for enforcing that beach acre tiles are skipped.
- /// Excluded outer tile count. Useful for enforcing that beach acre tiles are skipped.
- /// Excluded outer tile count. Useful for enforcing that beach acre tiles are skipped.
- /// True if can fit, false if not.
- public static bool CanFitDropped(int x, int y, int totalCount, int yCount, int leftX, int rightX, int topY, int botY)
- {
- var xCount = totalCount / yCount;
- if (x < leftX || (x + (xCount * 2)) > MapWidth - rightX)
- return false;
- if (y < topY || (y + (yCount * 2)) > MapHeight - botY)
- return false;
-
- return totalCount < (MapHeight * MapWidth / 32);
- }
-
- public static IReadOnlyList InjectItemsAsDropped(int mapX, int mapY, IReadOnlyList
- item)
- {
- int yStride = (item.Count > 16) ? 16 : item.Count;
- return InjectItemsAsDropped(mapX, mapY, item, yStride);
- }
-
- public static IReadOnlyList InjectItemsAsDropped(int mapX, int mapY, IReadOnlyList
- item, int yStride)
- {
- var xStride = item.Count / yStride;
- List result = new(yStride * xStride);
- for (int i = 0; i < xStride; i++)
- {
- var x = mapX + (i * 2);
- var y = mapY;
- var itemSlice = item.Skip(i * yStride).Take(yStride).ToArray();
-
- // Root+ExtensionY
- var offset = GetTileOffset(x, y);
- var data = GetColumnRoot(itemSlice);
- var column = new FieldItemColumn(x, y, offset, data);
- result.Add(column);
-
- // Ex X/XY
- ++x;
- offset = GetTileOffset(x, y);
- data = GetColumnExtension(itemSlice);
- column = new FieldItemColumn(x, y, offset, data);
- result.Add(column);
- }
- return result;
- }
-
- private static byte[] GetColumnRoot(Item[] items)
- {
- var col = new Item[items.Length * 2];
- for (int i = 0; i < items.Length; i++)
- {
- var item = items[i];
- var idx = i * 2;
- col[idx] = GetDroppedItem(item);
- col[idx + 1] = GetExtension(item, 0, 1);
- }
- return Item.SetArray(col);
- }
-
- private static byte[] GetColumnExtension(Item[] items)
- {
- var col = new Item[items.Length * 2];
- for (int i = 0; i < items.Length; i++)
- {
- var item = items[i];
- var idx = i * 2;
- col[idx] = GetExtension(item, 1, 0);
- col[idx + 1] = GetExtension(item, 1, 1);
- }
- return Item.SetArray(col);
- }
-
- private static int GetTileOffset(int x, int y)
- {
- return Item.SIZE * (y + (x * MapHeight));
- }
-
- private static Item GetDroppedItem(Item item)
- {
- var copy = new Item();
- copy.CopyFrom(item);
- copy.ClearFlags();
- copy.IsDropped = true;
- return copy;
- }
-
- private static Item GetExtension(Item item, byte x, byte y) => new(Item.EXTENSION) { ExtensionItemId = item.ItemId, ExtensionX = x, ExtensionY = y };
+ return CanFitDropped(x, y, totalCount, yCount, borderX, borderX, borderY, borderY);
}
-}
+
+ ///
+ /// Checks if the requested of items can be dropped on the field item layer. Does not check terrain or existing items.
+ ///
+ /// Coordinates should be 32x32 style instead of 16x16.
+ /// Raw X coordinate for the top-left item root tile.
+ /// Raw Y coordinate for the top-left item root tile.
+ /// Total count of items to be dropped (not tiles).
+ /// Count of items tall the overall spawn-rectangle is.
+ /// Excluded outer tile count. Useful for enforcing that beach acre tiles are skipped.
+ /// Excluded outer tile count. Useful for enforcing that beach acre tiles are skipped.
+ /// Excluded outer tile count. Useful for enforcing that beach acre tiles are skipped.
+ /// Excluded outer tile count. Useful for enforcing that beach acre tiles are skipped.
+ /// True if can fit, false if not.
+ public static bool CanFitDropped(int x, int y, int totalCount, int yCount, int leftX, int rightX, int topY, int botY)
+ {
+ var xCount = totalCount / yCount;
+ if (x < leftX || (x + (xCount * 2)) > MapWidth - rightX)
+ return false;
+ if (y < topY || (y + (yCount * 2)) > MapHeight - botY)
+ return false;
+
+ return totalCount < (MapHeight * MapWidth / 32);
+ }
+
+ public static IReadOnlyList InjectItemsAsDropped(int mapX, int mapY, IReadOnlyList
- item)
+ {
+ int yStride = (item.Count > 16) ? 16 : item.Count;
+ return InjectItemsAsDropped(mapX, mapY, item, yStride);
+ }
+
+ public static IReadOnlyList InjectItemsAsDropped(int mapX, int mapY, IReadOnlyList
- item, int yStride)
+ {
+ var xStride = item.Count / yStride;
+ List result = new(yStride * xStride);
+ for (int i = 0; i < xStride; i++)
+ {
+ var x = mapX + (i * 2);
+ var y = mapY;
+ var itemSlice = item.Skip(i * yStride).Take(yStride).ToArray();
+
+ // Root+ExtensionY
+ var offset = GetTileOffset(x, y);
+ var data = GetColumnRoot(itemSlice);
+ var column = new FieldItemColumn(x, y, offset, data);
+ result.Add(column);
+
+ // Ex X/XY
+ ++x;
+ offset = GetTileOffset(x, y);
+ data = GetColumnExtension(itemSlice);
+ column = new FieldItemColumn(x, y, offset, data);
+ result.Add(column);
+ }
+ return result;
+ }
+
+ private static byte[] GetColumnRoot(ReadOnlySpan
- items)
+ {
+ var col = new Item[items.Length * 2];
+ for (int i = 0; i < items.Length; i++)
+ {
+ var item = items[i];
+ var idx = i * 2;
+ col[idx] = GetDroppedItem(item);
+ col[idx + 1] = GetExtension(item, 0, 1);
+ }
+ return Item.SetArray(col);
+ }
+
+ private static byte[] GetColumnExtension(ReadOnlySpan
- items)
+ {
+ var col = new Item[items.Length * 2];
+ for (int i = 0; i < items.Length; i++)
+ {
+ var item = items[i];
+ var idx = i * 2;
+ col[idx] = GetExtension(item, 1, 0);
+ col[idx + 1] = GetExtension(item, 1, 1);
+ }
+ return Item.SetArray(col);
+ }
+
+ private static int GetTileOffset(int x, int y)
+ {
+ return Item.SIZE * (y + (x * MapHeight));
+ }
+
+ private static Item GetDroppedItem(Item item)
+ {
+ var copy = new Item();
+ copy.CopyFrom(item);
+ copy.ClearFlags();
+ copy.IsDropped = true;
+ return copy;
+ }
+
+ private static Item GetExtension(Item item, byte x, byte y) => new(Item.EXTENSION) { ExtensionItemId = item.ItemId, ExtensionX = x, ExtensionY = y };
+}
\ No newline at end of file
diff --git a/NHSE.Core/Editing/Inventory/PlayerItemSet.cs b/NHSE.Core/Editing/Inventory/PlayerItemSet.cs
index b6ae103..45dc446 100644
--- a/NHSE.Core/Editing/Inventory/PlayerItemSet.cs
+++ b/NHSE.Core/Editing/Inventory/PlayerItemSet.cs
@@ -1,135 +1,135 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using static System.Buffers.Binary.BinaryPrimitives;
-namespace NHSE.Core
+namespace NHSE.Core;
+
+///
+/// Handles operations for parsing the player inventory.
+///
+///
+/// Player Inventory is comprised of multiple values, which we won't bother reimplementing as a convertible data structure.
+///
Refer to GSavePlayerItemBaggage's ItemBag & ItemPocket in the dumped c-structure schema.
+///
+public static class PlayerItemSet
{
+ private const int ItemSet_Quantity = 2; // Pouch (Bag) & Pocket.
+ private const int ItemSet_ItemCount = 20; // 20 items per item set.
+ private const int ItemSet_ItemSize = Item.SIZE * ItemSet_ItemCount;
+ private const int ItemSet_MetaSize = 4 + ItemSet_ItemCount;
+ private const int ItemSet_TotalSize = (ItemSet_ItemSize + ItemSet_MetaSize) * ItemSet_Quantity;
+ private const int ShiftToTopOfStructure = -ItemSet_MetaSize - (Item.SIZE * ItemSet_ItemCount); // shifts slot1 offset => top of data structure
+
///
- /// Handles operations for parsing the player inventory.
+ /// Gets the Offset and Size to read from based on the Item 1 RAM offset.
///
- ///
- /// Player Inventory is comprised of multiple values, which we won't bother reimplementing as a convertible data structure.
- ///
Refer to GSavePlayerItemBaggage's ItemBag & ItemPocket in the dumped c-structure schema.
- ///
- public static class PlayerItemSet
+ /// Item Slot 1 offset in RAM
+ /// Offset to read from
+ /// Length of data to read from
+ public static void GetOffsetLength(uint slot1, out uint offset, out int length)
{
- private const int ItemSet_Quantity = 2; // Pouch (Bag) & Pocket.
- private const int ItemSet_ItemCount = 20; // 20 items per item set.
- private const int ItemSet_ItemSize = Item.SIZE * ItemSet_ItemCount;
- private const int ItemSet_MetaSize = 4 + ItemSet_ItemCount;
- private const int ItemSet_TotalSize = (ItemSet_ItemSize + ItemSet_MetaSize) * ItemSet_Quantity;
- private const int ShiftToTopOfStructure = -ItemSet_MetaSize - (Item.SIZE * ItemSet_ItemCount); // shifts slot1 offset => top of data structure
-
- ///
- /// Gets the Offset and Size to read from based on the Item 1 RAM offset.
- ///
- /// Item Slot 1 offset in RAM
- /// Offset to read from
- /// Length of data to read from
- public static void GetOffsetLength(uint slot1, out uint offset, out int length)
- {
- offset = (uint)((int)slot1 + ShiftToTopOfStructure);
- length = ItemSet_TotalSize;
- }
-
- ///
- /// Compares the raw data to the expected data layout.
- ///
- /// Raw RAM from the game from the offset read (as per ).
- /// True if valid, false if not valid or corrupt.
- public static bool ValidateItemBinary(byte[] data)
- {
- // Check the unlocked slot count -- expect 0,10,20
- var bagCount = BitConverter.ToUInt32(data, ItemSet_ItemSize);
- if (bagCount > ItemSet_ItemCount || bagCount % 10 != 0) // pouch21-39 count
- return false;
-
- var pocketCount = BitConverter.ToUInt32(data, ItemSet_ItemSize + ItemSet_MetaSize + ItemSet_ItemSize);
- if (pocketCount != ItemSet_ItemCount) // pouch0-19 count should be 20.
- return false;
-
- // Check the item wheel binding -- expect -1 or [0,7]
- // Disallow duplicate binds!
- // Don't bother checking that bind[i] (when ! -1) is not NONE at items[i]. We don't need to check everything!
- var bound = new List();
- if (!ValidateBindList(data, ItemSet_ItemSize + 4, bound))
- return false;
- if (!ValidateBindList(data, ItemSet_ItemSize + 4 + (ItemSet_ItemSize + ItemSet_MetaSize), bound))
- return false;
-
- return true;
- }
-
- private static bool ValidateBindList(byte[] data, int bindStart, ICollection bound)
- {
- for (int i = 0; i < ItemSet_ItemCount; i++)
- {
- var bind = data[bindStart + i];
- if (bind == 0xFF) // Not bound
- continue;
- if (bind > 7) // Only [0,7] permitted as the wheel has 8 spots
- return false;
- if (bound.Contains(bind)) // Wheel index is already bound to another item slot
- return false;
-
- bound.Add(bind);
- }
-
- return true;
- }
-
- ///
- /// Reads the items present in the player inventory packet and returns the list of items.
- ///
- /// Player Inventory packet
- public static Item[] ReadPlayerInventory(byte[] data)
- {
- var items = GetEmptyItemArray(40);
- ReadPlayerInventory(data, items);
- return items;
- }
-
- private static Item[] GetEmptyItemArray(int count)
- {
- var items = new Item[count];
- for (int i = 0; i < items.Length; i++)
- items[i] = new Item();
- return items;
- }
-
- ///
- /// Reads the items present in the player inventory packet into the list of items.
- ///
- /// Player Inventory packet
- /// 40 Item array
- public static void ReadPlayerInventory(byte[] data, IReadOnlyList- destination)
- {
- var pocket2 = destination.Take(20).ToArray();
- var pocket1 = destination.Skip(20).ToArray();
- var p1 = Item.GetArray(data.Slice(0, ItemSet_ItemSize));
- var p2 = Item.GetArray(data.Slice(ItemSet_ItemSize + 0x18, ItemSet_ItemSize));
-
- for (int i = 0; i < pocket1.Length; i++)
- pocket1[i].CopyFrom(p1[i]);
-
- for (int i = 0; i < pocket2.Length; i++)
- pocket2[i].CopyFrom(p2[i]);
- }
-
- ///
- /// Writes the items in the list of items to the player inventory packet.
- ///
- /// Player Inventory packet
- /// 40 Item array
- public static void WritePlayerInventory(byte[] data, IReadOnlyList
- source)
- {
- var pocket2 = source.Take(20).ToArray();
- var pocket1 = source.Skip(20).ToArray();
- var p1 = Item.SetArray(pocket1);
- var p2 = Item.SetArray(pocket2);
-
- p1.CopyTo(data, 0);
- p2.CopyTo(data, ItemSet_ItemSize + 0x18);
- }
+ offset = (uint)((int)slot1 + ShiftToTopOfStructure);
+ length = ItemSet_TotalSize;
}
-}
+
+ ///
+ /// Compares the raw data to the expected data layout.
+ ///
+ /// Raw RAM from the game from the offset read (as per ).
+ /// True if valid, false if not valid or corrupt.
+ public static bool ValidateItemBinary(ReadOnlySpan data)
+ {
+ // Check the unlocked slot count -- expect 0,10,20
+ var bagCount = ReadUInt32LittleEndian(data[ItemSet_ItemSize..]);
+ if (bagCount > ItemSet_ItemCount || bagCount % 10 != 0) // pouch21-39 count
+ return false;
+
+ var pocketCount = ReadUInt32LittleEndian(data[(ItemSet_ItemSize + ItemSet_MetaSize + ItemSet_ItemSize)..]);
+ if (pocketCount != ItemSet_ItemCount) // pouch0-19 count should be 20.
+ return false;
+
+ // Check the item wheel binding -- expect -1 or [0,7]
+ // Disallow duplicate binds!
+ // Don't bother checking that bind[i] (when ! -1) is not NONE at items[i]. We don't need to check everything!
+ var bound = new List();
+ if (!ValidateBindList(data, ItemSet_ItemSize + 4, bound))
+ return false;
+ if (!ValidateBindList(data, ItemSet_ItemSize + 4 + (ItemSet_ItemSize + ItemSet_MetaSize), bound))
+ return false;
+
+ return true;
+ }
+
+ private static bool ValidateBindList(ReadOnlySpan data, int bindStart, ICollection bound)
+ {
+ for (int i = 0; i < ItemSet_ItemCount; i++)
+ {
+ var bind = data[bindStart + i];
+ if (bind == 0xFF) // Not bound
+ continue;
+ if (bind > 7) // Only [0,7] permitted as the wheel has 8 spots
+ return false;
+ if (bound.Contains(bind)) // Wheel index is already bound to another item slot
+ return false;
+
+ bound.Add(bind);
+ }
+
+ return true;
+ }
+
+ ///
+ /// Reads the items present in the player inventory packet and returns the list of items.
+ ///
+ /// Player Inventory packet
+ public static Item[] ReadPlayerInventory(ReadOnlySpan data)
+ {
+ var items = GetEmptyItemArray(40);
+ ReadPlayerInventory(data, items);
+ return items;
+ }
+
+ private static Item[] GetEmptyItemArray(int count)
+ {
+ var items = new Item[count];
+ for (int i = 0; i < items.Length; i++)
+ items[i] = new Item();
+ return items;
+ }
+
+ ///
+ /// Reads the items present in the player inventory packet into the list of items.
+ ///
+ /// Player Inventory packet
+ /// 40 Item array
+ public static void ReadPlayerInventory(ReadOnlySpan data, IReadOnlyList
- destination)
+ {
+ var pocket2 = destination.Take(20).ToArray();
+ var pocket1 = destination.Skip(20).ToArray();
+ var p1 = Item.GetArray(data[..ItemSet_ItemSize]);
+ var p2 = Item.GetArray(data.Slice(ItemSet_ItemSize + 0x18, ItemSet_ItemSize));
+
+ for (int i = 0; i < pocket1.Length; i++)
+ pocket1[i].CopyFrom(p1[i]);
+
+ for (int i = 0; i < pocket2.Length; i++)
+ pocket2[i].CopyFrom(p2[i]);
+ }
+
+ ///
+ /// Writes the items in the list of items to the player inventory packet.
+ ///
+ /// Player Inventory packet
+ /// 40 Item array
+ public static void WritePlayerInventory(Span data, IReadOnlyList
- source)
+ {
+ var pocket2 = source.Take(20).ToArray();
+ var pocket1 = source.Skip(20).ToArray();
+ var p1 = Item.SetArray(pocket1);
+ var p2 = Item.SetArray(pocket2);
+
+ p1.CopyTo(data);
+ p2.CopyTo(data[(ItemSet_ItemSize + 0x18)..]);
+ }
+}
\ No newline at end of file
diff --git a/NHSE.Core/Editing/ItemRequest/IConfigItem.cs b/NHSE.Core/Editing/ItemRequest/IConfigItem.cs
index 82958b1..c670df3 100644
--- a/NHSE.Core/Editing/ItemRequest/IConfigItem.cs
+++ b/NHSE.Core/Editing/ItemRequest/IConfigItem.cs
@@ -1,23 +1,22 @@
-namespace NHSE.Core
+namespace NHSE.Core;
+
+///
+/// Interface describing how items should be configured prior to being dropped by the player.
+///
+public interface IConfigItem
{
///
- /// Interface describing how items should be configured prior to being dropped by the player.
+ /// Checks if the item should have wrapping paper applied.
///
- public interface IConfigItem
- {
- ///
- /// Checks if the item should have wrapping paper applied.
- ///
- bool WrapAllItems { get; }
+ bool WrapAllItems { get; }
- ///
- /// Wrapping paper type applied if is set.
- ///
- ItemWrappingPaper WrappingPaper { get; }
+ ///
+ /// Wrapping paper type applied if is set.
+ ///
+ ItemWrappingPaper WrappingPaper { get; }
- ///
- /// Checks if the Drop Compatibility check should be skipped.
- ///
- bool SkipDropCheck { get; }
- }
-}
+ ///
+ /// Checks if the Drop Compatibility check should be skipped.
+ ///
+ bool SkipDropCheck { get; }
+}
\ No newline at end of file
diff --git a/NHSE.Core/Editing/ItemRequest/ItemParser.cs b/NHSE.Core/Editing/ItemRequest/ItemParser.cs
index 53ec045..96bd067 100644
--- a/NHSE.Core/Editing/ItemRequest/ItemParser.cs
+++ b/NHSE.Core/Editing/ItemRequest/ItemParser.cs
@@ -2,383 +2,385 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using static System.Buffers.Binary.BinaryPrimitives;
-namespace NHSE.Core
+namespace NHSE.Core;
+
+///
+/// Logic for retrieving details based off input strings.
+///
+public static class ItemParser
{
///
- /// Logic for retrieving details based off input strings.
+ /// Invert the recipe dictionary so we can look up recipe IDs from an input item ID.
///
- public static class ItemParser
+ public static readonly IReadOnlyDictionary InvertedRecipeDictionary =
+ RecipeList.Recipes.ToDictionary(z => z.Value, z => z.Key);
+
+ // Users can put spaces between item codes, or newlines. Recognize both!
+ private static readonly string[] SplittersHex = [" ", "\n", "\r\n"];
+ private static readonly string[] SplittersName = [",", "\n", "\r\n"];
+
+ ///
+ /// Gets a list of items from the requested hex string(s).
+ ///
+ ///
+ /// If the first input is a language code (2 characters), the logic will try to parse item names for that language instead of item IDs.
+ ///
+ /// 8 byte hex item values (u64 format)
+ /// Options for packaging items
+ /// End destination of the item
+ public static IReadOnlyCollection
- GetItemsFromUserInput(string requestHex, IConfigItem cfg, ItemDestination type = ItemDestination.PlayerDropped)
{
- ///
- /// Invert the recipe dictionary so we can look up recipe IDs from an input item ID.
- ///
- public static readonly IReadOnlyDictionary InvertedRecipeDictionary =
- RecipeList.Recipes.ToDictionary(z => z.Value, z => z.Key);
-
- // Users can put spaces between item codes, or newlines. Recognize both!
- private static readonly string[] SplittersHex = {" ", "\n", "\r\n"};
- private static readonly string[] SplittersName = {",", "\n", "\r\n"};
-
- ///
- /// Gets a list of items from the requested hex string(s).
- ///
- ///
- /// If the first input is a language code (2 characters), the logic will try to parse item names for that language instead of item IDs.
- ///
- /// 8 byte hex item values (u64 format)
- /// Options for packaging items
- /// End destination of the item
- public static IReadOnlyCollection
- GetItemsFromUserInput(string requestHex, IConfigItem cfg, ItemDestination type = ItemDestination.PlayerDropped)
+ try
{
- try
- {
- // having a language 2char code will cause an exception in parsing; this is fine and is handled by our catch statement.
- var split = requestHex.Split(SplittersHex, StringSplitOptions.RemoveEmptyEntries);
- return GetItemsHexCode(split, cfg, type);
- }
- catch
- {
- var split = requestHex.Split(SplittersName, StringSplitOptions.RemoveEmptyEntries);
- return GetItemsLanguage(split, cfg, type, GameLanguage.DefaultLanguage);
- }
+ // having a language 2char code will cause an exception in parsing; this is fine and is handled by our catch statement.
+ var split = requestHex.Split(SplittersHex, StringSplitOptions.RemoveEmptyEntries);
+ return GetItemsHexCode(split, cfg, type);
}
-
- ///
- /// Gets a list of DIY item cards from the requested list of DIY IDs.
- ///
- ///
- /// If the first input is a language code (2 characters), the logic will try to parse item names for that language instead of DIY IDs.
- ///
- /// 8 byte hex item values (u64 format)
- public static IReadOnlyCollection
- GetDIYsFromUserInput(string requestHex)
+ catch
{
- try
- {
- // having a language 2char code will cause an exception in parsing; this is fine and is handled by our catch statement.
- var split = requestHex.Split(SplittersHex, StringSplitOptions.RemoveEmptyEntries);
- return GetDIYItemsHexCode(split);
- }
- catch
- {
- var split = requestHex.Split(SplittersName, StringSplitOptions.RemoveEmptyEntries);
- return GetDIYItemsLanguage(split);
- }
- }
-
- ///
- /// Gets a list of items from the requested list of DIY hex code strings.
- ///
- ///
- /// If a hex code parse fails or a recipe ID does not exist, exceptions will be thrown.
- ///
- /// List of recipe IDs as u16 hex
- public static IReadOnlyCollection
- GetDIYItemsHexCode(IReadOnlyList split)
- {
- var result = new Item[split.Count];
- for (int i = 0; i < result.Length; i++)
- {
- var text = split[i].Trim();
- bool parse = ulong.TryParse(text, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out var value);
- if (!parse)
- throw new Exception($"Item value out of expected range ({text}).");
-
- if (!RecipeList.Recipes.TryGetValue((ushort)value, out _))
- throw new Exception($"DIY recipe appears to be invalid ({text}).");
-
- result[i] = new Item(Item.DIYRecipe) { Count = (ushort)value };
- }
- return result;
- }
-
- ///
- /// Gets a list of DIY item cards from the requested list of item name strings.
- ///
- ///
- /// If a item name parse fails or a recipe ID does not exist, exceptions will be thrown.
- ///
- /// List of item names
- /// Language code to parse with. If the first entry in is a language code, it will be used instead of .
- public static IReadOnlyCollection
- GetDIYItemsLanguage(IReadOnlyList split, string lang = GameLanguage.DefaultLanguage)
- {
- if (split.Count > 1 && split[0].Length < 3)
- {
- var langIndex = GameLanguage.GetLanguageIndex(split[0]);
- lang = GameLanguage.Language2Char(langIndex);
- split = split.Skip(1).ToArray();
- }
-
- var result = new Item[split.Count];
- for (int i = 0; i < result.Length; i++)
- {
- var text = split[i].Trim();
- var item = GetItem(text, lang);
- if (!InvertedRecipeDictionary.TryGetValue(item.ItemId, out var diy))
- throw new Exception($"DIY recipe appears to be invalid ({text}).");
-
- result[i] = new Item(Item.DIYRecipe) { Count = diy };
- }
- return result;
- }
-
- ///
- /// Gets a list of items from the requested list of item name strings.
- ///
- ///
- /// If a item name parse fails or the item ID does not exist as a known item, exceptions will be thrown.
- ///
- /// List of item names
- /// Item packaging options
- /// Destination where the item will end up at
- /// Language code to parse with. If the first entry in is a language code, it will be used instead of .
- public static IReadOnlyCollection
- GetItemsLanguage(IReadOnlyList split, IConfigItem config, ItemDestination type = ItemDestination.PlayerDropped, string lang = GameLanguage.DefaultLanguage)
- {
- if (split.Count > 1 && split[0].Length < 3)
- {
- var langIndex = GameLanguage.GetLanguageIndex(split[0]);
- lang = GameLanguage.Language2Char(langIndex);
- split = split.Skip(1).ToArray();
- }
-
- var strings = GameInfo.Strings.itemlistdisplay;
- var result = new Item[split.Count];
- for (int i = 0; i < result.Length; i++)
- {
- var text = split[i].Trim();
- var item = CreateItem(text, i, config, type, lang);
-
- if (item.ItemId >= strings.Length)
- throw new Exception($"Item requested is out of expected range ({item.ItemId:X4} > {strings.Length:X4}).");
- if (string.IsNullOrWhiteSpace(strings[item.ItemId]))
- throw new Exception($"Item requested does not have a valid name ({item.ItemId:X4}).");
-
- result[i] = item;
- }
- return result;
- }
-
- ///
- /// Gets a list of items from the requested list of item hex code strings.
- ///
- ///
- /// If a hex code parse fails or a recipe ID does not exist, exceptions will be thrown.
- ///
- /// List of recipe IDs as u16 hex
- /// Item packaging options
- /// Destination where the item will end up at
- public static IReadOnlyCollection
- GetItemsHexCode(IReadOnlyList split, IConfigItem config, ItemDestination type = ItemDestination.PlayerDropped)
- {
- var strings = GameInfo.Strings.itemlistdisplay;
- var result = new Item[split.Count];
- for (int i = 0; i < result.Length; i++)
- {
- var text = split[i].Trim();
- var convert = GetBytesFromString(text);
- var item = CreateItem(convert, i, config, type);
-
- if (item.ItemId >= strings.Length)
- throw new Exception($"Item requested is out of expected range ({item.ItemId:X4} > {strings.Length:X4}).");
- if (string.IsNullOrWhiteSpace(strings[item.ItemId]))
- throw new Exception($"Item requested does not have a valid name ({item.ItemId:X4}).");
-
- result[i] = item;
- }
- return result;
- }
-
- private static byte[] GetBytesFromString(string text)
- {
- if (!ulong.TryParse(text, NumberStyles.AllowHexSpecifier, CultureInfo.CurrentCulture, out var value))
- return Item.NONE.ToBytes();
- return BitConverter.GetBytes(value);
- }
-
- private static Item CreateItem(string name, int requestIndex, IConfigItem config, ItemDestination type, string lang = "en")
- {
- var item = GetItem(name, lang);
- if (item.IsNone)
- throw new Exception($"Failed to convert item (index {requestIndex}: {name}) for Language {lang}.");
-
- return FinalizeItem(requestIndex, config, type, item);
- }
-
- private static Item CreateItem(byte[] convert, int requestIndex, IConfigItem config, ItemDestination type)
- {
- Item item;
- try
- {
- if (convert.Length != Item.SIZE)
- throw new Exception();
- item = convert.ToClass
- ();
- }
- catch (Exception ex)
- {
- throw new Exception($"Failed to convert item (index {requestIndex}: {ex.Message}).");
- }
-
- return FinalizeItem(requestIndex, config, type, item);
- }
-
- private static Item FinalizeItem(int requestIndex, IConfigItem config, ItemDestination type, Item item)
- {
- if (type == ItemDestination.PlayerDropped)
- {
- if (!ItemInfo.IsSaneItemForDrop(item) && !config.SkipDropCheck)
- throw new Exception($"Unsupported item: (index {requestIndex}).");
- if (config.WrapAllItems && item.ShouldWrapItem())
- item.SetWrapping(ItemWrapping.WrappingPaper, config.WrappingPaper, true);
- }
-
- item.IsDropped = type == ItemDestination.FieldItemDropped;
-
- return item;
- }
-
- private static readonly CompareInfo Comparer = CultureInfo.InvariantCulture.CompareInfo;
- private const CompareOptions optIncludeSymbols = CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreWidth;
- private const CompareOptions optIgnoreSymbols = CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreSymbols | CompareOptions.IgnoreWidth;
-
- ///
- /// Gets a sensitive compare option, depending on the input string's qualities.
- ///
- /// Input string
- /// Default options if no symbols,
- private static CompareOptions GetCompareOption(string str) => str.Any(ch => !char.IsLetterOrDigit(ch) && !char.IsWhiteSpace(ch)) ? optIgnoreSymbols & ~CompareOptions.IgnoreSymbols : optIgnoreSymbols;
-
- ///
- /// Gets the first item name-value that contains the (case insensitive).
- ///
- /// Requested Item
- /// Game strings language to fetch with
- /// Returns if no match found.
- public static Item GetItem(string itemName, string lang = "en")
- {
- var gStrings = GameInfo.GetStrings(lang);
- var strings = gStrings.ItemDataSource;
- var parsedItem = GetItem(itemName, strings);
- if (parsedItem != Item.NO_ITEM)
- return parsedItem;
-
- if (gStrings.HasAssociatedItems(itemName, out var items))
- {
- if (items?.Count == 1)
- return new Item((ushort)items[0].Value);
- }
-
- return Item.NO_ITEM;
- }
-
- ///
- /// Gets the first item name-value that contains the (case insensitive).
- ///
- /// Requested Item
- /// Game strings
- /// Returns if no match found.
- public static Item GetItem(string itemName, IReadOnlyList strings)
- {
- if (TryGetItem(itemName, strings, out var id))
- return new Item(id);
- return Item.NO_ITEM;
- }
-
- ///
- /// Gets the first item name-value that contains the (case insensitive).
- ///
- /// Requested Item
- /// List of item name-values
- /// Item ID, if found. Otherwise, 0
- /// True if found, false if none.
- public static bool TryGetItem(string itemName, IReadOnlyList strings, out ushort value)
- {
- if (TryGetItem(itemName, strings, out value, optIncludeSymbols))
- return true;
- return TryGetItem(itemName, strings, out value, optIgnoreSymbols);
- }
-
- private static bool TryGetItem(string itemName, IEnumerable strings, out ushort value, CompareOptions opt)
- {
- foreach (var item in strings)
- {
- var result = Comparer.Compare(item.Text, 0, itemName, 0, opt);
- if (result != 0)
- continue;
-
- value = (ushort)item.Value;
- return true;
- }
-
- value = Item.NONE;
- return false;
- }
-
- ///
- /// Gets an enumerable list of item key-value pairs that contain (case insensitive) the requested .
- ///
- /// Item name
- /// Item names (and their Item ID values)
- public static IEnumerable GetItemsMatching(string itemName, IEnumerable strings)
- {
- var opt = GetCompareOption(itemName);
- foreach (var item in strings)
- {
- var result = Comparer.IndexOf(item.Text, itemName, opt);
- if (result < 0)
- continue;
- yield return item;
- }
- }
-
- ///
- /// Gets an enumerable list of item key-value pairs that contain (case insensitive) the requested .
- ///
- ///
- /// Orders the items based on the closest match ().
- ///
- /// Item name
- /// Item names (and their Item ID values)
- public static IEnumerable GetItemsMatchingOrdered(string itemName, IEnumerable strings)
- {
- var matches = GetItemsMatching(itemName, strings);
- return GetItemsClosestOrdered(itemName, matches);
- }
-
- ///
- /// Gets an enumerable list of item key-value pairs ordered by the closest for the requested .
- ///
- /// Item name
- /// Item names (and their Item ID values)
- public static IEnumerable GetItemsClosestOrdered(string itemName, IEnumerable strings)
- {
- return strings.OrderBy(z => LevenshteinDistance.Compute(z.Text, itemName));
- }
-
- ///
- /// Gets the Item Name and raw 8-byte value as a string.
- ///
- /// Item value
- public static string GetItemText(Item item)
- {
- var value = BitConverter.ToUInt64(item.ToBytesClass(), 0);
- var name = GameInfo.Strings.GetItemName(item.ItemId);
- return $"{name}: {value:X16}";
- }
-
- ///
- /// Gets the u16 item ID from the input hex code.
- ///
- /// Hex code for the item (preferably 4 digits)
- public static ushort GetID(string text)
- {
- if (!ulong.TryParse(text.Trim(), NumberStyles.AllowHexSpecifier, CultureInfo.CurrentCulture, out var value))
- return Item.NONE;
- return (ushort)value;
+ var split = requestHex.Split(SplittersName, StringSplitOptions.RemoveEmptyEntries);
+ return GetItemsLanguage(split, cfg, type, GameLanguage.DefaultLanguage);
}
}
- public enum ItemDestination
+ ///
+ /// Gets a list of DIY item cards from the requested list of DIY IDs.
+ ///
+ ///
+ /// If the first input is a language code (2 characters), the logic will try to parse item names for that language instead of DIY IDs.
+ ///
+ /// 8 byte hex item values (u64 format)
+ public static IReadOnlyCollection
- GetDIYsFromUserInput(string requestHex)
{
- PlayerDropped,
- FieldItemDropped,
- HeldItem,
+ try
+ {
+ // having a language 2char code will cause an exception in parsing; this is fine and is handled by our catch statement.
+ var split = requestHex.Split(SplittersHex, StringSplitOptions.RemoveEmptyEntries);
+ return GetDIYItemsHexCode(split);
+ }
+ catch
+ {
+ var split = requestHex.Split(SplittersName, StringSplitOptions.RemoveEmptyEntries);
+ return GetDIYItemsLanguage(split);
+ }
+ }
+
+ ///
+ /// Gets a list of items from the requested list of DIY hex code strings.
+ ///
+ ///
+ /// If a hex code parse fails or a recipe ID does not exist, exceptions will be thrown.
+ ///
+ /// List of recipe IDs as u16 hex
+ public static IReadOnlyCollection
- GetDIYItemsHexCode(IReadOnlyList split)
+ {
+ var result = new Item[split.Count];
+ for (int i = 0; i < result.Length; i++)
+ {
+ var text = split[i].Trim();
+ bool parse = ulong.TryParse(text, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out var value);
+ if (!parse)
+ throw new Exception($"Item value out of expected range ({text}).");
+
+ if (!RecipeList.Recipes.TryGetValue((ushort)value, out _))
+ throw new Exception($"DIY recipe appears to be invalid ({text}).");
+
+ result[i] = new Item(Item.DIYRecipe) { Count = (ushort)value };
+ }
+ return result;
+ }
+
+ ///
+ /// Gets a list of DIY item cards from the requested list of item name strings.
+ ///
+ ///
+ /// If an item name parse fails or a recipe ID does not exist, exceptions will be thrown.
+ ///
+ /// List of item names
+ /// Language code to parse with. If the first entry in is a language code, it will be used instead of .
+ public static IReadOnlyCollection
- GetDIYItemsLanguage(IReadOnlyList split, string lang = GameLanguage.DefaultLanguage)
+ {
+ if (split.Count > 1 && split[0].Length < 3)
+ {
+ var langIndex = GameLanguage.GetLanguageIndex(split[0]);
+ lang = GameLanguage.Language2Char(langIndex);
+ split = split.Skip(1).ToArray();
+ }
+
+ var result = new Item[split.Count];
+ for (int i = 0; i < result.Length; i++)
+ {
+ var text = split[i].Trim();
+ var item = GetItem(text, lang);
+ if (!InvertedRecipeDictionary.TryGetValue(item.ItemId, out var diy))
+ throw new Exception($"DIY recipe appears to be invalid ({text}).");
+
+ result[i] = new Item(Item.DIYRecipe) { Count = diy };
+ }
+ return result;
+ }
+
+ ///
+ /// Gets a list of items from the requested list of item name strings.
+ ///
+ ///
+ /// If an item name parse fails or the item ID does not exist as a known item, exceptions will be thrown.
+ ///
+ /// List of item names
+ /// Item packaging options
+ /// Destination where the item will end up at
+ /// Language code to parse with. If the first entry in is a language code, it will be used instead of .
+ public static IReadOnlyCollection
- GetItemsLanguage(IReadOnlyList split, IConfigItem config, ItemDestination type = ItemDestination.PlayerDropped, string lang = GameLanguage.DefaultLanguage)
+ {
+ if (split.Count > 1 && split[0].Length < 3)
+ {
+ var langIndex = GameLanguage.GetLanguageIndex(split[0]);
+ lang = GameLanguage.Language2Char(langIndex);
+ split = split.Skip(1).ToArray();
+ }
+
+ var strings = GameInfo.Strings.itemlistdisplay;
+ var result = new Item[split.Count];
+ for (int i = 0; i < result.Length; i++)
+ {
+ var text = split[i].Trim();
+ var item = CreateItem(text, i, config, type, lang);
+
+ if (item.ItemId >= strings.Length)
+ throw new Exception($"Item requested is out of expected range ({item.ItemId:X4} > {strings.Length:X4}).");
+ if (string.IsNullOrWhiteSpace(strings[item.ItemId]))
+ throw new Exception($"Item requested does not have a valid name ({item.ItemId:X4}).");
+
+ result[i] = item;
+ }
+ return result;
+ }
+
+ ///
+ /// Gets a list of items from the requested list of item hex code strings.
+ ///
+ ///
+ /// If a hex code parse fails or a recipe ID does not exist, exceptions will be thrown.
+ ///
+ /// List of recipe IDs as u16 hex
+ /// Item packaging options
+ /// Destination where the item will end up at
+ public static IReadOnlyCollection
- GetItemsHexCode(IReadOnlyList split, IConfigItem config, ItemDestination type = ItemDestination.PlayerDropped)
+ {
+ var strings = GameInfo.Strings.itemlistdisplay;
+ var result = new Item[split.Count];
+ for (int i = 0; i < result.Length; i++)
+ {
+ var text = split[i].Trim();
+ var convert = GetBytesFromString(text);
+ var item = CreateItem(convert, i, config, type);
+
+ if (item.ItemId >= strings.Length)
+ throw new Exception($"Item requested is out of expected range ({item.ItemId:X4} > {strings.Length:X4}).");
+ if (string.IsNullOrWhiteSpace(strings[item.ItemId]))
+ throw new Exception($"Item requested does not have a valid name ({item.ItemId:X4}).");
+
+ result[i] = item;
+ }
+ return result;
+ }
+
+ private static byte[] GetBytesFromString(string text)
+ {
+ if (!ulong.TryParse(text, NumberStyles.AllowHexSpecifier, CultureInfo.CurrentCulture, out var value))
+ return Item.NONE.ToBytes();
+ var bytes = new byte[sizeof(ulong)];
+ WriteUInt64LittleEndian(bytes, value);
+ return bytes;
+ }
+
+ private static Item CreateItem(string name, int requestIndex, IConfigItem config, ItemDestination type, string lang = "en")
+ {
+ var item = GetItem(name, lang);
+ if (item.IsNone)
+ throw new Exception($"Failed to convert item (index {requestIndex}: {name}) for Language {lang}.");
+
+ return FinalizeItem(requestIndex, config, type, item);
+ }
+
+ private static Item CreateItem(byte[] convert, int requestIndex, IConfigItem config, ItemDestination type)
+ {
+ Item item;
+ try
+ {
+ if (convert.Length != Item.SIZE)
+ throw new Exception();
+ item = convert.ToClass
- ();
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Failed to convert item (index {requestIndex}: {ex.Message}).");
+ }
+
+ return FinalizeItem(requestIndex, config, type, item);
+ }
+
+ private static Item FinalizeItem(int requestIndex, IConfigItem config, ItemDestination type, Item item)
+ {
+ if (type == ItemDestination.PlayerDropped)
+ {
+ if (!ItemInfo.IsSaneItemForDrop(item) && !config.SkipDropCheck)
+ throw new Exception($"Unsupported item: (index {requestIndex}).");
+ if (config.WrapAllItems && item.ShouldWrapItem())
+ item.SetWrapping(ItemWrapping.WrappingPaper, config.WrappingPaper, true);
+ }
+
+ item.IsDropped = type == ItemDestination.FieldItemDropped;
+
+ return item;
+ }
+
+ private static readonly CompareInfo Comparer = CultureInfo.InvariantCulture.CompareInfo;
+ private const CompareOptions optIncludeSymbols = CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreWidth;
+ private const CompareOptions optIgnoreSymbols = CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreSymbols | CompareOptions.IgnoreWidth;
+
+ ///
+ /// Gets a sensitive compare option, depending on the input string's qualities.
+ ///
+ /// Input string
+ /// Default options if no symbols,
+ private static CompareOptions GetCompareOption(string str) => str.Any(ch => !char.IsLetterOrDigit(ch) && !char.IsWhiteSpace(ch)) ? optIgnoreSymbols & ~CompareOptions.IgnoreSymbols : optIgnoreSymbols;
+
+ ///
+ /// Gets the first item name-value that contains the (case-insensitive).
+ ///
+ /// Requested Item
+ /// Game strings language to fetch with
+ /// Returns if no match found.
+ public static Item GetItem(string itemName, string lang = "en")
+ {
+ var gStrings = GameInfo.GetStrings(lang);
+ var strings = gStrings.ItemDataSource;
+ var parsedItem = GetItem(itemName, strings);
+ if (parsedItem != Item.NO_ITEM)
+ return parsedItem;
+
+ if (gStrings.HasAssociatedItems(itemName, out var items))
+ {
+ if (items?.Count == 1)
+ return new Item((ushort)items[0].Value);
+ }
+
+ return Item.NO_ITEM;
+ }
+
+ ///
+ /// Gets the first item name-value that contains the (case-insensitive).
+ ///
+ /// Requested Item
+ /// Game strings
+ /// Returns if no match found.
+ public static Item GetItem(string itemName, IReadOnlyList strings)
+ {
+ if (TryGetItem(itemName, strings, out var id))
+ return new Item(id);
+ return Item.NO_ITEM;
+ }
+
+ ///
+ /// Gets the first item name-value that contains the (case-insensitive).
+ ///
+ /// Requested Item
+ /// List of item name-values
+ /// Item ID, if found. Otherwise, 0
+ /// True if found, false if none.
+ public static bool TryGetItem(string itemName, IReadOnlyList strings, out ushort value)
+ {
+ if (TryGetItem(itemName, strings, out value, optIncludeSymbols))
+ return true;
+ return TryGetItem(itemName, strings, out value, optIgnoreSymbols);
+ }
+
+ private static bool TryGetItem(string itemName, IEnumerable strings, out ushort value, CompareOptions opt)
+ {
+ foreach (var item in strings)
+ {
+ var result = Comparer.Compare(item.Text, 0, itemName, 0, opt);
+ if (result != 0)
+ continue;
+
+ value = (ushort)item.Value;
+ return true;
+ }
+
+ value = Item.NONE;
+ return false;
+ }
+
+ ///
+ /// Gets an enumerable list of item key-value pairs that contain (case-insensitive) the requested .
+ ///
+ /// Item name
+ /// Item names (and their Item ID values)
+ public static IEnumerable GetItemsMatching(string itemName, IEnumerable strings)
+ {
+ var opt = GetCompareOption(itemName);
+ foreach (var item in strings)
+ {
+ var result = Comparer.IndexOf(item.Text, itemName, opt);
+ if (result < 0)
+ continue;
+ yield return item;
+ }
+ }
+
+ ///
+ /// Gets an enumerable list of item key-value pairs that contain (case-insensitive) the requested .
+ ///
+ ///
+ /// Orders the items based on the closest match ().
+ ///
+ /// Item name
+ /// Item names (and their Item ID values)
+ public static IEnumerable GetItemsMatchingOrdered(string itemName, IEnumerable strings)
+ {
+ var matches = GetItemsMatching(itemName, strings);
+ return GetItemsClosestOrdered(itemName, matches);
+ }
+
+ ///
+ /// Gets an enumerable list of item key-value pairs ordered by the closest for the requested .
+ ///
+ /// Item name
+ /// Item names (and their Item ID values)
+ public static IEnumerable GetItemsClosestOrdered(string itemName, IEnumerable strings)
+ {
+ return strings.OrderBy(z => LevenshteinDistance.Compute(z.Text, itemName));
+ }
+
+ ///
+ /// Gets the Item Name and raw 8-byte value as a string.
+ ///
+ /// Item value
+ public static string GetItemText(Item item)
+ {
+ var value = ReadUInt64LittleEndian(item.ToBytesClass());
+ var name = GameInfo.Strings.GetItemName(item.ItemId);
+ return $"{name}: {value:X16}";
+ }
+
+ ///
+ /// Gets the u16 item ID from the input hex code.
+ ///
+ /// Hex code for the item (preferably 4 digits)
+ public static ushort GetID(string text)
+ {
+ if (!ulong.TryParse(text.Trim(), NumberStyles.AllowHexSpecifier, CultureInfo.CurrentCulture, out var value))
+ return Item.NONE;
+ return (ushort)value;
}
}
+
+public enum ItemDestination
+{
+ PlayerDropped,
+ FieldItemDropped,
+ HeldItem,
+}
\ No newline at end of file
diff --git a/NHSE.Core/Editing/ItemRequest/LevenshteinDistance.cs b/NHSE.Core/Editing/ItemRequest/LevenshteinDistance.cs
index d6c6b89..cbbe73d 100644
--- a/NHSE.Core/Editing/ItemRequest/LevenshteinDistance.cs
+++ b/NHSE.Core/Editing/ItemRequest/LevenshteinDistance.cs
@@ -1,50 +1,49 @@
using System;
-namespace NHSE.Core
+namespace NHSE.Core;
+
+public static class LevenshteinDistance
{
- public static class LevenshteinDistance
+ ///
+ /// Compute the distance between two strings.
+ /// http://www.dotnetperls.com/levenshtein
+ /// https://stackoverflow.com/a/13793600
+ ///
+ public static int Compute(string s, string t)
{
- ///
- /// Compute the distance between two strings.
- /// http://www.dotnetperls.com/levenshtein
- /// https://stackoverflow.com/a/13793600
- ///
- public static int Compute(string s, string t)
+ int n = s.Length;
+ int m = t.Length;
+ int[,] d = new int[n + 1, m + 1];
+
+ // Step 1
+ if (n == 0)
+ return m;
+
+ if (m == 0)
+ return n;
+
+ // Step 2
+ for (int i = 0; i <= n; d[i, 0] = i++) { }
+ for (int j = 0; j <= m; d[0, j] = j++) { }
+
+ // Step 3
+ for (int i = 1; i <= n; i++)
{
- int n = s.Length;
- int m = t.Length;
- int[,] d = new int[n + 1, m + 1];
-
- // Step 1
- if (n == 0)
- return m;
-
- if (m == 0)
- return n;
-
- // Step 2
- for (int i = 0; i <= n; d[i, 0] = i++) { }
- for (int j = 0; j <= m; d[0, j] = j++) { }
-
- // Step 3
- for (int i = 1; i <= n; i++)
+ //Step 4
+ for (int j = 1; j <= m; j++)
{
- //Step 4
- for (int j = 1; j <= m; j++)
- {
- // Step 5
- int cost = (t[j - 1] == s[i - 1]) ? 0 : 1;
+ // Step 5
+ int cost = (t[j - 1] == s[i - 1]) ? 0 : 1;
- // Step 6
- var x1 = d[i - 1, j] + 1;
- var x2 = d[i, j - 1] + 1;
- var x3 = d[i - 1, j - 1] + cost;
- d[i, j] = Math.Min(Math.Min(x1, x2), x3);
- }
+ // Step 6
+ var x1 = d[i - 1, j] + 1;
+ var x2 = d[i, j - 1] + 1;
+ var x3 = d[i - 1, j - 1] + cost;
+ d[i, j] = Math.Min(Math.Min(x1, x2), x3);
}
-
- // Step 7
- return d[n, m];
}
+
+ // Step 7
+ return d[n, m];
}
-}
+}
\ No newline at end of file
diff --git a/NHSE.Core/Encryption/Aes128Ctr.cs b/NHSE.Core/Encryption/Aes128Ctr.cs
index 61c36c7..1769c80 100644
--- a/NHSE.Core/Encryption/Aes128Ctr.cs
+++ b/NHSE.Core/Encryption/Aes128Ctr.cs
@@ -2,122 +2,128 @@
using System.Collections.Generic;
using System.Security.Cryptography;
-namespace NHSE.Core
+namespace NHSE.Core;
+// The MIT License (MIT)
+
+// Copyright (c) 2014 Hans Wolff
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+public sealed class Aes128CounterMode : SymmetricAlgorithm
{
- // The MIT License (MIT)
+ private readonly byte[] _counter;
+ private readonly Aes _aes = GetAes();
- // Copyright (c) 2014 Hans Wolff
-
- // Permission is hereby granted, free of charge, to any person obtaining a copy
- // of this software and associated documentation files (the "Software"), to deal
- // in the Software without restriction, including without limitation the rights
- // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- // copies of the Software, and to permit persons to whom the Software is
- // furnished to do so, subject to the following conditions:
-
- // The above copyright notice and this permission notice shall be included in
- // all copies or substantial portions of the Software.
-
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- // THE SOFTWARE.
-
- public sealed class Aes128CounterMode : SymmetricAlgorithm
+ private static Aes GetAes()
{
- private readonly byte[] _counter;
- private readonly AesManaged _aes = new() {Mode = CipherMode.ECB, Padding = PaddingMode.None};
-
- public Aes128CounterMode(byte[] counter)
- {
- const int expect = 0x10;
- if (counter.Length != expect)
- throw new ArgumentException($"Counter size must be same as block size (actual: {counter.Length}, expected: {expect})");
- _counter = counter;
- }
-
- public override ICryptoTransform CreateEncryptor(byte[] rgbKey, byte[] ignoredParameter) => new CounterModeCryptoTransform(_aes, rgbKey, _counter);
- public override ICryptoTransform CreateDecryptor(byte[] rgbKey, byte[] ignoredParameter) => new CounterModeCryptoTransform(_aes, rgbKey, _counter);
-
- public override void GenerateKey() => _aes.GenerateKey();
- public override void GenerateIV() { /* IV not needed in Counter Mode */ }
- protected override void Dispose(bool disposing) => _aes.Dispose();
+ var result = Aes.Create();
+ result.Mode = CipherMode.ECB;
+ result.Padding = PaddingMode.None;
+ return result;
}
- public sealed class CounterModeCryptoTransform : ICryptoTransform
+ public Aes128CounterMode(byte[] counter)
{
- private readonly byte[] _counter;
- private readonly ICryptoTransform _counterEncryptor;
- private readonly Queue _xorMask = new();
- private readonly SymmetricAlgorithm _symmetricAlgorithm;
-
- public CounterModeCryptoTransform(SymmetricAlgorithm symmetricAlgorithm, byte[] key, byte[] counter)
- {
- if (counter.Length != symmetricAlgorithm.BlockSize / 8)
- throw new ArgumentException($"Counter size must be same as block size (actual: {counter.Length}, expected: {symmetricAlgorithm.BlockSize / 8})");
-
- _symmetricAlgorithm = symmetricAlgorithm;
- _encryptOutput = new byte[counter.Length];
- _counter = counter;
-
- var zeroIv = new byte[counter.Length];
- _counterEncryptor = symmetricAlgorithm.CreateEncryptor(key, zeroIv);
- }
-
- public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount)
- {
- var output = new byte[inputCount];
- TransformBlock(inputBuffer, inputOffset, inputCount, output, 0);
- return output;
- }
-
- public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
- {
- var xm = _xorMask;
- for (var i = 0; i < inputCount; i++)
- {
- if (xm.Count == 0)
- EncryptCounterThenIncrement();
-
- var mask = xm.Dequeue();
- outputBuffer[outputOffset + i] = (byte)(inputBuffer[inputOffset + i] ^ mask);
- }
-
- return inputCount;
- }
-
- private readonly byte[] _encryptOutput;
-
- private void EncryptCounterThenIncrement()
- {
- var counterModeBlock = _encryptOutput;
-
- _counterEncryptor.TransformBlock(_counter, 0, _counter.Length, counterModeBlock, 0);
- IncrementCounter();
-
- var xm = _xorMask;
- foreach (var b in counterModeBlock)
- xm.Enqueue(b);
- }
-
- private void IncrementCounter()
- {
- var ctr = _counter;
- for (var i = ctr.Length - 1; i >= 0; i--)
- {
- if (++ctr[i] != 0)
- break;
- }
- }
-
- public int InputBlockSize => _symmetricAlgorithm.BlockSize / 8;
- public int OutputBlockSize => _symmetricAlgorithm.BlockSize / 8;
- public bool CanTransformMultipleBlocks => true;
- public bool CanReuseTransform => false;
-
- public void Dispose() => _counterEncryptor.Dispose();
+ const int expect = 0x10;
+ if (counter.Length != expect)
+ throw new ArgumentException($"Counter size must be same as block size (actual: {counter.Length}, expected: {expect})");
+ _counter = counter;
}
+
+ public override ICryptoTransform CreateEncryptor(byte[] rgbKey, byte[]? ignoredParameter) => new CounterModeCryptoTransform(_aes, rgbKey, _counter);
+ public override ICryptoTransform CreateDecryptor(byte[] rgbKey, byte[]? ignoredParameter) => new CounterModeCryptoTransform(_aes, rgbKey, _counter);
+
+ public override void GenerateKey() => _aes.GenerateKey();
+ public override void GenerateIV() { /* IV not needed in Counter Mode */ }
+ protected override void Dispose(bool disposing) => _aes.Dispose();
}
+
+public sealed class CounterModeCryptoTransform : ICryptoTransform
+{
+ private readonly byte[] _counter;
+ private readonly ICryptoTransform _counterEncryptor;
+ private readonly Queue _xorMask = new();
+ private readonly SymmetricAlgorithm _symmetricAlgorithm;
+
+ public CounterModeCryptoTransform(SymmetricAlgorithm symmetricAlgorithm, byte[] key, byte[] counter)
+ {
+ if (counter.Length != symmetricAlgorithm.BlockSize / 8)
+ throw new ArgumentException($"Counter size must be same as block size (actual: {counter.Length}, expected: {symmetricAlgorithm.BlockSize / 8})");
+
+ _symmetricAlgorithm = symmetricAlgorithm;
+ _encryptOutput = new byte[counter.Length];
+ _counter = counter;
+
+ var zeroIv = new byte[counter.Length];
+ _counterEncryptor = symmetricAlgorithm.CreateEncryptor(key, zeroIv);
+ }
+
+ public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount)
+ {
+ var output = new byte[inputCount];
+ TransformBlock(inputBuffer, inputOffset, inputCount, output, 0);
+ return output;
+ }
+
+ public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
+ {
+ var xm = _xorMask;
+ for (var i = 0; i < inputCount; i++)
+ {
+ if (xm.Count == 0)
+ EncryptCounterThenIncrement();
+
+ var mask = xm.Dequeue();
+ outputBuffer[outputOffset + i] = (byte)(inputBuffer[inputOffset + i] ^ mask);
+ }
+
+ return inputCount;
+ }
+
+ private readonly byte[] _encryptOutput;
+
+ private void EncryptCounterThenIncrement()
+ {
+ var counterModeBlock = _encryptOutput;
+
+ _counterEncryptor.TransformBlock(_counter, 0, _counter.Length, counterModeBlock, 0);
+ IncrementCounter();
+
+ var xm = _xorMask;
+ foreach (var b in counterModeBlock)
+ xm.Enqueue(b);
+ }
+
+ private void IncrementCounter()
+ {
+ var ctr = _counter;
+ for (var i = ctr.Length - 1; i >= 0; i--)
+ {
+ if (++ctr[i] != 0)
+ break;
+ }
+ }
+
+ public int InputBlockSize => _symmetricAlgorithm.BlockSize / 8;
+ public int OutputBlockSize => _symmetricAlgorithm.BlockSize / 8;
+ public bool CanTransformMultipleBlocks => true;
+ public bool CanReuseTransform => false;
+
+ public void Dispose() => _counterEncryptor.Dispose();
+}
\ No newline at end of file
diff --git a/NHSE.Core/Encryption/CryptoFile.cs b/NHSE.Core/Encryption/CryptoFile.cs
index 76644e5..fd66d1c 100644
--- a/NHSE.Core/Encryption/CryptoFile.cs
+++ b/NHSE.Core/Encryption/CryptoFile.cs
@@ -1,16 +1,3 @@
-namespace NHSE.Core
-{
- internal readonly ref struct CryptoFile
- {
- public readonly byte[] Data;
- public readonly byte[] Key;
- public readonly byte[] Ctr;
+namespace NHSE.Core;
- public CryptoFile(byte[] data, byte[] key, byte[] ctr)
- {
- Data = data;
- Key = key;
- Ctr = ctr;
- }
- }
-}
+internal readonly record struct CryptoFile(byte[] Data, byte[] Key, byte[] Ctr);
\ No newline at end of file
diff --git a/NHSE.Core/Encryption/EncryptedInt32.cs b/NHSE.Core/Encryption/EncryptedInt32.cs
index 73294b6..a6ef37b 100644
--- a/NHSE.Core/Encryption/EncryptedInt32.cs
+++ b/NHSE.Core/Encryption/EncryptedInt32.cs
@@ -1,81 +1,81 @@
using System;
+using static System.Buffers.Binary.BinaryPrimitives;
-namespace NHSE.Core
+namespace NHSE.Core;
+
+public sealed class EncryptedInt32
{
- public sealed class EncryptedInt32
+ // Encryption constant used to encrypt the int.
+ private const uint ENCRYPTION_CONSTANT = 0x80E32B11;
+ // Base shift count used in the encryption.
+ private const byte SHIFT_BASE = 3;
+
+ public readonly uint OriginalEncrypted;
+ public readonly ushort Adjust;
+ public readonly byte Shift;
+ public readonly byte Checksum;
+
+ public uint Value;
+
+ public override string ToString() => Value.ToString();
+
+ public EncryptedInt32(uint encryptedValue, ushort adjust = 0, byte shift = 0, byte checksum = 0)
{
- // Encryption constant used to encrypt the int.
- private const uint ENCRYPTION_CONSTANT = 0x80E32B11;
- // Base shift count used in the encryption.
- private const byte SHIFT_BASE = 3;
-
- public readonly uint OriginalEncrypted;
- public readonly ushort Adjust;
- public readonly byte Shift;
- public readonly byte Checksum;
-
- public uint Value;
-
- public override string ToString() => Value.ToString();
-
- public EncryptedInt32(uint encryptedValue, ushort adjust = 0, byte shift = 0, byte checksum = 0)
- {
- OriginalEncrypted = encryptedValue;
- Adjust = adjust;
- Shift = shift;
- Checksum = checksum;
- Value = Decrypt(encryptedValue, shift, adjust);
- }
-
- public void Write(byte[] data, int offset) => Write(this, data, offset);
-
- // Calculates a checksum for a given encrypted value
- // Checksum calculation is every byte of the encrypted in added together minus 0x2D.
- public static byte CalculateChecksum(uint value)
- {
- var byteSum = value + (value >> 16) + (value >> 24) + (value >> 8);
- return (byte)(byteSum - 0x2D);
- }
-
- public static uint Decrypt(uint encrypted, byte shift, ushort adjust)
- {
- // Decrypt the encrypted int using the given params.
- ulong val = ((ulong) encrypted) << ((32 - SHIFT_BASE - shift) & 0x3F);
- val += val >> 32;
- return ENCRYPTION_CONSTANT - adjust + (uint)val;
- }
-
- public static uint Encrypt(uint value, byte shift, ushort adjust)
- {
- ulong val = (ulong) (value + (adjust - ENCRYPTION_CONSTANT)) << (shift + SHIFT_BASE);
- return (uint) ((val >> 32) + val);
- }
-
- public static EncryptedInt32 ReadVerify(byte[] data, int offset)
- {
- var val = Read(data, offset);
- if (val.Checksum != CalculateChecksum(val.OriginalEncrypted))
- throw new ArgumentException($"Failed to verify the {nameof(EncryptedInt32)} at {nameof(offset)}");
- return val;
- }
-
- public static EncryptedInt32 Read(byte[] data, int offset)
- {
- var enc = BitConverter.ToUInt32(data, offset + 0);
- var adjust = BitConverter.ToUInt16(data, offset + 4);
- var shift = data[offset + 6];
- var chk = data[offset + 7];
- return new EncryptedInt32(enc, adjust, shift, chk);
- }
-
- public static void Write(EncryptedInt32 value, byte[] data, int offset)
- {
- uint enc = Encrypt(value.Value, value.Shift, value.Adjust);
- byte chk = CalculateChecksum(enc);
- BitConverter.GetBytes(enc).CopyTo(data, offset + 0);
- BitConverter.GetBytes(value.Adjust).CopyTo(data, offset + 4);
- data[offset + 6] = value.Shift;
- data[offset + 7] = chk;
- }
+ OriginalEncrypted = encryptedValue;
+ Adjust = adjust;
+ Shift = shift;
+ Checksum = checksum;
+ Value = Decrypt(encryptedValue, shift, adjust);
}
-}
+
+ public void Write(Span data) => Write(this, data);
+ public void Write(byte[] data, int offset) => Write(data.AsSpan(offset));
+
+ // Calculates a checksum for a given encrypted value
+ // Checksum calculation is every byte of the encrypted in added together minus 0x2D.
+ public static byte CalculateChecksum(uint value)
+ {
+ var byteSum = value + (value >> 16) + (value >> 24) + (value >> 8);
+ return (byte)(byteSum - 0x2D);
+ }
+
+ public static uint Decrypt(uint encrypted, byte shift, ushort adjust)
+ {
+ // Decrypt the encrypted int using the given params.
+ ulong val = ((ulong) encrypted) << ((32 - SHIFT_BASE - shift) & 0x3F);
+ val += val >> 32;
+ return ENCRYPTION_CONSTANT - adjust + (uint)val;
+ }
+
+ public static uint Encrypt(uint value, byte shift, ushort adjust)
+ {
+ ulong val = (ulong) (value + unchecked(adjust - ENCRYPTION_CONSTANT)) << (shift + SHIFT_BASE);
+ return (uint) ((val >> 32) + val);
+ }
+
+ public static EncryptedInt32 ReadVerify(ReadOnlySpan data, int offset)
+ {
+ var val = Read(data[offset..]);
+ if (val.Checksum != CalculateChecksum(val.OriginalEncrypted))
+ throw new ArgumentException($"Failed to verify the {nameof(EncryptedInt32)} at {nameof(offset)}");
+ return val;
+ }
+
+ public static EncryptedInt32 Read(ReadOnlySpan data)
+ {
+ var encrypted = ReadUInt32LittleEndian(data);
+ var adjust = ReadUInt16LittleEndian(data[4..]);
+ var shift = data[6];
+ var chk = data[7];
+ return new EncryptedInt32(encrypted, adjust, shift, chk);
+ }
+
+ public static void Write(EncryptedInt32 value, Span data)
+ {
+ var encrypted = Encrypt(value.Value, value.Shift, value.Adjust);
+ WriteUInt32LittleEndian(data, encrypted);
+ WriteUInt16LittleEndian(data[4..], value.Adjust);
+ data[6] = value.Shift;
+ data[7] = CalculateChecksum(encrypted);
+ }
+}
\ No newline at end of file
diff --git a/NHSE.Core/Encryption/EncryptedSaveFile.cs b/NHSE.Core/Encryption/EncryptedSaveFile.cs
index 858bd37..ffef86c 100644
--- a/NHSE.Core/Encryption/EncryptedSaveFile.cs
+++ b/NHSE.Core/Encryption/EncryptedSaveFile.cs
@@ -1,21 +1,20 @@
-namespace NHSE.Core
+namespace NHSE.Core;
+
+public readonly ref struct EncryptedSaveFile
{
- public readonly ref struct EncryptedSaveFile
+ public readonly byte[] Data;
+ public readonly byte[] Header;
+
+ public EncryptedSaveFile(byte[] data, byte[] header)
{
- public readonly byte[] Data;
- public readonly byte[] Header;
-
- public EncryptedSaveFile(byte[] data, byte[] header)
- {
- Data = data;
- Header = header;
- }
-
- #region Equality Comparison
- public override bool Equals(object obj) => false;
- public override int GetHashCode() => Data.GetHashCode();
- public static bool operator !=(EncryptedSaveFile left, EncryptedSaveFile right) => !(left == right);
- public static bool operator ==(EncryptedSaveFile left, EncryptedSaveFile right) => left.Data == right.Data && left.Header == right.Header;
- #endregion
+ Data = data;
+ Header = header;
}
-}
+
+ #region Equality Comparison
+ public override bool Equals(object? obj) => false;
+ public override int GetHashCode() => Data.GetHashCode();
+ public static bool operator !=(EncryptedSaveFile left, EncryptedSaveFile right) => !(left == right);
+ public static bool operator ==(EncryptedSaveFile left, EncryptedSaveFile right) => left.Data == right.Data && left.Header == right.Header;
+ #endregion
+}
\ No newline at end of file
diff --git a/NHSE.Core/Encryption/Encryption.cs b/NHSE.Core/Encryption/Encryption.cs
index d6a5562..11bb1c4 100644
--- a/NHSE.Core/Encryption/Encryption.cs
+++ b/NHSE.Core/Encryption/Encryption.cs
@@ -1,82 +1,81 @@
using System;
-namespace NHSE.Core
+namespace NHSE.Core;
+
+public static class Encryption
{
- public static class Encryption
+ private static byte[] GetParam(ReadOnlySpan data, in int index)
{
- private static byte[] GetParam(uint[] data, in int index)
- {
- var rand = new XorShift128(data[data[index] & 0x7F]);
- var prms = data[data[index + 1] & 0x7F] & 0x7F;
+ var rand = new XorShift128(data[(int)data[index] & 0x7F]);
+ var prms = data[(int)(data[index + 1] & 0x7F)] & 0x7F;
- var rndRollCount = (prms & 0xF) + 1;
- for (var i = 0; i < rndRollCount; i++)
- rand.GetU64();
+ var rndRollCount = (prms & 0xF) + 1;
+ for (var i = 0; i < rndRollCount; i++)
+ rand.GetU64();
- var result = new byte[0x10];
- for (var i = 0; i < result.Length; i++)
- result[i] = (byte)(rand.GetU32() >> 24);
+ var result = new byte[0x10];
+ for (var i = 0; i < result.Length; i++)
+ result[i] = (byte)(rand.GetU32() >> 24);
- return result;
- }
-
- ///
- /// Decrypts the using the in place.
- ///
- /// Header Data
- /// Encrypted SaveData
- public static void Decrypt(byte[] headerData, byte[] encData)
- {
- // First 256 bytes go unused
- var importantData = new uint[0x80];
- Buffer.BlockCopy(headerData, 0x100, importantData, 0, 0x200);
-
- // Set up Key
- var key = GetParam(importantData, 0);
-
- // Set up counter
- var counter = GetParam(importantData, 2);
-
- // Do the AES
- using var aesCtr = new Aes128CounterMode(counter);
- var transform = aesCtr.CreateDecryptor(key, counter);
-
- transform.TransformBlock(encData, 0, encData.Length, encData, 0);
- }
-
- private static CryptoFile GenerateHeaderFile(uint seed, byte[] versionData)
- {
- // Generate 128 Random uints which will be used for params
- var random = new XorShift128(seed);
- var encryptData = new uint[128];
- for (var i = 0; i < encryptData.Length; i++)
- encryptData[i] = random.GetU32();
-
- var headerData = new byte[0x300];
- Buffer.BlockCopy(versionData, 0, headerData, 0, 0x100);
- Buffer.BlockCopy(encryptData, 0, headerData, 0x100, 0x200);
- return new CryptoFile(headerData, GetParam(encryptData, 0), GetParam(encryptData, 2));
- }
-
- ///
- /// Encrypts the (savedata) using the provided .
- ///
- /// SaveData to encrypt
- /// Seed to encrypt with
- /// Version data to encrypt with
- /// Encrypted SaveData, and associated headerData
- public static EncryptedSaveFile Encrypt(byte[] data, uint seed, byte[] versionData)
- {
- // Generate header file and get key and counter
- var header = GenerateHeaderFile(seed, versionData);
-
- // Encrypt file
- using var aesCtr = new Aes128CounterMode(header.Ctr);
- var transform = aesCtr.CreateEncryptor(header.Key, header.Ctr);
- var encData = new byte[data.Length];
- transform.TransformBlock(data, 0, data.Length, encData, 0);
-
- return new EncryptedSaveFile(encData, header.Data);
- }
+ return result;
}
-}
+
+ ///
+ /// Decrypts the using the in place.
+ ///
+ /// Header Data
+ /// Encrypted SaveData
+ public static void Decrypt(byte[] headerData, byte[] encData)
+ {
+ // First 256 bytes go unused
+ var importantData = new uint[0x80];
+ Buffer.BlockCopy(headerData, 0x100, importantData, 0, 0x200);
+
+ // Set up Key
+ var key = GetParam(importantData, 0);
+
+ // Set up counter
+ var counter = GetParam(importantData, 2);
+
+ // Do the AES
+ using var aesCtr = new Aes128CounterMode(counter);
+ var transform = aesCtr.CreateDecryptor(key, counter);
+
+ transform.TransformBlock(encData, 0, encData.Length, encData, 0);
+ }
+
+ private static CryptoFile GenerateHeaderFile(uint seed, byte[] versionData)
+ {
+ // Generate 128 Random uints which will be used for params
+ var random = new XorShift128(seed);
+ var encryptData = new uint[128];
+ for (var i = 0; i < encryptData.Length; i++)
+ encryptData[i] = random.GetU32();
+
+ var headerData = new byte[0x300];
+ Buffer.BlockCopy(versionData, 0, headerData, 0, 0x100);
+ Buffer.BlockCopy(encryptData, 0, headerData, 0x100, 0x200);
+ return new CryptoFile(headerData, GetParam(encryptData, 0), GetParam(encryptData, 2));
+ }
+
+ ///
+ /// Encrypts the (savedata) using the provided .
+ ///
+ /// SaveData to encrypt
+ /// Seed to encrypt with
+ /// Version data to encrypt with
+ /// Encrypted SaveData, and associated headerData
+ public static EncryptedSaveFile Encrypt(byte[] data, uint seed, byte[] versionData)
+ {
+ // Generate header file and get key and counter
+ var header = GenerateHeaderFile(seed, versionData);
+
+ // Encrypt file
+ using var aesCtr = new Aes128CounterMode(header.Ctr);
+ var transform = aesCtr.CreateEncryptor(header.Key, header.Ctr);
+ var encData = new byte[data.Length];
+ transform.TransformBlock(data, 0, data.Length, encData, 0);
+
+ return new EncryptedSaveFile(encData, header.Data);
+ }
+}
\ No newline at end of file
diff --git a/NHSE.Core/Hashing/FileHashDetails.cs b/NHSE.Core/Hashing/FileHashDetails.cs
index 1a7b8b2..48d425b 100644
--- a/NHSE.Core/Hashing/FileHashDetails.cs
+++ b/NHSE.Core/Hashing/FileHashDetails.cs
@@ -1,32 +1,31 @@
using System.Collections.Generic;
-namespace NHSE.Core
+namespace NHSE.Core;
+
+///
+/// Contains the for a .
+///
+public sealed class FileHashDetails
{
///
- /// Contains the for a .
+ /// Name of the File that these apply to.
///
- public sealed class FileHashDetails
+ public readonly string FileName;
+
+ ///
+ /// Expected file size of the .
+ ///
+ public readonly uint FileSize;
+
+ ///
+ /// Hash specs that are done in this file.
+ ///
+ public readonly IReadOnlyList HashRegions;
+
+ public FileHashDetails(string fileName, uint fileSize, IReadOnlyList regions)
{
- ///
- /// Name of the File that these apply to.
- ///
- public readonly string FileName;
-
- ///
- /// Expected file size of the .
- ///
- public readonly uint FileSize;
-
- ///
- /// Hash specs that are done in this file.
- ///
- public readonly IReadOnlyList HashRegions;
-
- public FileHashDetails(string fileName, uint fileSize, IReadOnlyList regions)
- {
- FileName = fileName;
- FileSize = fileSize;
- HashRegions = regions;
- }
+ FileName = fileName;
+ FileSize = fileSize;
+ HashRegions = regions;
}
}
\ No newline at end of file
diff --git a/NHSE.Core/Hashing/FileHashInfo.cs b/NHSE.Core/Hashing/FileHashInfo.cs
index 0d1ee47..68650c1 100644
--- a/NHSE.Core/Hashing/FileHashInfo.cs
+++ b/NHSE.Core/Hashing/FileHashInfo.cs
@@ -1,25 +1,24 @@
using System.Collections.Generic;
using System.Linq;
-namespace NHSE.Core
+namespace NHSE.Core;
+
+public sealed class FileHashInfo
{
- public sealed class FileHashInfo
+ private readonly IReadOnlyDictionary List;
+
+ public FileHashInfo(FileHashInfo dupe) : this(dupe.List.Values) { }
+
+ public FileHashInfo(IEnumerable hashSets)
{
- private readonly IReadOnlyDictionary List;
-
- public FileHashInfo(FileHashInfo dupe) : this(dupe.List.Values) { }
-
- public FileHashInfo(IEnumerable hashSets)
- {
- var list = new Dictionary();
- foreach (var hashSet in hashSets)
- list[hashSet.FileSize] = hashSet;
- List = list;
- }
-
- public FileHashDetails? GetFile(string nameData)
- {
- return List.Values.FirstOrDefault(z => z.FileName == nameData);
- }
+ var list = new Dictionary();
+ foreach (var hashSet in hashSets)
+ list[hashSet.FileSize] = hashSet;
+ List = list;
}
-}
+
+ public FileHashDetails? GetFile(string nameData)
+ {
+ return List.Values.FirstOrDefault(z => z.FileName == nameData);
+ }
+}
\ No newline at end of file
diff --git a/NHSE.Core/Hashing/FileHashRegion.cs b/NHSE.Core/Hashing/FileHashRegion.cs
index e6de040..663e547 100644
--- a/NHSE.Core/Hashing/FileHashRegion.cs
+++ b/NHSE.Core/Hashing/FileHashRegion.cs
@@ -1,55 +1,21 @@
-namespace NHSE.Core
+namespace NHSE.Core;
+
+///
+/// Specifies the region that a validation hash is calculated over.
+///
+/// Offset of the calculated hash.
+/// Length of the hashed data.
+public readonly record struct FileHashRegion(int HashOffset, int Size)
{
///
- /// Specifies the region that a validation hash is calculated over.
+ /// Offset where the data to be hashed starts at (calculated).
///
- public readonly struct FileHashRegion
- {
- ///
- /// Offset of the calculated hash.
- ///
- public readonly int HashOffset;
+ public int BeginOffset => HashOffset + 4;
- ///
- /// Length of the hashed data.
- ///
- public readonly int Size;
+ ///
+ /// Offset where the data to be hashed ends at (calculated).
+ ///
+ public int EndOffset => BeginOffset + Size;
- ///
- /// Offset where the data to be hashed starts at (calculated).
- ///
- public int BeginOffset => HashOffset + 4;
-
- ///
- /// Offset where the data to be hashed ends at (calculated).
- ///
- public int EndOffset => BeginOffset + Size;
-
- public override string ToString() => $"0x{HashOffset:X}: (0x{BeginOffset:X}-0x{EndOffset:X})";
-
- public FileHashRegion(int hashOfs, int size)
- {
- HashOffset = hashOfs;
- Size = size;
- }
-
- #region Equality Comparison
- public override bool Equals(object obj) => obj is FileHashRegion r && r == this;
- // ReSharper disable once PossiblyImpureMethodCallOnReadonlyVariable
- public override int GetHashCode() => BeginOffset.GetHashCode();
-
- public static bool operator !=(FileHashRegion left, FileHashRegion right) => !(left == right);
-
- public static bool operator ==(FileHashRegion left, FileHashRegion right)
- {
- if (left.HashOffset != right.HashOffset)
- return false;
- if (left.BeginOffset != right.BeginOffset)
- return false;
- if (left.Size != right.Size)
- return false;
- return true;
- }
- #endregion
- }
+ public override string ToString() => $"0x{HashOffset:X}: (0x{BeginOffset:X}-0x{EndOffset:X})";
}
\ No newline at end of file
diff --git a/NHSE.Core/Hashing/FileHashRevision.cs b/NHSE.Core/Hashing/FileHashRevision.cs
index e418864..f5bbb8f 100644
--- a/NHSE.Core/Hashing/FileHashRevision.cs
+++ b/NHSE.Core/Hashing/FileHashRevision.cs
@@ -1,627 +1,559 @@
-namespace NHSE.Core
+namespace NHSE.Core;
+
+///
+/// Provides information for hashing different revisions of the game's savedata.
+///
+public static class FileHashRevision
{
- ///
- /// Provides information for hashing different revisions of the game's savedata.
- ///
- public static class FileHashRevision
- {
- private const string FN_MAIN = "main.dat";
- private const string FN_PERSONAL = "personal.dat";
- private const string FN_POSTBOX = "postbox.dat";
- private const string FN_PHOTO = "photo_studio_island.dat";
- private const string FN_PROFILE = "profile.dat";
- private const string FN_WHEREAREN = "wherearen.dat";
+ private const string FN_MAIN = "main.dat";
+ private const string FN_PERSONAL = "personal.dat";
+ private const string FN_POSTBOX = "postbox.dat";
+ private const string FN_PHOTO = "photo_studio_island.dat";
+ private const string FN_PROFILE = "profile.dat";
+ private const string FN_WHEREAREN = "wherearen.dat";
- #region REVISION 1.0.0
+ #region REVISION 1.0.0
- internal const int REV_100_MAIN = 0xAC0938;
- internal const int REV_100_PERSONAL = 0x6BC50;
- internal const int REV_100_POSTBOX = 0xB44580;
- internal const int REV_100_PHOTO = 0x263B4;
- internal const int REV_100_PROFILE = 0x69508;
+ internal const int REV_100_MAIN = 0xAC0938;
+ internal const int REV_100_PERSONAL = 0x6BC50;
+ internal const int REV_100_POSTBOX = 0xB44580;
+ internal const int REV_100_PHOTO = 0x263B4;
+ internal const int REV_100_PROFILE = 0x69508;
- public static readonly FileHashInfo REV_100 = new(new FileHashDetails[]
- {
- new(FN_MAIN, REV_100_MAIN, new FileHashRegion[]
- {
- new(0x000108, 0x1D6D4C),
- new(0x1D6E58, 0x323384),
- new(0x4FA2E8, 0x035AC4),
- new(0x52FDB0, 0x03607C),
- new(0x565F38, 0x035AC4),
- new(0x59BA00, 0x03607C),
- new(0x5D1B88, 0x035AC4),
- new(0x607650, 0x03607C),
- new(0x63D7D8, 0x035AC4),
- new(0x6732A0, 0x03607C),
- new(0x6A9428, 0x035AC4),
- new(0x6DEEF0, 0x03607C),
- new(0x715078, 0x035AC4),
- new(0x74AB40, 0x03607C),
- new(0x780CC8, 0x035AC4),
- new(0x7B6790, 0x03607C),
- new(0x7EC918, 0x035AC4),
- new(0x8223E0, 0x03607C),
- new(0x858460, 0x2684D4)
- }),
- new(FN_PERSONAL, REV_100_PERSONAL, new FileHashRegion[]
- {
- new(0x00108, 0x35AC4),
- new(0x35BD0, 0x3607C)
- }),
- new(FN_POSTBOX, REV_100_POSTBOX, new FileHashRegion[]
- {
- new(0x000100, 0xB4447C)
- }),
- new(FN_PHOTO, REV_100_PHOTO, new FileHashRegion[]
- {
- new(0x000100, 0x262B0)
- }),
- new(FN_PROFILE, REV_100_PROFILE, new FileHashRegion[]
- {
- new(0x000100, 0x69404)
- }),
- });
+ public static readonly FileHashInfo REV_100 = new([
+ new(FN_MAIN, REV_100_MAIN, [
+ new(0x000108, 0x1D6D4C),
+ new(0x1D6E58, 0x323384),
+ new(0x4FA2E8, 0x035AC4),
+ new(0x52FDB0, 0x03607C),
+ new(0x565F38, 0x035AC4),
+ new(0x59BA00, 0x03607C),
+ new(0x5D1B88, 0x035AC4),
+ new(0x607650, 0x03607C),
+ new(0x63D7D8, 0x035AC4),
+ new(0x6732A0, 0x03607C),
+ new(0x6A9428, 0x035AC4),
+ new(0x6DEEF0, 0x03607C),
+ new(0x715078, 0x035AC4),
+ new(0x74AB40, 0x03607C),
+ new(0x780CC8, 0x035AC4),
+ new(0x7B6790, 0x03607C),
+ new(0x7EC918, 0x035AC4),
+ new(0x8223E0, 0x03607C),
+ new(0x858460, 0x2684D4)
+ ]),
+ new(FN_PERSONAL, REV_100_PERSONAL, [
+ new(0x00108, 0x35AC4),
+ new(0x35BD0, 0x3607C)
+ ]),
+ new(FN_POSTBOX, REV_100_POSTBOX, [
+ new(0x000100, 0xB4447C)
+ ]),
+ new(FN_PHOTO, REV_100_PHOTO, [
+ new(0x000100, 0x262B0)
+ ]),
+ new(FN_PROFILE, REV_100_PROFILE, [
+ new(0x000100, 0x69404)
+ ])
+ ]);
- #endregion
+ #endregion
- #region REVISION 1.1.0
+ #region REVISION 1.1.0
- internal const int REV_110_MAIN = 0xAC2AA0;
- internal const int REV_110_PERSONAL = 0x6BED0;
- internal const int REV_110_POSTBOX = 0xB44590;
- internal const int REV_110_PHOTO = 0x263C0;
- internal const int REV_110_PROFILE = 0x69560;
+ internal const int REV_110_MAIN = 0xAC2AA0;
+ internal const int REV_110_PERSONAL = 0x6BED0;
+ internal const int REV_110_POSTBOX = 0xB44590;
+ internal const int REV_110_PHOTO = 0x263C0;
+ internal const int REV_110_PROFILE = 0x69560;
- public static readonly FileHashInfo REV_110 = new(new FileHashDetails[]
- {
- new(FN_MAIN, REV_110_MAIN, new FileHashRegion[]
- {
- new(0x000110, 0x1D6D5C),
- new(0x1D6E70, 0x323C0C),
- new(0x4FAB90, 0x035AFC),
- new(0x530690, 0x0362BC),
- new(0x566A60, 0x035AFC),
- new(0x59C560, 0x0362BC),
- new(0x5D2930, 0x035AFC),
- new(0x608430, 0x0362BC),
- new(0x63E800, 0x035AFC),
- new(0x674300, 0x0362BC),
- new(0x6AA6D0, 0x035AFC),
- new(0x6E01D0, 0x0362BC),
- new(0x7165A0, 0x035AFC),
- new(0x74C0A0, 0x0362BC),
- new(0x782470, 0x035AFC),
- new(0x7B7F70, 0x0362BC),
- new(0x7EE340, 0x035AFC),
- new(0x823E40, 0x0362BC),
- new(0x85A100, 0x26899C)
- }),
- new(FN_PERSONAL, REV_110_PERSONAL, new FileHashRegion[]
- {
- new(0x00110, 0x35AFC),
- new(0x35C10, 0x362BC)
- }),
- new(FN_POSTBOX, REV_110_POSTBOX, new FileHashRegion[]
- {
- new(0x000100, 0xB4448C)
- }),
- new(FN_PHOTO, REV_110_PHOTO, new FileHashRegion[]
- {
- new(0x000100, 0x262BC)
- }),
- new(FN_PROFILE, REV_110_PROFILE, new FileHashRegion[]
- {
- new(0x000100, 0x6945C)
- }),
- });
+ public static readonly FileHashInfo REV_110 = new([
+ new(FN_MAIN, REV_110_MAIN, [
+ new(0x000110, 0x1D6D5C),
+ new(0x1D6E70, 0x323C0C),
+ new(0x4FAB90, 0x035AFC),
+ new(0x530690, 0x0362BC),
+ new(0x566A60, 0x035AFC),
+ new(0x59C560, 0x0362BC),
+ new(0x5D2930, 0x035AFC),
+ new(0x608430, 0x0362BC),
+ new(0x63E800, 0x035AFC),
+ new(0x674300, 0x0362BC),
+ new(0x6AA6D0, 0x035AFC),
+ new(0x6E01D0, 0x0362BC),
+ new(0x7165A0, 0x035AFC),
+ new(0x74C0A0, 0x0362BC),
+ new(0x782470, 0x035AFC),
+ new(0x7B7F70, 0x0362BC),
+ new(0x7EE340, 0x035AFC),
+ new(0x823E40, 0x0362BC),
+ new(0x85A100, 0x26899C)
+ ]),
+ new(FN_PERSONAL, REV_110_PERSONAL, [
+ new(0x00110, 0x35AFC),
+ new(0x35C10, 0x362BC)
+ ]),
+ new(FN_POSTBOX, REV_110_POSTBOX, [
+ new(0x000100, 0xB4448C)
+ ]),
+ new(FN_PHOTO, REV_110_PHOTO, [
+ new(0x000100, 0x262BC)
+ ]),
+ new(FN_PROFILE, REV_110_PROFILE, [
+ new(0x000100, 0x6945C)
+ ])
+ ]);
- #endregion
+ #endregion
- #region REVISION 1.2.0
+ #region REVISION 1.2.0
- internal const int REV_120_MAIN = 0xACECD0;
- internal const int REV_120_PERSONAL = 0x6D6C0;
- internal const int REV_120_POSTBOX = REV_110_POSTBOX;
- internal const int REV_120_PHOTO = 0x2C9C0;
- internal const int REV_120_PROFILE = REV_110_PROFILE;
+ internal const int REV_120_MAIN = 0xACECD0;
+ internal const int REV_120_PERSONAL = 0x6D6C0;
+ internal const int REV_120_POSTBOX = REV_110_POSTBOX;
+ internal const int REV_120_PHOTO = 0x2C9C0;
+ internal const int REV_120_PROFILE = REV_110_PROFILE;
- public static readonly FileHashInfo REV_120 = new(new FileHashDetails[]
- {
- new(FN_MAIN, REV_120_MAIN, new FileHashRegion[]
- {
- new(0x000110, 0x1D6D5C),
- new(0x1D6E70, 0x323EBC),
- new(0x4FAE40, 0x035D2C),
- new(0x530B70, 0x03787C),
- new(0x568500, 0x035D2C),
- new(0x59E230, 0x03787C),
- new(0x5D5BC0, 0x035D2C),
- new(0x60B8F0, 0x03787C),
- new(0x643280, 0x035D2C),
- new(0x678FB0, 0x03787C),
- new(0x6B0940, 0x035D2C),
- new(0x6E6670, 0x03787C),
- new(0x71E000, 0x035D2C),
- new(0x753D30, 0x03787C),
- new(0x78B6C0, 0x035D2C),
- new(0x7C13F0, 0x03787C),
- new(0x7F8D80, 0x035D2C),
- new(0x82EAB0, 0x03787C),
- new(0x866330, 0x26899C)
- }),
- new(FN_PERSONAL, REV_120_PERSONAL, new FileHashRegion[]
- {
- new(0x00110, 0x35D2C),
- new(0x35E40, 0x3787C)
- }),
- new(FN_POSTBOX, REV_120_POSTBOX, new FileHashRegion[]
- {
- new(0x000100, 0xB4448C)
- }),
- new(FN_PHOTO, REV_120_PHOTO, new FileHashRegion[]
- {
- new(0x000100, 0x2C8BC)
- }),
- new(FN_PROFILE, REV_120_PROFILE, new FileHashRegion[]
- {
- new(0x000100, 0x6945C)
- }),
- });
+ public static readonly FileHashInfo REV_120 = new([
+ new(FN_MAIN, REV_120_MAIN, [
+ new(0x000110, 0x1D6D5C),
+ new(0x1D6E70, 0x323EBC),
+ new(0x4FAE40, 0x035D2C),
+ new(0x530B70, 0x03787C),
+ new(0x568500, 0x035D2C),
+ new(0x59E230, 0x03787C),
+ new(0x5D5BC0, 0x035D2C),
+ new(0x60B8F0, 0x03787C),
+ new(0x643280, 0x035D2C),
+ new(0x678FB0, 0x03787C),
+ new(0x6B0940, 0x035D2C),
+ new(0x6E6670, 0x03787C),
+ new(0x71E000, 0x035D2C),
+ new(0x753D30, 0x03787C),
+ new(0x78B6C0, 0x035D2C),
+ new(0x7C13F0, 0x03787C),
+ new(0x7F8D80, 0x035D2C),
+ new(0x82EAB0, 0x03787C),
+ new(0x866330, 0x26899C)
+ ]),
+ new(FN_PERSONAL, REV_120_PERSONAL, [
+ new(0x00110, 0x35D2C),
+ new(0x35E40, 0x3787C)
+ ]),
+ new(FN_POSTBOX, REV_120_POSTBOX, [
+ new(0x000100, 0xB4448C)
+ ]),
+ new(FN_PHOTO, REV_120_PHOTO, [
+ new(0x000100, 0x2C8BC)
+ ]),
+ new(FN_PROFILE, REV_120_PROFILE, [
+ new(0x000100, 0x6945C)
+ ])
+ ]);
- #endregion
+ #endregion
- #region REVISION 1.3.0
+ #region REVISION 1.3.0
- internal const int REV_130_MAIN = 0xACED80;
- internal const int REV_130_PERSONAL = 0x6D6D0;
- internal const int REV_130_POSTBOX = REV_110_POSTBOX;
- internal const int REV_130_PHOTO = REV_120_PHOTO;
- internal const int REV_130_PROFILE = REV_110_PROFILE;
+ internal const int REV_130_MAIN = 0xACED80;
+ internal const int REV_130_PERSONAL = 0x6D6D0;
+ internal const int REV_130_POSTBOX = REV_110_POSTBOX;
+ internal const int REV_130_PHOTO = REV_120_PHOTO;
+ internal const int REV_130_PROFILE = REV_110_PROFILE;
- public static readonly FileHashInfo REV_130 = new(new FileHashDetails[]
- {
- new(FN_MAIN, REV_130_MAIN, new FileHashRegion[]
- {
- new(0x000110, 0x1D6D5C),
- new(0x1D6E70, 0x323EEC),
- new(0x4FAE70, 0x035D2C),
- new(0x530BA0, 0x03788C),
- new(0x568540, 0x035D2C),
- new(0x59E270, 0x03788C),
- new(0x5D5c10, 0x035D2C),
- new(0x60B940, 0x03788C),
- new(0x6432E0, 0x035D2C),
- new(0x679010, 0x03788C),
- new(0x6B09B0, 0x035D2C),
- new(0x6E66E0, 0x03788C),
- new(0x71E080, 0x035D2C),
- new(0x753DB0, 0x03788C),
- new(0x78B750, 0x035D2C),
- new(0x7C1480, 0x03788C),
- new(0x7F8E20, 0x035D2C),
- new(0x82EB50, 0x03788C),
- new(0x8663E0, 0x26899C)
- }),
- new(FN_PERSONAL, REV_130_PERSONAL, new FileHashRegion[]
- {
- new(0x00110, 0x35D2C),
- new(0x35E40, 0x3788C)
- }),
- new(FN_POSTBOX, REV_130_POSTBOX, new FileHashRegion[]
- {
- new(0x000100, 0xB4448C)
- }),
- new(FN_PHOTO, REV_130_PHOTO, new FileHashRegion[]
- {
- new(0x000100, 0x2C8BC)
- }),
- new(FN_PROFILE, REV_130_PROFILE, new FileHashRegion[]
- {
- new(0x000100, 0x6945C)
- }),
- });
+ public static readonly FileHashInfo REV_130 = new([
+ new(FN_MAIN, REV_130_MAIN, [
+ new(0x000110, 0x1D6D5C),
+ new(0x1D6E70, 0x323EEC),
+ new(0x4FAE70, 0x035D2C),
+ new(0x530BA0, 0x03788C),
+ new(0x568540, 0x035D2C),
+ new(0x59E270, 0x03788C),
+ new(0x5D5c10, 0x035D2C),
+ new(0x60B940, 0x03788C),
+ new(0x6432E0, 0x035D2C),
+ new(0x679010, 0x03788C),
+ new(0x6B09B0, 0x035D2C),
+ new(0x6E66E0, 0x03788C),
+ new(0x71E080, 0x035D2C),
+ new(0x753DB0, 0x03788C),
+ new(0x78B750, 0x035D2C),
+ new(0x7C1480, 0x03788C),
+ new(0x7F8E20, 0x035D2C),
+ new(0x82EB50, 0x03788C),
+ new(0x8663E0, 0x26899C)
+ ]),
+ new(FN_PERSONAL, REV_130_PERSONAL, [
+ new(0x00110, 0x35D2C),
+ new(0x35E40, 0x3788C)
+ ]),
+ new(FN_POSTBOX, REV_130_POSTBOX, [
+ new(0x000100, 0xB4448C)
+ ]),
+ new(FN_PHOTO, REV_130_PHOTO, [
+ new(0x000100, 0x2C8BC)
+ ]),
+ new(FN_PROFILE, REV_130_PROFILE, [
+ new(0x000100, 0x6945C)
+ ])
+ ]);
- #endregion
+ #endregion
- #region REVISION 1.4.0
+ #region REVISION 1.4.0
- internal const int REV_140_MAIN = 0xB05790;
- internal const int REV_140_PERSONAL = 0x74420;
- internal const int REV_140_POSTBOX = REV_110_POSTBOX;
- internal const int REV_140_PHOTO = REV_120_PHOTO;
- internal const int REV_140_PROFILE = REV_110_PROFILE;
+ internal const int REV_140_MAIN = 0xB05790;
+ internal const int REV_140_PERSONAL = 0x74420;
+ internal const int REV_140_POSTBOX = REV_110_POSTBOX;
+ internal const int REV_140_PHOTO = REV_120_PHOTO;
+ internal const int REV_140_PROFILE = REV_110_PROFILE;
- public static readonly FileHashInfo REV_140 = new(new FileHashDetails[]
- {
- new(FN_MAIN, REV_140_MAIN, new FileHashRegion[]
- {
- new(0x000110, 0x1d6d5c),
- new(0x1d6e70, 0x323f2c),
- new(0x4faeb0, 0x035d2c),
- new(0x530be0, 0x03e5dc),
- new(0x56f2d0, 0x035d2c),
- new(0x5a5000, 0x03e5dc),
- new(0x5e36f0, 0x035d2c),
- new(0x619420, 0x03e5dc),
- new(0x657b10, 0x035d2c),
- new(0x68d840, 0x03e5dc),
- new(0x6cbf30, 0x035d2c),
- new(0x701c60, 0x03e5dc),
- new(0x740350, 0x035d2c),
- new(0x776080, 0x03e5dc),
- new(0x7b4770, 0x035d2c),
- new(0x7ea4a0, 0x03e5dc),
- new(0x828b90, 0x035d2c),
- new(0x85e8c0, 0x03e5dc),
- new(0x89cea0, 0x2688ec)
- }),
- new(FN_PERSONAL, REV_140_PERSONAL, new FileHashRegion[]
- {
- new(0x00110, 0x35D2C),
- new(0x35E40, 0x3E5DC)
- }),
- new(FN_POSTBOX, REV_140_POSTBOX, new FileHashRegion[]
- {
- new(0x000100, 0xB4448C)
- }),
- new(FN_PHOTO, REV_140_PHOTO, new FileHashRegion[]
- {
- new(0x000100, 0x2C8BC)
- }),
- new(FN_PROFILE, REV_140_PROFILE, new FileHashRegion[]
- {
- new(0x000100, 0x6945C)
- }),
- });
+ public static readonly FileHashInfo REV_140 = new([
+ new(FN_MAIN, REV_140_MAIN, [
+ new(0x000110, 0x1d6d5c),
+ new(0x1d6e70, 0x323f2c),
+ new(0x4faeb0, 0x035d2c),
+ new(0x530be0, 0x03e5dc),
+ new(0x56f2d0, 0x035d2c),
+ new(0x5a5000, 0x03e5dc),
+ new(0x5e36f0, 0x035d2c),
+ new(0x619420, 0x03e5dc),
+ new(0x657b10, 0x035d2c),
+ new(0x68d840, 0x03e5dc),
+ new(0x6cbf30, 0x035d2c),
+ new(0x701c60, 0x03e5dc),
+ new(0x740350, 0x035d2c),
+ new(0x776080, 0x03e5dc),
+ new(0x7b4770, 0x035d2c),
+ new(0x7ea4a0, 0x03e5dc),
+ new(0x828b90, 0x035d2c),
+ new(0x85e8c0, 0x03e5dc),
+ new(0x89cea0, 0x2688ec)
+ ]),
+ new(FN_PERSONAL, REV_140_PERSONAL, [
+ new(0x00110, 0x35D2C),
+ new(0x35E40, 0x3E5DC)
+ ]),
+ new(FN_POSTBOX, REV_140_POSTBOX, [
+ new(0x000100, 0xB4448C)
+ ]),
+ new(FN_PHOTO, REV_140_PHOTO, [
+ new(0x000100, 0x2C8BC)
+ ]),
+ new(FN_PROFILE, REV_140_PROFILE, [
+ new(0x000100, 0x6945C)
+ ])
+ ]);
- #endregion
+ #endregion
- #region REVISION 1.5.0
+ #region REVISION 1.5.0
- internal const int REV_150_MAIN = 0xB20750;
- internal const int REV_150_PERSONAL = 0x76390;
- internal const int REV_150_POSTBOX = REV_110_POSTBOX;
- internal const int REV_150_PHOTO = REV_120_PHOTO;
- internal const int REV_150_PROFILE = REV_110_PROFILE;
+ internal const int REV_150_MAIN = 0xB20750;
+ internal const int REV_150_PERSONAL = 0x76390;
+ internal const int REV_150_POSTBOX = REV_110_POSTBOX;
+ internal const int REV_150_PHOTO = REV_120_PHOTO;
+ internal const int REV_150_PROFILE = REV_110_PROFILE;
- public static readonly FileHashInfo REV_150 = new(new FileHashDetails[]
- {
- new(FN_MAIN, REV_150_MAIN, new FileHashRegion[]
- {
- new(0x000110, 0x1e215c),
- new(0x1e2270, 0x323f6c),
- new(0x5062f0, 0x03693c),
- new(0x53cc30, 0x03f93c),
- new(0x57c680, 0x03693c),
- new(0x5b2fc0, 0x03f93c),
- new(0x5f2a10, 0x03693c),
- new(0x629350, 0x03f93c),
- new(0x668da0, 0x03693c),
- new(0x69f6e0, 0x03f93c),
- new(0x6df130, 0x03693c),
- new(0x715a70, 0x03f93c),
- new(0x7554c0, 0x03693c),
- new(0x78be00, 0x03f93c),
- new(0x7cb850, 0x03693c),
- new(0x802190, 0x03f93c),
- new(0x841be0, 0x03693c),
- new(0x878520, 0x03f93c),
- new(0x8b7e60, 0x2688ec)
- }),
- new(FN_PERSONAL, REV_150_PERSONAL, new FileHashRegion[]
- {
- new(0x00110, 0x3693c),
- new(0x36a50, 0x3f93c)
- }),
- new(FN_POSTBOX, REV_150_POSTBOX, new FileHashRegion[]
- {
- new(0x000100, 0xB4448C)
- }),
- new(FN_PHOTO, REV_150_PHOTO, new FileHashRegion[]
- {
- new(0x000100, 0x2C8BC)
- }),
- new(FN_PROFILE, REV_150_PROFILE, new FileHashRegion[]
- {
- new(0x000100, 0x6945C)
- }),
- });
+ public static readonly FileHashInfo REV_150 = new([
+ new(FN_MAIN, REV_150_MAIN, [
+ new(0x000110, 0x1e215c),
+ new(0x1e2270, 0x323f6c),
+ new(0x5062f0, 0x03693c),
+ new(0x53cc30, 0x03f93c),
+ new(0x57c680, 0x03693c),
+ new(0x5b2fc0, 0x03f93c),
+ new(0x5f2a10, 0x03693c),
+ new(0x629350, 0x03f93c),
+ new(0x668da0, 0x03693c),
+ new(0x69f6e0, 0x03f93c),
+ new(0x6df130, 0x03693c),
+ new(0x715a70, 0x03f93c),
+ new(0x7554c0, 0x03693c),
+ new(0x78be00, 0x03f93c),
+ new(0x7cb850, 0x03693c),
+ new(0x802190, 0x03f93c),
+ new(0x841be0, 0x03693c),
+ new(0x878520, 0x03f93c),
+ new(0x8b7e60, 0x2688ec)
+ ]),
+ new(FN_PERSONAL, REV_150_PERSONAL, [
+ new(0x00110, 0x3693c),
+ new(0x36a50, 0x3f93c)
+ ]),
+ new(FN_POSTBOX, REV_150_POSTBOX, [
+ new(0x000100, 0xB4448C)
+ ]),
+ new(FN_PHOTO, REV_150_PHOTO, [
+ new(0x000100, 0x2C8BC)
+ ]),
+ new(FN_PROFILE, REV_150_PROFILE, [
+ new(0x000100, 0x6945C)
+ ])
+ ]);
- #endregion
+ #endregion
- #region REVISION 1.6.0
+ #region REVISION 1.6.0
- internal const int REV_160_MAIN = 0xB258E0;
- internal const int REV_160_PERSONAL = 0x76CF0;
- internal const int REV_160_POSTBOX = REV_110_POSTBOX;
- internal const int REV_160_PHOTO = REV_120_PHOTO;
- internal const int REV_160_PROFILE = REV_110_PROFILE;
+ internal const int REV_160_MAIN = 0xB258E0;
+ internal const int REV_160_PERSONAL = 0x76CF0;
+ internal const int REV_160_POSTBOX = REV_110_POSTBOX;
+ internal const int REV_160_PHOTO = REV_120_PHOTO;
+ internal const int REV_160_PROFILE = REV_110_PROFILE;
- public static readonly FileHashInfo REV_160 = new(new FileHashDetails[]
- {
- new(FN_MAIN, REV_160_MAIN, new FileHashRegion[]
- {
- new(0x000110, 0x1e215c),
- new(0x1e2270, 0x32403c),
- new(0x5063c0, 0x03693c),
- new(0x53cd00, 0x04029c),
- new(0x57d0b0, 0x03693c),
- new(0x5b39f0, 0x04029c),
- new(0x5f3da0, 0x03693c),
- new(0x62a6e0, 0x04029c),
- new(0x66aa90, 0x03693c),
- new(0x6a13d0, 0x04029c),
- new(0x6e1780, 0x03693c),
- new(0x7180c0, 0x04029c),
- new(0x758470, 0x03693c),
- new(0x78edb0, 0x04029c),
- new(0x7cf160, 0x03693c),
- new(0x805aa0, 0x04029c),
- new(0x845e50, 0x03693c),
- new(0x87c790, 0x04029c),
- new(0x8bca30, 0x268eac)
- }),
- new(FN_PERSONAL, REV_160_PERSONAL, new FileHashRegion[]
- {
- new(0x00110, 0x3693c),
- new(0x36a50, 0x4029c)
- }),
- new(FN_POSTBOX, REV_160_POSTBOX, new FileHashRegion[]
- {
- new(0x000100, 0xB4448C)
- }),
- new(FN_PHOTO, REV_160_PHOTO, new FileHashRegion[]
- {
- new(0x000100, 0x2C8BC)
- }),
- new(FN_PROFILE, REV_160_PROFILE, new FileHashRegion[]
- {
- new(0x000100, 0x6945C)
- }),
- });
+ public static readonly FileHashInfo REV_160 = new([
+ new(FN_MAIN, REV_160_MAIN, [
+ new(0x000110, 0x1e215c),
+ new(0x1e2270, 0x32403c),
+ new(0x5063c0, 0x03693c),
+ new(0x53cd00, 0x04029c),
+ new(0x57d0b0, 0x03693c),
+ new(0x5b39f0, 0x04029c),
+ new(0x5f3da0, 0x03693c),
+ new(0x62a6e0, 0x04029c),
+ new(0x66aa90, 0x03693c),
+ new(0x6a13d0, 0x04029c),
+ new(0x6e1780, 0x03693c),
+ new(0x7180c0, 0x04029c),
+ new(0x758470, 0x03693c),
+ new(0x78edb0, 0x04029c),
+ new(0x7cf160, 0x03693c),
+ new(0x805aa0, 0x04029c),
+ new(0x845e50, 0x03693c),
+ new(0x87c790, 0x04029c),
+ new(0x8bca30, 0x268eac)
+ ]),
+ new(FN_PERSONAL, REV_160_PERSONAL, [
+ new(0x00110, 0x3693c),
+ new(0x36a50, 0x4029c)
+ ]),
+ new(FN_POSTBOX, REV_160_POSTBOX, [
+ new(0x000100, 0xB4448C)
+ ]),
+ new(FN_PHOTO, REV_160_PHOTO, [
+ new(0x000100, 0x2C8BC)
+ ]),
+ new(FN_PROFILE, REV_160_PROFILE, [
+ new(0x000100, 0x6945C)
+ ])
+ ]);
- #endregion
+ #endregion
- #region REVISION 1.7.0
+ #region REVISION 1.7.0
- internal const int REV_170_MAIN = 0x849C30; // reduced size
- internal const int REV_170_PERSONAL = 0x64140; // reduced size
- internal const int REV_170_POSTBOX = 0x47430; // reduced size
- internal const int REV_170_PHOTO = REV_120_PHOTO;
- internal const int REV_170_PROFILE = REV_110_PROFILE;
+ internal const int REV_170_MAIN = 0x849C30; // reduced size
+ internal const int REV_170_PERSONAL = 0x64140; // reduced size
+ internal const int REV_170_POSTBOX = 0x47430; // reduced size
+ internal const int REV_170_PHOTO = REV_120_PHOTO;
+ internal const int REV_170_PROFILE = REV_110_PROFILE;
- public static readonly FileHashInfo REV_170 = new(new FileHashDetails[]
- {
- new(FN_MAIN, REV_170_MAIN, new FileHashRegion[]
- {
- new(0x000110, 0x1e215c),
- new(0x1e2270, 0x3221fc),
- new(0x504580, 0x03693c),
- new(0x53aec0, 0x02d6ec),
- new(0x5686c0, 0x03693c),
- new(0x59f000, 0x02d6ec),
- new(0x5cc800, 0x03693c),
- new(0x603140, 0x02d6ec),
- new(0x630940, 0x03693c),
- new(0x667280, 0x02d6ec),
- new(0x694a80, 0x03693c),
- new(0x6cb3c0, 0x02d6ec),
- new(0x6f8bc0, 0x03693c),
- new(0x72f500, 0x02d6ec),
- new(0x75cd00, 0x03693c),
- new(0x793640, 0x02d6ec),
- new(0x7c0e40, 0x03693c),
- new(0x7f7780, 0x02d6ec),
- new(0x824e70, 0x024dbc),
- }),
- new(FN_PERSONAL, REV_170_PERSONAL, new FileHashRegion[]
- {
- new(0x00110, 0x3693c),
- new(0x36a50, 0x2d6ec),
- }),
- new(FN_POSTBOX, REV_170_POSTBOX, new FileHashRegion[]
- {
- new(0x000100, 0x4732c)
- }),
- new(FN_PHOTO, REV_170_PHOTO, new FileHashRegion[]
- {
- new(0x000100, 0x2C8BC)
- }),
- new(FN_PROFILE, REV_170_PROFILE, new FileHashRegion[]
- {
- new(0x000100, 0x6945C)
- }),
- });
+ public static readonly FileHashInfo REV_170 = new([
+ new(FN_MAIN, REV_170_MAIN, [
+ new(0x000110, 0x1e215c),
+ new(0x1e2270, 0x3221fc),
+ new(0x504580, 0x03693c),
+ new(0x53aec0, 0x02d6ec),
+ new(0x5686c0, 0x03693c),
+ new(0x59f000, 0x02d6ec),
+ new(0x5cc800, 0x03693c),
+ new(0x603140, 0x02d6ec),
+ new(0x630940, 0x03693c),
+ new(0x667280, 0x02d6ec),
+ new(0x694a80, 0x03693c),
+ new(0x6cb3c0, 0x02d6ec),
+ new(0x6f8bc0, 0x03693c),
+ new(0x72f500, 0x02d6ec),
+ new(0x75cd00, 0x03693c),
+ new(0x793640, 0x02d6ec),
+ new(0x7c0e40, 0x03693c),
+ new(0x7f7780, 0x02d6ec),
+ new(0x824e70, 0x024dbc)
+ ]),
+ new(FN_PERSONAL, REV_170_PERSONAL, [
+ new(0x00110, 0x3693c),
+ new(0x36a50, 0x2d6ec)
+ ]),
+ new(FN_POSTBOX, REV_170_POSTBOX, [
+ new(0x000100, 0x4732c)
+ ]),
+ new(FN_PHOTO, REV_170_PHOTO, [
+ new(0x000100, 0x2C8BC)
+ ]),
+ new(FN_PROFILE, REV_170_PROFILE, [
+ new(0x000100, 0x6945C)
+ ])
+ ]);
- #endregion
+ #endregion
- #region REVISION 1.8.0 // Same as 1.7.0
+ #region REVISION 1.8.0 // Same as 1.7.0
- internal const int REV_180_MAIN = REV_170_MAIN;
- internal const int REV_180_PERSONAL = REV_170_PERSONAL;
- internal const int REV_180_POSTBOX = REV_170_POSTBOX;
- internal const int REV_180_PHOTO = REV_120_PHOTO;
- internal const int REV_180_PROFILE = REV_110_PROFILE;
+ internal const int REV_180_MAIN = REV_170_MAIN;
+ internal const int REV_180_PERSONAL = REV_170_PERSONAL;
+ internal const int REV_180_POSTBOX = REV_170_POSTBOX;
+ internal const int REV_180_PHOTO = REV_120_PHOTO;
+ internal const int REV_180_PROFILE = REV_110_PROFILE;
- public static readonly FileHashInfo REV_180 = new(REV_170);
+ public static readonly FileHashInfo REV_180 = new(REV_170);
- #endregion
+ #endregion
- #region REVISION 1.9.0
+ #region REVISION 1.9.0
- internal const int REV_190_MAIN = 0x86D560;
- internal const int REV_190_PERSONAL = 0x64160;
- internal const int REV_190_POSTBOX = REV_170_POSTBOX;
- internal const int REV_190_PHOTO = REV_120_PHOTO;
- internal const int REV_190_PROFILE = REV_110_PROFILE;
+ internal const int REV_190_MAIN = 0x86D560;
+ internal const int REV_190_PERSONAL = 0x64160;
+ internal const int REV_190_POSTBOX = REV_170_POSTBOX;
+ internal const int REV_190_PHOTO = REV_120_PHOTO;
+ internal const int REV_190_PROFILE = REV_110_PROFILE;
- public static readonly FileHashInfo REV_190 = new(new FileHashDetails[]
- {
- new(FN_MAIN, REV_190_MAIN, new FileHashRegion[]
- {
- new(0x000110, 0x1e215c),
- new(0x1e2270, 0x34582c),
- new(0x527bb0, 0x03693c),
- new(0x55e4f0, 0x02d70c),
- new(0x58bd10, 0x03693c),
- new(0x5c2650, 0x02d70c),
- new(0x5efe70, 0x03693c),
- new(0x6267b0, 0x02d70c),
- new(0x653fd0, 0x03693c),
- new(0x68a910, 0x02d70c),
- new(0x6b8130, 0x03693c),
- new(0x6eea70, 0x02d70c),
- new(0x71c290, 0x03693c),
- new(0x752bd0, 0x02d70c),
- new(0x7803f0, 0x03693c),
- new(0x7b6d30, 0x02d70c),
- new(0x7e4550, 0x03693c),
- new(0x81ae90, 0x02d70c),
- new(0x8485a0, 0x024fbc),
- }),
- new(FN_PERSONAL, REV_190_PERSONAL, new FileHashRegion[]
- {
- new(0x00110, 0x3693c),
- new(0x36a50, 0x2d70c),
- }),
- new(FN_POSTBOX, REV_190_POSTBOX, new FileHashRegion[]
- {
- new(0x000100, 0x4732c)
- }),
- new(FN_PHOTO, REV_190_PHOTO, new FileHashRegion[]
- {
- new(0x000100, 0x2C8BC)
- }),
- new(FN_PROFILE, REV_190_PROFILE, new FileHashRegion[]
- {
- new(0x000100, 0x6945C)
- }),
- });
+ public static readonly FileHashInfo REV_190 = new([
+ new(FN_MAIN, REV_190_MAIN, [
+ new(0x000110, 0x1e215c),
+ new(0x1e2270, 0x34582c),
+ new(0x527bb0, 0x03693c),
+ new(0x55e4f0, 0x02d70c),
+ new(0x58bd10, 0x03693c),
+ new(0x5c2650, 0x02d70c),
+ new(0x5efe70, 0x03693c),
+ new(0x6267b0, 0x02d70c),
+ new(0x653fd0, 0x03693c),
+ new(0x68a910, 0x02d70c),
+ new(0x6b8130, 0x03693c),
+ new(0x6eea70, 0x02d70c),
+ new(0x71c290, 0x03693c),
+ new(0x752bd0, 0x02d70c),
+ new(0x7803f0, 0x03693c),
+ new(0x7b6d30, 0x02d70c),
+ new(0x7e4550, 0x03693c),
+ new(0x81ae90, 0x02d70c),
+ new(0x8485a0, 0x024fbc)
+ ]),
+ new(FN_PERSONAL, REV_190_PERSONAL, [
+ new(0x00110, 0x3693c),
+ new(0x36a50, 0x2d70c)
+ ]),
+ new(FN_POSTBOX, REV_190_POSTBOX, [
+ new(0x000100, 0x4732c)
+ ]),
+ new(FN_PHOTO, REV_190_PHOTO, [
+ new(0x000100, 0x2C8BC)
+ ]),
+ new(FN_PROFILE, REV_190_PROFILE, [
+ new(0x000100, 0x6945C)
+ ])
+ ]);
- #endregion
+ #endregion
- #region REVISION 1.10.0
+ #region REVISION 1.10.0
- internal const int REV_1100_MAIN = 0x86D570;
- internal const int REV_1100_PERSONAL = REV_190_PERSONAL;
- internal const int REV_1100_POSTBOX = REV_170_POSTBOX;
- internal const int REV_1100_PHOTO = 0x2C9D0;
- internal const int REV_1100_PROFILE = REV_110_PROFILE;
+ internal const int REV_1100_MAIN = 0x86D570;
+ internal const int REV_1100_PERSONAL = REV_190_PERSONAL;
+ internal const int REV_1100_POSTBOX = REV_170_POSTBOX;
+ internal const int REV_1100_PHOTO = 0x2C9D0;
+ internal const int REV_1100_PROFILE = REV_110_PROFILE;
- public static readonly FileHashInfo REV_1100 = new(new FileHashDetails[]
- {
- new(FN_MAIN, REV_1100_MAIN, new FileHashRegion[]
- {
- new(0x000110, 0x1e216c),
- new(0x1e2280, 0x34582c),
- new(0x527bc0, 0x03693c),
- new(0x55e500, 0x02d70c),
- new(0x58bd20, 0x03693c),
- new(0x5c2660, 0x02d70c),
- new(0x5efe80, 0x03693c),
- new(0x6267c0, 0x02d70c),
- new(0x653fe0, 0x03693c),
- new(0x68a920, 0x02d70c),
- new(0x6b8140, 0x03693c),
- new(0x6eea80, 0x02d70c),
- new(0x71c2a0, 0x03693c),
- new(0x752be0, 0x02d70c),
- new(0x780400, 0x03693c),
- new(0x7b6d40, 0x02d70c),
- new(0x7e4560, 0x03693c),
- new(0x81aea0, 0x02d70c),
- new(0x8485b0, 0x024fbc),
- }),
- new(FN_PERSONAL, REV_1100_PERSONAL, new FileHashRegion[]
- {
- new(0x00110, 0x3693c),
- new(0x36a50, 0x2d70c),
- }),
- new(FN_POSTBOX, REV_1100_POSTBOX, new FileHashRegion[]
- {
- new(0x000100, 0x4732c)
- }),
- new(FN_PHOTO, REV_1100_PHOTO, new FileHashRegion[]
- {
- new(0x000100, 0x2c8cc)
- }),
- new(FN_PROFILE, REV_1100_PROFILE, new FileHashRegion[]
- {
- new(0x000100, 0x6945C)
- }),
- });
+ public static readonly FileHashInfo REV_1100 = new([
+ new(FN_MAIN, REV_1100_MAIN, [
+ new(0x000110, 0x1e216c),
+ new(0x1e2280, 0x34582c),
+ new(0x527bc0, 0x03693c),
+ new(0x55e500, 0x02d70c),
+ new(0x58bd20, 0x03693c),
+ new(0x5c2660, 0x02d70c),
+ new(0x5efe80, 0x03693c),
+ new(0x6267c0, 0x02d70c),
+ new(0x653fe0, 0x03693c),
+ new(0x68a920, 0x02d70c),
+ new(0x6b8140, 0x03693c),
+ new(0x6eea80, 0x02d70c),
+ new(0x71c2a0, 0x03693c),
+ new(0x752be0, 0x02d70c),
+ new(0x780400, 0x03693c),
+ new(0x7b6d40, 0x02d70c),
+ new(0x7e4560, 0x03693c),
+ new(0x81aea0, 0x02d70c),
+ new(0x8485b0, 0x024fbc)
+ ]),
+ new(FN_PERSONAL, REV_1100_PERSONAL, [
+ new(0x00110, 0x3693c),
+ new(0x36a50, 0x2d70c)
+ ]),
+ new(FN_POSTBOX, REV_1100_POSTBOX, [
+ new(0x000100, 0x4732c)
+ ]),
+ new(FN_PHOTO, REV_1100_PHOTO, [
+ new(0x000100, 0x2c8cc)
+ ]),
+ new(FN_PROFILE, REV_1100_PROFILE, [
+ new(0x000100, 0x6945C)
+ ])
+ ]);
- #endregion
+ #endregion
- #region REVISION 1.11.0 // Same as 1.10.0
+ #region REVISION 1.11.0 // Same as 1.10.0
- internal const int REV_1110_MAIN = REV_1100_MAIN;
- internal const int REV_1110_PERSONAL = REV_190_PERSONAL;
- internal const int REV_1110_POSTBOX = REV_170_POSTBOX;
- internal const int REV_1110_PHOTO = REV_1100_PHOTO;
- internal const int REV_1110_PROFILE = REV_110_PROFILE;
+ internal const int REV_1110_MAIN = REV_1100_MAIN;
+ internal const int REV_1110_PERSONAL = REV_190_PERSONAL;
+ internal const int REV_1110_POSTBOX = REV_170_POSTBOX;
+ internal const int REV_1110_PHOTO = REV_1100_PHOTO;
+ internal const int REV_1110_PROFILE = REV_110_PROFILE;
- public static readonly FileHashInfo REV_1110 = REV_1100;
+ public static readonly FileHashInfo REV_1110 = REV_1100;
- #endregion
+ #endregion
- #region REVISION 2.0.0
+ #region REVISION 2.0.0
- internal const int REV_200_MAIN = 0x8F1BB0;
- internal const int REV_200_PERSONAL = 0x6A520;
- internal const int REV_200_POSTBOX = REV_170_POSTBOX;
- internal const int REV_200_PHOTO = 0x2F650;
- internal const int REV_200_PROFILE = REV_110_PROFILE;
- internal const int REV_200_WHEREAREN = 0xB8A4E0;
+ internal const int REV_200_MAIN = 0x8F1BB0;
+ internal const int REV_200_PERSONAL = 0x6A520;
+ internal const int REV_200_POSTBOX = REV_170_POSTBOX;
+ internal const int REV_200_PHOTO = 0x2F650;
+ internal const int REV_200_PROFILE = REV_110_PROFILE;
+ internal const int REV_200_WHEREAREN = 0xB8A4E0;
- public static readonly FileHashInfo REV_200 = new(new FileHashDetails[]
- {
- new(FN_MAIN, REV_200_MAIN, new FileHashRegion[]
- {
- new(0x000110, 0x1e339c),
- new(0x1e34b0, 0x36406c),
- new(0x547630, 0x03693c),
- new(0x57df70, 0x033acc),
- new(0x5b1b50, 0x03693c),
- new(0x5e8490, 0x033acc),
- new(0x61c070, 0x03693c),
- new(0x6529b0, 0x033acc),
- new(0x686590, 0x03693c),
- new(0x6bced0, 0x033acc),
- new(0x6f0ab0, 0x03693c),
- new(0x7273f0, 0x033acc),
- new(0x75afd0, 0x03693c),
- new(0x791910, 0x033acc),
- new(0x7c54f0, 0x03693c),
- new(0x7fbe30, 0x033acc),
- new(0x82fa10, 0x03693c),
- new(0x866350, 0x033acc),
- new(0x899e20, 0x057d8c),
- }),
- new(FN_PERSONAL, REV_200_PERSONAL, new FileHashRegion[]
- {
- new(0x00110, 0x3693c),
- new(0x36a50, 0x33acc),
- }),
- new(FN_POSTBOX, REV_200_POSTBOX, new FileHashRegion[]
- {
- new(0x100, 0x4732c),
- }),
- new(FN_PHOTO, REV_200_PHOTO, new FileHashRegion[]
- {
- new(0x100, 0x2f54c),
- }),
- new(FN_PROFILE, REV_200_PROFILE, new FileHashRegion[]
- {
- new(0x100, 0x6945c),
- }),
- new(FN_WHEREAREN, REV_200_WHEREAREN, new FileHashRegion[]
- {
- new(0x100, 0xB8A3DC),
- }),
- } );
+ public static readonly FileHashInfo REV_200 = new([
+ new(FN_MAIN, REV_200_MAIN, [
+ new(0x000110, 0x1e339c),
+ new(0x1e34b0, 0x36406c),
+ new(0x547630, 0x03693c),
+ new(0x57df70, 0x033acc),
+ new(0x5b1b50, 0x03693c),
+ new(0x5e8490, 0x033acc),
+ new(0x61c070, 0x03693c),
+ new(0x6529b0, 0x033acc),
+ new(0x686590, 0x03693c),
+ new(0x6bced0, 0x033acc),
+ new(0x6f0ab0, 0x03693c),
+ new(0x7273f0, 0x033acc),
+ new(0x75afd0, 0x03693c),
+ new(0x791910, 0x033acc),
+ new(0x7c54f0, 0x03693c),
+ new(0x7fbe30, 0x033acc),
+ new(0x82fa10, 0x03693c),
+ new(0x866350, 0x033acc),
+ new(0x899e20, 0x057d8c)
+ ]),
+ new(FN_PERSONAL, REV_200_PERSONAL, [
+ new(0x00110, 0x3693c),
+ new(0x36a50, 0x33acc)
+ ]),
+ new(FN_POSTBOX, REV_200_POSTBOX, [
+ new(0x100, 0x4732c)
+ ]),
+ new(FN_PHOTO, REV_200_PHOTO, [
+ new(0x100, 0x2f54c)
+ ]),
+ new(FN_PROFILE, REV_200_PROFILE, [
+ new(0x100, 0x6945c)
+ ]),
+ new(FN_WHEREAREN, REV_200_WHEREAREN, [
+ new(0x100, 0xB8A3DC)
+ ])
+ ]);
- #endregion
- }
-}
+ #endregion
+}
\ No newline at end of file
diff --git a/NHSE.Core/Hashing/Murmur3.cs b/NHSE.Core/Hashing/Murmur3.cs
index 1c85a6c..8fa46e0 100644
--- a/NHSE.Core/Hashing/Murmur3.cs
+++ b/NHSE.Core/Hashing/Murmur3.cs
@@ -1,85 +1,77 @@
using System;
+using static System.Buffers.Binary.BinaryPrimitives;
-namespace NHSE.Core
+namespace NHSE.Core;
+
+///
+/// MurmurHash implementation used by Animal Crossing New Horizons
+///
+public static class Murmur3
{
- ///
- /// MurmurHash implementation used by Animal Crossing New Horizons
- ///
- public static class Murmur3
+ private static uint Murmur32_Scramble(uint k)
{
- private static uint Murmur32_Scramble(uint k)
- {
- k = (k * 0x16A88000) | ((k * 0xCC9E2D51) >> 17);
- k *= 0x1B873593;
- return k;
- }
-
- ///
- /// Updates the hash at the specified offset, using the input parameters.
- ///
- /// Data to hash
- /// Where the data-to-be-hashed starts
- /// Amount of data to hash
- /// Initial Murmur seed (optional)
- /// Calculated hash.
- public static uint GetMurmur3Hash(byte[] data, int offset, uint size, uint seed = 0)
- {
- uint checksum = seed;
- if (size > 3)
- {
- for (var i = 0; i < (size / sizeof(uint)); i++)
- {
- var val = BitConverter.ToUInt32(data, offset);
- checksum ^= Murmur32_Scramble(val);
- checksum = (checksum >> 19) | (checksum << 13);
- checksum = (checksum * 5) + 0xE6546B64;
- offset += 4;
- }
- }
-
- var remainder = size % sizeof(uint);
- if (remainder != 0)
- {
- uint val = BitConverter.ToUInt32(data, (int)((offset + size) - remainder));
- for (var i = 0; i < (sizeof(uint) - remainder); i++)
- val >>= 8;
- checksum ^= Murmur32_Scramble(val);
- }
-
- checksum ^= size;
- checksum ^= checksum >> 16;
- checksum *= 0x85EBCA6B;
- checksum ^= checksum >> 13;
- checksum *= 0xC2B2AE35;
- checksum ^= checksum >> 16;
- return checksum;
- }
-
- ///
- /// Updates the hash at the specified offset, using the input parameters.
- ///
- /// Data to hash
- /// Offset to write the hash
- /// Where the data-to-be-hashed starts
- /// Amount of data to hash
- /// Calculated hash that was written back to the data.
- public static uint UpdateMurmur32(byte[] data, int hashOffset, int readOffset, uint readSize)
- {
- var newHash = GetMurmur3Hash(data, readOffset, readSize);
- var hashBytes = BitConverter.GetBytes(newHash);
- hashBytes.CopyTo(data, hashOffset);
- return newHash;
- }
-
- ///
- /// Checks the hash at the specified offset to see if the stored value matches the calculated value.
- ///
- /// Data to hash
- /// Offset to write the hash
- /// Where the data-to-be-hashed starts
- /// Amount of data to hash
- /// Calculated hash matches the currently stored hash.
- public static bool VerifyMurmur32(byte[] data, int hashOffset, int readOffset, uint readSize)
- => BitConverter.ToUInt32(data, hashOffset) == GetMurmur3Hash(data, readOffset, readSize);
+ k = (k * 0x16A88000) | ((k * 0xCC9E2D51) >> 17);
+ k *= 0x1B873593;
+ return k;
}
-}
+
+ ///
+ /// Updates the hash at the specified offset, using the input parameters.
+ ///
+ /// Data to hash
+ /// Initial Murmur seed (optional)
+ /// Calculated hash.
+ public static uint GetMurmur3Hash(ReadOnlySpan data, uint seed = 0)
+ {
+ var checksum = seed;
+ var remaining = data;
+ while (remaining.Length >= sizeof(uint))
+ {
+ var val = ReadUInt32LittleEndian(remaining);
+ checksum ^= Murmur32_Scramble(val);
+ checksum = (checksum >> 19) | (checksum << 13);
+ checksum = (checksum * 5) + 0xE6546B64;
+ remaining = remaining[sizeof(uint)..];
+ }
+
+ if (!remaining.IsEmpty)
+ {
+ uint val = 0;
+ switch (remaining.Length)
+ {
+ case 3:
+ val |= (uint)remaining[2] << 16;
+ goto case 2;
+ case 2:
+ val |= (uint)remaining[1] << 8;
+ goto case 1;
+ case 1:
+ val |= remaining[0];
+ break;
+ }
+
+ checksum ^= Murmur32_Scramble(val);
+ }
+
+ checksum ^= (uint)data.Length;
+ checksum ^= checksum >> 16;
+ checksum *= 0x85EBCA6B;
+ checksum ^= checksum >> 13;
+ checksum *= 0xC2B2AE35;
+ checksum ^= checksum >> 16;
+ return checksum;
+ }
+
+ ///
+ /// Updates the hash at the specified offset, using the input parameters.
+ ///
+ /// Data to hash
+ /// Location two write the hash
+ /// Calculated hash that was written back to the data.
+ public static uint UpdateMurmur32(ReadOnlySpan data, Span hashDestination)
+ {
+ var newHash = GetMurmur3Hash(data);
+ WriteUInt32LittleEndian(hashDestination, newHash);
+ return newHash;
+ }
+}
\ No newline at end of file
diff --git a/NHSE.Core/NHSE.Core.csproj b/NHSE.Core/NHSE.Core.csproj
index 1b25623..758ad54 100644
--- a/NHSE.Core/NHSE.Core.csproj
+++ b/NHSE.Core/NHSE.Core.csproj
@@ -1,9 +1,5 @@
-
- net46;netstandard2.0
-
-
diff --git a/NHSE.Core/Save/Files/MainSave.cs b/NHSE.Core/Save/Files/MainSave.cs
index 1dad702..0e1df1f 100644
--- a/NHSE.Core/Save/Files/MainSave.cs
+++ b/NHSE.Core/Save/Files/MainSave.cs
@@ -1,266 +1,262 @@
using System;
using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using static System.Buffers.Binary.BinaryPrimitives;
-namespace NHSE.Core
+namespace NHSE.Core;
+
+///
+/// main.dat
+///
+public sealed class MainSave : EncryptedFilePair
{
- ///
- /// main.dat
- ///
- public sealed class MainSave : EncryptedFilePair
+ public readonly MainSaveOffsets Offsets;
+ public MainSave(string folder) : base(folder, "main") => Offsets = MainSaveOffsets.GetOffsets(Info);
+
+ public Hemisphere Hemisphere { get => (Hemisphere)Data[Offsets.WeatherArea]; set => Data[Offsets.WeatherArea] = (byte)value; }
+ public AirportColor AirportThemeColor { get => (AirportColor)Data[Offsets.AirportThemeColor]; set => Data[Offsets.AirportThemeColor] = (byte)value; }
+
+ public uint WeatherSeed
{
- public readonly MainSaveOffsets Offsets;
- public MainSave(string folder) : base(folder, "main") => Offsets = MainSaveOffsets.GetOffsets(Info);
-
- public Hemisphere Hemisphere { get => (Hemisphere)Data[Offsets.WeatherArea]; set => Data[Offsets.WeatherArea] = (byte)value; }
- public AirportColor AirportThemeColor { get => (AirportColor)Data[Offsets.AirportThemeColor]; set => Data[Offsets.AirportThemeColor] = (byte)value; }
- public uint WeatherSeed { get => BitConverter.ToUInt32(Data, Offsets.WeatherRandSeed); set => BitConverter.GetBytes(value).CopyTo(Data, Offsets.WeatherRandSeed); }
-
- public IVillager GetVillager(int index) => Offsets.ReadVillager(Data, index);
- public void SetVillager(IVillager value, int index) => Offsets.WriteVillager(value, Data, index);
-
- public IVillagerHouse GetVillagerHouse(int index) => Offsets.ReadVillagerHouse(Data, index);
- public void SetVillagerHouse(IVillagerHouse value, int index) => Offsets.WriteVillagerHouse(value, Data, index);
-
- public IVillager[] GetVillagers()
- {
- var villagers = new IVillager[MainSaveOffsets.VillagerCount];
- for (int i = 0; i < villagers.Length; i++)
- villagers[i] = GetVillager(i);
- return villagers;
- }
-
- public void SetVillagers(IReadOnlyList villagers)
- {
- for (int i = 0; i < villagers.Count; i++)
- SetVillager(villagers[i], i);
- }
-
- public IVillagerHouse[] GetVillagerHouses()
- {
- var villagers = new IVillagerHouse[MainSaveOffsets.VillagerCount];
- for (int i = 0; i < villagers.Length; i++)
- villagers[i] = GetVillagerHouse(i);
- return villagers;
- }
-
- public void SetVillagerHouses(IReadOnlyList villagers)
- {
- for (int i = 0; i < villagers.Count; i++)
- SetVillagerHouse(villagers[i], i);
- }
-
- public DesignPattern GetDesign(int index) => Offsets.ReadPattern(Data, index);
- public void SetDesign(DesignPattern value, int index, byte[] playerID, byte[] townID) => Offsets.WritePattern(value, Data, index, playerID, townID);
- public DesignPatternPRO GetDesignPRO(int index) => Offsets.ReadPatternPRO(Data, index);
- public void SetDesignPRO(DesignPatternPRO value, int index, byte[] playerID, byte[] townID) => Offsets.WritePatternPRO(value, Data, index, playerID, townID);
-
- public IReadOnlyList
- RecycleBin
- {
- get => Item.GetArray(Data.Slice(Offsets.LostItemBox, MainSaveOffsets.RecycleBinCount * Item.SIZE));
- set => Item.SetArray(value).CopyTo(Data, Offsets.LostItemBox);
- }
-
- public IReadOnlyList Buildings
- {
- get => Building.GetArray(Data.Slice(Offsets.MainFieldStructure, MainSaveOffsets.BuildingCount * Building.SIZE));
- set => Building.SetArray(value).CopyTo(Data, Offsets.MainFieldStructure);
- }
-
- public IPlayerHouse GetPlayerHouse(int index) => Offsets.ReadPlayerHouse(Data, index);
- public void SetPlayerHouse(IPlayerHouse value, int index) => Offsets.WritePlayerHouse(value, Data, index);
-
- public IPlayerHouse[] GetPlayerHouses()
- {
- var players = new IPlayerHouse[MainSaveOffsets.PlayerCount];
- for (int i = 0; i < players.Length; i++)
- players[i] = GetPlayerHouse(i);
- return players;
- }
-
- public void SetPlayerHouses(IReadOnlyList houses)
- {
- for (int i = 0; i < houses.Count; i++)
- SetPlayerHouse(houses[i], i);
- }
-
- public DesignPattern[] GetDesigns()
- {
- var result = new DesignPattern[Offsets.PatternCount];
- for (int i = 0; i value, byte[] playerID, byte[] townID)
- {
- var count = Math.Min(Offsets.PatternCount, value.Count);
- for (int i = 0; i < count; i++)
- SetDesign(value[i], i, playerID, townID);
- }
-
- public DesignPatternPRO[] GetDesignsPRO()
- {
- var result = new DesignPatternPRO[Offsets.PatternCount];
- for (int i = 0; i < result.Length; i++)
- result[i] = GetDesignPRO(i);
- return result;
- }
-
- public void SetDesignsPRO(IReadOnlyList value, byte[] playerID, byte[] townID)
- {
- var count = Math.Min(Offsets.PatternCount, value.Count);
- for (int i = 0; i < count; i++)
- SetDesignPRO(value[i], i, playerID, townID);
- }
-
- public DesignPattern FlagMyDesign
- {
- get => MainSaveOffsets.ReadPatternAtOffset(Data, Offsets.PatternFlag);
- set => value.Data.CopyTo(Data, Offsets.PatternFlag);
- }
-
- public DesignPatternPRO[] GetDesignsTailor()
- {
- var result = new DesignPatternPRO[MainSaveOffsets.PatternTailorCount];
- for (int i = 0; i < result.Length; i++)
- result[i] = MainSaveOffsets.ReadPatternPROAtOffset(Data, Offsets.PatternTailor + (i * DesignPatternPRO.SIZE));
- return result;
- }
-
- public void SetDesignsTailor(IReadOnlyList value)
- {
- var count = Math.Min(Offsets.PatternCount, value.Count);
- for (int i = 0; i < count; i++)
- value[i].Data.CopyTo(Data, Offsets.PatternTailor + (i * DesignPatternPRO.SIZE));
- }
-
- private const int EventFlagsSaveCount = 0x400;
-
- public short[] GetEventFlagLand()
- {
- var value = new short[EventFlagsSaveCount];
- Buffer.BlockCopy(Data, Offsets.EventFlagLand, value, 0, sizeof(short) * value.Length);
- return value;
- }
-
- public void SetEventFlagLand(short[] value)
- {
- Buffer.BlockCopy(value, 0, Data, Offsets.EventFlagLand, sizeof(short) * value.Length);
- }
-
- public TurnipStonk Turnips
- {
- get => Data.Slice(Offsets.ShopKabu, TurnipStonk.SIZE).ToClass();
- set => value.ToBytesClass().CopyTo(Data, Offsets.ShopKabu);
- }
-
- public Museum Museum
- {
- get => new(Data.Slice(Offsets.Museum, Museum.SIZE));
- set => value.Data.CopyTo(Data, Offsets.Museum);
- }
-
- public const int AcreWidth = 7 + (2 * 1); // 1 on each side cannot be traversed
- private const int AcreHeight = 6 + (2 * 1); // 1 on each side cannot be traversed
- private const int AcreMax = AcreWidth * AcreHeight;
- private const int AcreSizeAll = AcreMax * 2;
-
- public ushort GetAcre(int index)
- {
- if ((uint)index > AcreMax)
- throw new ArgumentOutOfRangeException(nameof(index));
- return BitConverter.ToUInt16(Data, Offsets.OutsideField + (index * 2));
- }
-
- public void SetAcre(int index, ushort value)
- {
- if ((uint)index > AcreMax)
- throw new ArgumentOutOfRangeException(nameof(index));
- BitConverter.GetBytes(value).CopyTo(Data, Offsets.OutsideField + (index * 2));
- }
-
- public byte[] GetAcreBytes() => Data.Slice(Offsets.OutsideField, AcreSizeAll);
-
- public void SetAcreBytes(byte[] data)
- {
- if (data.Length != AcreSizeAll)
- throw new ArgumentOutOfRangeException(nameof(data.Length));
- data.CopyTo(Data, Offsets.OutsideField);
- }
-
- public TerrainTile[] GetTerrainTiles() => TerrainTile.GetArray(Data.Slice(Offsets.LandMakingMap, MapGrid.MapTileCount16x16 * TerrainTile.SIZE));
- public void SetTerrainTiles(IReadOnlyList array) => TerrainTile.SetArray(array).CopyTo(Data, Offsets.LandMakingMap);
-
- public const int MapDesignNone = 0xF800;
-
- public ushort[] GetMapDesignTiles()
- {
- var value = new ushort[112*96];
- Buffer.BlockCopy(Data, Offsets.MyDesignMap, value, 0, sizeof(ushort) * value.Length);
- return value;
- }
-
- public void SetMapDesignTiles(ushort[] value)
- {
- Buffer.BlockCopy(value, 0, Data, Offsets.MyDesignMap, sizeof(ushort) * value.Length);
- }
-
- private const int FieldItemLayerSize = MapGrid.MapTileCount32x32 * Item.SIZE;
- private const int FieldItemFlagSize = MapGrid.MapTileCount32x32 / 8; // bitflags
-
- private int FieldItemLayer1 => Offsets.FieldItem;
- private int FieldItemLayer2 => Offsets.FieldItem + FieldItemLayerSize;
- public int FieldItemFlag1 => Offsets.FieldItem + (FieldItemLayerSize * 2);
- public int FieldItemFlag2 => Offsets.FieldItem + (FieldItemLayerSize * 2) + FieldItemFlagSize;
-
- public Item[] GetFieldItemLayer1() => Item.GetArray(Data.Slice(FieldItemLayer1, FieldItemLayerSize));
- public void SetFieldItemLayer1(IReadOnlyList
- array) => Item.SetArray(array).CopyTo(Data, FieldItemLayer1);
-
- public Item[] GetFieldItemLayer2() => Item.GetArray(Data.Slice(FieldItemLayer2, FieldItemLayerSize));
- public void SetFieldItemLayer2(IReadOnlyList
- array) => Item.SetArray(array).CopyTo(Data, FieldItemLayer2);
-
- public ushort OutsideFieldTemplateUniqueId
- {
- get => BitConverter.ToUInt16(Data, Offsets.OutsideField + AcreSizeAll);
- set => BitConverter.GetBytes(value).CopyTo(Data, Offsets.OutsideField + AcreSizeAll);
- }
-
- public ushort MainFieldParamUniqueID
- {
- get => BitConverter.ToUInt16(Data, Offsets.OutsideField + AcreSizeAll + 2);
- set => BitConverter.GetBytes(value).CopyTo(Data, Offsets.OutsideField + AcreSizeAll + 2);
- }
-
- public uint EventPlazaLeftUpX
- {
- get => BitConverter.ToUInt32(Data, Offsets.OutsideField + AcreSizeAll + 4);
- set => BitConverter.GetBytes(value).CopyTo(Data, Offsets.OutsideField + AcreSizeAll + 4);
- }
-
- public uint EventPlazaLeftUpZ
- {
- get => BitConverter.ToUInt32(Data, Offsets.OutsideField + AcreSizeAll + 8);
- set => BitConverter.GetBytes(value).CopyTo(Data, Offsets.OutsideField + AcreSizeAll + 8);
- }
-
- public GSaveVisitorNpc Visitor
- {
- get => Data.ToClass(Offsets.Visitor, GSaveVisitorNpc.SIZE);
- set => value.ToBytesClass().CopyTo(Data, Offsets.Visitor);
- }
-
- public GSaveFg SaveFg
- {
- get => Data.ToClass(Offsets.SaveFg, GSaveFg.SIZE);
- set => value.ToBytesClass().CopyTo(Data, Offsets.SaveFg);
- }
-
- public GSaveTime LastSaved => Data.Slice(Offsets.LastSavedTime, GSaveTime.SIZE).ToStructure();
-
- public GSaveBulletinBoard Bulletin
- {
- get => Data.Slice(Offsets.BulletinBoard, GSaveBulletinBoard.SIZE).ToStructure();
- set => value.ToBytes().CopyTo(Data, Offsets.BulletinBoard);
- }
+ get => ReadUInt32LittleEndian(Data[Offsets.WeatherRandSeed..]);
+ set => WriteUInt32LittleEndian(Data[Offsets.WeatherRandSeed..], value);
}
-}
+
+ public IVillager GetVillager(int index) => Offsets.ReadVillager(Data, index);
+ public void SetVillager(IVillager value, int index) => Offsets.WriteVillager(value, Data, index);
+
+ public IVillagerHouse GetVillagerHouse(int index) => Offsets.ReadVillagerHouse(Data, index);
+ public void SetVillagerHouse(IVillagerHouse value, int index) => Offsets.WriteVillagerHouse(value, Data, index);
+
+ public IVillager[] GetVillagers()
+ {
+ var villagers = new IVillager[MainSaveOffsets.VillagerCount];
+ for (int i = 0; i < villagers.Length; i++)
+ villagers[i] = GetVillager(i);
+ return villagers;
+ }
+
+ public void SetVillagers(IReadOnlyList villagers)
+ {
+ for (int i = 0; i < villagers.Count; i++)
+ SetVillager(villagers[i], i);
+ }
+
+ public IVillagerHouse[] GetVillagerHouses()
+ {
+ var villagers = new IVillagerHouse[MainSaveOffsets.VillagerCount];
+ for (int i = 0; i < villagers.Length; i++)
+ villagers[i] = GetVillagerHouse(i);
+ return villagers;
+ }
+
+ public void SetVillagerHouses(IReadOnlyList villagers)
+ {
+ for (int i = 0; i < villagers.Count; i++)
+ SetVillagerHouse(villagers[i], i);
+ }
+
+ public DesignPattern GetDesign(int index) => Offsets.ReadPattern(Data, index);
+ public void SetDesign(DesignPattern value, int index, Span playerID, Span townID) => Offsets.WritePattern(value, Data, index, playerID, townID);
+ public DesignPatternPRO GetDesignPRO(int index) => Offsets.ReadPatternPRO(Data, index);
+ public void SetDesignPRO(DesignPatternPRO value, int index, Span playerID, Span townID) => Offsets.WritePatternPRO(value, Data, index, playerID, townID);
+
+ public IReadOnlyList
- RecycleBin
+ {
+ get => Item.GetArray(Data.Slice(Offsets.LostItemBox, MainSaveOffsets.RecycleBinCount * Item.SIZE));
+ set => Item.SetArray(value).CopyTo(Data[Offsets.LostItemBox..]);
+ }
+
+ public IReadOnlyList Buildings
+ {
+ get => Building.GetArray(Data.Slice(Offsets.MainFieldStructure, MainSaveOffsets.BuildingCount * Building.SIZE));
+ set => Building.SetArray(value).CopyTo(Data[Offsets.MainFieldStructure..]);
+ }
+
+ public IPlayerHouse GetPlayerHouse(int index) => Offsets.ReadPlayerHouse(Data, index);
+ public void SetPlayerHouse(IPlayerHouse value, int index) => Offsets.WritePlayerHouse(value, Data, index);
+
+ public IPlayerHouse[] GetPlayerHouses()
+ {
+ var players = new IPlayerHouse[MainSaveOffsets.PlayerCount];
+ for (int i = 0; i < players.Length; i++)
+ players[i] = GetPlayerHouse(i);
+ return players;
+ }
+
+ public void SetPlayerHouses(IReadOnlyList houses)
+ {
+ for (int i = 0; i < houses.Count; i++)
+ SetPlayerHouse(houses[i], i);
+ }
+
+ public DesignPattern[] GetDesigns()
+ {
+ var result = new DesignPattern[Offsets.PatternCount];
+ for (int i = 0; i value, Span playerID, Span townID)
+ {
+ var count = Math.Min(Offsets.PatternCount, value.Count);
+ for (int i = 0; i < count; i++)
+ SetDesign(value[i], i, playerID, townID);
+ }
+
+ public DesignPatternPRO[] GetDesignsPRO()
+ {
+ var result = new DesignPatternPRO[Offsets.PatternCount];
+ for (int i = 0; i < result.Length; i++)
+ result[i] = GetDesignPRO(i);
+ return result;
+ }
+
+ public void SetDesignsPRO(IReadOnlyList value, Span playerID, Span townID)
+ {
+ var count = Math.Min(Offsets.PatternCount, value.Count);
+ for (int i = 0; i < count; i++)
+ SetDesignPRO(value[i], i, playerID, townID);
+ }
+
+ public DesignPattern FlagMyDesign
+ {
+ get => MainSaveOffsets.ReadPatternAtOffset(Data, Offsets.PatternFlag);
+ set => value.Data.CopyTo(Data[Offsets.PatternFlag..]);
+ }
+
+ public DesignPatternPRO[] GetDesignsTailor()
+ {
+ var result = new DesignPatternPRO[MainSaveOffsets.PatternTailorCount];
+ for (int i = 0; i < result.Length; i++)
+ result[i] = MainSaveOffsets.ReadPatternPROAtOffset(Data, Offsets.PatternTailor + (i * DesignPatternPRO.SIZE));
+ return result;
+ }
+
+ public void SetDesignsTailor(IReadOnlyList value)
+ {
+ var count = Math.Min(Offsets.PatternCount, value.Count);
+ for (int i = 0; i < count; i++)
+ value[i].Data.CopyTo(Data.Slice(Offsets.PatternTailor + (i * DesignPatternPRO.SIZE)));
+ }
+
+ private const int EventFlagsSaveCount = 0x400;
+
+ public Span EventFlagLand => MemoryMarshal.Cast