fix #407: windowed resolution offering decoupled from the video-mode list
Campaign CC gate round 1. The Config Resolution dropdown now offers DisplayModeCatalog.WindowedResolutions — the curated hardware modes UNIONed with the static modern-ladder sizes that fit the desktop — because a windowed pick is a plain Size write needing no video mode, and remote/RDP virtual displays advertise almost none (the live RDP display exposed exactly 1920x1080 + the 2056x1290 desktop, leaving the dropdown with nothing below 1920). The fullscreen apply still validates against the hardware Resolutions list plus the switcher's monitor-mode-list hard guard, so a fullscreen pick of a windowed-only entry refuses safely (log-and-stay, #388/#392) — IA-22's offered-implies-supported invariant narrows to the fullscreen half and its register row carries the amendment. Three new pure-union tests including the exact live RDP shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
26e6f984f4
commit
e601a496db
4 changed files with 119 additions and 5 deletions
|
|
@ -59,7 +59,7 @@ accepted-divergence entries (#96, #49, #50).
|
||||||
| IA-19 | Automatic combat acquisition is narrowed to attackable non-player monsters. Retail `AutoTarget` falls back to `SelectNext(SELECTION_TYPE_COMPASS_ITEM)`, whose combat filter can also admit attackable enemy players in compatible PK states. | `src/AcDream.Core/Combat/CombatTargetPolicy.cs`; consumers `src/AcDream.App/Interaction/WorldSelectionQuery.cs` (`IsHostileMonster`/`FindClosestHostileMonster`) and `SelectionInteractionController.cs` (`SelectClosestCombatTarget`). This row is auto-acquisition-only: as of #298, explicit-target admission and the combat camera route through the separate, retail-exact `WorldSelectionQuery.IsAttackableTarget` (`ObjectIsAttackable`-backed) instead, so a compatible-PK player is a valid manual attack/camera target — do not assume one predicate still serves both concerns. | Explicit product direction: Auto Target must never select NPCs, players, pets, or other objects; manual player-selection commands remain available | In PK play, Auto Target will not acquire an otherwise valid hostile player as retail would; the player must be selected manually | `ClientCombatSystem::AutoTarget @ 0x0056BC80`; `CPlayerSystem::SelectNext @ 0x0055F9A0`; `ClientCombatSystem::ObjectIsAttackable @ 0x0056A600` |
|
| IA-19 | Automatic combat acquisition is narrowed to attackable non-player monsters. Retail `AutoTarget` falls back to `SelectNext(SELECTION_TYPE_COMPASS_ITEM)`, whose combat filter can also admit attackable enemy players in compatible PK states. | `src/AcDream.Core/Combat/CombatTargetPolicy.cs`; consumers `src/AcDream.App/Interaction/WorldSelectionQuery.cs` (`IsHostileMonster`/`FindClosestHostileMonster`) and `SelectionInteractionController.cs` (`SelectClosestCombatTarget`). This row is auto-acquisition-only: as of #298, explicit-target admission and the combat camera route through the separate, retail-exact `WorldSelectionQuery.IsAttackableTarget` (`ObjectIsAttackable`-backed) instead, so a compatible-PK player is a valid manual attack/camera target — do not assume one predicate still serves both concerns. | Explicit product direction: Auto Target must never select NPCs, players, pets, or other objects; manual player-selection commands remain available | In PK play, Auto Target will not acquire an otherwise valid hostile player as retail would; the player must be selected manually | `ClientCombatSystem::AutoTarget @ 0x0056BC80`; `CPlayerSystem::SelectNext @ 0x0055F9A0`; `ClientCombatSystem::ObjectIsAttackable @ 0x0056A600` |
|
||||||
| IA-20 | The basic combat bar keeps dark-red media `0x0600715E` visible as the centered middle baseline. Retail skill-gates field `0x100005EF` to trained Recklessness; the separate bright child remains faithful live `SetPowerbarLevel` feedback from the absolute left edge. | `src/AcDream.App/UI/UiScrollbar.cs`; child-policy extraction in `src/AcDream.App/UI/Layout/DatWidgetFactory.cs` | Explicit connected visual direction: the dark middle track remains present behind live attack charge; the exact skill-gated treatment remains tracked by AP-112 | Untrained characters retain the dark-red baseline where retail may leave only the gray track; trained/untrained Recklessness presentation is not distinguishable | `gmCombatUI::RecvNotice_SetPowerbarLevel @ 0x004CC0E0`; `gmCombatUI::ListenToElementMessage @ 0x004CC430`; LayoutDesc `0x21000073` |
|
| IA-20 | The basic combat bar keeps dark-red media `0x0600715E` visible as the centered middle baseline. Retail skill-gates field `0x100005EF` to trained Recklessness; the separate bright child remains faithful live `SetPowerbarLevel` feedback from the absolute left edge. | `src/AcDream.App/UI/UiScrollbar.cs`; child-policy extraction in `src/AcDream.App/UI/Layout/DatWidgetFactory.cs` | Explicit connected visual direction: the dark middle track remains present behind live attack charge; the exact skill-gated treatment remains tracked by AP-112 | Untrained characters retain the dark-red baseline where retail may leave only the gray track; trained/untrained Recklessness presentation is not distinguishable | `gmCombatUI::RecvNotice_SetPowerbarLevel @ 0x004CC0E0`; `gmCombatUI::ListenToElementMessage @ 0x004CC430`; LayoutDesc `0x21000073` |
|
||||||
| IA-21 | When ACE sends player BoolProperty `68` (`SpellComponentsRequired`) false, acdream presents the retail scarab/prismatic-taper formula even without a directly carried school focus. With component enforcement enabled, retail's exact focus/infusion versus account-customized selection remains intact. | `src/AcDream.App/Spells/SpellComponentRequirementService.cs` | A component-disabled server has no actionable legacy recipe; explicit product direction is that this client/server mode uses the modern scarab/taper component presentation | A custom server could expect retail's legacy recipe to remain visible even though casting consumes no components | `ClientMagicSystem::AreSpellComponentsRequired @ 0x00567B90`; `ClientMagicSystem::GetAppropriateSpellFormula @ 0x00567D50`; `CSpellBase::InqScarabOnlyFormula @ 0x00597050` |
|
| IA-21 | When ACE sends player BoolProperty `68` (`SpellComponentsRequired`) false, acdream presents the retail scarab/prismatic-taper formula even without a directly carried school focus. With component enforcement enabled, retail's exact focus/infusion versus account-customized selection remains intact. | `src/AcDream.App/Spells/SpellComponentRequirementService.cs` | A component-disabled server has no actionable legacy recipe; explicit product direction is that this client/server mode uses the modern scarab/taper component presentation | A custom server could expect retail's legacy recipe to remain visible even though casting consumes no components | `ClientMagicSystem::AreSpellComponentsRequired @ 0x00567B90`; `ClientMagicSystem::GetAppropriateSpellFormula @ 0x00567D50`; `CSpellBase::InqScarabOnlyFormula @ 0x00597050` |
|
||||||
| IA-22 | **Filed 2026-08-13 (#391, user-directed: "we should only support modern resolutions. Not any old format").** The Config Resolution dropdown offers a CURATED list — the monitor's real mode enumeration filtered to modern widescreen families (16:9/16:10/21:9/32:9, ≥1280 wide, fitting the desktop; `DisplayModeCatalog.Curate`) — and its Defaults value is the desktop's own mode. Retail offered the adapter's complete enumeration including 4:3 legacy modes and authored `800x600` as the row default (`gmConfigUI::InitOptions SetDefaultValue(0x03200258)`; `gmClient::Init @0x004047af` `Device::ForceDisplayResolution(1, 0x320, 0x258)`). | `src/AcDream.App/Rendering/DisplayModeCatalog.cs`; `src/AcDream.App/UI/Layout/ConfigOptionsPageController.cs` (Resolution row); fixture fallback `src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs` (`AvailableResolutions`, 800x600 removed) | Explicit product direction; the curated list is also the fullscreen mode-switch validation source (#376/#388), so an offered mode is supported by construction — "Graphics mode not supported" crashes become unreachable from the dropdown. | A user wanting a genuine legacy 4:3 mode cannot pick it; retail-parity comparisons of the Config tab's list/default will show the deviation. | decomp sites in the Divergence column; ISSUES #391 |
|
| IA-22 | **Filed 2026-08-13 (#391, user-directed: "we should only support modern resolutions. Not any old format").** The Config Resolution dropdown offers a CURATED list — the monitor's real mode enumeration filtered to modern widescreen families (16:9/16:10/21:9/32:9, ≥1280 wide, fitting the desktop; `DisplayModeCatalog.Curate`) — and its Defaults value is the desktop's own mode. Retail offered the adapter's complete enumeration including 4:3 legacy modes and authored `800x600` as the row default (`gmConfigUI::InitOptions SetDefaultValue(0x03200258)`; `gmClient::Init @0x004047af` `Device::ForceDisplayResolution(1, 0x320, 0x258)`). | `src/AcDream.App/Rendering/DisplayModeCatalog.cs`; `src/AcDream.App/UI/Layout/ConfigOptionsPageController.cs` (Resolution row); fixture fallback `src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs` (`AvailableResolutions`, 800x600 removed) | Explicit product direction. **Amended 2026-08-16 (#407, Campaign CC gate round 1):** the dropdown now offers `DisplayModeCatalog.WindowedResolutions` — the curated hardware modes UNIONed with the static modern-ladder sizes that fit the desktop — because a WINDOWED pick is a plain Size write needing no video mode, and remote/RDP virtual displays advertise almost no modes (the live RDP display exposed only 1920x1080 + the 2056x1290 desktop, starving the dropdown). The original "an offered mode is supported by construction" invariant now holds for the FULLSCREEN half only: the fullscreen apply still validates against the hardware `Resolutions` list plus `GlfwDisplayModeSwitcher`'s monitor-mode-list hard guard, so a fullscreen pick of a windowed-only entry refuses safely (log-and-stay, #388; the #392 apply-result seam is that family's open follow-up) — "Graphics mode not supported" crashes remain unreachable from the dropdown. | A user wanting a genuine legacy 4:3 mode cannot pick it; retail-parity comparisons of the Config tab's list/default will show the deviation. | decomp sites in the Divergence column; ISSUES #391 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using AcDream.UI.Abstractions.Panels.Settings;
|
||||||
using Silk.NET.Windowing;
|
using Silk.NET.Windowing;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
@ -29,13 +30,28 @@ namespace AcDream.App.Rendering;
|
||||||
internal static class DisplayModeCatalog
|
internal static class DisplayModeCatalog
|
||||||
{
|
{
|
||||||
private static IReadOnlyList<string>? _resolutions;
|
private static IReadOnlyList<string>? _resolutions;
|
||||||
|
private static IReadOnlyList<string>? _windowedResolutions;
|
||||||
private static string? _desktopResolution;
|
private static string? _desktopResolution;
|
||||||
|
|
||||||
/// <summary>The curated list, or null when no catalog was installed
|
/// <summary>The curated HARDWARE mode list, or null when no catalog was
|
||||||
/// (fixture/headless callers — consumers fall back to the static
|
/// installed (fixture/headless callers — consumers fall back to the
|
||||||
/// preset ladder).</summary>
|
/// static preset ladder). This is the fullscreen mode-switch validation
|
||||||
|
/// source (#376/#388): a fullscreen pick must be a real adapter mode.</summary>
|
||||||
public static IReadOnlyList<string>? Resolutions => _resolutions;
|
public static IReadOnlyList<string>? Resolutions => _resolutions;
|
||||||
|
|
||||||
|
/// <summary>#407: the WINDOWED size offering — the curated hardware
|
||||||
|
/// modes UNIONed with the static modern ladder entries that fit the
|
||||||
|
/// desktop. A windowed client needs no video mode (a Size write is
|
||||||
|
/// displayable at any size ≤ desktop), so gating the windowed dropdown
|
||||||
|
/// on the adapter's mode list starved remote/virtual displays whose
|
||||||
|
/// drivers advertise almost nothing (the RDP display that exposed only
|
||||||
|
/// 1920x1080 + the desktop mode, found live at the Campaign CC gate).
|
||||||
|
/// Null when no catalog was installed. The fullscreen APPLY still
|
||||||
|
/// validates against <see cref="Resolutions"/> + the switcher's own
|
||||||
|
/// monitor-mode-list hard guard, so a fullscreen pick of a
|
||||||
|
/// windowed-only entry refuses safely (log-and-stay, #388/#392).</summary>
|
||||||
|
public static IReadOnlyList<string>? WindowedResolutions => _windowedResolutions;
|
||||||
|
|
||||||
/// <summary>The desktop's current mode as a "WxH" string — the Config
|
/// <summary>The desktop's current mode as a "WxH" string — the Config
|
||||||
/// Resolution row's Defaults value in production (see the class doc for
|
/// Resolution row's Defaults value in production (see the class doc for
|
||||||
/// why this replaces retail's authored 800x600). Null when no catalog
|
/// why this replaces retail's authored 800x600). Null when no catalog
|
||||||
|
|
@ -69,6 +85,7 @@ internal static class DisplayModeCatalog
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_resolutions = curated;
|
_resolutions = curated;
|
||||||
|
_windowedResolutions = BuildWindowedOffering(curated, (desktop.X, desktop.Y));
|
||||||
_desktopResolution = $"{desktop.X}x{desktop.Y}";
|
_desktopResolution = $"{desktop.X}x{desktop.Y}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,9 +93,59 @@ internal static class DisplayModeCatalog
|
||||||
internal static void ResetForTests()
|
internal static void ResetForTests()
|
||||||
{
|
{
|
||||||
_resolutions = null;
|
_resolutions = null;
|
||||||
|
_windowedResolutions = null;
|
||||||
_desktopResolution = null;
|
_desktopResolution = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// #407's pure union rule: the windowed offering is every curated
|
||||||
|
/// hardware mode plus every static-ladder entry that fits the desktop,
|
||||||
|
/// deduped, ascending by width then height — the same ordering
|
||||||
|
/// <see cref="Curate"/> emits so the dropdown reads identically on
|
||||||
|
/// physical and remote displays.
|
||||||
|
/// </summary>
|
||||||
|
internal static IReadOnlyList<string> BuildWindowedOffering(
|
||||||
|
IReadOnlyList<string> curated,
|
||||||
|
(int W, int H) desktop)
|
||||||
|
{
|
||||||
|
var keep = new SortedSet<(int W, int H)>(
|
||||||
|
Comparer<(int W, int H)>.Create(static (a, b) =>
|
||||||
|
a.W != b.W ? a.W.CompareTo(b.W) : a.H.CompareTo(b.H)));
|
||||||
|
|
||||||
|
foreach (string spec in curated)
|
||||||
|
{
|
||||||
|
if (TryParse(spec, out (int W, int H) mode))
|
||||||
|
keep.Add(mode);
|
||||||
|
}
|
||||||
|
foreach (string spec in DisplaySettings.AvailableResolutions)
|
||||||
|
{
|
||||||
|
if (TryParse(spec, out (int W, int H) mode)
|
||||||
|
&& mode.W <= desktop.W
|
||||||
|
&& mode.H <= desktop.H)
|
||||||
|
{
|
||||||
|
keep.Add(mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keep.Select(static m => $"{m.W}x{m.H}").ToArray();
|
||||||
|
|
||||||
|
static bool TryParse(string spec, out (int W, int H) mode)
|
||||||
|
{
|
||||||
|
mode = default;
|
||||||
|
string[] parts = spec.Split('x', 2);
|
||||||
|
if (parts.Length == 2
|
||||||
|
&& int.TryParse(parts[0], out int w)
|
||||||
|
&& int.TryParse(parts[1], out int h)
|
||||||
|
&& w > 0
|
||||||
|
&& h > 0)
|
||||||
|
{
|
||||||
|
mode = (w, h);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The pure curation rule (#391): keep a mode iff
|
/// The pure curation rule (#391): keep a mode iff
|
||||||
/// - it is a modern widescreen format (16:9, 16:10, or ultrawide 21:9 /
|
/// - it is a modern widescreen format (16:9, 16:10, or ultrawide 21:9 /
|
||||||
|
|
|
||||||
|
|
@ -2578,7 +2578,12 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
// default, installed at startup by the graphical host;
|
// default, installed at startup by the graphical host;
|
||||||
// fixture/headless mounts leave the catalog empty and the
|
// fixture/headless mounts leave the catalog empty and the
|
||||||
// controller falls back to the static preset ladder.
|
// controller falls back to the static preset ladder.
|
||||||
availableResolutions: Rendering.DisplayModeCatalog.Resolutions,
|
// #407: the dropdown offers the WINDOWED union (hardware
|
||||||
|
// modes + static-ladder sizes that fit the desktop) — a
|
||||||
|
// windowed Size write needs no video mode, and remote/RDP
|
||||||
|
// displays advertise almost none. The fullscreen APPLY
|
||||||
|
// still validates against the hardware list only.
|
||||||
|
availableResolutions: Rendering.DisplayModeCatalog.WindowedResolutions,
|
||||||
resolutionDefault: Rendering.DisplayModeCatalog.DesktopResolution);
|
resolutionDefault: Rendering.DisplayModeCatalog.DesktopResolution);
|
||||||
if (!configBound)
|
if (!configBound)
|
||||||
Console.WriteLine("[UI] options panel: Config tab rows did not bind.");
|
Console.WriteLine("[UI] options panel: Config tab rows did not bind.");
|
||||||
|
|
|
||||||
|
|
@ -105,4 +105,46 @@ public sealed class DisplayModeCatalogTests
|
||||||
// be re-selectable from the offered list.
|
// be re-selectable from the offered list.
|
||||||
Assert.Contains(DisplaySettings.Default.Resolution, DisplaySettings.AvailableResolutions);
|
Assert.Contains(DisplaySettings.Default.Resolution, DisplaySettings.AvailableResolutions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── #407: the windowed offering union (Campaign CC gate round 1) ────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildWindowedOffering_RdpStarvedModeList_GainsTheLadderSizesThatFit()
|
||||||
|
{
|
||||||
|
// The live RDP shape that motivated #407: the virtual display
|
||||||
|
// advertised exactly two modes (1920x1080 + the 2056x1290 desktop),
|
||||||
|
// so the hardware-gated dropdown offered nothing below 1920. The
|
||||||
|
// windowed union restores every static-ladder size that fits.
|
||||||
|
var offering = DisplayModeCatalog.BuildWindowedOffering(
|
||||||
|
["1920x1080", "2056x1290"], (2056, 1290));
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
["1280x720", "1366x768", "1600x900", "1920x1080", "2056x1290"],
|
||||||
|
offering);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildWindowedOffering_RichMonitor_IsTheDedupedUnion()
|
||||||
|
{
|
||||||
|
// On a physical monitor the hardware list already contains the
|
||||||
|
// ladder sizes — the union adds nothing and stays ascending/deduped.
|
||||||
|
var offering = DisplayModeCatalog.BuildWindowedOffering(
|
||||||
|
["1280x720", "1366x768", "1600x900", "1920x1080", "1920x1200", "2560x1440"],
|
||||||
|
Desktop2560);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
["1280x720", "1366x768", "1600x900", "1920x1080", "1920x1200", "2560x1440"],
|
||||||
|
offering);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildWindowedOffering_LadderEntriesLargerThanTheDesktop_StayExcluded()
|
||||||
|
{
|
||||||
|
// A 1600x900 desktop admits only the ladder sizes that fit; the
|
||||||
|
// desktop's own (hardware-curated) mode always survives the union.
|
||||||
|
var offering = DisplayModeCatalog.BuildWindowedOffering(
|
||||||
|
["1600x900"], (1600, 900));
|
||||||
|
|
||||||
|
Assert.Equal(["1280x720", "1366x768", "1600x900"], offering);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue