feat(render): Campaign V slice V11 commit 1 - delete ImGui, Studio, and the DevTools frontend
The ImGui developer-tools stack (AcDream.UI.ImGui), UI Studio (src/AcDream.App/Studio), and the DevToolsFramePresenter/ SettingsDevToolsCompositionPhase ImGui composition machinery are removed. Vulkan never composed a DevTools frontend (DevToolsEnabled already forced false whenever the backend was Vulkan); this commit makes that permanent by deleting the only implementation rather than leaving a dead branch behind. What moved: Studio/SampleData.cs is a live production dependency (InteractionRetainedUiComposition's character-sheet fallback, plus three UI.Layout test files) - git mv'd to src/AcDream.App/UI/Layout/SampleData.cs, namespace AcDream.App.UI.Layout, and trimmed to the SampleCharacter API that is actually still called (BuildObjectTable/AddItem/AddEquipped/the item-guid and icon constants had zero callers left once the Studio fixture provider that used them was deleted). What survives as backend-neutral seams, per the tests that still exercise them: IDevToolsFrameLifecycle (moved into RenderFramePreparationController.cs, now always bound to null), IFramebufferDevToolsTarget/FramebufferDevToolsBinding in FramebufferResizeController.cs (its concrete DevToolsFramebufferTarget adapter is deleted), and IDevToolsGameplayCommands in GameplayInputCommandController.cs (DevToolsGameplayCommands becomes a documented no-op instead of forwarding to the deleted presenter). A follow-up re-homes Settings/Debug onto the retained UI through IPanelRenderer; until then keybind remapping falls back to editing keybinds.json. DevToolsEnabled is now `private const bool DevToolsEnabled = false`. RuntimeOptions.DevTools is unchanged and still reaches VulkanGraphicsContext for the optional debug-utils extensions; Program.cs now logs one line when ACDREAM_DEVTOOLS=1 explaining that the ImGui UI is gone and the flag is Vulkan-only now. Removed: AcDream.UI.ImGui (project + ImGui.NET/Silk.NET.OpenGL.Extensions.ImGui package refs), src/AcDream.App/Studio (minus SampleData.cs), DevToolsFramePresenter.cs and everything only it constructed (ISettingsDevToolsCompositionFactory, RetailSettingsDevToolsCompositionFactory, DevToolsCompositionOwner, IGameWindowSettingsDevToolsPublication, SettingsDevToolsOptionalDependencies, the "developer tools" shutdown-ledger stage and its DevTools-typed fields on IngressShutdownRoots/ RenderShutdownRoots), the ui-studio Program.cs verb, and the cimgui native manifest entries in GraphicalHostPlatformServices. GameWindow.cs's DevTools composition branch, its _vitalsVm/_debugVm/_devToolsComposition/ _devToolsFramePresenter/_devToolsCommandBus fields, and every settingsDevTools .DevTools?.* access across FrameRootComposition.cs/SessionPlayerComposition.cs are gone with it. Build green; complete Release solution suite 8,830 / 5 skips (App Tests 4,097/3 skips run standalone - one #250-family zero-allocation test flakes under the full parallel `dotnet test AcDream.slnx` run, a pre-existing, documented class unrelated to this change). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
db4426d5ef
commit
844cf092a1
48 changed files with 227 additions and 6201 deletions
|
|
@ -52,7 +52,6 @@
|
|||
<ProjectReference Include="..\AcDream.Core.Net\AcDream.Core.Net.csproj" />
|
||||
<ProjectReference Include="..\AcDream.Content\AcDream.Content.csproj" />
|
||||
<ProjectReference Include="..\AcDream.UI.Abstractions\AcDream.UI.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\AcDream.UI.ImGui\AcDream.UI.ImGui.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="Rendering\Shaders\*.*">
|
||||
|
|
|
|||
|
|
@ -433,16 +433,17 @@ internal sealed class FrameRootCompositionPhase
|
|||
// Campaign V slice V6j: no debug-line renderer on the Vulkan arm.
|
||||
// DrawAndPublish flushes it INSIDE the world phase and that
|
||||
// renderer opens its own pass, which the frame's one backbuffer
|
||||
// pass forbids. The collision-wireframe toggle is DevTools-only
|
||||
// and DevTools is not composed there, so nothing is lost —
|
||||
// composing it would throw on the first wireframe frame rather
|
||||
// than silently misdraw (plan §5.5.14 item 7).
|
||||
// pass forbids. The collision-wireframe toggle was DevTools-only
|
||||
// and the ImGui DevTools frontend is gone as of Campaign V slice
|
||||
// V11, so nothing is lost — composing it would throw on the
|
||||
// first wireframe frame rather than silently misdraw (plan
|
||||
// §5.5.14 item 7).
|
||||
gl is not null ? foundation.DebugLines : null,
|
||||
d.PhysicsEngine,
|
||||
d.PlayerMode,
|
||||
d.PlayerController,
|
||||
d.DebugVmRenderFacts,
|
||||
settings.DevTools is not null);
|
||||
debugVmConsumerActive: false);
|
||||
var worldScenePasses = new WorldScenePassExecutor(
|
||||
worldPassSurface,
|
||||
worldFrameGlState,
|
||||
|
|
@ -570,11 +571,15 @@ internal sealed class FrameRootCompositionPhase
|
|||
live.PaperdollPresenter,
|
||||
live.CreatureAppraisalPresenter),
|
||||
retainedGameplayUi,
|
||||
settings.DevTools?.Presenter,
|
||||
// The ImGui developer-tools frontend was removed at Campaign V
|
||||
// slice V11; this optional hook is unbound until a follow-up
|
||||
// re-homes it onto the retained UI (IDevToolsFrameLifecycle stays
|
||||
// as the seam).
|
||||
devTools: null,
|
||||
privateScreenshot);
|
||||
var framePreparation = new RenderFramePreparationController(
|
||||
renderFrameResources,
|
||||
settings.DevTools?.Presenter,
|
||||
devTools: null,
|
||||
renderWeatherFrame);
|
||||
IRenderFramePostDiagnosticsPhase postDiagnostics =
|
||||
renderSceneShadowComparison is not null
|
||||
|
|
@ -602,8 +607,7 @@ internal sealed class FrameRootCompositionPhase
|
|||
privatePresentation,
|
||||
live.FrameDiagnostics,
|
||||
postDiagnostics,
|
||||
(IRenderFrameFailureRecovery?)settings.DevTools?.Presenter
|
||||
?? NullRenderFrameFailureRecovery.Instance);
|
||||
NullRenderFrameFailureRecovery.Instance);
|
||||
Fault(FrameRootCompositionPoint.RenderRootCreated);
|
||||
|
||||
var liveFrameCoordinator = new RetailLiveFrameCoordinator(
|
||||
|
|
|
|||
|
|
@ -426,7 +426,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
d.Character.LocalPlayer,
|
||||
playerGuid: () => d.PlayerIdentity.ServerGuid,
|
||||
activeToonName: () => d.Settings.ActiveToonKey,
|
||||
fallbackSheet: Studio.SampleData.SampleCharacter,
|
||||
fallbackSheet: SampleData.SampleCharacter,
|
||||
canSendRaise: () => late.GameRuntime.IsInWorld,
|
||||
sendRaiseAttribute: (statId, cost) =>
|
||||
late.GameRuntime.Advance(
|
||||
|
|
|
|||
|
|
@ -740,15 +740,6 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
d.MovementDiagnostics,
|
||||
d.Character.MovementSkills,
|
||||
d.ViewportAspect);
|
||||
if (d.SettingsDevTools.DevTools is { } devTools)
|
||||
{
|
||||
bindings.Adopt(
|
||||
"developer player mode",
|
||||
devTools.LateBindings.PlayerModeCommands.BindOwned(playerMode));
|
||||
bindings.Adopt(
|
||||
"developer command bus",
|
||||
devTools.CommandBus.BindOwned(liveSessionSource));
|
||||
}
|
||||
var playerModeAutoEntry = new PlayerModeAutoEntry(
|
||||
new LivePlayerModeAutoEntryContext(
|
||||
liveSessionSource,
|
||||
|
|
@ -832,8 +823,7 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
static value => value.Dispose());
|
||||
|
||||
AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? vitals =
|
||||
d.SettingsDevTools.DevTools?.Vitals
|
||||
?? interaction.RetainedUi?.Vitals;
|
||||
interaction.RetainedUi?.Vitals;
|
||||
var sessionRuntimeFactory = new LiveSessionRuntimeFactory(
|
||||
new LiveSessionPlayerRuntime(
|
||||
d.PlayerIdentity,
|
||||
|
|
@ -893,9 +883,10 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
d.Options.LivePass ?? string.Empty));
|
||||
Fault(SessionPlayerCompositionPoint.SessionHostCreated);
|
||||
|
||||
Action<string>? debugToast = d.SettingsDevTools.DevTools is { } devOwner
|
||||
? message => devOwner.Debug.AddToast(message)
|
||||
: null;
|
||||
// The ImGui developer-tools debug toast sink was removed at Campaign V
|
||||
// slice V11 along with the rest of the ImGui frontend; there is no
|
||||
// replacement toast surface yet.
|
||||
Action<string>? debugToast = null;
|
||||
var combatModeOperations = new LiveCombatModeOperations(
|
||||
new LiveSessionCombatModeAuthority(sessionHost),
|
||||
new LocalPlayerCombatEquipmentSource(
|
||||
|
|
@ -954,8 +945,7 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
var commands = new GameplayInputCommandController(
|
||||
new RetainedGameplayWindowCommands(
|
||||
interaction.RetainedUi?.Runtime),
|
||||
new DevToolsGameplayCommands(
|
||||
d.SettingsDevTools.DevTools?.Presenter),
|
||||
new DevToolsGameplayCommands(),
|
||||
runtimeDiagnostics,
|
||||
new PlayerModeGameplayCommands(
|
||||
d.PlayerMode,
|
||||
|
|
|
|||
|
|
@ -1,336 +1,27 @@
|
|||
using AcDream.App.Combat;
|
||||
using AcDream.App.Diagnostics;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Settings;
|
||||
using AcDream.Core.Chat;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Player;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
using AcDream.UI.Abstractions.Panels.Chat;
|
||||
using AcDream.UI.Abstractions.Panels.Debug;
|
||||
using AcDream.UI.Abstractions.Panels.Settings;
|
||||
using AcDream.UI.Abstractions.Panels.Vitals;
|
||||
using AcDream.UI.ImGui;
|
||||
using Silk.NET.Input;
|
||||
using Silk.NET.OpenGL;
|
||||
using Silk.NET.Windowing;
|
||||
|
||||
namespace AcDream.App.Composition;
|
||||
|
||||
/// <summary>
|
||||
/// Resolved display/audio/quality settings for the session. The optional
|
||||
/// ImGui developer-tools frontend that used to compose here (VitalsPanel,
|
||||
/// ChatPanel, DebugPanel, SettingsPanel via <c>AcDream.UI.ImGui</c>) was
|
||||
/// removed at Campaign V slice V11 along with the OpenGL backend it
|
||||
/// required — see docs/plans/2026-07-27-vulkan-campaign.md. A follow-up
|
||||
/// re-homes the Settings and Debug panels onto the retained UI through a new
|
||||
/// <c>IPanelRenderer</c> implementation; until then keybind remapping falls
|
||||
/// back to editing keybinds.json.
|
||||
/// </summary>
|
||||
internal sealed record SettingsDevToolsResult(
|
||||
AcDream.UI.Abstractions.Settings.QualitySettings ResolvedQuality,
|
||||
DevToolsCompositionOwner? DevTools);
|
||||
|
||||
internal sealed record SettingsDevToolsOptionalDependencies(
|
||||
IDevToolsRuntimeFacts Facts,
|
||||
IRuntimeKeyBindingTarget? KeyBindingTarget,
|
||||
DeferredCanonicalWorldEntityCountSource WorldEntities,
|
||||
DeferredRenderFrameDiagnosticsSource FrameDiagnostics,
|
||||
DeferredDevToolsPlayerModeCommands PlayerModeCommands);
|
||||
AcDream.UI.Abstractions.Settings.QualitySettings ResolvedQuality);
|
||||
|
||||
internal sealed record SettingsDevToolsDependencies(
|
||||
IView Window,
|
||||
RuntimeSettingsController Settings,
|
||||
IRuntimeSettingsStartupTarget StartupTarget,
|
||||
HostQuiescenceGate HostQuiescence,
|
||||
GameRuntime Runtime,
|
||||
SettingsDevToolsOptionalDependencies? DevTools,
|
||||
RuntimeDiagnosticCommandSlot DiagnosticCommands,
|
||||
CombatFeedbackSlot CombatFeedback,
|
||||
KeyBindings KeyBindings,
|
||||
FrameProfiler FrameProfiler,
|
||||
FramebufferResizeController FramebufferResize,
|
||||
Action<string> Log)
|
||||
{
|
||||
public RuntimeCommunicationState Communication =>
|
||||
Runtime.CommunicationOwner;
|
||||
|
||||
public CombatState Combat => Runtime.ActionOwner.Combat;
|
||||
|
||||
public LocalPlayerState LocalPlayer => Runtime.CharacterOwner.LocalPlayer;
|
||||
}
|
||||
|
||||
internal interface IGameWindowSettingsDevToolsPublication
|
||||
{
|
||||
void PublishDevTools(DevToolsCompositionOwner value);
|
||||
}
|
||||
|
||||
internal interface ISettingsDevToolsCompositionFactory
|
||||
{
|
||||
bool IsSupported(IInputContext input);
|
||||
|
||||
IDevToolsInputContext CreateInputContext(
|
||||
IInputContext input,
|
||||
HostQuiescenceGate quiescence);
|
||||
|
||||
IImGuiBootstrapper CreateBootstrap(
|
||||
GL gl,
|
||||
IView window,
|
||||
IInputContext input);
|
||||
|
||||
ImGuiPanelHost CreatePanelHost();
|
||||
|
||||
IDevToolsFrameBackend CreateBackend(
|
||||
IImGuiBootstrapper bootstrap,
|
||||
ImGuiPanelHost panels);
|
||||
}
|
||||
|
||||
internal sealed class RetailSettingsDevToolsCompositionFactory
|
||||
: ISettingsDevToolsCompositionFactory
|
||||
{
|
||||
public bool IsSupported(IInputContext input) =>
|
||||
input.Keyboards.Count > 0
|
||||
&& input.Mice.Count > 0
|
||||
&& input.Mice[0].ScrollWheels.Count > 0;
|
||||
|
||||
public IDevToolsInputContext CreateInputContext(
|
||||
IInputContext input,
|
||||
HostQuiescenceGate quiescence) =>
|
||||
new QuiescentInputContext(input, quiescence);
|
||||
|
||||
public IImGuiBootstrapper CreateBootstrap(
|
||||
GL gl,
|
||||
IView window,
|
||||
IInputContext input) =>
|
||||
new ImGuiBootstrapper(gl, window, input);
|
||||
|
||||
public ImGuiPanelHost CreatePanelHost() => new();
|
||||
|
||||
public IDevToolsFrameBackend CreateBackend(
|
||||
IImGuiBootstrapper bootstrap,
|
||||
ImGuiPanelHost panels) =>
|
||||
new ImGuiDevToolsFrameBackend(bootstrap, panels);
|
||||
}
|
||||
|
||||
internal enum SettingsDevToolsCompositionPoint
|
||||
{
|
||||
SettingsApplied,
|
||||
InputContextCreated,
|
||||
BootstrapCreated,
|
||||
InputActivated,
|
||||
PanelHostCreated,
|
||||
VitalsRegistered,
|
||||
ChatRegistered,
|
||||
DebugViewModelCreated,
|
||||
DiagnosticActionsBound,
|
||||
CombatFeedbackBound,
|
||||
DebugRegistered,
|
||||
SettingsViewModelBound,
|
||||
SettingsRegistered,
|
||||
BackendCreated,
|
||||
PresenterCreated,
|
||||
FramebufferBound,
|
||||
InitialLayoutApplied,
|
||||
DevToolsPublished,
|
||||
}
|
||||
IRuntimeSettingsStartupTarget StartupTarget);
|
||||
|
||||
/// <summary>
|
||||
/// Complete optional developer frontend lifetime. Construction rollback and
|
||||
/// normal shutdown share the same named, retryable release operations.
|
||||
/// </summary>
|
||||
internal sealed class DevToolsCompositionOwner : IDisposable
|
||||
{
|
||||
private readonly IDevToolsInputContext _input;
|
||||
private readonly IDevToolsFrameBackend _backend;
|
||||
private readonly ChatVM _chat;
|
||||
private readonly DebugVM _debug;
|
||||
private readonly RuntimeSettingsViewModelBinding? _settings;
|
||||
private readonly CombatFeedbackSlot _combatFeedback;
|
||||
private readonly FramebufferDevToolsBinding _framebuffer;
|
||||
private readonly ResourceShutdownTransaction _frontendShutdown;
|
||||
private bool _ownsInput;
|
||||
private bool _ownsBackend;
|
||||
private bool _ownsChat;
|
||||
private bool _ownsDebug;
|
||||
private bool _ownsSettings;
|
||||
private bool _ownsCombatBinding;
|
||||
private bool _ownsFramebuffer;
|
||||
|
||||
public DevToolsCompositionOwner(
|
||||
IDevToolsInputContext input,
|
||||
IDevToolsFrameBackend backend,
|
||||
DevToolsFramePresenter presenter,
|
||||
DevToolsCommandBusSource commandBus,
|
||||
VitalsVM vitals,
|
||||
ChatVM chat,
|
||||
DebugVM debug,
|
||||
SettingsDevToolsOptionalDependencies lateBindings,
|
||||
RuntimeSettingsViewModelBinding? settings,
|
||||
CombatFeedbackSlot combatFeedback,
|
||||
FramebufferDevToolsBinding framebuffer)
|
||||
{
|
||||
_input = input ?? throw new ArgumentNullException(nameof(input));
|
||||
_backend = backend ?? throw new ArgumentNullException(nameof(backend));
|
||||
Presenter = presenter ?? throw new ArgumentNullException(nameof(presenter));
|
||||
CommandBus = commandBus ?? throw new ArgumentNullException(nameof(commandBus));
|
||||
Vitals = vitals ?? throw new ArgumentNullException(nameof(vitals));
|
||||
_chat = chat ?? throw new ArgumentNullException(nameof(chat));
|
||||
_debug = debug ?? throw new ArgumentNullException(nameof(debug));
|
||||
LateBindings = lateBindings
|
||||
?? throw new ArgumentNullException(nameof(lateBindings));
|
||||
_settings = settings;
|
||||
_combatFeedback = combatFeedback ?? throw new ArgumentNullException(nameof(combatFeedback));
|
||||
_framebuffer = framebuffer ?? throw new ArgumentNullException(nameof(framebuffer));
|
||||
|
||||
_frontendShutdown = new ResourceShutdownTransaction(
|
||||
new ResourceShutdownStage("developer framebuffer binding",
|
||||
[
|
||||
new("framebuffer resize binding", ReleaseFramebuffer),
|
||||
]),
|
||||
new ResourceShutdownStage("developer backend",
|
||||
[
|
||||
new("ImGui backend", ReleaseBackend),
|
||||
]),
|
||||
new ResourceShutdownStage("developer view models",
|
||||
[
|
||||
new("settings view model binding", ReleaseSettings),
|
||||
new("combat feedback binding", ReleaseCombatBinding),
|
||||
new("debug combat subscriptions", ReleaseDebug),
|
||||
new("chat transcript subscription", ReleaseChat),
|
||||
]));
|
||||
}
|
||||
|
||||
public DevToolsFramePresenter Presenter { get; }
|
||||
public DevToolsCommandBusSource CommandBus { get; }
|
||||
public VitalsVM Vitals { get; }
|
||||
public DebugVM Debug => _debug;
|
||||
public SettingsDevToolsOptionalDependencies LateBindings { get; }
|
||||
|
||||
public bool IsDisposalComplete =>
|
||||
_frontendShutdown.IsComplete && !_ownsInput;
|
||||
|
||||
internal bool IsFrontendDisposalComplete => _frontendShutdown.IsComplete;
|
||||
|
||||
public void AdoptInput() => _ownsInput = true;
|
||||
public void AdoptBackend() => _ownsBackend = true;
|
||||
public void AdoptChat() => _ownsChat = true;
|
||||
public void AdoptDebug() => _ownsDebug = true;
|
||||
public void AdoptSettings() => _ownsSettings = _settings is not null;
|
||||
public void AdoptCombatBinding() => _ownsCombatBinding = true;
|
||||
public void AdoptFramebuffer() => _ownsFramebuffer = true;
|
||||
|
||||
public void CompleteOwnership()
|
||||
{
|
||||
if (!_ownsInput
|
||||
|| !_ownsBackend
|
||||
|| !_ownsChat
|
||||
|| !_ownsDebug
|
||||
|| !_ownsCombatBinding
|
||||
|| !_ownsFramebuffer
|
||||
|| (_settings is not null && !_ownsSettings))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Developer composition ownership is incomplete.");
|
||||
}
|
||||
}
|
||||
|
||||
public void DeactivateInput()
|
||||
{
|
||||
CommandBus.Deactivate();
|
||||
LateBindings.PlayerModeCommands.Deactivate();
|
||||
LateBindings.FrameDiagnostics.Deactivate();
|
||||
LateBindings.WorldEntities.Deactivate();
|
||||
if (_ownsInput)
|
||||
_input.Deactivate();
|
||||
}
|
||||
|
||||
public void DetachInput()
|
||||
{
|
||||
if (!_ownsInput)
|
||||
return;
|
||||
_input.Dispose();
|
||||
if (!_input.IsDisposalComplete)
|
||||
throw new InvalidOperationException(
|
||||
"Developer input callback cleanup remains incomplete.");
|
||||
_ownsInput = false;
|
||||
}
|
||||
|
||||
public void DisposeFrontend() => _frontendShutdown.CompleteOrThrow();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DeactivateInput();
|
||||
DisposeFrontend();
|
||||
DetachInput();
|
||||
}
|
||||
|
||||
private void ReleaseFramebuffer()
|
||||
{
|
||||
if (!_ownsFramebuffer)
|
||||
return;
|
||||
_framebuffer.Dispose();
|
||||
_ownsFramebuffer = false;
|
||||
}
|
||||
|
||||
private void ReleaseCombatBinding()
|
||||
{
|
||||
if (!_ownsCombatBinding)
|
||||
return;
|
||||
_combatFeedback.Unbind(_debug);
|
||||
_ownsCombatBinding = false;
|
||||
}
|
||||
|
||||
private void ReleaseSettings()
|
||||
{
|
||||
if (!_ownsSettings)
|
||||
return;
|
||||
_settings!.Dispose();
|
||||
_ownsSettings = false;
|
||||
}
|
||||
|
||||
private void ReleaseDebug()
|
||||
{
|
||||
if (!_ownsDebug)
|
||||
return;
|
||||
_debug.Dispose();
|
||||
_ownsDebug = false;
|
||||
}
|
||||
|
||||
private void ReleaseChat()
|
||||
{
|
||||
if (!_ownsChat)
|
||||
return;
|
||||
_chat.Dispose();
|
||||
_ownsChat = false;
|
||||
}
|
||||
|
||||
private void ReleaseBackend()
|
||||
{
|
||||
if (!_ownsBackend)
|
||||
return;
|
||||
_backend.Dispose();
|
||||
_ownsBackend = false;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class CombatFeedbackBinding : IDisposable
|
||||
{
|
||||
private readonly CombatFeedbackSlot _owner;
|
||||
private readonly DebugVM _viewModel;
|
||||
private bool _disposed;
|
||||
|
||||
public CombatFeedbackBinding(CombatFeedbackSlot owner, DebugVM viewModel)
|
||||
{
|
||||
_owner = owner ?? throw new ArgumentNullException(nameof(owner));
|
||||
_viewModel = viewModel ?? throw new ArgumentNullException(nameof(viewModel));
|
||||
_owner.Bind(_viewModel);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_owner.Unbind(_viewModel);
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Production Phase 3. Settings are required; the optional developer frontend
|
||||
/// may be abandoned only after every locally acquired edge has been removed.
|
||||
/// Production Phase 3: applies the resolved startup display/audio settings.
|
||||
/// </summary>
|
||||
internal sealed class SettingsDevToolsCompositionPhase :
|
||||
ISettingsDevToolsCompositionPhase<
|
||||
|
|
@ -340,20 +31,10 @@ internal sealed class SettingsDevToolsCompositionPhase :
|
|||
SettingsDevToolsResult>
|
||||
{
|
||||
private readonly SettingsDevToolsDependencies _dependencies;
|
||||
private readonly IGameWindowSettingsDevToolsPublication _publication;
|
||||
private readonly ISettingsDevToolsCompositionFactory _factory;
|
||||
private readonly Action<SettingsDevToolsCompositionPoint>? _faultInjection;
|
||||
|
||||
public SettingsDevToolsCompositionPhase(
|
||||
SettingsDevToolsDependencies dependencies,
|
||||
IGameWindowSettingsDevToolsPublication publication,
|
||||
ISettingsDevToolsCompositionFactory? factory = null,
|
||||
Action<SettingsDevToolsCompositionPoint>? faultInjection = null)
|
||||
public SettingsDevToolsCompositionPhase(SettingsDevToolsDependencies dependencies)
|
||||
{
|
||||
_dependencies = dependencies ?? throw new ArgumentNullException(nameof(dependencies));
|
||||
_publication = publication ?? throw new ArgumentNullException(nameof(publication));
|
||||
_factory = factory ?? new RetailSettingsDevToolsCompositionFactory();
|
||||
_faultInjection = faultInjection;
|
||||
}
|
||||
|
||||
public SettingsDevToolsResult Compose(
|
||||
|
|
@ -366,282 +47,6 @@ internal sealed class SettingsDevToolsCompositionPhase :
|
|||
ArgumentNullException.ThrowIfNull(content);
|
||||
|
||||
_dependencies.Settings.ApplyStartup(_dependencies.StartupTarget);
|
||||
Fault(SettingsDevToolsCompositionPoint.SettingsApplied);
|
||||
|
||||
DevToolsCompositionOwner? devTools = _dependencies.DevTools is { } optional
|
||||
? ComposeOptionalDevTools(platform, host, optional)
|
||||
: null;
|
||||
return new SettingsDevToolsResult(
|
||||
_dependencies.Settings.ResolvedQuality,
|
||||
devTools);
|
||||
return new SettingsDevToolsResult(_dependencies.Settings.ResolvedQuality);
|
||||
}
|
||||
|
||||
private DevToolsCompositionOwner? ComposeOptionalDevTools(
|
||||
GameWindowPlatformResult<GameWindowGraphics, IInputContext> platform,
|
||||
HostInputCameraResult host,
|
||||
SettingsDevToolsOptionalDependencies optional)
|
||||
{
|
||||
var scope = new CompositionAcquisitionScope();
|
||||
bool published = false;
|
||||
try
|
||||
{
|
||||
if (!_factory.IsSupported(platform.Input))
|
||||
{
|
||||
_dependencies.Log(
|
||||
"devtools: keyboard, mouse, or scroll input unavailable — devtools disabled");
|
||||
return null;
|
||||
}
|
||||
|
||||
var inputLease = scope.Acquire(
|
||||
"developer input context",
|
||||
() => _factory.CreateInputContext(
|
||||
platform.Input,
|
||||
_dependencies.HostQuiescence),
|
||||
static value => value.Dispose());
|
||||
IDevToolsInputContext input = inputLease.Resource;
|
||||
Fault(SettingsDevToolsCompositionPoint.InputContextCreated);
|
||||
|
||||
var bootstrapLease = scope.Acquire(
|
||||
"ImGui bootstrap",
|
||||
// ImGui is a GL-only frontend and is not ported to Vulkan — the
|
||||
// campaign deletes it at slice V11 — so a Vulkan host composes
|
||||
// no DevTools at all and never reaches here.
|
||||
() => _factory.CreateBootstrap(
|
||||
platform.Graphics.RequireGl("developer UI"),
|
||||
_dependencies.Window,
|
||||
input),
|
||||
static value => value.Dispose());
|
||||
IImGuiBootstrapper bootstrap = bootstrapLease.Resource;
|
||||
Fault(SettingsDevToolsCompositionPoint.BootstrapCreated);
|
||||
|
||||
input.Activate();
|
||||
Fault(SettingsDevToolsCompositionPoint.InputActivated);
|
||||
|
||||
ImGuiPanelHost panelHost = _factory.CreatePanelHost();
|
||||
Fault(SettingsDevToolsCompositionPoint.PanelHostCreated);
|
||||
|
||||
var vitals = new VitalsVM(
|
||||
_dependencies.Combat,
|
||||
_dependencies.LocalPlayer);
|
||||
var vitalsPanel = new VitalsPanel(vitals);
|
||||
panelHost.Register(vitalsPanel);
|
||||
Fault(SettingsDevToolsCompositionPoint.VitalsRegistered);
|
||||
|
||||
var chatLease = scope.Acquire(
|
||||
"developer chat view model",
|
||||
() => new ChatVM(
|
||||
_dependencies.Communication.Chat,
|
||||
commandTargets:
|
||||
_dependencies.Communication.CommandTargets)
|
||||
{
|
||||
FpsProvider = () => optional.Facts.Fps,
|
||||
PositionProvider = () => optional.Facts.PlayerPosition,
|
||||
},
|
||||
static value => value.Dispose());
|
||||
ChatVM chatVm = chatLease.Resource;
|
||||
var chatPanel = new ChatPanel(chatVm);
|
||||
panelHost.Register(chatPanel);
|
||||
Fault(SettingsDevToolsCompositionPoint.ChatRegistered);
|
||||
|
||||
var debugLease = scope.Acquire(
|
||||
"developer debug view model",
|
||||
() => CreateDebugViewModel(optional.Facts),
|
||||
static value => value.Dispose());
|
||||
DebugVM debugVm = debugLease.Resource;
|
||||
Fault(SettingsDevToolsCompositionPoint.DebugViewModelCreated);
|
||||
debugVm.CycleTimeOfDay = _dependencies.DiagnosticCommands.CycleTimeOfDay;
|
||||
debugVm.CycleWeather = _dependencies.DiagnosticCommands.CycleWeather;
|
||||
debugVm.ToggleCollisionWires =
|
||||
_dependencies.DiagnosticCommands.ToggleCollisionWireframes;
|
||||
debugVm.ToggleFlyMode = optional.PlayerModeCommands.ToggleFlyOrChase;
|
||||
Fault(SettingsDevToolsCompositionPoint.DiagnosticActionsBound);
|
||||
|
||||
var combatBindingLease = scope.Acquire(
|
||||
"combat feedback binding",
|
||||
() => new CombatFeedbackBinding(
|
||||
_dependencies.CombatFeedback,
|
||||
debugVm),
|
||||
static value => value.Dispose());
|
||||
Fault(SettingsDevToolsCompositionPoint.CombatFeedbackBound);
|
||||
|
||||
var debugPanel = new DebugPanel(debugVm);
|
||||
panelHost.Register(debugPanel);
|
||||
Fault(SettingsDevToolsCompositionPoint.DebugRegistered);
|
||||
|
||||
RuntimeSettingsViewModelBinding? settingsBinding = null;
|
||||
SettingsPanel? settingsPanel = null;
|
||||
CompositionAcquisitionScope.CompositionAcquisitionLease<RuntimeSettingsViewModelBinding>?
|
||||
settingsLease = null;
|
||||
if (host.InputDispatcher is not null
|
||||
&& optional.KeyBindingTarget is not null)
|
||||
{
|
||||
settingsLease = scope.Acquire(
|
||||
"settings view model binding",
|
||||
() => _dependencies.Settings.CreateViewModelBinding(
|
||||
_dependencies.KeyBindings,
|
||||
host.InputDispatcher,
|
||||
optional.KeyBindingTarget.Apply),
|
||||
static value => value.Dispose());
|
||||
settingsBinding = settingsLease.Resource;
|
||||
Fault(SettingsDevToolsCompositionPoint.SettingsViewModelBound);
|
||||
settingsPanel = new SettingsPanel(settingsBinding.ViewModel);
|
||||
panelHost.Register(settingsPanel);
|
||||
Fault(SettingsDevToolsCompositionPoint.SettingsRegistered);
|
||||
}
|
||||
|
||||
var backendLease = scope.Acquire(
|
||||
"developer frame backend",
|
||||
() => _factory.CreateBackend(bootstrap, panelHost),
|
||||
static value => value.Dispose());
|
||||
IDevToolsFrameBackend backend = backendLease.Resource;
|
||||
bootstrapLease.Transfer();
|
||||
Fault(SettingsDevToolsCompositionPoint.BackendCreated);
|
||||
|
||||
var cameraMenu = new DevToolsCameraMenuOperations(
|
||||
host.CameraController,
|
||||
optional.PlayerModeCommands);
|
||||
var commandBus = new DevToolsCommandBusSource();
|
||||
var presenter = new DevToolsFramePresenter(
|
||||
backend,
|
||||
cameraMenu,
|
||||
commandBus,
|
||||
_dependencies.FrameProfiler,
|
||||
new DevToolsPanelSet(
|
||||
vitalsPanel,
|
||||
chatPanel,
|
||||
debugPanel,
|
||||
debugVm,
|
||||
settingsPanel));
|
||||
Fault(SettingsDevToolsCompositionPoint.PresenterCreated);
|
||||
|
||||
var framebufferLease = scope.Acquire(
|
||||
"developer framebuffer binding",
|
||||
() => new FramebufferDevToolsBinding(
|
||||
_dependencies.FramebufferResize,
|
||||
new DevToolsFramebufferTarget(presenter)),
|
||||
static value => value.Dispose());
|
||||
Fault(SettingsDevToolsCompositionPoint.FramebufferBound);
|
||||
|
||||
presenter.ResetLayout(
|
||||
_dependencies.Window.Size.X,
|
||||
_dependencies.Window.Size.Y,
|
||||
DevToolsPanelLayoutCondition.FirstUseEver);
|
||||
Fault(SettingsDevToolsCompositionPoint.InitialLayoutApplied);
|
||||
|
||||
var ownerLease = scope.Acquire(
|
||||
"developer composition owner",
|
||||
() => new DevToolsCompositionOwner(
|
||||
input,
|
||||
backend,
|
||||
presenter,
|
||||
commandBus,
|
||||
vitals,
|
||||
chatVm,
|
||||
debugVm,
|
||||
optional,
|
||||
settingsBinding,
|
||||
_dependencies.CombatFeedback,
|
||||
framebufferLease.Resource),
|
||||
static value => value.Dispose());
|
||||
DevToolsCompositionOwner owner = ownerLease.Resource;
|
||||
|
||||
inputLease.Transfer();
|
||||
owner.AdoptInput();
|
||||
backendLease.Transfer();
|
||||
owner.AdoptBackend();
|
||||
chatLease.Transfer();
|
||||
owner.AdoptChat();
|
||||
debugLease.Transfer();
|
||||
owner.AdoptDebug();
|
||||
combatBindingLease.Transfer();
|
||||
owner.AdoptCombatBinding();
|
||||
if (settingsLease is not null)
|
||||
{
|
||||
settingsLease.Transfer();
|
||||
owner.AdoptSettings();
|
||||
}
|
||||
framebufferLease.Transfer();
|
||||
owner.AdoptFramebuffer();
|
||||
owner.CompleteOwnership();
|
||||
|
||||
owner = ownerLease.Publish(
|
||||
_publication.PublishDevTools);
|
||||
published = true;
|
||||
Fault(SettingsDevToolsCompositionPoint.DevToolsPublished);
|
||||
scope.Complete();
|
||||
_dependencies.Log(
|
||||
"devtools: ImGui panel host ready " +
|
||||
"(VitalsPanel + ChatPanel + DebugPanel + SettingsPanel registered)");
|
||||
return owner;
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
if (published)
|
||||
throw;
|
||||
try
|
||||
{
|
||||
scope.RollbackAndThrow(failure);
|
||||
}
|
||||
catch (Exception rolledBack) when (!HasIncompleteCleanup(rolledBack))
|
||||
{
|
||||
_dependencies.Log(
|
||||
$"devtools: ImGui init failed: {rolledBack.Message} — devtools disabled");
|
||||
return null;
|
||||
}
|
||||
|
||||
throw new System.Diagnostics.UnreachableException();
|
||||
}
|
||||
}
|
||||
|
||||
private DebugVM CreateDebugViewModel(IDevToolsRuntimeFacts facts)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(facts);
|
||||
return new DebugVM(
|
||||
() => facts.PlayerPosition,
|
||||
() => facts.PlayerHeadingDegrees,
|
||||
() => facts.PlayerCellId,
|
||||
() => facts.PlayerOnGround,
|
||||
() => facts.InPlayerMode,
|
||||
() => facts.InFlyMode,
|
||||
() => facts.VerticalVelocity,
|
||||
() => facts.EntityCount,
|
||||
() => facts.AnimatedCount,
|
||||
() => facts.VisibleLandblocks,
|
||||
() => facts.TotalLandblocks,
|
||||
() => facts.ShadowObjectCount,
|
||||
() => facts.NearestObjectDistance,
|
||||
() => facts.NearestObjectLabel,
|
||||
() => facts.Colliding,
|
||||
() => facts.CollisionWireframesVisible,
|
||||
() => facts.StreamingRadius,
|
||||
() => facts.MouseSensitivity,
|
||||
() => facts.ChaseDistance,
|
||||
() => facts.RmbOrbitHeld,
|
||||
() => facts.HourName,
|
||||
() => facts.DayFraction,
|
||||
() => facts.Weather,
|
||||
() => facts.ActiveLights,
|
||||
() => facts.RegisteredLights,
|
||||
() => facts.ParticleCount,
|
||||
() => facts.Fps,
|
||||
() => facts.FrameMilliseconds,
|
||||
_dependencies.Combat);
|
||||
}
|
||||
|
||||
private static bool HasIncompleteCleanup(Exception failure)
|
||||
{
|
||||
if (failure is ImGuiBootstrapperConstructionException)
|
||||
return true;
|
||||
if (failure is IRetryableResourceCleanup cleanup
|
||||
&& !cleanup.IsCleanupComplete)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (failure is AggregateException aggregate)
|
||||
return aggregate.InnerExceptions.Any(HasIncompleteCleanup);
|
||||
return failure.InnerException is { } inner && HasIncompleteCleanup(inner);
|
||||
}
|
||||
|
||||
private void Fault(SettingsDevToolsCompositionPoint point) =>
|
||||
_faultInjection?.Invoke(point);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,16 +30,26 @@ internal interface IDevToolsGameplayCommands
|
|||
void ToggleSettingsPanel();
|
||||
}
|
||||
|
||||
internal sealed class DevToolsGameplayCommands(DevToolsFramePresenter? presenter)
|
||||
: IDevToolsGameplayCommands
|
||||
/// <summary>
|
||||
/// The ImGui developer-tools frontend these commands used to forward to was
|
||||
/// removed at Campaign V slice V11. A follow-up re-homes the Settings and
|
||||
/// Debug panels onto the retained UI through a new <c>IPanelRenderer</c>
|
||||
/// implementation; until then these are no-ops so the frozen gameplay-action
|
||||
/// priority graph keeps a single, always-valid handoff target.
|
||||
/// </summary>
|
||||
internal sealed class DevToolsGameplayCommands : IDevToolsGameplayCommands
|
||||
{
|
||||
private readonly DevToolsFramePresenter? _presenter = presenter;
|
||||
public void ToggleDebugPanel()
|
||||
{
|
||||
}
|
||||
|
||||
public void ToggleDebugPanel() => _presenter?.ToggleDebugPanel();
|
||||
public void FocusChatInput()
|
||||
{
|
||||
}
|
||||
|
||||
public void FocusChatInput() => _presenter?.FocusChatInput();
|
||||
|
||||
public void ToggleSettingsPanel() => _presenter?.ToggleSettingsPanel();
|
||||
public void ToggleSettingsPanel()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
internal interface IPlayerModeGameplayCommands
|
||||
|
|
|
|||
|
|
@ -9,17 +9,21 @@ internal interface IInputCaptureSource
|
|||
bool DevToolsWantCaptureKeyboard { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The ImGui developer-tools frontend this used to poll for focus capture was
|
||||
/// removed at Campaign V slice V11; there is no developer UI to steal focus
|
||||
/// from world/retained-UI input anymore, so this always reports false.
|
||||
/// </summary>
|
||||
internal sealed class DevToolsInputCaptureSource
|
||||
{
|
||||
private readonly bool _enabled;
|
||||
public DevToolsInputCaptureSource(bool enabled)
|
||||
{
|
||||
_ = enabled;
|
||||
}
|
||||
|
||||
public DevToolsInputCaptureSource(bool enabled) => _enabled = enabled;
|
||||
public bool WantCaptureMouse => false;
|
||||
|
||||
public bool WantCaptureMouse =>
|
||||
_enabled && ImGuiNET.ImGui.GetIO().WantCaptureMouse;
|
||||
|
||||
public bool WantCaptureKeyboard =>
|
||||
_enabled && ImGuiNET.ImGui.GetIO().WantCaptureKeyboard;
|
||||
public bool WantCaptureKeyboard => false;
|
||||
}
|
||||
|
||||
internal sealed class RetainedUiInputCaptureSlot
|
||||
|
|
|
|||
|
|
@ -94,13 +94,11 @@ internal sealed record GraphicalHostPlatformServices(
|
|||
GraphicalHostOperatingSystem.Windows =>
|
||||
[
|
||||
new("window/input", "glfw3.dll"),
|
||||
new("developer UI", "cimgui.dll"),
|
||||
new("audio", "soft_oal.dll"),
|
||||
],
|
||||
GraphicalHostOperatingSystem.Linux =>
|
||||
[
|
||||
new("window/input", "libglfw.so.3"),
|
||||
new("developer UI", "libcimgui.so"),
|
||||
new("audio", "libopenal.so"),
|
||||
],
|
||||
_ => throw new ArgumentOutOfRangeException(
|
||||
|
|
|
|||
|
|
@ -13,24 +13,6 @@ ApplicationPathSet applicationPaths = graphicalPlatform.Paths;
|
|||
IReadOnlyList<string> migratedConfigurationFiles =
|
||||
GraphicalLegacyConfigurationMigrator.Migrate(applicationPaths);
|
||||
|
||||
if (args.Length >= 1 && args[0] == "ui-studio")
|
||||
{
|
||||
var so = AcDream.App.Studio.StudioOptions.Parse(args[1..]);
|
||||
using var sw = new AcDream.App.Studio.StudioWindow(
|
||||
so,
|
||||
applicationPaths);
|
||||
try
|
||||
{
|
||||
sw.Run();
|
||||
return 0;
|
||||
}
|
||||
catch (NotSupportedException error)
|
||||
{
|
||||
Console.Error.WriteLine(error.Message);
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Debug()
|
||||
.WriteTo.Console()
|
||||
|
|
@ -62,6 +44,14 @@ if (string.IsNullOrWhiteSpace(datDir))
|
|||
// raw env vars. See docs/architecture/code-structure.md §2 Rule 4.
|
||||
var runtimeOptions = RuntimeOptions.FromEnvironment(datDir);
|
||||
|
||||
if (runtimeOptions.DevTools)
|
||||
{
|
||||
Log.Information(
|
||||
"ACDREAM_DEVTOOLS=1: the ImGui developer UI was removed at Campaign V " +
|
||||
"slice V11 along with the OpenGL backend it required; this flag now " +
|
||||
"only selects the optional Vulkan validation/debug-utils extensions.");
|
||||
}
|
||||
|
||||
var worldGameState = new AcDream.Core.Plugins.WorldGameState();
|
||||
var worldEvents = new AcDream.Core.Plugins.WorldEvents();
|
||||
var uiRegistry = new AcDream.App.Plugins.BufferedUiRegistry();
|
||||
|
|
|
|||
|
|
@ -1,491 +0,0 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Net;
|
||||
using AcDream.UI.Abstractions;
|
||||
using AcDream.UI.Abstractions.Panels.Chat;
|
||||
using AcDream.UI.Abstractions.Panels.Debug;
|
||||
using AcDream.UI.Abstractions.Panels.Settings;
|
||||
using AcDream.UI.Abstractions.Panels.Vitals;
|
||||
using AcDream.UI.ImGui;
|
||||
using ImGuiNET;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal enum DevToolsPanelLayoutCondition
|
||||
{
|
||||
FirstUseEver,
|
||||
Always,
|
||||
}
|
||||
|
||||
internal enum DevToolsPanelKind
|
||||
{
|
||||
Vitals,
|
||||
Chat,
|
||||
Debug,
|
||||
Settings,
|
||||
}
|
||||
|
||||
internal interface IDevToolsFrameLifecycle : IRenderFrameFailureRecovery
|
||||
{
|
||||
void BeginFrame(float deltaSeconds);
|
||||
|
||||
void Render(double deltaSeconds, int viewportWidth, int viewportHeight);
|
||||
}
|
||||
|
||||
internal interface IDevToolsFrameBackend : IDisposable
|
||||
{
|
||||
void BeginFrame(float deltaSeconds);
|
||||
|
||||
void AbortFrame();
|
||||
|
||||
bool BeginMainMenuBar();
|
||||
|
||||
void EndMainMenuBar();
|
||||
|
||||
bool BeginMenu(string label);
|
||||
|
||||
void EndMenu();
|
||||
|
||||
bool MenuItem(string label, string? shortcut = null, bool selected = false);
|
||||
|
||||
void Separator();
|
||||
|
||||
void RenderPanels(PanelContext context);
|
||||
|
||||
void RenderDrawData();
|
||||
|
||||
void SetWindowLayout(
|
||||
string title,
|
||||
Vector2 position,
|
||||
Vector2 size,
|
||||
DevToolsPanelLayoutCondition condition);
|
||||
}
|
||||
|
||||
internal interface IDevToolsCameraMenuOperations
|
||||
{
|
||||
bool IsFlyMode { get; }
|
||||
|
||||
bool CollideCamera { get; set; }
|
||||
|
||||
void ToggleFlyOrChase();
|
||||
}
|
||||
|
||||
internal interface IDevToolsCommandBusSource
|
||||
{
|
||||
ICommandBus Current { get; }
|
||||
}
|
||||
|
||||
internal interface IDevToolsPanelSet
|
||||
{
|
||||
bool Contains(DevToolsPanelKind kind);
|
||||
|
||||
string? GetTitle(DevToolsPanelKind kind);
|
||||
|
||||
bool IsVisible(DevToolsPanelKind kind);
|
||||
|
||||
void Toggle(DevToolsPanelKind kind);
|
||||
|
||||
void FocusChatInput();
|
||||
|
||||
void AddDebugToast(string message);
|
||||
}
|
||||
|
||||
/// <summary>Concrete ImGui backend for the developer presentation owner.</summary>
|
||||
internal sealed class ImGuiDevToolsFrameBackend : IDevToolsFrameBackend, IDisposable
|
||||
{
|
||||
private readonly IImGuiBootstrapper _bootstrap;
|
||||
private readonly ImGuiPanelHost _panels;
|
||||
private bool _disposed;
|
||||
|
||||
public ImGuiDevToolsFrameBackend(
|
||||
IImGuiBootstrapper bootstrap,
|
||||
ImGuiPanelHost panels)
|
||||
{
|
||||
_bootstrap = bootstrap ?? throw new ArgumentNullException(nameof(bootstrap));
|
||||
_panels = panels ?? throw new ArgumentNullException(nameof(panels));
|
||||
}
|
||||
|
||||
public void BeginFrame(float deltaSeconds) => _bootstrap.BeginFrame(deltaSeconds);
|
||||
|
||||
public void AbortFrame() => _bootstrap.AbortFrame();
|
||||
|
||||
public bool BeginMainMenuBar() => ImGuiNET.ImGui.BeginMainMenuBar();
|
||||
|
||||
public void EndMainMenuBar() => ImGuiNET.ImGui.EndMainMenuBar();
|
||||
|
||||
public bool BeginMenu(string label) => ImGuiNET.ImGui.BeginMenu(label);
|
||||
|
||||
public void EndMenu() => ImGuiNET.ImGui.EndMenu();
|
||||
|
||||
public bool MenuItem(string label, string? shortcut = null, bool selected = false) =>
|
||||
ImGuiNET.ImGui.MenuItem(label, shortcut ?? string.Empty, selected);
|
||||
|
||||
public void Separator() => ImGuiNET.ImGui.Separator();
|
||||
|
||||
public void RenderPanels(PanelContext context) => _panels.RenderAll(context);
|
||||
|
||||
public void RenderDrawData() => _bootstrap.Render();
|
||||
|
||||
public void SetWindowLayout(
|
||||
string title,
|
||||
Vector2 position,
|
||||
Vector2 size,
|
||||
DevToolsPanelLayoutCondition condition)
|
||||
{
|
||||
ImGuiCond imguiCondition = condition switch
|
||||
{
|
||||
DevToolsPanelLayoutCondition.FirstUseEver => ImGuiCond.FirstUseEver,
|
||||
DevToolsPanelLayoutCondition.Always => ImGuiCond.Always,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(condition)),
|
||||
};
|
||||
ImGuiNET.ImGui.SetWindowPos(title, position, imguiCondition);
|
||||
ImGuiNET.ImGui.SetWindowSize(title, size, imguiCondition);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
_bootstrap.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>Mutable late-composition seam for player-mode menu operations.</summary>
|
||||
internal sealed class DevToolsCameraMenuOperations : IDevToolsCameraMenuOperations
|
||||
{
|
||||
private readonly CameraController _camera;
|
||||
private readonly IDevToolsPlayerModeCommands _playerMode;
|
||||
|
||||
public DevToolsCameraMenuOperations(
|
||||
CameraController camera,
|
||||
IDevToolsPlayerModeCommands playerMode)
|
||||
{
|
||||
_camera = camera ?? throw new ArgumentNullException(nameof(camera));
|
||||
_playerMode = playerMode ?? throw new ArgumentNullException(nameof(playerMode));
|
||||
}
|
||||
|
||||
public bool IsFlyMode => _camera.IsFlyMode;
|
||||
|
||||
public bool CollideCamera
|
||||
{
|
||||
get => AcDream.Core.Rendering.CameraDiagnostics.CollideCamera;
|
||||
set => AcDream.Core.Rendering.CameraDiagnostics.CollideCamera = value;
|
||||
}
|
||||
|
||||
public void ToggleFlyOrChase() => _playerMode.ToggleFlyOrChase();
|
||||
}
|
||||
|
||||
/// <summary>Resolves the reconnect-safe command bus at draw time.</summary>
|
||||
internal sealed class DevToolsCommandBusSource : IDevToolsCommandBusSource
|
||||
{
|
||||
private ILiveUiSessionTarget? _session;
|
||||
private bool _deactivated;
|
||||
|
||||
public ICommandBus Current =>
|
||||
!_deactivated && _session is { } session
|
||||
? session.Commands
|
||||
: NullCommandBus.Instance;
|
||||
|
||||
public void Bind(ILiveUiSessionTarget session)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(session);
|
||||
ObjectDisposedException.ThrowIf(_deactivated, this);
|
||||
if (_session is not null && !ReferenceEquals(_session, session))
|
||||
throw new InvalidOperationException(
|
||||
"Developer command-bus authority is already bound.");
|
||||
_session = session;
|
||||
}
|
||||
|
||||
public IDisposable BindOwned(ILiveUiSessionTarget session)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(session);
|
||||
ObjectDisposedException.ThrowIf(_deactivated, this);
|
||||
if (_session is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Developer command-bus authority is already bound.");
|
||||
}
|
||||
|
||||
_session = session;
|
||||
return new Binding(this, session);
|
||||
}
|
||||
|
||||
public void Unbind(ILiveUiSessionTarget session)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(session);
|
||||
if (ReferenceEquals(_session, session))
|
||||
_session = null;
|
||||
}
|
||||
|
||||
public void Deactivate()
|
||||
{
|
||||
_deactivated = true;
|
||||
_session = null;
|
||||
}
|
||||
|
||||
private sealed class Binding : IDisposable
|
||||
{
|
||||
private DevToolsCommandBusSource? _owner;
|
||||
private readonly ILiveUiSessionTarget _expected;
|
||||
|
||||
public Binding(
|
||||
DevToolsCommandBusSource owner,
|
||||
ILiveUiSessionTarget expected)
|
||||
{
|
||||
_owner = owner;
|
||||
_expected = expected;
|
||||
}
|
||||
|
||||
public void Dispose() =>
|
||||
Interlocked.Exchange(ref _owner, null)?.Unbind(_expected);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Typed panel operations used by menu and input presentation.</summary>
|
||||
internal sealed class DevToolsPanelSet : IDevToolsPanelSet
|
||||
{
|
||||
private readonly VitalsPanel _vitals;
|
||||
private readonly ChatPanel _chat;
|
||||
private readonly DebugPanel _debug;
|
||||
private readonly DebugVM _debugViewModel;
|
||||
private readonly SettingsPanel? _settings;
|
||||
|
||||
public DevToolsPanelSet(
|
||||
VitalsPanel vitals,
|
||||
ChatPanel chat,
|
||||
DebugPanel debug,
|
||||
DebugVM debugViewModel,
|
||||
SettingsPanel? settings)
|
||||
{
|
||||
_vitals = vitals ?? throw new ArgumentNullException(nameof(vitals));
|
||||
_chat = chat ?? throw new ArgumentNullException(nameof(chat));
|
||||
_debug = debug ?? throw new ArgumentNullException(nameof(debug));
|
||||
_debugViewModel = debugViewModel
|
||||
?? throw new ArgumentNullException(nameof(debugViewModel));
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public bool Contains(DevToolsPanelKind kind) =>
|
||||
kind != DevToolsPanelKind.Settings || _settings is not null;
|
||||
|
||||
public string? GetTitle(DevToolsPanelKind kind) => GetPanel(kind)?.Title;
|
||||
|
||||
public bool IsVisible(DevToolsPanelKind kind) => GetPanel(kind)?.IsVisible == true;
|
||||
|
||||
public void Toggle(DevToolsPanelKind kind)
|
||||
{
|
||||
if (GetPanel(kind) is { } panel)
|
||||
panel.IsVisible = !panel.IsVisible;
|
||||
}
|
||||
|
||||
public void FocusChatInput() => _chat.FocusInput();
|
||||
|
||||
public void AddDebugToast(string message) => _debugViewModel.AddToast(message);
|
||||
|
||||
private IPanel? GetPanel(DevToolsPanelKind kind) => kind switch
|
||||
{
|
||||
DevToolsPanelKind.Vitals => _vitals,
|
||||
DevToolsPanelKind.Chat => _chat,
|
||||
DevToolsPanelKind.Debug => _debug,
|
||||
DevToolsPanelKind.Settings => _settings,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(kind)),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Owns the optional ImGui developer frame, menu policy, panel actions, and
|
||||
/// reusable default layout. Retained gameplay UI remains a separate earlier
|
||||
/// presentation phase.
|
||||
/// </summary>
|
||||
internal sealed class DevToolsFramePresenter : IDevToolsFrameLifecycle
|
||||
{
|
||||
private readonly IDevToolsFrameBackend _backend;
|
||||
private readonly IDevToolsCameraMenuOperations _camera;
|
||||
private readonly IDevToolsCommandBusSource _commands;
|
||||
private readonly IDevToolsPanelSet _panels;
|
||||
private readonly AcDream.App.Diagnostics.FrameProfiler _profiler;
|
||||
private bool _frameOpen;
|
||||
|
||||
public DevToolsFramePresenter(
|
||||
IDevToolsFrameBackend backend,
|
||||
IDevToolsCameraMenuOperations camera,
|
||||
IDevToolsCommandBusSource commands,
|
||||
AcDream.App.Diagnostics.FrameProfiler profiler,
|
||||
IDevToolsPanelSet panels)
|
||||
{
|
||||
_backend = backend ?? throw new ArgumentNullException(nameof(backend));
|
||||
_camera = camera ?? throw new ArgumentNullException(nameof(camera));
|
||||
_commands = commands ?? throw new ArgumentNullException(nameof(commands));
|
||||
_profiler = profiler ?? throw new ArgumentNullException(nameof(profiler));
|
||||
_panels = panels ?? throw new ArgumentNullException(nameof(panels));
|
||||
}
|
||||
|
||||
public void BeginFrame(float deltaSeconds)
|
||||
{
|
||||
if (_frameOpen)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The previous developer-tools frame must render or abort before a new frame begins.");
|
||||
}
|
||||
|
||||
_frameOpen = true;
|
||||
_backend.BeginFrame(deltaSeconds);
|
||||
}
|
||||
|
||||
public void Render(double deltaSeconds, int viewportWidth, int viewportHeight)
|
||||
{
|
||||
if (!_frameOpen)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"BeginFrame must open the developer-tools frame before Render.");
|
||||
}
|
||||
|
||||
var context = new PanelContext((float)deltaSeconds, _commands.Current);
|
||||
DrawMenu(viewportWidth, viewportHeight);
|
||||
_backend.RenderPanels(context);
|
||||
using var stage = _profiler.BeginStage(AcDream.App.Diagnostics.FrameStage.ImGui);
|
||||
_backend.RenderDrawData();
|
||||
_frameOpen = false;
|
||||
}
|
||||
|
||||
public void AbortFrame()
|
||||
{
|
||||
if (!_frameOpen)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_backend.AbortFrame();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_frameOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void ToggleDebugPanel()
|
||||
{
|
||||
_panels.Toggle(DevToolsPanelKind.Debug);
|
||||
_panels.AddDebugToast(
|
||||
$"Debug panel {(_panels.IsVisible(DevToolsPanelKind.Debug) ? "ON" : "OFF")}");
|
||||
}
|
||||
|
||||
public void FocusChatInput() => _panels.FocusChatInput();
|
||||
|
||||
public void ToggleSettingsPanel()
|
||||
{
|
||||
_panels.Toggle(DevToolsPanelKind.Settings);
|
||||
}
|
||||
|
||||
public void ResetLayout(
|
||||
int viewportWidth,
|
||||
int viewportHeight,
|
||||
DevToolsPanelLayoutCondition condition)
|
||||
{
|
||||
float width = Math.Max(viewportWidth, 480);
|
||||
float height = Math.Max(viewportHeight, 320);
|
||||
|
||||
SetLayout(
|
||||
DevToolsPanelKind.Vitals,
|
||||
new Vector2(10f, 30f),
|
||||
new Vector2(220f, 110f),
|
||||
condition);
|
||||
SetLayout(
|
||||
DevToolsPanelKind.Chat,
|
||||
new Vector2(10f, height - 320f),
|
||||
new Vector2(450f, 300f),
|
||||
condition);
|
||||
SetLayout(
|
||||
DevToolsPanelKind.Debug,
|
||||
new Vector2(width - 380f, 30f),
|
||||
new Vector2(370f, 520f),
|
||||
condition);
|
||||
if (_panels.Contains(DevToolsPanelKind.Settings))
|
||||
{
|
||||
SetLayout(
|
||||
DevToolsPanelKind.Settings,
|
||||
new Vector2((width - 700f) * 0.5f, (height - 500f) * 0.5f),
|
||||
new Vector2(700f, 500f),
|
||||
condition);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawMenu(int viewportWidth, int viewportHeight)
|
||||
{
|
||||
if (!_backend.BeginMainMenuBar())
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
if (_backend.BeginMenu("View"))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_panels.Contains(DevToolsPanelKind.Settings)
|
||||
&& _backend.MenuItem("Settings", "F11"))
|
||||
{
|
||||
_panels.Toggle(DevToolsPanelKind.Settings);
|
||||
}
|
||||
if (_backend.MenuItem("Vitals"))
|
||||
_panels.Toggle(DevToolsPanelKind.Vitals);
|
||||
if (_backend.MenuItem("Chat"))
|
||||
_panels.Toggle(DevToolsPanelKind.Chat);
|
||||
if (_backend.MenuItem("Debug", "Ctrl+F1"))
|
||||
_panels.Toggle(DevToolsPanelKind.Debug);
|
||||
_backend.Separator();
|
||||
if (_backend.MenuItem("Reset window layout"))
|
||||
ResetLayout(
|
||||
viewportWidth,
|
||||
viewportHeight,
|
||||
DevToolsPanelLayoutCondition.Always);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_backend.EndMenu();
|
||||
}
|
||||
}
|
||||
|
||||
if (_backend.BeginMenu("Camera"))
|
||||
{
|
||||
try
|
||||
{
|
||||
string flyLabel = _camera.IsFlyMode
|
||||
? "Exit Free-Fly Mode"
|
||||
: "Enter Free-Fly Mode";
|
||||
if (_backend.MenuItem(flyLabel, "Ctrl+Shift+F"))
|
||||
_camera.ToggleFlyOrChase();
|
||||
|
||||
bool collide = _camera.CollideCamera;
|
||||
if (_backend.MenuItem(
|
||||
"Collide Camera (spring arm)",
|
||||
selected: collide))
|
||||
{
|
||||
_camera.CollideCamera = !collide;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_backend.EndMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_backend.EndMainMenuBar();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetLayout(
|
||||
DevToolsPanelKind panel,
|
||||
Vector2 position,
|
||||
Vector2 size,
|
||||
DevToolsPanelLayoutCondition condition)
|
||||
{
|
||||
string? title = _panels.GetTitle(panel);
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
_backend.SetWindowLayout(title, position, size, condition);
|
||||
}
|
||||
}
|
||||
|
|
@ -37,19 +37,6 @@ internal interface IFramebufferDevToolsTarget
|
|||
void ResetLayout(int width, int height);
|
||||
}
|
||||
|
||||
internal sealed class DevToolsFramebufferTarget(DevToolsFramePresenter presenter)
|
||||
: IFramebufferDevToolsTarget
|
||||
{
|
||||
private readonly DevToolsFramePresenter _presenter = presenter
|
||||
?? throw new ArgumentNullException(nameof(presenter));
|
||||
|
||||
public void ResetLayout(int width, int height) =>
|
||||
_presenter.ResetLayout(
|
||||
width,
|
||||
height,
|
||||
DevToolsPanelLayoutCondition.Always);
|
||||
}
|
||||
|
||||
/// <summary>Expected-owner lease for the optional Phase-3 resize edge.</summary>
|
||||
internal sealed class FramebufferDevToolsBinding : IDisposable
|
||||
{
|
||||
|
|
@ -124,8 +111,9 @@ internal sealed class FramebufferResizeController
|
|||
return;
|
||||
|
||||
// Frozen order: GL viewport, shared aspect publication, camera, then
|
||||
// optional ImGui layout reset. Late binding never replays an earlier
|
||||
// resize transition.
|
||||
// the optional developer-tools layout reset (unbound since Campaign V
|
||||
// slice V11 removed the ImGui frontend). Late binding never replays an
|
||||
// earlier resize transition.
|
||||
_viewport?.ResizeViewport(width, height);
|
||||
_viewportAspect.Update(width, height);
|
||||
_camera?.SetAspect(width / (float)height);
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ public sealed class GameWindow :
|
|||
IGameWindowPlatformPublication<GameWindowGraphics, IInputContext>,
|
||||
IGameWindowHostInputCameraPublication,
|
||||
IGameWindowContentEffectsAudioPublication,
|
||||
IGameWindowSettingsDevToolsPublication,
|
||||
IGameWindowWorldRenderPublication,
|
||||
IGameWindowInteractionRetainedUiPublication,
|
||||
IGameWindowLivePresentationPublication,
|
||||
|
|
@ -91,11 +90,11 @@ public sealed class GameWindow :
|
|||
private readonly AcDream.App.Rendering.WorldSceneDebugState
|
||||
_worldSceneDebugState = new();
|
||||
|
||||
// Phase I.2: the old StbTrueTypeSharp DebugOverlay was deleted in
|
||||
// favor of the ImGui-backed DebugPanel (see _debugVm below). The
|
||||
// TextRenderer + BitmapFont fields stay alive because they're shared
|
||||
// Phase I.2: the old StbTrueTypeSharp DebugOverlay was deleted in favor of
|
||||
// the ImGui-backed DebugPanel, which Campaign V slice V11 removed in turn.
|
||||
// The TextRenderer + BitmapFont fields stay alive because they're shared
|
||||
// with UiHost and reserved for the future world-space HUD (D.6 —
|
||||
// damage floaters, name plates) where ImGui can't reach into the 3D
|
||||
// damage floaters, name plates) where ImGui couldn't reach into the 3D
|
||||
// scene. They are no longer used for any debug overlay.
|
||||
private TextRenderer? _textRenderer;
|
||||
private BitmapFont? _debugFont;
|
||||
|
|
@ -379,12 +378,9 @@ public sealed class GameWindow :
|
|||
public AcDream.Core.Player.LocalPlayerState LocalPlayer =>
|
||||
_runtimeCharacter.LocalPlayer;
|
||||
|
||||
// Phase D.2a — ImGui devtools UI overlay. Null unless ACDREAM_DEVTOOLS=1.
|
||||
// See docs/plans/2026-04-24-ui-framework.md for the staged UI strategy.
|
||||
private AcDream.App.Rendering.DevToolsFramePresenter? _devToolsFramePresenter;
|
||||
private AcDream.App.Rendering.DevToolsCommandBusSource? _devToolsCommandBus;
|
||||
private DevToolsCompositionOwner? _devToolsComposition;
|
||||
private AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? _vitalsVm;
|
||||
// Phase D.2a — ImGui devtools UI overlay. Removed at Campaign V slice V11;
|
||||
// see docs/plans/2026-04-24-ui-framework.md for the staged UI strategy and
|
||||
// docs/plans/2026-07-27-vulkan-campaign.md for the removal.
|
||||
private AcDream.UI.Abstractions.Panels.Chat.ChatVM? _retailChatVm;
|
||||
// Phase D.2b — retained host + composition runtime. Null unless ACDREAM_RETAIL_UI=1.
|
||||
private AcDream.App.UI.UiHost? _uiHost;
|
||||
|
|
@ -418,19 +414,13 @@ public sealed class GameWindow :
|
|||
_creatureAppraisalFramePresenter;
|
||||
// Phase D.2b Task 9 — plugin UI registrations buffered before OnLoad; drained in OnLoad.
|
||||
private readonly AcDream.App.Plugins.BufferedUiRegistry? _uiRegistry;
|
||||
// Phase I.2: ImGui debug panel ViewModel. The devtools presenter owns
|
||||
// its panel; the VM remains here because runtime feedback producers bind
|
||||
// directly to it during composition.
|
||||
private AcDream.UI.Abstractions.Panels.Debug.DebugVM? _debugVm;
|
||||
// DevToolsEnabled reads through typed RuntimeOptions.
|
||||
//
|
||||
// Campaign V slice V6h: the developer frontend is ImGui, which is not ported
|
||||
// to Vulkan and which slice V11 deletes outright, so a Vulkan host composes
|
||||
// none regardless of ACDREAM_DEVTOOLS. The flag still reaches
|
||||
// VulkanInstanceFactory, where it selects the optional debug-utils
|
||||
// instance extensions.
|
||||
private bool DevToolsEnabled =>
|
||||
_options.DevTools && _options.RenderBackend != RenderBackendKind.Vulkan;
|
||||
// Campaign V slice V11 deleted the ImGui developer-tools frontend along
|
||||
// with the OpenGL backend it required, so no host ever composes a
|
||||
// developer UI regardless of ACDREAM_DEVTOOLS. The flag still reaches
|
||||
// VulkanGraphicsContext, where it selects the optional debug-utils
|
||||
// instance/device extensions — see the ACDREAM_DEVTOOLS log line in
|
||||
// Run() below, which is the one remaining observable effect of the flag.
|
||||
private const bool DevToolsEnabled = false;
|
||||
|
||||
// Phase G.1-G.2 world lighting/time state. The environment owner keeps
|
||||
// the clock, selected day group, and weather transitions coherent.
|
||||
|
|
@ -922,26 +912,6 @@ public sealed class GameWindow :
|
|||
_audioSink = value.HookSink;
|
||||
}
|
||||
|
||||
void IGameWindowSettingsDevToolsPublication.PublishDevTools(
|
||||
DevToolsCompositionOwner value)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
if (_devToolsComposition is not null
|
||||
|| _devToolsFramePresenter is not null
|
||||
|| _devToolsCommandBus is not null
|
||||
|| _debugVm is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The GameWindow composition shell already owns developer tools.");
|
||||
}
|
||||
|
||||
_devToolsComposition = value;
|
||||
_devToolsFramePresenter = value.Presenter;
|
||||
_devToolsCommandBus = value.CommandBus;
|
||||
_vitalsVm = value.Vitals;
|
||||
_debugVm = value.Debug;
|
||||
}
|
||||
|
||||
void IGameWindowWorldRenderPublication.PublishBindlessSupport(
|
||||
BindlessSupport value) =>
|
||||
PublishCompositionOwner(
|
||||
|
|
@ -1048,7 +1018,6 @@ public sealed class GameWindow :
|
|||
{
|
||||
_uiHost = retained.Host;
|
||||
_retailUiRuntime = retained.Runtime;
|
||||
_vitalsVm ??= retained.Vitals;
|
||||
_retailChatVm = retained.Chat;
|
||||
_characterSheetProvider = retained.CharacterSheet;
|
||||
_frameScreenshots = retained.Screenshots;
|
||||
|
|
@ -1347,65 +1316,15 @@ public sealed class GameWindow :
|
|||
Console.Error.WriteLine),
|
||||
this).Compose(platformResult, hostInputCamera),
|
||||
(platformResult, hostInputCamera, contentEffectsAudio) =>
|
||||
{
|
||||
SettingsDevToolsOptionalDependencies? optionalDevTools = null;
|
||||
if (DevToolsEnabled)
|
||||
{
|
||||
var devToolsWorldEntities =
|
||||
new DeferredCanonicalWorldEntityCountSource();
|
||||
var devToolsFrameDiagnostics =
|
||||
new DeferredRenderFrameDiagnosticsSource();
|
||||
var devToolsPlayerModeCommands =
|
||||
new DeferredDevToolsPlayerModeCommands();
|
||||
var devToolsFacts = new DevToolsRuntimeFacts(
|
||||
_localPlayerMode,
|
||||
_playerControllerSlot,
|
||||
hostInputCamera.CameraController,
|
||||
devToolsWorldEntities,
|
||||
_animatedEntities,
|
||||
_debugVmRenderFacts,
|
||||
_physicsEngine,
|
||||
_worldSceneDebugState,
|
||||
_renderRange,
|
||||
hostInputCamera.CameraPointerInput,
|
||||
_worldEnvironment,
|
||||
Lighting,
|
||||
contentEffectsAudio.ParticleSystem,
|
||||
devToolsFrameDiagnostics);
|
||||
IRuntimeKeyBindingTarget? keyBindingTarget =
|
||||
hostInputCamera.InputDispatcher is { } settingsDispatcher
|
||||
? new RuntimeKeyBindingTarget(
|
||||
settingsDispatcher,
|
||||
_applicationPaths.KeyBindingsFile)
|
||||
: null;
|
||||
optionalDevTools = new SettingsDevToolsOptionalDependencies(
|
||||
devToolsFacts,
|
||||
keyBindingTarget,
|
||||
devToolsWorldEntities,
|
||||
devToolsFrameDiagnostics,
|
||||
devToolsPlayerModeCommands);
|
||||
}
|
||||
|
||||
return new SettingsDevToolsCompositionPhase(
|
||||
new SettingsDevToolsCompositionPhase(
|
||||
new SettingsDevToolsDependencies(
|
||||
_window!,
|
||||
_runtimeSettings,
|
||||
new RuntimeSettingsStartupTargets(
|
||||
new SilkRuntimeDisplayWindowTarget(_window!),
|
||||
_displayFramePacing,
|
||||
hostInputCamera.CameraController,
|
||||
contentEffectsAudio.Audio?.Engine),
|
||||
_hostQuiescence,
|
||||
_runtime,
|
||||
optionalDevTools,
|
||||
_runtimeDiagnosticCommands,
|
||||
_combatFeedback,
|
||||
_keyBindings,
|
||||
_frameProfiler,
|
||||
_framebufferResize,
|
||||
Console.WriteLine),
|
||||
this).Compose(platformResult, hostInputCamera, contentEffectsAudio);
|
||||
},
|
||||
contentEffectsAudio.Audio?.Engine)))
|
||||
.Compose(platformResult, hostInputCamera, contentEffectsAudio),
|
||||
(platformResult, contentEffectsAudio, settingsDevTools) =>
|
||||
{
|
||||
const uint initialCenterLandblockId = 0xA9B4FFFFu;
|
||||
|
|
@ -1433,9 +1352,10 @@ public sealed class GameWindow :
|
|||
},
|
||||
(platformResult, hostInputCamera, contentEffectsAudio, settingsDevTools, worldRender) =>
|
||||
{
|
||||
Action<string>? compositionToast = settingsDevTools.DevTools is { } devTools
|
||||
? text => devTools.Debug.AddToast(text)
|
||||
: null;
|
||||
// The ImGui developer-tools debug toast sink was removed at
|
||||
// Campaign V slice V11 along with the rest of the ImGui
|
||||
// frontend; there is no replacement toast surface yet.
|
||||
Action<string>? compositionToast = null;
|
||||
return new InteractionRetainedUiCompositionPhase(
|
||||
new InteractionRetainedUiDependencies(
|
||||
_options,
|
||||
|
|
@ -1469,7 +1389,7 @@ public sealed class GameWindow :
|
|||
_window!,
|
||||
viewPlane),
|
||||
_uiFrameDiagnostics,
|
||||
settingsDevTools.DevTools?.Vitals,
|
||||
ExistingVitals: null,
|
||||
compositionToast,
|
||||
ClientTimerNow,
|
||||
Console.WriteLine,
|
||||
|
|
@ -1490,9 +1410,10 @@ public sealed class GameWindow :
|
|||
worldRender,
|
||||
interactionUi) =>
|
||||
{
|
||||
Action<string>? compositionToast = settingsDevTools.DevTools is { } devTools
|
||||
? text => devTools.Debug.AddToast(text)
|
||||
: null;
|
||||
// The ImGui developer-tools debug toast sink was removed at
|
||||
// Campaign V slice V11 along with the rest of the ImGui
|
||||
// frontend; there is no replacement toast surface yet.
|
||||
Action<string>? compositionToast = null;
|
||||
return new LivePresentationCompositionPhase(
|
||||
new LivePresentationDependencies(
|
||||
_options,
|
||||
|
|
@ -1527,8 +1448,8 @@ public sealed class GameWindow :
|
|||
_hookRouter,
|
||||
_renderDiagnosticLog,
|
||||
WorldTime,
|
||||
settingsDevTools.DevTools?.LateBindings.WorldEntities,
|
||||
settingsDevTools.DevTools?.LateBindings.FrameDiagnostics,
|
||||
DevWorldEntities: null,
|
||||
DevFrameDiagnostics: null,
|
||||
_uiFrameDiagnostics,
|
||||
Console.WriteLine,
|
||||
compositionToast),
|
||||
|
|
@ -1761,7 +1682,6 @@ public sealed class GameWindow :
|
|||
_kbSource,
|
||||
_retailUiLease,
|
||||
_uiHost,
|
||||
_devToolsComposition,
|
||||
_runtime,
|
||||
_runtimeSettings,
|
||||
_movementInput,
|
||||
|
|
@ -1797,7 +1717,6 @@ public sealed class GameWindow :
|
|||
new RenderShutdownRoots(
|
||||
_gpuFrameFlights,
|
||||
_gpuDevice,
|
||||
_devToolsComposition,
|
||||
_localPlayerTeleport,
|
||||
_portalTunnelFallback,
|
||||
_paperdollViewportRenderer,
|
||||
|
|
|
|||
|
|
@ -63,7 +63,6 @@ internal sealed record IngressShutdownRoots(
|
|||
RetailUiRuntimeLease RetailUi,
|
||||
// Keeps failed physical UI bindings alive through native-window release.
|
||||
UiHost? RetainedUiHost,
|
||||
DevToolsCompositionOwner? DevTools,
|
||||
GameRuntime Runtime,
|
||||
RuntimeSettingsController Settings,
|
||||
DispatcherMovementInputSource MovementInput,
|
||||
|
|
@ -102,7 +101,6 @@ internal sealed record LiveShutdownRoots(
|
|||
internal sealed record RenderShutdownRoots(
|
||||
GpuFrameFlightController? FrameFlights,
|
||||
IGpuDevice? GpuDevice,
|
||||
DevToolsCompositionOwner? DevTools,
|
||||
LocalPlayerTeleportController? LocalTeleport,
|
||||
TransferableResourceSlot<PortalTunnelPresentation> PortalTunnelFallback,
|
||||
PaperdollViewportRenderer? Paperdoll,
|
||||
|
|
@ -350,7 +348,6 @@ internal static class GameWindowShutdownManifest
|
|||
Hard("mouse source", () => ingress.MouseSource?.Deactivate()),
|
||||
Hard("keyboard source", () => ingress.KeyboardSource?.Deactivate()),
|
||||
Hard("retained UI input", ingress.RetailUi.QuiesceInput),
|
||||
Hard("developer tools input", () => ingress.DevTools?.DeactivateInput()),
|
||||
Hard("game runtime session", ingress.Runtime.StopSession),
|
||||
]),
|
||||
new ResourceShutdownStage("physical ingress cleanup",
|
||||
|
|
@ -359,7 +356,6 @@ internal static class GameWindowShutdownManifest
|
|||
Soft("retained gameplay", () => DisposeRetainedGameplay(ingress.RetainedGameplay)),
|
||||
Soft("gameplay actions", () => DisposeGameplayActions(ingress.GameplayActions)),
|
||||
Soft("retained UI input", ingress.RetailUi.DeactivateInput),
|
||||
Soft("developer tools input", () => ingress.DevTools?.DetachInput()),
|
||||
Soft("camera pointer", () => DisposeCameraPointer(ingress.CameraPointer)),
|
||||
Soft("dispatcher", () => DisposeDispatcher(ingress)),
|
||||
Soft("mouse source", () => DisposeMouseSource(ingress.MouseSource)),
|
||||
|
|
@ -420,7 +416,6 @@ internal static class GameWindowShutdownManifest
|
|||
]),
|
||||
new ResourceShutdownStage("render frontends",
|
||||
[
|
||||
Hard("developer tools", () => DisposeDevToolsFrontend(render.DevTools)),
|
||||
Hard("portal tunnel", () =>
|
||||
{
|
||||
render.LocalTeleport?.Dispose();
|
||||
|
|
@ -600,12 +595,4 @@ internal static class GameWindowShutdownManifest
|
|||
throw new InvalidOperationException("OpenAL native-resource cleanup remains pending.");
|
||||
}
|
||||
|
||||
private static void DisposeDevToolsFrontend(DevToolsCompositionOwner? owner)
|
||||
{
|
||||
if (owner is null)
|
||||
return;
|
||||
owner.DisposeFrontend();
|
||||
if (!owner.IsFrontendDisposalComplete)
|
||||
throw new InvalidOperationException("Developer-tools frontend cleanup remains incomplete.");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,20 @@ internal interface IRenderWeatherFramePhase
|
|||
void Tick(double deltaSeconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optional per-frame developer-presentation hook. Its one production
|
||||
/// implementation (the ImGui developer-tools frontend) was removed at
|
||||
/// Campaign V slice V11; the interface survives as the seam a follow-up
|
||||
/// re-homing Settings/Debug onto the retained UI will implement, and every
|
||||
/// current caller already treats it as optional (<c>null</c>-conditional).
|
||||
/// </summary>
|
||||
internal interface IDevToolsFrameLifecycle : IRenderFrameFailureRecovery
|
||||
{
|
||||
void BeginFrame(float deltaSeconds);
|
||||
|
||||
void Render(double deltaSeconds, int viewportWidth, int viewportHeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Preserves the accepted pre-world order after the GPU/resource transaction:
|
||||
/// begin optional developer UI, advance render-time weather, then hand the
|
||||
|
|
|
|||
|
|
@ -1,235 +0,0 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.UI;
|
||||
|
||||
namespace AcDream.App.Studio;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// DumpLayout — load a panel from the retail UI layout dump
|
||||
//
|
||||
// The dump stores every node's rect in ABSOLUTE screen coordinates (the
|
||||
// panel's design position in the retail UI, not relative to its parent).
|
||||
// Evidence: for the "inventory" panel, the root node is at x=500,y=138 and
|
||||
// its direct children are also at x=500,y=161 — the child y=161 is only
|
||||
// 23 pixels below the parent y=138, which makes sense as a child offset
|
||||
// (the header row), not as the raw rect. If the rects were parent-relative,
|
||||
// (500,161) would place the child way off the window.
|
||||
//
|
||||
// DumpLayout converts absolute → parent-relative by computing:
|
||||
// child.Left = child.Rect.X - parent.Rect.X
|
||||
// child.Top = child.Rect.Y - parent.Rect.Y
|
||||
//
|
||||
// The root node (ParentTraversalIndex == null) is placed at (0,0) so the
|
||||
// whole tree sits at the UiHost origin rather than at the panel's retail
|
||||
// screen position.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Builds a static <see cref="UiElement"/> tree from the retail UI layout dump
|
||||
/// JSON. The tree is a hierarchy of <see cref="DumpSpriteElement"/> (draws its
|
||||
/// sprite) or plain <see cref="UiElement"/> containers (Group nodes), with each
|
||||
/// node's <see cref="UiElement.EventId"/> set to the dump's element_id and
|
||||
/// <see cref="UiElement.Name"/> set to the widget_kind string.
|
||||
///
|
||||
/// <para>This source is STATIC — no controllers, no FixtureProvider, no live
|
||||
/// game data. It is a build reference for the UI Studio showing any of the 26
|
||||
/// retail windows without needing the production panel wired up.</para>
|
||||
/// </summary>
|
||||
public static class DumpLayout
|
||||
{
|
||||
/// <summary>
|
||||
/// Parse the dump at <paramref name="dumpPath"/>, find the panel whose slug
|
||||
/// matches <paramref name="slug"/>, and build a <see cref="UiElement"/> tree.
|
||||
///
|
||||
/// <para>
|
||||
/// <paramref name="resolve"/> maps a RenderSurface id (0x06xxxxxx) to a
|
||||
/// (GL texture handle, native width, native height) triple — pass
|
||||
/// <c>RenderStack.ResolveChrome</c> from the studio, or a stub returning
|
||||
/// (1,1,1) for tests.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>Returns null and sets <paramref name="error"/> on failure.</para>
|
||||
/// </summary>
|
||||
public static UiElement? Load(
|
||||
string dumpPath,
|
||||
string slug,
|
||||
Func<uint, (uint, int, int)> resolve,
|
||||
out string? error)
|
||||
{
|
||||
// ── 1. Parse the dump JSON ────────────────────────────────────────
|
||||
var dump = UiDumpModel.Parse(dumpPath);
|
||||
if (dump is null)
|
||||
{
|
||||
error = $"[dump] Failed to parse '{dumpPath}'.";
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── 2. Find the requested panel ───────────────────────────────────
|
||||
var panel = dump.Panels.FirstOrDefault(
|
||||
p => string.Equals(p.Slug, slug, StringComparison.OrdinalIgnoreCase));
|
||||
if (panel is null)
|
||||
{
|
||||
error = $"[dump] Panel slug '{slug}' not found. " +
|
||||
$"Available: {string.Join(", ", dump.Panels.Select(p => p.Slug))}";
|
||||
return null;
|
||||
}
|
||||
|
||||
if (panel.Nodes.Count == 0)
|
||||
{
|
||||
error = $"[dump] Panel '{slug}' has no nodes.";
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── 3. Build a traversal-index → node lookup ──────────────────────
|
||||
var byIndex = new Dictionary<int, DumpNode>(panel.Nodes.Count);
|
||||
foreach (var n in panel.Nodes)
|
||||
byIndex[n.TraversalIndex] = n;
|
||||
|
||||
// ── 4. Create UiElement objects for every node ────────────────────
|
||||
var elements = new Dictionary<int, UiElement>(panel.Nodes.Count);
|
||||
foreach (var node in panel.Nodes)
|
||||
{
|
||||
var el = BuildElement(node, resolve);
|
||||
elements[node.TraversalIndex] = el;
|
||||
}
|
||||
|
||||
// ── 5. Wire parent–child relationships + set parent-relative coords ─
|
||||
UiElement? root = null;
|
||||
foreach (var node in panel.Nodes)
|
||||
{
|
||||
var el = elements[node.TraversalIndex];
|
||||
|
||||
if (node.ParentTraversalIndex is null)
|
||||
{
|
||||
// Root node — place at (0,0) so the tree sits at the UiHost origin.
|
||||
// The panel's absolute rect offset is discarded here (it was the
|
||||
// retail design position inside the retail screen, which we don't need).
|
||||
el.Left = 0f;
|
||||
el.Top = 0f;
|
||||
root = el;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-root: convert absolute → parent-relative by subtracting parent rect.
|
||||
// child.Left = child.Rect.X - parent.Rect.X
|
||||
// child.Top = child.Rect.Y - parent.Rect.Y
|
||||
// This preserves the visual layout inside each group without placing the
|
||||
// entire panel at its retail screen origin.
|
||||
var parentNode = byIndex[node.ParentTraversalIndex.Value];
|
||||
el.Left = node.Rect.X - parentNode.Rect.X;
|
||||
el.Top = node.Rect.Y - parentNode.Rect.Y;
|
||||
|
||||
var parentEl = elements[node.ParentTraversalIndex.Value];
|
||||
parentEl.AddChild(el);
|
||||
}
|
||||
}
|
||||
|
||||
if (root is null)
|
||||
{
|
||||
error = $"[dump] Panel '{slug}': no root node found (all nodes have a parent).";
|
||||
return null;
|
||||
}
|
||||
|
||||
// Give the root the full panel dimensions (from the dump's width/height record).
|
||||
root.Width = panel.Width;
|
||||
root.Height = panel.Height;
|
||||
|
||||
error = null;
|
||||
return root;
|
||||
}
|
||||
|
||||
// ── Private helpers ───────────────────────────────────────────────────────
|
||||
|
||||
private static UiElement BuildElement(
|
||||
DumpNode node,
|
||||
Func<uint, (uint, int, int)> resolve)
|
||||
{
|
||||
uint imageId = UiDumpModel.PickImageId(node);
|
||||
var kind = node.WidgetKind ?? "Group";
|
||||
|
||||
UiElement el;
|
||||
if (imageId != 0 && !string.Equals(kind, "Group", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Sprite/Button/Scrollbar/Slider — create a sprite-drawing element.
|
||||
el = new DumpSpriteElement(imageId, resolve)
|
||||
{
|
||||
Name = kind,
|
||||
ClickThrough = true, // static mockup; no behavior
|
||||
Anchors = AnchorEdges.None,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// Group (or sprite without an image) — plain container, no own draw.
|
||||
el = new DumpGroupElement()
|
||||
{
|
||||
Name = kind,
|
||||
ClickThrough = true,
|
||||
Anchors = AnchorEdges.None,
|
||||
};
|
||||
}
|
||||
|
||||
// EventId is set from the dump's element_id (cast to uint — the decimal
|
||||
// values in the JSON represent the same dat handle used at runtime).
|
||||
el.EventId = (uint)node.ElementId;
|
||||
el.Left = node.Rect.X; // overwritten by caller per root/child logic
|
||||
el.Top = node.Rect.Y;
|
||||
el.Width = node.Rect.Width;
|
||||
el.Height = node.Rect.Height;
|
||||
|
||||
return el;
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// DumpSpriteElement — minimal element that draws a single sprite
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Draws a single sprite at its native size tiled to fill <see cref="UiElement.Width"/>
|
||||
/// × <see cref="UiElement.Height"/>. Used for Sprite/Button/Scrollbar/Slider nodes from
|
||||
/// the retail UI dump.
|
||||
///
|
||||
/// <para>We do NOT reuse <see cref="AcDream.App.UI.Layout.UiDatElement"/> here because
|
||||
/// that class requires an <c>ElementInfo</c> with a populated <c>StateMedia</c>
|
||||
/// dictionary — the dat-import plumbing — which is not needed for a static dump
|
||||
/// preview. A minimal subclass keeps the code simpler and the dependency surface
|
||||
/// smaller.</para>
|
||||
/// </summary>
|
||||
internal sealed class DumpSpriteElement : UiElement
|
||||
{
|
||||
private readonly uint _imageId;
|
||||
private readonly Func<uint, (uint tex, int w, int h)> _resolve;
|
||||
|
||||
public DumpSpriteElement(uint imageId, Func<uint, (uint tex, int w, int h)> resolve)
|
||||
{
|
||||
_imageId = imageId;
|
||||
_resolve = resolve;
|
||||
}
|
||||
|
||||
protected override void OnDraw(UiRenderContext ctx)
|
||||
{
|
||||
if (_imageId == 0) return;
|
||||
|
||||
var (tex, tw, th) = _resolve(_imageId);
|
||||
if (tex == 0 || tw == 0 || th == 0) return;
|
||||
|
||||
// Tile at native resolution (same as UiDatElement.OnDraw — UV-repeat on both
|
||||
// axes via GL_REPEAT, Width/tw and Height/th tile the texture).
|
||||
ctx.DrawSprite(tex, 0, 0, Width, Height,
|
||||
0, 0, Width / tw, Height / th, Vector4.One);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// DumpGroupElement — pure container (Group nodes from the dump)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Container element for dump Group nodes — no own draw, just hosts children.
|
||||
/// Extending UiElement directly (no OnDraw override) gives transparent groups,
|
||||
/// which matches Group nodes in the retail layout that have no background sprite.
|
||||
/// </summary>
|
||||
internal sealed class DumpGroupElement : UiElement
|
||||
{
|
||||
// No OnDraw — completely transparent container.
|
||||
}
|
||||
|
|
@ -1,225 +0,0 @@
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.UI.Abstractions.Panels.Settings;
|
||||
using DatReaderWriter;
|
||||
using AcDream.Content;
|
||||
|
||||
namespace AcDream.App.Studio;
|
||||
|
||||
/// <summary>
|
||||
/// Populates a loaded panel with sample data by calling the production
|
||||
/// controller Bind methods against <see cref="SampleData"/>.
|
||||
///
|
||||
/// <para>
|
||||
/// The studio is intentionally thin — there is no live game session, no
|
||||
/// server connection, and no network. FixtureProvider bridges that gap by
|
||||
/// feeding static fixtures (vitals percentages, a fake inventory, empty
|
||||
/// shortcut lists) so the bound widgets show plausible state instead of
|
||||
/// empty zeroes.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>IconIds approach:</b> raw-resolve stub — resolve the base <c>iconId</c>
|
||||
/// via <see cref="RenderStack.ResolveChrome"/> and return the GL handle
|
||||
/// directly. This is intentionally simpler than GameWindow's full
|
||||
/// <see cref="IconComposer"/> (5-layer composite). The raw icon is enough
|
||||
/// to confirm the grid cells draw something in the studio; the full
|
||||
/// compositor is the live-game concern, not the layout preview concern.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class FixtureProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Populate <paramref name="layout"/> with sample data appropriate for
|
||||
/// <paramref name="layoutId"/>. Calls the production controller Bind
|
||||
/// methods so the panel's widgets drive off the same code path as the
|
||||
/// full game.
|
||||
/// </summary>
|
||||
/// <param name="layoutId">The LayoutDesc dat id used to import the layout.</param>
|
||||
/// <param name="layout">The imported layout whose widgets to populate.</param>
|
||||
/// <param name="stack">The live render stack (for <see cref="RenderStack.ResolveChrome"/>
|
||||
/// and <see cref="RenderStack.VitalsDatFont"/>).</param>
|
||||
/// <param name="objects">A <see cref="ClientObjectTable"/> already seeded by
|
||||
/// <see cref="SampleData.BuildObjectTable"/>.</param>
|
||||
/// <param name="dats">The live DAT collection used to resolve per-list empty-slot sprites
|
||||
/// (same lookup GameWindow.OnLoad performs for the production binding).</param>
|
||||
public static IRetainedPanelController? Populate(
|
||||
uint layoutId,
|
||||
ImportedLayout layout,
|
||||
RenderStack stack,
|
||||
ClientObjectTable objects,
|
||||
IDatReaderWriter dats)
|
||||
{
|
||||
switch (layoutId)
|
||||
{
|
||||
case 0x2100006Cu: // vitals
|
||||
VitalsController.Bind(layout,
|
||||
healthPct: () => SampleData.HealthPct,
|
||||
staminaPct: () => SampleData.StaminaPct,
|
||||
manaPct: () => SampleData.ManaPct,
|
||||
healthText: () => "80/100",
|
||||
staminaText: () => "60/100",
|
||||
manaText: () => "90/100");
|
||||
return null;
|
||||
|
||||
case RadarController.LayoutId: // gmRadarUI
|
||||
return RadarController.Bind(
|
||||
layout,
|
||||
() => new UiRadarSnapshot(
|
||||
PlayerHeadingDegrees: 32f,
|
||||
Blips:
|
||||
[
|
||||
new UiRadarBlip(0x50000001u, "Portal to Holtburg", 82f, 43f,
|
||||
RadarColor(AcDream.Core.Ui.RadarBlipColors.Portal),
|
||||
AcDream.Core.Ui.RadarBlipShape.Plus),
|
||||
new UiRadarBlip(0x50000002u, "Drudge Prowler", 43f, 76f,
|
||||
RadarColor(AcDream.Core.Ui.RadarBlipColors.Creature),
|
||||
AcDream.Core.Ui.RadarBlipShape.Plus),
|
||||
new UiRadarBlip(0x50000003u, "Town Crier", 68f, 87f,
|
||||
RadarColor(AcDream.Core.Ui.RadarBlipColors.NPC),
|
||||
AcDream.Core.Ui.RadarBlipShape.Box),
|
||||
new UiRadarBlip(0x50000004u, "Selected PK", 39f, 39f,
|
||||
RadarColor(AcDream.Core.Ui.RadarBlipColors.PlayerKiller),
|
||||
AcDream.Core.Ui.RadarBlipShape.X,
|
||||
Selected: true),
|
||||
],
|
||||
CoordinatesText: "42.1N,33.3E"),
|
||||
datFont: stack.VitalsDatFont);
|
||||
|
||||
case CombatUiController.LayoutId:
|
||||
{
|
||||
var combat = new CombatState();
|
||||
var attacks = new RuntimeCombatAttackState(
|
||||
combat,
|
||||
canStartAttack: () => true,
|
||||
sendAttack: (_, _) => true,
|
||||
autoRepeatAttack: () => false);
|
||||
GameplaySettings gameplay = GameplaySettings.Default;
|
||||
CombatUiController? controller = CombatUiController.Bind(
|
||||
layout,
|
||||
combat,
|
||||
attacks,
|
||||
() => gameplay,
|
||||
value => gameplay = value,
|
||||
new CombatUiLabels(
|
||||
"Speed", "Power", "Repeat Attacks", "Auto Target", "Keep in View",
|
||||
"High", "Medium", "Low"),
|
||||
visible => layout.Root.Visible = visible);
|
||||
combat.SetCombatMode(CombatMode.Melee);
|
||||
return controller;
|
||||
}
|
||||
|
||||
case 0x21000016u: // toolbar
|
||||
return ToolbarController.Bind(
|
||||
layout,
|
||||
objects,
|
||||
shortcuts: new ShortcutStore(),
|
||||
iconIds: MakeIconIds(stack),
|
||||
useItem: _ => { },
|
||||
combatState: null,
|
||||
regularDigits: null,
|
||||
ghostedDigits: null,
|
||||
emptyDigits: null,
|
||||
sendAddShortcut: null,
|
||||
sendRemoveShortcut: null);
|
||||
|
||||
case 0x21000023u: // inventory + paperdoll
|
||||
{
|
||||
// Resolve the per-list empty-slot art from the dat cell template, matching the
|
||||
// exact lookup GameWindow.OnLoad performs (UIElement_ItemList::InternalCreateItem
|
||||
// 0x004e3570 → attr 0x1000000e → catalog 0x21000037 → ItemSlot_Empty).
|
||||
uint contentsEmpty = ItemListCellTemplate.ResolveEmptySprite(dats, 0x21000021u, 0x100001C6u);
|
||||
uint sideBagEmpty = ItemListCellTemplate.ResolveEmptySprite(dats, 0x21000022u, 0x100001CAu);
|
||||
uint mainPackEmpty = ItemListCellTemplate.ResolveEmptySprite(dats, 0x21000022u, 0x100001C9u);
|
||||
var paperdollEmpty = PaperdollSlotBackgrounds.ResolveEmptySprites(dats);
|
||||
|
||||
var iconIds = MakeIconIds(stack);
|
||||
var selection = new AcDream.Core.Selection.SelectionState();
|
||||
var itemInteraction = new ItemInteractionController(
|
||||
objects,
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(
|
||||
new InventoryTransactionState(objects)),
|
||||
new AcDream.Runtime.Gameplay.InteractionState(),
|
||||
() => SampleData.PlayerGuid,
|
||||
sendUse: null,
|
||||
sendUseWithTarget: null,
|
||||
sendWield: null,
|
||||
sendDrop: null);
|
||||
|
||||
InventoryController inventory = InventoryController.Bind(
|
||||
layout,
|
||||
objects,
|
||||
playerGuid: () => SampleData.PlayerGuid,
|
||||
iconIds: iconIds,
|
||||
strength: () => 100,
|
||||
selection: selection,
|
||||
datFont: stack.VitalsDatFont,
|
||||
contentsEmptySprite: contentsEmpty,
|
||||
sideBagEmptySprite: sideBagEmpty,
|
||||
mainPackEmptySprite: mainPackEmpty);
|
||||
|
||||
// Bind the paperdoll equip slots with their authored per-location UIItem prototypes.
|
||||
PaperdollController paperdoll = PaperdollController.Bind(
|
||||
layout,
|
||||
objects,
|
||||
playerGuid: () => SampleData.PlayerGuid,
|
||||
iconIds: iconIds,
|
||||
selection: selection,
|
||||
itemInteraction: itemInteraction,
|
||||
emptySlotSprite: contentsEmpty,
|
||||
datFont: stack.VitalsDatFont,
|
||||
emptySlotSprites: paperdollEmpty,
|
||||
ownsItemInteraction: true);
|
||||
return new RetainedPanelControllerGroup(inventory, paperdoll);
|
||||
}
|
||||
|
||||
case 0x2100002Eu: // gmStatManagementUI — Attributes/Skills/Titles window (LayoutDesc 0x2100002E)
|
||||
// Bind the REAL importer-mounted header + list elements (name/heritage/PK/level/
|
||||
// total-XP/XP-meter + the 9-row attribute list + footer State-A). NOT the text-report
|
||||
// sub-panel (that is gmCharacterInfoUI 0x2100001A → CharacterController).
|
||||
// LargeDatFont (0x40000001, MaxCharHeight=18) is used for the attribute row text;
|
||||
// fallback to VitalsDatFont (0x40000000, 16px) if unavailable.
|
||||
CharacterStatController.Bind(
|
||||
layout,
|
||||
data: SampleData.SampleCharacter,
|
||||
datFont: stack.VitalsDatFont,
|
||||
rowDatFont: stack.LargeDatFont ?? stack.VitalsDatFont,
|
||||
spriteResolve: stack.ResolveChrome);
|
||||
return null;
|
||||
|
||||
default:
|
||||
// Unknown layout — no-op; the panel renders structurally.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Build the <c>iconIds</c> delegate for toolbar / inventory controllers.
|
||||
///
|
||||
/// <para>
|
||||
/// Raw-resolve stub: resolve the base <paramref name="iconId"/> (arg 2)
|
||||
/// via <see cref="RenderStack.ResolveChrome"/> and return its GL handle.
|
||||
/// The remaining args (type, underlayId, overlayId, effects) are ignored
|
||||
/// for the studio — a single-layer icon is sufficient for layout preview.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>This is what the task spec calls "v1 raw-resolve stub".</para>
|
||||
/// </summary>
|
||||
private static Func<ItemType, uint, uint, uint, uint, uint> MakeIconIds(RenderStack stack)
|
||||
=> (_, iconId, _, _, _) =>
|
||||
{
|
||||
if (iconId == 0u) return 0u;
|
||||
var (handle, _, _) = stack.ResolveChrome(iconId);
|
||||
return handle;
|
||||
};
|
||||
|
||||
private static System.Numerics.Vector4 RadarColor(AcDream.Core.Ui.RadarBlipColors.Rgba color)
|
||||
=> new(color.Red, color.Green, color.Blue, color.Alpha);
|
||||
|
||||
}
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
using DatReaderWriter;
|
||||
using AcDream.Content;
|
||||
|
||||
namespace AcDream.App.Studio;
|
||||
|
||||
/// <summary>Which kind of source the studio is currently previewing.</summary>
|
||||
public enum LayoutSourceKind { DatLayout, Markup }
|
||||
|
||||
/// <summary>
|
||||
/// Wraps the two ways the UI Studio can load a panel to preview:
|
||||
/// a LayoutDesc dat id, or a KSML markup file path (Task 6 — unsupported now).
|
||||
///
|
||||
/// <para>Call <see cref="Load"/> with the current <see cref="StudioOptions"/> to
|
||||
/// import the layout and get the root <see cref="UiElement"/>. The result is also
|
||||
/// cached in <see cref="CurrentLayout"/> so <see cref="Reload"/> can re-run the same
|
||||
/// source without re-reading the options.</para>
|
||||
/// </summary>
|
||||
public sealed class LayoutSource
|
||||
{
|
||||
private readonly IDatReaderWriter _dats;
|
||||
private readonly Func<uint, (uint, int, int)> _resolve;
|
||||
private readonly UiDatFont? _datFont;
|
||||
private readonly Func<uint, UiDatFont?>? _fontResolve;
|
||||
|
||||
public LayoutSourceKind Kind { get; private set; }
|
||||
public uint? LayoutId { get; private set; }
|
||||
public string? MarkupPath { get; private set; }
|
||||
public string? LastError { get; private set; }
|
||||
public ImportedLayout? CurrentLayout { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a LayoutSource.
|
||||
/// </summary>
|
||||
/// <param name="fontResolve">Optional per-element font resolver: FontDid →
|
||||
/// <see cref="UiDatFont"/> (null when the font isn't in the dats). When supplied,
|
||||
/// elements with a non-zero FontDid receive their own dat font at build time
|
||||
/// instead of the shared <paramref name="datFont"/> global. Controllers that
|
||||
/// explicitly set <see cref="UiText.DatFont"/> after
|
||||
/// <see cref="ImportedLayout.FindElement"/> still override the build-time value.
|
||||
/// Pass null (default) for the original single-font behavior — the live
|
||||
/// <see cref="GameWindow"/> path passes null so it is provably unchanged.</param>
|
||||
public LayoutSource(
|
||||
IDatReaderWriter dats,
|
||||
Func<uint, (uint, int, int)> resolve,
|
||||
UiDatFont? datFont,
|
||||
Func<uint, UiDatFont?>? fontResolve = null)
|
||||
{
|
||||
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
||||
_resolve = resolve ?? throw new ArgumentNullException(nameof(resolve));
|
||||
_datFont = datFont;
|
||||
_fontResolve = fontResolve;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load the layout described by <paramref name="opts"/>. For a dat layout
|
||||
/// (<see cref="StudioOptions.LayoutId"/> is non-null) calls
|
||||
/// <see cref="LayoutImporter.Import"/>. For a markup path sets
|
||||
/// <see cref="LastError"/> and returns null (Task 6, not yet implemented).
|
||||
///
|
||||
/// Returns the root <see cref="UiElement"/> on success, or null on failure
|
||||
/// (check <see cref="LastError"/>).
|
||||
/// </summary>
|
||||
public UiElement? Load(StudioOptions opts)
|
||||
{
|
||||
LastError = null;
|
||||
CurrentLayout = null;
|
||||
|
||||
if (opts.MarkupPath is not null)
|
||||
{
|
||||
Kind = LayoutSourceKind.Markup;
|
||||
MarkupPath = opts.MarkupPath;
|
||||
LastError = "markup unsupported (Task 6)";
|
||||
return null;
|
||||
}
|
||||
|
||||
if (opts.LayoutId is null)
|
||||
{
|
||||
LastError = "ui-studio: no layout id or markup path specified.";
|
||||
return null;
|
||||
}
|
||||
|
||||
Kind = LayoutSourceKind.DatLayout;
|
||||
LayoutId = opts.LayoutId;
|
||||
return LoadDat(opts.LayoutId.Value);
|
||||
}
|
||||
|
||||
/// <summary>Re-run the most-recently-configured source without re-reading options.</summary>
|
||||
public UiElement? Reload()
|
||||
{
|
||||
LastError = null;
|
||||
CurrentLayout = null;
|
||||
|
||||
if (Kind == LayoutSourceKind.Markup)
|
||||
{
|
||||
LastError = "markup unsupported (Task 6)";
|
||||
return null;
|
||||
}
|
||||
|
||||
if (LayoutId is null)
|
||||
{
|
||||
LastError = "ui-studio: no layout id to reload.";
|
||||
return null;
|
||||
}
|
||||
|
||||
return LoadDat(LayoutId.Value);
|
||||
}
|
||||
|
||||
// ── Private ──────────────────────────────────────────────────────────────────
|
||||
|
||||
private UiElement? LoadDat(uint layoutId)
|
||||
{
|
||||
var imported = LayoutImporter.Import(_dats, layoutId, _resolve, _datFont, _fontResolve);
|
||||
if (imported is null)
|
||||
{
|
||||
LastError = $"ui-studio: LayoutDesc 0x{layoutId:X8} not found in dats.";
|
||||
return null;
|
||||
}
|
||||
|
||||
CurrentLayout = imported;
|
||||
return imported.Root;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,493 +0,0 @@
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
using AcDream.Core.Chat;
|
||||
using AcDream.UI.Abstractions;
|
||||
using AcDream.UI.Abstractions.Panels.Chat;
|
||||
using DatReaderWriter;
|
||||
using AcDream.Content;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.Studio;
|
||||
|
||||
/// <summary>
|
||||
/// Builds a multi-window UI desktop for the studio. It mounts several real
|
||||
/// imported panels into one production <see cref="UiHost"/> so drag, resize,
|
||||
/// click, focus, and z-order behavior all run through the same UI root as the
|
||||
/// game.
|
||||
/// </summary>
|
||||
internal static class MockupDesktop
|
||||
{
|
||||
public static void Load(IDatReaderWriter dats, RenderStack stack)
|
||||
{
|
||||
var objects = SampleData.BuildObjectTable();
|
||||
var windows = new List<MockupWindow>();
|
||||
|
||||
if (MountVitals(dats, stack, objects) is { } vitals)
|
||||
RecordWindow(windows, "Vitals", vitals, zOrder: 10);
|
||||
if (MountToolbar(dats, stack, objects) is { } toolbar)
|
||||
RecordWindow(windows, "Toolbar", toolbar, zOrder: 20);
|
||||
if (MountCharacter(dats, stack, objects) is { } character)
|
||||
RecordWindow(windows, "Character", character, zOrder: 30);
|
||||
if (MountInventory(dats, stack, objects) is { } inventory)
|
||||
RecordWindow(windows, "Inventory", inventory, zOrder: 40);
|
||||
if (MountChat(dats, stack) is { } chat)
|
||||
RecordWindow(windows, "Chat", chat, zOrder: 50);
|
||||
|
||||
MountControls(stack, windows);
|
||||
}
|
||||
|
||||
private static ImportedLayout? Import(IDatReaderWriter dats, uint layoutId, RenderStack stack)
|
||||
=> LayoutImporter.Import(dats, layoutId, stack.ResolveChrome, stack.VitalsDatFont,
|
||||
fontResolve: stack.ResolveDatFont);
|
||||
|
||||
private static RetailWindowHandle? MountVitals(IDatReaderWriter dats, RenderStack stack, AcDream.Core.Items.ClientObjectTable objects)
|
||||
{
|
||||
var layout = Import(dats, 0x2100006Cu, stack);
|
||||
if (layout is null) return null;
|
||||
|
||||
IRetainedPanelController? controller =
|
||||
FixtureProvider.Populate(0x2100006Cu, layout, stack, objects, dats);
|
||||
|
||||
return RetailWindowFrame.Mount(
|
||||
stack.UiHost.Root,
|
||||
layout.Root,
|
||||
stack.ResolveChrome,
|
||||
new RetailWindowFrame.Options
|
||||
{
|
||||
WindowName = WindowNames.Vitals,
|
||||
Chrome = RetailWindowChrome.Imported,
|
||||
Left = 12f,
|
||||
Top = 18f,
|
||||
ResizeX = true,
|
||||
ResizeY = false,
|
||||
MinWidth = 40f,
|
||||
ContentClickThrough = false,
|
||||
Controller = controller,
|
||||
});
|
||||
}
|
||||
|
||||
private static RetailWindowHandle? MountToolbar(IDatReaderWriter dats, RenderStack stack, AcDream.Core.Items.ClientObjectTable objects)
|
||||
{
|
||||
var layout = Import(dats, 0x21000016u, stack);
|
||||
if (layout is null) return null;
|
||||
|
||||
IRetainedPanelController? controller =
|
||||
FixtureProvider.Populate(0x21000016u, layout, stack, objects, dats);
|
||||
|
||||
const int border = RetailChromeSprites.Border;
|
||||
var toolbarRoot = layout.Root;
|
||||
float contentW = toolbarRoot.Width > 0f ? toolbarRoot.Width : 300f;
|
||||
float contentH = toolbarRoot.Height;
|
||||
RetailWindowHandle handle = RetailWindowFrame.Mount(
|
||||
stack.UiHost.Root,
|
||||
toolbarRoot,
|
||||
stack.ResolveChrome,
|
||||
new RetailWindowFrame.Options
|
||||
{
|
||||
WindowName = WindowNames.Toolbar,
|
||||
Chrome = RetailWindowChrome.CollapsibleNineSlice,
|
||||
Left = 12f,
|
||||
Top = 220f,
|
||||
ContentWidth = contentW,
|
||||
ContentHeight = contentH,
|
||||
Resizable = false,
|
||||
ResizeX = false,
|
||||
ResizeY = true,
|
||||
ResizableEdges = ResizeEdges.Bottom,
|
||||
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right,
|
||||
ContentClickThrough = true,
|
||||
Controller = controller,
|
||||
});
|
||||
var frame = (UiCollapsibleFrame)handle.OuterFrame;
|
||||
|
||||
ConfigureToolbarCollapse(layout, frame, contentH, border);
|
||||
return handle;
|
||||
}
|
||||
|
||||
private static void ConfigureToolbarCollapse(ImportedLayout layout, UiCollapsibleFrame frame, float contentH, int border)
|
||||
{
|
||||
uint[] row2Ids =
|
||||
{
|
||||
0x100006B6u,
|
||||
0x100006B7u, 0x100006B8u, 0x100006B9u, 0x100006BAu, 0x100006BBu,
|
||||
0x100006BCu, 0x100006BDu, 0x100006BEu, 0x100006BFu,
|
||||
0x100006C0u,
|
||||
};
|
||||
|
||||
var row2 = new List<UiElement>();
|
||||
float minRow2Top = float.MaxValue;
|
||||
foreach (var id in row2Ids)
|
||||
{
|
||||
if (layout.FindElement(id) is not { } element) continue;
|
||||
row2.Add(element);
|
||||
if (element.Top < minRow2Top) minRow2Top = element.Top;
|
||||
}
|
||||
|
||||
if (row2.Count == 0) return;
|
||||
|
||||
float expandedH = contentH + 2 * border;
|
||||
float collapsedH = minRow2Top + 2 * border;
|
||||
frame.CollapsedHeight = collapsedH;
|
||||
frame.ExpandedHeight = expandedH;
|
||||
frame.SecondRow = row2;
|
||||
frame.Resizable = true;
|
||||
frame.ResizableEdges = ResizeEdges.Bottom;
|
||||
frame.MinHeight = collapsedH;
|
||||
frame.MaxHeight = expandedH;
|
||||
}
|
||||
|
||||
private static RetailWindowHandle? MountCharacter(IDatReaderWriter dats, RenderStack stack, AcDream.Core.Items.ClientObjectTable objects)
|
||||
{
|
||||
var layout = Import(dats, 0x2100002Eu, stack);
|
||||
if (layout is null) return null;
|
||||
|
||||
IRetainedPanelController? controller =
|
||||
FixtureProvider.Populate(0x2100002Eu, layout, stack, objects, dats);
|
||||
|
||||
return RetailWindowFrame.Mount(
|
||||
stack.UiHost.Root,
|
||||
layout.Root,
|
||||
stack.ResolveChrome,
|
||||
new RetailWindowFrame.Options
|
||||
{
|
||||
WindowName = WindowNames.Character,
|
||||
Chrome = RetailWindowChrome.NineSlice,
|
||||
Left = 540f,
|
||||
Top = 18f,
|
||||
MaxHeight = 760f,
|
||||
ResizeX = false,
|
||||
ResizeY = true,
|
||||
ResizableEdges = ResizeEdges.Bottom,
|
||||
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom,
|
||||
ContentClickThrough = false,
|
||||
Controller = controller,
|
||||
});
|
||||
}
|
||||
|
||||
private static RetailWindowHandle? MountInventory(IDatReaderWriter dats, RenderStack stack, AcDream.Core.Items.ClientObjectTable objects)
|
||||
{
|
||||
var layout = Import(dats, 0x21000023u, stack);
|
||||
if (layout is null) return null;
|
||||
|
||||
IRetainedPanelController? controller =
|
||||
FixtureProvider.Populate(0x21000023u, layout, stack, objects, dats);
|
||||
|
||||
var inventoryRoot = layout.Root;
|
||||
float contentW = inventoryRoot.Width;
|
||||
float contentH = inventoryRoot.Height;
|
||||
RetailWindowHandle handle = RetailWindowFrame.Mount(
|
||||
stack.UiHost.Root,
|
||||
inventoryRoot,
|
||||
stack.ResolveChrome,
|
||||
new RetailWindowFrame.Options
|
||||
{
|
||||
WindowName = WindowNames.Inventory,
|
||||
Chrome = RetailWindowChrome.NineSlice,
|
||||
Left = 900f,
|
||||
Top = 18f,
|
||||
ContentWidth = contentW,
|
||||
ContentHeight = contentH,
|
||||
ResizeX = false,
|
||||
ResizeY = true,
|
||||
ResizableEdges = ResizeEdges.Bottom,
|
||||
MaxHeight = Math.Max(
|
||||
contentH + 2f * RetailChromeSprites.Border,
|
||||
stack.UiHost.Root.Height - 18f),
|
||||
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom,
|
||||
Controller = controller,
|
||||
});
|
||||
|
||||
StretchV(layout, 0x100001D0u);
|
||||
StretchV(layout, 0x100001CFu);
|
||||
StretchV(layout, 0x100001C6u);
|
||||
StretchV(layout, 0x100001C7u);
|
||||
PinTopLeft(layout, 0x100001CDu);
|
||||
PinTopLeft(layout, 0x100001CEu);
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
private static void StretchV(ImportedLayout layout, uint id)
|
||||
{
|
||||
if (layout.FindElement(id) is { } element)
|
||||
element.Anchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom;
|
||||
}
|
||||
|
||||
private static void PinTopLeft(ImportedLayout layout, uint id)
|
||||
{
|
||||
if (layout.FindElement(id) is { } element)
|
||||
element.Anchors = AnchorEdges.Left | AnchorEdges.Top;
|
||||
}
|
||||
|
||||
private static RetailWindowHandle? MountChat(IDatReaderWriter dats, RenderStack stack)
|
||||
{
|
||||
var rootInfo = LayoutImporter.ImportInfos(dats, ChatWindowController.LayoutId);
|
||||
if (rootInfo is null) return null;
|
||||
|
||||
var strings = new DatStringResolver(dats);
|
||||
var layout = LayoutImporter.Build(rootInfo, stack.ResolveChrome, stack.VitalsDatFont,
|
||||
fontResolve: stack.ResolveDatFont,
|
||||
stringResolve: strings.Resolve);
|
||||
|
||||
var chatLog = new ChatLog();
|
||||
chatLog.SetLocalPlayerGuid(SampleData.PlayerGuid);
|
||||
chatLog.OnSystemMessage("Welcome to the acdream UI mockup.", 0);
|
||||
chatLog.OnLocalSpeech("You", "This desktop is running through UiHost.", SampleData.PlayerGuid, isRanged: false);
|
||||
chatLog.OnChannelBroadcast(1, "Caith", "Drag and resize the windows here.", "General");
|
||||
chatLog.OnTellReceived("Alicia", "The old UI is starting to wake up.", 0x5000ABCDu);
|
||||
|
||||
var vm = new ChatVM(chatLog, displayLimit: 200);
|
||||
var controller = ChatWindowController.Bind(
|
||||
rootInfo,
|
||||
layout,
|
||||
vm,
|
||||
() => NullCommandBus.Instance,
|
||||
stack.VitalsDatFont,
|
||||
debugFont: null,
|
||||
stack.ResolveChrome);
|
||||
if (controller is null) return null;
|
||||
|
||||
var chatRoot = controller.Root;
|
||||
RetailWindowHandle handle = RetailWindowFrame.Mount(
|
||||
stack.UiHost.Root,
|
||||
chatRoot,
|
||||
stack.ResolveChrome,
|
||||
new RetailWindowFrame.Options
|
||||
{
|
||||
WindowName = WindowNames.Chat,
|
||||
Chrome = RetailWindowChrome.NineSlice,
|
||||
Left = 12f,
|
||||
Top = 390f,
|
||||
ContentWidth = 490f,
|
||||
ContentHeight = chatRoot.Height,
|
||||
RebaseContentLayout = true,
|
||||
DatConstraintSource = controller.DatWindowInfo,
|
||||
MinWidth = 200f,
|
||||
ResizeX = true,
|
||||
ResizeY = true,
|
||||
Opacity = 0.75f,
|
||||
StateController = controller,
|
||||
});
|
||||
controller.AttachWindow(handle);
|
||||
|
||||
stack.UiHost.Root.DefaultTextInput = controller.Input;
|
||||
return handle;
|
||||
}
|
||||
|
||||
private static void RecordWindow(
|
||||
List<MockupWindow> windows,
|
||||
string title,
|
||||
RetailWindowHandle handle,
|
||||
int zOrder)
|
||||
{
|
||||
UiElement element = handle.OuterFrame;
|
||||
element.ZOrder = zOrder;
|
||||
windows.Add(new MockupWindow(
|
||||
handle.Name,
|
||||
title,
|
||||
element,
|
||||
new MockupRect(element.Left, element.Top, element.Width, element.Height),
|
||||
zOrder));
|
||||
}
|
||||
|
||||
private static void MountControls(RenderStack stack, IReadOnlyList<MockupWindow> windows)
|
||||
{
|
||||
if (windows.Count == 0) return;
|
||||
|
||||
var font = stack.VitalsDatFont;
|
||||
var panel = new MockupControlPanel
|
||||
{
|
||||
Name = "mockup-controls",
|
||||
Left = 1010f,
|
||||
Top = 430f,
|
||||
Width = 258f,
|
||||
Height = 242f,
|
||||
Anchors = AnchorEdges.None,
|
||||
Draggable = true,
|
||||
Resizable = false,
|
||||
BackgroundColor = new Vector4(0.03f, 0.035f, 0.04f, 0.82f),
|
||||
BorderColor = new Vector4(0.55f, 0.45f, 0.27f, 0.95f),
|
||||
BorderThickness = 1f,
|
||||
ZOrder = 1_000,
|
||||
};
|
||||
|
||||
panel.AddChild(new MockupLabel(font)
|
||||
{
|
||||
Left = 12f,
|
||||
Top = 10f,
|
||||
Width = 220f,
|
||||
Height = 18f,
|
||||
Text = "Mockup",
|
||||
TextColor = new Vector4(0.95f, 0.86f, 0.62f, 1f),
|
||||
});
|
||||
|
||||
var toggles = new List<(MockupWindow Window, MockupButton Button)>();
|
||||
panel.AddChild(MakeButton(font, "Arrange", 12f, 34f, 108f, 24f, () =>
|
||||
{
|
||||
Arrange(windows, showAll: false);
|
||||
RefreshToggles(toggles);
|
||||
}));
|
||||
panel.AddChild(MakeButton(font, "Reset", 132f, 34f, 108f, 24f, () =>
|
||||
{
|
||||
Arrange(windows, showAll: true);
|
||||
RefreshToggles(toggles);
|
||||
}));
|
||||
|
||||
float y = 70f;
|
||||
foreach (var window in windows)
|
||||
{
|
||||
var button = MakeButton(font, "", 12f, y, 228f, 24f, () =>
|
||||
{
|
||||
stack.UiHost.ToggleWindow(window.Name);
|
||||
RefreshToggles(toggles);
|
||||
});
|
||||
toggles.Add((window, button));
|
||||
panel.AddChild(button);
|
||||
y += 30f;
|
||||
}
|
||||
|
||||
RefreshToggles(toggles);
|
||||
stack.UiHost.Root.AddChild(panel);
|
||||
}
|
||||
|
||||
private static MockupButton MakeButton(UiDatFont? font, string text,
|
||||
float left, float top, float width, float height, Action onClick)
|
||||
{
|
||||
var button = new MockupButton(font)
|
||||
{
|
||||
Left = left,
|
||||
Top = top,
|
||||
Width = width,
|
||||
Height = height,
|
||||
Text = text,
|
||||
};
|
||||
button.Click += onClick;
|
||||
return button;
|
||||
}
|
||||
|
||||
private static void Arrange(IReadOnlyList<MockupWindow> windows, bool showAll)
|
||||
{
|
||||
foreach (var window in windows)
|
||||
{
|
||||
var element = window.Element;
|
||||
element.Left = window.DefaultRect.Left;
|
||||
element.Top = window.DefaultRect.Top;
|
||||
element.Width = window.DefaultRect.Width;
|
||||
element.Height = window.DefaultRect.Height;
|
||||
element.ZOrder = window.DefaultZOrder;
|
||||
if (showAll)
|
||||
element.Visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void RefreshToggles(IReadOnlyList<(MockupWindow Window, MockupButton Button)> toggles)
|
||||
{
|
||||
foreach (var (window, button) in toggles)
|
||||
{
|
||||
bool visible = window.Element.Visible;
|
||||
button.Text = visible ? $"{window.Title} on" : $"{window.Title} off";
|
||||
button.TextColor = visible
|
||||
? new Vector4(0.96f, 0.93f, 0.82f, 1f)
|
||||
: new Vector4(0.62f, 0.62f, 0.62f, 1f);
|
||||
button.BackgroundColor = visible
|
||||
? new Vector4(0.14f, 0.13f, 0.10f, 0.94f)
|
||||
: new Vector4(0.06f, 0.065f, 0.07f, 0.82f);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record MockupWindow(
|
||||
string Name,
|
||||
string Title,
|
||||
UiElement Element,
|
||||
MockupRect DefaultRect,
|
||||
int DefaultZOrder);
|
||||
|
||||
private readonly record struct MockupRect(float Left, float Top, float Width, float Height);
|
||||
|
||||
private sealed class MockupControlPanel : UiPanel
|
||||
{
|
||||
protected override void OnDraw(UiRenderContext ctx)
|
||||
{
|
||||
if (BackgroundColor.W > 0f)
|
||||
ctx.DrawFill(0f, 0f, Width, Height, BackgroundColor);
|
||||
if (BorderColor.W > 0f && BorderThickness > 0f)
|
||||
ctx.DrawRectOutline(0f, 0f, Width, Height, BorderColor, BorderThickness);
|
||||
}
|
||||
|
||||
protected override void OnTick(double deltaSeconds)
|
||||
{
|
||||
base.OnTick(deltaSeconds);
|
||||
if (Parent is null) return;
|
||||
|
||||
int top = ZOrder;
|
||||
foreach (var sibling in Parent.Children)
|
||||
{
|
||||
if (!ReferenceEquals(sibling, this))
|
||||
top = Math.Max(top, sibling.ZOrder + 1);
|
||||
}
|
||||
ZOrder = top;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class MockupLabel : UiElement
|
||||
{
|
||||
private readonly UiDatFont? _font;
|
||||
|
||||
public string Text { get; set; } = string.Empty;
|
||||
public Vector4 TextColor { get; set; } = Vector4.One;
|
||||
|
||||
public MockupLabel(UiDatFont? font)
|
||||
{
|
||||
_font = font;
|
||||
ClickThrough = true;
|
||||
}
|
||||
|
||||
protected override void OnDraw(UiRenderContext ctx)
|
||||
{
|
||||
if (_font is null || Text.Length == 0) return;
|
||||
ctx.DrawStringDat(_font, Text, 0f, 0f, TextColor);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class MockupButton : UiPanel
|
||||
{
|
||||
private readonly UiDatFont? _font;
|
||||
|
||||
public event Action? Click;
|
||||
public string Text { get; set; } = string.Empty;
|
||||
public Vector4 TextColor { get; set; } = new(0.96f, 0.93f, 0.82f, 1f);
|
||||
|
||||
public MockupButton(UiDatFont? font)
|
||||
{
|
||||
_font = font;
|
||||
BackgroundColor = new Vector4(0.14f, 0.13f, 0.10f, 0.94f);
|
||||
BorderColor = new Vector4(0.45f, 0.38f, 0.24f, 1f);
|
||||
BorderThickness = 1f;
|
||||
}
|
||||
|
||||
public override bool HandlesClick => true;
|
||||
|
||||
public override bool OnEvent(in UiEvent e)
|
||||
{
|
||||
if (e.Type != UiEventType.Click || !Enabled) return false;
|
||||
Click?.Invoke();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnDraw(UiRenderContext ctx)
|
||||
{
|
||||
if (BackgroundColor.W > 0f)
|
||||
ctx.DrawFill(0f, 0f, Width, Height, BackgroundColor);
|
||||
if (BorderColor.W > 0f && BorderThickness > 0f)
|
||||
ctx.DrawRectOutline(0f, 0f, Width, Height, BorderColor, BorderThickness);
|
||||
|
||||
if (_font is null || Text.Length == 0) return;
|
||||
|
||||
float textW = _font.MeasureWidth(Text);
|
||||
float tx = MathF.Max(4f, (Width - textW) * 0.5f);
|
||||
float ty = (Height - _font.LineHeight) * 0.5f;
|
||||
ctx.DrawStringDat(_font, Text, tx, ty, TextColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.UI;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Studio;
|
||||
|
||||
/// <summary>
|
||||
/// Renders a <see cref="UiHost"/> into an off-screen FBO each frame and
|
||||
/// returns the color texture handle for display in ImGui.
|
||||
///
|
||||
/// <para>Pattern lifted verbatim from <see cref="AcDream.App.Rendering.PaperdollViewportRenderer"/>:
|
||||
/// RGBA8 color texture + Depth24Stencil8 renderbuffer, lazily (re)created on
|
||||
/// size change. The entire 2-D UI pass is sealed in a <see cref="GLStateScope"/>
|
||||
/// so it cannot disturb the surrounding ImGui GL state.</para>
|
||||
///
|
||||
/// <para>FBO origin is bottom-left (GL convention). The caller must flip V when
|
||||
/// displaying the texture in ImGui (pass uv0=(0,1), uv1=(1,0) to ImGui.Image)
|
||||
/// so the image appears right-side-up in ImGui's top-left coordinate system.</para>
|
||||
/// </summary>
|
||||
public sealed unsafe class PanelFbo : IDisposable
|
||||
{
|
||||
private readonly GL _gl;
|
||||
|
||||
// Off-screen target — lazily (re)created when the requested size changes.
|
||||
private uint _fbo;
|
||||
private uint _colorTex;
|
||||
private uint _depthRbo;
|
||||
private int _fbW;
|
||||
private int _fbH;
|
||||
|
||||
public PanelFbo(GL gl)
|
||||
{
|
||||
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Render <paramref name="host"/> (a full <see cref="UiHost"/> draw pass) into a
|
||||
/// private FBO at <paramref name="width"/> × <paramref name="height"/> pixels.
|
||||
/// Returns the GL color texture handle (0 on failure). The texture is valid until
|
||||
/// the next call to <see cref="Render"/> with a different size, or until <see cref="Dispose"/>.
|
||||
/// </summary>
|
||||
public uint Render(int width, int height, UiHost host)
|
||||
{
|
||||
if (width <= 0 || height <= 0 || host is null) return 0u;
|
||||
|
||||
EnsureFramebuffer(width, height);
|
||||
if (_fbo == 0) return 0u;
|
||||
|
||||
// Seal the entire pass: GLStateScope saves + restores every GL state the
|
||||
// UI draw touches (viewport, blend, FBO binding, etc.) so ImGui's own state
|
||||
// — set up by BeginFrame and expected intact by Render — is untouched.
|
||||
using var scope = new GLStateScope(_gl);
|
||||
|
||||
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, _fbo);
|
||||
_gl.Viewport(0, 0, (uint)width, (uint)height);
|
||||
_gl.Disable(EnableCap.ScissorTest);
|
||||
_gl.ClearColor(0.18f, 0.18f, 0.18f, 1f); // opaque dark-grey canvas background (the FBO IS the canvas)
|
||||
_gl.ClearDepth(1.0);
|
||||
_gl.DepthMask(true);
|
||||
_gl.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
|
||||
|
||||
host.Draw(new Vector2(width, height));
|
||||
|
||||
// FBO stays bound here; GLStateScope.Dispose() restores the previous binding.
|
||||
return _colorTex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the FBO color attachment back to CPU as a flat RGBA8 byte array.
|
||||
/// Must be called AFTER <see cref="Render"/> for the same <paramref name="width"/> and
|
||||
/// <paramref name="height"/> (so the FBO exists and is the right size).
|
||||
///
|
||||
/// <para>FBO origin is bottom-left (GL convention). The caller is responsible for
|
||||
/// flipping rows vertically before saving as a top-left-origin image format (PNG).</para>
|
||||
///
|
||||
/// <para>Returns an empty array when the FBO is not ready.</para>
|
||||
/// </summary>
|
||||
public unsafe byte[] ReadColorRgba(int width, int height)
|
||||
{
|
||||
if (_fbo == 0 || width <= 0 || height <= 0) return Array.Empty<byte>();
|
||||
|
||||
int byteCount = width * height * 4;
|
||||
var buf = new byte[byteCount];
|
||||
|
||||
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, _fbo);
|
||||
fixed (byte* p = buf)
|
||||
{
|
||||
_gl.ReadPixels(0, 0, (uint)width, (uint)height,
|
||||
PixelFormat.Rgba, PixelType.UnsignedByte, p);
|
||||
}
|
||||
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
// ── FBO lifecycle (mirrors PaperdollViewportRenderer.EnsureFramebuffer) ──────
|
||||
|
||||
private void EnsureFramebuffer(int width, int height)
|
||||
{
|
||||
if (_fbo != 0 && width == _fbW && height == _fbH) return;
|
||||
DeleteFramebuffer();
|
||||
|
||||
_fbW = width;
|
||||
_fbH = height;
|
||||
|
||||
_colorTex = _gl.GenTexture();
|
||||
_gl.BindTexture(TextureTarget.Texture2D, _colorTex);
|
||||
_gl.TexImage2D(TextureTarget.Texture2D, 0, InternalFormat.Rgba8,
|
||||
(uint)width, (uint)height, 0,
|
||||
PixelFormat.Rgba, PixelType.UnsignedByte, (void*)0);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter,
|
||||
(int)TextureMinFilter.Linear);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter,
|
||||
(int)TextureMinFilter.Linear);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS,
|
||||
(int)TextureWrapMode.ClampToEdge);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT,
|
||||
(int)TextureWrapMode.ClampToEdge);
|
||||
_gl.BindTexture(TextureTarget.Texture2D, 0);
|
||||
|
||||
_depthRbo = _gl.GenRenderbuffer();
|
||||
_gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, _depthRbo);
|
||||
_gl.RenderbufferStorage(RenderbufferTarget.Renderbuffer,
|
||||
InternalFormat.Depth24Stencil8, (uint)width, (uint)height);
|
||||
_gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, 0);
|
||||
|
||||
_fbo = _gl.GenFramebuffer();
|
||||
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, _fbo);
|
||||
_gl.FramebufferTexture2D(FramebufferTarget.Framebuffer,
|
||||
FramebufferAttachment.ColorAttachment0,
|
||||
TextureTarget.Texture2D, _colorTex, 0);
|
||||
_gl.FramebufferRenderbuffer(FramebufferTarget.Framebuffer,
|
||||
FramebufferAttachment.DepthStencilAttachment,
|
||||
RenderbufferTarget.Renderbuffer, _depthRbo);
|
||||
|
||||
var status = _gl.CheckFramebufferStatus(FramebufferTarget.Framebuffer);
|
||||
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
|
||||
if (status != GLEnum.FramebufferComplete)
|
||||
{
|
||||
Console.WriteLine($"[studio] PanelFbo incomplete: {status} ({width}x{height})");
|
||||
DeleteFramebuffer();
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteFramebuffer()
|
||||
{
|
||||
if (_fbo != 0) { _gl.DeleteFramebuffer(_fbo); _fbo = 0; }
|
||||
if (_colorTex != 0) { _gl.DeleteTexture(_colorTex); _colorTex = 0; }
|
||||
if (_depthRbo != 0) { _gl.DeleteRenderbuffer(_depthRbo); _depthRbo = 0; }
|
||||
_fbW = _fbH = 0;
|
||||
}
|
||||
|
||||
public void Dispose() => DeleteFramebuffer();
|
||||
}
|
||||
|
|
@ -1,261 +0,0 @@
|
|||
using AcDream.App.UI.Layout;
|
||||
using AcDream.Core.Items;
|
||||
|
||||
namespace AcDream.App.Studio;
|
||||
|
||||
/// <summary>
|
||||
/// Static sample data for the UI Studio fixture provider.
|
||||
/// Provides a pre-built <see cref="ClientObjectTable"/> populated with a
|
||||
/// representative player, their main-pack items, side bags, and equipped gear,
|
||||
/// so the 2-D inventory / paperdoll panels render populated when previewed in
|
||||
/// the studio without a live game session.
|
||||
///
|
||||
/// Icon ids used (all real 0x06xxxxxx RenderSurface ids confirmed in the dats):
|
||||
/// Inventory empty-slot sprite : 0x06004D20
|
||||
/// Generic "misc" item : 0x060011D4 (icon_misc_underlay / fallback)
|
||||
/// Iron Sword (melee weapon) : 0x060011CBu (acclient default weapon underlay)
|
||||
/// Leather Breastplate (armor) : 0x060011CFu (acclient default armor underlay)
|
||||
/// Leather Gloves : 0x060011F3u (acclient default clothing underlay)
|
||||
/// Steel Ring : 0x060011D5u (acclient default jewelry underlay)
|
||||
/// Healing Kit : 0x060011D4u (generic misc fallback)
|
||||
/// Spell components : 0x060011D4u (generic misc fallback)
|
||||
/// Side-bag 1 (Container) : 0x06004D20u
|
||||
/// Side-bag 2 (Container) : 0x06004D20u
|
||||
/// Equipped helm (HeadWear) : 0x060011F3u
|
||||
/// Equipped chest armor : 0x060011CFu
|
||||
/// Equipped melee weapon : 0x060011CBu
|
||||
///
|
||||
/// These are the icon *base* RenderSurface ids — the same ids GameWindow passes
|
||||
/// as `iconId` into the iconIds lambda. FixtureProvider resolves them via
|
||||
/// <see cref="Rendering.RenderStack.ResolveChrome"/> and returns the raw GL handle.
|
||||
/// </summary>
|
||||
public static class SampleData
|
||||
{
|
||||
// ── Guids ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Fake server guid for the studio's synthetic player.</summary>
|
||||
public const uint PlayerGuid = 0x50000001u;
|
||||
|
||||
// Items in main pack (slots 0–5).
|
||||
private const uint SwordGuid = 0x50000010u;
|
||||
private const uint ChestGuid = 0x50000011u;
|
||||
private const uint GlovesGuid = 0x50000012u;
|
||||
private const uint RingGuid = 0x50000013u;
|
||||
private const uint HealKitGuid = 0x50000014u;
|
||||
private const uint CompGuid = 0x50000015u;
|
||||
|
||||
// Side bags (also in main pack, ContainerId = PlayerGuid; slots 6 & 7).
|
||||
private const uint Bag1Guid = 0x50000020u;
|
||||
private const uint Bag2Guid = 0x50000021u;
|
||||
|
||||
// Equipped items (ContainerId = PlayerGuid, CurrentlyEquippedLocation set).
|
||||
private const uint HelmGuid = 0x50000030u;
|
||||
private const uint ChestEqGuid = 0x50000031u;
|
||||
private const uint WeaponEqGuid = 0x50000032u;
|
||||
|
||||
// ── Icon ids (0x06xxxxxx RenderSurface dat ids) ───────────────────────────
|
||||
|
||||
// These are the same underlay/fallback icon ids the IconComposer tests pin.
|
||||
private const uint IconWeapon = 0x060011CBu; // weapon underlay
|
||||
private const uint IconArmor = 0x060011CFu; // armor underlay
|
||||
private const uint IconClothing = 0x060011F3u; // clothing underlay
|
||||
private const uint IconJewelry = 0x060011D5u; // jewelry underlay
|
||||
private const uint IconMisc = 0x060011D4u; // misc / fallback underlay
|
||||
|
||||
// ── Public API ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Build a fresh <see cref="ClientObjectTable"/> populated with the
|
||||
/// studio's sample player + a realistic inventory snapshot.
|
||||
/// The table is owned by the caller and should be kept alive for the
|
||||
/// window's lifetime.
|
||||
/// </summary>
|
||||
public static ClientObjectTable BuildObjectTable()
|
||||
{
|
||||
var t = new ClientObjectTable();
|
||||
|
||||
// ── Player object ─────────────────────────────────────────────────
|
||||
t.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = PlayerGuid,
|
||||
Name = "Studio Player",
|
||||
Type = ItemType.Creature,
|
||||
ItemsCapacity = 102,
|
||||
ContainersCapacity = 7,
|
||||
});
|
||||
// Show all three authored sigil locations in inventory/paperdoll previews.
|
||||
t.UpdateIntProperty(
|
||||
PlayerGuid,
|
||||
AetheriaUnlocks.PropertyId,
|
||||
(int)AetheriaUnlockState.All);
|
||||
|
||||
// ── Loose items in main pack (slots 0–5) ──────────────────────────
|
||||
|
||||
AddItem(t, SwordGuid, ItemType.MeleeWeapon, IconWeapon, "Iron Sword", PlayerGuid, 0, burden: 60);
|
||||
AddItem(t, ChestGuid, ItemType.Armor, IconArmor, "Leather Breastplate", PlayerGuid, 1, burden: 200);
|
||||
AddItem(t, GlovesGuid, ItemType.Clothing, IconClothing, "Leather Gloves", PlayerGuid, 2, burden: 50);
|
||||
AddItem(t, RingGuid, ItemType.Jewelry, IconJewelry, "Steel Ring", PlayerGuid, 3, burden: 10);
|
||||
AddItem(t, HealKitGuid, ItemType.Misc, IconMisc, "Healing Kit", PlayerGuid, 4, burden: 30);
|
||||
AddItem(t, CompGuid, ItemType.SpellComponents, IconMisc, "Spell Comps", PlayerGuid, 5, burden: 25, stackSize: 50, stackMax: 100);
|
||||
|
||||
// ── Side bags (Container items in main pack, slots 6 & 7) ─────────
|
||||
|
||||
AddItem(t, Bag1Guid, ItemType.Container, IconMisc, "Small Pack 1",
|
||||
containerId: PlayerGuid, slot: 6, burden: 20, itemsCapacity: 24);
|
||||
AddItem(t, Bag2Guid, ItemType.Container, IconMisc, "Small Pack 2",
|
||||
containerId: PlayerGuid, slot: 7, burden: 20, itemsCapacity: 24);
|
||||
|
||||
// ── Equipped items (ContainerId = PlayerGuid, CurrentlyEquippedLocation set) ──
|
||||
|
||||
AddEquipped(t, HelmGuid, ItemType.Armor, IconClothing, "Tin Helm", EquipMask.HeadWear);
|
||||
AddEquipped(t, ChestEqGuid, ItemType.Armor, IconArmor, "Chain Coat", EquipMask.ChestArmor);
|
||||
AddEquipped(t, WeaponEqGuid, ItemType.MeleeWeapon, IconWeapon, "Wooden Sword", EquipMask.MeleeWeapon);
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
// ── Sample vital constants (used by FixtureProvider) ────────────────────
|
||||
|
||||
public const float HealthPct = 0.8f;
|
||||
public const float StaminaPct = 0.6f;
|
||||
public const float ManaPct = 0.9f;
|
||||
|
||||
// ── Sample character sheet (used by CharacterController in the Studio) ───
|
||||
|
||||
/// <summary>
|
||||
/// Returns a representative <see cref="CharacterSheet"/> for the studio's
|
||||
/// synthetic character. Values are plausible retail-scale numbers so the
|
||||
/// report renders with well-proportioned text in all sections.
|
||||
/// </summary>
|
||||
public static CharacterSheet SampleCharacter() => SampleCharacter(null);
|
||||
|
||||
public static CharacterSheet SampleCharacter(string? name) => new()
|
||||
{
|
||||
Name = string.IsNullOrWhiteSpace(name) ? "Studio Player" : name,
|
||||
Level = 126,
|
||||
Gender = "Female",
|
||||
Heritage = "Aluvian",
|
||||
Title = "the Adventurer",
|
||||
BirthDate = "January 5, 2001",
|
||||
PlayTime = "2 years, 114 days, 4 hours",
|
||||
Deaths = 42,
|
||||
|
||||
PkStatus = "Non-Player Killer",
|
||||
TotalXp = 1_250_000_000,
|
||||
XpToNextLevel = 42_000_000,
|
||||
XpFraction = 0.63f,
|
||||
|
||||
// Vitals: retail screenshot spec (Pass 1 acceptance criteria §Goal).
|
||||
HealthCurrent = 5, HealthMax = 5,
|
||||
StaminaCurrent = 10, StaminaMax = 10,
|
||||
ManaCurrent = 10, ManaMax = 10,
|
||||
|
||||
// Attributes: Strength + Quickness = 200; all others = 10 (retail screenshot spec §Goal).
|
||||
Strength = 200,
|
||||
Endurance = 10,
|
||||
Quickness = 200,
|
||||
Coordination = 10,
|
||||
Focus = 10,
|
||||
Self = 10,
|
||||
|
||||
UnspentSkillCredits = 12,
|
||||
SpecializedSkillCredits = 4,
|
||||
ChessRank = 12,
|
||||
FishingSkill = 4,
|
||||
|
||||
// Available skill credits (retail InqInt(0x18)); shown in Attributes tab footer State-A.
|
||||
SkillCredits = 96,
|
||||
|
||||
// Unassigned (banked) XP (retail InqInt64(2)); footer State-A line-2 value.
|
||||
UnassignedXp = 87_757_321_741L,
|
||||
|
||||
// Raise costs in retail display order (Strength, Endurance, Coordination, Quickness,
|
||||
// Focus, Self, Health, Stamina, Mana).
|
||||
// Str@200 = maxed → 0 (disabled). Quickness@200 = maxed → 0. Others @10 → affordable.
|
||||
// Focus@10 → 110 matches the authoritative retail screenshot (spec §4).
|
||||
// Formula bracket at value=10: ExperienceToAttributeLevel(11) − ExperienceToAttributeLevel(10).
|
||||
AttributeRaiseCosts = new long[] { 0L, 95L, 100L, 0L, 110L, 105L, 90L, 88L, 112L },
|
||||
AttributeRaise10Costs = new long[] { 0L, 950L, 1_000L, 0L, 1_100L, 1_050L, 900L, 880L, 1_120L },
|
||||
|
||||
// Real SkillTable icon IDs and train/specialize costs from client_portal.dat
|
||||
// SkillTable 0x0E000004. gmSkillUI groups/sorts these at bind time.
|
||||
Skills = new CharacterSkill[]
|
||||
{
|
||||
new( 6, "Melee Defense", 0x06000165u, CharacterSkillAdvancementClass.Specialized, 350, 354, false, 10, 20, 18_250_000L, 182_500_000L),
|
||||
new(34, "War Magic", 0x06001365u, CharacterSkillAdvancementClass.Specialized, 280, 285, false, 16, 28, 11_100_000L, 111_000_000L),
|
||||
|
||||
new(14, "Arcane Lore", 0x0600016Eu, CharacterSkillAdvancementClass.Trained, 260, 269, false, 4, 6, 7_500_000L, 75_000_000L),
|
||||
new(33, "Life Magic", 0x06001364u, CharacterSkillAdvancementClass.Trained, 250, 252, false, 12, 20, 6_800_000L, 68_000_000L),
|
||||
new(47, "Missile Weapons", 0x0600015Fu, CharacterSkillAdvancementClass.Trained, 220, 221, false, 6, 12, 5_250_000L, 52_500_000L),
|
||||
|
||||
new(21, "Healing", 0x06000133u, CharacterSkillAdvancementClass.Untrained, 10, 10, true, 6, 10, 0L),
|
||||
new(22, "Jump", 0x0600016Bu, CharacterSkillAdvancementClass.Untrained, 210, 210, true, 0, 4, 0L),
|
||||
new(36, "Loyalty", 0x06001367u, CharacterSkillAdvancementClass.Untrained, 10, 10, true, 0, 2, 0L),
|
||||
new(24, "Run", 0x06000173u, CharacterSkillAdvancementClass.Untrained, 390, 390, true, 0, 4, 0L),
|
||||
|
||||
new(38, "Alchemy", 0x060019E4u, CharacterSkillAdvancementClass.Untrained, 10, 10, false, 6, 12, 0L),
|
||||
new(39, "Cooking", 0x06001A54u, CharacterSkillAdvancementClass.Untrained, 10, 10, false, 4, 8, 0L),
|
||||
new(37, "Fletching", 0x06001A55u, CharacterSkillAdvancementClass.Untrained, 10, 10, false, 4, 8, 0L),
|
||||
},
|
||||
|
||||
CharacterInfoProperties = new Dictionary<uint, int>
|
||||
{
|
||||
[0x162u] = 2, // Swords melee mastery
|
||||
},
|
||||
|
||||
BurdenCurrent = 1200,
|
||||
BurdenMax = 4500,
|
||||
};
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
private static void AddItem(
|
||||
ClientObjectTable t,
|
||||
uint guid,
|
||||
ItemType type,
|
||||
uint iconId,
|
||||
string name,
|
||||
uint containerId,
|
||||
int slot,
|
||||
int burden = 0,
|
||||
int stackSize = 1,
|
||||
int stackMax = 1,
|
||||
int itemsCapacity = 0)
|
||||
{
|
||||
t.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = guid,
|
||||
Name = name,
|
||||
Type = type,
|
||||
IconId = iconId,
|
||||
Burden = burden,
|
||||
StackSize = stackSize,
|
||||
StackSizeMax = stackMax,
|
||||
ItemsCapacity = itemsCapacity,
|
||||
});
|
||||
t.MoveItem(guid, containerId, slot);
|
||||
}
|
||||
|
||||
private static void AddEquipped(
|
||||
ClientObjectTable t,
|
||||
uint guid,
|
||||
ItemType type,
|
||||
uint iconId,
|
||||
string name,
|
||||
EquipMask equipMask)
|
||||
{
|
||||
t.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = guid,
|
||||
Name = name,
|
||||
Type = type,
|
||||
IconId = iconId,
|
||||
ValidLocations = equipMask,
|
||||
CurrentlyEquippedLocation = equipMask,
|
||||
ContainerId = PlayerGuid,
|
||||
});
|
||||
// Update the equip location on the object via MoveItem (sets ContainerId +
|
||||
// CurrentlyEquippedLocation via the equip overload).
|
||||
t.MoveItem(guid, PlayerGuid, newSlot: -1, newEquipLocation: equipMask);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
namespace AcDream.App.Studio;
|
||||
|
||||
/// <summary>
|
||||
/// Defers a close requested by render work until the caller has completed the
|
||||
/// active render-frame transaction. Silk dispatches <c>Closing</c>
|
||||
/// synchronously, so closing directly from a render callback would otherwise
|
||||
/// dispose GPU resources while submitted work is still owned by that frame.
|
||||
/// </summary>
|
||||
internal sealed class StudioFrameCloseGate
|
||||
{
|
||||
private bool _requested;
|
||||
|
||||
public bool IsRequested => _requested;
|
||||
|
||||
public void Request()
|
||||
{
|
||||
_requested = true;
|
||||
}
|
||||
|
||||
public void CompleteFrame(Action close)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(close);
|
||||
|
||||
if (!_requested)
|
||||
return;
|
||||
|
||||
_requested = false;
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,295 +0,0 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.UI;
|
||||
using ImGuiNET;
|
||||
|
||||
namespace AcDream.App.Studio;
|
||||
|
||||
/// <summary>
|
||||
/// All canvas mouse events gathered by <see cref="StudioInspector.DrawCanvas"/> in one frame.
|
||||
/// All coordinates are already mapped to panel-local pixels (origin top-left, same as UiRoot).
|
||||
/// </summary>
|
||||
public readonly struct CanvasInputEvent
|
||||
{
|
||||
/// <summary>Mouse is currently hovering the canvas image. When false all other fields are 0 / false.</summary>
|
||||
public readonly bool IsHovered;
|
||||
/// <summary>Panel-local pixel coordinate of the mouse this frame (valid when <see cref="IsHovered"/>).</summary>
|
||||
public readonly int MouseX;
|
||||
/// <summary>Panel-local pixel coordinate of the mouse this frame (valid when <see cref="IsHovered"/>).</summary>
|
||||
public readonly int MouseY;
|
||||
/// <summary>Left-button went down this frame.</summary>
|
||||
public readonly bool LeftDown;
|
||||
/// <summary>Left-button came up this frame.</summary>
|
||||
public readonly bool LeftUp;
|
||||
/// <summary>Mouse-wheel scroll delta (lines, positive = up). Zero when no scroll.</summary>
|
||||
public readonly int ScrollDelta;
|
||||
|
||||
public CanvasInputEvent(bool hovered, int mx, int my, bool ld, bool lu, int scroll)
|
||||
{
|
||||
IsHovered = hovered;
|
||||
MouseX = mx;
|
||||
MouseY = my;
|
||||
LeftDown = ld;
|
||||
LeftUp = lu;
|
||||
ScrollDelta = scroll;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Four-pane ImGui IDE for the acdream UI Studio:
|
||||
/// <list type="bullet">
|
||||
/// <item><b>Toolbar</b> — panel picker (slug combo) across the top.</item>
|
||||
/// <item><b>Canvas</b> — shows the panel FBO texture; in Interact mode mouse events
|
||||
/// are forwarded to the panel UiHost (buttons/tabs respond); in Inspect mode a
|
||||
/// left-click hit-tests and selects the element under the cursor.</item>
|
||||
/// <item><b>Tree</b> — recursive ImGui tree of the element hierarchy; clicking a node
|
||||
/// sets <see cref="Selected"/>.</item>
|
||||
/// <item><b>Properties</b> — shows the <see cref="Selected"/> element's geometry,
|
||||
/// anchors, and z-order.</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para><b>Coordinate mapping for the canvas:</b>
|
||||
/// The FBO is rendered at the full window size and displayed 1:1 inside the Canvas ImGui
|
||||
/// sub-window. After <c>ImGui.Image</c> we call <c>ImGui.GetItemRectMin()</c> to get the
|
||||
/// screen-space top-left of the drawn image (accounting for the sub-window's title bar,
|
||||
/// padding, and any scrolling). Subtracting that from the raw mouse screen position gives
|
||||
/// panel-local pixels directly — no additional scale factor is needed because the image is
|
||||
/// drawn 1:1.</para>
|
||||
///
|
||||
/// <para><b>V-flip — no extra Y inversion needed:</b>
|
||||
/// The FBO origin is bottom-left (GL convention), so we pass uv0=(0,1), uv1=(1,0) to
|
||||
/// <c>ImGui.Image</c> to flip V. After this flip, displayed row 0 (top of the image on
|
||||
/// screen) corresponds to panel Y=0 (the top of the UI panel), matching UiRoot's
|
||||
/// top-left origin. Therefore the panel-local Y computed above maps directly into UiRoot
|
||||
/// without further inversion — do NOT flip Y again.</para>
|
||||
///
|
||||
/// <para>Layout: the four panes call <c>SetNextWindowPos</c> + <c>SetNextWindowSize</c>
|
||||
/// with <c>ImGuiCond.FirstUseEver</c> so they start docked but can be freely dragged.</para>
|
||||
/// </summary>
|
||||
public sealed class StudioInspector
|
||||
{
|
||||
/// <summary>Currently selected element (set by tree-click or canvas-click in Inspect mode).</summary>
|
||||
public UiElement? Selected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When true (default) canvas mouse events are forwarded to the panel UiHost so elements
|
||||
/// respond to clicks. When false a canvas click hit-tests and selects an element in the
|
||||
/// inspector tree instead. Toggle via the "Interact / Inspect" checkbox in the toolbar.
|
||||
/// </summary>
|
||||
public bool InteractMode { get; set; } = true;
|
||||
|
||||
// ── Toolbar ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Draw the "Studio" toolbar window (top strip) containing a slug combo-box and
|
||||
/// the Interact / Inspect mode toggle. Returns the newly-selected slug when the
|
||||
/// user picks a different panel, or null when unchanged.
|
||||
/// <para>The mode toggle sets <see cref="InteractMode"/>: checked = Interact (panel
|
||||
/// elements respond to clicks), unchecked = Inspect (clicks select elements in the
|
||||
/// tree).</para>
|
||||
/// </summary>
|
||||
/// <param name="slugs">All available panel slugs (from <c>UiDumpModel.ListSlugs</c>).</param>
|
||||
/// <param name="current">The slug of the panel currently loaded.</param>
|
||||
/// <param name="windowW">Studio window width (pixels).</param>
|
||||
public string? DrawToolbar(IReadOnlyList<string> slugs, string? current, int windowW)
|
||||
{
|
||||
ImGui.SetNextWindowPos(new Vector2(0f, 0f), ImGuiCond.FirstUseEver);
|
||||
ImGui.SetNextWindowSize(new Vector2(windowW, 40f), ImGuiCond.FirstUseEver);
|
||||
ImGui.Begin("Studio",
|
||||
ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse);
|
||||
|
||||
ImGui.SetNextItemWidth(300f);
|
||||
string? result = null;
|
||||
string preview = current ?? "(none)";
|
||||
if (ImGui.BeginCombo("Panel", preview))
|
||||
{
|
||||
foreach (var slug in slugs)
|
||||
{
|
||||
bool selected = string.Equals(slug, current, StringComparison.OrdinalIgnoreCase);
|
||||
if (ImGui.Selectable(slug, selected) && !selected)
|
||||
result = slug;
|
||||
if (selected)
|
||||
ImGui.SetItemDefaultFocus();
|
||||
}
|
||||
ImGui.EndCombo();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
bool interact = InteractMode;
|
||||
if (ImGui.Checkbox("Interact", ref interact))
|
||||
InteractMode = interact;
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip("Interact: canvas clicks reach the panel (buttons/tabs respond).\nUncheck to Inspect: clicks select elements in the tree.");
|
||||
|
||||
ImGui.End();
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Canvas ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Draw the "Canvas" ImGui window containing the panel FBO texture and return all
|
||||
/// canvas mouse events for this frame as a <see cref="CanvasInputEvent"/>.
|
||||
///
|
||||
/// <para><b>Coordinate mapping:</b> After <c>ImGui.Image</c>, <c>GetItemRectMin()</c>
|
||||
/// returns the actual screen-space top-left of the drawn image (accounting for the
|
||||
/// sub-window title bar, padding, and scrolling). Subtracting that from the raw ImGui
|
||||
/// mouse position gives panel-local pixels directly — no scale factor because the
|
||||
/// image is drawn 1:1.</para>
|
||||
///
|
||||
/// <para><b>V-flip — no extra Y inversion:</b> we pass uv0=(0,1) / uv1=(1,0) so the
|
||||
/// GL bottom-left origin is flipped to top-left on screen. After the flip, screen
|
||||
/// row 0 = panel Y 0 (top of the UI), so the computed Y already matches UiRoot's
|
||||
/// top-left origin — do NOT flip Y again.</para>
|
||||
///
|
||||
/// <para>If <see cref="Selected"/> is non-null a bright-green 2-pixel outline is
|
||||
/// drawn over it using the window draw list.</para>
|
||||
/// </summary>
|
||||
public CanvasInputEvent DrawCanvas(nint panelTex, int width, int height,
|
||||
int windowX, int windowW, int windowY, int windowH)
|
||||
{
|
||||
ImGui.SetNextWindowPos(new Vector2(windowX, windowY), ImGuiCond.FirstUseEver);
|
||||
ImGui.SetNextWindowSize(new Vector2(windowW, windowH), ImGuiCond.FirstUseEver);
|
||||
ImGui.Begin("Canvas");
|
||||
|
||||
var imageSize = new Vector2(width, height);
|
||||
// V-flip: FBO origin is bottom-left; ImGui images expect top-left.
|
||||
// uv0 = bottom-left of texture = top of the panel in screen space.
|
||||
// uv1 = top-right of texture = bottom of the panel in screen space.
|
||||
var uv0 = new Vector2(0f, 1f);
|
||||
var uv1 = new Vector2(1f, 0f);
|
||||
ImGui.Image(panelTex, imageSize, uv0, uv1);
|
||||
|
||||
// rectMin: screen-space top-left of the image AFTER ImGui.Image + any chrome offset.
|
||||
// This is what lets us translate raw mouse screen coords into panel-local pixels.
|
||||
var rectMin = ImGui.GetItemRectMin();
|
||||
|
||||
// ── Selection highlight ───────────────────────────────────────────────
|
||||
var el = Selected;
|
||||
if (el is not null && el.Width > 0f && el.Height > 0f)
|
||||
{
|
||||
var sp = el.ScreenPosition;
|
||||
var p0 = new Vector2(rectMin.X + sp.X, rectMin.Y + sp.Y);
|
||||
var p1 = new Vector2(p0.X + el.Width, p0.Y + el.Height);
|
||||
var dl = ImGui.GetWindowDrawList();
|
||||
dl.AddRect(p0, p1,
|
||||
ImGui.GetColorU32(new Vector4(0.2f, 1f, 0.4f, 1f)),
|
||||
0f, ImDrawFlags.None, 2f);
|
||||
}
|
||||
|
||||
// ── Gather canvas mouse events ────────────────────────────────────────
|
||||
// IsItemHovered is true when the mouse is over the Image item (not just the window).
|
||||
bool hovered = ImGui.IsItemHovered();
|
||||
int mx = 0, my = 0;
|
||||
bool leftDown = false, leftUp = false;
|
||||
int scroll = 0;
|
||||
|
||||
if (hovered)
|
||||
{
|
||||
var mousePos = ImGui.GetMousePos();
|
||||
// Panel-local pixel = mouse offset from the image's screen-space top-left.
|
||||
// Scale is 1:1 (image drawn at full FBO size). Y needs no extra flip — see summary.
|
||||
int ix = (int)(mousePos.X - rectMin.X);
|
||||
int iy = (int)(mousePos.Y - rectMin.Y);
|
||||
// Clamp to image bounds (mouse can be on the image edge pixel).
|
||||
if (ix >= 0 && ix < width && iy >= 0 && iy < height)
|
||||
{
|
||||
mx = ix;
|
||||
my = iy;
|
||||
leftDown = ImGui.IsMouseClicked(ImGuiMouseButton.Left);
|
||||
leftUp = ImGui.IsMouseReleased(ImGuiMouseButton.Left);
|
||||
float wheelY = ImGui.GetIO().MouseWheel;
|
||||
scroll = (int)wheelY; // positive = scroll up
|
||||
}
|
||||
else
|
||||
{
|
||||
// Mouse is over ImGui chrome (title bar, padding) adjacent to image — not over the panel.
|
||||
hovered = false;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.End();
|
||||
return new CanvasInputEvent(hovered, mx, my, leftDown, leftUp, scroll);
|
||||
}
|
||||
|
||||
// ── Tree ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Draw the "Tree" ImGui window. Clicking a node sets <see cref="Selected"/>.</summary>
|
||||
public void DrawTree(UiElement root, int windowX, int windowY, int windowW, int windowH)
|
||||
{
|
||||
ImGui.SetNextWindowPos(new Vector2(windowX, windowY), ImGuiCond.FirstUseEver);
|
||||
ImGui.SetNextWindowSize(new Vector2(windowW, windowH), ImGuiCond.FirstUseEver);
|
||||
ImGui.Begin("Tree");
|
||||
DrawTreeNode(root);
|
||||
ImGui.End();
|
||||
}
|
||||
|
||||
private void DrawTreeNode(UiElement el)
|
||||
{
|
||||
// Label: EventId (hex) + C# type name, e.g. "0x10000001 [UiDatElement]"
|
||||
string label = $"0x{el.EventId:X8} [{el.GetType().Name}]";
|
||||
|
||||
bool isSelected = ReferenceEquals(el, Selected);
|
||||
bool hasChildren = el.Children.Count > 0;
|
||||
|
||||
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags.OpenOnArrow
|
||||
| ImGuiTreeNodeFlags.SpanAvailWidth;
|
||||
if (!hasChildren)
|
||||
flags |= ImGuiTreeNodeFlags.Leaf;
|
||||
if (isSelected)
|
||||
flags |= ImGuiTreeNodeFlags.Selected;
|
||||
|
||||
bool open = ImGui.TreeNodeEx(label, flags);
|
||||
|
||||
// Click on the node label (not the arrow) selects it.
|
||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
|
||||
Selected = el;
|
||||
|
||||
if (open)
|
||||
{
|
||||
foreach (var child in el.Children)
|
||||
DrawTreeNode(child);
|
||||
ImGui.TreePop();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Properties ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Draw the "Properties" ImGui window for <see cref="Selected"/>.</summary>
|
||||
public void DrawProperties(int windowX, int windowY, int windowW, int windowH)
|
||||
{
|
||||
ImGui.SetNextWindowPos(new Vector2(windowX, windowY), ImGuiCond.FirstUseEver);
|
||||
ImGui.SetNextWindowSize(new Vector2(windowW, windowH), ImGuiCond.FirstUseEver);
|
||||
ImGui.Begin("Properties");
|
||||
|
||||
var el = Selected;
|
||||
if (el is null)
|
||||
{
|
||||
ImGui.TextUnformatted("(nothing selected)");
|
||||
ImGui.End();
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui.TextUnformatted($"Id (EventId): 0x{el.EventId:X8}");
|
||||
ImGui.TextUnformatted($"Type: {el.GetType().Name}");
|
||||
ImGui.TextUnformatted($"Name: {el.Name ?? "(null)"}");
|
||||
ImGui.Separator();
|
||||
ImGui.TextUnformatted($"Rect: ({el.Left}, {el.Top}, {el.Width} x {el.Height})");
|
||||
ImGui.TextUnformatted($"Anchors: {el.Anchors}");
|
||||
ImGui.TextUnformatted($"ZOrder: {el.ZOrder}");
|
||||
ImGui.Separator();
|
||||
ImGui.TextUnformatted($"Visible: {el.Visible}");
|
||||
ImGui.TextUnformatted($"Enabled: {el.Enabled}");
|
||||
ImGui.TextUnformatted($"ClickThrough: {el.ClickThrough}");
|
||||
ImGui.TextUnformatted($"Draggable: {el.Draggable}");
|
||||
ImGui.TextUnformatted($"Resizable: {el.Resizable}");
|
||||
ImGui.TextUnformatted($"IsDragSource: {el.IsDragSource}");
|
||||
ImGui.TextUnformatted($"HandlesClick: {el.HandlesClick}");
|
||||
ImGui.TextUnformatted($"Opacity: {el.Opacity:F2}");
|
||||
ImGui.Separator();
|
||||
var sp = el.ScreenPosition;
|
||||
ImGui.TextUnformatted($"ScreenPos: ({sp.X:F1}, {sp.Y:F1})");
|
||||
ImGui.TextUnformatted($"Children: {el.Children.Count}");
|
||||
|
||||
ImGui.End();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
namespace AcDream.App.Studio;
|
||||
|
||||
/// <summary>
|
||||
/// Parsed options for the acdream UI Studio standalone tool.
|
||||
/// Constructed by <see cref="Parse"/> from the command-line tokens that follow
|
||||
/// the <c>ui-studio</c> dispatch token.
|
||||
/// </summary>
|
||||
public sealed record StudioOptions(
|
||||
string DatDir,
|
||||
uint? LayoutId,
|
||||
string? MarkupPath,
|
||||
string? DumpSlug = null,
|
||||
string? DumpFile = null,
|
||||
string? ScreenshotPath = null,
|
||||
bool Mockup = false,
|
||||
string? CapabilityReportPath = null,
|
||||
bool AudioSmoke = false)
|
||||
{
|
||||
/// <summary>
|
||||
/// Parse studio options from the args that come AFTER the <c>ui-studio</c> token.
|
||||
///
|
||||
/// <para>Positional (first non-flag arg): dat directory. Falls back to
|
||||
/// <c>ACDREAM_DAT_DIR</c> when omitted.</para>
|
||||
/// <para><c>--layout 0xNNNN</c>: hex LayoutDesc dat id to preview.</para>
|
||||
/// <para><c>--markup <path></c>: path to a KSML markup file (Task 6, unsupported for now).</para>
|
||||
/// <para><c>--dump <slug></c>: load a panel from the retail UI dump JSON by slug
|
||||
/// (e.g. <c>inventory</c>, <c>radar</c>, <c>toolbar</c>). Static mockup — no controllers.</para>
|
||||
/// <para><c>--dump-file <path></c>: override the default dump file path
|
||||
/// (<c>docs/research/2026-06-25-retail-ui-layout-dump.json</c> from the solution root).
|
||||
/// Only meaningful when <c>--dump</c> is also given.</para>
|
||||
/// <para><c>--screenshot <path></c>: headless mode — render the loaded panel to a PNG
|
||||
/// at <paramref name="path"/> and exit without showing an interactive window.
|
||||
/// Combines with <c>--dump</c> or <c>--layout</c>.</para>
|
||||
/// <para>When neither <c>--layout</c>, <c>--markup</c>, nor <c>--dump</c> is given the
|
||||
/// default layout <c>0x2100006C</c> (vitals) is used.</para>
|
||||
/// </summary>
|
||||
public static StudioOptions Parse(string[] args)
|
||||
{
|
||||
string? datDir = null;
|
||||
uint? layoutId = null;
|
||||
string? markupPath = null;
|
||||
string? dumpSlug = null;
|
||||
string? dumpFile = null;
|
||||
string? screenshotPath = null;
|
||||
string? capabilityReportPath = null;
|
||||
bool mockup = false;
|
||||
bool audioSmoke = false;
|
||||
|
||||
for (int i = 0; i < args.Length; i++)
|
||||
{
|
||||
if (args[i] == "--layout" && i + 1 < args.Length)
|
||||
{
|
||||
var raw = args[++i];
|
||||
// Accept 0xNNNN or plain hex.
|
||||
if (raw.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||
raw = raw[2..];
|
||||
if (uint.TryParse(raw, System.Globalization.NumberStyles.HexNumber, null, out var id))
|
||||
layoutId = id;
|
||||
}
|
||||
else if (args[i] == "--markup" && i + 1 < args.Length)
|
||||
{
|
||||
markupPath = args[++i];
|
||||
}
|
||||
else if (args[i] == "--dump" && i + 1 < args.Length)
|
||||
{
|
||||
dumpSlug = args[++i];
|
||||
}
|
||||
else if (args[i] == "--dump-file" && i + 1 < args.Length)
|
||||
{
|
||||
dumpFile = args[++i];
|
||||
}
|
||||
else if (args[i] == "--screenshot" && i + 1 < args.Length)
|
||||
{
|
||||
screenshotPath = args[++i];
|
||||
}
|
||||
else if (args[i] == "--capability-report" && i + 1 < args.Length)
|
||||
{
|
||||
capabilityReportPath = args[++i];
|
||||
}
|
||||
else if (args[i] == "--audio-smoke")
|
||||
{
|
||||
audioSmoke = true;
|
||||
}
|
||||
else if (args[i] == "--mockup")
|
||||
{
|
||||
mockup = true;
|
||||
}
|
||||
else if (!args[i].StartsWith('-'))
|
||||
{
|
||||
datDir ??= args[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to ACDREAM_DAT_DIR when no positional dat dir was given.
|
||||
datDir ??= Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(datDir))
|
||||
throw new InvalidOperationException(
|
||||
"ui-studio: dat directory required — pass as first arg or set ACDREAM_DAT_DIR.");
|
||||
|
||||
// Default layout: vitals (0x2100006C), unless a dump slug or markup is requested.
|
||||
if (!mockup && layoutId is null && markupPath is null && dumpSlug is null)
|
||||
layoutId = 0x2100006Cu;
|
||||
|
||||
return new StudioOptions(
|
||||
datDir,
|
||||
layoutId,
|
||||
markupPath,
|
||||
dumpSlug,
|
||||
dumpFile,
|
||||
screenshotPath,
|
||||
mockup,
|
||||
capabilityReportPath,
|
||||
audioSmoke);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the dump file path for this session:
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="DumpFile"/> if explicitly set.</item>
|
||||
/// <item>Otherwise <c><solutionRoot>/docs/research/2026-06-25-retail-ui-layout-dump.json</c>.</item>
|
||||
/// </list>
|
||||
/// Returns null when neither the override nor the default file exists.
|
||||
/// </summary>
|
||||
public string? ResolveDumpFile()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(DumpFile))
|
||||
return DumpFile;
|
||||
|
||||
// Walk up from the App binary to the solution root (same approach as
|
||||
// ConformanceDats.SolutionRoot in the test project).
|
||||
var dir = AppContext.BaseDirectory;
|
||||
while (!string.IsNullOrEmpty(dir))
|
||||
{
|
||||
var candidate = Path.Combine(dir, "docs", "research",
|
||||
"2026-06-25-retail-ui-layout-dump.json");
|
||||
if (File.Exists(candidate))
|
||||
return candidate;
|
||||
dir = Path.GetDirectoryName(dir);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,675 +0,0 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Content;
|
||||
using AcDream.App.Platform;
|
||||
using AcDream.App.Audio;
|
||||
using AcDream.Core.Audio;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.Runtime.Platform;
|
||||
using DatReaderWriter;
|
||||
using Silk.NET.Input;
|
||||
using Silk.NET.Maths;
|
||||
using Silk.NET.OpenGL;
|
||||
using Silk.NET.Windowing;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using static Silk.NET.OpenGL.ClearBufferMask;
|
||||
|
||||
namespace AcDream.App.Studio;
|
||||
|
||||
/// <summary>
|
||||
/// Standalone Silk.NET window that boots the production render stack
|
||||
/// (<see cref="RenderBootstrap"/>) and previews a single UI panel
|
||||
/// identified by a <see cref="LayoutSource"/>.
|
||||
///
|
||||
/// Usage: dotnet run -- ui-studio [dat-dir] [--layout 0xNNNN] [--markup path] [--mockup]
|
||||
///
|
||||
/// Task 3 adds an ImGui IDE on top of the panel FBO:
|
||||
/// <list type="bullet">
|
||||
/// <item>Canvas pane — the panel rendered off-screen via <see cref="PanelFbo"/>.</item>
|
||||
/// <item>Tree pane — the element hierarchy; click-to-select.</item>
|
||||
/// <item>Properties pane — geometry/anchors/flags of the selected element.</item>
|
||||
/// <item>Click-to-inspect — a left-click in the canvas selects the topmost
|
||||
/// element under the cursor via <see cref="UiRoot.Pick"/>.</item>
|
||||
/// </list>
|
||||
///
|
||||
/// The window is intentionally thin: no game world, no physics, no streaming —
|
||||
/// just GL + UiHost + the layout under test, identical to how the panel
|
||||
/// appears inside <c>GameWindow</c>.
|
||||
/// </summary>
|
||||
public sealed class StudioWindow : IDisposable
|
||||
{
|
||||
private readonly StudioOptions _opts;
|
||||
private readonly ApplicationPathSet _applicationPaths;
|
||||
private readonly GraphicalHostPlatformServices _platformServices;
|
||||
|
||||
// Created in OnLoad, released in OnClosing.
|
||||
private IWindow? _window;
|
||||
private GL? _gl;
|
||||
private IInputContext? _input;
|
||||
private OpenAlAudioEngine? _audio;
|
||||
private IDatReaderWriter? _dats;
|
||||
private RenderStack? _stack;
|
||||
private LayoutSource? _source;
|
||||
|
||||
// Task 3 additions.
|
||||
private AcDream.UI.ImGui.ImGuiBootstrapper? _imgui;
|
||||
private PanelFbo? _panelFbo;
|
||||
private StudioInspector? _inspector;
|
||||
private UiElement? _panelRoot; // top-level element added to UiRoot (for hit-test + tree)
|
||||
|
||||
// UX-pass additions: panel picker + current-slug tracking.
|
||||
private string? _currentSlug; // slug of the panel currently displayed (null = non-dump mode)
|
||||
private string? _dumpFile; // resolved dump file path (once, in OnLoad)
|
||||
private IReadOnlyList<string> _dumpSlugs = Array.Empty<string>(); // all slugs from the dump
|
||||
|
||||
// Task 4: sample data table — built once in OnLoad and kept alive for the window's lifetime
|
||||
// so the controller subscriptions (ObjectAdded/ObjectMoved etc.) fire correctly.
|
||||
private AcDream.Core.Items.ClientObjectTable? _objects;
|
||||
private IRetainedPanelController? _fixtureController;
|
||||
|
||||
// Headless screenshot mode: set when --screenshot was passed.
|
||||
// True after the first OnRender fires the screenshot (guard against repeat).
|
||||
private bool _screenshotDone;
|
||||
private readonly StudioFrameCloseGate _frameCloseGate = new();
|
||||
private GraphicalCapabilityRecord? _capabilities;
|
||||
private string? _capabilityReportPath;
|
||||
|
||||
public StudioWindow(StudioOptions opts)
|
||||
: this(
|
||||
opts,
|
||||
GraphicalHostPlatformServices.Resolve())
|
||||
{
|
||||
}
|
||||
|
||||
internal StudioWindow(
|
||||
StudioOptions opts,
|
||||
ApplicationPathSet applicationPaths)
|
||||
: this(
|
||||
opts,
|
||||
GraphicalHostPlatformServices.Resolve() with
|
||||
{
|
||||
Paths = applicationPaths
|
||||
?? throw new ArgumentNullException(
|
||||
nameof(applicationPaths)),
|
||||
})
|
||||
{
|
||||
}
|
||||
|
||||
internal StudioWindow(
|
||||
StudioOptions opts,
|
||||
GraphicalHostPlatformServices platformServices)
|
||||
{
|
||||
_opts = opts ?? throw new ArgumentNullException(nameof(opts));
|
||||
_platformServices = platformServices
|
||||
?? throw new ArgumentNullException(nameof(platformServices));
|
||||
_applicationPaths = _platformServices.Paths;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Open the window and block until it is closed.
|
||||
/// Mirrors <c>GameWindow.Run()</c>.
|
||||
/// </summary>
|
||||
public void Run()
|
||||
{
|
||||
_platformServices.ConfigureWindowBackend();
|
||||
// Resolve quality settings the same way GameWindow.Run() does
|
||||
// (SettingsStore → QualitySettings.From → WithEnvOverrides).
|
||||
var startupStore = new AcDream.UI.Abstractions.Panels.Settings.SettingsStore(
|
||||
_applicationPaths.SettingsFile);
|
||||
var startupDisplay = startupStore.LoadDisplay();
|
||||
var startupBase = AcDream.UI.Abstractions.Settings.QualitySettings.From(startupDisplay.Quality);
|
||||
var startupQuality = AcDream.UI.Abstractions.Settings.QualitySettings.WithEnvOverrides(startupBase);
|
||||
|
||||
var options = WindowOptions.Default with
|
||||
{
|
||||
Size = new Vector2D<int>(1280, 720),
|
||||
Title = "acdream UI Studio",
|
||||
API = new GraphicsAPI(
|
||||
ContextAPI.OpenGL,
|
||||
ContextProfile.Core,
|
||||
ContextFlags.ForwardCompatible,
|
||||
new APIVersion(4, 3)),
|
||||
VSync = false,
|
||||
// MSAA from quality preset — must be baked into the GL context at creation.
|
||||
Samples = startupQuality.MsaaSamples,
|
||||
PreferredStencilBufferBits = 8,
|
||||
// Headless screenshot mode: hide the window so no desktop flash occurs.
|
||||
// The GL context is still fully valid on a hidden window; FBO rendering
|
||||
// is off-screen and independent of window visibility.
|
||||
IsVisible = _opts.ScreenshotPath is null,
|
||||
};
|
||||
|
||||
_window = Window.Create(options);
|
||||
_window.Load += OnLoad;
|
||||
_window.Update += OnUpdate;
|
||||
_window.Render += OnRender;
|
||||
_window.Closing += OnClosing;
|
||||
_window.Run();
|
||||
}
|
||||
|
||||
private void OnLoad()
|
||||
{
|
||||
_gl = GL.GetApi(_window!);
|
||||
_input = _window!.CreateInput();
|
||||
_capabilityReportPath = Path.GetFullPath(
|
||||
_opts.CapabilityReportPath
|
||||
?? Path.Combine(
|
||||
_applicationPaths.DiagnosticsDirectory,
|
||||
"graphical-capabilities.json"));
|
||||
_capabilities = GraphicalCapabilityGuard.CaptureVerifyAndWrite(
|
||||
_gl,
|
||||
_window!,
|
||||
_input,
|
||||
_platformServices,
|
||||
_capabilityReportPath);
|
||||
GraphicalCapabilityGuard.ThrowIfUnsupported(
|
||||
_capabilities,
|
||||
_capabilityReportPath);
|
||||
|
||||
if (_opts.AudioSmoke)
|
||||
{
|
||||
_audio = new OpenAlAudioEngine();
|
||||
bool playback = _audio.IsAvailable
|
||||
&& _audio.PlayUiWave(
|
||||
0xFFFF_FF01u,
|
||||
CreateAudioSmokeWave(),
|
||||
volume: 0.05f);
|
||||
_capabilities = _capabilities with
|
||||
{
|
||||
Audio = new GraphicalAudioCapabilities(
|
||||
Requested: true,
|
||||
Available: _audio.IsAvailable,
|
||||
PlaybackSubmitted: playback,
|
||||
DisposalComplete: _audio.IsDisposalComplete,
|
||||
Backend: "OpenAL Soft default device"),
|
||||
Lifecycle = _capabilities.Lifecycle with
|
||||
{
|
||||
OwnedAudioEngineCount = 1,
|
||||
},
|
||||
};
|
||||
GraphicalCapabilityReportWriter.Write(
|
||||
_capabilityReportPath,
|
||||
_capabilities);
|
||||
}
|
||||
|
||||
_dats = RuntimeDatCollectionFactory.OpenReadOnly(_opts.DatDir);
|
||||
|
||||
// Build QualitySettings for RenderBootstrap (same as Run() above — re-read
|
||||
// after the GL context is confirmed, mirroring GameWindow.OnLoad).
|
||||
var store = new AcDream.UI.Abstractions.Panels.Settings.SettingsStore(
|
||||
_applicationPaths.SettingsFile);
|
||||
var display = store.LoadDisplay();
|
||||
var quality = AcDream.UI.Abstractions.Settings.QualitySettings.WithEnvOverrides(
|
||||
AcDream.UI.Abstractions.Settings.QualitySettings.From(display.Quality));
|
||||
|
||||
_stack = RenderBootstrap.Create(
|
||||
_gl,
|
||||
_dats,
|
||||
new RenderBootstrapOptions(
|
||||
quality,
|
||||
_applicationPaths.DiagnosticsDirectory));
|
||||
|
||||
if (_opts.Mockup)
|
||||
MockupDesktop.Load(_dats, _stack);
|
||||
|
||||
// Load the panel described by options and add it to the UI tree.
|
||||
// Task 4b: --dump <slug> uses DumpLayout (static retail mockup, no controllers).
|
||||
// All other modes use LayoutSource + FixtureProvider (production path).
|
||||
//
|
||||
// Fix C: pass the per-element font resolver into LayoutSource so that elements
|
||||
// with a non-zero FontDid get their own dat font at build time. This is wired
|
||||
// ONLY in the studio path; GameWindow's Import calls continue to pass null so the
|
||||
// live game path is provably unchanged (follow-up: GameWindow font-resolver wire-up).
|
||||
_source = new LayoutSource(_dats, _stack.ResolveChrome, _stack.VitalsDatFont,
|
||||
fontResolve: _stack.ResolveDatFont);
|
||||
|
||||
// Resolve the dump file once (used by OnLoad + by LoadDumpPanel at runtime).
|
||||
_dumpFile = _opts.ResolveDumpFile();
|
||||
if (_dumpFile is not null)
|
||||
_dumpSlugs = UiDumpModel.ListSlugs(_dumpFile)
|
||||
.OrderBy(s => s, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
|
||||
UiElement? root = null;
|
||||
if (!_opts.Mockup && _opts.DumpSlug is not null)
|
||||
{
|
||||
if (_dumpFile is null)
|
||||
{
|
||||
Console.Error.WriteLine("[studio] --dump: retail UI dump file not found. " +
|
||||
"Expected docs/research/2026-06-25-retail-ui-layout-dump.json in the source tree, " +
|
||||
"or pass --dump-file <path>.");
|
||||
root = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
root = DumpLayout.Load(_dumpFile, _opts.DumpSlug, _stack.ResolveChrome, out var dumpErr);
|
||||
if (root is null)
|
||||
Console.Error.WriteLine($"[studio] dump load failed: {dumpErr}");
|
||||
else
|
||||
_currentSlug = _opts.DumpSlug;
|
||||
}
|
||||
}
|
||||
else if (!_opts.Mockup)
|
||||
{
|
||||
root = _source.Load(_opts);
|
||||
if (root is null)
|
||||
Console.Error.WriteLine($"[studio] panel load failed: {_source.LastError}");
|
||||
else
|
||||
NormalizeSinglePanelRoot(root);
|
||||
}
|
||||
|
||||
_panelRoot = root;
|
||||
if (root is not null)
|
||||
{
|
||||
_stack.UiHost.Root.AddChild(root);
|
||||
|
||||
// Task 4: populate the panel with sample data via production controllers,
|
||||
// so inventory / vitals / toolbar panels render with plausible content.
|
||||
// Dump source is static — no FixtureProvider needed.
|
||||
if (_opts.DumpSlug is null && _source.CurrentLayout is not null)
|
||||
{
|
||||
uint layoutId = _opts.LayoutId ?? 0x2100006Cu;
|
||||
_objects = SampleData.BuildObjectTable();
|
||||
_fixtureController = FixtureProvider.Populate(
|
||||
layoutId, _source.CurrentLayout, _stack, _objects, _dats);
|
||||
}
|
||||
}
|
||||
|
||||
// Task 3: ImGui IDE — interactive mode only.
|
||||
// Headless screenshot mode needs only PanelFbo; ImGui/inspector/input are skipped.
|
||||
_panelFbo = new PanelFbo(_gl);
|
||||
if (_opts.ScreenshotPath is null)
|
||||
{
|
||||
// Mockup mode draws UiHost directly to the window, so raw Silk mouse coordinates
|
||||
// already match UI pixels. Inspector mode draws UiHost inside an ImGui canvas, so
|
||||
// mouse is forwarded manually from DrawCanvas with canvas-local remapping.
|
||||
if (_opts.Mockup)
|
||||
foreach (var mouse in _input.Mice)
|
||||
_stack.UiHost.WireMouse(mouse);
|
||||
foreach (var kb in _input.Keyboards)
|
||||
_stack.UiHost.WireKeyboard(kb);
|
||||
|
||||
if (_opts.Mockup)
|
||||
return;
|
||||
|
||||
_imgui = new AcDream.UI.ImGui.ImGuiBootstrapper(
|
||||
_gl,
|
||||
_window!,
|
||||
_input);
|
||||
_inspector = new StudioInspector();
|
||||
}
|
||||
}
|
||||
|
||||
private static WaveData CreateAudioSmokeWave()
|
||||
{
|
||||
const int sampleRate = 8_000;
|
||||
const int sampleCount = 400;
|
||||
var pcm = new byte[sampleCount * sizeof(short)];
|
||||
for (int i = 0; i < sampleCount; i++)
|
||||
{
|
||||
double phase = i * (2.0 * Math.PI * 440.0 / sampleRate);
|
||||
short sample = (short)(Math.Sin(phase) * short.MaxValue * 0.08);
|
||||
BitConverter.TryWriteBytes(
|
||||
pcm.AsSpan(i * sizeof(short), sizeof(short)),
|
||||
sample);
|
||||
}
|
||||
|
||||
return new WaveData
|
||||
{
|
||||
ChannelCount = 1,
|
||||
SampleRate = sampleRate,
|
||||
BitsPerSample = 16,
|
||||
PcmBytes = pcm,
|
||||
Duration = TimeSpan.FromSeconds(
|
||||
sampleCount / (double)sampleRate),
|
||||
};
|
||||
}
|
||||
|
||||
private void OnUpdate(double dt) { }
|
||||
|
||||
private void OnRender(double dt)
|
||||
{
|
||||
if (_stack is null || _panelFbo is null) return;
|
||||
RenderStack frameStack = _stack;
|
||||
frameStack.BeginFrame();
|
||||
Exception? renderFailure = null;
|
||||
bool frameClosed = false;
|
||||
try
|
||||
{
|
||||
|
||||
// ── HEADLESS SCREENSHOT PATH ──────────────────────────────────────────────
|
||||
if (_opts.ScreenshotPath is not null)
|
||||
{
|
||||
if (_screenshotDone) return; // fire exactly once
|
||||
_screenshotDone = true;
|
||||
|
||||
// Pick render size from the loaded root's bounds (clamped to sane limits).
|
||||
// Fall back to 1280×720 when the root has no explicit size.
|
||||
int w = 1280, h = 720;
|
||||
if (!_opts.Mockup && _panelRoot is not null)
|
||||
{
|
||||
float rw = _panelRoot.Width;
|
||||
float rh = _panelRoot.Height;
|
||||
if (rw >= 1f && rh >= 1f)
|
||||
{
|
||||
w = Math.Clamp((int)rw, 256, 2048);
|
||||
h = Math.Clamp((int)rh, 256, 2048);
|
||||
}
|
||||
}
|
||||
|
||||
// Tick once so widget state is initialised (e.g. bar fills).
|
||||
_stack.UiHost.Tick(dt);
|
||||
|
||||
// Render the panel into the FBO.
|
||||
_panelFbo.Render(w, h, _stack.UiHost);
|
||||
|
||||
// Read back RGBA pixels (FBO origin = bottom-left).
|
||||
byte[] pixels = _panelFbo.ReadColorRgba(w, h);
|
||||
if (pixels.Length == 0)
|
||||
{
|
||||
Console.Error.WriteLine("[studio-screenshot] FBO readback returned no pixels.");
|
||||
_frameCloseGate.Request();
|
||||
return;
|
||||
}
|
||||
|
||||
// Flip rows vertically: FBO bottom-left → PNG top-left.
|
||||
int stride = w * 4;
|
||||
byte[] flipped = new byte[pixels.Length];
|
||||
for (int row = 0; row < h; row++)
|
||||
{
|
||||
System.Buffer.BlockCopy(pixels, row * stride, flipped, (h - 1 - row) * stride, stride);
|
||||
}
|
||||
|
||||
// Build ImageSharp image and save as PNG.
|
||||
var path = _opts.ScreenshotPath;
|
||||
var dir = Path.GetDirectoryName(path);
|
||||
if (!string.IsNullOrEmpty(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
using var img = Image.LoadPixelData<Rgba32>(flipped, w, h);
|
||||
img.SaveAsPng(path);
|
||||
Console.WriteLine($"[studio-screenshot] wrote {path} ({w}x{h})");
|
||||
|
||||
_frameCloseGate.Request();
|
||||
return;
|
||||
}
|
||||
|
||||
// ── INTERACTIVE PATH ──────────────────────────────────────────────────────
|
||||
if (_opts.Mockup)
|
||||
{
|
||||
var mockupGl = _stack.Gl;
|
||||
int mockupW = _window!.Size.X;
|
||||
int mockupH = _window!.Size.Y;
|
||||
_stack.UiHost.Tick(dt);
|
||||
mockupGl.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
|
||||
mockupGl.Viewport(0, 0, (uint)mockupW, (uint)mockupH);
|
||||
mockupGl.ClearColor(0.08f, 0.08f, 0.08f, 1f);
|
||||
mockupGl.Clear(ColorBufferBit | DepthBufferBit);
|
||||
_stack.UiHost.Draw(new Vector2(mockupW, mockupH));
|
||||
return;
|
||||
}
|
||||
|
||||
if (_imgui is null || _inspector is null) return;
|
||||
|
||||
var gl = _stack.Gl;
|
||||
int iw = _window!.Size.X;
|
||||
int ih = _window!.Size.Y;
|
||||
|
||||
// 1. Tick the UI widgets (OnRender's own dt — Update + Render fire with the same delta).
|
||||
_stack.UiHost.Tick(dt);
|
||||
|
||||
// 2. Render the panel into the off-screen FBO; get the color texture.
|
||||
// The FBO is the same logical size as the window, so element rects map 1:1 to
|
||||
// FBO pixels — no scale factor needed when displaying the canvas at full size.
|
||||
uint panelTex = _panelFbo.Render(iw, ih, _stack.UiHost);
|
||||
|
||||
// 3. Clear the window back-buffer (the dark ImGui background shows behind panes).
|
||||
gl.ClearColor(0.1f, 0.1f, 0.1f, 1f);
|
||||
gl.Clear(ColorBufferBit | DepthBufferBit);
|
||||
|
||||
// 4. Begin the ImGui frame.
|
||||
_imgui.BeginFrame((float)dt);
|
||||
|
||||
// ── Layout constants (fixed pane arrangement, FirstUseEver) ──────────────
|
||||
// MenuBar: always-on-top main menu bar (~22px) — panel picker lives here so it
|
||||
// is never covered by the floating panes (replaces the old 40px toolbar).
|
||||
// Tree: 280px wide on the left, below menu bar.
|
||||
// Canvas: centre strip between tree and properties.
|
||||
// Props: 340px wide on the right, below menu bar.
|
||||
const int kMenuBarH = 22; // ImGui default main menu bar height
|
||||
const int kTreeW = 280;
|
||||
const int kPropsW = 340;
|
||||
int canvasX = kTreeW;
|
||||
int canvasW = Math.Max(1, iw - kTreeW - kPropsW);
|
||||
int propsX = iw - kPropsW;
|
||||
int paneY = kMenuBarH;
|
||||
int paneH = Math.Max(1, ih - kMenuBarH);
|
||||
|
||||
// 5. Main menu bar — panel picker combo pinned to the window top.
|
||||
// BeginMainMenuBar returns true when the bar is visible (always is); the combo
|
||||
// inside it is always-on-top and is never occluded by Tree/Canvas/Props panes.
|
||||
string? pickedSlug = null;
|
||||
if (ImGuiNET.ImGui.BeginMainMenuBar())
|
||||
{
|
||||
ImGuiNET.ImGui.SetNextItemWidth(300f);
|
||||
string preview = _currentSlug ?? "(none)";
|
||||
if (ImGuiNET.ImGui.BeginCombo("Panel", preview))
|
||||
{
|
||||
foreach (var slug in _dumpSlugs)
|
||||
{
|
||||
bool selected = string.Equals(slug, _currentSlug,
|
||||
System.StringComparison.OrdinalIgnoreCase);
|
||||
if (ImGuiNET.ImGui.Selectable(slug, selected) && !selected)
|
||||
pickedSlug = slug;
|
||||
if (selected)
|
||||
ImGuiNET.ImGui.SetItemDefaultFocus();
|
||||
}
|
||||
ImGuiNET.ImGui.EndCombo();
|
||||
}
|
||||
ImGuiNET.ImGui.EndMainMenuBar();
|
||||
}
|
||||
if (pickedSlug is not null)
|
||||
LoadDumpPanel(pickedSlug);
|
||||
|
||||
// 6. Canvas pane — show the FBO texture; gather canvas mouse events.
|
||||
var canvasEvt = default(CanvasInputEvent);
|
||||
if (panelTex != 0)
|
||||
canvasEvt = _inspector.DrawCanvas(
|
||||
(nint)panelTex, iw, ih,
|
||||
canvasX, canvasW, paneY, paneH);
|
||||
|
||||
// 7. Forward canvas mouse events to the panel UiHost or the inspector tree.
|
||||
//
|
||||
// Coordinate mapping (see StudioInspector.DrawCanvas summary):
|
||||
// panel-local pixel = raw_mouse - ImGui.GetItemRectMin() (1:1 scale, no Y flip)
|
||||
// The image is drawn V-flipped (uv0.Y=1, uv1.Y=0) so screen top = panel Y=0.
|
||||
//
|
||||
// Interact mode (default): canvas mouse events go directly to UiRoot so elements respond.
|
||||
// OnMouseMove + OnMouseDown/Up + OnScroll are all forwarded.
|
||||
// A Console.WriteLine confirms each forwarded left-click for live verification.
|
||||
//
|
||||
// Inspect mode: left-click hit-tests and selects the element in the tree (old behavior).
|
||||
// OnMouseMove is still forwarded so hover/tooltip state in the panel stays live.
|
||||
if (canvasEvt.IsHovered)
|
||||
{
|
||||
int mx = canvasEvt.MouseX;
|
||||
int my = canvasEvt.MouseY;
|
||||
var root = _stack.UiHost.Root;
|
||||
|
||||
// Always forward mouse-move so hover highlights / tooltips in the panel work.
|
||||
root.OnMouseMove(mx, my);
|
||||
|
||||
if (_inspector.InteractMode)
|
||||
{
|
||||
// ── Interact: live panel interaction ──────────────────────────────
|
||||
if (canvasEvt.LeftDown)
|
||||
{
|
||||
Console.WriteLine($"[studio] canvas click → panel ({mx}, {my})");
|
||||
root.OnMouseDown(UiMouseButton.Left, mx, my);
|
||||
}
|
||||
if (canvasEvt.LeftUp)
|
||||
root.OnMouseUp(UiMouseButton.Left, mx, my);
|
||||
if (canvasEvt.ScrollDelta != 0)
|
||||
root.OnScroll(canvasEvt.ScrollDelta);
|
||||
}
|
||||
else
|
||||
{
|
||||
// ── Inspect: click selects an element in the tree ─────────────────
|
||||
if (canvasEvt.LeftDown)
|
||||
{
|
||||
var hit = root.Pick(mx, my);
|
||||
if (hit is not null)
|
||||
_inspector.Selected = hit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Element tree pane.
|
||||
if (_panelRoot is not null)
|
||||
_inspector.DrawTree(_panelRoot, 0, paneY, kTreeW, paneH);
|
||||
|
||||
// 9. Properties pane.
|
||||
_inspector.DrawProperties(propsX, paneY, kPropsW, paneH);
|
||||
|
||||
// 9. Finalise ImGui and flush draw data to the window.
|
||||
_imgui.Render();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
renderFailure = ex;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
frameStack.EndFrame();
|
||||
frameClosed = true;
|
||||
}
|
||||
catch (Exception closeFailure) when (renderFailure is not null)
|
||||
{
|
||||
throw new AggregateException(
|
||||
"UI Studio rendering failed and its GPU frame could not be closed.",
|
||||
renderFailure,
|
||||
closeFailure);
|
||||
}
|
||||
|
||||
if (frameClosed)
|
||||
_frameCloseGate.CompleteFrame(() => _window?.Close());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load a different dump panel at runtime (no relaunch required).
|
||||
/// Removes the current <see cref="_panelRoot"/> from the UI tree,
|
||||
/// loads the named slug from the dump, and installs the new root.
|
||||
/// Resets <see cref="StudioInspector.Selected"/> to null.
|
||||
/// No-op when the dump file is not available or the slug fails to load.
|
||||
/// </summary>
|
||||
public void LoadDumpPanel(string slug)
|
||||
{
|
||||
if (_stack is null || _inspector is null) return;
|
||||
if (_dumpFile is null)
|
||||
{
|
||||
Console.Error.WriteLine("[studio] LoadDumpPanel: dump file not available.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the existing panel root from the tree. Fixture controllers
|
||||
// own subscriptions into the sample table and end with their panel.
|
||||
_fixtureController?.Dispose();
|
||||
_fixtureController = null;
|
||||
if (_panelRoot is not null)
|
||||
{
|
||||
_stack.UiHost.Root.RemoveChild(_panelRoot);
|
||||
_panelRoot = null;
|
||||
}
|
||||
|
||||
// Load the new panel.
|
||||
var newRoot = DumpLayout.Load(_dumpFile, slug, _stack.ResolveChrome, out var err);
|
||||
if (newRoot is null)
|
||||
{
|
||||
Console.Error.WriteLine($"[studio] LoadDumpPanel('{slug}') failed: {err}");
|
||||
_currentSlug = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_stack.UiHost.Root.AddChild(newRoot);
|
||||
_panelRoot = newRoot;
|
||||
_currentSlug = slug;
|
||||
_inspector.Selected = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Studio dat-layout previews show one panel in isolation, not at its live-game
|
||||
/// screen position. Some top-level layouts (inventory: x=500,y=138) otherwise
|
||||
/// draw entirely outside the root-sized screenshot FBO.
|
||||
/// </summary>
|
||||
internal static void NormalizeSinglePanelRoot(UiElement root)
|
||||
{
|
||||
root.Left = 0f;
|
||||
root.Top = 0f;
|
||||
root.Anchors = AnchorEdges.None;
|
||||
}
|
||||
|
||||
private void OnClosing()
|
||||
{
|
||||
ReleaseContextResources();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ReleaseContextResources();
|
||||
_window?.Dispose();
|
||||
_window = null;
|
||||
UpdateFinalCapabilityReport();
|
||||
}
|
||||
|
||||
private void ReleaseContextResources()
|
||||
{
|
||||
_fixtureController?.Dispose();
|
||||
_fixtureController = null;
|
||||
_imgui?.Dispose();
|
||||
_panelFbo?.Dispose();
|
||||
_imgui = null;
|
||||
_panelFbo = null;
|
||||
// If OnClosing wasn't called (e.g. an exception before Run() completed), dispose the FULL
|
||||
// stack anyway — the review flagged that disposing only UiHost here leaked the rest.
|
||||
_stack?.Dispose();
|
||||
_dats?.Dispose();
|
||||
_audio?.Dispose();
|
||||
_input?.Dispose();
|
||||
_gl?.Dispose();
|
||||
_dats = null;
|
||||
_stack = null;
|
||||
_audio = null;
|
||||
_input = null;
|
||||
_gl = null;
|
||||
}
|
||||
|
||||
private void UpdateFinalCapabilityReport()
|
||||
{
|
||||
if (_capabilities is null
|
||||
|| string.IsNullOrWhiteSpace(_capabilityReportPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_capabilities = _capabilities with
|
||||
{
|
||||
Audio = _capabilities.Audio with
|
||||
{
|
||||
DisposalComplete = true,
|
||||
},
|
||||
Lifecycle = new GraphicalSmokeLifecycleCapabilities(
|
||||
OwnedWindowCount: 0,
|
||||
OwnedGlApiCount: 0,
|
||||
OwnedInputContextCount: 0,
|
||||
OwnedAudioEngineCount: 0,
|
||||
ShutdownComplete: true),
|
||||
};
|
||||
GraphicalCapabilityReportWriter.Write(
|
||||
_capabilityReportPath,
|
||||
_capabilities);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AcDream.App.Studio;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// UiDumpModel — POCOs for docs/research/2026-06-25-retail-ui-layout-dump.json
|
||||
//
|
||||
// Schema (v1):
|
||||
// { "version":1, "panels":[ { "id":int, "slug":string, "title":string,
|
||||
// "bucket":string, "parent_slug":string|null,
|
||||
// "width":int, "height":int,
|
||||
// "nodes":[ { "traversal_index":int, "element_id":int,
|
||||
// "layout_id":int, "parent_layout_id":int|null,
|
||||
// "parent_traversal_index":int|null, "base_layout_id":int,
|
||||
// "rect":{x,y,width,height},
|
||||
// "widget_kind":"Group"|"Sprite"|"Button"|"Scrollbar"|"Slider",
|
||||
// "state_set":{ "default_image":{image_id,alpha_image_id}|null,
|
||||
// "states":[{state_id,image:{...}}] }
|
||||
// } ] } ] }
|
||||
//
|
||||
// All ids in the dump are DECIMAL ints (e.g. element_id=268435925 = 0x100001D5,
|
||||
// image_id=100693194 = 0x060074CA). Cast to uint before use in dat/GL APIs.
|
||||
//
|
||||
// Rect coordinates are ABSOLUTE (screen-space origin = panel's design position
|
||||
// in retail layout, NOT relative to the parent). DumpLayout.Load converts them
|
||||
// to parent-relative when building the UiElement tree.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Top-level container for the retail UI layout dump.</summary>
|
||||
public sealed class UiDump
|
||||
{
|
||||
[JsonPropertyName("version")]
|
||||
public int Version { get; set; }
|
||||
|
||||
[JsonPropertyName("panels")]
|
||||
public List<DumpPanel> Panels { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>One panel (window) exported from the retail UI.</summary>
|
||||
public sealed class DumpPanel
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[JsonPropertyName("slug")]
|
||||
public string Slug { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("bucket")]
|
||||
public string Bucket { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("parent_slug")]
|
||||
public string? ParentSlug { get; set; }
|
||||
|
||||
[JsonPropertyName("width")]
|
||||
public float Width { get; set; }
|
||||
|
||||
[JsonPropertyName("height")]
|
||||
public float Height { get; set; }
|
||||
|
||||
[JsonPropertyName("nodes")]
|
||||
public List<DumpNode> Nodes { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>One element node within a panel's traversal list.</summary>
|
||||
public sealed class DumpNode
|
||||
{
|
||||
[JsonPropertyName("traversal_index")]
|
||||
public int TraversalIndex { get; set; }
|
||||
|
||||
[JsonPropertyName("element_id")]
|
||||
public long ElementId { get; set; }
|
||||
|
||||
[JsonPropertyName("layout_id")]
|
||||
public long LayoutId { get; set; }
|
||||
|
||||
[JsonPropertyName("parent_layout_id")]
|
||||
public long? ParentLayoutId { get; set; }
|
||||
|
||||
[JsonPropertyName("parent_traversal_index")]
|
||||
public int? ParentTraversalIndex { get; set; }
|
||||
|
||||
[JsonPropertyName("base_layout_id")]
|
||||
public long BaseLayoutId { get; set; }
|
||||
|
||||
[JsonPropertyName("rect")]
|
||||
public DumpRect Rect { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("widget_kind")]
|
||||
public string WidgetKind { get; set; } = "Group";
|
||||
|
||||
[JsonPropertyName("state_set")]
|
||||
public DumpStateSet StateSet { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>Absolute screen-space rect (see comment above — must subtract parent rect for UiElement).</summary>
|
||||
public sealed class DumpRect
|
||||
{
|
||||
[JsonPropertyName("x")]
|
||||
public float X { get; set; }
|
||||
|
||||
[JsonPropertyName("y")]
|
||||
public float Y { get; set; }
|
||||
|
||||
[JsonPropertyName("width")]
|
||||
public float Width { get; set; }
|
||||
|
||||
[JsonPropertyName("height")]
|
||||
public float Height { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>State set for a node — default image plus per-state overrides.</summary>
|
||||
public sealed class DumpStateSet
|
||||
{
|
||||
[JsonPropertyName("default_image")]
|
||||
public DumpImage? DefaultImage { get; set; }
|
||||
|
||||
[JsonPropertyName("states")]
|
||||
public List<DumpState> States { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>Image reference (RenderSurface dat id + optional separate alpha surface).</summary>
|
||||
public sealed class DumpImage
|
||||
{
|
||||
[JsonPropertyName("image_id")]
|
||||
public long ImageId { get; set; }
|
||||
|
||||
[JsonPropertyName("alpha_image_id")]
|
||||
public long? AlphaImageId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>A named state override.</summary>
|
||||
public sealed class DumpState
|
||||
{
|
||||
[JsonPropertyName("state_id")]
|
||||
public int StateId { get; set; }
|
||||
|
||||
[JsonPropertyName("image")]
|
||||
public DumpImage Image { get; set; } = new();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Helper statics
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Parsing helpers for the retail UI dump JSON.
|
||||
/// </summary>
|
||||
public static class UiDumpModel
|
||||
{
|
||||
private static readonly JsonSerializerOptions _opts = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
AllowTrailingCommas = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
};
|
||||
|
||||
/// <summary>Parse the full dump from a file path. Returns null on failure.</summary>
|
||||
public static UiDump? Parse(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
return JsonSerializer.Deserialize<UiDump>(stream, _opts);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the list of slugs in the dump (for smoke-testing every panel).
|
||||
/// Returns an empty list if the file cannot be parsed.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<string> ListSlugs(string path)
|
||||
{
|
||||
var dump = Parse(path);
|
||||
if (dump is null) return Array.Empty<string>();
|
||||
return dump.Panels.Select(p => p.Slug).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pick the sprite id to use for a node: prefer default_image.image_id;
|
||||
/// fall back to states[0].image.image_id; return 0 if neither exists.
|
||||
/// </summary>
|
||||
public static uint PickImageId(DumpNode node)
|
||||
{
|
||||
if (node.StateSet.DefaultImage is { ImageId: > 0 } di)
|
||||
return (uint)di.ImageId;
|
||||
if (node.StateSet.States.Count > 0 && node.StateSet.States[0].Image.ImageId > 0)
|
||||
return (uint)node.StateSet.States[0].Image.ImageId;
|
||||
return 0u;
|
||||
}
|
||||
}
|
||||
94
src/AcDream.App/UI/Layout/SampleData.cs
Normal file
94
src/AcDream.App/UI/Layout/SampleData.cs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Static sample character-sheet data. It is the character-sheet fallback
|
||||
/// used when no live session has produced a real <see cref="CharacterSheet"/>
|
||||
/// yet, and the fixture the character/attribute UI panel tests bind against.
|
||||
/// </summary>
|
||||
public static class SampleData
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a representative <see cref="CharacterSheet"/>. Values are
|
||||
/// plausible retail-scale numbers so the report renders with
|
||||
/// well-proportioned text in all sections.
|
||||
/// </summary>
|
||||
public static CharacterSheet SampleCharacter() => SampleCharacter(null);
|
||||
|
||||
public static CharacterSheet SampleCharacter(string? name) => new()
|
||||
{
|
||||
Name = string.IsNullOrWhiteSpace(name) ? "Studio Player" : name,
|
||||
Level = 126,
|
||||
Gender = "Female",
|
||||
Heritage = "Aluvian",
|
||||
Title = "the Adventurer",
|
||||
BirthDate = "January 5, 2001",
|
||||
PlayTime = "2 years, 114 days, 4 hours",
|
||||
Deaths = 42,
|
||||
|
||||
PkStatus = "Non-Player Killer",
|
||||
TotalXp = 1_250_000_000,
|
||||
XpToNextLevel = 42_000_000,
|
||||
XpFraction = 0.63f,
|
||||
|
||||
// Vitals: retail screenshot spec (Pass 1 acceptance criteria §Goal).
|
||||
HealthCurrent = 5, HealthMax = 5,
|
||||
StaminaCurrent = 10, StaminaMax = 10,
|
||||
ManaCurrent = 10, ManaMax = 10,
|
||||
|
||||
// Attributes: Strength + Quickness = 200; all others = 10 (retail screenshot spec §Goal).
|
||||
Strength = 200,
|
||||
Endurance = 10,
|
||||
Quickness = 200,
|
||||
Coordination = 10,
|
||||
Focus = 10,
|
||||
Self = 10,
|
||||
|
||||
UnspentSkillCredits = 12,
|
||||
SpecializedSkillCredits = 4,
|
||||
ChessRank = 12,
|
||||
FishingSkill = 4,
|
||||
|
||||
// Available skill credits (retail InqInt(0x18)); shown in Attributes tab footer State-A.
|
||||
SkillCredits = 96,
|
||||
|
||||
// Unassigned (banked) XP (retail InqInt64(2)); footer State-A line-2 value.
|
||||
UnassignedXp = 87_757_321_741L,
|
||||
|
||||
// Raise costs in retail display order (Strength, Endurance, Coordination, Quickness,
|
||||
// Focus, Self, Health, Stamina, Mana).
|
||||
// Str@200 = maxed → 0 (disabled). Quickness@200 = maxed → 0. Others @10 → affordable.
|
||||
// Focus@10 → 110 matches the authoritative retail screenshot (spec §4).
|
||||
// Formula bracket at value=10: ExperienceToAttributeLevel(11) − ExperienceToAttributeLevel(10).
|
||||
AttributeRaiseCosts = new long[] { 0L, 95L, 100L, 0L, 110L, 105L, 90L, 88L, 112L },
|
||||
AttributeRaise10Costs = new long[] { 0L, 950L, 1_000L, 0L, 1_100L, 1_050L, 900L, 880L, 1_120L },
|
||||
|
||||
// Real SkillTable icon IDs and train/specialize costs from client_portal.dat
|
||||
// SkillTable 0x0E000004. gmSkillUI groups/sorts these at bind time.
|
||||
Skills = new CharacterSkill[]
|
||||
{
|
||||
new( 6, "Melee Defense", 0x06000165u, CharacterSkillAdvancementClass.Specialized, 350, 354, false, 10, 20, 18_250_000L, 182_500_000L),
|
||||
new(34, "War Magic", 0x06001365u, CharacterSkillAdvancementClass.Specialized, 280, 285, false, 16, 28, 11_100_000L, 111_000_000L),
|
||||
|
||||
new(14, "Arcane Lore", 0x0600016Eu, CharacterSkillAdvancementClass.Trained, 260, 269, false, 4, 6, 7_500_000L, 75_000_000L),
|
||||
new(33, "Life Magic", 0x06001364u, CharacterSkillAdvancementClass.Trained, 250, 252, false, 12, 20, 6_800_000L, 68_000_000L),
|
||||
new(47, "Missile Weapons", 0x0600015Fu, CharacterSkillAdvancementClass.Trained, 220, 221, false, 6, 12, 5_250_000L, 52_500_000L),
|
||||
|
||||
new(21, "Healing", 0x06000133u, CharacterSkillAdvancementClass.Untrained, 10, 10, true, 6, 10, 0L),
|
||||
new(22, "Jump", 0x0600016Bu, CharacterSkillAdvancementClass.Untrained, 210, 210, true, 0, 4, 0L),
|
||||
new(36, "Loyalty", 0x06001367u, CharacterSkillAdvancementClass.Untrained, 10, 10, true, 0, 2, 0L),
|
||||
new(24, "Run", 0x06000173u, CharacterSkillAdvancementClass.Untrained, 390, 390, true, 0, 4, 0L),
|
||||
|
||||
new(38, "Alchemy", 0x060019E4u, CharacterSkillAdvancementClass.Untrained, 10, 10, false, 6, 12, 0L),
|
||||
new(39, "Cooking", 0x06001A54u, CharacterSkillAdvancementClass.Untrained, 10, 10, false, 4, 8, 0L),
|
||||
new(37, "Fletching", 0x06001A55u, CharacterSkillAdvancementClass.Untrained, 10, 10, false, 4, 8, 0L),
|
||||
},
|
||||
|
||||
CharacterInfoProperties = new Dictionary<uint, int>
|
||||
{
|
||||
[0x162u] = 2, // Swords melee mastery
|
||||
},
|
||||
|
||||
BurdenCurrent = 1200,
|
||||
BurdenMax = 4500,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- Pivoted from Hexa.NET.ImGui to ImGui.NET + Silk.NET.OpenGL.Extensions.ImGui
|
||||
on 2026-04-25 because Hexa's native OpenGL3 backend does its own GL
|
||||
function resolution via GLFW / SDL internally; with Silk.NET (no GLFW/SDL)
|
||||
it crashed in InitNative with 0xC0000005. The Silk.NET extension is
|
||||
purpose-built for this scenario — it wraps ImGui.NET's backend using
|
||||
Silk.NET's own GL binding. See docs/plans/2026-04-24-ui-framework.md
|
||||
"Risk: Hexa.NET.ImGui stops being maintained → mitigation: switch to
|
||||
ImGui.NET is a one-morning operation." This WAS the one-morning
|
||||
operation, triggered by a different Hexa issue (GL-loader absence). -->
|
||||
<PackageReference Include="ImGui.NET" Version="1.91.6.1" />
|
||||
<PackageReference Include="Silk.NET.OpenGL.Extensions.ImGui" Version="2.23.0" />
|
||||
<PackageReference Include="Silk.NET.Input" Version="2.23.0" />
|
||||
<PackageReference Include="Silk.NET.OpenGL" Version="2.23.0" />
|
||||
<PackageReference Include="Silk.NET.Windowing" Version="2.23.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AcDream.UI.Abstractions\AcDream.UI.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
using Silk.NET.Input;
|
||||
using Silk.NET.OpenGL;
|
||||
using Silk.NET.OpenGL.Extensions.ImGui;
|
||||
using Silk.NET.Windowing;
|
||||
|
||||
namespace AcDream.UI.ImGui;
|
||||
|
||||
/// <summary>
|
||||
/// Owns the <c>ImGuiController</c> from <c>Silk.NET.OpenGL.Extensions.ImGui</c>,
|
||||
/// which handles the whole Silk.NET ↔ ImGui.NET integration:
|
||||
/// <list type="bullet">
|
||||
/// <item>Creates the ImGui context + OpenGL3 backend using Silk.NET's GL binding
|
||||
/// (no GLFW / SDL dependency — unlike Hexa.NET.ImGui, which assumed one).</item>
|
||||
/// <item>Subscribes to Silk.NET's window + input events to drive IO.</item>
|
||||
/// <item>Per frame: <c>Update(dt)</c> calls <c>ImGui.NewFrame()</c>; <c>Render()</c>
|
||||
/// calls <c>ImGui.Render()</c> + uploads draw data via its OpenGL3 backend.</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para>
|
||||
/// Instance-scoped rather than static so GL-context lifetime is explicit.
|
||||
/// <c>GameWindow</c> owns the one instance and disposes on shutdown.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// History: tried <c>Hexa.NET.ImGui</c> + <c>Hexa.NET.ImGui.Backends.OpenGL3</c> first
|
||||
/// per the original plan, but its native OpenGL3 backend resolves GL functions
|
||||
/// via GLFW / SDL internally and crashed (0xC0000005) in <c>InitNative</c> without
|
||||
/// one of those present. Pivoted to the official Silk.NET extension on 2026-04-25.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public interface IImGuiBootstrapper : IDisposable
|
||||
{
|
||||
void BeginFrame(float deltaSeconds);
|
||||
void Render();
|
||||
void AbortFrame();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The upstream Silk controller does not expose a partially constructed
|
||||
/// instance. A constructor failure therefore requires teardown of the owning
|
||||
/// GL/window host instead of being treated as a recoverable optional-frontend
|
||||
/// failure.
|
||||
/// </summary>
|
||||
public sealed class ImGuiBootstrapperConstructionException : Exception
|
||||
{
|
||||
internal ImGuiBootstrapperConstructionException(Exception innerException)
|
||||
: base(
|
||||
"Silk ImGui construction did not publish a cleanup owner; " +
|
||||
"the graphics host must be torn down.",
|
||||
innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ImGuiBootstrapper : IImGuiBootstrapper
|
||||
{
|
||||
private readonly ImGuiController _controller;
|
||||
|
||||
public ImGuiBootstrapper(GL gl, IView window, IInputContext input)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gl);
|
||||
ArgumentNullException.ThrowIfNull(window);
|
||||
ArgumentNullException.ThrowIfNull(input);
|
||||
// ImGuiController constructor handles:
|
||||
// - ImGui.CreateContext()
|
||||
// - ImGuiOpenGL3 shader + vertex-buffer init (via Silk.NET GL)
|
||||
// - Keyboard + mouse event subscription (bound to Silk.NET IInputContext)
|
||||
// - Default style = dark
|
||||
nint priorContext = ImGuiNET.ImGui.GetCurrentContext();
|
||||
try
|
||||
{
|
||||
_controller = new ImGuiController(gl, window, input);
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
nint partialContext = ImGuiNET.ImGui.GetCurrentContext();
|
||||
if (partialContext != 0 && partialContext != priorContext)
|
||||
{
|
||||
ImGuiNET.ImGui.DestroyContext(partialContext);
|
||||
ImGuiNET.ImGui.SetCurrentContext(priorContext);
|
||||
}
|
||||
throw new ImGuiBootstrapperConstructionException(failure);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Begin an ImGui frame. Call BEFORE any <c>ImGui.*</c> widget calls.
|
||||
/// Internally: consumes buffered input events, calls <c>ImGui.NewFrame()</c>.
|
||||
/// </summary>
|
||||
public void BeginFrame(float deltaSeconds) => _controller.Update(deltaSeconds);
|
||||
|
||||
/// <summary>
|
||||
/// Finalise the ImGui frame and draw to the framebuffer. Call AFTER all
|
||||
/// panel draws, within the same frame as <see cref="BeginFrame"/>. The
|
||||
/// OpenGL3 backend save/restores the GL state it touches (shader, VAO,
|
||||
/// texture, blend, scissor); state not in its save-list (e.g.
|
||||
/// <c>GL_FRAMEBUFFER_SRGB</c>) is caller's responsibility.
|
||||
/// </summary>
|
||||
public void Render() => _controller.Render();
|
||||
|
||||
/// <summary>
|
||||
/// Close an unfinished frame through the owning Silk controller. Silk's
|
||||
/// controller clears its private frame-begun flag before uploading draw
|
||||
/// data, so this is both the correct abort path for pre-render failures and
|
||||
/// an idempotent no-op when a render upload failed after logical close.
|
||||
/// Calling raw <c>ImGui.EndFrame</c> here would leave the controller's state
|
||||
/// open and poison its next <see cref="BeginFrame"/> call.
|
||||
/// </summary>
|
||||
public void AbortFrame() => _controller.Render();
|
||||
|
||||
public void Dispose() => _controller.Dispose();
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
using AcDream.UI.Abstractions;
|
||||
|
||||
namespace AcDream.UI.ImGui;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IPanelHost"/> implementation for the ImGui backend. Owns the
|
||||
/// registered panel set; iterates + draws every frame when the caller is
|
||||
/// inside an ImGui frame (between <c>ImGui.NewFrame</c> and
|
||||
/// <c>ImGui.Render</c>).
|
||||
///
|
||||
/// <para>
|
||||
/// <b>This class does not call <c>ImGui.NewFrame</c> / <c>ImGui.Render</c>
|
||||
/// itself.</b> Those belong to the caller (GameWindow) so GL-state
|
||||
/// ownership is explicit and the render-loop integration point is obvious.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class ImGuiPanelHost : IPanelHost
|
||||
{
|
||||
private readonly Dictionary<string, IPanel> _panels = new();
|
||||
private readonly ImGuiPanelRenderer _renderer = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Register(IPanel panel)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(panel);
|
||||
_panels[panel.Id] = panel; // idempotent by Id
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Unregister(string panelId) => _panels.Remove(panelId);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RenderAll(PanelContext ctx)
|
||||
{
|
||||
// Order-independent — ImGui windows stack in the order they're drawn
|
||||
// for focus purposes but we have <=1 panel in D.2a.
|
||||
foreach (var panel in _panels.Values)
|
||||
{
|
||||
if (!panel.IsVisible) continue;
|
||||
panel.Render(ctx, _renderer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Current registered count (for diagnostics).</summary>
|
||||
public int Count => _panels.Count;
|
||||
}
|
||||
|
|
@ -1,256 +0,0 @@
|
|||
using System.Numerics;
|
||||
using AcDream.UI.Abstractions;
|
||||
using ImGuiNET;
|
||||
|
||||
namespace AcDream.UI.ImGui;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IPanelRenderer"/> implemented as thin wrappers around
|
||||
/// ImGui.NET calls. This is the ONLY place where ImGuiNET types appear
|
||||
/// outside of bootstrap plumbing — panels that need a feature must
|
||||
/// extend the abstraction here, not by importing ImGuiNET in panel
|
||||
/// files.
|
||||
/// </summary>
|
||||
public sealed class ImGuiPanelRenderer : IPanelRenderer
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool Begin(string title) => ImGuiNET.ImGui.Begin(title);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void End() => ImGuiNET.ImGui.End();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Text(string text) => ImGuiNET.ImGui.TextUnformatted(text);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SameLine() => ImGuiNET.ImGui.SameLine();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Separator() => ImGuiNET.ImGui.Separator();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ProgressBar(float fraction, float width, string? overlay = null)
|
||||
{
|
||||
// Clamp defensively; ImGui clamps internally but the abstraction
|
||||
// contract promises to handle out-of-range values.
|
||||
if (fraction < 0f) fraction = 0f;
|
||||
else if (fraction > 1f) fraction = 1f;
|
||||
|
||||
var size = new Vector2(width, 0f); // height 0 → ImGui picks based on font
|
||||
ImGuiNET.ImGui.ProgressBar(fraction, size, overlay ?? string.Empty);
|
||||
}
|
||||
|
||||
// -- Phase I.1 widget extensions ---------------------------------
|
||||
|
||||
/// <inheritdoc />
|
||||
public void TextColored(Vector4 rgba, string text)
|
||||
=> ImGuiNET.ImGui.TextColored(rgba, text);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CollapsingHeader(string label, bool defaultOpen = true)
|
||||
=> ImGuiNET.ImGui.CollapsingHeader(
|
||||
label,
|
||||
defaultOpen ? ImGuiTreeNodeFlags.DefaultOpen : ImGuiTreeNodeFlags.None);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TreeNode(string label) => ImGuiNET.ImGui.TreeNode(label);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void TreePop() => ImGuiNET.ImGui.TreePop();
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Checkbox(string label, ref bool value)
|
||||
=> ImGuiNET.ImGui.Checkbox(label, ref value);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Button(string label) => ImGuiNET.ImGui.Button(label);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Combo(string label, ref int selectedIndex, string[] items)
|
||||
=> ImGuiNET.ImGui.Combo(label, ref selectedIndex, items, items.Length);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool SliderFloat(string label, ref float value, float min, float max)
|
||||
=> ImGuiNET.ImGui.SliderFloat(label, ref value, min, max);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void PlotLines(
|
||||
string label,
|
||||
float[] values,
|
||||
int count,
|
||||
int offset = 0,
|
||||
string? overlay = null,
|
||||
float? min = null,
|
||||
float? max = null,
|
||||
Vector2? size = null)
|
||||
{
|
||||
// ImGui.NET 1.91.6.1's PlotLines binding takes `ref float values`
|
||||
// (pointer-to-first-element semantics) plus a separate values_count
|
||||
// and values_offset. The "no fixed bound" / "default size" sentinels
|
||||
// are float.MaxValue and Vector2.Zero respectively — we pass those
|
||||
// when the caller leaves the optional args null.
|
||||
if (count <= 0 || values.Length == 0)
|
||||
{
|
||||
// Nothing to plot — emit the label so layout doesn't shift but
|
||||
// skip the native call (ref to values[0] would NRE on empty).
|
||||
ImGuiNET.ImGui.TextUnformatted(label);
|
||||
return;
|
||||
}
|
||||
|
||||
float scaleMin = min ?? float.MaxValue;
|
||||
float scaleMax = max ?? float.MaxValue;
|
||||
Vector2 graphSize = size ?? Vector2.Zero;
|
||||
ImGuiNET.ImGui.PlotLines(
|
||||
label,
|
||||
ref values[0],
|
||||
count,
|
||||
offset,
|
||||
overlay ?? string.Empty,
|
||||
scaleMin,
|
||||
scaleMax,
|
||||
graphSize);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void BeginTable(string id, int columns)
|
||||
=> ImGuiNET.ImGui.BeginTable(id, columns);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void TableNextColumn() => ImGuiNET.ImGui.TableNextColumn();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void EndTable() => ImGuiNET.ImGui.EndTable();
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool InputTextSubmit(string label, ref string buffer, int maxLen, out string? submitted)
|
||||
{
|
||||
// EnterReturnsTrue: the call returns true on the frame the user
|
||||
// pressed Enter. On every other frame ImGui still mutates `buffer`
|
||||
// as the user types; we just don't surface a submit.
|
||||
bool entered = ImGuiNET.ImGui.InputText(
|
||||
label,
|
||||
ref buffer,
|
||||
(uint)maxLen,
|
||||
ImGuiInputTextFlags.EnterReturnsTrue);
|
||||
if (entered)
|
||||
{
|
||||
submitted = buffer;
|
||||
buffer = string.Empty; // contract: clear for next frame
|
||||
return true;
|
||||
}
|
||||
submitted = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Spacing() => ImGuiNET.ImGui.Spacing();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dummy(Vector2 size) => ImGuiNET.ImGui.Dummy(size);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void TextWrapped(string text) => ImGuiNET.ImGui.TextWrapped(text);
|
||||
|
||||
// -- Phase J Tier 3 — scrollable child for chat-style layouts ---------
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool BeginChild(string id, Vector2 size, bool border = false)
|
||||
{
|
||||
// ImGuiChildFlags has changed names across ImGui.NET versions
|
||||
// (Border vs Borders); 0x01 is the stable bit value for "draw
|
||||
// a border". Casting from a numeric literal sidesteps the
|
||||
// version-skew without requiring a hard reference to either
|
||||
// enum spelling.
|
||||
bool open = ImGuiNET.ImGui.BeginChild(id, size, (ImGuiChildFlags)(border ? 0x01 : 0));
|
||||
if (open)
|
||||
{
|
||||
// Title-bar-only drag fix (chat tail specifically): empty
|
||||
// clicks inside a scrollable child fall through to the
|
||||
// parent window for drag-init, which is exactly what the
|
||||
// user reported in the chat panel ("clicking anywhere
|
||||
// moves the window"). An InvisibleButton sized to the
|
||||
// child's content region absorbs those clicks so they
|
||||
// don't propagate. Real widgets drawn afterwards still
|
||||
// claim their own clicks (click priority = "last drawn,
|
||||
// first checked"). Wheel scrolling is window-level, not
|
||||
// item-level, so the absorber doesn't interfere with
|
||||
// the chat tail's auto-scroll.
|
||||
//
|
||||
// Scoped to BeginChild only (NOT Begin) because Begin's
|
||||
// body might host tab bars whose hit-testing competes with
|
||||
// an absorber on equal terms — adding it at Begin level
|
||||
// broke Settings tab clicks.
|
||||
var avail = ImGuiNET.ImGui.GetContentRegionAvail();
|
||||
if (avail.X > 0f && avail.Y > 0f)
|
||||
{
|
||||
var savedCursor = ImGuiNET.ImGui.GetCursorPos();
|
||||
ImGuiNET.ImGui.InvisibleButton("##childbodyabsorb", avail);
|
||||
ImGuiNET.ImGui.SetCursorPos(savedCursor);
|
||||
}
|
||||
}
|
||||
return open;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void EndChild() => ImGuiNET.ImGui.EndChild();
|
||||
|
||||
/// <inheritdoc />
|
||||
public float FrameHeightWithSpacing() => ImGuiNET.ImGui.GetFrameHeightWithSpacing();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetScrollHereY(float ratio) => ImGuiNET.ImGui.SetScrollHereY(ratio);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetKeyboardFocusHere() => ImGuiNET.ImGui.SetKeyboardFocusHere();
|
||||
|
||||
// -- Phase K.3 — main menu bar -----------------------------------------
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool BeginMainMenuBar() => ImGuiNET.ImGui.BeginMainMenuBar();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void EndMainMenuBar() => ImGuiNET.ImGui.EndMainMenuBar();
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool BeginMenu(string label) => ImGuiNET.ImGui.BeginMenu(label);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void EndMenu() => ImGuiNET.ImGui.EndMenu();
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool MenuItem(string label, string? shortcut = null)
|
||||
=> shortcut is null
|
||||
? ImGuiNET.ImGui.MenuItem(label)
|
||||
: ImGuiNET.ImGui.MenuItem(label, shortcut);
|
||||
|
||||
// -- Tab bar -----------------------------------------------------------
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool BeginTabBar(string id) => ImGuiNET.ImGui.BeginTabBar(id);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void EndTabBar() => ImGuiNET.ImGui.EndTabBar();
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool BeginTabItem(string label) => ImGuiNET.ImGui.BeginTabItem(label);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void EndTabItem() => ImGuiNET.ImGui.EndTabItem();
|
||||
|
||||
// -- Selectable / copyable text ---------------------------------------
|
||||
|
||||
/// <inheritdoc />
|
||||
public void TextMultilineReadOnly(string id, string content, Vector2 size)
|
||||
{
|
||||
// ImGui's InputTextMultiline takes a `ref string` even with the
|
||||
// ReadOnly flag — we just hand it a local copy. maxLength caps
|
||||
// what the user could type if ReadOnly were ever cleared; we
|
||||
// size it to the current content (+1 for ImGui's internal NUL
|
||||
// terminator in some bindings). Min of 1 keeps the empty case
|
||||
// from confusing native bindings.
|
||||
string buffer = content;
|
||||
uint maxLen = (uint)System.Math.Max(content.Length + 1, 1);
|
||||
ImGuiNET.ImGui.InputTextMultiline(id, ref buffer, maxLen, size,
|
||||
ImGuiInputTextFlags.ReadOnly);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue