Skip to content

Master#208

Merged
ALDamico merged 18 commits into
developfrom
master
Jun 7, 2026
Merged

Master#208
ALDamico merged 18 commits into
developfrom
master

Conversation

@ALDamico

@ALDamico ALDamico commented Jun 7, 2026

Copy link
Copy Markdown
Owner

No description provided.

ALDamico and others added 18 commits April 25, 2026 16:46
fix(ci): replace main with master in workflow branch filters and README
fix(ci): merge deploy-appcast into release.yml
* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>
* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>
* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>
* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix AppImage filename and remove space from portable ZIP name

- AppImage: add package release suffix (-1) to match PupNet output
  (tomb-launcher-{version}-1.x86_64.AppImage)
- Portable ZIP: use hyphens instead of spaces to avoid glob and shell
  quoting issues throughout the pipeline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): replace placeholder AppDescription in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): fix PublisherLinkUrl in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): clean up pupnet.conf metadata

- SetupMinWindowsVersion: 6.2 -> 10 (.NET 10 requires Windows 10+)
- PublisherLinkName: use display name instead of URL
- SetupCommandPrompt: clear (GUI app, no CLI entry needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>
* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix AppImage filename and remove space from portable ZIP name

- AppImage: add package release suffix (-1) to match PupNet output
  (tomb-launcher-{version}-1.x86_64.AppImage)
- Portable ZIP: use hyphens instead of spaces to avoid glob and shell
  quoting issues throughout the pipeline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): replace placeholder AppDescription in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): fix PublisherLinkUrl in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): clean up pupnet.conf metadata

- SetupMinWindowsVersion: 6.2 -> 10 (.NET 10 requires Windows 10+)
- PublisherLinkName: use display name instead of URL
- SetupCommandPrompt: clear (GUI app, no CLI entry needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(ci): use bash array for appcast common args

Reduces code duplication and avoids long backslash continuation chains
that may cause parsing issues on certain runners.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>
* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix AppImage filename and remove space from portable ZIP name

- AppImage: add package release suffix (-1) to match PupNet output
  (tomb-launcher-{version}-1.x86_64.AppImage)
- Portable ZIP: use hyphens instead of spaces to avoid glob and shell
  quoting issues throughout the pipeline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): replace placeholder AppDescription in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): fix PublisherLinkUrl in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): clean up pupnet.conf metadata

- SetupMinWindowsVersion: 6.2 -> 10 (.NET 10 requires Windows 10+)
- PublisherLinkName: use display name instead of URL
- SetupCommandPrompt: clear (GUI app, no CLI entry needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(ci): use bash array for appcast common args

Reduces code duplication and avoids long backslash continuation chains
that may cause parsing issues on certain runners.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix scp port flag and ssh-keyscan port for non-standard SSH port

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>
* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix AppImage filename and remove space from portable ZIP name

- AppImage: add package release suffix (-1) to match PupNet output
  (tomb-launcher-{version}-1.x86_64.AppImage)
- Portable ZIP: use hyphens instead of spaces to avoid glob and shell
  quoting issues throughout the pipeline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): replace placeholder AppDescription in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): fix PublisherLinkUrl in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): clean up pupnet.conf metadata

- SetupMinWindowsVersion: 6.2 -> 10 (.NET 10 requires Windows 10+)
- PublisherLinkName: use display name instead of URL
- SetupCommandPrompt: clear (GUI app, no CLI entry needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(ci): use bash array for appcast common args

Reduces code duplication and avoids long backslash continuation chains
that may cause parsing issues on certain runners.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix scp port flag and ssh-keyscan port for non-standard SSH port

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): upload appcast.xml.signature alongside appcast.xml

NetSparkle in Strict mode verifies the appcast using the .signature
file — without it, the signature check fails and no updates are detected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>
* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists (#76)

Allows re-running the release pipeline without failing on duplicate tags.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix deploy-appcast job

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Master (#85)

* Release/1.1.0 (#77)

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): fix appcast-deploy job

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): fix deploy-appcast job

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): fix deploy-appcast job

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix AppImage filename and remove space from portable ZIP name

- AppImage: add package release suffix (-1) to match PupNet output
  (tomb-launcher-{version}-1.x86_64.AppImage)
- Portable ZIP: use hyphens instead of spaces to avoid glob and shell
  quoting issues throughout the pipeline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): replace placeholder AppDescription in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): fix PublisherLinkUrl in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): clean up pupnet.conf metadata

- SetupMinWindowsVersion: 6.2 -> 10 (.NET 10 requires Windows 10+)
- PublisherLinkName: use display name instead of URL
- SetupCommandPrompt: clear (GUI app, no CLI entry needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): fix deploy-appcast job

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix AppImage filename and remove space from portable ZIP name

- AppImage: add package release suffix (-1) to match PupNet output
  (tomb-launcher-{version}-1.x86_64.AppImage)
- Portable ZIP: use hyphens instead of spaces to avoid glob and shell
  quoting issues throughout the pipeline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): replace placeholder AppDescription in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): fix PublisherLinkUrl in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): clean up pupnet.conf metadata

- SetupMinWindowsVersion: 6.2 -> 10 (.NET 10 requires Windows 10+)
- PublisherLinkName: use display name instead of URL
- SetupCommandPrompt: clear (GUI app, no CLI entry needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(ci): use bash array for appcast common args

Reduces code duplication and avoids long backslash continuation chains
that may cause parsing issues on certain runners.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* Release/1.1.0 (#83)

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix AppImage filename and remove space from portable ZIP name

- AppImage: add package release suffix (-1) to match PupNet output
  (tomb-launcher-{version}-1.x86_64.AppImage)
- Portable ZIP: use hyphens instead of spaces to avoid glob and shell
  quoting issues throughout the pipeline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): replace placeholder AppDescription in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): fix PublisherLinkUrl in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): clean up pupnet.conf metadata

- SetupMinWindowsVersion: 6.2 -> 10 (.NET 10 requires Windows 10+)
- PublisherLinkName: use display name instead of URL
- SetupCommandPrompt: clear (GUI app, no CLI entry needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(ci): use bash array for appcast common args

Reduces code duplication and avoids long backslash continuation chains
that may cause parsing issues on certain runners.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix scp port flag and ssh-keyscan port for non-standard SSH port

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* Release/1.1.0 (#84)

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix AppImage filename and remove space from portable ZIP name

- AppImage: add package release suffix (-1) to match PupNet output
  (tomb-launcher-{version}-1.x86_64.AppImage)
- Portable ZIP: use hyphens instead of spaces to avoid glob and shell
  quoting issues throughout the pipeline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): replace placeholder AppDescription in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): fix PublisherLinkUrl in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): clean up pupnet.conf metadata

- SetupMinWindowsVersion: 6.2 -> 10 (.NET 10 requires Windows 10+)
- PublisherLinkName: use display name instead of URL
- SetupCommandPrompt: clear (GUI app, no CLI entry needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(ci): use bash array for appcast common args

Reduces code duplication and avoids long backslash continuation chains
that may cause parsing issues on certain runners.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix scp port flag and ssh-keyscan port for non-standard SSH port

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): upload appcast.xml.signature alongside appcast.xml

NetSparkle in Strict mode verifies the appcast using the .signature
file — without it, the signature check fails and no updates are detected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* refactor: migrate away from automapper

* chore(deps): add Riok.Mapperly 4.3.1

Initial step of the AutoMapper → Mapperly migration tracked on this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: remove unused PlaySessionDto

PlaySessionDto and its two consumer methods on PlaySessionDataService
(GetLastPlaySession, GetPlaySessionsByGameId) were not called by anyone.
Drops one mapping pair from the AutoMapper → Mapperly migration scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "chore(deps): add Riok.Mapperly 4.3.1"

This reverts commit 31847b590b1841fa7fecc38db8440d5348eea7ea.

* refactor(mapping): switch language mapping from AutoMapper to manual

First step of the migration to manual mapping. AvailableLanguageDto ↔
ApplicationLanguageViewModel is now handled by a SettingsMapper instance
class (registered as DI singleton) instead of AutoMapper. The two unused
dead methods on SettingsPageService (GetApplicationLanguage,
GetApplicationTheme) are dropped along the way.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): complete SettingsMapper migration

Migrates DownloaderConfiguration ↔ DownloaderViewModel and
UnzipBackendDto ↔ UnzipBackendViewModel to manual mapping in
SettingsMapper. SettingsProfile is now empty and deleted.
DownloaderSettingsViewModel drops MapperConfiguration injection
in favour of SettingsMapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate AppCrash mapping to manual mapper

Introduces TombLauncher.Data/Mapping/ as the location for entity↔DTO
mappers. AppCrashMapper lives there, keeping entities contained within
the data layer. AppCrashProfile deleted, SettingsPageViewModel drops
its MapperConfiguration dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate statistics mapping to manual mapper

Introduces StatisticsMapper for the three statistics mappings.
DailyStatisticsAverageGameDurationViewModel renamed to
DailyGameDurationViewModel (used for both average and total),
its AverageGameDuration property renamed to GameDuration.
DailyStatisticsViewModel deleted (superseded by DailyGameDurationViewModel).
StatisticsProfile retains only the SavegameViewModel mapping (Fase 5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate GameLink mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate GameHash mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate FileBackup mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate Game and GameMetadata mapping to manual mappers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate DownloaderSearchPayload mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate LaunchOptions mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate GameWithStats mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate search result mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate savegame and FileBackup mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: update CLAUDE.md to reflect manual mapper convention

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: remove stale TombLauncher.Mapping using directive

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(deps): add Riok.Mapperly 4.3.1

Initial step of the AutoMapper → Mapperly migration tracked on this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: remove unused PlaySessionDto

PlaySessionDto and its two consumer methods on PlaySessionDataService
(GetLastPlaySession, GetPlaySessionsByGameId) were not called by anyone.
Drops one mapping pair from the AutoMapper → Mapperly migration scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "chore(deps): add Riok.Mapperly 4.3.1"

This reverts commit 31847b590b1841fa7fecc38db8440d5348eea7ea.

* refactor(mapping): switch language mapping from AutoMapper to manual

First step of the migration to manual mapping. AvailableLanguageDto ↔
ApplicationLanguageViewModel is now handled by a SettingsMapper instance
class (registered as DI singleton) instead of AutoMapper. The two unused
dead methods on SettingsPageService (GetApplicationLanguage,
GetApplicationTheme) are dropped along the way.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): complete SettingsMapper migration

Migrates DownloaderConfiguration ↔ DownloaderViewModel and
UnzipBackendDto ↔ UnzipBackendViewModel to manual mapping in
SettingsMapper. SettingsProfile is now empty and deleted.
DownloaderSettingsViewModel drops MapperConfiguration injection
in favour of SettingsMapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate AppCrash mapping to manual mapper

Introduces TombLauncher.Data/Mapping/ as the location for entity↔DTO
mappers. AppCrashMapper lives there, keeping entities contained within
the data layer. AppCrashProfile deleted, SettingsPageViewModel drops
its MapperConfiguration dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate statistics mapping to manual mapper

