diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..afac14a --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(dotnet build:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/.gitignore b/.gitignore index 9491a2f..dfc3dbf 100644 --- a/.gitignore +++ b/.gitignore @@ -194,6 +194,7 @@ PublishScripts/ # NuGet Packages *.nupkg +!LocalPackages/*.nupkg # NuGet Symbol Packages *.snupkg # The packages folder can be ignored because of Package Restore diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..57d77db --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Run Debug", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "Build Debug", + "program": "${workspaceFolder}/SOTFEdit/bin/Debug/net8.0-windows/SOTFEdit.exe", + "args": [], + "cwd": "${workspaceFolder}/SOTFEdit", + "stopAtEntry": false, + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..c175585 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,112 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Clean Release", + "type": "process", + "command": "pwsh", + "args": [ + "-Command", + "cls; dotnet clean SOTFEdit.sln --configuration Release --verbosity quiet" + ], + "problemMatcher": [], + "presentation": { + "reveal": "never", + "echo": false, + "focus": false, + "panel": "shared" + } + }, + { + "label": "Build Release", + "type": "process", + "command": "dotnet", + "args": [ + "build", + "SOTFEdit\\SOTFEdit.csproj", + "--configuration", + "Release" + ], + "problemMatcher": "$msCompile", + "dependsOn": [ + "Clean Release" + ], + "group": "build" + }, + { + "label": "Clean Debug", + "type": "process", + "command": "pwsh", + "args": [ + "-Command", + "cls; dotnet clean SOTFEdit.sln --configuration Debug --verbosity quiet" + ], + "problemMatcher": [], + "presentation": { + "reveal": "never", + "echo": false, + "focus": false, + "panel": "shared" + } + }, + { + "label": "Build Debug", + "type": "process", + "command": "dotnet", + "args": [ + "build", + "SOTFEdit\\SOTFEdit.csproj", + "--configuration", + "Debug" + ], + "problemMatcher": "$msCompile", + "dependsOn": [ + "Clean Debug" + ], + "group": { + "kind": "build", + "isDefault": false + } + }, + { + "label": "Run Release", + "type": "process", + "command": "dotnet", + "args": [ + "run", + "--project", + "SOTFEdit\\SOTFEdit.csproj", + "--configuration", + "Release" + ], + "group": { + "kind": "test", + "isDefault": false + }, + "presentation": { + "reveal": "always", + "panel": "shared" + } + }, + { + "label": "Run Debug", + "type": "process", + "command": "dotnet", + "args": [ + "run", + "--project", + "SOTFEdit\\SOTFEdit.csproj", + "--configuration", + "Debug" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "reveal": "always" + }, + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 031b97c..2993403 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,101 @@ # Changelog +## v1.2.1 (2025-12-02) + +### Bug Fixes +- **Map Window:** + - Fixed POI "done" status not refreshing when savegame auto-reloads + - Fixed "Hide Completed" filter not updating after auto-reload + - POI icons and visibility now correctly update when inventory changes are detected + - Fixed Area and Requirements filters not persisting between sessions + - Fixed Area filter values not updating when language is changed + +### Technical Changes +- Added `SelectedSavegameChangedEvent` listener to `MapViewModel` +- Implemented `RefreshAllPoiStatus()` method to refresh `IsDone` property and re-apply filters +- Added `RefreshDoneStatus()` public method to `BasePoi` class +- Added `ToString()` override to `AreaFilter` and `StaticAreaFilter` for proper serialization +- Made `StaticAreaFilter.Name` dynamic by storing translation key instead of translated string +- Added `LanguageChangedEvent` listener to `MapFilter` to refresh filter names + +## v1.2.0 (2025-11-05) + +### Features +- **Hot-Swap Language Switching:** + - Language changes now apply immediately without requiring application restart + - All UI elements update in real-time when changing language in settings + - Menu items, buttons, labels, and other text automatically refresh + +### Improvements +- **Translation System:** + - Migrated from JSON to YAML format for translation files (20% smaller file size) + - Implemented reactive translation bindings for dynamic language updates + - Added translation caching for improved performance (eliminated ~4000+ redundant YAML deserializations during map load) + - Fixed translation loading performance issue that caused application hang on map load + +### Technical Changes +- Created `TranslationProxy` class with `INotifyPropertyChanged` for reactive bindings +- Enhanced `TranslateExtension` to support both reactive bindings and static strings (FallbackValue compatibility) +- Added `LanguageChangedEvent` messaging for ViewModel property refresh +- Implemented `TranslationManager.ChangeCulture()` for centralized culture switching +- Added smart context detection to `TranslateExtension` (DependencyProperty vs. non-bindable contexts) + +## v1.1.0 (2025-11-04) + +### Features +- **Map Window Enhancements:** + - Added "Auto Reload" feature to automatically reload the savegame when it changes on disk + - Auto Reload now automatically loads the most recent savegame if none is selected + - Auto Reload triggers immediately when the map window opens (no 5-second delay) + - Added "Auto Connect" feature for companion app connectivity + - Map window now remembers which monitor it was on and opens maximized on the correct screen + - Fixed window restore behavior - properly restores to saved size when un-maximizing + - Added "Map -" / "Karte -" / "Mapa -" prefix to map window title for clarity + +### Improvements +- **Translation System:** + - Fixed main window title to show connection status, savegame info, and modified date + - Modified date display now shows just the date without "Modified:" / "Zuletzt geändert:" / "Zmodyfikowano:" prefix + +### Technical Changes +- Improved window position and state management using WPF RestoreBounds +- Added System.IO using directive for directory modification detection +- Enhanced multi-monitor support with proper window positioning + +## v1.0.2 (2025-11-02) + +### Changed +- Migrated POI completion tracking from StringCollection to a comma-separated string in settings for improved compatibility and performance. +- Refactored POI completion logic in BasePoi.cs to use the new string-based storage. +- Removed all legacy StringCollection logic for POI completion. + +### Notes +- All code referencing POI completion now uses `DonePoiIdListString`. +- This is a breaking change for users with old settings files; migration will occur automatically on first use. + +## v1.0.1 (2025-11-01) + +### Features +- Added ability to manually mark certain POIs as "done" (completed) on the map. +- POIs marked as done now display a special overlay icon. +- The "Hide completed" filter now hides both collected and manually completed POIs, including those in bunkers and caves. +- Not all POIs can be marked as done—only those with a real in-game identifier support this feature. + +### Improvements +- Updated translations and documentation to reflect the new filter and POI features. +- Cleaned up VS Code debug tasks and launch configuration to remove duplicates and confusion. + +## v1.0.0 + +### Major Changes +- Migrated the entire project from .NET 6 to .NET 8 for improved performance, security, and long-term support. +- Updated all dependencies and project files to be compatible with .NET 8. +- Improved build and runtime stability on modern Windows systems. + +### Notes +- This update may include further enhancements or fixes as part of the migration process. +- Please ensure you have the .NET 8.0 Desktop Runtime installed to run this version. + ## v0.12.10 Fix crash due to corrupt storages (caused by ingame bug) diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..26ab702 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,5 @@ + + + + + diff --git a/LocalPackages/.keep b/LocalPackages/.keep new file mode 100644 index 0000000..5b97cde --- /dev/null +++ b/LocalPackages/.keep @@ -0,0 +1,4 @@ +# Remove LocalPackages from .gitignore to allow it in git +# (auto-generated by GitHub Copilot) + +# LocalPackages is often used for local NuGet packages. Remove or comment out any ignore rules for it. diff --git a/LocalPackages/tqklibrary.autogrid.1.2.29.nupkg b/LocalPackages/tqklibrary.autogrid.1.2.29.nupkg new file mode 100644 index 0000000..624008c Binary files /dev/null and b/LocalPackages/tqklibrary.autogrid.1.2.29.nupkg differ diff --git a/README.md b/README.md index 9d6dbcf..4e40b00 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,17 @@ + # SOTFEdit - Sons of The Forest Savegame Editor +**Main reason for this fork:** Migration to .NET 8 for improved performance, security, and long-term support. This fork updates the project from .NET 6 to .NET 8 and may include further enhancements or fixes. + + ![Screenshot](https://abload.de/img/sotfeditfadso.jpg) -[![Build](https://github.com/codengine/SOTFEdit/actions/workflows/build.yaml/badge.svg)](https://github.com/codengine/SOTFEdit/actions/workflows/build.yaml) -[![GitHub release (latest by date)](https://img.shields.io/github/v/release/codengine/SOTFEdit)](https://github.com/codengine/SOTFEdit/releases) -[![GitHub all releases](https://img.shields.io/github/downloads/codengine/SOTFEdit/total)](https://github.com/codengine/SOTFEdit/releases) -![GitHub](https://img.shields.io/github/license/codengine/SOTFEdit) + +# Badges (may require workflow/release setup) +[![Build Status](https://github.com/Za-Pa-Al/SOTFEdit/actions/workflows/build.yaml/badge.svg)](https://github.com/Za-Pa-Al/SOTFEdit/actions/workflows/build.yaml) +[![Latest Release](https://img.shields.io/github/v/release/Za-Pa-Al/SOTFEdit?label=release)](https://github.com/Za-Pa-Al/SOTFEdit/releases) +[![Downloads](https://img.shields.io/github/downloads/Za-Pa-Al/SOTFEdit/total?label=downloads)](https://github.com/Za-Pa-Al/SOTFEdit/releases) +[![License](https://img.shields.io/github/license/Za-Pa-Al/SOTFEdit)](https://github.com/Za-Pa-Al/SOTFEdit/blob/master/LICENSE) A savegame editor for "Sons of The Forest". @@ -33,10 +39,18 @@ A savegame editor for "Sons of The Forest". - [Attributions](#attributions) - [Icons](#icons) +nothing more, nothing less. + +## About This Fork + +This is a fork of [codengine/SOTFEdit](https://github.com/codengine/SOTFEdit) maintained by [Za-Pa-Al](https://github.com/Za-Pa-Al). +It may include custom changes, fixes, or experimental features not present in the original project. + +**Original project credits and license apply.** + ## Disclaimer -This project is in no way or form associated with the developers of the game. It is just a non-commercial fan project, -nothing more, nothing less. +This project is in no way or form associated with the developers of the game. It is just a non-commercial fan project, nothing more, nothing less. ## Features @@ -68,14 +82,16 @@ nothing more, nothing less. - Backup changed files automatically - ... more features are planned + ## Download -- You can find the newest version at the [Releases page](https://github.com/codengine/SOTFEdit/releases) +- You can find the newest version at the [Releases page](https://github.com/Za-Pa-Al/SOTFEdit/releases) + ## Requirements -- Windows 8+ -- [.net 6.0 Desktop Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) +- Windows 10+ +- [.NET 8.0 Desktop Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) ## Usage @@ -145,17 +161,35 @@ Some positions are missing but they may be added in the future. - Teleport the Player and Followers to Actors/NPCs, the player, Zip Lines, Structures, Caves, Bunkers, Helicopters - Remove Zip Lines from the map - Spawn Actors/NPCs at target areas +- **Auto Reload**: Automatically reloads the savegame when changes are detected on disk, keeping the map synchronized with your game +- **Auto Connect**: Automatically connects to the companion app when available +- Multi-monitor support with window position memory ### Options You can enable or disable icons using the "Options" button in the top left corner. +**Auto Reload and Auto Connect** checkboxes are available at the top of the map window: +- **Auto Reload**: When enabled, the map will automatically detect when your savegame is modified and reload the data. If no savegame is loaded, it will automatically load the most recent one. +- **Auto Connect**: When enabled, the map will automatically attempt to connect to the companion app. + Some important notes regarding the filters: -- "Show only uncollected items" will show/hide uncollected items including bunkers and caves where they can be found + - "Hide completed" will hide all items and POIs that are either collected or marked as done, including those in bunkers and caves - Area - Mainly affects Actors or, in general terms, positions where we have the exact coordinates - Requirements - Show/Hide caves, bunkers and items which are accessible/inaccessible +### Marking POIs as Done + +Some points of interest (POIs) on the map can now be manually marked as "done" (completed) by the user. This allows you to keep track of which locations you have already visited or completed, even if the game does not automatically track this for you. + +- To mark a POI as done, use the context menu or the checkbox (where available) in the map interface. +- POIs that are marked as done will display a special overlay icon on the map. +- The "Hide completed" filter will hide all POIs that are either collected (for items) or manually marked as done. +- Not all POIs can be marked as done—only those with a real in-game identifier support this feature. + +This feature helps you organize your exploration and avoid revisiting locations you have already completed. + ### Teleportation You can only teleport to locations where we have the exact coordinates either from the savegame itself or attached to @@ -231,6 +265,7 @@ Be careful: This feature will most likely kill your performance and may corrupt Make sure to enable backups! ``` + ## Troubleshooting One of the items in inventory is listed as "Unknown"? @@ -239,8 +274,7 @@ One of the items in inventory is listed as "Unknown"? My game does not work anymore? -- If you have selected to create backups before saving, you can just delete the old files and restore the files that are - suffixed with ".bak*". +- If you have selected to create backups before saving, you can just delete the old files and restore the files that are suffixed with ".bak*". I get errors and the application does strange things @@ -252,30 +286,43 @@ I can not change "IsRobbyDead" or "IsVirginiaDead" The program does not start -- Make sure that .net 6.0 Desktop Runtime is installed. Also make sure to extract all files from the archive if you - downloaded the zip archive manually. Lastly, check if any antivirus is blocking the editor +- Make sure that .NET 8.0 Desktop Runtime is installed. Also make sure to extract all files from the archive if you downloaded the zip archive manually. Lastly, check if any antivirus is blocking the editor Antivirus (Windows Defender for example, Smartscreen) is complaining -- This is due to the fact that it is a self-developed application which is not signed. It's safe to just ignore the - warning. The code is all hosted on Github. +- This is due to the fact that it is a self-developed application which is not signed. It's safe to just ignore the warning. The code is all hosted on Github. "Could not load file or assembly" -- Make sure to have .net 6.0 Desktop Runtime installed (either x86 or x64) +- Make sure to have .NET 8.0 Desktop Runtime installed (either x86 or x64) My changes are not applied or reverted -- In some cases, the Cloud Saving Feature of Steam overwrites changes done by SOTFEdit. You can fix that if you start - the Game (not a game session!), edit a savegame and THEN start the game session. +- In some cases, the Cloud Saving Feature of Steam overwrites changes done by SOTFEdit. You can fix that if you start the Game (not a game session!), edit a savegame and THEN start the game session. Lakes, rivers etc. are gone - This happens after teleporting in and out of caves. This should be fixed when you teleport again in and out + ## Contributing -Feel free to report any unknown items or any feature requests. PRs are also welcome. +Feel free to report any unknown items or feature requests. Pull requests are welcome! + +If you want to contribute, please fork this repository ([Za-Pa-Al/SOTFEdit](https://github.com/Za-Pa-Al/SOTFEdit)), make your changes, and open a pull request. To keep your fork up to date with the original, add the original repo as an upstream remote: + +```sh +git remote add upstream https://github.com/codengine/SOTFEdit.git +``` + +Then fetch and merge as needed: + +```sh +git fetch upstream +git merge upstream/master +``` + +See the [GitHub documentation on forking](https://docs.github.com/en/get-started/quickstart/fork-a-repo) for more details. ## Final Words diff --git a/SOTFEdit.Companion.Shared.Tests/SOTFEdit.Companion.Shared.Tests.csproj b/SOTFEdit.Companion.Shared.Tests/SOTFEdit.Companion.Shared.Tests.csproj index 4b67eb1..e712069 100644 --- a/SOTFEdit.Companion.Shared.Tests/SOTFEdit.Companion.Shared.Tests.csproj +++ b/SOTFEdit.Companion.Shared.Tests/SOTFEdit.Companion.Shared.Tests.csproj @@ -3,7 +3,7 @@ enable enable - net6.0 + net8.0 diff --git a/SOTFEdit.Companion.Shared/Messages/CompanionAddPoiMessage.cs b/SOTFEdit.Companion.Shared/Messages/CompanionAddPoiMessage.cs index 63e785b..5e84adf 100644 --- a/SOTFEdit.Companion.Shared/Messages/CompanionAddPoiMessage.cs +++ b/SOTFEdit.Companion.Shared/Messages/CompanionAddPoiMessage.cs @@ -6,13 +6,13 @@ namespace SOTFEdit.Companion.Shared.Messages; public class CompanionAddPoiMessage : ICompanionMessage { [Key(0)] - public string Title { get; set; } + public string Title { get; set; } = string.Empty; [Key(1)] - public string Description { get; set; } + public string Description { get; set; } = string.Empty; [Key(2)] - public byte[] Screenshot { get; set; } + public byte[] Screenshot { get; set; } = Array.Empty(); [Key(3)] public float X { get; set; } diff --git a/SOTFEdit.Companion.Shared/Messages/CompanionPosCollectionMessage.cs b/SOTFEdit.Companion.Shared/Messages/CompanionPosCollectionMessage.cs index d841518..0132904 100644 --- a/SOTFEdit.Companion.Shared/Messages/CompanionPosCollectionMessage.cs +++ b/SOTFEdit.Companion.Shared/Messages/CompanionPosCollectionMessage.cs @@ -7,4 +7,9 @@ public class CompanionPosCollectionMessage : ICompanionMessage { [Key(0)] public List Positions { get; set; } + + public CompanionPosCollectionMessage() + { + Positions = new List(); + } } \ No newline at end of file diff --git a/SOTFEdit.Companion.Shared/SOTFEdit.Companion.Shared.csproj b/SOTFEdit.Companion.Shared/SOTFEdit.Companion.Shared.csproj index 980a1d5..d3841b1 100644 --- a/SOTFEdit.Companion.Shared/SOTFEdit.Companion.Shared.csproj +++ b/SOTFEdit.Companion.Shared/SOTFEdit.Companion.Shared.csproj @@ -3,13 +3,14 @@ enable enable - net6.0 + net8.0 Release;Debug AnyCPU + false - + diff --git a/SOTFEdit.code-workspace b/SOTFEdit.code-workspace new file mode 100644 index 0000000..9b8f614 --- /dev/null +++ b/SOTFEdit.code-workspace @@ -0,0 +1,11 @@ +{ + "folders": [ + { + "path": "." + }, + { + "path": "../../../../Tech/Development/LLM Library" + } + ], + "settings": {} +} \ No newline at end of file diff --git a/SOTFEdit/App.xaml.cs b/SOTFEdit/App.xaml.cs index 76a2ad7..d44bb65 100644 --- a/SOTFEdit/App.xaml.cs +++ b/SOTFEdit/App.xaml.cs @@ -32,8 +32,8 @@ namespace SOTFEdit; /// public partial class App { - public const string Version = "0.12.10"; - private static readonly ILogger Logger = LogManager.GetCurrentClassLogger(); + public const string Version = "1.2.1"; + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); public App() { diff --git a/SOTFEdit/Infrastructure/Converters/CoordinateConverter.cs b/SOTFEdit/Infrastructure/Converters/CoordinateConverter.cs index e63bf40..32cc214 100644 --- a/SOTFEdit/Infrastructure/Converters/CoordinateConverter.cs +++ b/SOTFEdit/Infrastructure/Converters/CoordinateConverter.cs @@ -13,19 +13,19 @@ public static class CoordinateConverter static CoordinateConverter() { // Input data: pixel coordinates and corresponding in-game coordinates - float[] pixelXs = { 1311, 1592.5f, 1426, 1037, 688, 772.3f, 709.0f, 786.2f, 789.8f, 899.5f, 1259.8f }; - float[] pixelYs = { 2064, 2155, 1494.5f, 1425, 1407, 1486.3f, 1713.7f, 1867.3f, 2020.1f, 2125.4f, 1822.8f }; + float[] pixelXs = [1311, 1592.5f, 1426, 1037, 688, 772.3f, 709.0f, 786.2f, 789.8f, 899.5f, 1259.8f]; + float[] pixelYs = [2064, 2155, 1494.5f, 1425, 1407, 1486.3f, 1713.7f, 1867.3f, 2020.1f, 2125.4f, 1822.8f]; float[] ingameXs = - { + [ -719.0499f, -444.96793f, -604.09875f, -989.2298f, -1328.8882f, -1245.4705f, -1308.0143f, -1232.5912f, -1226.8536f, -1112.021f, -770.1515f - }; + ]; float[] ingameYs = - { + [ -17.139717f, -103.88483f, 541.12946f, 611.64136f, 625.7892f, 549.34155f, 326.0083f, 176.03516f, 27.191362f, -74.49177f, 219.75914f - }; + ]; // Calculate the inverse scale and offset for the x-axis var inverseScaleXandOffsetX = CalculateScaleAndOffset(ingameXs, pixelXs); diff --git a/SOTFEdit/Infrastructure/FlatExtractingYamlLocalizationSource.cs b/SOTFEdit/Infrastructure/FlatExtractingYamlLocalizationSource.cs new file mode 100644 index 0000000..57dad77 --- /dev/null +++ b/SOTFEdit/Infrastructure/FlatExtractingYamlLocalizationSource.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using libc.translation; +using YamlDotNet.Serialization; + +namespace SOTFEdit.Infrastructure; + +/// +/// YAML-based localization source that flattens nested YAML structures into dot-notation keys. +/// Compatible with the existing FlatExtractingJsonLocalizationSource behavior. +/// +public class FlatExtractingYamlLocalizationSource : ILocalizationSource +{ + private readonly string _basePath; + private readonly PropertyCaseSensitivity _caseSensitivity; + private readonly IDeserializer _yamlDeserializer; + private readonly Dictionary> _translationCache; + + public FlatExtractingYamlLocalizationSource(string basePath, PropertyCaseSensitivity caseSensitivity) + { + _basePath = basePath ?? throw new ArgumentNullException(nameof(basePath)); + _caseSensitivity = caseSensitivity; + _translationCache = new Dictionary>(); + + _yamlDeserializer = new DeserializerBuilder() + .Build(); + } + + public Dictionary GetTranslations(CultureInfo culture) + { + // Check cache first + var cultureName = culture.Name; + if (_translationCache.TryGetValue(cultureName, out var cachedTranslations)) + { + return cachedTranslations; + } + + var yamlFilePath = Path.Combine(_basePath, $"{cultureName}.yaml"); + + if (!File.Exists(yamlFilePath)) + { + return new Dictionary(); + } + + try + { + var yamlContent = File.ReadAllText(yamlFilePath); + var data = _yamlDeserializer.Deserialize>(yamlContent); + + if (data == null) + { + return new Dictionary(); + } + + var flatTranslations = new Dictionary( + _caseSensitivity == PropertyCaseSensitivity.CaseInsensitive + ? StringComparer.OrdinalIgnoreCase + : StringComparer.Ordinal + ); + + FlattenDictionary(data, string.Empty, flatTranslations); + + // Cache the translations + _translationCache[cultureName] = flatTranslations; + + return flatTranslations; + } + catch (Exception ex) + { + throw new InvalidOperationException( + $"Failed to load YAML translations from '{yamlFilePath}'", ex); + } + } + + private void FlattenDictionary(Dictionary source, string prefix, Dictionary target) + { + foreach (var kvp in source) + { + var key = kvp.Key?.ToString(); + if (key == null) continue; + + var fullKey = string.IsNullOrEmpty(prefix) ? key : $"{prefix}.{key}"; + + if (kvp.Value is Dictionary nestedDict) + { + FlattenDictionary(nestedDict, fullKey, target); + } + else if (kvp.Value != null) + { + var value = kvp.Value.ToString(); + if (value != null) + { + target[fullKey] = value; + } + } + } + } + + public IEnumerable GetAvailableCultures() + { + if (!Directory.Exists(_basePath)) + { + return Enumerable.Empty(); + } + + return Directory.GetFiles(_basePath, "*.yaml") + .Select(file => Path.GetFileNameWithoutExtension(file)) + .Where(cultureName => !string.IsNullOrEmpty(cultureName)) + .Select(cultureName => + { + try + { + return new CultureInfo(cultureName); + } + catch + { + return null; + } + }) + .Where(culture => culture != null) + .Cast() + .ToList(); + } + + public string Get(string culture, string key) + { + try + { + var cultureInfo = new CultureInfo(culture); + var translations = GetTranslations(cultureInfo); + return translations.TryGetValue(key, out var value) ? value : key; + } + catch + { + return key; + } + } + + public IDictionary GetAll(string culture) + { + try + { + var cultureInfo = new CultureInfo(culture); + return GetTranslations(cultureInfo); + } + catch + { + return new Dictionary(); + } + } +} diff --git a/SOTFEdit/Infrastructure/LanguageManager.cs b/SOTFEdit/Infrastructure/LanguageManager.cs index 975d549..b31b036 100644 --- a/SOTFEdit/Infrastructure/LanguageManager.cs +++ b/SOTFEdit/Infrastructure/LanguageManager.cs @@ -12,7 +12,7 @@ public static class LanguageManager public static IEnumerable GetAvailableCultures() { - return new DirectoryInfo(LangPath).GetFiles("*.json") + return new DirectoryInfo(LangPath).GetFiles("*.yaml") .Select(fileInfo => Path.GetFileNameWithoutExtension(fileInfo.FullName)) .ToList(); } @@ -20,7 +20,7 @@ public static IEnumerable GetAvailableCultures() public static ILocalizer BuildLocalizer() { ILocalizationSource source = - new FlatExtractingJsonLocalizationSource(LangPath, PropertyCaseSensitivity.CaseSensitive); + new FlatExtractingYamlLocalizationSource(LangPath, PropertyCaseSensitivity.CaseSensitive); return new Localizer(source); } } \ No newline at end of file diff --git a/SOTFEdit/Infrastructure/NaturalStringComparator.cs b/SOTFEdit/Infrastructure/NaturalStringComparator.cs index e1bac35..61b2e36 100644 --- a/SOTFEdit/Infrastructure/NaturalStringComparator.cs +++ b/SOTFEdit/Infrastructure/NaturalStringComparator.cs @@ -22,8 +22,11 @@ public sealed class NaturalStringComparer : IComparer /// Zero: x equals y. /// Greater than zero: x is greater than y. /// - public int Compare(string x, string y) + public int Compare(string? x, string? y) { + if (ReferenceEquals(x, y)) return 0; + if (x is null) return y is null ? 0 : -1; + if (y is null) return 1; var indexX = 0; var indexY = 0; while (true) diff --git a/SOTFEdit/Infrastructure/TranslateExtension.cs b/SOTFEdit/Infrastructure/TranslateExtension.cs index 0968284..6afc870 100644 --- a/SOTFEdit/Infrastructure/TranslateExtension.cs +++ b/SOTFEdit/Infrastructure/TranslateExtension.cs @@ -1,5 +1,10 @@ using System; +using System.ComponentModel; +using System.Windows; +using System.Windows.Data; using System.Windows.Markup; +using CommunityToolkit.Mvvm.Messaging; +using SOTFEdit.Model.Events; namespace SOTFEdit.Infrastructure; @@ -14,6 +19,48 @@ public TranslateExtension(string key) public override object ProvideValue(IServiceProvider serviceProvider) { + // Check if we're being used in a context that supports bindings + if (serviceProvider.GetService(typeof(IProvideValueTarget)) is IProvideValueTarget target) + { + // If the target property is not a DependencyProperty, return the static string + // This handles cases like FallbackValue, TargetNullValue, etc. + if (target.TargetProperty is not System.Windows.DependencyProperty) + { + return TranslationManager.Get(_key); + } + + // Create a binding to a TranslationProxy that will update when language changes + var proxy = new TranslationProxy(_key); + var binding = new Binding(nameof(TranslationProxy.Value)) + { + Source = proxy, + Mode = BindingMode.OneWay + }; + + return binding.ProvideValue(serviceProvider); + } + return TranslationManager.Get(_key); } + + private class TranslationProxy : INotifyPropertyChanged + { + private readonly string _key; + + public TranslationProxy(string key) + { + _key = key; + WeakReferenceMessenger.Default.Register(this, (_, _) => + { + Application.Current?.Dispatcher.Invoke(() => + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value))); + }); + }); + } + + public string Value => TranslationManager.Get(_key); + + public event PropertyChangedEventHandler? PropertyChanged; + } } \ No newline at end of file diff --git a/SOTFEdit/Infrastructure/TranslationManager.cs b/SOTFEdit/Infrastructure/TranslationManager.cs index 98a7848..d851137 100644 --- a/SOTFEdit/Infrastructure/TranslationManager.cs +++ b/SOTFEdit/Infrastructure/TranslationManager.cs @@ -1,6 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Globalization; +using CommunityToolkit.Mvvm.Messaging; using JetBrains.Annotations; using libc.translation; +using SOTFEdit.Model.Events; namespace SOTFEdit.Infrastructure; @@ -8,6 +12,8 @@ public static class TranslationManager { private static readonly ILocalizer Localizer = LanguageManager.BuildLocalizer(); + public static event EventHandler? LanguageChanged; + [ContractAnnotation("fallback:null => null")] public static string Get(string key, string? fallback = null, bool fallbackIsTranslationKey = true) { @@ -39,4 +45,16 @@ public static IDictionary GetAll(string culture) { return Localizer.GetAll(culture); } + + public static void ChangeCulture(string culture) + { + var cultureInfo = new CultureInfo(culture); + CultureInfo.CurrentCulture = cultureInfo; + CultureInfo.CurrentUICulture = cultureInfo; + CultureInfo.DefaultThreadCurrentCulture = cultureInfo; + CultureInfo.DefaultThreadCurrentUICulture = cultureInfo; + + LanguageChanged?.Invoke(null, EventArgs.Empty); + WeakReferenceMessenger.Default.Send(new LanguageChangedEvent()); + } } \ No newline at end of file diff --git a/SOTFEdit/Model/Actors/ActorModifier.cs b/SOTFEdit/Model/Actors/ActorModifier.cs index 6336bd8..e8613b4 100644 --- a/SOTFEdit/Model/Actors/ActorModifier.cs +++ b/SOTFEdit/Model/Actors/ActorModifier.cs @@ -172,8 +172,9 @@ private void Modify(JToken vailWorldSim, List matchedActors, UpdateActor if (data.ModifyOptions.TeleportMode == "PlayerToNpc") { - var actorPos = data.Actor.Position; - var playerPos = _playerPageViewModel.PlayerState.Pos; + + var actorPos = data.Actor.Position!; + var playerPos = _playerPageViewModel.PlayerState.Pos!; Teleporter.MovePlayerToPos(ref playerPos, ref actorPos); if (actorTokenForActorInData != null) diff --git a/SOTFEdit/Model/Actors/FollowerModifier.cs b/SOTFEdit/Model/Actors/FollowerModifier.cs index 5cc3fec..665c75f 100644 --- a/SOTFEdit/Model/Actors/FollowerModifier.cs +++ b/SOTFEdit/Model/Actors/FollowerModifier.cs @@ -451,7 +451,8 @@ private static bool HasDifferencesInMemories(JArray existingInfluences, return true; } - var newInfluencesByType = newInfluences.DistinctBy(influence => influence.TypeId) + var newInfluencesByType = newInfluences + .DistinctBy(influence => influence.TypeId) .ToDictionary(influence => influence.TypeId); foreach (var existingInfluence in existingInfluences) diff --git a/SOTFEdit/Model/Events/InventoryReloadedEvent.cs b/SOTFEdit/Model/Events/InventoryReloadedEvent.cs new file mode 100644 index 0000000..0c8c5d3 --- /dev/null +++ b/SOTFEdit/Model/Events/InventoryReloadedEvent.cs @@ -0,0 +1,5 @@ +namespace SOTFEdit.Model.Events; + +public class InventoryReloadedEvent +{ +} diff --git a/SOTFEdit/Model/Events/LanguageChangedEvent.cs b/SOTFEdit/Model/Events/LanguageChangedEvent.cs new file mode 100644 index 0000000..38aacbf --- /dev/null +++ b/SOTFEdit/Model/Events/LanguageChangedEvent.cs @@ -0,0 +1,5 @@ +namespace SOTFEdit.Model.Events; + +public class LanguageChangedEvent +{ +} diff --git a/SOTFEdit/Model/Events/StructuresReloadedEvent.cs b/SOTFEdit/Model/Events/StructuresReloadedEvent.cs new file mode 100644 index 0000000..19558be --- /dev/null +++ b/SOTFEdit/Model/Events/StructuresReloadedEvent.cs @@ -0,0 +1,5 @@ +namespace SOTFEdit.Model.Events; + +public class StructuresReloadedEvent +{ +} diff --git a/SOTFEdit/Model/Item.cs b/SOTFEdit/Model/Item.cs index fd10cbf..81b0744 100644 --- a/SOTFEdit/Model/Item.cs +++ b/SOTFEdit/Model/Item.cs @@ -13,7 +13,7 @@ public class Item : ICloneable private string? _normalizedLowercaseType; public int Id { get; init; } public string Name => TranslationManager.Get("items." + Id); - public string Type { get; init; } + public string Type { get; init; } = string.Empty; public FoodSpoilModuleDefinition? FoodSpoilModuleDefinition { get; init; } public SourceActorModuleDefinition? SourceActorModuleDefinition { get; init; } public bool IsInventoryItem { get; init; } = true; @@ -37,7 +37,7 @@ public string NormalizedLowercaseName private string NormalizedLowercaseType { - get { return _normalizedLowercaseType ??= TranslationHelper.Normalize(Type).ToLower(); } + get { return _normalizedLowercaseType ??= TranslationHelper.Normalize(Type).ToLower(); } } public object Clone() diff --git a/SOTFEdit/Model/Map/ActorPoi.cs b/SOTFEdit/Model/Map/ActorPoi.cs index 7c495eb..1a2d31a 100644 --- a/SOTFEdit/Model/Map/ActorPoi.cs +++ b/SOTFEdit/Model/Map/ActorPoi.cs @@ -11,7 +11,7 @@ public partial class ActorPoi : BasePoi { private readonly bool _isFollower; - public ActorPoi(Actor actor) : base(actor.Position) + public ActorPoi(Actor actor) : base(actor.Position!) { Actor = actor; if (actor.TypeId is not (Constants.Actors.KelvinTypeId or Constants.Actors.VirginiaTypeId)) diff --git a/SOTFEdit/Model/Map/AreaFilter.cs b/SOTFEdit/Model/Map/AreaFilter.cs index 4e8f716..4551796 100644 --- a/SOTFEdit/Model/Map/AreaFilter.cs +++ b/SOTFEdit/Model/Map/AreaFilter.cs @@ -5,13 +5,13 @@ namespace SOTFEdit.Model.Map; public class AreaFilter : IAreaFilter { public static readonly IAreaFilter All = - new StaticAreaFilter(TranslationManager.Get("map.areaFilter.types.all"), _ => true); + new StaticAreaFilter("map.areaFilter.types.all", _ => true); public static readonly IAreaFilter Surface = - new StaticAreaFilter(TranslationManager.Get("map.areaFilter.types.surfaceOnly"), poi => !poi.IsUnderground); + new StaticAreaFilter("map.areaFilter.types.surfaceOnly", poi => !poi.IsUnderground); public static readonly IAreaFilter CavesOrBunkers = - new StaticAreaFilter(TranslationManager.Get("map.areaFilter.types.undergroundOnly"), poi => poi.IsUnderground); + new StaticAreaFilter("map.areaFilter.types.undergroundOnly", poi => poi.IsUnderground); private readonly Area _area; @@ -31,4 +31,9 @@ public bool ShouldInclude(IPoi poi) } public string Name => _area.Name; + + public override string ToString() + { + return Name; + } } \ No newline at end of file diff --git a/SOTFEdit/Model/Map/BasePoi.cs b/SOTFEdit/Model/Map/BasePoi.cs index 3922c60..86faa54 100644 --- a/SOTFEdit/Model/Map/BasePoi.cs +++ b/SOTFEdit/Model/Map/BasePoi.cs @@ -1,4 +1,6 @@ -using System.Windows.Media.Imaging; +using System; +using System.Windows.Media.Imaging; +using System.Linq; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; @@ -11,6 +13,76 @@ namespace SOTFEdit.Model.Map; public abstract partial class BasePoi : ObservableObject, IPoi { + public virtual int Id { get; init; } = -1; + public bool HasValidId => Id != -1; + + private static readonly char[] DonePoiSeparator = [',']; + + public bool IsDone + { + get + { + var ids = (Settings.Default.DonePoiIdListString ?? "") + .Split(DonePoiSeparator, StringSplitOptions.RemoveEmptyEntries) + .Select(s => int.TryParse(s, out var id) ? (int?)id : null) + .Where(id => id.HasValue) + .Select(id => id.GetValueOrDefault()) + .ToList(); + return ids.Contains(Id); + } + set + { + var idList = (Settings.Default.DonePoiIdListString ?? "") + .Split(DonePoiSeparator, StringSplitOptions.RemoveEmptyEntries) + .Select(s => int.TryParse(s, out var id) ? (int?)id : null) + .Where(id => id.HasValue) + .Select(id => id.GetValueOrDefault()) + .ToList(); + + if (value) + { + if (!idList.Contains(Id)) + { + idList.Add(Id); + Settings.Default.DonePoiIdListString = string.Join(",", idList); + Settings.Default.Save(); + } + } + else + { + if (idList.Contains(Id)) + { + idList.Remove(Id); + Settings.Default.DonePoiIdListString = string.Join(",", idList); + Settings.Default.Save(); + } + } + + OnPropertyChanged(nameof(IsDone)); + OnPropertyChanged(nameof(DoneButtonText)); + + // Re-apply filter if hiding should be triggered by setting done + var mainWindow = System.Windows.Application.Current?.MainWindow; + if (mainWindow?.DataContext is SOTFEdit.ViewModel.MapViewModel mapViewModel && mapViewModel.MapFilter != null) + { + ApplyFilter(mapViewModel.MapFilter); + } + } + } + + public string DoneButtonText => IsDone ? "Mark as Undone" : "Mark as Done"; + + [RelayCommand] + private void ToggleDone() + { + IsDone = !IsDone; + } + + public void RefreshDoneStatus() + { + OnPropertyChanged(nameof(IsDone)); + OnPropertyChanged(nameof(DoneButtonText)); + } [NotifyPropertyChangedFor(nameof(Visible))] [ObservableProperty] private bool _enabled; diff --git a/SOTFEdit/Model/Map/CaveOrBunkerPoi.cs b/SOTFEdit/Model/Map/CaveOrBunkerPoi.cs index 0fe9315..ba9f52d 100644 --- a/SOTFEdit/Model/Map/CaveOrBunkerPoi.cs +++ b/SOTFEdit/Model/Map/CaveOrBunkerPoi.cs @@ -6,17 +6,17 @@ namespace SOTFEdit.Model.Map; public class CaveOrBunkerPoi : DefaultGenericInformationalPoi, IPoiWithItems { - private readonly HashSet _inventoryItems; + private HashSet _inventoryItems; private readonly IEnumerable? _items; private readonly IEnumerable? _objects; - private CaveOrBunkerPoi(float x, float y, Position? teleport, string title, string? description, + private CaveOrBunkerPoi(int id, float x, float y, Position? teleport, string title, string? description, string? screenshot, string icon, IEnumerable? requirements, IEnumerable? items, HashSet inventoryItems, IEnumerable? objects, bool isUnderground = false, string? wikiLink = null) : - base(x, y, teleport, title, description, screenshot, icon, requirements, isUnderground, wikiLink) + base(id, x, y, teleport, title, description, screenshot, icon, requirements, isUnderground, wikiLink) { _items = items; _objects = objects; @@ -43,7 +43,11 @@ public override void ApplyFilter(MapFilter mapFilter) protected override bool ShouldFilter(MapFilter mapFilter) { - return (mapFilter.ShowOnlyUncollectedItems && HasAllItemsInInventory()) || base.ShouldFilter(mapFilter); + if (mapFilter.HideCompleted && (HasAllItemsInInventory() || IsDone)) + { + return true; + } + return base.ShouldFilter(mapFilter); } protected override bool FullTextFilter(string normalizedLowercaseFullText) @@ -61,10 +65,17 @@ private bool HasAllItemsInInventory() return _items?.All(HasItemInInventory) ?? true; } + public void RefreshInventory(HashSet inventoryItems) + { + _inventoryItems = inventoryItems; + OnPropertyChanged(nameof(Items)); + } + public new static CaveOrBunkerPoi Of(RawPoi rawPoi, ItemList itemList, string icon, HashSet inventoryItems, AreaMaskManager areaMaskManager, bool enabled) { var poi = new CaveOrBunkerPoi( + rawPoi.Id, rawPoi.X, rawPoi.Y, rawPoi.Teleport?.ToPosition(areaMaskManager), diff --git a/SOTFEdit/Model/Map/CustomMapPoi.cs b/SOTFEdit/Model/Map/CustomMapPoi.cs index 716d9e7..904b782 100644 --- a/SOTFEdit/Model/Map/CustomMapPoi.cs +++ b/SOTFEdit/Model/Map/CustomMapPoi.cs @@ -14,14 +14,14 @@ public partial class CustomMapPoi : DefaultGenericInformationalPoi private readonly string _screenshotDirectory; private CustomMapPoi(int id, Position teleport, string title, string? description, string screenshotDirectory, - string? screenshot) : base(teleport.X, teleport.Z, teleport, title, description, screenshot, IconFile, null, + string? screenshot) : base(id, teleport.X, teleport.Z, teleport, title, description, screenshot, IconFile, null, teleport.Area.IsUnderground()) { _screenshotDirectory = screenshotDirectory; Id = id; } - public int Id { get; } + public override int Id { get; init; } public static BitmapImage CategoryIcon => LoadBaseIcon(IconFile, 24, 24); diff --git a/SOTFEdit/Model/Map/DefaultGenericInformationalPoi.cs b/SOTFEdit/Model/Map/DefaultGenericInformationalPoi.cs index faf71de..f1ad443 100644 --- a/SOTFEdit/Model/Map/DefaultGenericInformationalPoi.cs +++ b/SOTFEdit/Model/Map/DefaultGenericInformationalPoi.cs @@ -6,9 +6,10 @@ namespace SOTFEdit.Model.Map; public class DefaultGenericInformationalPoi : InformationalPoi { + public override int Id { get; init; } private readonly string _icon; - protected DefaultGenericInformationalPoi(float x, float y, Position? teleport, string title, string? description, + protected DefaultGenericInformationalPoi(int id, float x, float y, Position? teleport, string title, string? description, string? screenshot, string icon, IEnumerable? requirements, bool isUnderground = false, string? wikiLink = null) : base(x, y, teleport, @@ -17,6 +18,7 @@ protected DefaultGenericInformationalPoi(float x, float y, Position? teleport, s requirements, screenshot, isUnderground, wikiLink) { + Id = id; _icon = icon; } @@ -27,6 +29,7 @@ public static DefaultGenericInformationalPoi Of(RawPoi rawPoi, ItemList itemList HashSet inventoryItems, AreaMaskManager areaMaskManager, bool enabled) { var poi = new DefaultGenericInformationalPoi( + rawPoi.Id, rawPoi.X, rawPoi.Y, rawPoi.Teleport?.ToPosition(areaMaskManager), @@ -45,4 +48,13 @@ public static DefaultGenericInformationalPoi Of(RawPoi rawPoi, ItemList itemList return poi; } + + protected override bool ShouldFilter(MapFilter mapFilter) + { + if (mapFilter.HideCompleted && IsDone) + { + return true; + } + return base.ShouldFilter(mapFilter); + } } \ No newline at end of file diff --git a/SOTFEdit/Model/Map/ItemPoi.cs b/SOTFEdit/Model/Map/ItemPoi.cs index 88d341b..978af17 100644 --- a/SOTFEdit/Model/Map/ItemPoi.cs +++ b/SOTFEdit/Model/Map/ItemPoi.cs @@ -8,6 +8,7 @@ namespace SOTFEdit.Model.Map; public class ItemPoi : InformationalPoi { + public override int Id { get; init; } private readonly IEnumerable? _altItems; private readonly HashSet _inventoryItems; @@ -73,7 +74,12 @@ public override void ApplyFilter(MapFilter mapFilter) protected override bool ShouldFilter(MapFilter mapFilter) { - return (mapFilter.ShowOnlyUncollectedItems && HasAllItemsInInventory()) || base.ShouldFilter(mapFilter); + // Hide if done and HideCompleted is set + if (mapFilter.HideCompleted && (HasAllItemsInInventory() || IsDone)) + { + return true; + } + return base.ShouldFilter(mapFilter); } protected override bool FullTextFilter(string normalizedLowercaseFullText) @@ -90,7 +96,20 @@ private bool AnyAltItemContains(string normalizedLowercaseFullText) private bool HasAllItemsInInventory() { - return HasItemInInventory(_item) && (_altItems?.All(HasItemInInventory) ?? true); + // Check if the main item is in inventory + if (!HasItemInInventory(_item)) + { + return false; + } + + // If there are no alternative items, return true + if (_altItems == null) + { + return true; + } + + // All alternative items must be in inventory + return _altItems.All(HasItemInInventory); } private bool HasItemInInventory(Item item) diff --git a/SOTFEdit/Model/Map/MapFilter.cs b/SOTFEdit/Model/Map/MapFilter.cs index 81ccba3..9c4d04e 100644 --- a/SOTFEdit/Model/Map/MapFilter.cs +++ b/SOTFEdit/Model/Map/MapFilter.cs @@ -2,7 +2,9 @@ using System.Collections.Generic; using System.Linq; using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Messaging; using SOTFEdit.Infrastructure; +using SOTFEdit.Model.Events; namespace SOTFEdit.Model.Map; @@ -25,32 +27,74 @@ public enum RequirementsFilterType private RequirementsFilterType _requirementsFilter = RequirementsFilterType.All; [ObservableProperty] - private bool _showOnlyUncollectedItems; + private bool _hideCompleted = Settings.Default.MapFilterHideCompleted; public string? NormalizedLowercaseFullText; public MapFilter(AreaMaskManager areaManager) { var allAreas = areaManager.GetAllAreas(); - AreaFilterTypeValues = new List - { - Map.AreaFilter.All, Map.AreaFilter.Surface, Map.AreaFilter.CavesOrBunkers - }; - - AreaFilterTypeValues.AddRange(allAreas.Where(area => !area.IsSurface()).OrderBy(area => area.Name) - .Select(area => new AreaFilter(area))); + AreaFilterTypeValues = + [ + Map.AreaFilter.All, Map.AreaFilter.Surface, Map.AreaFilter.CavesOrBunkers, + .. allAreas.Where(area => !area.IsSurface()).OrderBy(area => area.Name) + .Select(area => new AreaFilter(area)), + ]; RequirementsFilterTypeValues = Enum.GetValues(typeof(RequirementsFilterType)).Cast() .Select(v => new ComboBoxItemAndValue( TranslationManager.Get($"map.requirementsFilter.types.{v}"), v)); + + // Load persisted filter values + var areaFilterSetting = Settings.Default.MapFilterAreaFilter; + if (!string.IsNullOrWhiteSpace(areaFilterSetting)) + { + var match = AreaFilterTypeValues.FirstOrDefault(a => a.ToString() == areaFilterSetting); + if (match != null) + _areaFilter = match; + } + _fullText = Settings.Default.MapFilterFullText; + if (Enum.TryParse( + Settings.Default.MapFilterRequirementsFilter, out RequirementsFilterType req)) + _requirementsFilter = req; + _hideCompleted = Settings.Default.MapFilterHideCompleted; + + // Listen for language changes to refresh filter names + WeakReferenceMessenger.Default.Register(this, (_, _) => OnLanguageChanged()); + } + + private void OnLanguageChanged() + { + // Notify that AreaFilterTypeValues may have changed (StaticAreaFilter.Name is now dynamic) + OnPropertyChanged(nameof(AreaFilterTypeValues)); } public List AreaFilterTypeValues { get; } public IEnumerable> RequirementsFilterTypeValues { get; } + partial void OnAreaFilterChanged(IAreaFilter value) + { + Settings.Default.MapFilterAreaFilter = value?.ToString(); + Settings.Default.Save(); + } + partial void OnFullTextChanged(string? value) { NormalizedLowercaseFullText = value != null ? TranslationHelper.Normalize(value).ToLower() : null; + Settings.Default.MapFilterFullText = value; + Settings.Default.Save(); + } + + partial void OnRequirementsFilterChanged(RequirementsFilterType value) + { + Settings.Default.MapFilterRequirementsFilter = value.ToString(); + Settings.Default.Save(); + } + + partial void OnHideCompletedChanged(bool value) + { + Settings.Default.MapFilterHideCompleted = value; + Settings.Default.Save(); } } \ No newline at end of file diff --git a/SOTFEdit/Model/Map/Static/PoiLoader.cs b/SOTFEdit/Model/Map/Static/PoiLoader.cs index a168ebc..894747f 100644 --- a/SOTFEdit/Model/Map/Static/PoiLoader.cs +++ b/SOTFEdit/Model/Map/Static/PoiLoader.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using Newtonsoft.Json.Linq; +using NLog; using SOTFEdit.Companion.Shared; using SOTFEdit.Infrastructure; using SOTFEdit.Infrastructure.Companion; @@ -13,6 +14,8 @@ namespace SOTFEdit.Model.Map.Static; public class PoiLoader { + private static readonly ILogger Logger = LogManager.GetCurrentClassLogger(); + private readonly AreaMaskManager _areaMaskManager; private readonly CompanionPoiStorage _companionPoiStorage; private readonly InventoryPageViewModel _inventoryPageViewModel; @@ -54,10 +57,18 @@ public IEnumerable Load() }; } + public HashSet GetInventoryItemsPublic() + { + return GetInventoryItems(); + } + private HashSet GetInventoryItems() { var itemsWithHashes = _items.GetItemsWithHashes(); + var collectionCount = _inventoryPageViewModel.InventoryCollectionView.Cast().Count(); + Logger.Debug($"PoiLoader: GetInventoryItems - InventoryCollectionView has {collectionCount} items"); + var inventoryItems = _inventoryPageViewModel.InventoryCollectionView.OfType() .SelectMany(item => { @@ -73,6 +84,7 @@ private HashSet GetInventoryItems() inventoryItems.Add(selectedCloth.Id); } + Logger.Debug($"PoiLoader: GetInventoryItems - Returning {inventoryItems.Count} inventory item IDs"); return inventoryItems; } @@ -149,11 +161,11 @@ private IPoiGrouper LoadItemPois(HashSet inventoryItems, bool filterForComp var poisByType = rawPoiCollection.Items.SelectMany(kvp => kvp.Value.Pois.Select(poi => ItemPoi.Of(kvp.Key, kvp.Value, poi, _items, inventoryItems, _areaMaskManager, false))) - .Where(poi => poi != null) + .Where(poi => poi != null && !string.IsNullOrEmpty(poi.Item.Item.Type)) .Select(poi => poi!) .Where(poi => - !filterForCompanion || rawPoiCollection.AllowedGroupsForCompanion.Contains(poi.Item.Item.Type)) - .GroupBy(poi => poi.Item.Item.Type) + !filterForCompanion || rawPoiCollection.AllowedGroupsForCompanion.Contains(poi.Item.Item.Type!)) + .GroupBy(poi => poi.Item.Item.Type!) .OrderBy(g => g.Key) .ToDictionary(g => g.Key, g => g.ToList()); diff --git a/SOTFEdit/Model/Map/Static/RawPoi.cs b/SOTFEdit/Model/Map/Static/RawPoi.cs index c9c595e..e9baa80 100644 --- a/SOTFEdit/Model/Map/Static/RawPoi.cs +++ b/SOTFEdit/Model/Map/Static/RawPoi.cs @@ -4,7 +4,7 @@ namespace SOTFEdit.Model.Map.Static; // ReSharper disable once ClassNeverInstantiated.Global -public record RawPoi(string? Title, string? Description, float X, float Y, int[]? Requirements, +public record RawPoi(int Id, string? Title, string? Description, float X, float Y, int[]? Requirements, int[]? Items, string[]? Objects, string? Screenshot, bool IsUnderground = false, string? Wiki = null, Teleport? Teleport = null) { diff --git a/SOTFEdit/Model/Map/StaticAreaFilter.cs b/SOTFEdit/Model/Map/StaticAreaFilter.cs index 312d7dc..29864b7 100644 --- a/SOTFEdit/Model/Map/StaticAreaFilter.cs +++ b/SOTFEdit/Model/Map/StaticAreaFilter.cs @@ -1,21 +1,28 @@ using System; +using SOTFEdit.Infrastructure; namespace SOTFEdit.Model.Map; public class StaticAreaFilter : IAreaFilter { private readonly Predicate _predicate; + private readonly string _translationKey; - public StaticAreaFilter(string name, Predicate predicate) + public StaticAreaFilter(string translationKey, Predicate predicate) { _predicate = predicate; - Name = name; + _translationKey = translationKey; } - public string Name { get; } + public string Name => TranslationManager.Get(_translationKey); public bool ShouldInclude(IPoi poi) { return _predicate.Invoke(poi); } + + public override string ToString() + { + return Name; + } } \ No newline at end of file diff --git a/SOTFEdit/Model/Map/StructurePoi.cs b/SOTFEdit/Model/Map/StructurePoi.cs index 3715715..72c0a26 100644 --- a/SOTFEdit/Model/Map/StructurePoi.cs +++ b/SOTFEdit/Model/Map/StructurePoi.cs @@ -29,6 +29,20 @@ public override void ApplyFilter(MapFilter mapFilter) protected override bool ShouldFilter(MapFilter mapFilter) { + var hideCompleted = mapFilter.HideCompleted; + var isDone = IsDone; + + if (hideCompleted && isDone) + { + NLog.LogManager.GetCurrentClassLogger().Debug($"StructurePoi.ShouldFilter: Hiding {Title} (ID={Id}), HideCompleted={hideCompleted}, IsDone={isDone}"); + return true; + } + + if (hideCompleted && !isDone) + { + NLog.LogManager.GetCurrentClassLogger().Trace($"StructurePoi.ShouldFilter: NOT hiding {Title} (ID={Id}), HideCompleted={hideCompleted}, IsDone={isDone}"); + } + return mapFilter.RequirementsFilter == MapFilter.RequirementsFilterType.InaccessibleOnly || base.ShouldFilter(mapFilter); } diff --git a/SOTFEdit/Model/Map/WorldItemPoi.cs b/SOTFEdit/Model/Map/WorldItemPoi.cs index 6977280..85b83a2 100644 --- a/SOTFEdit/Model/Map/WorldItemPoi.cs +++ b/SOTFEdit/Model/Map/WorldItemPoi.cs @@ -37,6 +37,10 @@ public override void ApplyFilter(MapFilter mapFilter) protected override bool ShouldFilter(MapFilter mapFilter) { + if (mapFilter.HideCompleted && IsDone) + { + return true; + } return mapFilter.RequirementsFilter == MapFilter.RequirementsFilterType.InaccessibleOnly || base.ShouldFilter(mapFilter); } diff --git a/SOTFEdit/Model/Position.cs b/SOTFEdit/Model/Position.cs index cc6ff9c..7199a3e 100644 --- a/SOTFEdit/Model/Position.cs +++ b/SOTFEdit/Model/Position.cs @@ -119,8 +119,8 @@ private List> DistributeCoordinatesInGrid(int count, int spa var currentLevel = 0; var currentDirection = 0; - float[] directionX = { 1, 0, -1, 0 }; - float[] directionY = { 0, -1, 0, 1 }; + float[] directionX = [1, 0, -1, 0]; + float[] directionY = [0, -1, 0, 1]; var curX = X; var curZ = Z; diff --git a/SOTFEdit/Model/SaveData/Actor/Influence.cs b/SOTFEdit/Model/SaveData/Actor/Influence.cs index 3fe3de9..2b30a05 100644 --- a/SOTFEdit/Model/SaveData/Actor/Influence.cs +++ b/SOTFEdit/Model/SaveData/Actor/Influence.cs @@ -16,7 +16,7 @@ public partial class Influence : ObservableObject [ObservableProperty] private float _sentiment; - public string TypeId { get; init; } + public string TypeId { get; init; } = string.Empty; [JsonIgnore] public static IEnumerable AllTypes => new[] { Type.Player, Type.Cannibal, Type.Creepy }; diff --git a/SOTFEdit/Model/SaveData/Armour/PlayerArmourDataModel.cs b/SOTFEdit/Model/SaveData/Armour/PlayerArmourDataModel.cs index 48a5a99..30e6449 100644 --- a/SOTFEdit/Model/SaveData/Armour/PlayerArmourDataModel.cs +++ b/SOTFEdit/Model/SaveData/Armour/PlayerArmourDataModel.cs @@ -5,12 +5,12 @@ namespace SOTFEdit.Model.SaveData.Armour; // ReSharper disable once ClassNeverInstantiated.Global public record PlayerArmourDataModel : SotfBaseModel { - public DataModel Data { get; init; } + public DataModel? Data { get; init; } // ReSharper disable once ClassNeverInstantiated.Global public record DataModel { [JsonConverter(typeof(StringTypeConverter))] - public PlayerArmourSystemModel PlayerArmourSystem { get; init; } + public PlayerArmourSystemModel? PlayerArmourSystem { get; init; } } } \ No newline at end of file diff --git a/SOTFEdit/Model/SaveData/Armour/PlayerArmourSystemModel.cs b/SOTFEdit/Model/SaveData/Armour/PlayerArmourSystemModel.cs index 913a67f..875a8c8 100644 --- a/SOTFEdit/Model/SaveData/Armour/PlayerArmourSystemModel.cs +++ b/SOTFEdit/Model/SaveData/Armour/PlayerArmourSystemModel.cs @@ -6,7 +6,7 @@ namespace SOTFEdit.Model.SaveData.Armour; // ReSharper disable once ClassNeverInstantiated.Global public record PlayerArmourSystemModel : SotfBaseModel { - private static readonly int[] ArmorSlots = { 6, 7, 10, 11, 4, 5, 8, 9, 1, 0 }; + private static readonly int[] ArmorSlots = [6, 7, 10, 11, 4, 5, 8, 9, 1, 0]; public List ArmourPieces { get; private set; } = new(); public static bool Merge(PlayerArmourSystemModel armourSystemModel, diff --git a/SOTFEdit/Model/SaveData/Inventory/ItemInstanceManagerDataModel.cs b/SOTFEdit/Model/SaveData/Inventory/ItemInstanceManagerDataModel.cs index 3242744..2ef4b5f 100644 --- a/SOTFEdit/Model/SaveData/Inventory/ItemInstanceManagerDataModel.cs +++ b/SOTFEdit/Model/SaveData/Inventory/ItemInstanceManagerDataModel.cs @@ -5,5 +5,5 @@ namespace SOTFEdit.Model.SaveData.Inventory; // ReSharper disable once ClassNeverInstantiated.Global public record ItemInstanceManagerDataModel : SotfBaseModel { - public List ItemBlocks { get; set; } + public List? ItemBlocks { get; set; } } \ No newline at end of file diff --git a/SOTFEdit/Model/SaveData/Inventory/PlayerInventoryDataModel.cs b/SOTFEdit/Model/SaveData/Inventory/PlayerInventoryDataModel.cs index 9630126..ebd5981 100644 --- a/SOTFEdit/Model/SaveData/Inventory/PlayerInventoryDataModel.cs +++ b/SOTFEdit/Model/SaveData/Inventory/PlayerInventoryDataModel.cs @@ -8,7 +8,7 @@ namespace SOTFEdit.Model.SaveData.Inventory; // ReSharper disable once ClassNeverInstantiated.Global public record PlayerInventoryDataModel { - public DataModel Data { get; set; } + public DataModel? Data { get; set; } public static bool Merge(SaveDataWrapper saveDataWrapper, List selectedItems) { @@ -30,6 +30,6 @@ public static bool Merge(SaveDataWrapper saveDataWrapper, List se public class DataModel { [JsonConverter(typeof(StringTypeConverter))] - public PlayerInventoryModel PlayerInventory { get; set; } + public PlayerInventoryModel? PlayerInventory { get; set; } } } \ No newline at end of file diff --git a/SOTFEdit/Model/SaveData/Inventory/PlayerInventoryModel.cs b/SOTFEdit/Model/SaveData/Inventory/PlayerInventoryModel.cs index 1551014..432e0ec 100644 --- a/SOTFEdit/Model/SaveData/Inventory/PlayerInventoryModel.cs +++ b/SOTFEdit/Model/SaveData/Inventory/PlayerInventoryModel.cs @@ -11,7 +11,7 @@ namespace SOTFEdit.Model.SaveData.Inventory; public record PlayerInventoryModel { public List? EquippedItems { get; set; } - public ItemInstanceManagerDataModel ItemInstanceManagerData { get; set; } + public ItemInstanceManagerDataModel? ItemInstanceManagerData { get; set; } public static bool Merge(JToken playerInventory, List selectedItems) { diff --git a/SOTFEdit/Model/Savegame/Savegame.cs b/SOTFEdit/Model/Savegame/Savegame.cs index 4f860ff..21bd119 100644 --- a/SOTFEdit/Model/Savegame/Savegame.cs +++ b/SOTFEdit/Model/Savegame/Savegame.cs @@ -193,7 +193,7 @@ private void ReadSaveData() var patternMatch = _nameFilePattern.Match(Path.GetFileName(nameFile)); if (!patternMatch.Success || patternMatch.Groups.Count < 2) { - Logger.Warn($"Name pattern does not match on {nameFile}, will return raw filename"); + Logger.Trace($"Name pattern does not match on {nameFile}, will return raw filename"); return Path.GetFileNameWithoutExtension(nameFile).Replace("_", " "); } diff --git a/SOTFEdit/SOTFEdit.csproj b/SOTFEdit/SOTFEdit.csproj index 647b3a1..4a1804c 100644 --- a/SOTFEdit/SOTFEdit.csproj +++ b/SOTFEdit/SOTFEdit.csproj @@ -1,152 +1,160 @@  + + WinExe + net8.0-windows + enable + true + False + false + icons8-kleine-axt-doodle-96.ico + 1.2.1 + + - WinExe - net6.0-windows - enable - true - False - false - icons8-kleine-axt-doodle-96.ico - + - - - Always - - - Always - - + + + Always + + + Always + + + + true + - - - - - - - - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - True - True - Settings.settings - - - Code - AdvancedItemStorageUserControl.xaml - - + + + True + True + Settings.settings + + + Code + AdvancedItemStorageUserControl.xaml + + - - - PreserveNewest - - - SettingsSingleFileGenerator - Settings.Designer.cs - - + + + PreserveNewest + + + SettingsSingleFileGenerator + Settings.Designer.cs + + - - - MSBuild:Compile - Wpf - Designer - - - MSBuild:Compile - Wpf - Designer - - - MSBuild:Compile - Wpf - Designer - - - MSBuild:Compile - Wpf - Designer - - - MSBuild:Compile - Wpf - Designer - - - MSBuild:Compile - Wpf - Designer - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - Wpf - Designer - - - MSBuild:Compile - Wpf - Designer - - + + + MSBuild:Compile + Wpf + Designer + + + MSBuild:Compile + Wpf + Designer + + + MSBuild:Compile + Wpf + Designer + + + MSBuild:Compile + Wpf + Designer + + + MSBuild:Compile + Wpf + Designer + + + MSBuild:Compile + Wpf + Designer + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + Wpf + Designer + + + MSBuild:Compile + Wpf + Designer + + - - - - + + + + \ No newline at end of file diff --git a/SOTFEdit/Settings.Designer.cs b/SOTFEdit/Settings.Designer.cs index 86c4fd8..523939a 100644 --- a/SOTFEdit/Settings.Designer.cs +++ b/SOTFEdit/Settings.Designer.cs @@ -7,247 +7,464 @@ // //------------------------------------------------------------------------------ -namespace SOTFEdit { - - +namespace SOTFEdit +{ + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.9.0.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { - + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase + { + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default { - get { + + public static Settings Default + { + get + { return defaultInstance; } } - + [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("")] - public string SavegamePath { - get { + public string SavegamePath + { + get + { return ((string)(this["SavegamePath"])); } - set { + set + { this["SavegamePath"] = value; } } - + [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("True")] - public bool UpgradeRequired { - get { + public bool UpgradeRequired + { + get + { return ((bool)(this["UpgradeRequired"])); } - set { + set + { this["UpgradeRequired"] = value; } } - + [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("True")] - public bool CheckForUpdates { - get { + public bool CheckForUpdates + { + get + { return ((bool)(this["CheckForUpdates"])); } - set { + set + { this["CheckForUpdates"] = value; } } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("Normal")] + public string MapWindowState + { + get + { + return ((string)(this["MapWindowState"])); + } + set + { + this["MapWindowState"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("0")] + public int MapWindowScreenIndex + { + get + { + return ((int)(this["MapWindowScreenIndex"])); + } + set + { + this["MapWindowScreenIndex"] = value; + } + } [global::System.Configuration.DefaultSettingValueAttribute("")] - public string LastFoundVersion { - get { + public string LastFoundVersion + { + get + { return ((string)(this["LastFoundVersion"])); } - set { + set + { this["LastFoundVersion"] = value; } } - + [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("Dark")] - public string Theme { - get { + public string Theme + { + get + { return ((string)(this["Theme"])); } - set { + set + { this["Theme"] = value; } } - + [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("Blue")] - public string ThemeAccent { - get { + public string ThemeAccent + { + get + { return ((string)(this["ThemeAccent"])); } - set { + set + { this["ThemeAccent"] = value; } } - + [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("InitialAndOne")] - public global::SOTFEdit.ApplicationSettings.BackupMode BackupMode { - get { + public global::SOTFEdit.ApplicationSettings.BackupMode BackupMode + { + get + { return ((global::SOTFEdit.ApplicationSettings.BackupMode)(this["BackupMode"])); } - set { + set + { this["BackupMode"] = value; } } - + [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("")] - public string LastSavegame { - get { + public string LastSavegame + { + get + { return ((string)(this["LastSavegame"])); } - set { + set + { this["LastSavegame"] = value; } } - + [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("")] - public string Language { - get { + public string Language + { + get + { return ((string)(this["Language"])); } - set { + set + { this["Language"] = value; } } - + [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("")] - public string LastSavegameName { - get { + public string LastSavegameName + { + get + { return ((string)(this["LastSavegameName"])); } - set { + set + { this["LastSavegameName"] = value; } } - + [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("True")] - public bool FirstRun { - get { + public bool FirstRun + { + get + { return ((bool)(this["FirstRun"])); } - set { + set + { this["FirstRun"] = value; } } - + [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("_null_")] - public string SelectedMapGroups { - get { + public string SelectedMapGroups + { + get + { return ((string)(this["SelectedMapGroups"])); } - set { + set + { this["SelectedMapGroups"] = value; } } - + [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("127.0.0.1")] - public string CompanionAddress { - get { + public string CompanionAddress + { + get + { return ((string)(this["CompanionAddress"])); } - set { + set + { this["CompanionAddress"] = value; } } - + [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("35321")] - public int CompanionPort { - get { + public int CompanionPort + { + get + { return ((int)(this["CompanionPort"])); } - set { + set + { this["CompanionPort"] = value; } } - + [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("15")] - public short CompanionConnectTimeout { - get { + public short CompanionConnectTimeout + { + get + { return ((short)(this["CompanionConnectTimeout"])); } - set { + set + { this["CompanionConnectTimeout"] = value; } } - + [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("Dark")] - public global::SOTFEdit.ViewModel.MapType MapType { - get { + public global::SOTFEdit.ViewModel.MapType MapType + { + get + { return ((global::SOTFEdit.ViewModel.MapType)(this["MapType"])); } - set { + set + { this["MapType"] = value; } } - + [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("30")] - public short CompanionKeepAliveInterval { - get { + public short CompanionKeepAliveInterval + { + get + { return ((short)(this["CompanionKeepAliveInterval"])); } - set { + set + { this["CompanionKeepAliveInterval"] = value; } } - + [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("0.1")] - public decimal CompanionMapPositionUpdateInterval { - get { + public decimal CompanionMapPositionUpdateInterval + { + get + { return ((decimal)(this["CompanionMapPositionUpdateInterval"])); } - set { + set + { this["CompanionMapPositionUpdateInterval"] = value; } } - + [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("False")] - public bool AskForBackups { - get { + public bool AskForBackups + { + get + { return ((bool)(this["AskForBackups"])); } - set { + set + { this["AskForBackups"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("True")] + public bool MapFilterHideCompleted + { + get + { + return ((bool)(this["MapFilterHideCompleted"])); + } + set + { + this["MapFilterHideCompleted"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("All")] + public string MapFilterAreaFilter + { + get + { + return ((string)(this["MapFilterAreaFilter"])); + } + set + { + this["MapFilterAreaFilter"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("All")] + public string MapFilterRequirementsFilter + { + get + { + return ((string)(this["MapFilterRequirementsFilter"])); + } + set + { + this["MapFilterRequirementsFilter"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string MapFilterFullText + { + get + { + return ((string)(this["MapFilterFullText"])); + } + set + { + this["MapFilterFullText"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string DonePoiIdListString + { + get + { + return ((string)(this["DonePoiIdListString"])); + } + set + { + this["DonePoiIdListString"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("0")] + public double MapWindowLeft + { + get { return ((double)(this["MapWindowLeft"])); } + set { this["MapWindowLeft"] = value; } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("0")] + public double MapWindowTop + { + get { return ((double)(this["MapWindowTop"])); } + set { this["MapWindowTop"] = value; } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("800")] + public double MapWindowWidth + { + get { return ((double)(this["MapWindowWidth"])); } + set { this["MapWindowWidth"] = value; } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("600")] + public double MapWindowHeight + { + get { return ((double)(this["MapWindowHeight"])); } + set { this["MapWindowHeight"] = value; } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool AutoConnect + { + get { return ((bool)(this["AutoConnect"])); } + set { this["AutoConnect"] = value; } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool AutoReload + { + get { return ((bool)(this["AutoReload"])); } + set { this["AutoReload"] = value; } + } } } diff --git a/SOTFEdit/Settings.settings b/SOTFEdit/Settings.settings index 97440c0..71ba7ea 100644 --- a/SOTFEdit/Settings.settings +++ b/SOTFEdit/Settings.settings @@ -1,65 +1,169 @@  - - + + - - - - + + + + True - + True - - + + Normal + + + 0 + + + - + Dark - + Blue - + InitialAndOne - - + + - - + + - - + + - + True - + _null_ - + 127.0.0.1 - + 35321 - + 15 - + Dark - + 30 - + 0.1 - + + False + + + True + + + All + + + All + + + + + + + + + 0 + + + 0 + + + 800 + + + 600 + + + False + + False - - + \ No newline at end of file diff --git a/SOTFEdit/View/MainWindow.xaml b/SOTFEdit/View/MainWindow.xaml index eba49e9..e5f5476 100644 --- a/SOTFEdit/View/MainWindow.xaml +++ b/SOTFEdit/View/MainWindow.xaml @@ -71,7 +71,7 @@ - + diff --git a/SOTFEdit/View/MainWindow.xaml.cs b/SOTFEdit/View/MainWindow.xaml.cs index e9fd53f..0cc9512 100644 --- a/SOTFEdit/View/MainWindow.xaml.cs +++ b/SOTFEdit/View/MainWindow.xaml.cs @@ -25,7 +25,7 @@ namespace SOTFEdit.View; /// Interaction logic for MainWindow.xaml /// // ReSharper disable once UnusedMember.Global -public partial class MainWindow +public partial class MainWindow : MahApps.Metro.Controls.MetroWindow { private static readonly ILogger Logger = LogManager.GetCurrentClassLogger(); @@ -39,20 +39,65 @@ public partial class MainWindow public MainWindow() { - SetupListeners(); - DataContext = _dataContext = Ioc.Default.GetRequiredService(); - InitializeComponent(); + SetupListeners(); + DataContext = _dataContext = Ioc.Default.GetRequiredService(); + InitializeComponent(); - App.GetAssemblyVersion(out var assemblyName, out var assemblyVersion); + App.GetAssemblyVersion(out var assemblyName, out var assemblyVersion); + _baseTitle = $"{assemblyName} v{assemblyVersion}"; + Title = _baseTitle; + UpdateWindowTitle(); + Loaded += OnLoaded; + } + + private void UpdateWindowTitle() + { + var companionManager = Ioc.Default.GetRequiredService(); + var isConnected = companionManager.IsConnected(); + var status = companionManager.Status; + string connectionPart; + if (isConnected) + { + var ip = SOTFEdit.Settings.Default.CompanionAddress; + var port = SOTFEdit.Settings.Default.CompanionPort; + connectionPart = TranslationManager.GetFormatted("windows.main.connection.connected", ip, port); + } + else if (status == Infrastructure.Companion.CompanionConnectionManager.ConnectionStatus.Connecting) + { + connectionPart = TranslationManager.Get("windows.main.connection.connecting"); + } + else + { + connectionPart = TranslationManager.Get("windows.main.connection.disconnected"); + } - _baseTitle = $"{assemblyName} v{assemblyVersion}"; - Title = _baseTitle; + var savegame = SavegameManager.SelectedSavegame; + string savegamePart; + string modifiedPart = string.Empty; + if (savegame != null) + { + string type = savegame.PrintableType; + var fileName = savegame.FullPath.Split(System.IO.Path.DirectorySeparatorChar).LastOrDefault(); + savegamePart = TranslationManager.GetFormatted("windows.main.savegame.loaded", type!, fileName!); + var modified = savegame.SavegameStore.LastWriteTime; + modifiedPart = TranslationManager.GetFormatted("windows.main.modified", modified.ToString("yyyy-MM-dd HH:mm")!); + } + else + { + savegamePart = TranslationManager.Get("windows.main.savegame.none"); + } - Loaded += OnLoaded; + string assemblyName; + Semver.SemVersion assemblyVersion; + App.GetAssemblyVersion(out assemblyName, out assemblyVersion); + Title = TranslationManager.GetFormatted("windows.main.title", assemblyName, assemblyVersion.ToString(), connectionPart, savegamePart, modifiedPart); } private void SetupListeners() { + // Subscribe to language changes for hot-swap support + TranslationManager.LanguageChanged += OnLanguageChanged; + WeakReferenceMessenger.Default.Register(this, (_, message) => OnSavegameStored(message)); WeakReferenceMessenger.Default.Register(this, @@ -107,6 +152,23 @@ private void SetupListeners() (_, _) => OnOpenCompanionSetupWindowEvent()); } + private void OnLanguageChanged(object? sender, EventArgs e) + { + // The TranslateExtension now uses reactive bindings that auto-update + // We just need to refresh DataContext-bound properties and window title + + // 1. Re-bind DataContext to refresh ViewModel properties + var currentContext = DataContext; + DataContext = null; + DataContext = currentContext; + + // 2. Invalidate all commands + CommandManager.InvalidateRequerySuggested(); + + // 3. Update window title + UpdateWindowTitle(); + } + private void OnOpenCompanionSetupWindowEvent() { var window = new CompanionSetupWindow(this); @@ -445,11 +507,7 @@ private void OnSelectedSavegameChangedEvent(Savegame? selectedSavegame) { Application.Current.Dispatcher.Invoke(() => { - Title = _baseTitle + (selectedSavegame != null - ? TranslationManager.GetFormatted("windows.main.title", selectedSavegame.Title, - selectedSavegame.PrintableType, selectedSavegame.LastSaveTime, - selectedSavegame.SavegameStore.LastWriteTime) - : ""); + UpdateWindowTitle(); }); } diff --git a/SOTFEdit/View/Map/Details/CaveOrBunkerDetailsTemplate.xaml b/SOTFEdit/View/Map/Details/CaveOrBunkerDetailsTemplate.xaml index 7e84967..2570387 100644 --- a/SOTFEdit/View/Map/Details/CaveOrBunkerDetailsTemplate.xaml +++ b/SOTFEdit/View/Map/Details/CaveOrBunkerDetailsTemplate.xaml @@ -10,7 +10,7 @@ - + @@ -52,6 +52,14 @@ + \ No newline at end of file diff --git a/SOTFEdit/View/Map/Details/DefaultPoiDetailsTemplate.xaml b/SOTFEdit/View/Map/Details/DefaultPoiDetailsTemplate.xaml index 7e099aa..4fb94c0 100644 --- a/SOTFEdit/View/Map/Details/DefaultPoiDetailsTemplate.xaml +++ b/SOTFEdit/View/Map/Details/DefaultPoiDetailsTemplate.xaml @@ -27,6 +27,14 @@ \ No newline at end of file diff --git a/SOTFEdit/View/Map/Details/GenericInformationalPoiDetailTemplate.xaml b/SOTFEdit/View/Map/Details/GenericInformationalPoiDetailTemplate.xaml index 9c8651c..91447b2 100644 --- a/SOTFEdit/View/Map/Details/GenericInformationalPoiDetailTemplate.xaml +++ b/SOTFEdit/View/Map/Details/GenericInformationalPoiDetailTemplate.xaml @@ -49,6 +49,11 @@ + \ No newline at end of file diff --git a/SOTFEdit/View/MapSpawnActorsWindow.xaml.cs b/SOTFEdit/View/MapSpawnActorsWindow.xaml.cs index 4f86292..b398d02 100644 --- a/SOTFEdit/View/MapSpawnActorsWindow.xaml.cs +++ b/SOTFEdit/View/MapSpawnActorsWindow.xaml.cs @@ -9,7 +9,7 @@ namespace SOTFEdit.View; -public partial class MapSpawnActorsWindow : ICloseable +public partial class MapSpawnActorsWindow : MahApps.Metro.Controls.MetroWindow, ICloseable { public MapSpawnActorsWindow(Window owner, BasePoi destination) { diff --git a/SOTFEdit/View/MapTeleportWindow.xaml.cs b/SOTFEdit/View/MapTeleportWindow.xaml.cs index 281878d..51edf0d 100644 --- a/SOTFEdit/View/MapTeleportWindow.xaml.cs +++ b/SOTFEdit/View/MapTeleportWindow.xaml.cs @@ -9,7 +9,7 @@ namespace SOTFEdit.View; -public partial class MapTeleportWindow : ICloseable +public partial class MapTeleportWindow : MahApps.Metro.Controls.MetroWindow, ICloseable { public MapTeleportWindow(Window owner, BasePoi destination, MapTeleportWindowViewModel.TeleportationMode teleportationMode) diff --git a/SOTFEdit/View/MapWindow.xaml b/SOTFEdit/View/MapWindow.xaml index d067bcf..41e2ec9 100644 --- a/SOTFEdit/View/MapWindow.xaml +++ b/SOTFEdit/View/MapWindow.xaml @@ -1,68 +1,106 @@ - + - + - + - - - + + + + + @@ -84,24 +122,37 @@ - + - + - + + + + @@ -109,9 +160,7 @@ - + @@ -122,118 +171,158 @@ - - + + - + - - + + - + + PoiTemplate="{StaticResource PoiTemplate}" + ZipPoiTemplate="{StaticResource ZipPoiTemplate}" + ZiplineTemplate="{StaticResource ZiplineTemplate}" /> + WorldItemPoiDetailsTemplate="{StaticResource WorldItemPoiDetailsTemplate}" + ZipPointPoiDetailsTemplate="{StaticResource ZipPointPoiDetailsTemplate}" /> - + - - - + + +