Introduces StatisticsMapper for the three statistics mappings.
DailyStatisticsAverageGameDurationViewModel renamed to
DailyGameDurationViewModel (used for both average and total),
its AverageGameDuration property renamed to GameDuration.
DailyStatisticsViewModel deleted (superseded by DailyGameDurationViewModel).
StatisticsProfile retains only the SavegameViewModel mapping (Fase 5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate GameLink mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate GameHash mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate FileBackup mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate Game and GameMetadata mapping to manual mappers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate DownloaderSearchPayload mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate LaunchOptions mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate GameWithStats mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate search result mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate savegame and FileBackup mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: update CLAUDE.md to reflect manual mapper convention

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: remove stale TombLauncher.Mapping using directive

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Update src/TombLauncher.Data/Mapping/GameLinkMapper.cs

Co-authored-by: codefactor-io[bot] <47775046+codefactor-io[bot]@users.noreply.github.com>

* Update src/TombLauncher/Mappers/LaunchOptionsMapper.cs

Co-authored-by: codefactor-io[bot] <47775046+codefactor-io[bot]@users.noreply.github.com>

* chore: align namespace and remove stale TombLauncher.Mapping usings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: remove AutoMapper configuration test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: codefactor-io[bot] <47775046+codefactor-io[bot]@users.noreply.github.com>

* feat(compatibility): add Proton support and per-game launcher settings (#109)

* feat(settings,launch-options): add Wine compatibility section and convert launch options to page

- New Compatibility settings section (WinePath, WinePrefix) visible on Linux only
- Wine detection at startup: WINE env var -> WINEPATH env var -> PATH scan
- Warning notification (GobletBrokenLine) if Wine not found
- LaunchOptionsDialogView/ViewModel replaced by LaunchOptionsView/ViewModel (PageViewModel)
- LaunchOptions promoted to direct button in GameBar (TerminalLine icon)
- Per-game WinePrefix override in LaunchOptionsView, visible on Linux only
- WinePrefix added to LaunchOptionsDto, Game entity and UpdateLaunchOptions
- DB migration: AddGameWinePrefix
- SettingsPageService.PersistCurrentConfigAsync() extracted as reusable method
- GameWithStatsService: WinePath read from IAppConfiguration.Compatibility (no hardcoded fallback)

* docs: fix add-page workflow view naming convention (View suffix, Pages/ folder)

* loc: simplify Wine-related UI strings for non-technical users

* style(ui): use text-muted class on LabeledTextBox label

* feat(ui): add CardSectionHeader control to LaunchOptionsView

- New CardSectionHeader control in TombLauncher.Controls (IconKind + Title)
- LaunchOptionsView: icon+title header on each card
  (PlayLargeLine/Launch, HammerLine/Setup, GroupLine/CommunityPatch, Goblet2Line/Compatibility)
- Add GENERAL and COMMUNITY_PATCH localization keys (en-US, it-IT)
- Community patch tooltip: 'some versions of TRX' instead of 'TRX'
- LabeledTextBox label now uses text-muted class

* fix(ui,navigation): sticky save button in LaunchOptionsView; fix GameWithStats refresh on back

- LaunchOptionsView: restructured with Grid (ScrollViewer row * + buttons row Auto)
  Save button now uses IconButton btn-primary/Save3Line, always visible outside scroll area
- Fix: GameWithStatsViewModel data not refreshing when navigating back from LaunchOptions

* fix(data): rewrite UpdateLaunchOptions using EF change tracker

Replace ExecuteUpdateAsync/ExecuteDeleteAsync (bulk ops that bypass EF tracking)
with a single tracked ToListAsync query + direct property assignment.
This ensures GetGameById returns fresh data after saving launch options,
fixing the stale executable path shown in GameDetailsViewModel on back navigation.

* feat(stats,settings): implement Wine version detection

- IPlatformSpecificFeatures: add GetWineVersion(string winePath)
- LinuxPlatformSpecificFeatures: runs wine --version, captures stdout
- WindowsPlatformSpecificFeatures: returns null (Wine not supported)
- CompatibilitySettingsViewModel: WineVersion auto-refreshed via OnWinePathChanged
- CompatibilitySettingsView: read-only Wine version row below WINEPREFIX
- StatisticsPageViewModel: WineVersion populated on init via IAppConfiguration.Compatibility.WinePath
- StatisticsPageView: row 7 with Goblet2Line icon, visible only on Linux (IsWineSupported)
- Localization: WINE_VERSION added in en-US and it-IT

* fix(linux): wrap wine launch in bash + wineserver -w for correct process tracking

wine exits immediately after spawning the Windows process into wineserver,
causing play time tracking and save file monitoring to stop prematurely.
Wrapping with 'bash -c "wine ...; wineserver -w"' keeps the monitored process
alive until wineserver shuts down (i.e. until the game truly exits).

* feat(proton): add Proton launcher support with compatibility settings

- Add ProtonGameLauncher: sets STEAM_COMPAT_DATA_PATH (with default),
  STEAM_COMPAT_INSTALL_PATH, STEAM_COMPAT_CLIENT_INSTALL_PATH (derived from
  Proton binary path), STEAM_APPID=0; strips NVIDIA PRIME vars inherited from
  parent process that break Wine rendering; uses bash login shell (-lc)
- Add WineGameLauncher, WindowsGameLauncher, IGameLauncher abstractions
- Add ProtonInstallationDto, CompatibilityTool enum
- Add CompatibilityToolToIconConverter
- LinuxPlatformSpecificFeatures: FindAvailableProtonInstallations(),
  CollectSteamRoots() (parses libraryfolders.vdf), GetProtonVersion()
- CompatibilitySettingsView/ViewModel: Wine/Proton tool selector, Proton
  installation ComboBox with auto-detect from steamapps, manual fallback,
  STEAM_COMPAT_DATA_PATH override, Proton version display
- GameWithStatsService: capture stdout/stderr on non-zero exit (Linux only)
- SavegameCardView: split MARK_UNMARK key into MARK_/UNMARK_ for better i18n,
  use BooleanToStringConverter for context-aware tooltip
- Localization: WINE_VERSION, MARK_AS_START_OF_LEVEL_SAVE,
  UNMARK_AS_START_OF_LEVEL_SAVE added in en-US and it-IT
- Add unit tests for CompatibilityConfig, GameLauncher, LinuxPlatformFeatures

* refactor(settings): remove WinePath from GameDetails config

WinePath was migrated to CompatibilityConfig in a previous session.
Now that the one-time migration path in App.axaml.cs is no longer
needed, remove the residual property from IGameDetailsConfig,
GameDetailsConfig, LayeredAppConfiguration, GameDetailsCoreSettings
and its construction in SettingsProvider.

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* fix(settings): compatibility tool change now takes effect without restart

IGameLauncher is now resolved on each game launch via Func<IGameLauncher>
instead of being captured at construction time in GameWithStatsService.

Also refactors InitializeServices into a fluent chain of IServiceCollection
extension methods, each with a single responsibility:
- AddAppConfiguration: loads JSON layers, registers ILayeredAppConfiguration/IAppConfiguration
- AddPlatformSpecificFeatures: registers IPlatformSpecificFeatures, IGameLauncher and its factory
- AddSaveGameManagement: savegame header provider, processor and query/command services
- AddTheming, AddFileOperations, AddLocalization, AddNavigation: focused singletons
- AddGameManagement: installer, engine detector, game merger
- AddPopups: IPopupService
- AddDownloaders: HTTP clients, downloader implementations, download manager
- AddNotifications, AddUpdater: notification and update services
- AddDatabaseAccess now resolves IAppConfiguration from sp internally

* refactor(app): move CheckCompatibilityToolAsync to AppUtils

Eliminates the private wrapper method in App, calling AppUtils directly
from ShowMainWindow. Unused usings (IconPacks, Configuration, Sections)
removed from App.axaml.cs.

* refactor(app): move ApplyInitialSettings to AppUtils

App.axaml.cs now delegates both startup helpers to AppUtils,
leaving it with only wiring/orchestration logic.

* style(app): use unqualified ChangeTheme call within AppUtils

* refactor(app): move OnUnhandledException to AppUtils

Registered directly as AppUtils.OnUnhandledException on the Dispatcher
event — no disadvantages vs instance method in this context (singleton
event source, no instance capture needed).
Removed unused usings from App.axaml.cs.

* refactor(navigation): move INavigationTarget and ITopBarCommand to Contracts, introduce BusyScope pattern

- Move INavigationTarget and ITopBarCommand from TombLauncher.Core.Navigation to TombLauncher.Contracts.Navigation
- ITopBarCommand.Icon changed from PackIconRemixIconKind to Enum to remove icon library dependency from Contracts
- CommandViewModel.Icon likewise changed to Enum
- INavigationTarget simplified: SetBusy(bool,string) removed from public interface, BusyScope(string) introduced as the primary RAII pattern
- SetBusy(string) kept/added for fire-and-forget cases (PlayGame, LaunchSetup, LaunchCommunitySetup)
- GameWithStatsService, StatisticsPageViewModel, GameListViewModel, SavegameListViewModel refactored to BusyScope pattern
- async void Uninstall fixed to async Task
- Unused usings and IoC.Default references removed

* fix(ui): fix BusyMessage not visible in RingLoader overlay

- RingLoader: move Width/Height constraint from UserControl to inner Viewbox,
  so the message TextBlock below the ring is no longer clipped to 80x80 bounds
- RingLoader: restore RelativeSource binding (was working, just clipped)
- GameListViewModel: add missing .GetLocalizedString() on FETCHING_GAMES key
- MainWindow: remove redundant debug TextBlock for BusyMessage

* refactor(compatibility): move CompatibilityTool enum to TombLauncher.Contracts

Moved CompatibilityTool from TombLauncher.Configuration.Sections to
TombLauncher.Contracts.Enums, alongside the other shared enums.

This removes the circular dependency that would have blocked adding
CompatibilityTool? fields to GameMetadataDto (TombLauncher.Core) and
the Game entity (TombLauncher.Data), both of which cannot reference
the main TombLauncher project.

Updated all consumers:
- CompatibilityConfig.cs, ICompatibilityConfig.cs
- LayeredAppConfiguration.cs
- PlatformSpecificFeaturesCollectionExtensions.cs
- CompatibilitySettingsViewModel.cs
- CompatibilityToolToIconConverter.cs
- AppUtils.cs
- CompatibilitySettingsView.axaml (xmlns:cfg alias)

* refactor(compatibility): introduce GameLaunchContext, per-game CompatibilityTool and ExtraEnvVars

- IGameLauncher.GetLaunchStartInfo now takes GameLaunchContext instead of 4 loose params
- GameLaunchContext carries ExecutableFileName, Arguments, WorkingDirectory, PrefixPath, ExtraEnvVars
- Wine/Proton/Windows launchers apply ExtraEnvVars last (per-game overrides)
- Rename WinePrefix -> CompatibilityPrefixPath across config, DTOs, models, ViewModels and Views
- Add CompatibilityTool (int) and CompatibilityToolPath to Game entity and DTOs
- Add GameEnvironmentVariable entity + EnvironmentVariableDto
- Add migration AddPerGameCompatibilitySettings
- LaunchOptionsProfile: map CompatibilityPrefixPath

* feat(settings,launch-options): add Wine compatibility section and convert launch options to page

- New Compatibility settings section (WinePath, WinePrefix) visible on Linux only
- Wine detection at startup: WINE env var -> WINEPATH env var -> PATH scan
- Warning notification (GobletBrokenLine) if Wine not found
- LaunchOptionsDialogView/ViewModel replaced by LaunchOptionsView/ViewModel (PageViewModel)
- LaunchOptions promoted to direct button in GameBar (TerminalLine icon)
- Per-game WinePrefix override in LaunchOptionsView, visible on Linux only
- WinePrefix added to LaunchOptionsDto, Game entity and UpdateLaunchOptions
- DB migration: AddGameWinePrefix
- SettingsPageService.PersistCurrentConfigAsync() extracted as reusable method
- GameWithStatsService: WinePath read from IAppConfiguration.Compatibility (no hardcoded fallback)

# Conflicts:
#	src/TombLauncher.Data/Database/Services/GameDataService.cs
#	src/TombLauncher/App.axaml.cs
#	src/TombLauncher/Extensions/ViewModelsServiceCollectionExtensions.cs
#	src/TombLauncher/Factories/Mapping/MappingUtils.cs
#	src/TombLauncher/Factories/Profiles/LaunchOptionsProfile.cs
#	src/TombLauncher/Services/GameDetailsService.cs
#	src/TombLauncher/Services/GameWithStatsService.cs
#	src/TombLauncher/ViewModels/Dialogs/LaunchOptionsDialogViewModel.cs
#	src/TombLauncher/ViewModels/Pages/GameDetailsViewModel.cs
#	src/TombLauncher/Views/GameBar.axaml

* docs: fix add-page workflow view naming convention (View suffix, Pages/ folder)

* loc: simplify Wine-related UI strings for non-technical users

* style(ui): use text-muted class on LabeledTextBox label

* feat(ui): add CardSectionHeader control to LaunchOptionsView

- New CardSectionHeader control in TombLauncher.Controls (IconKind + Title)
- LaunchOptionsView: icon+title header on each card
  (PlayLargeLine/Launch, HammerLine/Setup, GroupLine/CommunityPatch, Goblet2Line/Compatibility)
- Add GENERAL and COMMUNITY_PATCH localization keys (en-US, it-IT)
- Community patch tooltip: 'some versions of TRX' instead of 'TRX'
- LabeledTextBox label now uses text-muted class

* fix(ui,navigation): sticky save button in LaunchOptionsView; fix GameWithStats refresh on back

- LaunchOptionsView: restructured with Grid (ScrollViewer row * + buttons row Auto)
  Save button now uses IconButton btn-primary/Save3Line, always visible outside scroll area
- Fix: GameWithStatsViewModel data not refreshing when navigating back from LaunchOptions

* fix(data): rewrite UpdateLaunchOptions using EF change tracker

Replace ExecuteUpdateAsync/ExecuteDeleteAsync (bulk ops that bypass EF tracking)
with a single tracked ToListAsync query + direct property assignment.
This ensures GetGameById returns fresh data after saving launch options,
fixing the stale executable path shown in GameDetailsViewModel on back navigation.

* feat(stats,settings): implement Wine version detection

- IPlatformSpecificFeatures: add GetWineVersion(string winePath)
- LinuxPlatformSpecificFeatures: runs wine --version, captures stdout
- WindowsPlatformSpecificFeatures: returns null (Wine not supported)
- CompatibilitySettingsViewModel: WineVersion auto-refreshed via OnWinePathChanged
- CompatibilitySettingsView: read-only Wine version row below WINEPREFIX
- StatisticsPageViewModel: WineVersion populated on init via IAppConfiguration.Compatibility.WinePath
- StatisticsPageView: row 7 with Goblet2Line icon, visible only on Linux (IsWineSupported)
- Localization: WINE_VERSION added in en-US and it-IT

* fix(linux): wrap wine launch in bash + wineserver -w for correct process tracking

wine exits immediately after spawning the Windows process into wineserver,
causing play time tracking and save file monitoring to stop prematurely.
Wrapping with 'bash -c "wine ...; wineserver -w"' keeps the monitored process
alive until wineserver shuts down (i.e. until the game truly exits).

* feat(proton): add Proton launcher support with compatibility settings

- Add ProtonGameLauncher: sets STEAM_COMPAT_DATA_PATH (with default),
  STEAM_COMPAT_INSTALL_PATH, STEAM_COMPAT_CLIENT_INSTALL_PATH (derived from
  Proton binary path), STEAM_APPID=0; strips NVIDIA PRIME vars inherited from
  parent process that break Wine rendering; uses bash login shell (-lc)
- Add WineGameLauncher, WindowsGameLauncher, IGameLauncher abstractions
- Add ProtonInstallationDto, CompatibilityTool enum
- Add CompatibilityToolToIconConverter
- LinuxPlatformSpecificFeatures: FindAvailableProtonInstallations(),
  CollectSteamRoots() (parses libraryfolders.vdf), GetProtonVersion()
- CompatibilitySettingsView/ViewModel: Wine/Proton tool selector, Proton
  installation ComboBox with auto-detect from steamapps, manual fallback,
  STEAM_COMPAT_DATA_PATH override, Proton version display
- GameWithStatsService: capture stdout/stderr on non-zero exit (Linux only)
- SavegameCardView: split MARK_UNMARK key into MARK_/UNMARK_ for better i18n,
  use BooleanToStringConverter for context-aware tooltip
- Localization: WINE_VERSION, MARK_AS_START_OF_LEVEL_SAVE,
  UNMARK_AS_START_OF_LEVEL_SAVE added in en-US and it-IT
- Add unit tests for CompatibilityConfig, GameLauncher, LinuxPlatformFeatures

* refactor(settings): remove WinePath from GameDetails config

WinePath was migrated to CompatibilityConfig in a previous session.
Now that the one-time migration path in App.axaml.cs is no longer
needed, remove the residual property from IGameDetailsConfig,
GameDetailsConfig, LayeredAppConfiguration, GameDetailsCoreSettings
and its construction in SettingsProvider.

* fix(settings): compatibility tool change now takes effect without restart

IGameLauncher is now resolved on each game launch via Func<IGameLauncher>
instead of being captured at construction time in GameWithStatsService.

Also refactors InitializeServices into a fluent chain of IServiceCollection
extension methods, each with a single responsibility:
- AddAppConfiguration: loads JSON layers, registers ILayeredAppConfiguration/IAppConfiguration
- AddPlatformSpecificFeatures: registers IPlatformSpecificFeatures, IGameLauncher and its factory
- AddSaveGameManagement: savegame header provider, processor and query/command services
- AddTheming, AddFileOperations, AddLocalization, AddNavigation: focused singletons
- AddGameManagement: installer, engine detector, game merger
- AddPopups: IPopupService
- AddDownloaders: HTTP clients, downloader implementations, download manager
- AddNotifications, AddUpdater: notification and update services
- AddDatabaseAccess now resolves IAppConfiguration from sp internally

* refactor(app): move CheckCompatibilityToolAsync to AppUtils

Eliminates the private wrapper method in App, calling AppUtils directly
from ShowMainWindow. Unused usings (IconPacks, Configuration, Sections)
removed from App.axaml.cs.

* refactor(app): move ApplyInitialSettings to AppUtils

App.axaml.cs now delegates both startup helpers to AppUtils,
leaving it with only wiring/orchestration logic.

* style(app): use unqualified ChangeTheme call within AppUtils

* refactor(app): move OnUnhandledException to AppUtils

Registered directly as AppUtils.OnUnhandledException on the Dispatcher
event — no disadvantages vs instance method in this context (singleton
event source, no instance capture needed).
Removed unused usings from App.axaml.cs.

* refactor(navigation): move INavigationTarget and ITopBarCommand to Contracts, introduce BusyScope pattern

- Move INavigationTarget and ITopBarCommand from TombLauncher.Core.Navigation to TombLauncher.Contracts.Navigation
- ITopBarCommand.Icon changed from PackIconRemixIconKind to Enum to remove icon library dependency from Contracts
- CommandViewModel.Icon likewise changed to Enum
- INavigationTarget simplified: SetBusy(bool,string) removed from public interface, BusyScope(string) introduced as the primary RAII pattern
- SetBusy(string) kept/added for fire-and-forget cases (PlayGame, LaunchSetup, LaunchCommunitySetup)
- GameWithStatsService, StatisticsPageViewModel, GameListViewModel, SavegameListViewModel refactored to BusyScope pattern
- async void Uninstall fixed to async Task
- Unused usings and IoC.Default references removed

* fix(ui): fix BusyMessage not visible in RingLoader overlay

- RingLoader: move Width/Height constraint from UserControl to inner Viewbox,
  so the message TextBlock below the ring is no longer clipped to 80x80 bounds
- RingLoader: restore RelativeSource binding (was working, just clipped)
- GameListViewModel: add missing .GetLocalizedString() on FETCHING_GAMES key
- MainWindow: remove redundant debug TextBlock for BusyMessage

* refactor(compatibility): move CompatibilityTool enum to TombLauncher.Contracts

Moved CompatibilityTool from TombLauncher.Configuration.Sections to
TombLauncher.Contracts.Enums, alongside the other shared enums.

This removes the circular dependency that would have blocked adding
CompatibilityTool? fields to GameMetadataDto (TombLauncher.Core) and
the Game entity (TombLauncher.Data), both of which cannot reference
the main TombLauncher project.

Updated all consumers:
- CompatibilityConfig.cs, ICompatibilityConfig.cs
- LayeredAppConfiguration.cs
- PlatformSpecificFeaturesCollectionExtensions.cs
- CompatibilitySettingsViewModel.cs
- CompatibilityToolToIconConverter.cs
- AppUtils.cs
- CompatibilitySettingsView.axaml (xmlns:cfg alias)

* refactor(compatibility): introduce GameLaunchContext, per-game CompatibilityTool and ExtraEnvVars

- IGameLauncher.GetLaunchStartInfo now takes GameLaunchContext instead of 4 loose params
- GameLaunchContext carries ExecutableFileName, Arguments, WorkingDirectory, PrefixPath, ExtraEnvVars
- Wine/Proton/Windows launchers apply ExtraEnvVars last (per-game overrides)
- Rename WinePrefix -> CompatibilityPrefixPath across config, DTOs, models, ViewModels and Views
- Add CompatibilityTool (int) and CompatibilityToolPath to Game entity and DTOs
- Add GameEnvironmentVariable entity + EnvironmentVariableDto
- Add migration AddPerGameCompatibilitySettings
- LaunchOptionsProfile: map CompatibilityPrefixPath

* fix(compatibility): wire per-game CompatibilityTool, EnvVars and prefix through all layers

- Add IEnvironmentVariable contract and EnvironmentVariableMapper (ToDto/ToEntity)
- Extend IGameMetadata, GameMetadataDto, Game entity with CompatibilityTool,
  CompatibilityToolPath and ExtraEnvVars
- GameMapper: map new fields in both ToDto and ToGame; CompatibilityTool now
  defaults to Unspecified in CompatibilityConfig so layered merge works correctly
- GameDataService.UpdateLaunchOptions: add missing SetupExecutable block, persist
  ExtraEnvVars via EnvironmentVariableMapper.ToEntities, fix wrong executable added
  when GameExecutable backup was absent
- Add .Include(g => g.EnvironmentVariables) to all EF queries that call GameMapper
- GameMetadataViewModel / LaunchOptionsViewModel: expose CompatibilityTool,
  CompatibilityToolPath, ExtraEnvVars; InitFromGame populates them from the game VM
- LaunchOptionsMapper.ToDto: map CompatibilityPrefixPath, CompatibilityTool,
  CompatibilityToolPath, ExtraEnvVars
- GameWithStatsService.LaunchProcess: select launcher from per-game CompatibilityTool
  (falling back to global factory when Unspecified); pass ExtraEnvVars to context
- LayeredAppConfiguration: fix CompatibilityTool merge — check both User and Defaults
  before falling back to Wine; add "CompatibilityTool": "Wine" to appsettings.json
- Register EnvironmentVariableMapper in DI; restore GetGameById on GameDataService
- Update GameLauncherTests to use GameLaunchContext; fix CompatibilityConfigTests using

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(mappers,data): add mapper unit tests and UpdateLaunchOptions integration tests

- EnvironmentVariableMapperTests: ToDto/ToEntity/ToDtos/ToEntities
- GameMapperTests: ToDto and ToGame for compatibility fields and env vars
- LaunchOptionsMapperTests: compatibility fields, ExtraEnvVars, optional executables
- UpdateLaunchOptionsTests (EF Core InMemory): CompatibilityTool/Path/Prefix
  persistence, env var add/replace/clear, setup executable add/update/remove
- SearchMapperTests: TitlePic propagated through ToDto overloads
- Add Microsoft.EntityFrameworkCore.InMemory 10.0.2 to test project

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(platform): add ExpandPath to IPlatformSpecificFeatures for tilde expansion

Linux expands leading ~ to $HOME; Windows returns the path unchanged.
GameWithStatsService applies ExpandPath before passing PrefixPath to the launcher,
so users can type ~/... in the UI and have it resolve correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ui): add website link to sidebar navigation (#111)

Adds a link to tomblauncher.app in the main sidebar, alongside the existing GitHub link. Includes WebsiteLink in the config layer, ApplicationCoreSettings DTO, and both localization files (en-US / it-IT). Also moves CoreSettings.cs to the Dtos/Configuration subfolder.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ui): add About page with acknowledgements (#110) (#112)

* feat(ui): add About page with app info and acknowledgements section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(ui): extract SidebarNavigationItem control to reduce sidebar duplication

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ui): populate About page acknowledgements section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(release): bump version to 1.2.0 (#113)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>
Co-authored-by: codefactor-io[bot] <47775046+codefactor-io[bot]@users.noreply.github.com>
* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists (#76)

Allows re-running the release pipeline without failing on duplicate tags.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix deploy-appcast job

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Master (#85)

* Release/1.1.0 (#77)

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): fix appcast-deploy job

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): fix deploy-appcast job

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): fix deploy-appcast job

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix AppImage filename and remove space from portable ZIP name

- AppImage: add package release suffix (-1) to match PupNet output
  (tomb-launcher-{version}-1.x86_64.AppImage)
- Portable ZIP: use hyphens instead of spaces to avoid glob and shell
  quoting issues throughout the pipeline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): replace placeholder AppDescription in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): fix PublisherLinkUrl in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): clean up pupnet.conf metadata

- SetupMinWindowsVersion: 6.2 -> 10 (.NET 10 requires Windows 10+)
- PublisherLinkName: use display name instead of URL
- SetupCommandPrompt: clear (GUI app, no CLI entry needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): fix deploy-appcast job

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix AppImage filename and remove space from portable ZIP name

- AppImage: add package release suffix (-1) to match PupNet output
  (tomb-launcher-{version}-1.x86_64.AppImage)
- Portable ZIP: use hyphens instead of spaces to avoid glob and shell
  quoting issues throughout the pipeline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): replace placeholder AppDescription in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): fix PublisherLinkUrl in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): clean up pupnet.conf metadata

- SetupMinWindowsVersion: 6.2 -> 10 (.NET 10 requires Windows 10+)
- PublisherLinkName: use display name instead of URL
- SetupCommandPrompt: clear (GUI app, no CLI entry needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(ci): use bash array for appcast common args

Reduces code duplication and avoids long backslash continuation chains
that may cause parsing issues on certain runners.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* Release/1.1.0 (#83)

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix AppImage filename and remove space from portable ZIP name

- AppImage: add package release suffix (-1) to match PupNet output
  (tomb-launcher-{version}-1.x86_64.AppImage)
- Portable ZIP: use hyphens instead of spaces to avoid glob and shell
  quoting issues throughout the pipeline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): replace placeholder AppDescription in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): fix PublisherLinkUrl in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): clean up pupnet.conf metadata

- SetupMinWindowsVersion: 6.2 -> 10 (.NET 10 requires Windows 10+)
- PublisherLinkName: use display name instead of URL
- SetupCommandPrompt: clear (GUI app, no CLI entry needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(ci): use bash array for appcast common args

Reduces code duplication and avoids long backslash continuation chains
that may cause parsing issues on certain runners.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix scp port flag and ssh-keyscan port for non-standard SSH port

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* Release/1.1.0 (#84)

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix AppImage filename and remove space from portable ZIP name

- AppImage: add package release suffix (-1) to match PupNet output
  (tomb-launcher-{version}-1.x86_64.AppImage)
- Portable ZIP: use hyphens instead of spaces to avoid glob and shell
  quoting issues throughout the pipeline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): replace placeholder AppDescription in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): fix PublisherLinkUrl in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): clean up pupnet.conf metadata

- SetupMinWindowsVersion: 6.2 -> 10 (.NET 10 requires Windows 10+)
- PublisherLinkName: use display name instead of URL
- SetupCommandPrompt: clear (GUI app, no CLI entry needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(ci): use bash array for appcast common args

Reduces code duplication and avoids long backslash continuation chains
that may cause parsing issues on certain runners.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix scp port flag and ssh-keyscan port for non-standard SSH port

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): upload appcast.xml.signature alongside appcast.xml

NetSparkle in Strict mode verifies the appcast using the .signature
file — without it, the signature check fails and no updates are detected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* refactor: migrate away from automapper

* chore(deps): add Riok.Mapperly 4.3.1

Initial step of the AutoMapper → Mapperly migration tracked on this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: remove unused PlaySessionDto

PlaySessionDto and its two consumer methods on PlaySessionDataService
(GetLastPlaySession, GetPlaySessionsByGameId) were not called by anyone.
Drops one mapping pair from the AutoMapper → Mapperly migration scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "chore(deps): add Riok.Mapperly 4.3.1"

This reverts commit 31847b590b1841fa7fecc38db8440d5348eea7ea.

* refactor(mapping): switch language mapping from AutoMapper to manual

First step of the migration to manual mapping. AvailableLanguageDto ↔
ApplicationLanguageViewModel is now handled by a SettingsMapper instance
class (registered as DI singleton) instead of AutoMapper. The two unused
dead methods on SettingsPageService (GetApplicationLanguage,
GetApplicationTheme) are dropped along the way.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): complete SettingsMapper migration

Migrates DownloaderConfiguration ↔ DownloaderViewModel and
UnzipBackendDto ↔ UnzipBackendViewModel to manual mapping in
SettingsMapper. SettingsProfile is now empty and deleted.
DownloaderSettingsViewModel drops MapperConfiguration injection
in favour of SettingsMapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate AppCrash mapping to manual mapper

Introduces TombLauncher.Data/Mapping/ as the location for entity↔DTO
mappers. AppCrashMapper lives there, keeping entities contained within
the data layer. AppCrashProfile deleted, SettingsPageViewModel drops
its MapperConfiguration dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate statistics mapping to manual mapper

Introduces StatisticsMapper for the three statistics mappings.
DailyStatisticsAverageGameDurationViewModel renamed to
DailyGameDurationViewModel (used for both average and total),
its AverageGameDuration property renamed to GameDuration.
DailyStatisticsViewModel deleted (superseded by DailyGameDurationViewModel).
StatisticsProfile retains only the SavegameViewModel mapping (Fase 5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate GameLink mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate GameHash mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate FileBackup mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate Game and GameMetadata mapping to manual mappers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate DownloaderSearchPayload mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate LaunchOptions mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate GameWithStats mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate search result mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate savegame and FileBackup mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: update CLAUDE.md to reflect manual mapper convention

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: remove stale TombLauncher.Mapping using directive

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(deps): add Riok.Mapperly 4.3.1

Initial step of the AutoMapper → Mapperly migration tracked on this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: remove unused PlaySessionDto

PlaySessionDto and its two consumer methods on PlaySessionDataService
(GetLastPlaySession, GetPlaySessionsByGameId) were not called by anyone.
Drops one mapping pair from the AutoMapper → Mapperly migration scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "chore(deps): add Riok.Mapperly 4.3.1"

This reverts commit 31847b590b1841fa7fecc38db8440d5348eea7ea.

* refactor(mapping): switch language mapping from AutoMapper to manual

First step of the migration to manual mapping. AvailableLanguageDto ↔
ApplicationLanguageViewModel is now handled by a SettingsMapper instance
class (registered as DI singleton) instead of AutoMapper. The two unused
dead methods on SettingsPageService (GetApplicationLanguage,
GetApplicationTheme) are dropped along the way.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): complete SettingsMapper migration

Migrates DownloaderConfiguration ↔ DownloaderViewModel and
UnzipBackendDto ↔ UnzipBackendViewModel to manual mapping in
SettingsMapper. SettingsProfile is now empty and deleted.
DownloaderSettingsViewModel drops MapperConfiguration injection
in favour of SettingsMapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate AppCrash mapping to manual mapper

Introduces TombLauncher.Data/Mapping/ as the location for entity↔DTO
mappers. AppCrashMapper lives there, keeping entities contained within
the data layer. AppCrashProfile deleted, SettingsPageViewModel drops
its MapperConfiguration dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate statistics mapping to manual mapper

Introduces StatisticsMapper for the three statistics mappings.
DailyStatisticsAverageGameDurationViewModel renamed to
DailyGameDurationViewModel (used for both average and total),
its AverageGameDuration property renamed to GameDuration.
DailyStatisticsViewModel deleted (superseded by DailyGameDurationViewModel).
StatisticsProfile retains only the SavegameViewModel mapping (Fase 5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate GameLink mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate GameHash mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate FileBackup mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate Game and GameMetadata mapping to manual mappers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate DownloaderSearchPayload mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate LaunchOptions mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate GameWithStats mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate search result mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate savegame and FileBackup mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: update CLAUDE.md to reflect manual mapper convention

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: remove stale TombLauncher.Mapping using directive

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Update src/TombLauncher.Data/Mapping/GameLinkMapper.cs

Co-authored-by: codefactor-io[bot] <47775046+codefactor-io[bot]@users.noreply.github.com>

* Update src/TombLauncher/Mappers/LaunchOptionsMapper.cs

Co-authored-by: codefactor-io[bot] <47775046+codefactor-io[bot]@users.noreply.github.com>

* chore: align namespace and remove stale TombLauncher.Mapping usings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: remove AutoMapper configuration test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: codefactor-io[bot] <47775046+codefactor-io[bot]@users.noreply.github.com>

* feat(compatibility): add Proton support and per-game launcher settings (#109)

* feat(settings,launch-options): add Wine compatibility section and convert launch options to page

- New Compatibility settings section (WinePath, WinePrefix) visible on Linux only
- Wine detection at startup: WINE env var -> WINEPATH env var -> PATH scan
- Warning notification (GobletBrokenLine) if Wine not found
- LaunchOptionsDialogView/ViewModel replaced by LaunchOptionsView/ViewModel (PageViewModel)
- LaunchOptions promoted to direct button in GameBar (TerminalLine icon)
- Per-game WinePrefix override in LaunchOptionsView, visible on Linux only
- WinePrefix added to LaunchOptionsDto, Game entity and UpdateLaunchOptions
- DB migration: AddGameWinePrefix
- SettingsPageService.PersistCurrentConfigAsync() extracted as reusable method
- GameWithStatsService: WinePath read from IAppConfiguration.Compatibility (no hardcoded fallback)

* docs: fix add-page workflow view naming convention (View suffix, Pages/ folder)

* loc: simplify Wine-related UI strings for non-technical users

* style(ui): use text-muted class on LabeledTextBox label

* feat(ui): add CardSectionHeader control to LaunchOptionsView

- New CardSectionHeader control in TombLauncher.Controls (IconKind + Title)
- LaunchOptionsView: icon+title header on each card
  (PlayLargeLine/Launch, HammerLine/Setup, GroupLine/CommunityPatch, Goblet2Line/Compatibility)
- Add GENERAL and COMMUNITY_PATCH localization keys (en-US, it-IT)
- Community patch tooltip: 'some versions of TRX' instead of 'TRX'
- LabeledTextBox label now uses text-muted class

* fix(ui,navigation): sticky save button in LaunchOptionsView; fix GameWithStats refresh on back

- LaunchOptionsView: restructured with Grid (ScrollViewer row * + buttons row Auto)
  Save button now uses IconButton btn-primary/Save3Line, always visible outside scroll area
- Fix: GameWithStatsViewModel data not refreshing when navigating back from LaunchOptions

* fix(data): rewrite UpdateLaunchOptions using EF change tracker

Replace ExecuteUpdateAsync/ExecuteDeleteAsync (bulk ops that bypass EF tracking)
with a single tracked ToListAsync query + direct property assignment.
This ensures GetGameById returns fresh data after saving launch options,
fixing the stale executable path shown in GameDetailsViewModel on back navigation.

* feat(stats,settings): implement Wine version detection

- IPlatformSpecificFeatures: add GetWineVersion(string winePath)
- LinuxPlatformSpecificFeatures: runs wine --version, captures stdout
- WindowsPlatformSpecificFeatures: returns null (Wine not supported)
- CompatibilitySettingsViewModel: WineVersion auto-refreshed via OnWinePathChanged
- CompatibilitySettingsView: read-only Wine version row below WINEPREFIX
- StatisticsPageViewModel: WineVersion populated on init via IAppConfiguration.Compatibility.WinePath
- StatisticsPageView: row 7 with Goblet2Line icon, visible only on Linux (IsWineSupported)
- Localization: WINE_VERSION added in en-US and it-IT

* fix(linux): wrap wine launch in bash + wineserver -w for correct process tracking

wine exits immediately after spawning the Windows process into wineserver,
causing play time tracking and save file monitoring to stop prematurely.
Wrapping with 'bash -c "wine ...; wineserver -w"' keeps the monitored process
alive until wineserver shuts down (i.e. until the game truly exits).

* feat(proton): add Proton launcher support with compatibility settings

- Add ProtonGameLauncher: sets STEAM_COMPAT_DATA_PATH (with default),
  STEAM_COMPAT_INSTALL_PATH, STEAM_COMPAT_CLIENT_INSTALL_PATH (derived from
  Proton binary path), STEAM_APPID=0; strips NVIDIA PRIME vars inherited from
  parent process that break Wine rendering; uses bash login shell (-lc)
- Add WineGameLauncher, WindowsGameLauncher, IGameLauncher abstractions
- Add ProtonInstallationDto, CompatibilityTool enum
- Add CompatibilityToolToIconConverter
- LinuxPlatformSpecificFeatures: FindAvailableProtonInstallations(),
  CollectSteamRoots() (parses libraryfolders.vdf), GetProtonVersion()
- CompatibilitySettingsView/ViewModel: Wine/Proton tool selector, Proton
  installation ComboBox with auto-detect from steamapps, manual fallback,
  STEAM_COMPAT_DATA_PATH override, Proton version display
- GameWithStatsService: capture stdout/stderr on non-zero exit (Linux only)
- SavegameCardView: split MARK_UNMARK key into MARK_/UNMARK_ for better i18n,
  use BooleanToStringConverter for context-aware tooltip
- Localization: WINE_VERSION, MARK_AS_START_OF_LEVEL_SAVE,
  UNMARK_AS_START_OF_LEVEL_SAVE added in en-US and it-IT
- Add unit tests for CompatibilityConfig, GameLauncher, LinuxPlatformFeatures

* refactor(settings): remove WinePath from GameDetails config

WinePath was migrated to CompatibilityConfig in a previous session.
Now that the one-time migration path in App.axaml.cs is no longer
needed, remove the residual property from IGameDetailsConfig,
GameDetailsConfig, LayeredAppConfiguration, GameDetailsCoreSettings
and its construction in SettingsProvider.

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* fix(settings): compatibility tool change now takes effect without restart

IGameLauncher is now resolved on each game launch via Func<IGameLauncher>
instead of being captured at construction time in GameWithStatsService.

Also refactors InitializeServices into a fluent chain of IServiceCollection
extension methods, each with a single responsibility:
- AddAppConfiguration: loads JSON layers, registers ILayeredAppConfiguration/IAppConfiguration
- AddPlatformSpecificFeatures: registers IPlatformSpecificFeatures, IGameLauncher and its factory
- AddSaveGameManagement: savegame header provider, processor and query/command services
- AddTheming, AddFileOperations, AddLocalization, AddNavigation: focused singletons
- AddGameManagement: installer, engine detector, game merger
- AddPopups: IPopupService
- AddDownloaders: HTTP clients, downloader implementations, download manager
- AddNotifications, AddUpdater: notification and update services
- AddDatabaseAccess now resolves IAppConfiguration from sp internally

* refactor(app): move CheckCompatibilityToolAsync to AppUtils

Eliminates the private wrapper method in App, calling AppUtils directly
from ShowMainWindow. Unused usings (IconPacks, Configuration, Sections)
removed from App.axaml.cs.

* refactor(app): move ApplyInitialSettings to AppUtils

App.axaml.cs now delegates both startup helpers to AppUtils,
leaving it with only wiring/orchestration logic.

* style(app): use unqualified ChangeTheme call within AppUtils

* refactor(app): move OnUnhandledException to AppUtils

Registered directly as AppUtils.OnUnhandledException on the Dispatcher
event — no disadvantages vs instance method in this context (singleton
event source, no instance capture needed).
Removed unused usings from App.axaml.cs.

* refactor(navigation): move INavigationTarget and ITopBarCommand to Contracts, introduce BusyScope pattern

- Move INavigationTarget and ITopBarCommand from TombLauncher.Core.Navigation to TombLauncher.Contracts.Navigation
- ITopBarCommand.Icon changed from PackIconRemixIconKind to Enum to remove icon library dependency from Contracts
- CommandViewModel.Icon likewise changed to Enum
- INavigationTarget simplified: SetBusy(bool,string) removed from public interface, BusyScope(string) introduced as the primary RAII pattern
- SetBusy(string) kept/added for fire-and-forget cases (PlayGame, LaunchSetup, LaunchCommunitySetup)
- GameWithStatsService, StatisticsPageViewModel, GameListViewModel, SavegameListViewModel refactored to BusyScope pattern
- async void Uninstall fixed to async Task
- Unused usings and IoC.Default references removed

* fix(ui): fix BusyMessage not visible in RingLoader overlay

- RingLoader: move Width/Height constraint from UserControl to inner Viewbox,
  so the message TextBlock below the ring is no longer clipped to 80x80 bounds
- RingLoader: restore RelativeSource binding (was working, just clipped)
- GameListViewModel: add missing .GetLocalizedString() on FETCHING_GAMES key
- MainWindow: remove redundant debug TextBlock for BusyMessage

* refactor(compatibility): move CompatibilityTool enum to TombLauncher.Contracts

Moved CompatibilityTool from TombLauncher.Configuration.Sections to
TombLauncher.Contracts.Enums, alongside the other shared enums.

This removes the circular dependency that would have blocked adding
CompatibilityTool? fields to GameMetadataDto (TombLauncher.Core) and
the Game entity (TombLauncher.Data), both of which cannot reference
the main TombLauncher project.

Updated all consumers:
- CompatibilityConfig.cs, ICompatibilityConfig.cs
- LayeredAppConfiguration.cs
- PlatformSpecificFeaturesCollectionExtensions.cs
- CompatibilitySettingsViewModel.cs
- CompatibilityToolToIconConverter.cs
- AppUtils.cs
- CompatibilitySettingsView.axaml (xmlns:cfg alias)

* refactor(compatibility): introduce GameLaunchContext, per-game CompatibilityTool and ExtraEnvVars

- IGameLauncher.GetLaunchStartInfo now takes GameLaunchContext instead of 4 loose params
- GameLaunchContext carries ExecutableFileName, Arguments, WorkingDirectory, PrefixPath, ExtraEnvVars
- Wine/Proton/Windows launchers apply ExtraEnvVars last (per-game overrides)
- Rename WinePrefix -> CompatibilityPrefixPath across config, DTOs, models, ViewModels and Views
- Add CompatibilityTool (int) and CompatibilityToolPath to Game entity and DTOs
- Add GameEnvironmentVariable entity + EnvironmentVariableDto
- Add migration AddPerGameCompatibilitySettings
- LaunchOptionsProfile: map CompatibilityPrefixPath

* feat(settings,launch-options): add Wine compatibility section and convert launch options to page

- New Compatibility settings section (WinePath, WinePrefix) visible on Linux only
- Wine detection at startup: WINE env var -> WINEPATH env var -> PATH scan
- Warning notification (GobletBrokenLine) if Wine not found
- LaunchOptionsDialogView/ViewModel replaced by LaunchOptionsView/ViewModel (PageViewModel)
- LaunchOptions promoted to direct button in GameBar (TerminalLine icon)
- Per-game WinePrefix override in LaunchOptionsView, visible on Linux only
- WinePrefix added to LaunchOptionsDto, Game entity and UpdateLaunchOptions
- DB migration: AddGameWinePrefix
- SettingsPageService.PersistCurrentConfigAsync() extracted as reusable method
- GameWithStatsService: WinePath read from IAppConfiguration.Compatibility (no hardcoded fallback)

# Conflicts:
#	src/TombLauncher.Data/Database/Services/GameDataService.cs
#	src/TombLauncher/App.axaml.cs
#	src/TombLauncher/Extensions/ViewModelsServiceCollectionExtensions.cs
#	src/TombLauncher/Factories/Mapping/MappingUtils.cs
#	src/TombLauncher/Factories/Profiles/LaunchOptionsProfile.cs
#	src/TombLauncher/Services/GameDetailsService.cs
#	src/TombLauncher/Services/GameWithStatsService.cs
#	src/TombLauncher/ViewModels/Dialogs/LaunchOptionsDialogViewModel.cs
#	src/TombLauncher/ViewModels/Pages/GameDetailsViewModel.cs
#	src/TombLauncher/Views/GameBar.axaml

* docs: fix add-page workflow view naming convention (View suffix, Pages/ folder)

* loc: simplify Wine-related UI strings for non-technical users

* style(ui): use text-muted class on LabeledTextBox label

* feat(ui): add CardSectionHeader control to LaunchOptionsView

- New CardSectionHeader control in TombLauncher.Controls (IconKind + Title)
- LaunchOptionsView: icon+title header on each card
  (PlayLargeLine/Launch, HammerLine/Setup, GroupLine/CommunityPatch, Goblet2Line/Compatibility)
- Add GENERAL and COMMUNITY_PATCH localization keys (en-US, it-IT)
- Community patch tooltip: 'some versions of TRX' instead of 'TRX'
- LabeledTextBox label now uses text-muted class

* fix(ui,navigation): sticky save button in LaunchOptionsView; fix GameWithStats refresh on back

- LaunchOptionsView: restructured with Grid (ScrollViewer row * + buttons row Auto)
  Save button now uses IconButton btn-primary/Save3Line, always visible outside scroll area
- Fix: GameWithStatsViewModel data not refreshing when navigating back from LaunchOptions

* fix(data): rewrite UpdateLaunchOptions using EF change tracker

Replace ExecuteUpdateAsync/ExecuteDeleteAsync (bulk ops that bypass EF tracking)
with a single tracked ToListAsync query + direct property assignment.
This ensures GetGameById returns fresh data after saving launch options,
fixing the stale executable path shown in GameDetailsViewModel on back navigation.

* feat(stats,settings): implement Wine version detection

- IPlatformSpecificFeatures: add GetWineVersion(string winePath)
- LinuxPlatformSpecificFeatures: runs wine --version, captures stdout
- WindowsPlatformSpecificFeatures: returns null (Wine not supported)
- CompatibilitySettingsViewModel: WineVersion auto-refreshed via OnWinePathChanged
- CompatibilitySettingsView: read-only Wine version row below WINEPREFIX
- StatisticsPageViewModel: WineVersion populated on init via IAppConfiguration.Compatibility.WinePath
- StatisticsPageView: row 7 with Goblet2Line icon, visible only on Linux (IsWineSupported)
- Localization: WINE_VERSION added in en-US and it-IT

* fix(linux): wrap wine launch in bash + wineserver -w for correct process tracking

wine exits immediately after spawning the Windows process into wineserver,
causing play time tracking and save file monitoring to stop prematurely.
Wrapping with 'bash -c "wine ...; wineserver -w"' keeps the monitored process
alive until wineserver shuts down (i.e. until the game truly exits).

* feat(proton): add Proton launcher support with compatibility settings

- Add ProtonGameLauncher: sets STEAM_COMPAT_DATA_PATH (with default),
  STEAM_COMPAT_INSTALL_PATH, STEAM_COMPAT_CLIENT_INSTALL_PATH (derived from
  Proton binary path), STEAM_APPID=0; strips NVIDIA PRIME vars inherited from
  parent process that break Wine rendering; uses bash login shell (-lc)
- Add WineGameLauncher, WindowsGameLauncher, IGameLauncher abstractions
- Add ProtonInstallationDto, CompatibilityTool enum
- Add CompatibilityToolToIconConverter
- LinuxPlatformSpecificFeatures: FindAvailableProtonInstallations(),
  CollectSteamRoots() (parses libraryfolders.vdf), GetProtonVersion()
- CompatibilitySettingsView/ViewModel: Wine/Proton tool selector, Proton
  installation ComboBox with auto-detect from steamapps, manual fallback,
  STEAM_COMPAT_DATA_PATH override, Proton version display
- GameWithStatsService: capture stdout/stderr on non-zero exit (Linux only)
- SavegameCardView: split MARK_UNMARK key into MARK_/UNMARK_ for better i18n,
  use BooleanToStringConverter for context-aware tooltip
- Localization: WINE_VERSION, MARK_AS_START_OF_LEVEL_SAVE,
  UNMARK_AS_START_OF_LEVEL_SAVE added in en-US and it-IT
- Add unit tests for CompatibilityConfig, GameLauncher, LinuxPlatformFeatures

* refactor(settings): remove WinePath from GameDetails config

WinePath was migrated to CompatibilityConfig in a previous session.
Now that the one-time migration path in App.axaml.cs is no longer
needed, remove the residual property from IGameDetailsConfig,
GameDetailsConfig, LayeredAppConfiguration, GameDetailsCoreSettings
and its construction in SettingsProvider.

* fix(settings): compatibility tool change now takes effect without restart

IGameLauncher is now resolved on each game launch via Func<IGameLauncher>
instead of being captured at construction time in GameWithStatsService.

Also refactors InitializeServices into a fluent chain of IServiceCollection
extension methods, each with a single responsibility:
- AddAppConfiguration: loads JSON layers, registers ILayeredAppConfiguration/IAppConfiguration
- AddPlatformSpecificFeatures: registers IPlatformSpecificFeatures, IGameLauncher and its factory
- AddSaveGameManagement: savegame header provider, processor and query/command services
- AddTheming, AddFileOperations, AddLocalization, AddNavigation: focused singletons
- AddGameManagement: installer, engine detector, game merger
- AddPopups: IPopupService
- AddDownloaders: HTTP clients, downloader implementations, download manager
- AddNotifications, AddUpdater: notification and update services
- AddDatabaseAccess now resolves IAppConfiguration from sp internally

* refactor(app): move CheckCompatibilityToolAsync to AppUtils

Eliminates the private wrapper method in App, calling AppUtils directly
from ShowMainWindow. Unused usings (IconPacks, Configuration, Sections)
removed from App.axaml.cs.

* refactor(app): move ApplyInitialSettings to AppUtils

App.axaml.cs now delegates both startup helpers to AppUtils,
leaving it with only wiring/orchestration logic.

* style(app): use unqualified ChangeTheme call within AppUtils

* refactor(app): move OnUnhandledException to AppUtils

Registered directly as AppUtils.OnUnhandledException on the Dispatcher
event — no disadvantages vs instance method in this context (singleton
event source, no instance capture needed).
Removed unused usings from App.axaml.cs.

* refactor(navigation): move INavigationTarget and ITopBarCommand to Contracts, introduce BusyScope pattern

- Move INavigationTarget and ITopBarCommand from TombLauncher.Core.Navigation to TombLauncher.Contracts.Navigation
- ITopBarCommand.Icon changed from PackIconRemixIconKind to Enum to remove icon library dependency from Contracts
- CommandViewModel.Icon likewise changed to Enum
- INavigationTarget simplified: SetBusy(bool,string) removed from public interface, BusyScope(string) introduced as the primary RAII pattern
- SetBusy(string) kept/added for fire-and-forget cases (PlayGame, LaunchSetup, LaunchCommunitySetup)
- GameWithStatsService, StatisticsPageViewModel, GameListViewModel, SavegameListViewModel refactored to BusyScope pattern
- async void Uninstall fixed to async Task
- Unused usings and IoC.Default references removed

* fix(ui): fix BusyMessage not visible in RingLoader overlay

- RingLoader: move Width/Height constraint from UserControl to inner Viewbox,
  so the message TextBlock below the ring is no longer clipped to 80x80 bounds
- RingLoader: restore RelativeSource binding (was working, just clipped)
- GameListViewModel: add missing .GetLocalizedString() on FETCHING_GAMES key
- MainWindow: remove redundant debug TextBlock for BusyMessage

* refactor(compatibility): move CompatibilityTool enum to TombLauncher.Contracts

Moved CompatibilityTool from TombLauncher.Configuration.Sections to
TombLauncher.Contracts.Enums, alongside the other shared enums.

This removes the circular dependency that would have blocked adding
CompatibilityTool? fields to GameMetadataDto (TombLauncher.Core) and
the Game entity (TombLauncher.Data), both of which cannot reference
the main TombLauncher project.

Updated all consumers:
- CompatibilityConfig.cs, ICompatibilityConfig.cs
- LayeredAppConfiguration.cs
- PlatformSpecificFeaturesCollectionExtensions.cs
- CompatibilitySettingsViewModel.cs
- CompatibilityToolToIconConverter.cs
- AppUtils.cs
- CompatibilitySettingsView.axaml (xmlns:cfg alias)

* refactor(compatibility): introduce GameLaunchContext, per-game CompatibilityTool and ExtraEnvVars

- IGameLauncher.GetLaunchStartInfo now takes GameLaunchContext instead of 4 loose params
- GameLaunchContext carries ExecutableFileName, Arguments, WorkingDirectory, PrefixPath, ExtraEnvVars
- Wine/Proton/Windows launchers apply ExtraEnvVars last (per-game overrides)
- Rename WinePrefix -> CompatibilityPrefixPath across config, DTOs, models, ViewModels and Views
- Add CompatibilityTool (int) and CompatibilityToolPath to Game entity and DTOs
- Add GameEnvironmentVariable entity + EnvironmentVariableDto
- Add migration AddPerGameCompatibilitySettings
- LaunchOptionsProfile: map CompatibilityPrefixPath

* fix(compatibility): wire per-game CompatibilityTool, EnvVars and prefix through all layers

- Add IEnvironmentVariable contract and EnvironmentVariableMapper (ToDto/ToEntity)
- Extend IGameMetadata, GameMetadataDto, Game entity with CompatibilityTool,
  CompatibilityToolPath and ExtraEnvVars
- GameMapper: map new fields in both ToDto and ToGame; CompatibilityTool now
  defaults to Unspecified in CompatibilityConfig so layered merge works correctly
- GameDataService.UpdateLaunchOptions: add missing SetupExecutable block, persist
  ExtraEnvVars via EnvironmentVariableMapper.ToEntities, fix wrong executable added
  when GameExecutable backup was absent
- Add .Include(g => g.EnvironmentVariables) to all EF queries that call GameMapper
- GameMetadataViewModel / LaunchOptionsViewModel: expose CompatibilityTool,
  CompatibilityToolPath, ExtraEnvVars; InitFromGame populates them from the game VM
- LaunchOptionsMapper.ToDto: map CompatibilityPrefixPath, CompatibilityTool,
  CompatibilityToolPath, ExtraEnvVars
- GameWithStatsService.LaunchProcess: select launcher from per-game CompatibilityTool
  (falling back to global factory when Unspecified); pass ExtraEnvVars to context
- LayeredAppConfiguration: fix CompatibilityTool merge — check both User and Defaults
  before falling back to Wine; add "CompatibilityTool": "Wine" to appsettings.json
- Register EnvironmentVariableMapper in DI; restore GetGameById on GameDataService
- Update GameLauncherTests to use GameLaunchContext; fix CompatibilityConfigTests using

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(mappers,data): add mapper unit tests and UpdateLaunchOptions integration tests

- EnvironmentVariableMapperTests: ToDto/ToEntity/ToDtos/ToEntities
- GameMapperTests: ToDto and ToGame for compatibility fields and env vars
- LaunchOptionsMapperTests: compatibility fields, ExtraEnvVars, optional executables
- UpdateLaunchOptionsTests (EF Core InMemory): CompatibilityTool/Path/Prefix
  persistence, env var add/replace/clear, setup executable add/update/remove
- SearchMapperTests: TitlePic propagated through ToDto overloads
- Add Microsoft.EntityFrameworkCore.InMemory 10.0.2 to test project

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(platform): add ExpandPath to IPlatformSpecificFeatures for tilde expansion

Linux expands leading ~ to $HOME; Windows returns the path unchanged.
GameWithStatsService applies ExpandPath before passing PrefixPath to the launcher,
so users can type ~/... in the UI and have it resolve correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ui): add website link to sidebar navigation (#111)

Adds a link to tomblauncher.app in the main sidebar, alongside the existing GitHub link. Includes WebsiteLink in the config layer, ApplicationCoreSettings DTO, and both localization files (en-US / it-IT). Also moves CoreSettings.cs to the Dtos/Configuration subfolder.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ui): add About page with acknowledgements (#110) (#112)

* feat(ui): add About page with app info and acknowledgements section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(ui): extract SidebarNavigationItem control to reduce sidebar duplication

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ui): populate About page acknowledgements section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(release): bump version to 1.2.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>
Co-authored-by: codefactor-io[bot] <47775046+codefactor-io[bot]@users.noreply.github.com>
* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists (#76)

Allows re-running the release pipeline without failing on duplicate tags.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix deploy-appcast job

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Master (#85)

* Release/1.1.0 (#77)

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): fix appcast-deploy job

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): fix deploy-appcast job

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): fix deploy-appcast job

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix AppImage filename and remove space from portable ZIP name

- AppImage: add package release suffix (-1) to match PupNet output
  (tomb-launcher-{version}-1.x86_64.AppImage)
- Portable ZIP: use hyphens instead of spaces to avoid glob and shell
  quoting issues throughout the pipeline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): replace placeholder AppDescription in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): fix PublisherLinkUrl in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): clean up pupnet.conf metadata

- SetupMinWindowsVersion: 6.2 -> 10 (.NET 10 requires Windows 10+)
- PublisherLinkName: use display name instead of URL
- SetupCommandPrompt: clear (GUI app, no CLI entry needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): fix deploy-appcast job

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix AppImage filename and remove space from portable ZIP name

- AppImage: add package release suffix (-1) to match PupNet output
  (tomb-launcher-{version}-1.x86_64.AppImage)
- Portable ZIP: use hyphens instead of spaces to avoid glob and shell
  quoting issues throughout the pipeline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): replace placeholder AppDescription in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): fix PublisherLinkUrl in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): clean up pupnet.conf metadata

- SetupMinWindowsVersion: 6.2 -> 10 (.NET 10 requires Windows 10+)
- PublisherLinkName: use display name instead of URL
- SetupCommandPrompt: clear (GUI app, no CLI entry needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(ci): use bash array for appcast common args

Reduces code duplication and avoids long backslash continuation chains
that may cause parsing issues on certain runners.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* Release/1.1.0 (#83)

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix AppImage filename and remove space from portable ZIP name

- AppImage: add package release suffix (-1) to match PupNet output
  (tomb-launcher-{version}-1.x86_64.AppImage)
- Portable ZIP: use hyphens instead of spaces to avoid glob and shell
  quoting issues throughout the pipeline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): replace placeholder AppDescription in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): fix PublisherLinkUrl in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): clean up pupnet.conf metadata

- SetupMinWindowsVersion: 6.2 -> 10 (.NET 10 requires Windows 10+)
- PublisherLinkName: use display name instead of URL
- SetupCommandPrompt: clear (GUI app, no CLI entry needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(ci): use bash array for appcast common args

Reduces code duplication and avoids long backslash continuation chains
that may cause parsing issues on certain runners.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix scp port flag and ssh-keyscan port for non-standard SSH port

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* Release/1.1.0 (#84)

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix AppImage filename and remove space from portable ZIP name

- AppImage: add package release suffix (-1) to match PupNet output
  (tomb-launcher-{version}-1.x86_64.AppImage)
- Portable ZIP: use hyphens instead of spaces to avoid glob and shell
  quoting issues throughout the pipeline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): replace placeholder AppDescription in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): fix PublisherLinkUrl in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): clean up pupnet.conf metadata

- SetupMinWindowsVersion: 6.2 -> 10 (.NET 10 requires Windows 10+)
- PublisherLinkName: use display name instead of URL
- SetupCommandPrompt: clear (GUI app, no CLI entry needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(ci): use bash array for appcast common args

Reduces code duplication and avoids long backslash continuation chains
that may cause parsing issues on certain runners.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix scp port flag and ssh-keyscan port for non-standard SSH port

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): upload appcast.xml.signature alongside appcast.xml

NetSparkle in Strict mode verifies the appcast using the .signature
file — without it, the signature check fails and no updates are detected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* refactor: migrate away from automapper

* chore(deps): add Riok.Mapperly 4.3.1

Initial step of the AutoMapper → Mapperly migration tracked on this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: remove unused PlaySessionDto

PlaySessionDto and its two consumer methods on PlaySessionDataService
(GetLastPlaySession, GetPlaySessionsByGameId) were not called by anyone.
Drops one mapping pair from the AutoMapper → Mapperly migration scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "chore(deps): add Riok.Mapperly 4.3.1"

This reverts commit 31847b590b1841fa7fecc38db8440d5348eea7ea.

* refactor(mapping): switch language mapping from AutoMapper to manual

First step of the migration to manual mapping. AvailableLanguageDto ↔
ApplicationLanguageViewModel is now handled by a SettingsMapper instance
class (registered as DI singleton) instead of AutoMapper. The two unused
dead methods on SettingsPageService (GetApplicationLanguage,
GetApplicationTheme) are dropped along the way.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): complete SettingsMapper migration

Migrates DownloaderConfiguration ↔ DownloaderViewModel and
UnzipBackendDto ↔ UnzipBackendViewModel to manual mapping in
SettingsMapper. SettingsProfile is now empty and deleted.
DownloaderSettingsViewModel drops MapperConfiguration injection
in favour of SettingsMapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate AppCrash mapping to manual mapper

Introduces TombLauncher.Data/Mapping/ as the location for entity↔DTO
mappers. AppCrashMapper lives there, keeping entities contained within
the data layer. AppCrashProfile deleted, SettingsPageViewModel drops
its MapperConfiguration dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate statistics mapping to manual mapper

Introduces StatisticsMapper for the three statistics mappings.
DailyStatisticsAverageGameDurationViewModel renamed to
DailyGameDurationViewModel (used for both average and total),
its AverageGameDuration property renamed to GameDuration.
DailyStatisticsViewModel deleted (superseded by DailyGameDurationViewModel).
StatisticsProfile retains only the SavegameViewModel mapping (Fase 5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate GameLink mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate GameHash mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate FileBackup mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate Game and GameMetadata mapping to manual mappers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate DownloaderSearchPayload mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate LaunchOptions mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate GameWithStats mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate search result mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate savegame and FileBackup mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: update CLAUDE.md to reflect manual mapper convention

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: remove stale TombLauncher.Mapping using directive

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(deps): add Riok.Mapperly 4.3.1

Initial step of the AutoMapper → Mapperly migration tracked on this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: remove unused PlaySessionDto

PlaySessionDto and its two consumer methods on PlaySessionDataService
(GetLastPlaySession, GetPlaySessionsByGameId) were not called by anyone.
Drops one mapping pair from the AutoMapper → Mapperly migration scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "chore(deps): add Riok.Mapperly 4.3.1"

This reverts commit 31847b590b1841fa7fecc38db8440d5348eea7ea.

* refactor(mapping): switch language mapping from AutoMapper to manual

First step of the migration to manual mapping. AvailableLanguageDto ↔
ApplicationLanguageViewModel is now handled by a SettingsMapper instance
class (registered as DI singleton) instead of AutoMapper. The two unused
dead methods on SettingsPageService (GetApplicationLanguage,
GetApplicationTheme) are dropped along the way.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): complete SettingsMapper migration

Migrates DownloaderConfiguration ↔ DownloaderViewModel and
UnzipBackendDto ↔ UnzipBackendViewModel to manual mapping in
SettingsMapper. SettingsProfile is now empty and deleted.
DownloaderSettingsViewModel drops MapperConfiguration injection
in favour of SettingsMapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate AppCrash mapping to manual mapper

Introduces TombLauncher.Data/Mapping/ as the location for entity↔DTO
mappers. AppCrashMapper lives there, keeping entities contained within
the data layer. AppCrashProfile deleted, SettingsPageViewModel drops
its MapperConfiguration dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate statistics mapping to manual mapper

Introduces StatisticsMapper for the three statistics mappings.
DailyStatisticsAverageGameDurationViewModel renamed to
DailyGameDurationViewModel (used for both average and total),
its AverageGameDuration property renamed to GameDuration.
DailyStatisticsViewModel deleted (superseded by DailyGameDurationViewModel).
StatisticsProfile retains only the SavegameViewModel mapping (Fase 5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate GameLink mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate GameHash mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate FileBackup mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate Game and GameMetadata mapping to manual mappers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate DownloaderSearchPayload mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate LaunchOptions mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate GameWithStats mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate search result mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate savegame and FileBackup mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: update CLAUDE.md to reflect manual mapper convention

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: remove stale TombLauncher.Mapping using directive

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Update src/TombLauncher.Data/Mapping/GameLinkMapper.cs

Co-authored-by: codefactor-io[bot] <47775046+codefactor-io[bot]@users.noreply.github.com>

* Update src/TombLauncher/Mappers/LaunchOptionsMapper.cs

Co-authored-by: codefactor-io[bot] <47775046+codefactor-io[bot]@users.noreply.github.com>

* chore: align namespace and remove stale TombLauncher.Mapping usings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: remove AutoMapper configuration test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: codefactor-io[bot] <47775046+codefactor-io[bot]@users.noreply.github.com>

* feat(compatibility): add Proton support and per-game launcher settings (#109)

* feat(settings,launch-options): add Wine compatibility section and convert launch options to page

- New Compatibility settings section (WinePath, WinePrefix) visible on Linux only
- Wine detection at startup: WINE env var -> WINEPATH env var -> PATH scan
- Warning notification (GobletBrokenLine) if Wine not found
- LaunchOptionsDialogView/ViewModel replaced by LaunchOptionsView/ViewModel (PageViewModel)
- LaunchOptions promoted to direct button in GameBar (TerminalLine icon)
- Per-game WinePrefix override in LaunchOptionsView, visible on Linux only
- WinePrefix added to LaunchOptionsDto, Game entity and UpdateLaunchOptions
- DB migration: AddGameWinePrefix
- SettingsPageService.PersistCurrentConfigAsync() extracted as reusable method
- GameWithStatsService: WinePath read from IAppConfiguration.Compatibility (no hardcoded fallback)

* docs: fix add-page workflow view naming convention (View suffix, Pages/ folder)

* loc: simplify Wine-related UI strings for non-technical users

* style(ui): use text-muted class on LabeledTextBox label

* feat(ui): add CardSectionHeader control to LaunchOptionsView

- New CardSectionHeader control in TombLauncher.Controls (IconKind + Title)
- LaunchOptionsView: icon+title header on each card
  (PlayLargeLine/Launch, HammerLine/Setup, GroupLine/CommunityPatch, Goblet2Line/Compatibility)
- Add GENERAL and COMMUNITY_PATCH localization keys (en-US, it-IT)
- Community patch tooltip: 'some versions of TRX' instead of 'TRX'
- LabeledTextBox label now uses text-muted class

* fix(ui,navigation): sticky save button in LaunchOptionsView; fix GameWithStats refresh on back

- LaunchOptionsView: restructured with Grid (ScrollViewer row * + buttons row Auto)
  Save button now uses IconButton btn-primary/Save3Line, always visible outside scroll area
- Fix: GameWithStatsViewModel data not refreshing when navigating back from LaunchOptions

* fix(data): rewrite UpdateLaunchOptions using EF change tracker

Replace ExecuteUpdateAsync/ExecuteDeleteAsync (bulk ops that bypass EF tracking)
with a single tracked ToListAsync query + direct property assignment.
This ensures GetGameById returns fresh data after saving launch options,
fixing the stale executable path shown in GameDetailsViewModel on back navigation.

* feat(stats,settings): implement Wine version detection

- IPlatformSpecificFeatures: add GetWineVersion(string winePath)
- LinuxPlatformSpecificFeatures: runs wine --version, captures stdout
- WindowsPlatformSpecificFeatures: returns null (Wine not supported)
- CompatibilitySettingsViewModel: WineVersion auto-refreshed via OnWinePathChanged
- CompatibilitySettingsView: read-only Wine version row below WINEPREFIX
- StatisticsPageViewModel: WineVersion populated on init via IAppConfiguration.Compatibility.WinePath
- StatisticsPageView: row 7 with Goblet2Line icon, visible only on Linux (IsWineSupported)
- Localization: WINE_VERSION added in en-US and it-IT

* fix(linux): wrap wine launch in bash + wineserver -w for correct process tracking

wine exits immediately after spawning the Windows process into wineserver,
causing play time tracking and save file monitoring to stop prematurely.
Wrapping with 'bash -c "wine ...; wineserver -w"' keeps the monitored process
alive until wineserver shuts down (i.e. until the game truly exits).

* feat(proton): add Proton launcher support with compatibility settings

- Add ProtonGameLauncher: sets STEAM_COMPAT_DATA_PATH (with default),
  STEAM_COMPAT_INSTALL_PATH, STEAM_COMPAT_CLIENT_INSTALL_PATH (derived from
  Proton binary path), STEAM_APPID=0; strips NVIDIA PRIME vars inherited from
  parent process that break Wine rendering; uses bash login shell (-lc)
- Add WineGameLauncher, WindowsGameLauncher, IGameLauncher abstractions
- Add ProtonInstallationDto, CompatibilityTool enum
- Add CompatibilityToolToIconConverter
- LinuxPlatformSpecificFeatures: FindAvailableProtonInstallations(),
  CollectSteamRoots() (parses libraryfolders.vdf), GetProtonVersion()
- CompatibilitySettingsView/ViewModel: Wine/Proton tool selector, Proton
  installation ComboBox with auto-detect from steamapps, manual fallback,
  STEAM_COMPAT_DATA_PATH override, Proton version display
- GameWithStatsService: capture stdout/stderr on non-zero exit (Linux only)
- SavegameCardView: split MARK_UNMARK key into MARK_/UNMARK_ for better i18n,
  use BooleanToStringConverter for context-aware tooltip
- Localization: WINE_VERSION, MARK_AS_START_OF_LEVEL_SAVE,
  UNMARK_AS_START_OF_LEVEL_SAVE added in en-US and it-IT
- Add unit tests for CompatibilityConfig, GameLauncher, LinuxPlatformFeatures

* refactor(settings): remove WinePath from GameDetails config

WinePath was migrated to CompatibilityConfig in a previous session.
Now that the one-time migration path in App.axaml.cs is no longer
needed, remove the residual property from IGameDetailsConfig,
GameDetailsConfig, LayeredAppConfiguration, GameDetailsCoreSettings
and its construction in SettingsProvider.

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* fix(settings): compatibility tool change now takes effect without restart

IGameLauncher is now resolved on each game launch via Func<IGameLauncher>
instead of being captured at construction time in GameWithStatsService.

Also refactors InitializeServices into a fluent chain of IServiceCollection
extension methods, each with a single responsibility:
- AddAppConfiguration: loads JSON layers, registers ILayeredAppConfiguration/IAppConfiguration
- AddPlatformSpecificFeatures: registers IPlatformSpecificFeatures, IGameLauncher and its factory
- AddSaveGameManagement: savegame header provider, processor and query/command services
- AddTheming, AddFileOperations, AddLocalization, AddNavigation: focused singletons
- AddGameManagement: installer, engine detector, game merger
- AddPopups: IPopupService
- AddDownloaders: HTTP clients, downloader implementations, download manager
- AddNotifications, AddUpdater: notification and update services
- AddDatabaseAccess now resolves IAppConfiguration from sp internally

* refactor(app): move CheckCompatibilityToolAsync to AppUtils

Eliminates the private wrapper method in App, calling AppUtils directly
from ShowMainWindow. Unused usings (IconPacks, Configuration, Sections)
removed from App.axaml.cs.

* refactor(app): move ApplyInitialSettings to AppUtils

App.axaml.cs now delegates both startup helpers to AppUtils,
leaving it with only wiring/orchestration logic.

* style(app): use unqualified ChangeTheme call within AppUtils

* refactor(app): move OnUnhandledException to AppUtils

Registered directly as AppUtils.OnUnhandledException on the Dispatcher
event — no disadvantages vs instance method in this context (singleton
event source, no instance capture needed).
Removed unused usings from App.axaml.cs.

* refactor(navigation): move INavigationTarget and ITopBarCommand to Contracts, introduce BusyScope pattern

- Move INavigationTarget and ITopBarCommand from TombLauncher.Core.Navigation to TombLauncher.Contracts.Navigation
- ITopBarCommand.Icon changed from PackIconRemixIconKind to Enum to remove icon library dependency from Contracts
- CommandViewModel.Icon likewise changed to Enum
- INavigationTarget simplified: SetBusy(bool,string) removed from public interface, BusyScope(string) introduced as the primary RAII pattern
- SetBusy(string) kept/added for fire-and-forget cases (PlayGame, LaunchSetup, LaunchCommunitySetup)
- GameWithStatsService, StatisticsPageViewModel, GameListViewModel, SavegameListViewModel refactored to BusyScope pattern
- async void Uninstall fixed to async Task
- Unused usings and IoC.Default references removed

* fix(ui): fix BusyMessage not visible in RingLoader overlay

- RingLoader: move Width/Height constraint from UserControl to inner Viewbox,
  so the message TextBlock below the ring is no longer clipped to 80x80 bounds
- RingLoader: restore RelativeSource binding (was working, just clipped)
- GameListViewModel: add missing .GetLocalizedString() on FETCHING_GAMES key
- MainWindow: remove redundant debug TextBlock for BusyMessage

* refactor(compatibility): move CompatibilityTool enum to TombLauncher.Contracts

Moved CompatibilityTool from TombLauncher.Configuration.Sections to
TombLauncher.Contracts.Enums, alongside the other shared enums.

This removes the circular dependency that would have blocked adding
CompatibilityTool? fields to GameMetadataDto (TombLauncher.Core) and
the Game entity (TombLauncher.Data), both of which cannot reference
the main TombLauncher project.

Updated all consumers:
- CompatibilityConfig.cs, ICompatibilityConfig.cs
- LayeredAppConfiguration.cs
- PlatformSpecificFeaturesCollectionExtensions.cs
- CompatibilitySettingsViewModel.cs
- CompatibilityToolToIconConverter.cs
- AppUtils.cs
- CompatibilitySettingsView.axaml (xmlns:cfg alias)

* refactor(compatibility): introduce GameLaunchContext, per-game CompatibilityTool and ExtraEnvVars

- IGameLauncher.GetLaunchStartInfo now takes GameLaunchContext instead of 4 loose params
- GameLaunchContext carries ExecutableFileName, Arguments, WorkingDirectory, PrefixPath, ExtraEnvVars
- Wine/Proton/Windows launchers apply ExtraEnvVars last (per-game overrides)
- Rename WinePrefix -> CompatibilityPrefixPath across config, DTOs, models, ViewModels and Views
- Add CompatibilityTool (int) and CompatibilityToolPath to Game entity and DTOs
- Add GameEnvironmentVariable entity + EnvironmentVariableDto
- Add migration AddPerGameCompatibilitySettings
- LaunchOptionsProfile: map CompatibilityPrefixPath

* feat(settings,launch-options): add Wine compatibility section and convert launch options to page

- New Compatibility settings section (WinePath, WinePrefix) visible on Linux only
- Wine detection at startup: WINE env var -> WINEPATH env var -> PATH scan
- Warning notification (GobletBrokenLine) if Wine not found
- LaunchOptionsDialogView/ViewModel replaced by LaunchOptionsView/ViewModel (PageViewModel)
- LaunchOptions promoted to direct button in GameBar (TerminalLine icon)
- Per-game WinePrefix override in LaunchOptionsView, visible on Linux only
- WinePrefix added to LaunchOptionsDto, Game entity and UpdateLaunchOptions
- DB migration: AddGameWinePrefix
- SettingsPageService.PersistCurrentConfigAsync() extracted as reusable method
- GameWithStatsService: WinePath read from IAppConfiguration.Compatibility (no hardcoded fallback)

# Conflicts:
#	src/TombLauncher.Data/Database/Services/GameDataService.cs
#	src/TombLauncher/App.axaml.cs
#	src/TombLauncher/Extensions/ViewModelsServiceCollectionExtensions.cs
#	src/TombLauncher/Factories/Mapping/MappingUtils.cs
#	src/TombLauncher/Factories/Profiles/LaunchOptionsProfile.cs
#	src/TombLauncher/Services/GameDetailsService.cs
#	src/TombLauncher/Services/GameWithStatsService.cs
#	src/TombLauncher/ViewModels/Dialogs/LaunchOptionsDialogViewModel.cs
#	src/TombLauncher/ViewModels/Pages/GameDetailsViewModel.cs
#	src/TombLauncher/Views/GameBar.axaml

* docs: fix add-page workflow view naming convention (View suffix, Pages/ folder)

* loc: simplify Wine-related UI strings for non-technical users

* style(ui): use text-muted class on LabeledTextBox label

* feat(ui): add CardSectionHeader control to LaunchOptionsView

- New CardSectionHeader control in TombLauncher.Controls (IconKind + Title)
- LaunchOptionsView: icon+title header on each card
  (PlayLargeLine/Launch, HammerLine/Setup, GroupLine/CommunityPatch, Goblet2Line/Compatibility)
- Add GENERAL and COMMUNITY_PATCH localization keys (en-US, it-IT)
- Community patch tooltip: 'some versions of TRX' instead of 'TRX'
- LabeledTextBox label now uses text-muted class

* fix(ui,navigation): sticky save button in LaunchOptionsView; fix GameWithStats refresh on back

- LaunchOptionsView: restructured with Grid (ScrollViewer row * + buttons row Auto)
  Save button now uses IconButton btn-primary/Save3Line, always visible outside scroll area
- Fix: GameWithStatsViewModel data not refreshing when navigating back from LaunchOptions

* fix(data): rewrite UpdateLaunchOptions using EF change tracker

Replace ExecuteUpdateAsync/ExecuteDeleteAsync (bulk ops that bypass EF tracking)
with a single tracked ToListAsync query + direct property assignment.
This ensures GetGameById returns fresh data after saving launch options,
fixing the stale executable path shown in GameDetailsViewModel on back navigation.

* feat(stats,settings): implement Wine version detection

- IPlatformSpecificFeatures: add GetWineVersion(string winePath)
- LinuxPlatformSpecificFeatures: runs wine --version, captures stdout
- WindowsPlatformSpecificFeatures: returns null (Wine not supported)
- CompatibilitySettingsViewModel: WineVersion auto-refreshed via OnWinePathChanged
- CompatibilitySettingsView: read-only Wine version row below WINEPREFIX
- StatisticsPageViewModel: WineVersion populated on init via IAppConfiguration.Compatibility.WinePath
- StatisticsPageView: row 7 with Goblet2Line icon, visible only on Linux (IsWineSupported)
- Localization: WINE_VERSION added in en-US and it-IT

* fix(linux): wrap wine launch in bash + wineserver -w for correct process tracking

wine exits immediately after spawning the Windows process into wineserver,
causing play time tracking and save file monitoring to stop prematurely.
Wrapping with 'bash -c "wine ...; wineserver -w"' keeps the monitored process
alive until wineserver shuts down (i.e. until the game truly exits).

* feat(proton): add Proton launcher support with compatibility settings

- Add ProtonGameLauncher: sets STEAM_COMPAT_DATA_PATH (with default),
  STEAM_COMPAT_INSTALL_PATH, STEAM_COMPAT_CLIENT_INSTALL_PATH (derived from
  Proton binary path), STEAM_APPID=0; strips NVIDIA PRIME vars inherited from
  parent process that break Wine rendering; uses bash login shell (-lc)
- Add WineGameLauncher, WindowsGameLauncher, IGameLauncher abstractions
- Add ProtonInstallationDto, CompatibilityTool enum
- Add CompatibilityToolToIconConverter
- LinuxPlatformSpecificFeatures: FindAvailableProtonInstallations(),
  CollectSteamRoots() (parses libraryfolders.vdf), GetProtonVersion()
- CompatibilitySettingsView/ViewModel: Wine/Proton tool selector, Proton
  installation ComboBox with auto-detect from steamapps, manual fallback,
  STEAM_COMPAT_DATA_PATH override, Proton version display
- GameWithStatsService: capture stdout/stderr on non-zero exit (Linux only)
- SavegameCardView: split MARK_UNMARK key into MARK_/UNMARK_ for better i18n,
  use BooleanToStringConverter for context-aware tooltip
- Localization: WINE_VERSION, MARK_AS_START_OF_LEVEL_SAVE,
  UNMARK_AS_START_OF_LEVEL_SAVE added in en-US and it-IT
- Add unit tests for CompatibilityConfig, GameLauncher, LinuxPlatformFeatures

* refactor(settings): remove WinePath from GameDetails config

WinePath was migrated to CompatibilityConfig in a previous session.
Now that the one-time migration path in App.axaml.cs is no longer
needed, remove the residual property from IGameDetailsConfig,
GameDetailsConfig, LayeredAppConfiguration, GameDetailsCoreSettings
and its construction in SettingsProvider.

* fix(settings): compatibility tool change now takes effect without restart

IGameLauncher is now resolved on each game launch via Func<IGameLauncher>
instead of being captured at construction time in GameWithStatsService.

Also refactors InitializeServices into a fluent chain of IServiceCollection
extension methods, each with a single responsibility:
- AddAppConfiguration: loads JSON layers, registers ILayeredAppConfiguration/IAppConfiguration
- AddPlatformSpecificFeatures: registers IPlatformSpecificFeatures, IGameLauncher and its factory
- AddSaveGameManagement: savegame header provider, processor and query/command services
- AddTheming, AddFileOperations, AddLocalization, AddNavigation: focused singletons
- AddGameManagement: installer, engine detector, game merger
- AddPopups: IPopupService
- AddDownloaders: HTTP clients, downloader implementations, download manager
- AddNotifications, AddUpdater: notification and update services
- AddDatabaseAccess now resolves IAppConfiguration from sp internally

* refactor(app): move CheckCompatibilityToolAsync to AppUtils

Eliminates the private wrapper method in App, calling AppUtils directly
from ShowMainWindow. Unused usings (IconPacks, Configuration, Sections)
removed from App.axaml.cs.

* refactor(app): move ApplyInitialSettings to AppUtils

App.axaml.cs now delegates both startup helpers to AppUtils,
leaving it with only wiring/orchestration logic.

* style(app): use unqualified ChangeTheme call within AppUtils

* refactor(app): move OnUnhandledException to AppUtils

Registered directly as AppUtils.OnUnhandledException on the Dispatcher
event — no disadvantages vs instance method in this context (singleton
event source, no instance capture needed).
Removed unused usings from App.axaml.cs.

* refactor(navigation): move INavigationTarget and ITopBarCommand to Contracts, introduce BusyScope pattern

- Move INavigationTarget and ITopBarCommand from TombLauncher.Core.Navigation to TombLauncher.Contracts.Navigation
- ITopBarCommand.Icon changed from PackIconRemixIconKind to Enum to remove icon library dependency from Contracts
- CommandViewModel.Icon likewise changed to Enum
- INavigationTarget simplified: SetBusy(bool,string) removed from public interface, BusyScope(string) introduced as the primary RAII pattern
- SetBusy(string) kept/added for fire-and-forget cases (PlayGame, LaunchSetup, LaunchCommunitySetup)
- GameWithStatsService, StatisticsPageViewModel, GameListViewModel, SavegameListViewModel refactored to BusyScope pattern
- async void Uninstall fixed to async Task
- Unused usings and IoC.Default references removed

* fix(ui): fix BusyMessage not visible in RingLoader overlay

- RingLoader: move Width/Height constraint from UserControl to inner Viewbox,
  so the message TextBlock below the ring is no longer clipped to 80x80 bounds
- RingLoader: restore RelativeSource binding (was working, just clipped)
- GameListViewModel: add missing .GetLocalizedString() on FETCHING_GAMES key
- MainWindow: remove redundant debug TextBlock for BusyMessage

* refactor(compatibility): move CompatibilityTool enum to TombLauncher.Contracts

Moved CompatibilityTool from TombLauncher.Configuration.Sections to
TombLauncher.Contracts.Enums, alongside the other shared enums.

This removes the circular dependency that would have blocked adding
CompatibilityTool? fields to GameMetadataDto (TombLauncher.Core) and
the Game entity (TombLauncher.Data), both of which cannot reference
the main TombLauncher project.

Updated all consumers:
- CompatibilityConfig.cs, ICompatibilityConfig.cs
- LayeredAppConfiguration.cs
- PlatformSpecificFeaturesCollectionExtensions.cs
- CompatibilitySettingsViewModel.cs
- CompatibilityToolToIconConverter.cs
- AppUtils.cs
- CompatibilitySettingsView.axaml (xmlns:cfg alias)

* refactor(compatibility): introduce GameLaunchContext, per-game CompatibilityTool and ExtraEnvVars

- IGameLauncher.GetLaunchStartInfo now takes GameLaunchContext instead of 4 loose params
- GameLaunchContext carries ExecutableFileName, Arguments, WorkingDirectory, PrefixPath, ExtraEnvVars
- Wine/Proton/Windows launchers apply ExtraEnvVars last (per-game overrides)
- Rename WinePrefix -> CompatibilityPrefixPath across config, DTOs, models, ViewModels and Views
- Add CompatibilityTool (int) and CompatibilityToolPath to Game entity and DTOs
- Add GameEnvironmentVariable entity + EnvironmentVariableDto
- Add migration AddPerGameCompatibilitySettings
- LaunchOptionsProfile: map CompatibilityPrefixPath

* fix(compatibility): wire per-game CompatibilityTool, EnvVars and prefix through all layers

- Add IEnvironmentVariable contract and EnvironmentVariableMapper (ToDto/ToEntity)
- Extend IGameMetadata, GameMetadataDto, Game entity with CompatibilityTool,
  CompatibilityToolPath and ExtraEnvVars
- GameMapper: map new fields in both ToDto and ToGame; CompatibilityTool now
  defaults to Unspecified in CompatibilityConfig so layered merge works correctly
- GameDataService.UpdateLaunchOptions: add missing SetupExecutable block, persist
  ExtraEnvVars via EnvironmentVariableMapper.ToEntities, fix wrong executable added
  when GameExecutable backup was absent
- Add .Include(g => g.EnvironmentVariables) to all EF queries that call GameMapper
- GameMetadataViewModel / LaunchOptionsViewModel: expose CompatibilityTool,
  CompatibilityToolPath, ExtraEnvVars; InitFromGame populates them from the game VM
- LaunchOptionsMapper.ToDto: map CompatibilityPrefixPath, CompatibilityTool,
  CompatibilityToolPath, ExtraEnvVars
- GameWithStatsService.LaunchProcess: select launcher from per-game CompatibilityTool
  (falling back to global factory when Unspecified); pass ExtraEnvVars to context
- LayeredAppConfiguration: fix CompatibilityTool merge — check both User and Defaults
  before falling back to Wine; add "CompatibilityTool": "Wine" to appsettings.json
- Register EnvironmentVariableMapper in DI; restore GetGameById on GameDataService
- Update GameLauncherTests to use GameLaunchContext; fix CompatibilityConfigTests using

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(mappers,data): add mapper unit tests and UpdateLaunchOptions integration tests

- EnvironmentVariableMapperTests: ToDto/ToEntity/ToDtos/ToEntities
- GameMapperTests: ToDto and ToGame for compatibility fields and env vars
- LaunchOptionsMapperTests: compatibility fields, ExtraEnvVars, optional executables
- UpdateLaunchOptionsTests (EF Core InMemory): CompatibilityTool/Path/Prefix
  persistence, env var add/replace/clear, setup executable add/update/remove
- SearchMapperTests: TitlePic propagated through ToDto overloads
- Add Microsoft.EntityFrameworkCore.InMemory 10.0.2 to test project

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(platform): add ExpandPath to IPlatformSpecificFeatures for tilde expansion

Linux expands leading ~ to $HOME; Windows returns the path unchanged.
GameWithStatsService applies ExpandPath before passing PrefixPath to the launcher,
so users can type ~/... in the UI and have it resolve correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ui): add website link to sidebar navigation (#111)

Adds a link to tomblauncher.app in the main sidebar, alongside the existing GitHub link. Includes WebsiteLink in the config layer, ApplicationCoreSettings DTO, and both localization files (en-US / it-IT). Also moves CoreSettings.cs to the Dtos/Configuration subfolder.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ui): add About page with acknowledgements (#110) (#112)

* feat(ui): add About page with app info and acknowledgements section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(ui): extract SidebarNavigationItem control to reduce sidebar duplication

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ui): populate About page acknowledgements section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(release): bump version to 1.2.0 (#113)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(patchers): add tombpc.dat gameflow reader and GameflowDto mapper  (#116)

Introduces TombLauncher.Patchers with a complete pipeline for reading
TR2/TR3 tombpc.dat script files and converting them into a UI-friendly
GameflowDto.

- GameflowReader: binary reader for the full tombpc.dat structure
  (header, counts, flags, XOR key, string arrays, sequences, item strings)
- GameflowMapper: converts Gameflow → GameflowDto, decoding XOR-encrypted
  strings, splitting sequences per level, and mapping BAM angles to degrees
- Shared models: GameflowDto, LevelInfo, SequenceInfo, Tr2Item, Tr3Item
- TRX skeleton: TrxGameflow, Level, SequenceItem models and patcher stub
- Utilities: AngleExtensions (BAM↔degrees), SplitAt, FromGameTicks
- Tests: full reader coverage on two custom levels (Silver Machine TR2,
  Escapade in Utah TR3) and mapper coverage

* docs(readme): add screenshots section from tomblauncher.app (#117)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ai): add AI-assisted troubleshooting (Laura)

Introduces an experimental AI chat feature ("Talk to Laura") backed by a
RAG pipeline, disabled by default and hidden from the sidebar when off.

Core AI pipeline
- RagService: streaming RAG with Semantic Kernel, per-request Kernel.Clone
  for thread safety, documentation injected into user message (not system
  prompt) to avoid accumulation across turns
- VectorSearchService: sqlite-vec vector search with distance threshold 0.20
- GameDiagnosticsPlugin: SK plugin exposing crash log, exit code and engine
  info to the model via auto function calling
- TroubleshootingContextService: builds per-game context from the database
- Support for Ollama and LM Studio backends (OllamaBackendService,
  LmStudioBackendService) with endpoint normalization and model listing

Settings UI
- AiSettingsView: backend picker, endpoint/embedding model health checks
  with flashing indicator, dynamic model list via ComboBox, KB update button
- ServiceCheckView: reusable status indicator extracted from duplicated code

Chat UI
- AiChatView: streaming message bubbles, tool-use indicator, empty state,
  auto-scroll, Enter to send; Laura menu item hidden when AI disabled
- Open chat from game details page with pre-loaded troubleshooting context

Knowledge base
- KnowledgeBase.Embedder: console tool that chunks markdown docs, generates
  embeddings via OpenAI-compatible API and writes sqlite-vec DB
- SFTP upload (--upload flag): SHA-256 manifest, atomic rename on server,
  private key or password auth
- KbUpdateService: client-side auto-update on startup — fetches manifest,
  checks schema version and generatedAt, downloads + verifies + swaps DB
- CryptoUtils: unified MD5/SHA-256 helpers (renamed from Md5Utils)

Bug fixes
- Fix sync/async stream mix crash on game exit (BeginOutputReadLine +
  ReadToEndAsync incompatibility)
- Fix IHttpClientFactory CI build failure (HintPath → PackageReference)

* feat(platform): add game support matrix (#89) (#122)

Introduces a per-OS engine support matrix to IPlatformSpecificFeatures:

- Add EngineSupportState enum (NoSupport, SupportedWithCompatibilityLayer,
  NativePatchingAvailable, FullSupport)
- Add ISupportMatrix interface with IReadOnlyDictionary<GameEngine, EngineSupportState>
- Implement LinuxSupportMatrix, WindowsSupportMatrix, MacOsSupportMatrix
- Wire ISupportMatrix into IPlatformSpecificFeatures and both platform implementations
- Expose EngineSupportState in GameDetailsViewModel via GameDetailsService
- Show a status icon in GameBar with danger class for unsupported engines
- Add EngineSupportStateToIconConverter and EngineSupportStateToTooltipConverter
- Add localization strings for all four states (en-US, it-IT)

* fix(launch-options): populate executables correctly and handle IO errors (#124)

Use GetEnumerationOptions() (case-insensitive, skips reparse points) instead
of SearchOption.AllDirectories when scanning the install directory, fixing
controls not populated on page open. Catch IOException in LoadAsync and show
a localized error messagebox before navigating back. Remove orphan
GetGameLaunchStartInfo from LinuxPlatformSpecificFeatures.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ai): integrate support matrix into SK as a plugin (#125)

Add SupportMatrixPlugin exposing ISupportMatrix to the Semantic Kernel
so the AI can report engine support state during troubleshooting.
Adopt null-object pattern on TroubleshootingContext (IsSet sentinel) to
remove nullable parameters from plugin constructors and AskAsync.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(readme): document AI-assisted troubleshooting feature (Laura) (#… (#126)

docs(readme): document AI-assisted troubleshooting feature (Laura) (#121)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ai): correct remote KB manifest URL (#128)

The manifest file is at kb_manifest.json, not manifest.json.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(patchers): add widescreen patcher with backup/restore pipeline (#129)

* feat(patchers): port widescreen aspect ratio patcher from old branch

Brings over the widescreen patcher logic (contracts, patcher, test asset)
originally developed on feature/patchers, adjusted for the src/tests layout.
IPatcher.EngineDetector still references TombRaiderEngineDetector — needs
resolution before the project compiles (see issue #88).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(patchers): move EngineDetectorResult to Contracts, wire IEngineDetector

- EngineDetectorResult moved…
* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists (#76)

Allows re-running the release pipeline without failing on duplicate tags.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix deploy-appcast job

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Master (#85)

* Release/1.1.0 (#77)

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): fix appcast-deploy job

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): fix deploy-appcast job

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): fix deploy-appcast job

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix AppImage filename and remove space from portable ZIP name

- AppImage: add package release suffix (-1) to match PupNet output
  (tomb-launcher-{version}-1.x86_64.AppImage)
- Portable ZIP: use hyphens instead of spaces to avoid glob and shell
  quoting issues throughout the pipeline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): replace placeholder AppDescription in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): fix PublisherLinkUrl in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): clean up pupnet.conf metadata

- SetupMinWindowsVersion: 6.2 -> 10 (.NET 10 requires Windows 10+)
- PublisherLinkName: use display name instead of URL
- SetupCommandPrompt: clear (GUI app, no CLI entry needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): fix deploy-appcast job

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix AppImage filename and remove space from portable ZIP name

- AppImage: add package release suffix (-1) to match PupNet output
  (tomb-launcher-{version}-1.x86_64.AppImage)
- Portable ZIP: use hyphens instead of spaces to avoid glob and shell
  quoting issues throughout the pipeline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): replace placeholder AppDescription in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): fix PublisherLinkUrl in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): clean up pupnet.conf metadata

- SetupMinWindowsVersion: 6.2 -> 10 (.NET 10 requires Windows 10+)
- PublisherLinkName: use display name instead of URL
- SetupCommandPrompt: clear (GUI app, no CLI entry needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(ci): use bash array for appcast common args

Reduces code duplication and avoids long backslash continuation chains
that may cause parsing issues on certain runners.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* Release/1.1.0 (#83)

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix AppImage filename and remove space from portable ZIP name

- AppImage: add package release suffix (-1) to match PupNet output
  (tomb-launcher-{version}-1.x86_64.AppImage)
- Portable ZIP: use hyphens instead of spaces to avoid glob and shell
  quoting issues throughout the pipeline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): replace placeholder AppDescription in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): fix PublisherLinkUrl in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): clean up pupnet.conf metadata

- SetupMinWindowsVersion: 6.2 -> 10 (.NET 10 requires Windows 10+)
- PublisherLinkName: use display name instead of URL
- SetupCommandPrompt: clear (GUI app, no CLI entry needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(ci): use bash array for appcast common args

Reduces code duplication and avoids long backslash continuation chains
that may cause parsing issues on certain runners.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix scp port flag and ssh-keyscan port for non-standard SSH port

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* Release/1.1.0 (#84)

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix AppImage filename and remove space from portable ZIP name

- AppImage: add package release suffix (-1) to match PupNet output
  (tomb-launcher-{version}-1.x86_64.AppImage)
- Portable ZIP: use hyphens instead of spaces to avoid glob and shell
  quoting issues throughout the pipeline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): replace placeholder AppDescription in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): fix PublisherLinkUrl in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): clean up pupnet.conf metadata

- SetupMinWindowsVersion: 6.2 -> 10 (.NET 10 requires Windows 10+)
- PublisherLinkName: use display name instead of URL
- SetupCommandPrompt: clear (GUI app, no CLI entry needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(ci): use bash array for appcast common args

Reduces code duplication and avoids long backslash continuation chains
that may cause parsing issues on certain runners.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix scp port flag and ssh-keyscan port for non-standard SSH port

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): upload appcast.xml.signature alongside appcast.xml

NetSparkle in Strict mode verifies the appcast using the .signature
file — without it, the signature check fails and no updates are detected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* refactor: migrate away from automapper

* chore(deps): add Riok.Mapperly 4.3.1

Initial step of the AutoMapper → Mapperly migration tracked on this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: remove unused PlaySessionDto

PlaySessionDto and its two consumer methods on PlaySessionDataService
(GetLastPlaySession, GetPlaySessionsByGameId) were not called by anyone.
Drops one mapping pair from the AutoMapper → Mapperly migration scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "chore(deps): add Riok.Mapperly 4.3.1"

This reverts commit 31847b590b1841fa7fecc38db8440d5348eea7ea.

* refactor(mapping): switch language mapping from AutoMapper to manual

First step of the migration to manual mapping. AvailableLanguageDto ↔
ApplicationLanguageViewModel is now handled by a SettingsMapper instance
class (registered as DI singleton) instead of AutoMapper. The two unused
dead methods on SettingsPageService (GetApplicationLanguage,
GetApplicationTheme) are dropped along the way.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): complete SettingsMapper migration

Migrates DownloaderConfiguration ↔ DownloaderViewModel and
UnzipBackendDto ↔ UnzipBackendViewModel to manual mapping in
SettingsMapper. SettingsProfile is now empty and deleted.
DownloaderSettingsViewModel drops MapperConfiguration injection
in favour of SettingsMapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate AppCrash mapping to manual mapper

Introduces TombLauncher.Data/Mapping/ as the location for entity↔DTO
mappers. AppCrashMapper lives there, keeping entities contained within
the data layer. AppCrashProfile deleted, SettingsPageViewModel drops
its MapperConfiguration dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate statistics mapping to manual mapper

Introduces StatisticsMapper for the three statistics mappings.
DailyStatisticsAverageGameDurationViewModel renamed to
DailyGameDurationViewModel (used for both average and total),
its AverageGameDuration property renamed to GameDuration.
DailyStatisticsViewModel deleted (superseded by DailyGameDurationViewModel).
StatisticsProfile retains only the SavegameViewModel mapping (Fase 5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate GameLink mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate GameHash mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate FileBackup mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate Game and GameMetadata mapping to manual mappers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate DownloaderSearchPayload mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate LaunchOptions mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate GameWithStats mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate search result mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate savegame and FileBackup mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: update CLAUDE.md to reflect manual mapper convention

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: remove stale TombLauncher.Mapping using directive

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(deps): add Riok.Mapperly 4.3.1

Initial step of the AutoMapper → Mapperly migration tracked on this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: remove unused PlaySessionDto

PlaySessionDto and its two consumer methods on PlaySessionDataService
(GetLastPlaySession, GetPlaySessionsByGameId) were not called by anyone.
Drops one mapping pair from the AutoMapper → Mapperly migration scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "chore(deps): add Riok.Mapperly 4.3.1"

This reverts commit 31847b590b1841fa7fecc38db8440d5348eea7ea.

* refactor(mapping): switch language mapping from AutoMapper to manual

First step of the migration to manual mapping. AvailableLanguageDto ↔
ApplicationLanguageViewModel is now handled by a SettingsMapper instance
class (registered as DI singleton) instead of AutoMapper. The two unused
dead methods on SettingsPageService (GetApplicationLanguage,
GetApplicationTheme) are dropped along the way.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): complete SettingsMapper migration

Migrates DownloaderConfiguration ↔ DownloaderViewModel and
UnzipBackendDto ↔ UnzipBackendViewModel to manual mapping in
SettingsMapper. SettingsProfile is now empty and deleted.
DownloaderSettingsViewModel drops MapperConfiguration injection
in favour of SettingsMapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate AppCrash mapping to manual mapper

Introduces TombLauncher.Data/Mapping/ as the location for entity↔DTO
mappers. AppCrashMapper lives there, keeping entities contained within
the data layer. AppCrashProfile deleted, SettingsPageViewModel drops
its MapperConfiguration dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate statistics mapping to manual mapper

Introduces StatisticsMapper for the three statistics mappings.
DailyStatisticsAverageGameDurationViewModel renamed to
DailyGameDurationViewModel (used for both average and total),
its AverageGameDuration property renamed to GameDuration.
DailyStatisticsViewModel deleted (superseded by DailyGameDurationViewModel).
StatisticsProfile retains only the SavegameViewModel mapping (Fase 5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate GameLink mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate GameHash mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate FileBackup mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate Game and GameMetadata mapping to manual mappers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate DownloaderSearchPayload mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate LaunchOptions mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate GameWithStats mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate search result mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate savegame and FileBackup mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: update CLAUDE.md to reflect manual mapper convention

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: remove stale TombLauncher.Mapping using directive

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Update src/TombLauncher.Data/Mapping/GameLinkMapper.cs

Co-authored-by: codefactor-io[bot] <47775046+codefactor-io[bot]@users.noreply.github.com>

* Update src/TombLauncher/Mappers/LaunchOptionsMapper.cs

Co-authored-by: codefactor-io[bot] <47775046+codefactor-io[bot]@users.noreply.github.com>

* chore: align namespace and remove stale TombLauncher.Mapping usings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: remove AutoMapper configuration test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: codefactor-io[bot] <47775046+codefactor-io[bot]@users.noreply.github.com>

* feat(compatibility): add Proton support and per-game launcher settings (#109)

* feat(settings,launch-options): add Wine compatibility section and convert launch options to page

- New Compatibility settings section (WinePath, WinePrefix) visible on Linux only
- Wine detection at startup: WINE env var -> WINEPATH env var -> PATH scan
- Warning notification (GobletBrokenLine) if Wine not found
- LaunchOptionsDialogView/ViewModel replaced by LaunchOptionsView/ViewModel (PageViewModel)
- LaunchOptions promoted to direct button in GameBar (TerminalLine icon)
- Per-game WinePrefix override in LaunchOptionsView, visible on Linux only
- WinePrefix added to LaunchOptionsDto, Game entity and UpdateLaunchOptions
- DB migration: AddGameWinePrefix
- SettingsPageService.PersistCurrentConfigAsync() extracted as reusable method
- GameWithStatsService: WinePath read from IAppConfiguration.Compatibility (no hardcoded fallback)

* docs: fix add-page workflow view naming convention (View suffix, Pages/ folder)

* loc: simplify Wine-related UI strings for non-technical users

* style(ui): use text-muted class on LabeledTextBox label

* feat(ui): add CardSectionHeader control to LaunchOptionsView

- New CardSectionHeader control in TombLauncher.Controls (IconKind + Title)
- LaunchOptionsView: icon+title header on each card
  (PlayLargeLine/Launch, HammerLine/Setup, GroupLine/CommunityPatch, Goblet2Line/Compatibility)
- Add GENERAL and COMMUNITY_PATCH localization keys (en-US, it-IT)
- Community patch tooltip: 'some versions of TRX' instead of 'TRX'
- LabeledTextBox label now uses text-muted class

* fix(ui,navigation): sticky save button in LaunchOptionsView; fix GameWithStats refresh on back

- LaunchOptionsView: restructured with Grid (ScrollViewer row * + buttons row Auto)
  Save button now uses IconButton btn-primary/Save3Line, always visible outside scroll area
- Fix: GameWithStatsViewModel data not refreshing when navigating back from LaunchOptions

* fix(data): rewrite UpdateLaunchOptions using EF change tracker

Replace ExecuteUpdateAsync/ExecuteDeleteAsync (bulk ops that bypass EF tracking)
with a single tracked ToListAsync query + direct property assignment.
This ensures GetGameById returns fresh data after saving launch options,
fixing the stale executable path shown in GameDetailsViewModel on back navigation.

* feat(stats,settings): implement Wine version detection

- IPlatformSpecificFeatures: add GetWineVersion(string winePath)
- LinuxPlatformSpecificFeatures: runs wine --version, captures stdout
- WindowsPlatformSpecificFeatures: returns null (Wine not supported)
- CompatibilitySettingsViewModel: WineVersion auto-refreshed via OnWinePathChanged
- CompatibilitySettingsView: read-only Wine version row below WINEPREFIX
- StatisticsPageViewModel: WineVersion populated on init via IAppConfiguration.Compatibility.WinePath
- StatisticsPageView: row 7 with Goblet2Line icon, visible only on Linux (IsWineSupported)
- Localization: WINE_VERSION added in en-US and it-IT

* fix(linux): wrap wine launch in bash + wineserver -w for correct process tracking

wine exits immediately after spawning the Windows process into wineserver,
causing play time tracking and save file monitoring to stop prematurely.
Wrapping with 'bash -c "wine ...; wineserver -w"' keeps the monitored process
alive until wineserver shuts down (i.e. until the game truly exits).

* feat(proton): add Proton launcher support with compatibility settings

- Add ProtonGameLauncher: sets STEAM_COMPAT_DATA_PATH (with default),
  STEAM_COMPAT_INSTALL_PATH, STEAM_COMPAT_CLIENT_INSTALL_PATH (derived from
  Proton binary path), STEAM_APPID=0; strips NVIDIA PRIME vars inherited from
  parent process that break Wine rendering; uses bash login shell (-lc)
- Add WineGameLauncher, WindowsGameLauncher, IGameLauncher abstractions
- Add ProtonInstallationDto, CompatibilityTool enum
- Add CompatibilityToolToIconConverter
- LinuxPlatformSpecificFeatures: FindAvailableProtonInstallations(),
  CollectSteamRoots() (parses libraryfolders.vdf), GetProtonVersion()
- CompatibilitySettingsView/ViewModel: Wine/Proton tool selector, Proton
  installation ComboBox with auto-detect from steamapps, manual fallback,
  STEAM_COMPAT_DATA_PATH override, Proton version display
- GameWithStatsService: capture stdout/stderr on non-zero exit (Linux only)
- SavegameCardView: split MARK_UNMARK key into MARK_/UNMARK_ for better i18n,
  use BooleanToStringConverter for context-aware tooltip
- Localization: WINE_VERSION, MARK_AS_START_OF_LEVEL_SAVE,
  UNMARK_AS_START_OF_LEVEL_SAVE added in en-US and it-IT
- Add unit tests for CompatibilityConfig, GameLauncher, LinuxPlatformFeatures

* refactor(settings): remove WinePath from GameDetails config

WinePath was migrated to CompatibilityConfig in a previous session.
Now that the one-time migration path in App.axaml.cs is no longer
needed, remove the residual property from IGameDetailsConfig,
GameDetailsConfig, LayeredAppConfiguration, GameDetailsCoreSettings
and its construction in SettingsProvider.

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* fix(settings): compatibility tool change now takes effect without restart

IGameLauncher is now resolved on each game launch via Func<IGameLauncher>
instead of being captured at construction time in GameWithStatsService.

Also refactors InitializeServices into a fluent chain of IServiceCollection
extension methods, each with a single responsibility:
- AddAppConfiguration: loads JSON layers, registers ILayeredAppConfiguration/IAppConfiguration
- AddPlatformSpecificFeatures: registers IPlatformSpecificFeatures, IGameLauncher and its factory
- AddSaveGameManagement: savegame header provider, processor and query/command services
- AddTheming, AddFileOperations, AddLocalization, AddNavigation: focused singletons
- AddGameManagement: installer, engine detector, game merger
- AddPopups: IPopupService
- AddDownloaders: HTTP clients, downloader implementations, download manager
- AddNotifications, AddUpdater: notification and update services
- AddDatabaseAccess now resolves IAppConfiguration from sp internally

* refactor(app): move CheckCompatibilityToolAsync to AppUtils

Eliminates the private wrapper method in App, calling AppUtils directly
from ShowMainWindow. Unused usings (IconPacks, Configuration, Sections)
removed from App.axaml.cs.

* refactor(app): move ApplyInitialSettings to AppUtils

App.axaml.cs now delegates both startup helpers to AppUtils,
leaving it with only wiring/orchestration logic.

* style(app): use unqualified ChangeTheme call within AppUtils

* refactor(app): move OnUnhandledException to AppUtils

Registered directly as AppUtils.OnUnhandledException on the Dispatcher
event — no disadvantages vs instance method in this context (singleton
event source, no instance capture needed).
Removed unused usings from App.axaml.cs.

* refactor(navigation): move INavigationTarget and ITopBarCommand to Contracts, introduce BusyScope pattern

- Move INavigationTarget and ITopBarCommand from TombLauncher.Core.Navigation to TombLauncher.Contracts.Navigation
- ITopBarCommand.Icon changed from PackIconRemixIconKind to Enum to remove icon library dependency from Contracts
- CommandViewModel.Icon likewise changed to Enum
- INavigationTarget simplified: SetBusy(bool,string) removed from public interface, BusyScope(string) introduced as the primary RAII pattern
- SetBusy(string) kept/added for fire-and-forget cases (PlayGame, LaunchSetup, LaunchCommunitySetup)
- GameWithStatsService, StatisticsPageViewModel, GameListViewModel, SavegameListViewModel refactored to BusyScope pattern
- async void Uninstall fixed to async Task
- Unused usings and IoC.Default references removed

* fix(ui): fix BusyMessage not visible in RingLoader overlay

- RingLoader: move Width/Height constraint from UserControl to inner Viewbox,
  so the message TextBlock below the ring is no longer clipped to 80x80 bounds
- RingLoader: restore RelativeSource binding (was working, just clipped)
- GameListViewModel: add missing .GetLocalizedString() on FETCHING_GAMES key
- MainWindow: remove redundant debug TextBlock for BusyMessage

* refactor(compatibility): move CompatibilityTool enum to TombLauncher.Contracts

Moved CompatibilityTool from TombLauncher.Configuration.Sections to
TombLauncher.Contracts.Enums, alongside the other shared enums.

This removes the circular dependency that would have blocked adding
CompatibilityTool? fields to GameMetadataDto (TombLauncher.Core) and
the Game entity (TombLauncher.Data), both of which cannot reference
the main TombLauncher project.

Updated all consumers:
- CompatibilityConfig.cs, ICompatibilityConfig.cs
- LayeredAppConfiguration.cs
- PlatformSpecificFeaturesCollectionExtensions.cs
- CompatibilitySettingsViewModel.cs
- CompatibilityToolToIconConverter.cs
- AppUtils.cs
- CompatibilitySettingsView.axaml (xmlns:cfg alias)

* refactor(compatibility): introduce GameLaunchContext, per-game CompatibilityTool and ExtraEnvVars

- IGameLauncher.GetLaunchStartInfo now takes GameLaunchContext instead of 4 loose params
- GameLaunchContext carries ExecutableFileName, Arguments, WorkingDirectory, PrefixPath, ExtraEnvVars
- Wine/Proton/Windows launchers apply ExtraEnvVars last (per-game overrides)
- Rename WinePrefix -> CompatibilityPrefixPath across config, DTOs, models, ViewModels and Views
- Add CompatibilityTool (int) and CompatibilityToolPath to Game entity and DTOs
- Add GameEnvironmentVariable entity + EnvironmentVariableDto
- Add migration AddPerGameCompatibilitySettings
- LaunchOptionsProfile: map CompatibilityPrefixPath

* feat(settings,launch-options): add Wine compatibility section and convert launch options to page

- New Compatibility settings section (WinePath, WinePrefix) visible on Linux only
- Wine detection at startup: WINE env var -> WINEPATH env var -> PATH scan
- Warning notification (GobletBrokenLine) if Wine not found
- LaunchOptionsDialogView/ViewModel replaced by LaunchOptionsView/ViewModel (PageViewModel)
- LaunchOptions promoted to direct button in GameBar (TerminalLine icon)
- Per-game WinePrefix override in LaunchOptionsView, visible on Linux only
- WinePrefix added to LaunchOptionsDto, Game entity and UpdateLaunchOptions
- DB migration: AddGameWinePrefix
- SettingsPageService.PersistCurrentConfigAsync() extracted as reusable method
- GameWithStatsService: WinePath read from IAppConfiguration.Compatibility (no hardcoded fallback)

# Conflicts:
#	src/TombLauncher.Data/Database/Services/GameDataService.cs
#	src/TombLauncher/App.axaml.cs
#	src/TombLauncher/Extensions/ViewModelsServiceCollectionExtensions.cs
#	src/TombLauncher/Factories/Mapping/MappingUtils.cs
#	src/TombLauncher/Factories/Profiles/LaunchOptionsProfile.cs
#	src/TombLauncher/Services/GameDetailsService.cs
#	src/TombLauncher/Services/GameWithStatsService.cs
#	src/TombLauncher/ViewModels/Dialogs/LaunchOptionsDialogViewModel.cs
#	src/TombLauncher/ViewModels/Pages/GameDetailsViewModel.cs
#	src/TombLauncher/Views/GameBar.axaml

* docs: fix add-page workflow view naming convention (View suffix, Pages/ folder)

* loc: simplify Wine-related UI strings for non-technical users

* style(ui): use text-muted class on LabeledTextBox label

* feat(ui): add CardSectionHeader control to LaunchOptionsView

- New CardSectionHeader control in TombLauncher.Controls (IconKind + Title)
- LaunchOptionsView: icon+title header on each card
  (PlayLargeLine/Launch, HammerLine/Setup, GroupLine/CommunityPatch, Goblet2Line/Compatibility)
- Add GENERAL and COMMUNITY_PATCH localization keys (en-US, it-IT)
- Community patch tooltip: 'some versions of TRX' instead of 'TRX'
- LabeledTextBox label now uses text-muted class

* fix(ui,navigation): sticky save button in LaunchOptionsView; fix GameWithStats refresh on back

- LaunchOptionsView: restructured with Grid (ScrollViewer row * + buttons row Auto)
  Save button now uses IconButton btn-primary/Save3Line, always visible outside scroll area
- Fix: GameWithStatsViewModel data not refreshing when navigating back from LaunchOptions

* fix(data): rewrite UpdateLaunchOptions using EF change tracker

Replace ExecuteUpdateAsync/ExecuteDeleteAsync (bulk ops that bypass EF tracking)
with a single tracked ToListAsync query + direct property assignment.
This ensures GetGameById returns fresh data after saving launch options,
fixing the stale executable path shown in GameDetailsViewModel on back navigation.

* feat(stats,settings): implement Wine version detection

- IPlatformSpecificFeatures: add GetWineVersion(string winePath)
- LinuxPlatformSpecificFeatures: runs wine --version, captures stdout
- WindowsPlatformSpecificFeatures: returns null (Wine not supported)
- CompatibilitySettingsViewModel: WineVersion auto-refreshed via OnWinePathChanged
- CompatibilitySettingsView: read-only Wine version row below WINEPREFIX
- StatisticsPageViewModel: WineVersion populated on init via IAppConfiguration.Compatibility.WinePath
- StatisticsPageView: row 7 with Goblet2Line icon, visible only on Linux (IsWineSupported)
- Localization: WINE_VERSION added in en-US and it-IT

* fix(linux): wrap wine launch in bash + wineserver -w for correct process tracking

wine exits immediately after spawning the Windows process into wineserver,
causing play time tracking and save file monitoring to stop prematurely.
Wrapping with 'bash -c "wine ...; wineserver -w"' keeps the monitored process
alive until wineserver shuts down (i.e. until the game truly exits).

* feat(proton): add Proton launcher support with compatibility settings

- Add ProtonGameLauncher: sets STEAM_COMPAT_DATA_PATH (with default),
  STEAM_COMPAT_INSTALL_PATH, STEAM_COMPAT_CLIENT_INSTALL_PATH (derived from
  Proton binary path), STEAM_APPID=0; strips NVIDIA PRIME vars inherited from
  parent process that break Wine rendering; uses bash login shell (-lc)
- Add WineGameLauncher, WindowsGameLauncher, IGameLauncher abstractions
- Add ProtonInstallationDto, CompatibilityTool enum
- Add CompatibilityToolToIconConverter
- LinuxPlatformSpecificFeatures: FindAvailableProtonInstallations(),
  CollectSteamRoots() (parses libraryfolders.vdf), GetProtonVersion()
- CompatibilitySettingsView/ViewModel: Wine/Proton tool selector, Proton
  installation ComboBox with auto-detect from steamapps, manual fallback,
  STEAM_COMPAT_DATA_PATH override, Proton version display
- GameWithStatsService: capture stdout/stderr on non-zero exit (Linux only)
- SavegameCardView: split MARK_UNMARK key into MARK_/UNMARK_ for better i18n,
  use BooleanToStringConverter for context-aware tooltip
- Localization: WINE_VERSION, MARK_AS_START_OF_LEVEL_SAVE,
  UNMARK_AS_START_OF_LEVEL_SAVE added in en-US and it-IT
- Add unit tests for CompatibilityConfig, GameLauncher, LinuxPlatformFeatures

* refactor(settings): remove WinePath from GameDetails config

WinePath was migrated to CompatibilityConfig in a previous session.
Now that the one-time migration path in App.axaml.cs is no longer
needed, remove the residual property from IGameDetailsConfig,
GameDetailsConfig, LayeredAppConfiguration, GameDetailsCoreSettings
and its construction in SettingsProvider.

* fix(settings): compatibility tool change now takes effect without restart

IGameLauncher is now resolved on each game launch via Func<IGameLauncher>
instead of being captured at construction time in GameWithStatsService.

Also refactors InitializeServices into a fluent chain of IServiceCollection
extension methods, each with a single responsibility:
- AddAppConfiguration: loads JSON layers, registers ILayeredAppConfiguration/IAppConfiguration
- AddPlatformSpecificFeatures: registers IPlatformSpecificFeatures, IGameLauncher and its factory
- AddSaveGameManagement: savegame header provider, processor and query/command services
- AddTheming, AddFileOperations, AddLocalization, AddNavigation: focused singletons
- AddGameManagement: installer, engine detector, game merger
- AddPopups: IPopupService
- AddDownloaders: HTTP clients, downloader implementations, download manager
- AddNotifications, AddUpdater: notification and update services
- AddDatabaseAccess now resolves IAppConfiguration from sp internally

* refactor(app): move CheckCompatibilityToolAsync to AppUtils

Eliminates the private wrapper method in App, calling AppUtils directly
from ShowMainWindow. Unused usings (IconPacks, Configuration, Sections)
removed from App.axaml.cs.

* refactor(app): move ApplyInitialSettings to AppUtils

App.axaml.cs now delegates both startup helpers to AppUtils,
leaving it with only wiring/orchestration logic.

* style(app): use unqualified ChangeTheme call within AppUtils

* refactor(app): move OnUnhandledException to AppUtils

Registered directly as AppUtils.OnUnhandledException on the Dispatcher
event — no disadvantages vs instance method in this context (singleton
event source, no instance capture needed).
Removed unused usings from App.axaml.cs.

* refactor(navigation): move INavigationTarget and ITopBarCommand to Contracts, introduce BusyScope pattern

- Move INavigationTarget and ITopBarCommand from TombLauncher.Core.Navigation to TombLauncher.Contracts.Navigation
- ITopBarCommand.Icon changed from PackIconRemixIconKind to Enum to remove icon library dependency from Contracts
- CommandViewModel.Icon likewise changed to Enum
- INavigationTarget simplified: SetBusy(bool,string) removed from public interface, BusyScope(string) introduced as the primary RAII pattern
- SetBusy(string) kept/added for fire-and-forget cases (PlayGame, LaunchSetup, LaunchCommunitySetup)
- GameWithStatsService, StatisticsPageViewModel, GameListViewModel, SavegameListViewModel refactored to BusyScope pattern
- async void Uninstall fixed to async Task
- Unused usings and IoC.Default references removed

* fix(ui): fix BusyMessage not visible in RingLoader overlay

- RingLoader: move Width/Height constraint from UserControl to inner Viewbox,
  so the message TextBlock below the ring is no longer clipped to 80x80 bounds
- RingLoader: restore RelativeSource binding (was working, just clipped)
- GameListViewModel: add missing .GetLocalizedString() on FETCHING_GAMES key
- MainWindow: remove redundant debug TextBlock for BusyMessage

* refactor(compatibility): move CompatibilityTool enum to TombLauncher.Contracts

Moved CompatibilityTool from TombLauncher.Configuration.Sections to
TombLauncher.Contracts.Enums, alongside the other shared enums.

This removes the circular dependency that would have blocked adding
CompatibilityTool? fields to GameMetadataDto (TombLauncher.Core) and
the Game entity (TombLauncher.Data), both of which cannot reference
the main TombLauncher project.

Updated all consumers:
- CompatibilityConfig.cs, ICompatibilityConfig.cs
- LayeredAppConfiguration.cs
- PlatformSpecificFeaturesCollectionExtensions.cs
- CompatibilitySettingsViewModel.cs
- CompatibilityToolToIconConverter.cs
- AppUtils.cs
- CompatibilitySettingsView.axaml (xmlns:cfg alias)

* refactor(compatibility): introduce GameLaunchContext, per-game CompatibilityTool and ExtraEnvVars

- IGameLauncher.GetLaunchStartInfo now takes GameLaunchContext instead of 4 loose params
- GameLaunchContext carries ExecutableFileName, Arguments, WorkingDirectory, PrefixPath, ExtraEnvVars
- Wine/Proton/Windows launchers apply ExtraEnvVars last (per-game overrides)
- Rename WinePrefix -> CompatibilityPrefixPath across config, DTOs, models, ViewModels and Views
- Add CompatibilityTool (int) and CompatibilityToolPath to Game entity and DTOs
- Add GameEnvironmentVariable entity + EnvironmentVariableDto
- Add migration AddPerGameCompatibilitySettings
- LaunchOptionsProfile: map CompatibilityPrefixPath

* fix(compatibility): wire per-game CompatibilityTool, EnvVars and prefix through all layers

- Add IEnvironmentVariable contract and EnvironmentVariableMapper (ToDto/ToEntity)
- Extend IGameMetadata, GameMetadataDto, Game entity with CompatibilityTool,
  CompatibilityToolPath and ExtraEnvVars
- GameMapper: map new fields in both ToDto and ToGame; CompatibilityTool now
  defaults to Unspecified in CompatibilityConfig so layered merge works correctly
- GameDataService.UpdateLaunchOptions: add missing SetupExecutable block, persist
  ExtraEnvVars via EnvironmentVariableMapper.ToEntities, fix wrong executable added
  when GameExecutable backup was absent
- Add .Include(g => g.EnvironmentVariables) to all EF queries that call GameMapper
- GameMetadataViewModel / LaunchOptionsViewModel: expose CompatibilityTool,
  CompatibilityToolPath, ExtraEnvVars; InitFromGame populates them from the game VM
- LaunchOptionsMapper.ToDto: map CompatibilityPrefixPath, CompatibilityTool,
  CompatibilityToolPath, ExtraEnvVars
- GameWithStatsService.LaunchProcess: select launcher from per-game CompatibilityTool
  (falling back to global factory when Unspecified); pass ExtraEnvVars to context
- LayeredAppConfiguration: fix CompatibilityTool merge — check both User and Defaults
  before falling back to Wine; add "CompatibilityTool": "Wine" to appsettings.json
- Register EnvironmentVariableMapper in DI; restore GetGameById on GameDataService
- Update GameLauncherTests to use GameLaunchContext; fix CompatibilityConfigTests using

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(mappers,data): add mapper unit tests and UpdateLaunchOptions integration tests

- EnvironmentVariableMapperTests: ToDto/ToEntity/ToDtos/ToEntities
- GameMapperTests: ToDto and ToGame for compatibility fields and env vars
- LaunchOptionsMapperTests: compatibility fields, ExtraEnvVars, optional executables
- UpdateLaunchOptionsTests (EF Core InMemory): CompatibilityTool/Path/Prefix
  persistence, env var add/replace/clear, setup executable add/update/remove
- SearchMapperTests: TitlePic propagated through ToDto overloads
- Add Microsoft.EntityFrameworkCore.InMemory 10.0.2 to test project

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(platform): add ExpandPath to IPlatformSpecificFeatures for tilde expansion

Linux expands leading ~ to $HOME; Windows returns the path unchanged.
GameWithStatsService applies ExpandPath before passing PrefixPath to the launcher,
so users can type ~/... in the UI and have it resolve correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ui): add website link to sidebar navigation (#111)

Adds a link to tomblauncher.app in the main sidebar, alongside the existing GitHub link. Includes WebsiteLink in the config layer, ApplicationCoreSettings DTO, and both localization files (en-US / it-IT). Also moves CoreSettings.cs to the Dtos/Configuration subfolder.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ui): add About page with acknowledgements (#110) (#112)

* feat(ui): add About page with app info and acknowledgements section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(ui): extract SidebarNavigationItem control to reduce sidebar duplication

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ui): populate About page acknowledgements section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(release): bump version to 1.2.0 (#113)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(patchers): add tombpc.dat gameflow reader and GameflowDto mapper  (#116)

Introduces TombLauncher.Patchers with a complete pipeline for reading
TR2/TR3 tombpc.dat script files and converting them into a UI-friendly
GameflowDto.

- GameflowReader: binary reader for the full tombpc.dat structure
  (header, counts, flags, XOR key, string arrays, sequences, item strings)
- GameflowMapper: converts Gameflow → GameflowDto, decoding XOR-encrypted
  strings, splitting sequences per level, and mapping BAM angles to degrees
- Shared models: GameflowDto, LevelInfo, SequenceInfo, Tr2Item, Tr3Item
- TRX skeleton: TrxGameflow, Level, SequenceItem models and patcher stub
- Utilities: AngleExtensions (BAM↔degrees), SplitAt, FromGameTicks
- Tests: full reader coverage on two custom levels (Silver Machine TR2,
  Escapade in Utah TR3) and mapper coverage

* docs(readme): add screenshots section from tomblauncher.app (#117)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ai): add AI-assisted troubleshooting (Laura)

Introduces an experimental AI chat feature ("Talk to Laura") backed by a
RAG pipeline, disabled by default and hidden from the sidebar when off.

Core AI pipeline
- RagService: streaming RAG with Semantic Kernel, per-request Kernel.Clone
  for thread safety, documentation injected into user message (not system
  prompt) to avoid accumulation across turns
- VectorSearchService: sqlite-vec vector search with distance threshold 0.20
- GameDiagnosticsPlugin: SK plugin exposing crash log, exit code and engine
  info to the model via auto function calling
- TroubleshootingContextService: builds per-game context from the database
- Support for Ollama and LM Studio backends (OllamaBackendService,
  LmStudioBackendService) with endpoint normalization and model listing

Settings UI
- AiSettingsView: backend picker, endpoint/embedding model health checks
  with flashing indicator, dynamic model list via ComboBox, KB update button
- ServiceCheckView: reusable status indicator extracted from duplicated code

Chat UI
- AiChatView: streaming message bubbles, tool-use indicator, empty state,
  auto-scroll, Enter to send; Laura menu item hidden when AI disabled
- Open chat from game details page with pre-loaded troubleshooting context

Knowledge base
- KnowledgeBase.Embedder: console tool that chunks markdown docs, generates
  embeddings via OpenAI-compatible API and writes sqlite-vec DB
- SFTP upload (--upload flag): SHA-256 manifest, atomic rename on server,
  private key or password auth
- KbUpdateService: client-side auto-update on startup — fetches manifest,
  checks schema version and generatedAt, downloads + verifies + swaps DB
- CryptoUtils: unified MD5/SHA-256 helpers (renamed from Md5Utils)

Bug fixes
- Fix sync/async stream mix crash on game exit (BeginOutputReadLine +
  ReadToEndAsync incompatibility)
- Fix IHttpClientFactory CI build failure (HintPath → PackageReference)

* feat(platform): add game support matrix (#89) (#122)

Introduces a per-OS engine support matrix to IPlatformSpecificFeatures:

- Add EngineSupportState enum (NoSupport, SupportedWithCompatibilityLayer,
  NativePatchingAvailable, FullSupport)
- Add ISupportMatrix interface with IReadOnlyDictionary<GameEngine, EngineSupportState>
- Implement LinuxSupportMatrix, WindowsSupportMatrix, MacOsSupportMatrix
- Wire ISupportMatrix into IPlatformSpecificFeatures and both platform implementations
- Expose EngineSupportState in GameDetailsViewModel via GameDetailsService
- Show a status icon in GameBar with danger class for unsupported engines
- Add EngineSupportStateToIconConverter and EngineSupportStateToTooltipConverter
- Add localization strings for all four states (en-US, it-IT)

* fix(launch-options): populate executables correctly and handle IO errors (#124)

Use GetEnumerationOptions() (case-insensitive, skips reparse points) instead
of SearchOption.AllDirectories when scanning the install directory, fixing
controls not populated on page open. Catch IOException in LoadAsync and show
a localized error messagebox before navigating back. Remove orphan
GetGameLaunchStartInfo from LinuxPlatformSpecificFeatures.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ai): integrate support matrix into SK as a plugin (#125)

Add SupportMatrixPlugin exposing ISupportMatrix to the Semantic Kernel
so the AI can report engine support state during troubleshooting.
Adopt null-object pattern on TroubleshootingContext (IsSet sentinel) to
remove nullable parameters from plugin constructors and AskAsync.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(readme): document AI-assisted troubleshooting feature (Laura) (#… (#126)

docs(readme): document AI-assisted troubleshooting feature (Laura) (#121)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ai): correct remote KB manifest URL (#128)

The manifest file is at kb_manifest.json, not manifest.json.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(patchers): add widescreen patcher with backup/restore pipeline (#129)

* feat(patchers): port widescreen aspect ratio patcher from old branch

Brings over the widescreen patcher logic (contracts, patcher, test asset)
originally developed on feature/patchers, adjusted for the src/tests layout.
IPatcher.EngineDetector still references TombRaiderEngineDetector — needs
resolution before the project compiles (see issue #88).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(patchers): move EngineDetectorResult to Contracts, wire IEngineDetector

- EngineDetectorResult moved…
* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists (#76)

Allows re-running the release pipeline without failing on duplicate tags.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix deploy-appcast job

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Master (#85)

* Release/1.1.0 (#77)

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): fix appcast-deploy job

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): fix deploy-appcast job

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): fix deploy-appcast job

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix AppImage filename and remove space from portable ZIP name

- AppImage: add package release suffix (-1) to match PupNet output
  (tomb-launcher-{version}-1.x86_64.AppImage)
- Portable ZIP: use hyphens instead of spaces to avoid glob and shell
  quoting issues throughout the pipeline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): replace placeholder AppDescription in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): fix PublisherLinkUrl in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): clean up pupnet.conf metadata

- SetupMinWindowsVersion: 6.2 -> 10 (.NET 10 requires Windows 10+)
- PublisherLinkName: use display name instead of URL
- SetupCommandPrompt: clear (GUI app, no CLI entry needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): fix deploy-appcast job

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix AppImage filename and remove space from portable ZIP name

- AppImage: add package release suffix (-1) to match PupNet output
  (tomb-launcher-{version}-1.x86_64.AppImage)
- Portable ZIP: use hyphens instead of spaces to avoid glob and shell
  quoting issues throughout the pipeline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): replace placeholder AppDescription in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): fix PublisherLinkUrl in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): clean up pupnet.conf metadata

- SetupMinWindowsVersion: 6.2 -> 10 (.NET 10 requires Windows 10+)
- PublisherLinkName: use display name instead of URL
- SetupCommandPrompt: clear (GUI app, no CLI entry needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(ci): use bash array for appcast common args

Reduces code duplication and avoids long backslash continuation chains
that may cause parsing issues on certain runners.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* Release/1.1.0 (#83)

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix AppImage filename and remove space from portable ZIP name

- AppImage: add package release suffix (-1) to match PupNet output
  (tomb-launcher-{version}-1.x86_64.AppImage)
- Portable ZIP: use hyphens instead of spaces to avoid glob and shell
  quoting issues throughout the pipeline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): replace placeholder AppDescription in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): fix PublisherLinkUrl in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): clean up pupnet.conf metadata

- SetupMinWindowsVersion: 6.2 -> 10 (.NET 10 requires Windows 10+)
- PublisherLinkName: use display name instead of URL
- SetupCommandPrompt: clear (GUI app, no CLI entry needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(ci): use bash array for appcast common args

Reduces code duplication and avoids long backslash continuation chains
that may cause parsing issues on certain runners.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix scp port flag and ssh-keyscan port for non-standard SSH port

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* Release/1.1.0 (#84)

* fix: avoid crash on Wine/Proton game directories in statistics

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* test(path-utils): add tests for symlink, proton_pfx skip, and inaccessible directory handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(datetime-extensions): add tests for DateTimeExtensions and DayOfWeekCultureSensitiveComparer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(md5-utils): add tests for both ComputeMd5Hash overloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(datetime-extensions): annotate Average and Sum parameters as nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add BooleanToIconConverter tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(value-converters): remove unused converters and fix BooleanToStringConverter.ConvertBack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(value-converters): add tests for remaining 20 converters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(axaml): replace deleted converters with direct bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tier-2): add tests for SavegameHeaderReader, Tr1xSavegameHeaderReader, ZipManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [CodeFactor] Apply fixes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* fix(ci): skip tag creation if tag already exists

Allows re-running the release pipeline without failing on duplicate tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): pass --repo to gh release download in deploy-appcast job

The deploy-appcast job has no checkout step, so gh cannot infer the
repository from the git context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): add --public-key-override to appcast generation steps

netsparkle-generate-appcast requires both private and public key to
sign artifacts, even when using --private-key-override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix AppImage filename and remove space from portable ZIP name

- AppImage: add package release suffix (-1) to match PupNet output
  (tomb-launcher-{version}-1.x86_64.AppImage)
- Portable ZIP: use hyphens instead of spaces to avoid glob and shell
  quoting issues throughout the pipeline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): replace placeholder AppDescription in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): fix PublisherLinkUrl in pupnet.conf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deploy): clean up pupnet.conf metadata

- SetupMinWindowsVersion: 6.2 -> 10 (.NET 10 requires Windows 10+)
- PublisherLinkName: use display name instead of URL
- SetupCommandPrompt: clear (GUI app, no CLI entry needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(ci): use bash array for appcast common args

Reduces code duplication and avoids long backslash continuation chains
that may cause parsing issues on certain runners.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): fix scp port flag and ssh-keyscan port for non-standard SSH port

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): upload appcast.xml.signature alongside appcast.xml

NetSparkle in Strict mode verifies the appcast using the .signature
file — without it, the signature check fails and no updates are detected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: codefactor-io <support@codefactor.io>

* refactor: migrate away from automapper

* chore(deps): add Riok.Mapperly 4.3.1

Initial step of the AutoMapper → Mapperly migration tracked on this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: remove unused PlaySessionDto

PlaySessionDto and its two consumer methods on PlaySessionDataService
(GetLastPlaySession, GetPlaySessionsByGameId) were not called by anyone.
Drops one mapping pair from the AutoMapper → Mapperly migration scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "chore(deps): add Riok.Mapperly 4.3.1"

This reverts commit 31847b590b1841fa7fecc38db8440d5348eea7ea.

* refactor(mapping): switch language mapping from AutoMapper to manual

First step of the migration to manual mapping. AvailableLanguageDto ↔
ApplicationLanguageViewModel is now handled by a SettingsMapper instance
class (registered as DI singleton) instead of AutoMapper. The two unused
dead methods on SettingsPageService (GetApplicationLanguage,
GetApplicationTheme) are dropped along the way.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): complete SettingsMapper migration

Migrates DownloaderConfiguration ↔ DownloaderViewModel and
UnzipBackendDto ↔ UnzipBackendViewModel to manual mapping in
SettingsMapper. SettingsProfile is now empty and deleted.
DownloaderSettingsViewModel drops MapperConfiguration injection
in favour of SettingsMapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate AppCrash mapping to manual mapper

Introduces TombLauncher.Data/Mapping/ as the location for entity↔DTO
mappers. AppCrashMapper lives there, keeping entities contained within
the data layer. AppCrashProfile deleted, SettingsPageViewModel drops
its MapperConfiguration dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate statistics mapping to manual mapper

Introduces StatisticsMapper for the three statistics mappings.
DailyStatisticsAverageGameDurationViewModel renamed to
DailyGameDurationViewModel (used for both average and total),
its AverageGameDuration property renamed to GameDuration.
DailyStatisticsViewModel deleted (superseded by DailyGameDurationViewModel).
StatisticsProfile retains only the SavegameViewModel mapping (Fase 5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate GameLink mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate GameHash mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate FileBackup mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate Game and GameMetadata mapping to manual mappers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate DownloaderSearchPayload mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate LaunchOptions mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate GameWithStats mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate search result mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate savegame and FileBackup mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: update CLAUDE.md to reflect manual mapper convention

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: remove stale TombLauncher.Mapping using directive

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(deps): add Riok.Mapperly 4.3.1

Initial step of the AutoMapper → Mapperly migration tracked on this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: remove unused PlaySessionDto

PlaySessionDto and its two consumer methods on PlaySessionDataService
(GetLastPlaySession, GetPlaySessionsByGameId) were not called by anyone.
Drops one mapping pair from the AutoMapper → Mapperly migration scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "chore(deps): add Riok.Mapperly 4.3.1"

This reverts commit 31847b590b1841fa7fecc38db8440d5348eea7ea.

* refactor(mapping): switch language mapping from AutoMapper to manual

First step of the migration to manual mapping. AvailableLanguageDto ↔
ApplicationLanguageViewModel is now handled by a SettingsMapper instance
class (registered as DI singleton) instead of AutoMapper. The two unused
dead methods on SettingsPageService (GetApplicationLanguage,
GetApplicationTheme) are dropped along the way.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): complete SettingsMapper migration

Migrates DownloaderConfiguration ↔ DownloaderViewModel and
UnzipBackendDto ↔ UnzipBackendViewModel to manual mapping in
SettingsMapper. SettingsProfile is now empty and deleted.
DownloaderSettingsViewModel drops MapperConfiguration injection
in favour of SettingsMapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate AppCrash mapping to manual mapper

Introduces TombLauncher.Data/Mapping/ as the location for entity↔DTO
mappers. AppCrashMapper lives there, keeping entities contained within
the data layer. AppCrashProfile deleted, SettingsPageViewModel drops
its MapperConfiguration dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate statistics mapping to manual mapper

Introduces StatisticsMapper for the three statistics mappings.
DailyStatisticsAverageGameDurationViewModel renamed to
DailyGameDurationViewModel (used for both average and total),
its AverageGameDuration property renamed to GameDuration.
DailyStatisticsViewModel deleted (superseded by DailyGameDurationViewModel).
StatisticsProfile retains only the SavegameViewModel mapping (Fase 5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mapping): migrate GameLink mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate GameHash mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate FileBackup mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate Game and GameMetadata mapping to manual mappers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate DownloaderSearchPayload mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate LaunchOptions mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate GameWithStats mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate search result mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(mapping): migrate savegame and FileBackup mapping to manual mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: update CLAUDE.md to reflect manual mapper convention

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: remove stale TombLauncher.Mapping using directive

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Update src/TombLauncher.Data/Mapping/GameLinkMapper.cs

Co-authored-by: codefactor-io[bot] <47775046+codefactor-io[bot]@users.noreply.github.com>

* Update src/TombLauncher/Mappers/LaunchOptionsMapper.cs

Co-authored-by: codefactor-io[bot] <47775046+codefactor-io[bot]@users.noreply.github.com>

* chore: align namespace and remove stale TombLauncher.Mapping usings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: remove AutoMapper configuration test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: codefactor-io[bot] <47775046+codefactor-io[bot]@users.noreply.github.com>

* feat(compatibility): add Proton support and per-game launcher settings (#109)

* feat(settings,launch-options): add Wine compatibility section and convert launch options to page

- New Compatibility settings section (WinePath, WinePrefix) visible on Linux only
- Wine detection at startup: WINE env var -> WINEPATH env var -> PATH scan
- Warning notification (GobletBrokenLine) if Wine not found
- LaunchOptionsDialogView/ViewModel replaced by LaunchOptionsView/ViewModel (PageViewModel)
- LaunchOptions promoted to direct button in GameBar (TerminalLine icon)
- Per-game WinePrefix override in LaunchOptionsView, visible on Linux only
- WinePrefix added to LaunchOptionsDto, Game entity and UpdateLaunchOptions
- DB migration: AddGameWinePrefix
- SettingsPageService.PersistCurrentConfigAsync() extracted as reusable method
- GameWithStatsService: WinePath read from IAppConfiguration.Compatibility (no hardcoded fallback)

* docs: fix add-page workflow view naming convention (View suffix, Pages/ folder)

* loc: simplify Wine-related UI strings for non-technical users

* style(ui): use text-muted class on LabeledTextBox label

* feat(ui): add CardSectionHeader control to LaunchOptionsView

- New CardSectionHeader control in TombLauncher.Controls (IconKind + Title)
- LaunchOptionsView: icon+title header on each card
  (PlayLargeLine/Launch, HammerLine/Setup, GroupLine/CommunityPatch, Goblet2Line/Compatibility)
- Add GENERAL and COMMUNITY_PATCH localization keys (en-US, it-IT)
- Community patch tooltip: 'some versions of TRX' instead of 'TRX'
- LabeledTextBox label now uses text-muted class

* fix(ui,navigation): sticky save button in LaunchOptionsView; fix GameWithStats refresh on back

- LaunchOptionsView: restructured with Grid (ScrollViewer row * + buttons row Auto)
  Save button now uses IconButton btn-primary/Save3Line, always visible outside scroll area
- Fix: GameWithStatsViewModel data not refreshing when navigating back from LaunchOptions

* fix(data): rewrite UpdateLaunchOptions using EF change tracker

Replace ExecuteUpdateAsync/ExecuteDeleteAsync (bulk ops that bypass EF tracking)
with a single tracked ToListAsync query + direct property assignment.
This ensures GetGameById returns fresh data after saving launch options,
fixing the stale executable path shown in GameDetailsViewModel on back navigation.

* feat(stats,settings): implement Wine version detection

- IPlatformSpecificFeatures: add GetWineVersion(string winePath)
- LinuxPlatformSpecificFeatures: runs wine --version, captures stdout
- WindowsPlatformSpecificFeatures: returns null (Wine not supported)
- CompatibilitySettingsViewModel: WineVersion auto-refreshed via OnWinePathChanged
- CompatibilitySettingsView: read-only Wine version row below WINEPREFIX
- StatisticsPageViewModel: WineVersion populated on init via IAppConfiguration.Compatibility.WinePath
- StatisticsPageView: row 7 with Goblet2Line icon, visible only on Linux (IsWineSupported)
- Localization: WINE_VERSION added in en-US and it-IT

* fix(linux): wrap wine launch in bash + wineserver -w for correct process tracking

wine exits immediately after spawning the Windows process into wineserver,
causing play time tracking and save file monitoring to stop prematurely.
Wrapping with 'bash -c "wine ...; wineserver -w"' keeps the monitored process
alive until wineserver shuts down (i.e. until the game truly exits).

* feat(proton): add Proton launcher support with compatibility settings

- Add ProtonGameLauncher: sets STEAM_COMPAT_DATA_PATH (with default),
  STEAM_COMPAT_INSTALL_PATH, STEAM_COMPAT_CLIENT_INSTALL_PATH (derived from
  Proton binary path), STEAM_APPID=0; strips NVIDIA PRIME vars inherited from
  parent process that break Wine rendering; uses bash login shell (-lc)
- Add WineGameLauncher, WindowsGameLauncher, IGameLauncher abstractions
- Add ProtonInstallationDto, CompatibilityTool enum
- Add CompatibilityToolToIconConverter
- LinuxPlatformSpecificFeatures: FindAvailableProtonInstallations(),
  CollectSteamRoots() (parses libraryfolders.vdf), GetProtonVersion()
- CompatibilitySettingsView/ViewModel: Wine/Proton tool selector, Proton
  installation ComboBox with auto-detect from steamapps, manual fallback,
  STEAM_COMPAT_DATA_PATH override, Proton version display
- GameWithStatsService: capture stdout/stderr on non-zero exit (Linux only)
- SavegameCardView: split MARK_UNMARK key into MARK_/UNMARK_ for better i18n,
  use BooleanToStringConverter for context-aware tooltip
- Localization: WINE_VERSION, MARK_AS_START_OF_LEVEL_SAVE,
  UNMARK_AS_START_OF_LEVEL_SAVE added in en-US and it-IT
- Add unit tests for CompatibilityConfig, GameLauncher, LinuxPlatformFeatures

* refactor(settings): remove WinePath from GameDetails config

WinePath was migrated to CompatibilityConfig in a previous session.
Now that the one-time migration path in App.axaml.cs is no longer
needed, remove the residual property from IGameDetailsConfig,
GameDetailsConfig, LayeredAppConfiguration, GameDetailsCoreSettings
and its construction in SettingsProvider.

* fix(statistics): avoid crash on inaccessible Proton prefix directories

PathUtils.GetDirectorySize crashed with UnauthorizedAccessException and
IOException when enumerating game folders that contain a Proton prefix.
The prefix includes root-owned subdirs and symlinks (e.g. dosdevices/z: -> /)
that lead to /proc entries for dead processes.

Fix: switch to a manual recursive helper that:
- Skips the proton_pfx directory entirely (not part of the game)
- Skips symlinks via AttributesToSkip = ReparsePoint
- Ignores inaccessible entries (IgnoreInaccessible = true)
- Catches any residual UnauthorizedAccessException / IOException per directory

* fix(settings): compatibility tool change now takes effect without restart

IGameLauncher is now resolved on each game launch via Func<IGameLauncher>
instead of being captured at construction time in GameWithStatsService.

Also refactors InitializeServices into a fluent chain of IServiceCollection
extension methods, each with a single responsibility:
- AddAppConfiguration: loads JSON layers, registers ILayeredAppConfiguration/IAppConfiguration
- AddPlatformSpecificFeatures: registers IPlatformSpecificFeatures, IGameLauncher and its factory
- AddSaveGameManagement: savegame header provider, processor and query/command services
- AddTheming, AddFileOperations, AddLocalization, AddNavigation: focused singletons
- AddGameManagement: installer, engine detector, game merger
- AddPopups: IPopupService
- AddDownloaders: HTTP clients, downloader implementations, download manager
- AddNotifications, AddUpdater: notification and update services
- AddDatabaseAccess now resolves IAppConfiguration from sp internally

* refactor(app): move CheckCompatibilityToolAsync to AppUtils

Eliminates the private wrapper method in App, calling AppUtils directly
from ShowMainWindow. Unused usings (IconPacks, Configuration, Sections)
removed from App.axaml.cs.

* refactor(app): move ApplyInitialSettings to AppUtils

App.axaml.cs now delegates both startup helpers to AppUtils,
leaving it with only wiring/orchestration logic.

* style(app): use unqualified ChangeTheme call within AppUtils

* refactor(app): move OnUnhandledException to AppUtils

Registered directly as AppUtils.OnUnhandledException on the Dispatcher
event — no disadvantages vs instance method in this context (singleton
event source, no instance capture needed).
Removed unused usings from App.axaml.cs.

* refactor(navigation): move INavigationTarget and ITopBarCommand to Contracts, introduce BusyScope pattern

- Move INavigationTarget and ITopBarCommand from TombLauncher.Core.Navigation to TombLauncher.Contracts.Navigation
- ITopBarCommand.Icon changed from PackIconRemixIconKind to Enum to remove icon library dependency from Contracts
- CommandViewModel.Icon likewise changed to Enum
- INavigationTarget simplified: SetBusy(bool,string) removed from public interface, BusyScope(string) introduced as the primary RAII pattern
- SetBusy(string) kept/added for fire-and-forget cases (PlayGame, LaunchSetup, LaunchCommunitySetup)
- GameWithStatsService, StatisticsPageViewModel, GameListViewModel, SavegameListViewModel refactored to BusyScope pattern
- async void Uninstall fixed to async Task
- Unused usings and IoC.Default references removed

* fix(ui): fix BusyMessage not visible in RingLoader overlay

- RingLoader: move Width/Height constraint from UserControl to inner Viewbox,
  so the message TextBlock below the ring is no longer clipped to 80x80 bounds
- RingLoader: restore RelativeSource binding (was working, just clipped)
- GameListViewModel: add missing .GetLocalizedString() on FETCHING_GAMES key
- MainWindow: remove redundant debug TextBlock for BusyMessage

* refactor(compatibility): move CompatibilityTool enum to TombLauncher.Contracts

Moved CompatibilityTool from TombLauncher.Configuration.Sections to
TombLauncher.Contracts.Enums, alongside the other shared enums.

This removes the circular dependency that would have blocked adding
CompatibilityTool? fields to GameMetadataDto (TombLauncher.Core) and
the Game entity (TombLauncher.Data), both of which cannot reference
the main TombLauncher project.

Updated all consumers:
- CompatibilityConfig.cs, ICompatibilityConfig.cs
- LayeredAppConfiguration.cs
- PlatformSpecificFeaturesCollectionExtensions.cs
- CompatibilitySettingsViewModel.cs
- CompatibilityToolToIconConverter.cs
- AppUtils.cs
- CompatibilitySettingsView.axaml (xmlns:cfg alias)

* refactor(compatibility): introduce GameLaunchContext, per-game CompatibilityTool and ExtraEnvVars

- IGameLauncher.GetLaunchStartInfo now takes GameLaunchContext instead of 4 loose params
- GameLaunchContext carries ExecutableFileName, Arguments, WorkingDirectory, PrefixPath, ExtraEnvVars
- Wine/Proton/Windows launchers apply ExtraEnvVars last (per-game overrides)
- Rename WinePrefix -> CompatibilityPrefixPath across config, DTOs, models, ViewModels and Views
- Add CompatibilityTool (int) and CompatibilityToolPath to Game entity and DTOs
- Add GameEnvironmentVariable entity + EnvironmentVariableDto
- Add migration AddPerGameCompatibilitySettings
- LaunchOptionsProfile: map CompatibilityPrefixPath

* feat(settings,launch-options): add Wine compatibility section and convert launch options to page

- New Compatibility settings section (WinePath, WinePrefix) visible on Linux only
- Wine detection at startup: WINE env var -> WINEPATH env var -> PATH scan
- Warning notification (GobletBrokenLine) if Wine not found
- LaunchOptionsDialogView/ViewModel replaced by LaunchOptionsView/ViewModel (PageViewModel)
- LaunchOptions promoted to direct button in GameBar (TerminalLine icon)
- Per-game WinePrefix override in LaunchOptionsView, visible on Linux only
- WinePrefix added to LaunchOptionsDto, Game entity and UpdateLaunchOptions
- DB migration: AddGameWinePrefix
- SettingsPageService.PersistCurrentConfigAsync() extracted as reusable method
- GameWithStatsService: WinePath read from IAppConfiguration.Compatibility (no hardcoded fallback)

# Conflicts:
#	src/TombLauncher.Data/Database/Services/GameDataService.cs
#	src/TombLauncher/App.axaml.cs
#	src/TombLauncher/Extensions/ViewModelsServiceCollectionExtensions.cs
#	src/TombLauncher/Factories/Mapping/MappingUtils.cs
#	src/TombLauncher/Factories/Profiles/LaunchOptionsProfile.cs
#	src/TombLauncher/Services/GameDetailsService.cs
#	src/TombLauncher/Services/GameWithStatsService.cs
#	src/TombLauncher/ViewModels/Dialogs/LaunchOptionsDialogViewModel.cs
#	src/TombLauncher/ViewModels/Pages/GameDetailsViewModel.cs
#	src/TombLauncher/Views/GameBar.axaml

* docs: fix add-page workflow view naming convention (View suffix, Pages/ folder)

* loc: simplify Wine-related UI strings for non-technical users

* style(ui): use text-muted class on LabeledTextBox label

* feat(ui): add CardSectionHeader control to LaunchOptionsView

- New CardSectionHeader control in TombLauncher.Controls (IconKind + Title)
- LaunchOptionsView: icon+title header on each card
  (PlayLargeLine/Launch, HammerLine/Setup, GroupLine/CommunityPatch, Goblet2Line/Compatibility)
- Add GENERAL and COMMUNITY_PATCH localization keys (en-US, it-IT)
- Community patch tooltip: 'some versions of TRX' instead of 'TRX'
- LabeledTextBox label now uses text-muted class

* fix(ui,navigation): sticky save button in LaunchOptionsView; fix GameWithStats refresh on back

- LaunchOptionsView: restructured with Grid (ScrollViewer row * + buttons row Auto)
  Save button now uses IconButton btn-primary/Save3Line, always visible outside scroll area
- Fix: GameWithStatsViewModel data not refreshing when navigating back from LaunchOptions

* fix(data): rewrite UpdateLaunchOptions using EF change tracker

Replace ExecuteUpdateAsync/ExecuteDeleteAsync (bulk ops that bypass EF tracking)
with a single tracked ToListAsync query + direct property assignment.
This ensures GetGameById returns fresh data after saving launch options,
fixing the stale executable path shown in GameDetailsViewModel on back navigation.

* feat(stats,settings): implement Wine version detection

- IPlatformSpecificFeatures: add GetWineVersion(string winePath)
- LinuxPlatformSpecificFeatures: runs wine --version, captures stdout
- WindowsPlatformSpecificFeatures: returns null (Wine not supported)
- CompatibilitySettingsViewModel: WineVersion auto-refreshed via OnWinePathChanged
- CompatibilitySettingsView: read-only Wine version row below WINEPREFIX
- StatisticsPageViewModel: WineVersion populated on init via IAppConfiguration.Compatibility.WinePath
- StatisticsPageView: row 7 with Goblet2Line icon, visible only on Linux (IsWineSupported)
- Localization: WINE_VERSION added in en-US and it-IT

* fix(linux): wrap wine launch in bash + wineserver -w for correct process tracking

wine exits immediately after spawning the Windows process into wineserver,
causing play time tracking and save file monitoring to stop prematurely.
Wrapping with 'bash -c "wine ...; wineserver -w"' keeps the monitored process
alive until wineserver shuts down (i.e. until the game truly exits).

* feat(proton): add Proton launcher support with compatibility settings

- Add ProtonGameLauncher: sets STEAM_COMPAT_DATA_PATH (with default),
  STEAM_COMPAT_INSTALL_PATH, STEAM_COMPAT_CLIENT_INSTALL_PATH (derived from
  Proton binary path), STEAM_APPID=0; strips NVIDIA PRIME vars inherited from
  parent process that break Wine rendering; uses bash login shell (-lc)
- Add WineGameLauncher, WindowsGameLauncher, IGameLauncher abstractions
- Add ProtonInstallationDto, CompatibilityTool enum
- Add CompatibilityToolToIconConverter
- LinuxPlatformSpecificFeatures: FindAvailableProtonInstallations(),
  CollectSteamRoots() (parses libraryfolders.vdf), GetProtonVersion()
- CompatibilitySettingsView/ViewModel: Wine/Proton tool selector, Proton
  installation ComboBox with auto-detect from steamapps, manual fallback,
  STEAM_COMPAT_DATA_PATH override, Proton version display
- GameWithStatsService: capture stdout/stderr on non-zero exit (Linux only)
- SavegameCardView: split MARK_UNMARK key into MARK_/UNMARK_ for better i18n,
  use BooleanToStringConverter for context-aware tooltip
- Localization: WINE_VERSION, MARK_AS_START_OF_LEVEL_SAVE,
  UNMARK_AS_START_OF_LEVEL_SAVE added in en-US and it-IT
- Add unit tests for CompatibilityConfig, GameLauncher, LinuxPlatformFeatures

* refactor(settings): remove WinePath from GameDetails config

WinePath was migrated to CompatibilityConfig in a previous session.
Now that the one-time migration path in App.axaml.cs is no longer
needed, remove the residual property from IGameDetailsConfig,
GameDetailsConfig, LayeredAppConfiguration, GameDetailsCoreSettings
and its construction in SettingsProvider.

* fix(settings): compatibility tool change now takes effect without restart

IGameLauncher is now resolved on each game launch via Func<IGameLauncher>
instead of being captured at construction time in GameWithStatsService.

Also refactors InitializeServices into a fluent chain of IServiceCollection
extension methods, each with a single responsibility:
- AddAppConfiguration: loads JSON layers, registers ILayeredAppConfiguration/IAppConfiguration
- AddPlatformSpecificFeatures: registers IPlatformSpecificFeatures, IGameLauncher and its factory
- AddSaveGameManagement: savegame header provider, processor and query/command services
- AddTheming, AddFileOperations, AddLocalization, AddNavigation: focused singletons
- AddGameManagement: installer, engine detector, game merger
- AddPopups: IPopupService
- AddDownloaders: HTTP clients, downloader implementations, download manager
- AddNotifications, AddUpdater: notification and update services
- AddDatabaseAccess now resolves IAppConfiguration from sp internally

* refactor(app): move CheckCompatibilityToolAsync to AppUtils

Eliminates the private wrapper method in App, calling AppUtils directly
from ShowMainWindow. Unused usings (IconPacks, Configuration, Sections)
removed from App.axaml.cs.

* refactor(app): move ApplyInitialSettings to AppUtils

App.axaml.cs now delegates both startup helpers to AppUtils,
leaving it with only wiring/orchestration logic.

* style(app): use unqualified ChangeTheme call within AppUtils

* refactor(app): move OnUnhandledException to AppUtils

Registered directly as AppUtils.OnUnhandledException on the Dispatcher
event — no disadvantages vs instance method in this context (singleton
event source, no instance capture needed).
Removed unused usings from App.axaml.cs.

* refactor(navigation): move INavigationTarget and ITopBarCommand to Contracts, introduce BusyScope pattern

- Move INavigationTarget and ITopBarCommand from TombLauncher.Core.Navigation to TombLauncher.Contracts.Navigation
- ITopBarCommand.Icon changed from PackIconRemixIconKind to Enum to remove icon library dependency from Contracts
- CommandViewModel.Icon likewise changed to Enum
- INavigationTarget simplified: SetBusy(bool,string) removed from public interface, BusyScope(string) introduced as the primary RAII pattern
- SetBusy(string) kept/added for fire-and-forget cases (PlayGame, LaunchSetup, LaunchCommunitySetup)
- GameWithStatsService, StatisticsPageViewModel, GameListViewModel, SavegameListViewModel refactored to BusyScope pattern
- async void Uninstall fixed to async Task
- Unused usings and IoC.Default references removed

* fix(ui): fix BusyMessage not visible in RingLoader overlay

- RingLoader: move Width/Height constraint from UserControl to inner Viewbox,
  so the message TextBlock below the ring is no longer clipped to 80x80 bounds
- RingLoader: restore RelativeSource binding (was working, just clipped)
- GameListViewModel: add missing .GetLocalizedString() on FETCHING_GAMES key
- MainWindow: remove redundant debug TextBlock for BusyMessage

* refactor(compatibility): move CompatibilityTool enum to TombLauncher.Contracts

Moved CompatibilityTool from TombLauncher.Configuration.Sections to
TombLauncher.Contracts.Enums, alongside the other shared enums.

This removes the circular dependency that would have blocked adding
CompatibilityTool? fields to GameMetadataDto (TombLauncher.Core) and
the Game entity (TombLauncher.Data), both of which cannot reference
the main TombLauncher project.

Updated all consumers:
- CompatibilityConfig.cs, ICompatibilityConfig.cs
- LayeredAppConfiguration.cs
- PlatformSpecificFeaturesCollectionExtensions.cs
- CompatibilitySettingsViewModel.cs
- CompatibilityToolToIconConverter.cs
- AppUtils.cs
- CompatibilitySettingsView.axaml (xmlns:cfg alias)

* refactor(compatibility): introduce GameLaunchContext, per-game CompatibilityTool and ExtraEnvVars

- IGameLauncher.GetLaunchStartInfo now takes GameLaunchContext instead of 4 loose params
- GameLaunchContext carries ExecutableFileName, Arguments, WorkingDirectory, PrefixPath, ExtraEnvVars
- Wine/Proton/Windows launchers apply ExtraEnvVars last (per-game overrides)
- Rename WinePrefix -> CompatibilityPrefixPath across config, DTOs, models, ViewModels and Views
- Add CompatibilityTool (int) and CompatibilityToolPath to Game entity and DTOs
- Add GameEnvironmentVariable entity + EnvironmentVariableDto
- Add migration AddPerGameCompatibilitySettings
- LaunchOptionsProfile: map CompatibilityPrefixPath

* fix(compatibility): wire per-game CompatibilityTool, EnvVars and prefix through all layers

- Add IEnvironmentVariable contract and EnvironmentVariableMapper (ToDto/ToEntity)
- Extend IGameMetadata, GameMetadataDto, Game entity with CompatibilityTool,
  CompatibilityToolPath and ExtraEnvVars
- GameMapper: map new fields in both ToDto and ToGame; CompatibilityTool now
  defaults to Unspecified in CompatibilityConfig so layered merge works correctly
- GameDataService.UpdateLaunchOptions: add missing SetupExecutable block, persist
  ExtraEnvVars via EnvironmentVariableMapper.ToEntities, fix wrong executable added
  when GameExecutable backup was absent
- Add .Include(g => g.EnvironmentVariables) to all EF queries that call GameMapper
- GameMetadataViewModel / LaunchOptionsViewModel: expose CompatibilityTool,
  CompatibilityToolPath, ExtraEnvVars; InitFromGame populates them from the game VM
- LaunchOptionsMapper.ToDto: map CompatibilityPrefixPath, CompatibilityTool,
  CompatibilityToolPath, ExtraEnvVars
- GameWithStatsService.LaunchProcess: select launcher from per-game CompatibilityTool
  (falling back to global factory when Unspecified); pass ExtraEnvVars to context
- LayeredAppConfiguration: fix CompatibilityTool merge — check both User and Defaults
  before falling back to Wine; add "CompatibilityTool": "Wine" to appsettings.json
- Register EnvironmentVariableMapper in DI; restore GetGameById on GameDataService
- Update GameLauncherTests to use GameLaunchContext; fix CompatibilityConfigTests using

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(mappers,data): add mapper unit tests and UpdateLaunchOptions integration tests

- EnvironmentVariableMapperTests: ToDto/ToEntity/ToDtos/ToEntities
- GameMapperTests: ToDto and ToGame for compatibility fields and env vars
- LaunchOptionsMapperTests: compatibility fields, ExtraEnvVars, optional executables
- UpdateLaunchOptionsTests (EF Core InMemory): CompatibilityTool/Path/Prefix
  persistence, env var add/replace/clear, setup executable add/update/remove
- SearchMapperTests: TitlePic propagated through ToDto overloads
- Add Microsoft.EntityFrameworkCore.InMemory 10.0.2 to test project

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(platform): add ExpandPath to IPlatformSpecificFeatures for tilde expansion

Linux expands leading ~ to $HOME; Windows returns the path unchanged.
GameWithStatsService applies ExpandPath before passing PrefixPath to the launcher,
so users can type ~/... in the UI and have it resolve correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ui): add website link to sidebar navigation (#111)

Adds a link to tomblauncher.app in the main sidebar, alongside the existing GitHub link. Includes WebsiteLink in the config layer, ApplicationCoreSettings DTO, and both localization files (en-US / it-IT). Also moves CoreSettings.cs to the Dtos/Configuration subfolder.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ui): add About page with acknowledgements (#110) (#112)

* feat(ui): add About page with app info and acknowledgements section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(ui): extract SidebarNavigationItem control to reduce sidebar duplication

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ui): populate About page acknowledgements section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(release): bump version to 1.2.0 (#113)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(patchers): add tombpc.dat gameflow reader and GameflowDto mapper  (#116)

Introduces TombLauncher.Patchers with a complete pipeline for reading
TR2/TR3 tombpc.dat script files and converting them into a UI-friendly
GameflowDto.

- GameflowReader: binary reader for the full tombpc.dat structure
  (header, counts, flags, XOR key, string arrays, sequences, item strings)
- GameflowMapper: converts Gameflow → GameflowDto, decoding XOR-encrypted
  strings, splitting sequences per level, and mapping BAM angles to degrees
- Shared models: GameflowDto, LevelInfo, SequenceInfo, Tr2Item, Tr3Item
- TRX skeleton: TrxGameflow, Level, SequenceItem models and patcher stub
- Utilities: AngleExtensions (BAM↔degrees), SplitAt, FromGameTicks
- Tests: full reader coverage on two custom levels (Silver Machine TR2,
  Escapade in Utah TR3) and mapper coverage

* docs(readme): add screenshots section from tomblauncher.app (#117)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ai): add AI-assisted troubleshooting (Laura)

Introduces an experimental AI chat feature ("Talk to Laura") backed by a
RAG pipeline, disabled by default and hidden from the sidebar when off.

Core AI pipeline
- RagService: streaming RAG with Semantic Kernel, per-request Kernel.Clone
  for thread safety, documentation injected into user message (not system
  prompt) to avoid accumulation across turns
- VectorSearchService: sqlite-vec vector search with distance threshold 0.20
- GameDiagnosticsPlugin: SK plugin exposing crash log, exit code and engine
  info to the model via auto function calling
- TroubleshootingContextService: builds per-game context from the database
- Support for Ollama and LM Studio backends (OllamaBackendService,
  LmStudioBackendService) with endpoint normalization and model listing

Settings UI
- AiSettingsView: backend picker, endpoint/embedding model health checks
  with flashing indicator, dynamic model list via ComboBox, KB update button
- ServiceCheckView: reusable status indicator extracted from duplicated code

Chat UI
- AiChatView: streaming message bubbles, tool-use indicator, empty state,
  auto-scroll, Enter to send; Laura menu item hidden when AI disabled
- Open chat from game details page with pre-loaded troubleshooting context

Knowledge base
- KnowledgeBase.Embedder: console tool that chunks markdown docs, generates
  embeddings via OpenAI-compatible API and writes sqlite-vec DB
- SFTP upload (--upload flag): SHA-256 manifest, atomic rename on server,
  private key or password auth
- KbUpdateService: client-side auto-update on startup — fetches manifest,
  checks schema version and generatedAt, downloads + verifies + swaps DB
- CryptoUtils: unified MD5/SHA-256 helpers (renamed from Md5Utils)

Bug fixes
- Fix sync/async stream mix crash on game exit (BeginOutputReadLine +
  ReadToEndAsync incompatibility)
- Fix IHttpClientFactory CI build failure (HintPath → PackageReference)

* feat(platform): add game support matrix (#89) (#122)

Introduces a per-OS engine support matrix to IPlatformSpecificFeatures:

- Add EngineSupportState enum (NoSupport, SupportedWithCompatibilityLayer,
  NativePatchingAvailable, FullSupport)
- Add ISupportMatrix interface with IReadOnlyDictionary<GameEngine, EngineSupportState>
- Implement LinuxSupportMatrix, WindowsSupportMatrix, MacOsSupportMatrix
- Wire ISupportMatrix into IPlatformSpecificFeatures and both platform implementations
- Expose EngineSupportState in GameDetailsViewModel via GameDetailsService
- Show a status icon in GameBar with danger class for unsupported engines
- Add EngineSupportStateToIconConverter and EngineSupportStateToTooltipConverter
- Add localization strings for all four states (en-US, it-IT)

* fix(launch-options): populate executables correctly and handle IO errors (#124)

Use GetEnumerationOptions() (case-insensitive, skips reparse points) instead
of SearchOption.AllDirectories when scanning the install directory, fixing
controls not populated on page open. Catch IOException in LoadAsync and show
a localized error messagebox before navigating back. Remove orphan
GetGameLaunchStartInfo from LinuxPlatformSpecificFeatures.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ai): integrate support matrix into SK as a plugin (#125)

Add SupportMatrixPlugin exposing ISupportMatrix to the Semantic Kernel
so the AI can report engine support state during troubleshooting.
Adopt null-object pattern on TroubleshootingContext (IsSet sentinel) to
remove nullable parameters from plugin constructors and AskAsync.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(readme): document AI-assisted troubleshooting feature (Laura) (#… (#126)

docs(readme): document AI-assisted troubleshooting feature (Laura) (#121)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ai): correct remote KB manifest URL (#128)

The manifest file is at kb_manifest.json, not manifest.json.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(patchers): add widescreen patcher with backup/restore pipeline (#129)

* feat(patchers): port widescreen aspect ratio patcher from old branch

Brings over the widescreen patcher logic (contracts, patcher, test asset)
originally developed on feature/patchers, adjusted for the src/tests layout.
IPatcher.EngineDetector still references TombRaiderEngineDetector — needs
resolution before the project compiles (see issue #88).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(patchers): move EngineDetectorResult to Contracts, wire IEngineDetector

- EngineDetectorResult move…
@ALDamico
ALDamico merged commit e108402 into develop Jun 7, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant