From 9aaf97e7853db68a6e72c865803741795b3aa6a7 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 27 Jul 2026 18:27:52 +0200 Subject: [PATCH] Revert "Campaign V slice V4a" - it lost world multisampling This reverts ceec3bc4. Two independent reasons, either sufficient. The rendering regression. The slice deleted TextRenderGlStateScope, which saved GL_MULTISAMPLE and GL_SAMPLE_ALPHA_TO_COVERAGE on entry, disabled them for the text pass, and restored them on exit (TextRenderGlStateScope.cs:111-112 and 153-154 at the parent commit). Its replacement bakes that state into the text pipeline but nothing restores it, and GlGpuPassEncoder.Dispose does not either. Every world renderer is still raw GL at this point in the campaign, so from the first UI frame onward the world drew with multisampling disabled. The offline pixel gate caught it: 1,791 of 563,200 compared pixels differed, 0.318% against a 0.001 threshold. The commit message attributed this to wall-clock-driven ambient animation shifting phase, and committed through the failure. That explanation does not survive its own control: capturing twice at the reverted-to commit differs by 19 pixels and twice at the slice's own commit by 8, while base-versus-head differs by 1,791 - a 224x gap that no shared-noise source explains. An amplified difference image settles it visually: the changed pixels are the silhouette edges of every tree, building and rock, with terrain interiors, water and the entire UI untouched. That is the signature of losing edge antialiasing, not of animated sprites. This is the exact failure mode two existing memory notes already warn about - a mid-frame renderer must set every GL state it uses rather than inherit it, and issue #52's lesson that a rendering migration must audit per-pass GL state before declaring itself done. The scope. The brief was three small leaf renderers plus additive frame- lifecycle wiring, roughly ten files. The commit changed 334 files with 3,665 insertions and 3,845 deletions, including 323 public-to-internal visibility conversions across the App assembly, 55 test files, two retired conformance tests, and a self-described temporary escape hatch for bridging raw-GL viewport textures. Even without the regression, that is not separable into the part worth keeping and the part worth dropping. Reverting rather than patching because the good work here - the RHI frame lifecycle wiring and a genuine render-state-cache staleness fix - is small enough to redo cleanly against a tightened spec, while untangling it from 300+ files of unrelated churn is not. Post-revert: Release build clean, App suite back to 3,843 passed / 3 skipped, offline pixel gate passing at 19 differing pixels. Co-Authored-By: Claude Fable 5 --- src/AcDream.App/Audio/AudioHookSink.cs | 22 +- src/AcDream.App/Audio/OpenAlAudioEngine.cs | 46 +- .../Composition/FrameRootComposition.cs | 5 +- .../Composition/HostInputCameraComposition.cs | 16 +- .../InteractionRetainedUiComposition.cs | 11 +- .../LivePresentationComposition.cs | 7 +- .../Composition/WorldRenderComposition.cs | 37 +- src/AcDream.App/Diagnostics/FrameProfiler.cs | 38 +- .../Diagnostics/FrameStatsBuffer.cs | 12 +- src/AcDream.App/GlobalUsings.cs | 5 - .../Input/LocalPlayerProjectionController.cs | 4 +- src/AcDream.App/Input/PlayerModeAutoEntry.cs | 18 +- .../Input/RetailLocalPlayerFrameController.cs | 4 +- src/AcDream.App/Input/SilkKeyboardSource.cs | 4 +- src/AcDream.App/Input/SilkMouseSource.cs | 4 +- .../Physics/LocalPlayerShadowState.cs | 4 +- src/AcDream.App/Physics/RemoteTeleportHook.cs | 6 +- src/AcDream.App/Plugins/AppPluginHost.cs | 4 +- src/AcDream.App/Plugins/BufferedUiRegistry.cs | 6 +- src/AcDream.App/Plugins/SerilogAdapter.cs | 4 +- src/AcDream.App/Rendering/BitmapFont.cs | 97 +++-- src/AcDream.App/Rendering/CameraController.cs | 8 +- src/AcDream.App/Rendering/CellVisibility.cs | 60 +-- src/AcDream.App/Rendering/ChaseCamera.cs | 16 +- src/AcDream.App/Rendering/ClipFrame.cs | 34 +- .../Rendering/ClipFrameAssembler.cs | 10 +- src/AcDream.App/Rendering/ClipPlaneSet.cs | 74 ++-- .../Rendering/CompositeTextureArrayCache.cs | 4 +- .../CreatureAppraisalPresentation.cs | 7 +- .../Rendering/DebugLineRenderer.cs | 167 ++++--- src/AcDream.App/Rendering/DollCamera.cs | 26 +- .../Rendering/DollEntityBuilder.cs | 26 +- .../EquippedChildRenderController.cs | 6 +- .../ExternalViewportTextureBridge.cs | 73 ---- src/AcDream.App/Rendering/FlyCamera.cs | 4 +- src/AcDream.App/Rendering/FrustumCuller.cs | 12 +- src/AcDream.App/Rendering/GameWindow.cs | 80 ++-- .../Rendering/Gpu/Gl/GlGpuDevice.cs | 84 ---- .../Rendering/Gpu/Gl/GlGpuPassEncoder.cs | 3 +- .../Rendering/Gpu/Gl/GlRenderStateCache.cs | 13 +- src/AcDream.App/Rendering/ICamera.cs | 4 +- .../Rendering/ICameraCollisionProbe.cs | 8 +- src/AcDream.App/Rendering/IndoorDrawPlan.cs | 12 +- .../Rendering/InteriorEntityPartition.cs | 28 +- src/AcDream.App/Rendering/NdcScissorRect.cs | 18 +- src/AcDream.App/Rendering/OrbitCamera.cs | 2 +- src/AcDream.App/Rendering/OutdoorCellNode.cs | 16 +- .../Rendering/PaperdollFramePresenter.cs | 7 +- .../Rendering/PaperdollViewportRenderer.cs | 4 +- src/AcDream.App/Rendering/ParticleRenderer.cs | 14 +- .../Rendering/PhysicsCameraCollisionProbe.cs | 22 +- .../Rendering/PortalDepthMaskRenderer.cs | 46 +- src/AcDream.App/Rendering/PortalProjection.cs | 44 +- .../Rendering/PortalTunnelCamera.cs | 4 +- .../Rendering/PortalTunnelPresentation.cs | 4 +- src/AcDream.App/Rendering/PortalView.cs | 40 +- .../Rendering/PortalVisibilityBuilder.cs | 166 +++---- src/AcDream.App/Rendering/RenderBootstrap.cs | 57 +-- .../Rendering/RenderFrameOrchestrator.cs | 46 -- .../RenderFrameResourceController.cs | 13 +- .../Residency/ResidencyBudgetOptions.cs | 4 +- .../Rendering/RetailChaseCamera.cs | 100 ++--- .../Rendering/RetailCursorManager.cs | 4 +- .../Rendering/RetailPViewRenderer.cs | 176 ++++---- .../RetailParticleGeometryClassifier.cs | 4 +- src/AcDream.App/Rendering/SamplerCache.cs | 8 +- .../Rendering/SceneLightingUboBinding.cs | 6 +- .../Rendering/ScreenPolygonClip.cs | 4 +- src/AcDream.App/Rendering/Shader.cs | 18 +- .../Rendering/Shaders/debug_line.vert | 10 +- .../Rendering/Shaders/ui_text.frag | 25 +- .../Rendering/Shaders/ui_text.vert | 9 +- src/AcDream.App/Rendering/Sky/SkyRenderer.cs | 106 ++--- .../Rendering/TeleportViewPlaneController.cs | 4 +- src/AcDream.App/Rendering/TerrainAtlas.cs | 18 +- .../Rendering/TerrainModernRenderer.cs | 26 +- .../Rendering/TextRenderGlStateScope.cs | 156 +++++++ src/AcDream.App/Rendering/TextRenderer.cs | 407 ++++++++++++------ src/AcDream.App/Rendering/TextureCache.cs | 176 +++----- .../Rendering/Vfx/AnimationHookFrameQueue.cs | 4 +- .../Rendering/Vfx/EntityEffectController.cs | 4 +- .../Rendering/Vfx/EntityEffectPoseRegistry.cs | 6 +- .../Rendering/Vfx/EntityEffectProfile.cs | 4 +- .../Rendering/Vfx/EntityScriptActivator.cs | 6 +- .../Vfx/LiveEntityLightController.cs | 4 +- .../Vfx/ParticleVisibilityController.cs | 4 +- src/AcDream.App/Rendering/ViewconeCuller.cs | 26 +- .../Rendering/Wb/AnimatedEntityState.cs | 10 +- .../Rendering/Wb/BindlessSupport.cs | 8 +- .../Rendering/Wb/BufferUsageExtensions.cs | 4 +- src/AcDream.App/Rendering/Wb/Building.cs | 8 +- .../Rendering/Wb/BuildingLoader.cs | 12 +- .../Rendering/Wb/BuildingRegistry.cs | 14 +- .../Rendering/Wb/DebugRenderSettings.cs | 4 +- .../Wb/DrawElementsIndirectCommand.cs | 4 +- .../Rendering/Wb/EntitySpawnAdapter.cs | 12 +- .../Rendering/Wb/EnvCellLandblockBuild.cs | 8 +- .../Wb/EnvCellLandblockPublication.cs | 6 +- .../Wb/EnvCellMeshPreparationScheduler.cs | 4 +- .../Rendering/Wb/EnvCellRenderer.cs | 138 +++--- .../Rendering/Wb/EnvCellSceneryInstance.cs | 6 +- .../Rendering/Wb/EnvCellVisibilitySnapshot.cs | 8 +- src/AcDream.App/Rendering/Wb/GLHelpers.cs | 4 +- src/AcDream.App/Rendering/Wb/GLSLShader.cs | 4 +- src/AcDream.App/Rendering/Wb/GeometryUtils.cs | 4 +- .../Rendering/Wb/GlobalMeshBuffer.cs | 4 +- .../Rendering/Wb/GpuMemoryTracker.cs | 10 +- .../Rendering/Wb/IEntityTextureLifetime.cs | 4 +- .../Rendering/Wb/IWbMeshAdapter.cs | 6 +- src/AcDream.App/Rendering/Wb/InstanceData.cs | 4 +- .../Rendering/Wb/LandblockSpawnAdapter.cs | 8 +- .../Rendering/Wb/ManagedGLFrameBuffer.cs | 4 +- .../Rendering/Wb/ManagedGLIndexBuffer.cs | 4 +- .../Rendering/Wb/ManagedGLTexture.cs | 4 +- .../Rendering/Wb/ManagedGLTextureArray.cs | 8 +- .../Rendering/Wb/ManagedGLUniformBuffer.cs | 4 +- .../Rendering/Wb/ManagedGLVertexArray.cs | 4 +- .../Rendering/Wb/ManagedGLVertexBuffer.cs | 4 +- .../Rendering/Wb/ModernRenderData.cs | 16 +- .../Rendering/Wb/ObjectMeshManager.cs | 36 +- .../Rendering/Wb/OpenGLGraphicsDevice.cs | 4 +- .../Rendering/Wb/RenderStateCache.cs | 10 +- src/AcDream.App/Rendering/Wb/SceneData.cs | 4 +- .../Rendering/Wb/TextureAtlasManager.cs | 4 +- .../Rendering/Wb/TextureFormatExtensions.cs | 4 +- .../Rendering/Wb/TextureParameters.cs | 8 +- .../Wb/WbDrawDispatcher.PackedOracle.cs | 4 +- .../Rendering/Wb/WbDrawDispatcher.cs | 314 +++++++------- src/AcDream.App/Rendering/Wb/WbFrustum.cs | 8 +- src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs | 36 +- src/AcDream.App/Rendering/Wb/WbRenderPass.cs | 4 +- .../WorldSceneDiagnosticsController.cs | 5 +- src/AcDream.App/RuntimeOptions.cs | 10 +- src/AcDream.App/Spells/MagicRuntime.cs | 6 +- .../Streaming/DungeonStreamingGate.cs | 16 +- .../Streaming/GpuLandblockRetirement.cs | 6 +- src/AcDream.App/Streaming/GpuWorldState.cs | 44 +- src/AcDream.App/Streaming/LandblockBuild.cs | 4 +- .../Streaming/LandblockBuildFactory.cs | 62 +-- .../Streaming/LandblockBuildRequest.cs | 6 +- .../Streaming/LandblockPhysicsPublisher.cs | 10 +- .../LandblockPresentationPipeline.cs | 6 +- .../LandblockPresentationRetirementOwner.cs | 4 +- .../Streaming/LandblockRenderPublisher.cs | 8 +- .../LandblockRetirementCoordinator.cs | 8 +- .../LandblockStaticPresentationPublisher.cs | 8 +- .../Streaming/LandblockStreamJob.cs | 22 +- .../Streaming/LandblockStreamResultCost.cs | 6 +- .../Streaming/LandblockStreamTier.cs | 10 +- .../Streaming/LandblockStreamer.cs | 14 +- .../Streaming/StreamingCompletionQueue.cs | 4 +- .../Streaming/StreamingController.cs | 50 +-- .../Streaming/StreamingMutationException.cs | 4 +- src/AcDream.App/Streaming/StreamingRegion.cs | 34 +- .../Streaming/StreamingWorkBudget.cs | 16 +- .../Streaming/StreamingWorkBudgetOptions.cs | 4 +- src/AcDream.App/Streaming/TwoTierDiff.cs | 8 +- .../Streaming/WorldGenerationQuiescence.cs | 4 +- src/AcDream.App/Studio/DumpLayout.cs | 72 ++-- src/AcDream.App/Studio/FixtureProvider.cs | 24 +- src/AcDream.App/Studio/LayoutSource.cs | 18 +- src/AcDream.App/Studio/PanelFbo.cs | 12 +- src/AcDream.App/Studio/SampleData.cs | 38 +- src/AcDream.App/Studio/StudioInspector.cs | 42 +- src/AcDream.App/Studio/StudioOptions.cs | 10 +- src/AcDream.App/Studio/StudioWindow.cs | 54 +-- src/AcDream.App/Studio/UiDumpModel.cs | 32 +- src/AcDream.App/UI/ClientCommandController.cs | 6 +- src/AcDream.App/UI/ControlsIni.cs | 8 +- .../UI/CursorFeedbackController.cs | 24 +- .../UI/GameplayConfirmationController.cs | 4 +- src/AcDream.App/UI/IItemListDragHandler.cs | 12 +- .../UI/IRetainedPanelController.cs | 4 +- .../UI/IRetainedWindowStateController.cs | 6 +- src/AcDream.App/UI/IUiDatStateful.cs | 6 +- src/AcDream.App/UI/IUiGlobalTimeListener.cs | 4 +- src/AcDream.App/UI/IUiViewportRenderer.cs | 4 +- src/AcDream.App/UI/IconComposer.cs | 90 ++-- src/AcDream.App/UI/ItemDragPayload.cs | 10 +- .../UI/ItemInteractionController.cs | 12 +- .../UI/Layout/AppraisalUiController.cs | 24 +- .../UI/Layout/CharacterController.cs | 8 +- src/AcDream.App/UI/Layout/CharacterSheet.cs | 44 +- .../UI/Layout/CharacterSheetProvider.cs | 26 +- .../UI/Layout/CharacterStatController.cs | 154 +++---- .../UI/Layout/ChatWindowController.cs | 66 +-- .../UI/Layout/CombatUiController.cs | 6 +- .../UI/Layout/ComponentBookTemplateFactory.cs | 14 +- .../UI/Layout/CreatureAppraisalRows.cs | 22 +- .../UI/Layout/DatStringResolver.cs | 4 +- src/AcDream.App/UI/Layout/DatWidgetFactory.cs | 92 ++-- .../UI/Layout/EffectRowTemplateFactory.cs | 14 +- .../UI/Layout/EffectsUiController.cs | 14 +- src/AcDream.App/UI/Layout/ElementReader.cs | 24 +- .../UI/Layout/ExternalContainerController.cs | 20 +- .../UI/Layout/IndicatorBarController.cs | 6 +- .../UI/Layout/InventoryController.cs | 66 +-- .../UI/Layout/ItemAppraisalReport.cs | 8 +- .../UI/Layout/ItemAppraisalTextFormatter.cs | 4 +- .../UI/Layout/ItemCooldownAssets.cs | 4 +- .../UI/Layout/ItemCooldownUiController.cs | 4 +- .../UI/Layout/ItemListCellTemplate.cs | 14 +- .../UI/Layout/JumpPowerbarController.cs | 4 +- src/AcDream.App/UI/Layout/LayoutImporter.cs | 66 +-- .../UI/Layout/LinkStatusUiController.cs | 6 +- .../UI/Layout/MiniGameUiController.cs | 4 +- .../UI/Layout/PaperdollClickMap.cs | 4 +- .../UI/Layout/PaperdollController.cs | 44 +- .../UI/Layout/PaperdollSlotBackgrounds.cs | 4 +- src/AcDream.App/UI/Layout/RadarController.cs | 4 +- .../UI/Layout/RadarSnapshotProvider.cs | 4 +- .../UI/Layout/RetailAppraisalNameResolver.cs | 4 +- src/AcDream.App/UI/Layout/RetailDialogData.cs | 6 +- .../UI/Layout/RetailDialogFactory.cs | 4 +- .../UI/Layout/RetailFpsController.cs | 4 +- .../UI/Layout/RetailPanelUiController.cs | 4 +- .../UI/Layout/RetailWindowFrame.cs | 10 +- .../UI/Layout/SelectedObjectController.cs | 58 +-- .../SpellExamineComponentTemplateFactory.cs | 12 +- .../UI/Layout/SpellbookRowStyle.cs | 4 +- .../UI/Layout/SpellbookWindowController.cs | 18 +- .../UI/Layout/SpellcastingUiController.cs | 26 +- .../UI/Layout/ToolbarController.cs | 74 ++-- .../UI/Layout/ToolbarInputController.cs | 4 +- src/AcDream.App/UI/Layout/UiDatElement.cs | 28 +- src/AcDream.App/UI/Layout/UiPropertyBag.cs | 2 +- .../UI/Layout/UiTemplateListSlot.cs | 4 +- .../UI/Layout/VitaeUiController.cs | 6 +- src/AcDream.App/UI/Layout/VitalsController.cs | 14 +- .../Layout/VividTargetIndicatorController.cs | 12 +- src/AcDream.App/UI/MarkupDocument.cs | 12 +- src/AcDream.App/UI/RetailChromeSprites.cs | 46 +- .../UI/RetailItemConfirmationController.cs | 4 +- src/AcDream.App/UI/RetailPanelCatalog.cs | 4 +- ...tailSkillTrainingConfirmationController.cs | 4 +- src/AcDream.App/UI/RetailUiRuntime.cs | 68 +-- src/AcDream.App/UI/RetailWindowHandle.cs | 4 +- .../UI/RetailWindowLayoutPersistence.cs | 4 +- src/AcDream.App/UI/RetailWindowManager.cs | 4 +- .../UI/RetainedPanelControllerGroup.cs | 4 +- .../UI/Testing/RetailUiAutomationProbe.cs | 8 +- .../Testing/RetailUiAutomationScriptRunner.cs | 8 +- src/AcDream.App/UI/UiButton.cs | 30 +- src/AcDream.App/UI/UiButtonStateMachine.cs | 4 +- src/AcDream.App/UI/UiCatalogSlot.cs | 22 +- src/AcDream.App/UI/UiCollapsibleFrame.cs | 18 +- src/AcDream.App/UI/UiDatFont.cs | 34 +- src/AcDream.App/UI/UiDialogRoot.cs | 4 +- src/AcDream.App/UI/UiElement.cs | 52 +-- src/AcDream.App/UI/UiEvent.cs | 36 +- src/AcDream.App/UI/UiField.cs | 40 +- src/AcDream.App/UI/UiHost.cs | 30 +- src/AcDream.App/UI/UiItemList.cs | 14 +- src/AcDream.App/UI/UiItemSlot.cs | 122 +++--- src/AcDream.App/UI/UiLayoutPolicy.cs | 6 +- src/AcDream.App/UI/UiMenu.cs | 48 +-- src/AcDream.App/UI/UiMeter.cs | 50 +-- src/AcDream.App/UI/UiNineSlicePanel.cs | 22 +- src/AcDream.App/UI/UiPanel.cs | 30 +- src/AcDream.App/UI/UiRadar.cs | 8 +- src/AcDream.App/UI/UiRenderContext.cs | 30 +- src/AcDream.App/UI/UiRoot.cs | 46 +- src/AcDream.App/UI/UiScrollable.cs | 8 +- src/AcDream.App/UI/UiScrollablePanel.cs | 4 +- src/AcDream.App/UI/UiScrollbar.cs | 42 +- src/AcDream.App/UI/UiShortcutDigitGraphics.cs | 4 +- src/AcDream.App/UI/UiText.cs | 50 +-- src/AcDream.App/UI/UiTextureElement.cs | 8 +- src/AcDream.App/UI/UiViewport.cs | 10 +- src/AcDream.App/UI/WindowNames.cs | 4 +- src/AcDream.App/UI/WorldDropDispatch.cs | 4 +- .../ExternalContainerLifecycleController.cs | 4 +- .../World/ILiveEntityRadarSource.cs | 4 +- .../World/ILiveEntitySpatialQuery.cs | 4 +- .../LiveEntityPartArrayEnterWorldPort.cs | 4 +- .../World/LiveEntityPresentationController.cs | 4 +- ...iveEntityProjectionWithdrawalController.cs | 6 +- src/AcDream.App/World/LiveEntityRuntime.cs | 18 +- .../World/LiveEntityRuntimeViews.cs | 6 +- .../HostInputCameraCompositionTests.cs | 4 - .../InteractionRetainedUiCompositionTests.cs | 3 +- .../SettingsDevToolsCompositionTests.cs | 1 - .../WorldRenderCompositionTests.cs | 12 +- tests/AcDream.App.Tests/GlobalUsings.cs | 1 - .../Rendering/GlTextureOwnershipTests.cs | 13 +- .../Gpu/Gl/GlRenderStateCacheTests.cs | 3 +- .../RenderFrameResourceControllerTests.cs | 8 +- .../Rendering/ResourceCleanupGroupTests.cs | 41 +- .../TextRendererFailureSafetyTests.cs | 217 ++++++++++ .../Studio/DumpLayoutTests.cs | 3 +- .../Studio/LayoutSourceTests.cs | 3 +- .../UI/CursorFeedbackControllerTests.cs | 7 +- .../UI/DatWidgetFactoryZOrderTests.cs | 3 +- .../UI/DragDropSpineTests.cs | 49 ++- .../UI/ItemInteractionControllerTests.cs | 3 +- .../UI/Layout/AppraisalUiControllerTests.cs | 19 +- .../UI/Layout/CharacterLayoutImportProbe.cs | 7 +- .../UI/Layout/CharacterStatControllerTests.cs | 39 +- .../UI/Layout/ChatLayoutConformanceTests.cs | 3 +- .../UI/Layout/ChatWindowControllerTests.cs | 3 +- .../UI/Layout/CombatLayoutConformanceTests.cs | 3 +- .../UI/Layout/CombatUiControllerTests.cs | 3 +- .../UI/Layout/CreatureAppraisalRowsTests.cs | 3 +- .../DatWidgetFactoryFontResolveTests.cs | 3 +- .../UI/Layout/DatWidgetFactoryTests.cs | 5 +- .../UI/Layout/EffectsUiControllerTests.cs | 13 +- .../ExternalContainerControllerTests.cs | 19 +- .../UI/Layout/FixtureLoader.cs | 43 +- .../UI/Layout/IndicatorBarControllerTests.cs | 3 +- .../UI/Layout/InventoryControllerTests.cs | 43 +- .../UI/Layout/InventoryFrameImportProbe.cs | 9 +- .../Layout/ItemCooldownUiControllerTests.cs | 9 +- .../JumpPowerbarLayoutConformanceTests.cs | 3 +- .../UI/Layout/LayoutImporterTests.cs | 3 +- .../UI/Layout/PaperdollClickMapTests.cs | 3 +- .../UI/Layout/PaperdollControllerTests.cs | 3 +- .../UI/Layout/RadarControllerTests.cs | 3 +- .../UI/Layout/RetailPanelUiControllerTests.cs | 3 +- .../UI/Layout/RetailWindowFrameTests.cs | 3 +- .../Layout/SelectedObjectControllerTests.cs | 3 +- ...ellExamineComponentTemplateFactoryTests.cs | 7 +- .../Layout/SpellbookWindowControllerTests.cs | 17 +- .../Layout/SpellcastingUiControllerTests.cs | 11 +- .../UI/Layout/ToolbarControllerTests.cs | 103 +++-- .../UI/Layout/UiDatElementTests.cs | 17 +- .../UI/Layout/UiViewportFactoryTests.cs | 3 +- .../UI/MarkupDocumentTests.cs | 9 +- .../UI/RetailUiAutomationProbeTests.cs | 7 +- .../UI/RetailUiInteractionFlowTests.cs | 11 +- tests/AcDream.App.Tests/UI/UiButtonTests.cs | 3 +- .../UI/UiCollapsibleFrameTests.cs | 5 +- tests/AcDream.App.Tests/UI/UiDatFontTests.cs | 5 +- tests/AcDream.App.Tests/UI/UiItemSlotTests.cs | 21 +- .../AcDream.App.Tests/UI/UiRootInputTests.cs | 5 +- 334 files changed, 3841 insertions(+), 3661 deletions(-) delete mode 100644 src/AcDream.App/Rendering/ExternalViewportTextureBridge.cs create mode 100644 src/AcDream.App/Rendering/TextRenderGlStateScope.cs create mode 100644 tests/AcDream.App.Tests/Rendering/TextRendererFailureSafetyTests.cs diff --git a/src/AcDream.App/Audio/AudioHookSink.cs b/src/AcDream.App/Audio/AudioHookSink.cs index 9f91b8cc..a687fafc 100644 --- a/src/AcDream.App/Audio/AudioHookSink.cs +++ b/src/AcDream.App/Audio/AudioHookSink.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using AcDream.Core.Audio; @@ -19,33 +19,33 @@ namespace AcDream.App.Audio; /// Wiring: /// /// -/// → direct play of SoundHook.Id (a +/// → direct play of SoundHook.Id (a /// Wave dat id) at the entity's world position. Used for custom / /// per-animation audio like weapon swoosh or spell chant. /// /// -/// → look up the entity's SoundTable + +/// → look up the entity's SoundTable + /// the hook's SoundType, roll one /// via /// , play its wave. Retail's "footstep -/// that varies slightly" mechanism — also how attack / damage sounds +/// that varies slightly" mechanism — also how attack / damage sounds /// pick a creature-specific variant. /// /// -/// → same as SoundHook but with +/// → same as SoundHook but with /// pitch / volume overrides baked into the hook. /// /// /// /// /// -/// Entity → SoundTable id is resolved via an +/// Entity → SoundTable id is resolved via an /// callback passed in at construction; the renderer's per-entity state /// bag knows the PhysicsObj's SoundTableId (retail: /// PhysicsObj.soundtable_id). /// /// -internal sealed class AudioHookSink : IAnimationHookSink +public sealed class AudioHookSink : IAnimationHookSink { private readonly OpenAlAudioEngine _engine; private readonly DatSoundCache _cache; @@ -81,7 +81,7 @@ internal sealed class AudioHookSink : IAnimationHookSink case SoundTweakedHook stw: // SoundTweakedHook is a direct wave play with volume + // priority overrides baked into the hook itself (NOT a - // SoundTable lookup — that's SoundTableHook). Retail uses + // SoundTable lookup — that's SoundTableHook). Retail uses // this for the rare "explicit wave + explicit volume" case. Play(entityId, entityWorldPosition, waveId: (uint)stw.SoundId, @@ -90,7 +90,7 @@ internal sealed class AudioHookSink : IAnimationHookSink pitch: 1f); break; - // All the visual-only hooks (Scale, Luminous, Diffuse, …) + // All the visual-only hooks (Scale, Luminous, Diffuse, …) // are for other sinks to handle. } } @@ -138,7 +138,7 @@ internal sealed class AudioHookSink : IAnimationHookSink /// entity. Retail stores this on PhysicsObj.soundtable_id; our /// renderer keeps per-entity state that includes it. /// -internal interface IEntitySoundTable +public interface IEntitySoundTable { /// /// Return the SoundTable dat id (0x20xxxxxx) for , @@ -151,7 +151,7 @@ internal interface IEntitySoundTable /// Simple dictionary-backed ; the renderer /// assigns entries as it hydrates entities. /// -internal sealed class DictionaryEntitySoundTable : IEntitySoundTable +public sealed class DictionaryEntitySoundTable : IEntitySoundTable { private readonly Dictionary _table = new(); diff --git a/src/AcDream.App/Audio/OpenAlAudioEngine.cs b/src/AcDream.App/Audio/OpenAlAudioEngine.cs index e94409e2..959415ea 100644 --- a/src/AcDream.App/Audio/OpenAlAudioEngine.cs +++ b/src/AcDream.App/Audio/OpenAlAudioEngine.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using AcDream.Core.Audio; @@ -7,8 +7,8 @@ using Silk.NET.OpenAL; namespace AcDream.App.Audio; /// -/// OpenAL-backed audio engine (Phase E.2) — faithful to retail's -/// 16-voice pool and inverse-square falloff behaviour (r05 §5.3). +/// OpenAL-backed audio engine (Phase E.2) — faithful to retail's +/// 16-voice pool and inverse-square falloff behaviour (r05 §5.3). /// /// /// Architecture: @@ -16,7 +16,7 @@ namespace AcDream.App.Audio; /// /// Single + bound to the /// system default device. Cross-platform (WASAPI / WinMM / -/// PulseAudio / CoreAudio — whichever OpenAL-Soft picks). +/// PulseAudio / CoreAudio — whichever OpenAL-Soft picks). /// /// /// Fixed 16-source pool for 3D positional sounds. When all 16 are @@ -27,13 +27,13 @@ namespace AcDream.App.Audio; /// /// /// Separate UI source pool (4 sources) for flat 2D UI clicks / -/// wooshes — not subject to the 3D eviction game. +/// wooshes — not subject to the 3D eviction game. /// /// /// PCM buffer cache keyed by Wave dat id so the same footstep isn't /// re-uploaded to the GL-equivalent AL buffers on every hit. Bounded /// by a byte budget () enforced -/// with LRU eviction — see . A +/// with LRU eviction — see . A /// buffer still attached to a live source is never evicted (AL /// rejects deleting a bound buffer); eviction re-queries live AL /// source state rather than tracking a second copy of it. @@ -60,15 +60,15 @@ internal interface IWorldAudioQuiescence void ResumeWorldAudio(); } -internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescence +public sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescence { - // ── Backends ───────────────────────────────────────────────────────────── + // ── Backends ───────────────────────────────────────────────────────────── private AL? _al; private OpenAlResourceLifetime? _resources; private bool _available; private bool _disposed; - // ── Pools ──────────────────────────────────────────────────────────────── + // ── Pools ──────────────────────────────────────────────────────────────── private const int PoolSize3D = 16; // retail 16-slot voice pool private const int PoolSizeUi = 4; @@ -88,7 +88,7 @@ internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiesc private readonly uint[] _poolUi = new uint[PoolSizeUi]; - // ── Buffer cache (Wave dat id → AL buffer) ─────────────────────────────── + // ── Buffer cache (Wave dat id → AL buffer) ─────────────────────────────── // Budget rationale: decoded PCM waves run ~100-500 KB each (same sizing // as DatSoundCache's payload LRU, which this cache re-uploads from). 48 // MiB gives comfortable headroom for the working set of a play session @@ -99,11 +99,11 @@ internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiesc private readonly Dictionary _bufferByWaveId = new(); private readonly AlBufferBudgetTracker _bufferBudget = new(DefaultBufferByteBudget); - // ── Ambient handles (StartAmbient/StopAmbient) ─────────────────────────── + // ── Ambient handles (StartAmbient/StopAmbient) ─────────────────────────── private readonly Dictionary _ambientSources = new(); private int _nextAmbientHandle = 1; - // ── Public volume knobs ────────────────────────────────────────────────── + // ── Public volume knobs ────────────────────────────────────────────────── public float MasterVolume { get; set; } = 1f; public float SfxVolume { get; set; } = 1f; public float MusicVolume { get; set; } = 0.7f; @@ -219,7 +219,7 @@ internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiesc _al = null; } - // ── IAudioEngine ───────────────────────────────────────────────────────── + // ── IAudioEngine ───────────────────────────────────────────────────────── public void SetListener( float posX, float posY, float posZ, @@ -241,7 +241,7 @@ internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiesc } /// - /// Not exposed on IAudioEngine but used by the hook sink — play a raw + /// Not exposed on IAudioEngine but used by the hook sink — play a raw /// WaveData blob at a 3D position with full priority/volume controls. /// Returns true on success, false if the buffer was rejected. /// @@ -278,7 +278,7 @@ internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiesc if (_pool3D[idx].PlayingGain < effectiveGain) { slotIdx = idx; break; } } } - if (slotIdx < 0) return false; // no slot quieter than us — drop + if (slotIdx < 0) return false; // no slot quieter than us — drop var slot = _pool3D[slotIdx]; _al.SourceStop(slot.SourceId); @@ -355,7 +355,7 @@ internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiesc return true; } - // IAudioEngine implementations — the enum-based overloads are less + // IAudioEngine implementations — the enum-based overloads are less // useful than the raw-Wave overloads above, since the hook sink already // has access to decoded WaveData. Left as no-ops for now; R5 defines // SoundId as a sparse subset of retail enums. @@ -366,7 +366,7 @@ internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiesc public int StartAmbient(SoundId id, float x, float y, float z) { - // Looping ambient — needs a decoded wave + WaveId. The hook sink + // Looping ambient — needs a decoded wave + WaveId. The hook sink // doesn't route ambient; a separate landblock-attached ambient // system (outside R5) will drive this. For now: reserve a handle. int handle = _nextAmbientHandle++; @@ -383,17 +383,17 @@ internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiesc } } - public void PlayMusic(string resourceName, bool loop) { /* R5 §6 MIDI — not ported */ } + public void PlayMusic(string resourceName, bool loop) { /* R5 §6 MIDI — not ported */ } public void StopMusic() { /* ditto */ } - // ── Private helpers ────────────────────────────────────────────────────── + // ── Private helpers ────────────────────────────────────────────────────── private uint EnsureBuffer(uint waveId, WaveData wave) { if (!_available || _al is null) return 0; if (_bufferByWaveId.TryGetValue(waveId, out var existing)) { - // Buffer id 0 is the "unsupported format" negative marker — no + // Buffer id 0 is the "unsupported format" negative marker — no // payload, not tracked by the budget, nothing to touch. if (existing != 0) _bufferBudget.Touch(waveId); @@ -425,15 +425,15 @@ internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiesc /// /// Evict least-recently-used AL buffers until the resident-byte budget /// is satisfied again. A buffer still bound to a live source (3D pool, - /// UI pool, or an ambient source) is protected — alDeleteBuffers - /// fails on a buffer that's still attached to a source — so eviction + /// UI pool, or an ambient source) is protected — alDeleteBuffers + /// fails on a buffer that's still attached to a source — so eviction /// never targets one; nor does it target , /// the buffer just created for this call and /// hasn't attached to a source yet. If every resident buffer is /// protected the budget is temporarily exceeded rather than looping /// forever; the bounded pool sizes (16 + 4 + ambient) cap how large /// that overage can get. Evicted waves replay through - /// again on next use — re-upload from + /// again on next use — re-upload from /// , identical to a first play. /// private void EvictBuffersOverBudget(uint protectedBufferId) diff --git a/src/AcDream.App/Composition/FrameRootComposition.cs b/src/AcDream.App/Composition/FrameRootComposition.cs index 4caa35fa..0319914f 100644 --- a/src/AcDream.App/Composition/FrameRootComposition.cs +++ b/src/AcDream.App/Composition/FrameRootComposition.cs @@ -263,6 +263,8 @@ internal sealed class FrameRootCompositionPhase live.DrawDispatcher, live.EnvCellRenderer, live.PortalDepthMask, + foundation.TextRenderer, + interaction.RetainedUi?.Host.TextRenderer, live.ClipFrame, foundation.Terrain, foundation.SceneLighting), @@ -361,7 +363,6 @@ internal sealed class FrameRootCompositionPhase d.CellVisibility), d.WorldSceneDebugState, foundation.DebugLines, - host.GpuFrameLifetime, d.PhysicsEngine, d.PlayerMode, d.PlayerController, @@ -508,7 +509,7 @@ internal sealed class FrameRootCompositionPhase : (IRenderFramePostDiagnosticsPhase?)lifecycleAutomation ?? NullRenderFramePostDiagnosticsPhase.Instance; var renderFrame = new RenderFrameOrchestrator( - host.GpuFrameLifetime, + host.GpuFrameFlights, new FrameProfilerGpuMeasurement(d.FrameProfiler, d.Gl), framePreparation, worldSceneRenderer, diff --git a/src/AcDream.App/Composition/HostInputCameraComposition.cs b/src/AcDream.App/Composition/HostInputCameraComposition.cs index b3f0ca91..52e0592b 100644 --- a/src/AcDream.App/Composition/HostInputCameraComposition.cs +++ b/src/AcDream.App/Composition/HostInputCameraComposition.cs @@ -12,7 +12,6 @@ internal interface IGameWindowHostInputCameraPublication { void PublishGpuFrameFlights(GpuFrameFlightController value); void PublishGpuDevice(IGpuDevice value); - void PublishGpuFrameLifetime(GpuDeviceFrameLifetime value); void PublishKeyboardSource(SilkKeyboardSource value); void PublishMouseSource(SilkMouseSource value); void PublishMouseLookCursor(IMouseLookCursor value); @@ -24,7 +23,6 @@ internal interface IGameWindowHostInputCameraPublication internal sealed record HostInputCameraResult( GpuFrameFlightController GpuFrameFlights, IGpuDevice GpuDevice, - GpuDeviceFrameLifetime GpuFrameLifetime, WorldRenderDiagnostics WorldRenderDiagnostics, SilkKeyboardSource? KeyboardSource, SilkMouseSource? MouseSource, @@ -237,10 +235,9 @@ internal sealed class HostInputCameraCompositionPhase : // exist — it owns its own BindlessSupport detection (see // GlGpuDevice's class comment), so unlike the legacy WB render path it // has no dependency on WorldRenderCompositionPhase running first. - // Campaign V slice V4a is the first real consumer (TextRenderer, - // BitmapFont, DebugLineRenderer, TextureCache's UI upload path); it is + // Nothing consumes this device yet (Campaign V slice V1); it is // proven against the real driver here and torn down with the render - // stack. + // stack so later slices (starting at V4a) have somewhere to plug in. IGpuDevice gpuDevice = scope.Acquire( "GPU device (RHI)", () => _factory.CreateGpuDevice(gl, gpuFrames), @@ -248,14 +245,6 @@ internal sealed class HostInputCameraCompositionPhase : _publication.PublishGpuDevice); Fault(HostInputCameraCompositionPoint.GpuDevicePublished); - // V4a's structural addition: the shared IGpuFrame lifecycle that - // ported renderers allocate rings and open passes against. Owns no - // disposable resource of its own — it wraps gpuDevice, whose scope - // above already tears it down — so it is published as a plain value, - // not a second acquisition. - var gpuFrameLifetime = new GpuDeviceFrameLifetime(gpuDevice); - _publication.PublishGpuFrameLifetime(gpuFrameLifetime); - WorldRenderDiagnostics diagnostics = _factory.CreateWorldRenderDiagnostics( gl, @@ -356,7 +345,6 @@ internal sealed class HostInputCameraCompositionPhase : return new HostInputCameraResult( gpuFrames, gpuDevice, - gpuFrameLifetime, diagnostics, keyboard, mouse, diff --git a/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs b/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs index e2b9e7e8..7f189414 100644 --- a/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs +++ b/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs @@ -33,10 +33,9 @@ namespace AcDream.App.Composition; internal sealed record InteractionRetainedUiDependencies( RuntimeOptions Options, GL Gl, - IGpuDevice GpuDevice, - Func CurrentGpuFrame, IView Window, IInputContext Input, + string ShadersDirectory, IDatReaderWriter Dats, object DatLock, TextureCache TextureCache, @@ -390,8 +389,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory d.Character.LocalPlayer); UiHost host = lease.AcquireHost( () => new UiHost( - d.GpuDevice, - d.CurrentGpuFrame, + d.Gl, + d.ShadersDirectory, d.DebugFont, d.HostQuiescence)); checkpoint(InteractionRetainedUiCompositionPoint.UiHostAcquired); @@ -478,9 +477,9 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory host.WireKeyboard(keyboard); checkpoint(InteractionRetainedUiCompositionPoint.KeyboardInputWired); - (GpuTextureSlot, int, int) ResolveChrome(uint id) + (uint, int, int) ResolveChrome(uint id) { - GpuTextureSlot texture = d.TextureCache.GetOrUploadRenderSurface( + uint texture = d.TextureCache.GetOrUploadRenderSurface( id, out int width, out int height); diff --git a/src/AcDream.App/Composition/LivePresentationComposition.cs b/src/AcDream.App/Composition/LivePresentationComposition.cs index ead0ec98..0e3cfe73 100644 --- a/src/AcDream.App/Composition/LivePresentationComposition.cs +++ b/src/AcDream.App/Composition/LivePresentationComposition.cs @@ -38,7 +38,6 @@ namespace AcDream.App.Composition; internal sealed record LivePresentationDependencies( RuntimeOptions Options, GL Gl, - IGpuDevice GpuDevice, IWindow Window, object DatLock, RuntimeSettingsController Settings, @@ -796,8 +795,7 @@ internal sealed class LivePresentationCompositionPhase paperdollLease.Resource, new RetailPaperdollFrameView( viewport, - new PaperdollInventoryVisibility(inventoryFrame), - d.GpuDevice), + new PaperdollInventoryVisibility(inventoryFrame)), new RetailPaperdollDollFactory( new LivePaperdollEntityLookup(liveEntities), d.PlayerIdentity, @@ -844,8 +842,7 @@ internal sealed class LivePresentationCompositionPhase new RetailCreatureAppraisalFrameView( creatureViewport, examinationFrame, - appraisalController, - d.GpuDevice), + appraisalController), new RetailCreatureAppraisalCloneFactory( new LiveCreatureAppraisalEntityLookup(liveEntities))); } diff --git a/src/AcDream.App/Composition/WorldRenderComposition.cs b/src/AcDream.App/Composition/WorldRenderComposition.cs index 4decbc81..bdbc10f6 100644 --- a/src/AcDream.App/Composition/WorldRenderComposition.cs +++ b/src/AcDream.App/Composition/WorldRenderComposition.cs @@ -52,7 +52,6 @@ internal sealed record WorldRenderDependencies( WorldEnvironmentController Environment, IGameRenderResourceLifetime RenderResources, IGpuResourceRetirementQueue ResourceRetirement, - IGpuDevice GpuDevice, ResidencyBudgetOptions ResidencyBudgets, uint InitialCenterLandblockId, string DiagnosticsDirectory, @@ -90,10 +89,10 @@ internal interface IWorldRenderCompositionFactory void SetTerrainAnisotropic(TerrainAtlas atlas, int level); Shader CreateTerrainShader(GL gl, string shadersDirectory); SceneLightingUboBinding CreateSceneLighting(GL gl); - DebugLineRenderer CreateDebugLines(IGpuDevice device); + DebugLineRenderer CreateDebugLines(GL gl, string shadersDirectory); byte[]? TryLoadDebugFont(); - BitmapFont CreateDebugFont(IGpuDevice device, byte[] bytes); - TextRenderer CreateTextRenderer(IGpuDevice device); + BitmapFont CreateDebugFont(GL gl, byte[] bytes); + TextRenderer CreateTextRenderer(GL gl, string shadersDirectory); TerrainModernRenderer CreateTerrain( GL gl, BindlessSupport bindless, @@ -113,7 +112,6 @@ internal interface IWorldRenderCompositionFactory ResidencyBudgetOptions budgets); TextureCache CreateTextureCache( GL gl, - IGpuDevice device, IDatReaderWriter dats, BindlessSupport bindless, IGpuResourceRetirementQueue retirement, @@ -216,15 +214,21 @@ internal sealed class RetailWorldRenderCompositionFactory public SceneLightingUboBinding CreateSceneLighting(GL gl) => new(gl); - public DebugLineRenderer CreateDebugLines(IGpuDevice device) => new(device); + public DebugLineRenderer CreateDebugLines( + GL gl, + string shadersDirectory) => + new(gl, shadersDirectory); public byte[]? TryLoadDebugFont() => BitmapFont.TryLoadSystemMonospaceFont(); - public BitmapFont CreateDebugFont(IGpuDevice device, byte[] bytes) => - new(device, bytes, pixelHeight: 15f, atlasSize: 512); + public BitmapFont CreateDebugFont(GL gl, byte[] bytes) => + new(gl, bytes, pixelHeight: 15f, atlasSize: 512); - public TextRenderer CreateTextRenderer(IGpuDevice device) => new(device); + public TextRenderer CreateTextRenderer( + GL gl, + string shadersDirectory) => + new(gl, shadersDirectory); public TerrainModernRenderer CreateTerrain( GL gl, @@ -290,7 +294,6 @@ internal sealed class RetailWorldRenderCompositionFactory public TextureCache CreateTextureCache( GL gl, - IGpuDevice device, IDatReaderWriter dats, BindlessSupport bindless, IGpuResourceRetirementQueue retirement, @@ -298,7 +301,6 @@ internal sealed class RetailWorldRenderCompositionFactory ResidencyBudgetOptions budgets) => new( gl, - device, dats, bindless, retirement, @@ -480,12 +482,12 @@ internal sealed class WorldRenderCompositionPhase DebugLineRenderer debugLines = AcquireAndPublish( scope, "debug lines", - () => _factory.CreateDebugLines(_dependencies.GpuDevice), + () => _factory.CreateDebugLines(gl, shadersDirectory), _publication.PublishDebugLines, WorldRenderCompositionPoint.DebugLinesPublished); (BitmapFont? debugFont, TextRenderer? textRenderer) = - ComposeOptionalHudResources(scope); + ComposeOptionalHudResources(scope, gl, shadersDirectory); TerrainModernRenderer terrain = AcquireAndPublish( scope, @@ -533,7 +535,6 @@ internal sealed class WorldRenderCompositionPhase "texture cache", () => _factory.CreateTextureCache( gl, - _dependencies.GpuDevice, content.Dats, bindless, _dependencies.ResourceRetirement, @@ -585,7 +586,9 @@ internal sealed class WorldRenderCompositionPhase } private (BitmapFont? Font, TextRenderer? Text) ComposeOptionalHudResources( - CompositionAcquisitionScope scope) + CompositionAcquisitionScope scope, + GL gl, + string shadersDirectory) { byte[]? fontBytes = _factory.TryLoadDebugFont(); if (fontBytes is null) @@ -597,13 +600,13 @@ internal sealed class WorldRenderCompositionPhase var fontLease = scope.Acquire( "world HUD font", - () => _factory.CreateDebugFont(_dependencies.GpuDevice, fontBytes), + () => _factory.CreateDebugFont(gl, fontBytes), _factory.Release); BitmapFont font = fontLease.Resource; Fault(WorldRenderCompositionPoint.DebugFontCreated); var textLease = scope.Acquire( "world HUD text renderer", - () => _factory.CreateTextRenderer(_dependencies.GpuDevice), + () => _factory.CreateTextRenderer(gl, shadersDirectory), _factory.Release); TextRenderer text = textLease.Resource; Fault(WorldRenderCompositionPoint.TextRendererCreated); diff --git a/src/AcDream.App/Diagnostics/FrameProfiler.cs b/src/AcDream.App/Diagnostics/FrameProfiler.cs index f49c1810..69d02add 100644 --- a/src/AcDream.App/Diagnostics/FrameProfiler.cs +++ b/src/AcDream.App/Diagnostics/FrameProfiler.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; @@ -10,11 +10,11 @@ using Silk.NET.OpenGL; namespace AcDream.App.Diagnostics; /// Stage indices for per-frame CPU attribution. -internal enum FrameStage +public enum FrameStage { /// Whole OnUpdate body (simulation + streaming apply). Update = 0, - /// WbMeshAdapter.Tick — staged mesh/texture GPU upload drain. + /// WbMeshAdapter.Tick — staged mesh/texture GPU upload drain. Upload = 1, /// ImGui Render (dev overlay). ImGui = 2, @@ -23,11 +23,11 @@ internal enum FrameStage } /// -/// One ACDREAM_FRAME_HISTORY CSV row — every field the aggregated +/// One ACDREAM_FRAME_HISTORY CSV row — every field the aggregated /// [frame-prof] report discards when its 5-second window resets. /// Stage fields mirror positionally (Update / /// Upload / ImGui / Pacing, matching 's -/// names array) — if grows, extend this +/// names array) — if grows, extend this /// record, , and the CSV header /// together. GpuUs is -1 for a frame with no available GPU /// sample (warm-up, or ACDREAM_WB_DIAG=1 self-disable). @@ -44,7 +44,7 @@ internal readonly record struct FrameHistoryRecord( long PacingUs); /// -/// MP0 (2026-07-05) — the permanent honest frame profiler. One +/// MP0 (2026-07-05) — the permanent honest frame profiler. One /// FrameBoundary call at the top of the accepted render transaction /// measures CPU frame time as the delta between consecutive boundaries /// (captures the FULL frame including present) and samples per-frame allocated @@ -56,29 +56,29 @@ internal readonly record struct FrameHistoryRecord( /// is true; costs one /// bool check per frame when off. /// -/// Permanent apparatus — every MP-track gate reads it; do not strip. +/// Permanent apparatus — every MP-track gate reads it; do not strip. /// Whole-frame GPU timing self-disables under ACDREAM_WB_DIAG=1 /// (nested TimeElapsed is illegal GL; see GpuFrameTimer). /// -/// 2026-07-24 measurement-tooling review — the aggregated report +/// 2026-07-24 measurement-tooling review — the aggregated report /// resets its ring buffers every ~5 s (), /// so route-wide p50/p95/p99 distributions across a whole soak cannot be /// reconstructed after the fact. /// (ACDREAM_FRAME_HISTORY=<path>) opts into a SEPARATE /// per-frame history: one per frame in a /// preallocated, grow-as-needed (1 int + 1 double + -/// 7 longs ≈ 72 bytes/record; a multi-hour capture at 165 fps is roughly -/// 165 * 3600 * 72 bytes ≈ 43 MB/hour — fine for a bounded diagnostic run, +/// 7 longs ≈ 72 bytes/record; a multi-hour capture at 165 fps is roughly +/// 165 * 3600 * 72 bytes ≈ 43 MB/hour — fine for a bounded diagnostic run, /// not for unattended day-long capture). Its initial capacity is about 9 MiB /// and covers the canonical route above 300 FPS without a frame-thread resize. /// ZERO frame-thread I/O: the CSV is written once, from /// , at shutdown. Recording only takes effect while -/// is ALSO true — it +/// is ALSO true — it /// reuses that instrumentation rather than duplicating it. Does not /// change the [frame-prof] report format or any existing metric. -/// Spec: docs/superpowers/specs/2026-07-05-modern-pipeline-design.md §5. +/// Spec: docs/superpowers/specs/2026-07-05-modern-pipeline-design.md §5. /// -internal sealed class FrameProfiler : IDisposable +public sealed class FrameProfiler : IDisposable { private const int WindowCapacity = 2048; // ~12 s at 165 fps private const int HistoryInitialCapacity = 131072; @@ -136,7 +136,7 @@ internal sealed class FrameProfiler : IDisposable if (_wasEnabled) { // Dispose (not just Stop) so a later re-enable rebuilds the - // query ring fresh — a kept instance would poll slots left + // query ring fresh — a kept instance would poll slots left // pending from BEFORE the pause and report temporally stale // GPU samples. Safe here: this runs at the top of OnRender // with the GL context current. @@ -163,7 +163,7 @@ internal sealed class FrameProfiler : IDisposable { // First enabled frame (startup or runtime toggle-on): establish // baselines, emit nothing. Clear any stage ticks a StageScope - // disposed after toggle-off may have accumulated mid-pause — + // disposed after toggle-off may have accumulated mid-pause — // EndStage still runs on scopes that were live when the flag // flipped, and that partial delta must not leak into the first // re-enabled frame. @@ -274,7 +274,7 @@ internal sealed class FrameProfiler : IDisposable internal void EndStage(FrameStage stage, long startTimestamp) => _stageAccumTicks[(int)stage] += Stopwatch.GetTimestamp() - startTimestamp; - /// Pure report formatter — unit-tested; invariant culture. + /// Pure report formatter — unit-tested; invariant culture. public static string FormatReport( int frameCount, FrameStatsBuffer cpu, FrameStatsBuffer gpu, bool gpuActive, @@ -301,7 +301,7 @@ internal sealed class FrameProfiler : IDisposable return sb.ToString(); } - /// Pure CSV formatter — unit-tested; invariant culture. One header row plus one row per record. + /// Pure CSV formatter — unit-tested; invariant culture. One header row plus one row per record. internal static void WriteHistoryCsv( IEnumerable records, TextWriter writer, @@ -330,7 +330,7 @@ internal sealed class FrameProfiler : IDisposable /// /// Shutdown-only write of the accumulated as CSV - /// (the ONLY I/O this feature performs — never from ). + /// (the ONLY I/O this feature performs — never from ). /// Failures are logged, not thrown: a history-export problem must never /// block the rest of the render-owner shutdown chain /// (GameWindowLifetime's Hard("frame profiler", ...) stage). @@ -355,7 +355,7 @@ internal sealed class FrameProfiler : IDisposable } /// Disposable stage scope; default instance is a no-op. -internal readonly struct StageScope : IDisposable +public readonly struct StageScope : IDisposable { private readonly FrameProfiler? _owner; private readonly FrameStage _stage; diff --git a/src/AcDream.App/Diagnostics/FrameStatsBuffer.cs b/src/AcDream.App/Diagnostics/FrameStatsBuffer.cs index d94936ce..60773235 100644 --- a/src/AcDream.App/Diagnostics/FrameStatsBuffer.cs +++ b/src/AcDream.App/Diagnostics/FrameStatsBuffer.cs @@ -1,16 +1,16 @@ -using System; +using System; namespace AcDream.App.Diagnostics; /// -/// MP0 (2026-07-05) — fixed-capacity ring buffer of long samples +/// MP0 (2026-07-05) — fixed-capacity ring buffer of long samples /// (microseconds or bytes) with percentile/max over the current window. /// Pure and allocation-free after construction: /// sorts into a preallocated scratch array, so the 5-second report path -/// allocates nothing. Not thread-safe — owned by the window loop thread. -/// Spec: docs/superpowers/specs/2026-07-05-modern-pipeline-design.md §5. +/// allocates nothing. Not thread-safe — owned by the window loop thread. +/// Spec: docs/superpowers/specs/2026-07-05-modern-pipeline-design.md §5. /// -internal sealed class FrameStatsBuffer +public sealed class FrameStatsBuffer { private readonly long[] _samples; private readonly long[] _scratch; @@ -41,7 +41,7 @@ internal sealed class FrameStatsBuffer /// /// Nearest-rank percentile over the current window: element at - /// ceil(q·n) in the ascending sort (1-based), 0 when empty. + /// ceil(q·n) in the ascending sort (1-based), 0 when empty. /// public long Percentile(double q) { diff --git a/src/AcDream.App/GlobalUsings.cs b/src/AcDream.App/GlobalUsings.cs index 7e8a6845..5213b6cc 100644 --- a/src/AcDream.App/GlobalUsings.cs +++ b/src/AcDream.App/GlobalUsings.cs @@ -2,8 +2,3 @@ global using AcDream.Runtime.Gameplay; global using AcDream.Runtime.Physics; global using ILocalPlayerMotionSource = AcDream.Runtime.Gameplay.IRuntimeLocalPlayerMotionSource; -// Campaign V slice V4a: GpuTextureSlot (and the rest of the RHI contract) is -// used pervasively once TextRenderer/TextureCache's UI path stop dealing in -// raw GL texture names, so it is imported project-wide rather than adding a -// `using AcDream.App.Rendering.Gpu;` to every UI file that resolves a sprite. -global using AcDream.App.Rendering.Gpu; diff --git a/src/AcDream.App/Input/LocalPlayerProjectionController.cs b/src/AcDream.App/Input/LocalPlayerProjectionController.cs index 0e6cf13f..8a32c4d7 100644 --- a/src/AcDream.App/Input/LocalPlayerProjectionController.cs +++ b/src/AcDream.App/Input/LocalPlayerProjectionController.cs @@ -1,4 +1,4 @@ -using AcDream.Core.World; +using AcDream.Core.World; using AcDream.App.Physics; using AcDream.App.World; @@ -59,7 +59,7 @@ internal sealed class LiveLocalPlayerProjectionRuntime /// Projects the canonical local physics body into world rendering and spatial /// buckets without advancing it. /// -internal sealed class LocalPlayerProjectionController +public sealed class LocalPlayerProjectionController { private readonly ILocalPlayerProjectionRuntime _runtime; diff --git a/src/AcDream.App/Input/PlayerModeAutoEntry.cs b/src/AcDream.App/Input/PlayerModeAutoEntry.cs index 2c629a11..cc158601 100644 --- a/src/AcDream.App/Input/PlayerModeAutoEntry.cs +++ b/src/AcDream.App/Input/PlayerModeAutoEntry.cs @@ -1,4 +1,4 @@ -using System; +using System; using AcDream.App.Net; using AcDream.App.Streaming; using AcDream.App.World; @@ -6,7 +6,7 @@ using AcDream.App.World; namespace AcDream.App.Input; /// -/// Phase K.2 — one-shot guard that auto-enters player mode after a +/// Phase K.2 — one-shot guard that auto-enters player mode after a /// successful login once every prerequisite is satisfied. The update-frame /// orchestrator ticks it through a typed production context. /// @@ -16,7 +16,7 @@ namespace AcDream.App.Input; /// entity has been streamed into the world dictionary, the player /// movement controller is constructible, and the initial world is fully /// drawable and collidable) plus a manual-override path -/// (the user can flip into fly mode before the auto-entry fires — +/// (the user can flip into fly mode before the auto-entry fires — /// their choice wins). All five interact with each other in a way /// that's painful to test through GameWindow but trivial here against /// fakes. @@ -25,11 +25,11 @@ namespace AcDream.App.Input; /// /// The public surface is: /// -/// — call after EnterWorld succeeds to +/// — call after EnterWorld succeeds to /// arm the entry trigger. -/// — call when the user manually enters +/// — call when the user manually enters /// fly mode (or any other code path that pre-empts the auto-entry). -/// — call once per frame; runs the +/// — call once per frame; runs the /// guard and fires the entry callback when armed AND every /// precondition is satisfied; returns true on the firing tick. /// @@ -101,7 +101,7 @@ internal sealed class LivePlayerModeAutoEntryContext } } -internal sealed class PlayerModeAutoEntry +public sealed class PlayerModeAutoEntry { private sealed class DelegateContext : IPlayerModeAutoEntryContext { @@ -163,7 +163,7 @@ internal sealed class PlayerModeAutoEntry /// Retail keeps position completion behind one blocking cell-load edge; /// acdream's asynchronous domains must converge before entry. /// Action invoked on the firing - /// tick. The same routine the manual Tab handler invokes (fly → + /// tick. The same routine the manual Tab handler invokes (fly → /// player transition). Must construct the controller + chase /// camera and switch the active camera; the auto-entry doesn't /// reach inside. @@ -202,7 +202,7 @@ internal sealed class PlayerModeAutoEntry /// /// Disarm the trigger without firing the callback. Call when the /// user has manually entered fly mode (or any other code path - /// that pre-empts the auto-entry) — the user's choice wins. + /// that pre-empts the auto-entry) — the user's choice wins. /// public void Cancel() => _armed = false; diff --git a/src/AcDream.App/Input/RetailLocalPlayerFrameController.cs b/src/AcDream.App/Input/RetailLocalPlayerFrameController.cs index f66bee33..ca4ee1b9 100644 --- a/src/AcDream.App/Input/RetailLocalPlayerFrameController.cs +++ b/src/AcDream.App/Input/RetailLocalPlayerFrameController.cs @@ -1,4 +1,4 @@ -using AcDream.App.Update; +using AcDream.App.Update; using AcDream.Runtime; namespace AcDream.App.Input; @@ -17,7 +17,7 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram { private readonly RuntimeLocalPlayerFrameController _runtime; - internal readonly record struct PresentationFrame( + public readonly record struct PresentationFrame( MovementResult Movement, bool Hidden, bool AdvancedBeforeNetwork); diff --git a/src/AcDream.App/Input/SilkKeyboardSource.cs b/src/AcDream.App/Input/SilkKeyboardSource.cs index 061e258e..bf1c17d1 100644 --- a/src/AcDream.App/Input/SilkKeyboardSource.cs +++ b/src/AcDream.App/Input/SilkKeyboardSource.cs @@ -1,4 +1,4 @@ -using AcDream.App.Rendering; +using AcDream.App.Rendering; using AcDream.UI.Abstractions.Input; using Silk.NET.Input; @@ -73,7 +73,7 @@ internal sealed class SilkKeyboardEventSurface : IKeyboardEventSurface /// subscription. Logical deactivation makes copied delegates inert while /// physical removal is retried. /// -internal sealed class SilkKeyboardSource : IKeyboardSource, IDisposable +public sealed class SilkKeyboardSource : IKeyboardSource, IDisposable { private readonly IKeyboardEventSurface _surface; private readonly HostQuiescenceGate _quiescence; diff --git a/src/AcDream.App/Input/SilkMouseSource.cs b/src/AcDream.App/Input/SilkMouseSource.cs index d0df94cb..a8530a64 100644 --- a/src/AcDream.App/Input/SilkMouseSource.cs +++ b/src/AcDream.App/Input/SilkMouseSource.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.App.Rendering; using AcDream.UI.Abstractions.Input; using Silk.NET.Input; @@ -100,7 +100,7 @@ internal sealed class SilkMouseEventSurface : IMouseEventSurface } /// Reversible Silk mouse bridge with immediate logical cutoff. -internal sealed class SilkMouseSource : IMouseSource, IDisposable +public sealed class SilkMouseSource : IMouseSource, IDisposable { private readonly IMouseEventSurface _surface; private readonly IInputCaptureSource _capture; diff --git a/src/AcDream.App/Physics/LocalPlayerShadowState.cs b/src/AcDream.App/Physics/LocalPlayerShadowState.cs index db419aee..777384da 100644 --- a/src/AcDream.App/Physics/LocalPlayerShadowState.cs +++ b/src/AcDream.App/Physics/LocalPlayerShadowState.cs @@ -1,11 +1,11 @@ -using System.Numerics; +using System.Numerics; namespace AcDream.App.Physics; /// Session-scoped cache of the local player's last published shadow pose. internal sealed class LocalPlayerShadowState { - internal readonly record struct Snapshot( + public readonly record struct Snapshot( Vector3 Position, Quaternion Orientation, uint CellId); diff --git a/src/AcDream.App/Physics/RemoteTeleportHook.cs b/src/AcDream.App/Physics/RemoteTeleportHook.cs index ed689267..a0b54769 100644 --- a/src/AcDream.App/Physics/RemoteTeleportHook.cs +++ b/src/AcDream.App/Physics/RemoteTeleportHook.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Physics; +using AcDream.Core.Physics; namespace AcDream.App.Physics; @@ -8,7 +8,7 @@ namespace AcDream.App.Physics; /// testable while the concrete movement/interpolation/target owners remain /// in the composition root. /// -internal static class RemoteTeleportHook +public static class RemoteTeleportHook { private const WeenieError TeleportCancelContext = (WeenieError)0x3Cu; @@ -46,7 +46,7 @@ internal static class RemoteTeleportHook } } -internal sealed record RemoteTeleportHookActions( +public sealed record RemoteTeleportHookActions( Action CancelMoveTo, Action UnStick, Action StopInterpolating, diff --git a/src/AcDream.App/Plugins/AppPluginHost.cs b/src/AcDream.App/Plugins/AppPluginHost.cs index c8b99ed2..bfabab86 100644 --- a/src/AcDream.App/Plugins/AppPluginHost.cs +++ b/src/AcDream.App/Plugins/AppPluginHost.cs @@ -1,8 +1,8 @@ -using AcDream.Plugin.Abstractions; +using AcDream.Plugin.Abstractions; namespace AcDream.App.Plugins; -internal sealed class AppPluginHost : IPluginHost +public sealed class AppPluginHost : IPluginHost { public AppPluginHost( IPluginLogger log, diff --git a/src/AcDream.App/Plugins/BufferedUiRegistry.cs b/src/AcDream.App/Plugins/BufferedUiRegistry.cs index b8c431f4..bcab04fb 100644 --- a/src/AcDream.App/Plugins/BufferedUiRegistry.cs +++ b/src/AcDream.App/Plugins/BufferedUiRegistry.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using AcDream.Plugin.Abstractions; namespace AcDream.App.Plugins; @@ -8,9 +8,9 @@ namespace AcDream.App.Plugins; /// Program.cs before the GL window opens) until GameWindow drains them into the /// UiHost tree after construction. /// -internal sealed class BufferedUiRegistry : IUiRegistry +public sealed class BufferedUiRegistry : IUiRegistry { - internal readonly record struct Pending(string MarkupPath, object Binding); + public readonly record struct Pending(string MarkupPath, object Binding); private readonly List _pending = new(); diff --git a/src/AcDream.App/Plugins/SerilogAdapter.cs b/src/AcDream.App/Plugins/SerilogAdapter.cs index d336b019..9f0b4f09 100644 --- a/src/AcDream.App/Plugins/SerilogAdapter.cs +++ b/src/AcDream.App/Plugins/SerilogAdapter.cs @@ -1,8 +1,8 @@ -using AcDream.Plugin.Abstractions; +using AcDream.Plugin.Abstractions; namespace AcDream.App.Plugins; -internal sealed class SerilogAdapter : IPluginLogger +public sealed class SerilogAdapter : IPluginLogger { private readonly Serilog.ILogger _log; public SerilogAdapter(Serilog.ILogger log) => _log = log; diff --git a/src/AcDream.App/Rendering/BitmapFont.cs b/src/AcDream.App/Rendering/BitmapFont.cs index 73ecb214..9306b4bd 100644 --- a/src/AcDream.App/Rendering/BitmapFont.cs +++ b/src/AcDream.App/Rendering/BitmapFont.cs @@ -1,20 +1,20 @@ -using System; +using System; using System.IO; +using Silk.NET.OpenGL; using StbTrueTypeSharp; namespace AcDream.App.Rendering; /// /// A pixel-font atlas rasterized from a TTF at load time using stb_truetype. -/// Glyphs are packed into a single-channel (R8) texture registered in the -/// device's global texture table. Call to resolve an -/// ASCII codepoint to UV + metrics. +/// Glyphs are packed into a single-channel (R8) GL texture. Call +/// to resolve an ASCII codepoint to UV + metrics. /// /// Only printable ASCII (32..127) is supported for the debug overlay. /// -internal sealed unsafe class BitmapFont : IDisposable +public sealed unsafe class BitmapFont : IDisposable { - internal readonly struct Glyph + public readonly struct Glyph { public readonly float UvMinX; public readonly float UvMinY; @@ -34,23 +34,23 @@ internal sealed unsafe class BitmapFont : IDisposable } } + private readonly GL _gl; private readonly Glyph[] _glyphs; private readonly int _firstChar; private readonly int _numChars; - private readonly IGpuDevice _device; - private readonly IGpuTexture _texture; + private readonly ResourceCleanupGroup _resources; - public GpuTextureSlot TextureId { get; } + public uint TextureId { get; } public float PixelHeight { get; } public float LineHeight { get; } public float Ascent { get; } public int AtlasWidth { get; } public int AtlasHeight { get; } - public BitmapFont(IGpuDevice device, byte[] ttfBytes, float pixelHeight, + public BitmapFont(GL gl, byte[] ttfBytes, float pixelHeight, int atlasSize = 512, int firstChar = 32, int numChars = 96) { - _device = device ?? throw new ArgumentNullException(nameof(device)); + _gl = gl; PixelHeight = pixelHeight; AtlasWidth = atlasSize; AtlasHeight = atlasSize; @@ -96,31 +96,65 @@ internal sealed unsafe class BitmapFont : IDisposable adv: bc.xadvance); } - // Upload atlas as a single-channel texture (R8) and register it in the - // device's global texture table. Linear + clamp-to-edge matches the - // GL path's prior fixed sampler state exactly (mip filtering is moot — - // the atlas is a single mip level). - IGpuTexture texture = _device.CreateTexture(new GpuTextureDescription( - "bitmap-font-atlas", - GpuTextureKind.Texture2D, - GpuTextureFormat.R8Unorm, - Width: AtlasWidth, - Height: AtlasHeight, - LayerCount: 1, - MipLevelCount: 1)); + // Upload atlas as a single-channel GL texture (R8). Publish the GL + // name into the construction ledger before any later upload/state + // command can fail. + var resources = new ResourceCleanupGroup(); + uint texture = 0; try { - texture.Upload(0, 0, pixels); - IGpuSampler sampler = _device.CreateSampler(GpuSamplerDescription.WorldClamp); - TextureId = _device.RegisterTexture(texture, sampler); + texture = GlResourceCommand.CreateTexture(_gl, "BitmapFont atlas"); + uint ownedTexture = texture; + resources.Add( + "bitmap-font atlas", + () => GlResourceCommand.DeleteTexture( + _gl, + ownedTexture, + $"delete BitmapFont atlas {ownedTexture}")); + _gl.GetInteger(GetPName.TextureBinding2D, out int previousTexture); + _gl.GetInteger(GetPName.UnpackAlignment, out int previousAlignment); + GlResourceCommand.Execute(_gl, "initialize BitmapFont atlas", () => + { + try + { + _gl.BindTexture(TextureTarget.Texture2D, texture); + _gl.PixelStore(PixelStoreParameter.UnpackAlignment, 1); + fixed (byte* ptr = pixels) + { + _gl.TexImage2D(TextureTarget.Texture2D, 0, + (int)InternalFormat.R8, + (uint)AtlasWidth, (uint)AtlasHeight, 0, + PixelFormat.Red, PixelType.UnsignedByte, ptr); + } + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, + (int)TextureMinFilter.Linear); + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, + (int)TextureMagFilter.Linear); + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, + (int)TextureWrapMode.ClampToEdge); + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, + (int)TextureWrapMode.ClampToEdge); + } + finally + { + _gl.PixelStore( + PixelStoreParameter.UnpackAlignment, + previousAlignment); + _gl.BindTexture( + TextureTarget.Texture2D, + unchecked((uint)previousTexture)); + } + }); } - catch + catch (Exception constructionFailure) { - texture.Dispose(); - throw; + resources.RollbackConstructionAndThrow( + "BitmapFont construction failed and its GL atlas did not cleanly roll back.", + constructionFailure); } - _texture = texture; + TextureId = texture; + _resources = resources; } public bool TryGetGlyph(char c, out Glyph g) @@ -149,8 +183,7 @@ internal sealed unsafe class BitmapFont : IDisposable public void Dispose() { - _device.ReleaseTextureSlot(TextureId); - _texture.Dispose(); + _resources.RetryCleanup(); } /// diff --git a/src/AcDream.App/Rendering/CameraController.cs b/src/AcDream.App/Rendering/CameraController.cs index 0944e344..60b84ac5 100644 --- a/src/AcDream.App/Rendering/CameraController.cs +++ b/src/AcDream.App/Rendering/CameraController.cs @@ -1,9 +1,9 @@ -// src/AcDream.App/Rendering/CameraController.cs +// src/AcDream.App/Rendering/CameraController.cs using AcDream.Core.Rendering; namespace AcDream.App.Rendering; -internal sealed class CameraController +public sealed class CameraController { internal readonly record struct CameraState( int ModeCode, @@ -19,7 +19,7 @@ internal sealed class CameraController /// The renderer-facing active camera. Both the legacy and retail /// chase cameras are held simultaneously so that flipping /// takes effect - /// on the very next access to this property — no re-entry required, + /// on the very next access to this property — no re-entry required, /// no notification mechanism, no stale state. /// public ICamera Active @@ -59,7 +59,7 @@ internal sealed class CameraController /// /// Store both cameras simultaneously; picks - /// between them per-read via the flag — no re-entry needed on flip. + /// between them per-read via the flag — no re-entry needed on flip. /// public void EnterChaseMode(ChaseCamera legacy, RetailChaseCamera retail) { diff --git a/src/AcDream.App/Rendering/CellVisibility.cs b/src/AcDream.App/Rendering/CellVisibility.cs index 544d60cd..31b9ebca 100644 --- a/src/AcDream.App/Rendering/CellVisibility.cs +++ b/src/AcDream.App/Rendering/CellVisibility.cs @@ -1,11 +1,11 @@ -// CellVisibility.cs — portal-based interior cell visibility system. +// CellVisibility.cs — portal-based interior cell visibility system. // // Stage 3 (2026-06-02): FindCameraCell + grace-frame AABB fallback deleted. // The physics membership answer (CellGraph.CurrCell) is now the mandatory root; -// ComputeVisibilityFromRoot(null, …) returns null (outdoor root) rather than +// ComputeVisibilityFromRoot(null, …) returns null (outdoor root) rather than // falling back to an independent AABB position resolve. This matches retail's // CellManager::ChangePosition (0x004559B0) which does not re-derive the cell -// from a static position — it reads the swept transition-owned CurrCell. +// from a static position — it reads the swept transition-owned CurrCell. // // This file is intentionally free of GL / rendering types. It depends only on // System.Numerics so it can be unit-tested without a GPU context. @@ -23,7 +23,7 @@ namespace AcDream.App.Rendering; /// A loaded EnvCell with portal connectivity and spatial data, used by /// for portal-traversal visibility decisions. /// -internal sealed class LoadedCell +public sealed class LoadedCell { /// Full 32-bit cell ID, e.g. 0xA9B40105. public uint CellId; @@ -87,7 +87,7 @@ internal sealed class LoadedCell public uint? BuildingId { get; internal set; } /// - /// Phase U.4c: the stab_list PVS as full (landblock-prefixed) cell ids — retail + /// Phase U.4c: the stab_list PVS as full (landblock-prefixed) cell ids — retail /// CEnvCell.stab_list (acclient.h ~30925), the stable set of cells potentially /// visible from this cell, precomputed by the AC content tools. Refreshed only at /// hydration (= retail's per-cell-entry grab_visible_cells, decomp:311878). @@ -98,7 +98,7 @@ internal sealed class LoadedCell public IReadOnlyList VisibleCells = System.Array.Empty(); /// - /// Phase U.4c: retail CEnvCell.seen_outside (acclient.h ~30925) — this cell sees + /// Phase U.4c: retail CEnvCell.seen_outside (acclient.h ~30925) — this cell sees /// the exterior (an exit portal is reachable from it). Retail gates the landscape /// data + draw decision on the camera cell's value (RenderNormalMode decomp:92649, /// grab_visible_cells decomp:311878). The stable anchor for the terrain-draw test. @@ -107,7 +107,7 @@ internal sealed class LoadedCell /// /// Render unification (2026-06-07): true for the synthetic OUTDOOR cell node built by - /// — the outdoor world modelled as a flood-graph cell whose + /// — the outdoor world modelled as a flood-graph cell whose /// shell is the landscape. seeds OutsideView /// full-screen when the root carries this flag (so terrain/sky/scenery draw as the node's shell). /// An explicit flag, not a cell-id heuristic: interior EnvCell ids are >= 0x100 in production but @@ -123,19 +123,19 @@ internal sealed class LoadedCell /// is the dat's reciprocal back-link: the index of /// the portal WITHIN the neighbour cell's portal list that points back through /// this same opening. Retail indexes the reciprocal directly via this field -/// (arg2->other_portal_id, decomp:433557) rather than scanning — which +/// (arg2->other_portal_id, decomp:433557) rather than scanning — which /// is what lets a cell with TWO portals to the same neighbour resolve each /// opening against its OWN reciprocal polygon instead of the first match. /// /// -internal readonly record struct CellPortalInfo( +public readonly record struct CellPortalInfo( ushort OtherCellId, ushort PolygonId, ushort Flags, ushort OtherPortalId); /// /// Clip plane derived from a portal polygon, in cell-local space. /// Plane equation: Normal.X*x + Normal.Y*y + Normal.Z*z + D = 0. /// -internal struct PortalClipPlane +public struct PortalClipPlane { /// Plane normal (cell-local space, unit length). public Vector3 Normal; @@ -146,8 +146,8 @@ internal struct PortalClipPlane /// /// Which half-space is "inside" this cell (the side from which you look outward /// through the portal): - /// 0 → camera dot-product must be >= 0 (positive half-space is inside) - /// 1 → camera dot-product must be <= 0 (negative half-space is inside) + /// 0 → camera dot-product must be >= 0 (positive half-space is inside) + /// 1 → camera dot-product must be <= 0 (negative half-space is inside) /// Determined from cell centroid position relative to the portal plane. /// Ported from ACME EnvCellManager.cs ~line 404. /// @@ -155,12 +155,12 @@ internal struct PortalClipPlane } /// -/// Phase U.4c flap probe (diagnostic — OBSOLETE as of Stage 3). Previously tracked +/// Phase U.4c flap probe (diagnostic — OBSOLETE as of Stage 3). Previously tracked /// which branch of FindCameraCell (now deleted) resolved the camera cell. Retained /// for binary compatibility with the [flap-cam] probe log site in GameWindow.cs that /// still prints (always None post-Stage 3). /// -internal enum CameraCellResolution +public enum CameraCellResolution { /// No cell contains the eye (outdoors), or not yet resolved. None, @@ -171,14 +171,14 @@ internal enum CameraCellResolution /// The eye is inside a cell found by the full brute-force scan. BruteForce, /// The eye is inside NO cell, but the previous cell is kept alive for a - /// few grace frames — the "stale root" case the flap probe watches for. + /// few grace frames — the "stale root" case the flap probe watches for. Grace, } /// /// Result of a portal-based visibility BFS from the camera cell. /// -internal sealed class VisibilityResult +public sealed class VisibilityResult { /// Full cell IDs (e.g. 0x01D90105) that should be rendered this frame. public HashSet VisibleCellIds { get; init; } = new(); @@ -207,7 +207,7 @@ internal sealed class VisibilityResult /// Ported faithfully from ACME's EnvCellManager.cs portal-visibility region. /// Constants and control flow match the ACME implementation. /// -internal sealed class CellVisibility +public sealed class CellVisibility { // ------------------------------------------------------------------ // Constants (ACME ground-truth values) @@ -234,7 +234,7 @@ internal sealed class CellVisibility public VisibilityResult? LastVisibilityResult { get; private set; } /// - /// Stage 3 (2026-06-02): always — the FindCameraCell + /// Stage 3 (2026-06-02): always — the FindCameraCell /// AABB grace-frame resolver was deleted; the physics membership answer is the sole root. /// Retained for the [flap-cam] probe log line in GameWindow.cs. /// @@ -292,7 +292,7 @@ internal sealed class CellVisibility /// /// Phase A8 (2026-05-28): enumerates the loaded cells that belong to a /// landblock prefix. Used by LandblockRenderPublisher when building - /// the per-landblock BuildingRegistry — the per-frame + /// the per-landblock BuildingRegistry — the per-frame /// drainedCells dict misses cells loaded on prior frames, so the /// stamping loop in needs access to /// every cell currently in the landblock to ensure BuildingId is set. @@ -356,15 +356,15 @@ internal sealed class CellVisibility /// /// UCG W2/Stage 3: compute visibility from a supplied root cell (the physics membership /// answer). When is null (pre-spawn, or player outside all indoor - /// cells), returns null — the caller interprets null as the outdoor root (no portal + /// cells), returns null — the caller interprets null as the outdoor root (no portal /// frame, everything slot 0, terrain ungated). The legacy AABB FindCameraCell fallback is /// deleted as of Stage 3; is the sole authority. /// Retail anchor: CellManager::ChangePosition @ 0x004559B0 reads the transition-owned - /// curr_cell — it does NOT re-derive from a static position. + /// curr_cell — it does NOT re-derive from a static position. /// /// /// The render-registered that physics determined the player is inside, - /// or null when pre-spawn or the player is in an outdoor landcell. Null → outdoor root path. + /// or null when pre-spawn or the player is in an outdoor landcell. Null → outdoor root path. /// /// /// Used as the viewer position for the portal-side test in the BFS when root is non-null. @@ -389,13 +389,13 @@ internal sealed class CellVisibility } // ------------------------------------------------------------------ - // FindCameraCell — DELETED in Stage 3 (2026-06-02) + // FindCameraCell — DELETED in Stage 3 (2026-06-02) // ------------------------------------------------------------------ // The AABB + grace-frame camera-cell resolver was removed. Production code - // now exclusively uses ComputeVisibilityFromRoot(root, …) where root is the + // now exclusively uses ComputeVisibilityFromRoot(root, …) where root is the // transition-owned CellGraph.CurrCell (set by ResolveCellId/Stage 2 physics). // Retail anchor: CellManager::ChangePosition (0x004559B0) reads curr_cell - // from the sweep — it never re-derives from a static position. + // from the sweep — it never re-derives from a static position. // // GetVisibleCells (used by ComputeVisibility below for test compatibility) // still uses the brute-force AABB scan internally. @@ -471,12 +471,12 @@ internal sealed class CellVisibility /// /// UCG W2: BFS visibility traversal from a pre-resolved root cell. - /// The root is the correct membership answer (supplied by the caller — + /// The root is the correct membership answer (supplied by the caller — /// physics CurrCell via , or AABB /// scan via for test compat). /// /// The BFS body is byte-identical to the original GetVisibleCells - /// implementation — only root acquisition was extracted out. + /// implementation — only root acquisition was extracted out. /// private VisibilityResult? GetVisibleCellsFromRoot(LoadedCell cameraCell, Vector3 cameraPos) { @@ -499,7 +499,7 @@ internal sealed class CellVisibility { var portal = cell.Portals[i]; - // Exit portal → outdoor terrain should be visible. + // Exit portal → outdoor terrain should be visible. if (portal.OtherCellId == 0xFFFF) { result.HasExitPortalVisible = true; @@ -522,8 +522,8 @@ internal sealed class CellVisibility var localCam = Vector3.Transform(cameraPos, cell.InverseWorldTransform); float dot = Vector3.Dot(plane.Normal, localCam) + plane.D; - // InsideSide == 0 → inside is positive half-space; reject if dot < -ε. - // InsideSide == 1 → inside is negative half-space; reject if dot > ε. + // InsideSide == 0 → inside is positive half-space; reject if dot < -ε. + // InsideSide == 1 → inside is negative half-space; reject if dot > ε. // Source: ACME EnvCellManager.cs lines 1458-1459. if (plane.InsideSide == 0 && dot < -PointInCellEpsilon) continue; diff --git a/src/AcDream.App/Rendering/ChaseCamera.cs b/src/AcDream.App/Rendering/ChaseCamera.cs index b6b33c28..f58476f2 100644 --- a/src/AcDream.App/Rendering/ChaseCamera.cs +++ b/src/AcDream.App/Rendering/ChaseCamera.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; namespace AcDream.App.Rendering; @@ -8,7 +8,7 @@ namespace AcDream.App.Rendering; /// character. Implements so it plugs into the /// existing renderer pipeline. /// -internal sealed class ChaseCamera : ICamera +public sealed class ChaseCamera : ICamera { public Vector3 Position { get; private set; } public float Aspect { get; set; } = 16f / 9f; @@ -37,9 +37,9 @@ internal sealed class ChaseCamera : ICamera // (at distance * sin(Pitch)) so the player can be viewed from a low // angle. Clamped to -0.7 to avoid pushing the camera deep underground; // at -0.7 and Distance=8 the camera is ~5m below player-Z which will - // clip terrain on hills but is OK on flat ground. 1.4 ≈ looking + // clip terrain on hills but is OK on flat ground. 1.4 ≈ looking // straight down. Wider than the old [0.05, 1.4] so mouse-Y moves the - // camera in both directions from the neutral [~20°] default. + // camera in both directions from the neutral [~20°] default. private const float PitchMin = -0.7f; private const float PitchMax = 1.4f; @@ -47,7 +47,7 @@ internal sealed class ChaseCamera : ICamera private Vector3 _lookAt; // K-fix12 (2026-04-26): retail-feel jump camera. The camera Z is - // pinned to the LAST GROUNDED Z while the player is airborne — the + // pinned to the LAST GROUNDED Z while the player is airborne — the // character rises above the camera on screen, visually matching // retail's "you can see yourself jump" feedback. Walking on the // ground tracks Z directly (no lag on hill transitions); falling @@ -90,10 +90,10 @@ internal sealed class ChaseCamera : ICamera { _trackedZ = playerPosition.Z; // catch up to falls / drops } - // else: airborne and rising — keep _trackedZ pinned. + // else: airborne and rising — keep _trackedZ pinned. // Look-at uses the actual player Z so the camera always points - // at the character — when the player rises above the pinned + // at the character — when the player rises above the pinned // camera the look-at tilts up to keep them centered in frame. _lookAt = playerPosition + new Vector3(0f, 0f, EyeHeight); @@ -109,7 +109,7 @@ internal sealed class ChaseCamera : ICamera Position = new Vector3( playerPosition.X - forwardX * horizontalDist, playerPosition.Y - forwardY * horizontalDist, - _trackedZ + EyeHeight + verticalDist); // ← uses tracked Z (pinned to ground while airborne) + _trackedZ + EyeHeight + verticalDist); // ← uses tracked Z (pinned to ground while airborne) } /// diff --git a/src/AcDream.App/Rendering/ClipFrame.cs b/src/AcDream.App/Rendering/ClipFrame.cs index cd02c440..76174b22 100644 --- a/src/AcDream.App/Rendering/ClipFrame.cs +++ b/src/AcDream.App/Rendering/ClipFrame.cs @@ -1,20 +1,20 @@ -// ClipFrame.cs +// ClipFrame.cs // // Phase U.3: the GPU-side container + uploader for the SHARED per-frame clip // data consumed by mesh_modern.vert (SSBO binding=2) and terrain_modern.vert // (UBO binding=2). This is the "shared" half of the U.3 clip mechanism; the // per-instance slot index buffer (SSBO binding=3) is PER-RENDERER and owned by // each renderer (WbDrawDispatcher / EnvCellRenderer), parallel to its instance -// buffer — it is NOT here. +// buffer — it is NOT here. // // === The contract (both shader sides obey) =================================== // binding=2 mesh SSBO holds an array of CellClip, one per "slot": // struct CellClip { uint count; uint _p0; uint _p1; uint _p2; vec4 planes[8]; }; // std430 layout: count at byte 0, three pad uints at 4/8/12, planes[8] at 16 -// (vec4 stride 16) → 144 bytes per slot. Slot 0 is RESERVED = no-clip (count 0). +// (vec4 stride 16) → 144 bytes per slot. Slot 0 is RESERVED = no-clip (count 0). // binding=2 terrain UBO holds the single OutsideView region: // layout(std140) { int uTerrainClipCount; vec4 uTerrainClipPlanes[8]; }; -// std140 layout: count at byte 0 (padded to 16), planes[8] at 16 → 144 bytes. +// std140 layout: count at byte 0 (padded to 16), planes[8] at 16 → 144 bytes. // // In U.3 a ClipFrame is built via NoClip(): one slot (slot 0, count 0) and a // terrain count of 0. Everything renders exactly as before. U.4 populates real @@ -40,16 +40,16 @@ namespace AcDream.App.Rendering; /// std430 / std140 byte layout. Per-instance slot buffers (binding=3) are owned by /// each renderer, not here. /// -internal sealed class ClipFrame : IDisposable +public sealed class ClipFrame : IDisposable { // ---- Layout constants (mirror mesh_modern.vert + terrain_modern.vert) ---- - /// Max planes per clip region — matches the shader's planes[8] + /// Max planes per clip region — matches the shader's planes[8] /// and GL's guaranteed GL_MAX_CLIP_DISTANCES >= 8. public const int MaxPlanes = 8; /// std430 stride of one CellClip: 16 (count + 3 pad uints) + - /// 8 × 16 (vec4 planes) = 144 bytes. + /// 8 × 16 (vec4 planes) = 144 bytes. public const int CellClipStrideBytes = 16 + MaxPlanes * 16; // 144 /// Byte offset of planes[0] within a CellClip (after the @@ -57,7 +57,7 @@ internal sealed class ClipFrame : IDisposable public const int CellClipPlanesOffset = 16; /// std140 size of the terrain UBO block: int count padded to 16, then - /// 8 × 16 (vec4 planes) = 144 bytes. Same number as the SSBO stride by + /// 8 × 16 (vec4 planes) = 144 bytes. Same number as the SSBO stride by /// coincidence of the 16-byte vec4 rule, but a DIFFERENT layout family. public const int TerrainUboBytes = 16 + MaxPlanes * 16; // 144 @@ -66,7 +66,7 @@ internal sealed class ClipFrame : IDisposable public const uint MeshClipSsboBinding = 2; /// UBO binding index for the terrain OutsideView clip region - /// (terrain_modern.vert binding=2). UBO namespace — distinct from the SSBO + /// (terrain_modern.vert binding=2). UBO namespace — distinct from the SSBO /// binding=2 above. public const uint TerrainClipUboBinding = 2; @@ -139,23 +139,23 @@ internal sealed class ClipFrame : IDisposable /// /// The U.3 default frame: exactly slot 0 (no-clip, count 0) and a terrain - /// count of 0. The whole scene renders ungated — identical to pre-U.3. U.4 + /// count of 0. The whole scene renders ungated — identical to pre-U.3. U.4 /// replaces this with a frame built from real portal visibility. /// public static ClipFrame NoClip() { - // One slot, all zeros: count=0 ⇒ shader passes every plane. + // One slot, all zeros: count=0 ⇒ shader passes every plane. var bytes = new byte[CellClipStrideBytes]; return new ClipFrame(bytes, slotCount: 1); } - /// Number of clip slots currently packed (always >= 1 — slot 0 is + /// Number of clip slots currently packed (always >= 1 — slot 0 is /// the reserved no-clip slot). public int SlotCount => _slotCount; /// - /// Phase U.4: reset this frame back to the NoClip state — exactly slot 0 - /// (no-clip, count 0) and a terrain count of 0 — WITHOUT allocating a new + /// Phase U.4: reset this frame back to the NoClip state — exactly slot 0 + /// (no-clip, count 0) and a terrain count of 0 — WITHOUT allocating a new /// frame or new GL buffers. The single long-lived _clipFrame in /// GameWindow is reset + re-packed every frame by , /// then uploaded through one SSBO and one terrain arena per fenced frame slot. @@ -209,7 +209,7 @@ internal sealed class ClipFrame : IDisposable /// /// Append one clip region (becomes the next slot index) from a /// . Only the convex-plane case is supported in - /// U.3 — Count > 0 packs that many planes; Count == 0 packs a + /// U.3 — Count > 0 packs that many planes; Count == 0 packs a /// no-clip region (pass-all). The scissor / nothing-visible fallbacks that /// can carry are deferred to U.4 (which will draw /// the AABB box or skip the cell on the CPU side, not via this slot). Returns @@ -259,7 +259,7 @@ internal sealed class ClipFrame : IDisposable /// /// Set the terrain OutsideView clip region (the single region the terrain /// shader gates against). length 0 ungates terrain - /// (count 0). U.3 callers never touch this — leaves it + /// (count 0). U.3 callers never touch this — leaves it /// at count 0. U.4 calls it with the OutsideView planes. /// public void SetTerrainClip(ReadOnlySpan planes) @@ -579,7 +579,7 @@ internal sealed class ClipFrame : IDisposable } /// A single std140 terrain-clip record within a frame-slot UBO arena. -internal readonly record struct TerrainClipBufferBinding( +public readonly record struct TerrainClipBufferBinding( uint Buffer, int OffsetBytes, int SizeBytes) diff --git a/src/AcDream.App/Rendering/ClipFrameAssembler.cs b/src/AcDream.App/Rendering/ClipFrameAssembler.cs index 2a38bff1..38b72df4 100644 --- a/src/AcDream.App/Rendering/ClipFrameAssembler.cs +++ b/src/AcDream.App/Rendering/ClipFrameAssembler.cs @@ -1,4 +1,4 @@ -// ClipFrameAssembler.cs +// ClipFrameAssembler.cs // // Retail PView assembly policy. PortalVisibilityBuilder produces a retail-like // view graph: one portal_view list per visible cell plus an outside_view list. @@ -21,7 +21,7 @@ namespace AcDream.App.Rendering; /// /// How the landscape-through-outside_view pass should be interpreted. /// -internal enum TerrainClipMode +public enum TerrainClipMode { /// All outside_view slices have convex plane clips. Planes, @@ -37,13 +37,13 @@ internal enum TerrainClipMode /// One retail portal_view slice mapped to a GPU clip slot. The AABB is retained /// for passes that cannot write gl_ClipDistance and must use scissor. /// -internal readonly record struct ClipViewSlice(int Slot, Vector4 NdcAabb, Vector4[] Planes); +public readonly record struct ClipViewSlice(int Slot, Vector4 NdcAabb, Vector4[] Planes); /// /// Result of : populated clip buffers /// plus routing data consumed by the render orchestration. /// -internal sealed class ClipFrameAssembly +public sealed class ClipFrameAssembly { public ClipFrame Frame { get; private set; } = null!; @@ -241,7 +241,7 @@ internal sealed class ClipFrameAssembly } } -internal static class ClipFrameAssembler +public static class ClipFrameAssembler { public static ClipFrameAssembly Assemble( ClipFrame frame, diff --git a/src/AcDream.App/Rendering/ClipPlaneSet.cs b/src/AcDream.App/Rendering/ClipPlaneSet.cs index 9a468fd0..dc4f0bd8 100644 --- a/src/AcDream.App/Rendering/ClipPlaneSet.cs +++ b/src/AcDream.App/Rendering/ClipPlaneSet.cs @@ -1,4 +1,4 @@ -// ClipPlaneSet.cs +// ClipPlaneSet.cs // // Phase U.2c: turn a CellView (a cell's accumulated screen-space clip region, // in NDC) into a small set of clip-space half-space planes for the GPU's @@ -6,18 +6,18 @@ // convex plane set. // // This is the bridge between PortalVisibilityBuilder's 2D NDC view polygons and -// the per-vertex clip the mesh/terrain shaders will perform (Phase U.2c → U.2e). +// the per-vertex clip the mesh/terrain shaders will perform (Phase U.2c → U.2e). // Pure System.Numerics math; NO GL. The shader consumes each plane as -// d = nx*clip.x + ny*clip.y + 0*clip.z + dw*clip.w (>= 0 ⇒ keep) +// d = nx*clip.x + ny*clip.y + 0*clip.z + dw*clip.w (>= 0 ⇒ keep) // where (nx, ny, dw) = the plane's (normal.xy, offset). z is always 0 because a -// screen-space (NDC) edge is a vertical slab in clip space — independent of depth. +// screen-space (NDC) edge is a vertical slab in clip space — independent of depth. // // === The convexity rule (read before touching this file) ===================== // gl_ClipDistance planes are a CONJUNCTION of half-spaces, i.e. exactly ONE // convex region (their intersection). A CellView with MORE THAN ONE polygon is a // UNION of convex regions, which is in general NOT convex and CANNOT be // represented by one plane set. Emitting just the first/largest polygon's planes -// would clip away the others → a real visibility bug (under-inclusion). +// would clip away the others → a real visibility bug (under-inclusion). // // Therefore From() NEVER emits a single polygon's planes when the CellView holds // several. Multi-polygon (and >8-edge) regions degrade to the UNION AABB scissor: @@ -27,13 +27,13 @@ // // === The three Count==0 states (how a consumer tells them apart) ============= // Count == 0 can mean three different things; the consumer MUST distinguish: -// (a) Empty — IsNothingVisible == true, UseScissorFallback == false. -// The cell/region isn't visible at all → DRAW NOTHING. The +// (a) Empty — IsNothingVisible == true, UseScissorFallback == false. +// The cell/region isn't visible at all → DRAW NOTHING. The // ScissorNdcAabb is a degenerate inverted box (min > max) so that a // consumer which naively scissors on it still draws nothing. -// (b) Scissor — UseScissorFallback == true, IsNothingVisible == false. +// (b) Scissor — UseScissorFallback == true, IsNothingVisible == false. // The convex-plane budget was exceeded (multi-polygon or >8 edges) -// → DRAW the ScissorNdcAabb box (a valid min<=max NDC rectangle). +// → DRAW the ScissorNdcAabb box (a valid min<=max NDC rectangle). // There is no third Count==0 state produced by From(). (A separate "no-clip, // pass-all" slot 0 is constructed by the consumer directly, not via From().) // When Count > 0, Planes carries the convex gate and the scissor fields are unused. @@ -45,26 +45,26 @@ using System.Numerics; namespace AcDream.App.Rendering; /// -/// An NDC convex view region reduced to ≤8 clip-space gl_ClipDistance planes, or a +/// An NDC convex view region reduced to ≤8 clip-space gl_ClipDistance planes, or a /// scissor AABB fallback. See the file header for the convexity rule and the three /// Count==0 states. /// -internal readonly struct ClipPlaneSet +public readonly struct ClipPlaneSet { // Max simultaneous hardware clip planes we target (GL guarantees >= 8). private const int MaxPlanes = 8; // Collinear-edge merge threshold. Two consecutive edge directions are treated as - // the same edge when the turn between them is below ~0.5° (retail copy_view does a - // ~1px screen-space dedup). |sin θ| for unit dirs = |cross|; sin(0.5°) ≈ 0.0087265. + // the same edge when the turn between them is below ~0.5° (retail copy_view does a + // ~1px screen-space dedup). |sin θ| for unit dirs = |cross|; sin(0.5°) ≈ 0.0087265. private const float CollinearSinEps = 0.0087265f; - // Drop a vertex whose two incident edges are shorter than this (NDC) — a duplicate + // Drop a vertex whose two incident edges are shorter than this (NDC) — a duplicate // or near-duplicate point that would otherwise yield a garbage normalized normal. private const float DegenerateEdgeLen = 1e-6f; // A polygon whose absolute signed area (full area, not 2x) falls below this is a line - // or point — zero screen coverage ⇒ nothing visible. A real portal opening has area far + // or point — zero screen coverage ⇒ nothing visible. A real portal opening has area far // above this (e.g. the sliver-clip test region is 0.4); only an edge-on projection gets here. private const float MinPolygonArea = 1e-7f; private readonly Vector4[] _planes; @@ -77,7 +77,7 @@ internal readonly struct ClipPlaneSet ScissorNdcAabb = scissorNdcAabb; } - /// Number of active clip planes, 0..8. 0 ⇒ inspect + /// Number of active clip planes, 0..8. 0 ⇒ inspect /// and to decide between "draw the AABB" and "draw nothing". public int Count => _planes?.Length ?? 0; @@ -89,11 +89,11 @@ internal readonly struct ClipPlaneSet // its frame-scoped slice instead of cloning every plane payload a second time. internal Vector4[] PlaneArray => _planes ?? Array.Empty(); - /// True ⇒ the convex-plane budget was exceeded; gate on + /// True ⇒ the convex-plane budget was exceeded; gate on /// instead (draw the box). Always false when > 0 or when the region is empty. public bool UseScissorFallback { get; } - /// True ⇒ the region is not visible at all; the consumer draws NOTHING. + /// True ⇒ the region is not visible at all; the consumer draws NOTHING. /// Mutually exclusive with , and only meaningful when Count == 0. public bool IsNothingVisible { get; } @@ -106,20 +106,20 @@ internal readonly struct ClipPlaneSet public static ClipPlaneSet Empty { get; } = new(Array.Empty(), useScissorFallback: false, isNothingVisible: true, scissorNdcAabb: DegenerateAabb); - // Inverted box (min > max) — any sane AABB intersection against it is empty. + // Inverted box (min > max) — any sane AABB intersection against it is empty. private static Vector4 DegenerateAabb => new(1f, 1f, -1f, -1f); /// - /// Reduce a CellView's NDC clip region to a ClipPlaneSet. One convex polygon (≤8 edges - /// after collinear-merge) → per-edge planes; multi-polygon or >8 edges → union-AABB scissor; - /// empty/degenerate → . See the file header for the full rule. + /// Reduce a CellView's NDC clip region to a ClipPlaneSet. One convex polygon (≤8 edges + /// after collinear-merge) → per-edge planes; multi-polygon or >8 edges → union-AABB scissor; + /// empty/degenerate → . See the file header for the full rule. /// public static ClipPlaneSet From(CellView region) { if (region is null || region.IsEmpty || region.Polygons.Count == 0) return Empty; - // MORE THAN ONE polygon ⇒ union, not convex ⇒ never emit one polygon's planes. + // MORE THAN ONE polygon ⇒ union, not convex ⇒ never emit one polygon's planes. // Over-include via the union AABB (safe). region.Min/Max already track the union. if (region.Polygons.Count > 1) return Scissor(region.MinX, region.MinY, region.MaxX, region.MaxY); @@ -152,15 +152,15 @@ internal readonly struct ClipPlaneSet { int count = NormalizeAndMerge(input, verts); - // Fewer than 3 distinct edges survive ⇒ a sliver/line with no area. There is no + // Fewer than 3 distinct edges survive ⇒ a sliver/line with no area. There is no // meaningful AABB to over-include (a zero-area region), so treat it as nothing visible. if (count < 3) return Empty; ReadOnlySpan normalized = verts[..count]; - // A single convex polygon with too many edges to fit the hardware budget ⇒ scissor - // on ITS own AABB (still a superset of the polygon → over-include, safe). + // A single convex polygon with too many edges to fit the hardware budget ⇒ scissor + // on ITS own AABB (still a superset of the polygon → over-include, safe). if (count > MaxPlanes) return Scissor(normalized); @@ -173,10 +173,10 @@ internal readonly struct ClipPlaneSet Vector2 q = normalized[(i + 1) % count]; Vector2 dir = q - p; // Inward normal for CCW winding: perp(dir) = (-dir.y, dir.x) points to the polygon's - // interior (the "left" side of the directed edge p→q). + // interior (the "left" side of the directed edge p→q). Vector2 n = Vector2.Normalize(new Vector2(-dir.Y, dir.X)); - // Plane: n·x + d >= 0 inside, with d = -(n·p). In clip space with NDC x = clip.x/clip.w: - // dist = n.x*clip.x + n.y*clip.y + 0*clip.z + (-(n·p))*clip.w (>= 0 ⇒ keep) + // Plane: n·x + d >= 0 inside, with d = -(n·p). In clip space with NDC x = clip.x/clip.w: + // dist = n.x*clip.x + n.y*clip.y + 0*clip.z + (-(n·p))*clip.w (>= 0 ⇒ keep) planes[i] = new Vector4(n.X, n.Y, 0f, -Vector2.Dot(n, p)); } return new ClipPlaneSet(planes, useScissorFallback: false, isNothingVisible: false, scissorNdcAabb: DegenerateAabb); @@ -233,9 +233,9 @@ internal readonly struct ClipPlaneSet if (SignedArea2(points[..count]) < 0f) points[..count].Reverse(); - // 3) Merge collinear edges: drop vertex i when edge (i-1→i) and edge (i→i+1) point the same - // way (turn angle < ~0.5°). Iterate until stable — removing one vertex can expose a new - // collinear triple. |cross(a,b)| of unit dirs = |sin θ|; dot>0 rules out a 180° reversal. + // 3) Merge collinear edges: drop vertex i when edge (i-1→i) and edge (i→i+1) point the same + // way (turn angle < ~0.5°). Iterate until stable — removing one vertex can expose a new + // collinear triple. |cross(a,b)| of unit dirs = |sin θ|; dot>0 rules out a 180° reversal. bool changed = true; while (changed && count >= 3) { @@ -260,12 +260,12 @@ internal readonly struct ClipPlaneSet d0 /= l0; d1 /= l1; - float cross = d0.X * d1.Y - d0.Y * d1.X; // sin θ - float dot = d0.X * d1.X + d0.Y * d1.Y; // cos θ + float cross = d0.X * d1.Y - d0.Y * d1.X; // sin θ + float dot = d0.X * d1.X + d0.Y * d1.Y; // cos θ if (dot > 0f && MathF.Abs(cross) < CollinearSinEps) { points[(i + 1)..count].CopyTo(points[i..]); - count--; // cur lies on the straight line prev→next + count--; // cur lies on the straight line prev→next changed = true; break; } @@ -276,7 +276,7 @@ internal readonly struct ClipPlaneSet return 0; // Final degeneracy gate: a polygon with negligible area is a line/point even if it still - // has >= 3 distinct vertices (e.g. an edge-on portal, or a near-collinear triple the 0.5° + // has >= 3 distinct vertices (e.g. an edge-on portal, or a near-collinear triple the 0.5° // merge didn't quite collapse). Emitting its planes would yield an empty half-space // intersection that silently gates out everything; report it honestly as nothing-visible. if (MathF.Abs(SignedArea2(points[..count])) * 0.5f < MinPolygonArea) @@ -285,7 +285,7 @@ internal readonly struct ClipPlaneSet return count; } - // Twice the signed area (the "shoelace" sum). > 0 ⇒ CCW, < 0 ⇒ CW. + // Twice the signed area (the "shoelace" sum). > 0 ⇒ CCW, < 0 ⇒ CW. private static float SignedArea2(ReadOnlySpan poly) { float a = 0f; diff --git a/src/AcDream.App/Rendering/CompositeTextureArrayCache.cs b/src/AcDream.App/Rendering/CompositeTextureArrayCache.cs index 713577ac..cb9899a8 100644 --- a/src/AcDream.App/Rendering/CompositeTextureArrayCache.cs +++ b/src/AcDream.App/Rendering/CompositeTextureArrayCache.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Textures; +using AcDream.Core.Textures; using AcDream.Core.World; using Silk.NET.OpenGL; @@ -8,7 +8,7 @@ namespace AcDream.App.Rendering; /// Location of one decoded entity-material composite in a resident bindless /// texture array. The modern mesh shader consumes this exact pair. /// -internal readonly record struct BindlessTextureLocation(ulong Handle, uint Layer); +public readonly record struct BindlessTextureLocation(ulong Handle, uint Layer); internal enum CompositeTextureKind : byte { diff --git a/src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs b/src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs index 7828a97f..1cecd9ef 100644 --- a/src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs +++ b/src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs @@ -114,18 +114,15 @@ internal sealed class RetailCreatureAppraisalFrameView : private readonly UiViewport _viewport; private readonly UiElement _windowFrame; private readonly AppraisalUiController _controller; - private readonly ExternalViewportTextureBridge _textureBridge; public RetailCreatureAppraisalFrameView( UiViewport viewport, UiElement windowFrame, - AppraisalUiController controller, - IGpuDevice gpuDevice) + AppraisalUiController controller) { _viewport = viewport ?? throw new ArgumentNullException(nameof(viewport)); _windowFrame = windowFrame ?? throw new ArgumentNullException(nameof(windowFrame)); _controller = controller ?? throw new ArgumentNullException(nameof(controller)); - _textureBridge = new ExternalViewportTextureBridge(gpuDevice); } public bool TryGetVisibleTarget( @@ -152,7 +149,7 @@ internal sealed class RetailCreatureAppraisalFrameView : } public void SetTextureHandle(uint textureHandle) => - _viewport.TextureHandle = _textureBridge.Resolve(textureHandle); + _viewport.TextureHandle = textureHandle; private static bool IsEffectivelyVisible(UiElement element) { diff --git a/src/AcDream.App/Rendering/DebugLineRenderer.cs b/src/AcDream.App/Rendering/DebugLineRenderer.cs index d84bb6e6..7f883a78 100644 --- a/src/AcDream.App/Rendering/DebugLineRenderer.cs +++ b/src/AcDream.App/Rendering/DebugLineRenderer.cs @@ -1,56 +1,94 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Numerics; using System.Runtime.InteropServices; +using Silk.NET.OpenGL; namespace AcDream.App.Rendering; /// -/// Minimal debug line renderer for visualizing collision shapes, +/// Minimal GL debug line renderer for visualizing collision shapes, /// bounding boxes, and other debug geometry. Collect lines each frame /// via / , then call -/// to upload + draw them through the current -/// . +/// to upload + draw them. /// -/// Campaign V slice V4a: ported onto . Owns one -/// pipeline (LINE_LIST topology, depth disabled — lines must show through -/// geometry, matching the prior explicit DepthTest disable) and draws -/// with the frame's ring rather than a persistent respecialized VBO. Vertex -/// format is (vec3 pos, vec3 color) = 24 bytes per vertex, unchanged. +/// Uses a single shared VBO that's respecialized each frame. Vertex +/// format is (vec3 pos, vec3 color) = 24 bytes per vertex. /// -internal sealed class DebugLineRenderer : IDisposable +public sealed unsafe class DebugLineRenderer : IDisposable { - private readonly IGpuDevice _device; - private readonly IGpuPipeline _pipeline; - - private static readonly GpuVertexLayout VertexLayout = new( - StrideBytes: 24, - [ - new GpuVertexAttribute(0, GpuVertexFormat.Float3, 0), - new GpuVertexAttribute(1, GpuVertexFormat.Float3, 12), - ]); + private readonly GL _gl; + private readonly Shader _shader; + private readonly uint _vao; + private readonly uint _vbo; + private readonly ResourceCleanupGroup _resources; private readonly List _buffer = new(4096); private int _vertexCount; + private int _capacityBytes; - public DebugLineRenderer(IGpuDevice device) + public DebugLineRenderer(GL gl, string shaderDir) { - _device = device ?? throw new ArgumentNullException(nameof(device)); - _pipeline = _device.CreatePipeline(new GpuPipelineDescription + _gl = gl ?? throw new ArgumentNullException(nameof(gl)); + ArgumentException.ThrowIfNullOrWhiteSpace(shaderDir); + var resources = new ResourceCleanupGroup(); + Shader? shader = null; + uint vao = 0; + uint vbo = 0; + try { - Name = "debug-line", - Shaders = new GpuShaderSet("debug_line"), - VertexLayout = VertexLayout, - Topology = GpuPrimitiveTopology.LineList, - Blend = GpuBlendMode.None, - // Retail debug lines are drawn visible THROUGH geometry — the - // prior GL path captured+disabled DepthTest around the draw and - // restored whatever the caller had before. A dedicated pipeline - // bakes "always visible" directly, which is simpler and exactly - // as behaviour-preserving since nothing else shares this pipeline. - Depth = GpuDepthState.Disabled, - Cull = GpuCullMode.None, - ColorWrite = true, - }); + shader = new Shader(gl, + Path.Combine(shaderDir, "debug_line.vert"), + Path.Combine(shaderDir, "debug_line.frag")); + resources.Add("debug-line shader", shader.Dispose); + vao = GlResourceCommand.CreateName( + gl, + "debug-line VAO", + gl.GenVertexArray, + gl.DeleteVertexArray); + uint ownedVao = vao; + resources.Add( + "debug-line VAO", + () => GlResourceCommand.DeleteVertexArray( + gl, + ownedVao, + $"delete debug-line VAO {ownedVao}")); + vbo = GlResourceCommand.CreateName( + gl, + "debug-line VBO", + gl.GenBuffer, + gl.DeleteBuffer); + uint ownedVbo = vbo; + resources.Add( + "debug-line VBO", + () => GlResourceCommand.DeleteBuffer( + gl, + ownedVbo, + $"delete debug-line VBO {ownedVbo}")); + + GlResourceCommand.Execute(gl, "configure debug-line vertex state", () => + { + gl.BindVertexArray(vao); + gl.BindBuffer(BufferTargetARB.ArrayBuffer, vbo); + // 24-byte stride: vec3 pos + vec3 color + gl.EnableVertexAttribArray(0); + gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 6 * sizeof(float), (void*)0); + gl.EnableVertexAttribArray(1); + gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, 6 * sizeof(float), (void*)(3 * sizeof(float))); + gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0); + gl.BindVertexArray(0); + }); + } + catch (Exception constructionFailure) + { + resources.RollbackConstructionAndThrow( + "DebugLineRenderer construction failed and its GL prefix did not cleanly roll back.", + constructionFailure); + } + + _resources = resources; + _shader = shader!; + _vao = vao; + _vbo = vbo; } /// Clear accumulated lines. Call at the start of each frame. @@ -131,40 +169,45 @@ internal sealed class DebugLineRenderer : IDisposable AddLine(c[2], c[6], color); AddLine(c[3], c[7], color); } - /// Upload + draw all accumulated lines against the current frame. - public void Flush(Matrix4x4 view, Matrix4x4 projection, IGpuFrame frame) + /// Upload + draw all accumulated lines. + public void Flush(Matrix4x4 view, Matrix4x4 projection) { if (_vertexCount == 0) return; - ArgumentNullException.ThrowIfNull(frame); - int byteCount = _buffer.Count * sizeof(float); - GpuRingAllocation allocation = frame.AllocateRing(byteCount, GpuRingUsage.Vertex); - CollectionsMarshal.AsSpan(_buffer).CopyTo(allocation.AsSpan()); + _shader.Use(); + _shader.SetMatrix4("uView", view); + _shader.SetMatrix4("uProjection", projection); - using IGpuPassEncoder pass = frame.BeginPass(new GpuPassDescription + _gl.BindVertexArray(_vao); + _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo); + + int neededBytes = _buffer.Count * sizeof(float); + if (neededBytes > _capacityBytes) { - Name = "debug-lines", - Color = new GpuColorAttachment( - Target: null, - Load: GpuLoadOp.Load, - Store: GpuStoreOp.Store, - ClearColor: default), - Depth = null, - SampleCount = 1, - }); - pass.BindPipeline(_pipeline); - pass.BindVertexBuffer(allocation.Buffer, allocation.OffsetBytes); - // Same combined-matrix convention every other ported shader uses - // (WbDrawDispatcher, TerrainModernRenderer, ParticleRenderer): - // C# multiplies view * projection once and uploads the single - // uViewProjection the shader now declares, replacing the separate - // uView/uProjection uniforms. - pass.SetPushConstants(GpuPushConstants.Default with { ViewProjection = view * projection }); - pass.Draw((uint)_vertexCount, instanceCount: 1, firstVertex: 0, firstInstance: 0); + fixed (float* ptr = CollectionsMarshal.AsSpan(_buffer)) + _gl.BufferData(BufferTargetARB.ArrayBuffer, (nuint)neededBytes, ptr, BufferUsageARB.DynamicDraw); + _capacityBytes = neededBytes; + } + else + { + fixed (float* ptr = CollectionsMarshal.AsSpan(_buffer)) + _gl.BufferSubData(BufferTargetARB.ArrayBuffer, 0, (nuint)neededBytes, ptr); + } + + // Depth test on so lines get occluded by geometry (but we want them + // visible through geometry — disable depth test so everything shows). + bool wasDepthEnabled = _gl.IsEnabled(EnableCap.DepthTest); + _gl.Disable(EnableCap.DepthTest); + + _gl.DrawArrays(PrimitiveType.Lines, 0, (uint)_vertexCount); + + if (wasDepthEnabled) _gl.Enable(EnableCap.DepthTest); + + _gl.BindVertexArray(0); } public void Dispose() { - _pipeline.Dispose(); + _resources.RetryCleanup(); } } diff --git a/src/AcDream.App/Rendering/DollCamera.cs b/src/AcDream.App/Rendering/DollCamera.cs index 6c13f234..3a37cdb1 100644 --- a/src/AcDream.App/Rendering/DollCamera.cs +++ b/src/AcDream.App/Rendering/DollCamera.cs @@ -1,26 +1,26 @@ -using System; +using System; using System.Numerics; namespace AcDream.App.Rendering; /// -/// Fixed camera for the paperdoll mini-scene — retail-exact, ported from the gmPaperDollUI viewport -/// setup (decomp 0x004a5a39–0x004a5a69). The viewport (element 0x100001d5) is configured by +/// Fixed camera for the paperdoll mini-scene — retail-exact, ported from the gmPaperDollUI viewport +/// setup (decomp 0x004a5a39–0x004a5a69). The viewport (element 0x100001d5) is configured by /// UIElement_Viewport::SetCamera(position, direction) with: /// -/// position = (0.12, −2.4, 0.88) [hex 0x3df5c28f, 0xc019999a, 0x3f6147ae] -/// direction = (0, 0, 0) ⇒ CreatureMode::SetCameraDirection resets the view frame to +/// position = (0.12, −2.4, 0.88) [hex 0x3df5c28f, 0xc019999a, 0x3f6147ae] +/// direction = (0, 0, 0) ⇒ CreatureMode::SetCameraDirection resets the view frame to /// IDENTITY (euler_set_rotate(0,0,0) then rotate(0,0,0)), so the camera looks -/// straight down +Y with +Z up — NO yaw, NO pitch. +/// straight down +Y with +Z up — NO yaw, NO pitch. /// /// The doll is framed purely by camera POSITION + FOV, NOT by aiming at the body: at distance 2.4 with -/// a Ï€/4 (45°) vertical FOV the visible vertical band is z≈[−0.11, 1.87], which covers the whole ~1.6 m -/// figure even though the model origin sits at the FEET (z=0). FOV Ï€/4 is CreatureMode's default +/// a π/4 (45°) vertical FOV the visible vertical band is z≈[−0.11, 1.87], which covers the whole ~1.6 m +/// figure even though the model origin sits at the FEET (z=0). FOV π/4 is CreatureMode's default /// m_fFOVRadians (ctor 0x004543cf, hex 0x3f490fdb); default ambient is (0.3,0.3,0.3); the /// paperdoll uses UseSharpMode (not SmartboxFOV) so Render::SetFOVRad(m_fFOVRadians) applies. /// /// An earlier hand-tune aimed the camera at mid-body to "fit the figure", which introduced a -/// spurious yaw (Target.x ≠ Eye.x) that rotated the view and turned the doll's face away from the +/// spurious yaw (Target.x ≠ Eye.x) that rotated the view and turned the doll's face away from the /// viewer. This restores retail's zero-yaw frame, where full-body framing comes from the eye height + /// FOV, not from aiming. /// @@ -28,15 +28,15 @@ namespace AcDream.App.Rendering; /// convention as so the doll's triangle winding + back-face culling match the /// world render pass. AC up-axis = +Z. /// -internal sealed class DollCamera : ICamera +public sealed class DollCamera : ICamera { - // Retail paperdoll camera origin (decomp 0x004a5a51–0x004a5a61). + // Retail paperdoll camera origin (decomp 0x004a5a51–0x004a5a61). internal static readonly Vector3 RetailEye = new(0.12f, -2.4f, 0.88f); - // Identity view orientation ⇒ look straight down +Y (no yaw/pitch). Target = Eye + (0,1,0). + // Identity view orientation ⇒ look straight down +Y (no yaw/pitch). Target = Eye + (0,1,0). private static readonly Vector3 Target = new(0.12f, -1.4f, 0.88f); private static readonly Vector3 Up = Vector3.UnitZ; // AC up-axis = +Z, same as ChaseCamera - /// Vertical field of view — retail CreatureMode default m_fFOVRadians = Ï€/4 (45°). + /// Vertical field of view — retail CreatureMode default m_fFOVRadians = π/4 (45°). public float FovRadians { get; set; } = MathF.PI / 4f; public float Near { get; set; } = 0.1f; // same near plane as ChaseCamera / retail znear diff --git a/src/AcDream.App/Rendering/DollEntityBuilder.cs b/src/AcDream.App/Rendering/DollEntityBuilder.cs index eb562ba7..3d64ccb3 100644 --- a/src/AcDream.App/Rendering/DollEntityBuilder.cs +++ b/src/AcDream.App/Rendering/DollEntityBuilder.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using AcDream.Core.World; @@ -6,13 +6,13 @@ using AcDream.Core.World; namespace AcDream.App.Rendering; /// -/// Builds the dedicated paperdoll WorldEntity — retail's makeObject(player) +/// Builds the dedicated paperdoll WorldEntity — retail's makeObject(player) /// clone: the player's Setup id + current ObjDesc (base palette + subpalette overlays /// + part overrides), posed at the scene origin facing the viewer. /// /// /// The palette / part-override mapping mirrors the inline construction in -/// GameWindow.cs around lines 3390–3431. Extracted here so it is +/// GameWindow.cs around lines 3390–3431. Extracted here so it is /// unit-testable without dats and so the paperdoll renderer owns a clean /// seam: it calls with fresh player state each time the /// ObjDesc changes, and the renderer only has to swap the entity into its @@ -26,7 +26,7 @@ namespace AcDream.App.Rendering; /// server-assigned guid. /// /// -internal static class DollEntityBuilder +public static class DollEntityBuilder { /// /// Reserved synthetic guid for the paperdoll clone. High, deliberately @@ -38,18 +38,18 @@ internal static class DollEntityBuilder /// Reserved render-local entity id for the doll. FIXED (the doll is a singleton) /// and high enough never to collide with LiveEntityRuntime's local ids. The /// renderer passes this in animatedEntityIds so the dispatcher treats the - /// doll as animated — which BYPASSES the Tier-1 classification cache + /// doll as animated — which BYPASSES the Tier-1 classification cache /// (WbDrawDispatcher.cs:1142). That matters because a re-dress builds a NEW /// WorldEntity with this SAME id; without the cache bypass the dispatcher would /// serve the previous doll's cached batches and the new gear wouldn't appear. /// public const uint DollRenderId = 0xDA11_D012u; - // retail RedressCreature: CPhysicsObj::set_heading(191.367905°) (decomp 0x004a3c0a). Frame::set_heading(h) - // (decomp 0x00535e40) builds the facing vector as (sin h°, cos h°, 0): at h=0 the creature faces +Y (AC + // retail RedressCreature: CPhysicsObj::set_heading(191.367905°) (decomp 0x004a3c0a). Frame::set_heading(h) + // (decomp 0x00535e40) builds the facing vector as (sin h°, cos h°, 0): at h=0 the creature faces +Y (AC // North), the heading increasing CLOCKWISE toward +X. We rotate the doll's default +Y-forward body about - // +Z; System.Numerics rotates +Y → (−sin θ, cos θ), so to land on retail's (sin h, cos h) the angle is - // NEGATED (θ = −h). Using +h mirrors the X-lean (~22° off → the face reads as turned away from the viewer). + // +Z; System.Numerics rotates +Y → (−sin θ, cos θ), so to land on retail's (sin h, cos h) the angle is + // NEGATED (θ = −h). Using +h mirrors the X-lean (~22° off → the face reads as turned away from the viewer). private const float _headingDegrees = 191.367905f; private static readonly float _headingRad = -_headingDegrees * (MathF.PI / 180f); private static readonly Quaternion _dollRotation = @@ -62,17 +62,17 @@ internal static class DollEntityBuilder /// Pre-resolved mesh refs (may be empty; caller fills them in). /// /// ObjDesc base palette id (0x04xxxxxx). Passed only when subpalettes are - /// also present — mirrors GameWindow which only builds a PaletteOverride + /// also present — mirrors GameWindow which only builds a PaletteOverride /// when SubPalettes.Count > 0. /// /// /// Subpalette overlays from the server ObjDesc. Each tuple carries the /// subpalette dat id, byte offset into the base palette, and byte length. - /// Null or empty → PaletteOverride on the returned entity is null. + /// Null or empty → PaletteOverride on the returned entity is null. /// /// /// AnimPartChange swaps from the server ObjDesc. Each tuple is a - /// (PartIndex, replacement GfxObj id) pair. Null or empty → empty array. + /// (PartIndex, replacement GfxObj id) pair. Null or empty → empty array. /// public static WorldEntity Build( uint setupId, @@ -82,7 +82,7 @@ internal static class DollEntityBuilder IReadOnlyList<(byte PartIndex, uint GfxObjId)>? partOverrides = null) { // --- palette override (mirrors GameWindow:3395-3405) --- - // Only build when there are sub-palette overlays — same gate as GameWindow. + // Only build when there are sub-palette overlays — same gate as GameWindow. PaletteOverride? paletteOverride = null; if (subPalettes is { Count: > 0 } spList) { diff --git a/src/AcDream.App/Rendering/EquippedChildRenderController.cs b/src/AcDream.App/Rendering/EquippedChildRenderController.cs index efb1b568..e408afa8 100644 --- a/src/AcDream.App/Rendering/EquippedChildRenderController.cs +++ b/src/AcDream.App/Rendering/EquippedChildRenderController.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.App.World; using AcDream.App.Rendering.Vfx; using AcDream.Core.Items; @@ -23,7 +23,7 @@ namespace AcDream.App.Rendering; /// and recomposes it after the parent's animation /// advances each frame. /// -internal sealed class EquippedChildRenderController : IDisposable +public sealed class EquippedChildRenderController : IDisposable { private readonly IDatReaderWriter _dats; private readonly object _datLock; @@ -1674,7 +1674,7 @@ internal sealed class EquippedChildRenderController : IDisposable "has no exact projection key."); } -internal enum ChildUnparentDisposition +public enum ChildUnparentDisposition { NotAttached, Completed, diff --git a/src/AcDream.App/Rendering/ExternalViewportTextureBridge.cs b/src/AcDream.App/Rendering/ExternalViewportTextureBridge.cs deleted file mode 100644 index 1be7e29c..00000000 --- a/src/AcDream.App/Rendering/ExternalViewportTextureBridge.cs +++ /dev/null @@ -1,73 +0,0 @@ -namespace AcDream.App.Rendering; - -/// -/// Campaign V slice V4a bridge: memoizes a for the -/// most recent raw GL colour-texture name a still-unmigrated private-viewport -/// renderer (, -/// ) produced, so -/// UiViewport.TextureHandle — now a — has -/// something to draw. Those renderers still allocate their FBO colour -/// attachment directly on GL (V4g's scope: "PrivateEntityViewportRenderer -/// → IGpuRenderTarget"), so this bridge — not their own campaign slice — is -/// what lets the RETAINED UI side of the seam move onto the RHI now. -/// -/// Registers lazily and only re-registers when the producer hands back a -/// DIFFERENT raw name (the FBO's colour texture is stable across frames at a -/// fixed viewport size and only regenerates on resize) — a naive -/// register-every-frame would exhaust the 16384-slot table in seconds. -/// Deleted at slice V4g once those renderers publish a real -/// directly. -/// -internal sealed class ExternalViewportTextureBridge -{ - private readonly IGpuDevice _device; - private readonly IGpuSampler _sampler; - private uint _lastRawName; - private GpuTextureSlot _lastSlot = GpuTextureSlot.Unassigned; - - public ExternalViewportTextureBridge(IGpuDevice device) - { - _device = device ?? throw new ArgumentNullException(nameof(device)); - // Matches the FBO colour attachment's own fixed GL_LINEAR / CLAMP_TO_EDGE - // parameters (PrivateEntityViewportRenderer.EnsureFramebuffer, - // PaperdollViewportRenderer's equivalent): a bindless handle's - // filtering comes from the bound SAMPLER object, not the texture's - // own — now irrelevant — TEXTURE_MIN_FILTER/TEXTURE_MAG_FILTER. - _sampler = device.CreateSampler(GpuSamplerDescription.WorldClamp); - } - - /// - /// Resolves (0 = nothing rendered - /// this call, matching the producers' existing "0 = no texture" return) - /// to a texture-table slot, registering it once per distinct name. - /// - public GpuTextureSlot Resolve(uint rawGlColorTextureName) - { - if (rawGlColorTextureName == 0) - { - Release(); - return GpuTextureSlot.Unassigned; - } - if (rawGlColorTextureName == _lastRawName && _lastSlot.IsAssigned) - return _lastSlot; - - Release(); - if (_device is not Gpu.Gl.GlGpuDevice glDevice) - { - throw new NotSupportedException( - "ExternalViewportTextureBridge only supports the GL backend. " + - "Slice V4g removes this bridge before any other backend ships."); - } - _lastSlot = glDevice.RegisterExternalColorTexture(rawGlColorTextureName, _sampler); - _lastRawName = rawGlColorTextureName; - return _lastSlot; - } - - private void Release() - { - if (_lastSlot.IsAssigned) - _device.ReleaseTextureSlot(_lastSlot); - _lastSlot = GpuTextureSlot.Unassigned; - _lastRawName = 0; - } -} diff --git a/src/AcDream.App/Rendering/FlyCamera.cs b/src/AcDream.App/Rendering/FlyCamera.cs index 39cb0f76..49a5341b 100644 --- a/src/AcDream.App/Rendering/FlyCamera.cs +++ b/src/AcDream.App/Rendering/FlyCamera.cs @@ -1,9 +1,9 @@ -// src/AcDream.App/Rendering/FlyCamera.cs +// src/AcDream.App/Rendering/FlyCamera.cs using System.Numerics; namespace AcDream.App.Rendering; -internal sealed class FlyCamera : ICamera +public sealed class FlyCamera : ICamera { public Vector3 Position { get; set; } = new(96, 96, 150); public float Yaw { get; set; } = MathF.PI / 2f; // facing +Y diff --git a/src/AcDream.App/Rendering/FrustumCuller.cs b/src/AcDream.App/Rendering/FrustumCuller.cs index 39cbef0b..3c792bbb 100644 --- a/src/AcDream.App/Rendering/FrustumCuller.cs +++ b/src/AcDream.App/Rendering/FrustumCuller.cs @@ -1,13 +1,13 @@ -using System.Numerics; +using System.Numerics; namespace AcDream.App.Rendering; /// -/// Six normalized view-frustum planes extracted from a View×Projection matrix. +/// Six normalized view-frustum planes extracted from a View×Projection matrix. /// Each plane is represented as (normal.X, normal.Y, normal.Z, distance) where /// dot(normal, point) + distance >= 0 means the point is on the visible side. /// -internal readonly struct FrustumPlanes +public readonly struct FrustumPlanes { public readonly Vector4 Left; public readonly Vector4 Right; @@ -27,7 +27,7 @@ internal readonly struct FrustumPlanes } /// - /// Extracts the six frustum planes from a combined View×Projection matrix + /// Extracts the six frustum planes from a combined View×Projection matrix /// using the Gribb-Hartmann method. System.Numerics.Matrix4x4 is row-major, /// so rows are accessed directly via M{row}{col} fields. /// @@ -64,7 +64,7 @@ internal readonly struct FrustumPlanes /// /// Conservative AABB-vs-frustum culling. Zero allocations; suitable for per-frame use. /// -internal static class FrustumCuller +public static class FrustumCuller { /// /// Returns true if the axis-aligned bounding box defined by @@ -74,7 +74,7 @@ internal static class FrustumCuller /// public static bool IsAabbVisible(FrustumPlanes planes, Vector3 min, Vector3 max) { - // For each plane, test the AABB's "most-positive vertex" — + // For each plane, test the AABB's "most-positive vertex" — // the corner most in the direction of the plane normal. If // that corner is behind the plane, the entire box is outside. return TestPlane(planes.Left, min, max) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index a27f71fe..a638ba48 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Plugins; +using AcDream.Core.Plugins; using AcDream.App.Composition; using AcDream.App.Physics; using AcDream.App.Rendering.Gpu; @@ -21,7 +21,7 @@ using Silk.NET.Windowing; namespace AcDream.App.Rendering; -internal sealed class GameWindow : +public sealed class GameWindow : IDisposable, IGameWindowPlatformPublication, IGameWindowHostInputCameraPublication, @@ -70,7 +70,7 @@ internal sealed class GameWindow : private AcDream.App.Interaction.WorldSelectionQuery? _worldSelectionQuery; private AcDream.App.Interaction.SelectionInteractionController? _selectionInteractions; /// Phase N.5: ARB_bindless_texture + ARB_shader_draw_parameters - /// support. Required at startup — missing bindless throws + /// support. Required at startup — missing bindless throws /// in OnLoad. private AcDream.App.Rendering.Wb.BindlessSupport? _bindlessSupport; private SamplerCache? _samplerCache; @@ -78,14 +78,14 @@ internal sealed class GameWindow : // K-fix4 (2026-04-26): default OFF. The orange BSP / green cylinder // wireframes are noisy outdoors and confuse first-time users into // thinking they're a rendering bug. Ctrl+F2 toggles, the DebugPanel - // → Diagnostics → "Toggle collision wires" button toggles too. + // → Diagnostics → "Toggle collision wires" button toggles too. 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 - // with UiHost and reserved for the future world-space HUD (D.6 — + // with UiHost and reserved for the future world-space HUD (D.6 — // damage floaters, name plates) where ImGui can't reach into the 3D // scene. They are no longer used for any debug overlay. private TextRenderer? _textRenderer; @@ -98,7 +98,7 @@ internal sealed class GameWindow : "1", System.StringComparison.Ordinal); - // MP0 (2026-07-05): permanent frame profiler — one FrameBoundary call + // MP0 (2026-07-05): permanent frame profiler — one FrameBoundary call // per OnRender + three stage scopes. All logic lives in // AcDream.App.Diagnostics.FrameProfiler (structure rule 1). private readonly AcDream.App.Diagnostics.FrameProfiler _frameProfiler = new(); @@ -116,7 +116,6 @@ internal sealed class GameWindow : private IDisposable? _frameGraphPublication; private AcDream.App.Rendering.GpuFrameFlightController? _gpuFrameFlights; private IGpuDevice? _gpuDevice; - private AcDream.App.Rendering.GpuDeviceFrameLifetime? _gpuFrameLifetime; private readonly AcDream.App.Rendering.GameFrameGraphSlot _frameGraphs = new(); private readonly AcDream.App.Rendering.GameRenderResourceLifetime _renderResourceLifetime = new(); @@ -145,11 +144,11 @@ internal sealed class GameWindow : _localPlayerTeleport; private readonly AcDream.App.Rendering.WorldRenderRangeState _renderRange = new(nearRadius: 4, farRadius: 12); - // Phase B.3: physics engine — populated from the streaming pipeline. + // Phase B.3: physics engine — populated from the streaming pipeline. private AcDream.Core.Physics.PhysicsEngine _physicsEngine => _runtimeEntityObjects.Physics.Engine; - // Task 4: physics data cache — BSP trees + collision shapes extracted from + // Task 4: physics data cache — BSP trees + collision shapes extracted from // GfxObj/Setup dats during streaming. Populated on the worker thread; // ConcurrentDictionary inside makes cross-thread access safe. private AcDream.Core.Physics.PhysicsDataCache _physicsDataCache => @@ -186,7 +185,7 @@ internal sealed class GameWindow : private readonly CellVisibility _cellVisibility = new(); // Phase A.1 hotfix / Phase A.5 T10: DatCollection is NOT thread-safe. - // DatReaderWriter's DatBinReader uses a shared buffer position internally — + // DatReaderWriter's DatBinReader uses a shared buffer position internally — // concurrent _dats.Get calls from the streaming worker thread (T11+) and // the render thread (LandblockBuildFactory on the worker; live-spawn // handlers + animation ticks on the render @@ -227,7 +226,7 @@ internal sealed class GameWindow : // Phase U.3: the shared per-frame clip data (binding=2 mesh SSBO + terrain // UBO). In U.3 a single ClipFrame.NoClip() instance is created lazily (??=) and - // REUSED across frames — its GL buffers persist; only the cheap CPU-side no-clip + // REUSED across frames — its GL buffers persist; only the cheap CPU-side no-clip // state is re-uploaded each frame before terrain/entities draw, so the whole // scene renders ungated (identical to pre-U.3). The buffer ids are handed to the // three renderers so each re-binds binding=2 immediately before its own draw. @@ -249,7 +248,7 @@ internal sealed class GameWindow : /// /// Tier 1 cache (#53): per-entity classification results for static /// entities (those NOT in ). Conceptually - /// paired with — that dictionary is the + /// paired with — that dictionary is the /// gating predicate, this cache is the lookup that depends on it. /// Passed to at /// construction time. Tasks 9-10 of the cache plan wire the per-entity @@ -284,7 +283,7 @@ internal sealed class GameWindow : private readonly AcDream.App.Rendering.Vfx.ParticleVisibilityController _particleVisibility = new(); private readonly AcDream.App.Rendering.Vfx.EntityEffectPoseRegistry _effectPoses = new(); private AcDream.App.Rendering.Vfx.AnimationHookFrameQueue? _animationHookFrames; - // Phase 6 — retail PhysicsScript runtime. Receives PlayScript (0xF754) + // Phase 6 — retail PhysicsScript runtime. Receives PlayScript (0xF754) // and typed PlayScriptType (0xF755) events through one effect owner, then // fans every dat-defined hook to particles, audio, lights, translucency, // and nested/default-script routing at its StartTime offset. @@ -295,7 +294,7 @@ internal sealed class GameWindow : private readonly AcDream.App.Rendering.RetailAlphaQueue _retailAlphaQueue; // Remote-entity motion inference: tracks when each remote entity last // moved meaningfully. Used in TickAnimations to swap to Ready when - // position has stalled for >StopIdleMs — retail observer pattern per + // position has stalled for >StopIdleMs — retail observer pattern per // ACE Player_Tick.cs line 368: the client never sends "released forward" // MoveToState, so the server never broadcasts an explicit stop. Observer // must infer it from position deltas. @@ -363,21 +362,21 @@ internal sealed class GameWindow : /// Persisted hotbar shortcuts from the last PlayerDescription (D.5.1 toolbar source). public IReadOnlyList Shortcuts => _runtimeInventory.Shortcuts.Items; - // Issue #5 — caches CreatureProfile.{Stamina, Mana, *Max} from + // Issue #5 — caches CreatureProfile.{Stamina, Mana, *Max} from // PlayerDescription so the Vitals HUD can render those bars. - // Issue #6 — wired to SpellBook so GetMaxApprox folds enchantment + // Issue #6 — wired to SpellBook so GetMaxApprox folds enchantment // buffs into the max formula via Spellbook.GetVitalMod. public AcDream.Core.Player.LocalPlayerState LocalPlayer => _runtimeCharacter.LocalPlayer; - // Phase D.2a — ImGui devtools UI overlay. Null unless ACDREAM_DEVTOOLS=1. + // 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; private AcDream.UI.Abstractions.Panels.Chat.ChatVM? _retailChatVm; - // Phase D.2b — retained host + composition runtime. Null unless ACDREAM_RETAIL_UI=1. + // Phase D.2b — retained host + composition runtime. Null unless ACDREAM_RETAIL_UI=1. private AcDream.App.UI.UiHost? _uiHost; private AcDream.App.UI.RetailUiRuntime? _retailUiRuntime; private readonly AcDream.App.UI.RetailUiRuntimeLease _retailUiLease = new(); @@ -399,7 +398,7 @@ internal sealed class GameWindow : private AcDream.App.Spells.MagicRuntime? _magicRuntime; private MagicCatalog? _magicCatalog; private readonly AcDream.Core.Items.StackSplitQuantityState _stackSplitQuantity = new(); - // Phase D.2b Sub-phase C Slice 2 — the 3-D doll viewport: an off-screen RTT renderer, the UiViewport + // Phase D.2b Sub-phase C Slice 2 — the 3-D doll viewport: an off-screen RTT renderer, the UiViewport // widget that blits it, the inventory frame (for the open-gate), and a dirty flag (re-dress on 0xF625). private AcDream.App.Rendering.PaperdollViewportRenderer? _paperdollViewportRenderer; private AcDream.App.Rendering.PaperdollFramePresenter? _paperdollFramePresenter; @@ -407,7 +406,7 @@ internal sealed class GameWindow : _creatureAppraisalViewportRenderer; private AcDream.App.Rendering.CreatureAppraisalFramePresenter? _creatureAppraisalFramePresenter; - // Phase D.2b Task 9 — plugin UI registrations buffered before OnLoad; drained in OnLoad. + // 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 @@ -428,7 +427,7 @@ internal sealed class GameWindow : private AcDream.App.Rendering.Vfx.LiveEntityLightController? _liveEntityLights; private AcDream.App.World.LiveEntityPresentationController? _liveEntityPresentation; - // #188 — TransparentPartHook fires from the animation pipeline drive + // #188 — TransparentPartHook fires from the animation pipeline drive // a per-(entity,part) translucency ramp; WbDrawDispatcher reads it // per frame. Wired into the hook router in OnLoad, advanced once per // frame in the main loop regardless of _animatedEntities membership @@ -473,10 +472,10 @@ internal sealed class GameWindow : _localPlayerAnimation; private AcDream.App.Physics.LocalPlayerShadowSynchronizer? _localPlayerShadowSynchronizer; - // Phase D.2b-C — live character-sheet assembly + raise flow (extracted + // Phase D.2b-C — live character-sheet assembly + raise flow (extracted // feature class; GameWindow only wires it). Null unless ACDREAM_RETAIL_UI=1. private AcDream.App.UI.Layout.CharacterSheetProvider? _characterSheetProvider; - // Phase K.2 — auto-enter player mode after a successful login. Armed + // Phase K.2 — auto-enter player mode after a successful login. Armed // by LiveSessionHost's entered-world transition; ticked from // OnUpdate; disarmed if the user manually enters fly mode (or any // other path that pre-empts the chase camera). Skipped entirely @@ -485,7 +484,7 @@ internal sealed class GameWindow : // the bool here. private AcDream.App.Input.PlayerModeAutoEntry? _playerModeAutoEntry; - // Phase K.1b / Slice 8 F — one semantic input path. Transitional actions + // Phase K.1b / Slice 8 F — one semantic input path. Transitional actions // flow through GameplayInputActionRouter; held movement is polled through // InputDispatcher. Raw axis motion belongs to CameraPointerInputController. private AcDream.App.Input.SilkKeyboardSource? _kbSource; @@ -507,7 +506,7 @@ internal sealed class GameWindow : // configuration path, // falling back to the retail-faithful defaults if the file is missing // or corrupt. This is THE single source of truth for the keymap at - // startup — no other call to RetailDefaults() / AcdreamCurrentDefaults() + // startup — no other call to RetailDefaults() / AcdreamCurrentDefaults() // should land in the GameWindow construction path. private readonly AcDream.UI.Abstractions.Input.KeyBindings _keyBindings; private readonly GraphicalHostPlatformServices _platformServices; @@ -523,7 +522,7 @@ internal sealed class GameWindow : } // Phase 4.7: optional live connection to an ACE server. Enabled only when - // ACDREAM_LIVE=1 is in the environment — fully backward compatible with + // ACDREAM_LIVE=1 is in the environment — fully backward compatible with // the offline rendering pipeline. // Runtime owns the canonical session generation and transport lifetime. // The window retains only the composition handles needed for startup and @@ -539,7 +538,7 @@ internal sealed class GameWindow : // ACDREAM_LIVE=1 was set when the window came up. // Backed by RuntimeOptions.LiveMode via the _options field. /// - /// Phase 6.6/6.7: server-guid → local WorldEntity lookup so + /// Phase 6.6/6.7: server-guid → local WorldEntity lookup so /// UpdateMotion and UpdatePosition handlers can find the entity the /// server is talking about. This is the canonical materialized top-level /// projection, including live objects parked in pending landblocks; @@ -558,7 +557,7 @@ internal sealed class GameWindow : /// private IReadOnlyDictionary LastSpawns => _liveEntities?.Snapshots ?? EmptyLiveSpawnMap; - // R5-V2: the local player's CPhysicsObj stand-in — owns the player's + // R5-V2: the local player's CPhysicsObj stand-in — owns the player's // TargetManager voyeur system, stored on the exact live record so remote // entities chasing the player resolve it. Replaces the AP-79 // _playerMoveToTarget* poll fields. @@ -567,14 +566,14 @@ internal sealed class GameWindow : private EntityPhysicsHost? _playerHost => _playerHostSlot.Host; - // R5-V2: guid → per-entity IPhysicsObjHost registry (retail's + // R5-V2: guid → per-entity IPhysicsObjHost registry (retail's // CObjectMaint::GetObjectA lookup). Backs every host's GetObjectA seam, // giving the TargetManager voyeur round-trip its cross-entity delivery // path. Populated for remotes plus the PlayerModeController local entry // (player); pruned only by logical LiveEntityRuntime teardown. // ServerControlledVelocityStaleSeconds moved to RemotePhysicsUpdater (#184 - // Slice 2a — the DR tick's stale-velocity anim-stop was its only user). + // Slice 2a — the DR tick's stale-velocity anim-stop was its only user). public GameWindow( AcDream.App.RuntimeOptions options, WorldGameState worldGameState, @@ -685,7 +684,7 @@ internal sealed class GameWindow : var options = WindowOptions.Default with { Size = new Vector2D(1280, 720), - Title = "acdream — phase 1", + Title = "acdream — phase 1", API = new GraphicsAPI( ContextAPI.OpenGL, ContextProfile.Core, @@ -749,10 +748,6 @@ internal sealed class GameWindow : IGpuDevice value) => PublishCompositionOwner(ref _gpuDevice, value, "GPU device (RHI)"); - void IGameWindowHostInputCameraPublication.PublishGpuFrameLifetime( - GpuDeviceFrameLifetime value) => - PublishCompositionOwner(ref _gpuFrameLifetime, value, "GPU frame lifetime"); - void IGameWindowHostInputCameraPublication.PublishKeyboardSource( AcDream.App.Input.SilkKeyboardSource value) => PublishCompositionOwner(ref _kbSource, value, "keyboard source"); @@ -1308,7 +1303,6 @@ internal sealed class GameWindow : _worldEnvironment, _renderResourceLifetime, _gpuFrameFlights!, - _gpuDevice!, _options.ResidencyBudgets, initialCenterLandblockId, _applicationPaths.DiagnosticsDirectory, @@ -1328,10 +1322,9 @@ internal sealed class GameWindow : new InteractionRetainedUiDependencies( _options, platformResult.Graphics, - hostInputCamera.GpuDevice, - () => hostInputCamera.GpuFrameLifetime.Current, _window!, platformResult.Input, + worldRender.Foundation.ShadersDirectory, contentEffectsAudio.Dats, _datLock, worldRender.Foundation.TextureCache, @@ -1381,7 +1374,6 @@ internal sealed class GameWindow : new LivePresentationDependencies( _options, platformResult.Graphics, - _gpuDevice!, _window!, _datLock, _runtimeSettings, @@ -1549,7 +1541,7 @@ internal sealed class GameWindow : _frameGraphs.Tick(new AcDream.App.Update.UpdateFrameInput(dt)); } - // Performance overlay state — updated every ~0.5s and written to the + // Performance overlay state — updated every ~0.5s and written to the // window title so there's zero rendering cost (no font/overlay needed). private void OnRender(double deltaSeconds) { @@ -1565,20 +1557,20 @@ internal sealed class GameWindow : // IsEntityCurrentlyMoving REMOVED (2026-07-09): it powered a cache-bypass // narrowing that dropped settled-open doors / faded-out walls back onto the // stale Tier-1 rest-pose cache entry (the door/fade "flip-back"). See the - // animatedIds build site — every Sequencer entity is now added + // animatedIds build site — every Sequencer entity is now added // unconditionally, which is the known-good pre-optimization behavior. - // R3-W6: UpdatePlayerAnimation DELETED — the player's sequencer is + // R3-W6: UpdatePlayerAnimation DELETED — the player's sequencer is // driven through the SAME MotionTableDispatchSink/DefaultSink funnel // remotes use (edge-driven DoMotion/StopMotion/set_hold_run in // PlayerMovementController; airborne-Falling falls out of // contact_allows_move + apply_current_movement). The #45 sidestep - // 1.248x factor + ACDREAM_ANIM_SPEED_SCALE died with it — + // 1.248x factor + ACDREAM_ANIM_SPEED_SCALE died with it — // EXPECTED-DIFF: local sidestep pacing now matches how remotes have // always played (w6-cutover-map.md R3). /// - /// L.0 Display tab: framebuffer-resize handler — update GL viewport + /// L.0 Display tab: framebuffer-resize handler — update GL viewport /// + camera aspect when the window is resized (by the user dragging /// the corner OR by the runtime display target applying a saved /// Resolution). Without this, the viewport stays pinned at the diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuDevice.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuDevice.cs index 3a681b41..b416b597 100644 --- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuDevice.cs +++ b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuDevice.cs @@ -190,26 +190,9 @@ internal sealed class GlGpuDevice : IGpuDevice string fragmentPath = Path.Combine(_shadersDirectory, $"{description.Shaders.Name}.frag"); string vertexSource = File.ReadAllText(vertexPath); string fragmentSource = File.ReadAllText(fragmentPath); - - // Every RHI-created pipeline gets the same common.glsl preamble the - // legacy Shader(gl, vert, frag, includeCommonPreamble: true) call - // sites already opt into (mesh_modern, terrain_modern): the set-0 - // binding-9 texture-table buffer + ACDREAM_TEXTURE_HANDLE macro that - // slice V2 introduced. A pipeline whose shaders never reference the - // macro simply carries an unused SSBO declaration — harmless — so - // there is no reason for callers to opt in per pipeline. - string commonSource = CommonPreambleSource; - vertexSource = Shader.InjectPreamble(vertexSource, commonSource); - fragmentSource = Shader.InjectPreamble(fragmentSource, commonSource); - return new GlGpuPipeline(_gl, Retirement, description, vertexSource, fragmentSource); } - private string? _commonPreambleSource; - - private string CommonPreambleSource => _commonPreambleSource ??= - File.ReadAllText(Path.Combine(_shadersDirectory, "common.glsl")); - public IGpuRenderTarget CreateRenderTarget(in GpuRenderTargetDescription description) { ThrowIfDisposed(); @@ -232,33 +215,6 @@ internal sealed class GlGpuDevice : IGpuDevice return new GpuTextureSlot(slot); } - /// - /// Registers an externally-created, externally-owned GL texture name - /// (a private-viewport FBO colour attachment) into the same texture table - /// uses. Not part of - /// — PrivateEntityViewportRenderer/PaperdollViewportRenderer - /// still allocate their own FBO textures directly on GL pre-V4g, so - /// their retained-UI consumers () need a bridge - /// that does not require the caller to own an . - /// The caller keeps deleting the GL name itself; this only ever writes - /// the bindless handle into the table and returns the slot. Deleted with - /// the GL backend once slice V4g moves those renderers onto - /// and they can call - /// like every other consumer. - /// - internal GpuTextureSlot RegisterExternalColorTexture(uint glTextureName, IGpuSampler sampler) - { - ThrowIfDisposed(); - ArgumentNullException.ThrowIfNull(sampler); - if (sampler is not GlGpuSampler glSampler) - throw new ArgumentException("The GL backend can only register a GL sampler.", nameof(sampler)); - - uint slot = _textureSlotAllocator.Allocate(); - ulong handle = _bindless.GetResidentHandle(glTextureName, glSampler.GlName); - WriteHandle(slot, handle); - return new GpuTextureSlot(slot); - } - public void ReleaseTextureSlot(GpuTextureSlot slot) { ThrowIfDisposed(); @@ -283,17 +239,6 @@ internal sealed class GlGpuDevice : IGpuDevice long serial = ++_nextSerial; int slot = _frameFlights.CurrentSlot; _ringStates[slot].Reset(); - // Campaign V slice V4a: during the migration, every still-legacy - // renderer (WbDrawDispatcher, terrain, particles, EnvCellRenderer, ...) - // mutates real GL program/blend/depth/cull state directly, outside this - // device's ApplyRenderState. _renderState has no way to observe those - // calls, so its cached "current" snapshot goes stale the moment any of - // them runs between two RHI binds. Resetting once per frame is the same - // defensive move BeginPass already makes after a forced clear (see the - // comment there) — it costs one redundant state application on the - // frame's first BindPipeline, in exchange for never diffing against a - // baseline the driver has since moved past. - _renderState.Reset(); return new GlGpuFrame(this, slot, serial); } @@ -338,17 +283,6 @@ internal sealed class GlGpuDevice : IGpuDevice _textureHandleTable.AsSpan(tableStart, tableEnd - tableStart)); _textureTableBuffer.Upload((long)tableStart * sizeof(ulong), bytes); } - - // Bind the device's own table to binding 9 immediately before every - // RHI-issued draw. Storage-buffer bindings are global GL context - // state, and the still-unmigrated legacy renderers (WbDrawDispatcher, - // EnvCellRenderer, TerrainModernRenderer, ParticleRenderer) rebind - // their OWN interim per-renderer table to the same binding right - // before their own draws (slice V2's documented pattern) — so this - // device must re-claim binding 9 before ITS draws too, or an RHI - // draw issued after a legacy draw in the same frame would read the - // wrong table. - _gl.BindBufferBase(GLEnum.ShaderStorageBuffer, GpuBindingModel.StorageTextureTable, _textureTableBuffer.GlName); } internal void BeginPass(GpuPassDescription description) @@ -461,24 +395,6 @@ internal sealed class GlGpuDevice : IGpuDevice { _gl.ColorMask(desired.ColorWrite, desired.ColorWrite, desired.ColorWrite, desired.ColorWrite); } - if (changes.Multisample) - { - // Vulkan bakes multisample rasterization into the pipeline via its - // declared sample count — there is no separate enable bit. The GL - // backend mirrors that: GL_MULTISAMPLE tracks - // GpuPipelineDescription.SampleCount rather than being left at - // whatever the previous pass happened to leave it. This matters - // because the default framebuffer can itself be multisampled - // (QualitySettings MSAA): a single-sample UI pipeline binding - // while GL_MULTISAMPLE is still enabled from an earlier world - // pass converts each glyph's soft alpha edge into dithered MSAA - // coverage instead of a clean alpha blend (the pre-RHI - // TextRenderer explicitly disabled it for the same reason). - if (desired.Multisample) - _gl.Enable(EnableCap.Multisample); - else - _gl.Disable(EnableCap.Multisample); - } GLHelpers.ThrowOnResourceError(_gl, "apply GL render state"); } diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPassEncoder.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPassEncoder.cs index 819f8e6d..f3ac77c8 100644 --- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPassEncoder.cs +++ b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPassEncoder.cs @@ -51,8 +51,7 @@ internal sealed class GlGpuPassEncoder : IGpuPassEncoder description.Cull, description.FrontFace, description.AlphaToCoverage, - description.ColorWrite, - Multisample: description.SampleCount > 1); + description.ColorWrite); _device.ApplyRenderState(desired); _gl.BindVertexArray(p.GlVertexArray); diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlRenderStateCache.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlRenderStateCache.cs index 5414a9a5..846f8d35 100644 --- a/src/AcDream.App/Rendering/Gpu/Gl/GlRenderStateCache.cs +++ b/src/AcDream.App/Rendering/Gpu/Gl/GlRenderStateCache.cs @@ -15,8 +15,7 @@ internal readonly record struct GlRenderStateSnapshot( GpuCullMode Cull, GpuFrontFace FrontFace, bool AlphaToCoverage, - bool ColorWrite, - bool Multisample); + bool ColorWrite); /// Which GL state calls are needed to move from the previous snapshot to the new one. internal readonly record struct GlRenderStateChanges( @@ -28,15 +27,14 @@ internal readonly record struct GlRenderStateChanges( bool Cull, bool FrontFace, bool AlphaToCoverage, - bool ColorWrite, - bool Multisample) + bool ColorWrite) { public bool AnyChange => Program || Blend || DepthTest || DepthWrite || DepthCompare - || Cull || FrontFace || AlphaToCoverage || ColorWrite || Multisample; + || Cull || FrontFace || AlphaToCoverage || ColorWrite; /// Every dimension reported changed — used for the first apply after a reset. - internal static GlRenderStateChanges All { get; } = new(true, true, true, true, true, true, true, true, true, true); + internal static GlRenderStateChanges All { get; } = new(true, true, true, true, true, true, true, true, true); } /// @@ -70,8 +68,7 @@ internal sealed class GlRenderStateCache p.Cull != desired.Cull, p.FrontFace != desired.FrontFace, p.AlphaToCoverage != desired.AlphaToCoverage, - p.ColorWrite != desired.ColorWrite, - p.Multisample != desired.Multisample); + p.ColorWrite != desired.ColorWrite); } /// Discards the cached baseline — the next reports every dimension changed. diff --git a/src/AcDream.App/Rendering/ICamera.cs b/src/AcDream.App/Rendering/ICamera.cs index 16c3a237..3aeaf987 100644 --- a/src/AcDream.App/Rendering/ICamera.cs +++ b/src/AcDream.App/Rendering/ICamera.cs @@ -1,8 +1,8 @@ -using System.Numerics; +using System.Numerics; namespace AcDream.App.Rendering; -internal interface ICamera +public interface ICamera { Matrix4x4 View { get; } Matrix4x4 Projection { get; } diff --git a/src/AcDream.App/Rendering/ICameraCollisionProbe.cs b/src/AcDream.App/Rendering/ICameraCollisionProbe.cs index de609d66..2ad67136 100644 --- a/src/AcDream.App/Rendering/ICameraCollisionProbe.cs +++ b/src/AcDream.App/Rendering/ICameraCollisionProbe.cs @@ -1,14 +1,14 @@ -using System.Numerics; +using System.Numerics; namespace AcDream.App.Rendering; /// /// Result of a camera spring-arm sweep: the collided eye position AND the cell the swept /// viewer-sphere ended in (retail viewer_cell = sphere_path.curr_cell, update_viewer -/// pc:92871). The cell is graph-tracked by the transition — no AABB, no grace frames — so it +/// pc:92871). The cell is graph-tracked by the transition — no AABB, no grace frames — so it /// is the robust per-frame "which cell is the camera in?" answer that roots the render. /// -internal readonly record struct CameraSweepResult(Vector3 Eye, uint ViewerCellId); +public readonly record struct CameraSweepResult(Vector3 Eye, uint ViewerCellId); /// /// Sweeps a small sphere from the camera pivot (player head) toward the @@ -16,7 +16,7 @@ internal readonly record struct CameraSweepResult(Vector3 Eye, uint ViewerCellId /// lets collide its eye without depending on /// the physics engine directly (and stay unit-testable with a fake). /// -internal interface ICameraCollisionProbe +public interface ICameraCollisionProbe { /// /// Roll a collision sphere from to diff --git a/src/AcDream.App/Rendering/IndoorDrawPlan.cs b/src/AcDream.App/Rendering/IndoorDrawPlan.cs index 6696f09a..a3aab2a2 100644 --- a/src/AcDream.App/Rendering/IndoorDrawPlan.cs +++ b/src/AcDream.App/Rendering/IndoorDrawPlan.cs @@ -1,18 +1,18 @@ -// IndoorDrawPlan.cs +// IndoorDrawPlan.cs // // Pure (GL-free) port of the membership half of retail PView::DrawCells (0x5a4840): // the reverse cell_draw_list iterated per portal_view slice. EVERY visible cell with a -// non-empty view is included — there is NO "drawable" filter. Dropping cells without a -// clip-slot was the grey-walls bug (the cell's sealed shell never drew → clear color showed). +// non-empty view is included — there is NO "drawable" filter. Dropping cells without a +// clip-slot was the grey-walls bug (the cell's sealed shell never drew → clear color showed). using System.Collections.Generic; namespace AcDream.App.Rendering; -internal readonly record struct CellDrawEntry(uint CellId, IReadOnlyList Slices); +public readonly record struct CellDrawEntry(uint CellId, IReadOnlyList Slices); -internal static class IndoorDrawPlan +public static class IndoorDrawPlan { - /// Reverse OrderedVisibleCells (far→near), each visible cell with its view + /// Reverse OrderedVisibleCells (far→near), each visible cell with its view /// slices. Mirrors DrawCells' shell/object loops. Cells whose view is empty are skipped /// (they are not actually visible); no other cell is ever dropped. public static List ShellPass(PortalVisibilityFrame frame) diff --git a/src/AcDream.App/Rendering/InteriorEntityPartition.cs b/src/AcDream.App/Rendering/InteriorEntityPartition.cs index ad57505e..08da6b15 100644 --- a/src/AcDream.App/Rendering/InteriorEntityPartition.cs +++ b/src/AcDream.App/Rendering/InteriorEntityPartition.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Numerics; using AcDream.Core.World; @@ -8,27 +8,27 @@ namespace AcDream.App.Rendering; /// Splits a frame's landblock entities into the draw buckets used by the /// retail-style DrawInside flood. /// -/// T1 (fused BR-2/3, 2026-06-11) — retail draw-order contract: the +/// T1 (fused BR-2/3, 2026-06-11) — retail draw-order contract: the /// frame draws STATIC world first (terrain, building shells, scenery, then /// flooded interior cells + their static object lists), and every DYNAMIC /// (server-spawned: player, NPCs, doors, items) draws LAST, depth-tested, -/// never hard-clipped. This is what makes the aperture depth punch safe — +/// never hard-clipped. This is what makes the aperture depth punch safe — /// when the punch erases depth inside a doorway, no dynamic has been drawn /// yet, so nothing visible is destroyed (retail: objects draw per cell AFTER /// cells, PView::DrawCells epilogue Ghidra 0x005a4840; the first BR-2 attempt /// punched after dynamics and erased the player, reverted 88be519). /// /// -/// — indoor STATICS (dat-baked, ServerGuid==0) +/// — indoor STATICS (dat-baked, ServerGuid==0) /// per visible cell, drawn with their cell. -/// — outdoor statics (building +/// — outdoor statics (building /// shells, scenery stabs), drawn with the world/landscape pass. -/// — ALL server-spawned entities +/// — ALL server-spawned entities /// (ServerGuid != 0) regardless of cell, plus unresolved-cell live entities; /// drawn in the frame's single LAST entity pass. /// /// -internal static class InteriorEntityPartition +public static class InteriorEntityPartition { internal enum ProjectionClass : byte { @@ -51,13 +51,13 @@ internal static class InteriorEntityPartition void AbortFrame(); } - internal sealed class Result + public sealed class Result { public Dictionary> ByCell { get; } = new(); public List OutdoorStatic { get; } = new(); public List Dynamics { get; } = new(); - // MP-Alloc: scratch for PruneEmptyCellBuckets — reused across frames + // MP-Alloc: scratch for PruneEmptyCellBuckets — reused across frames // so pruning itself doesn't allocate. private readonly List _emptyCellScratch = new(); @@ -82,7 +82,7 @@ internal static class InteriorEntityPartition /// entries (either newly emptied, or a leftover key from a previous /// frame's visible-cell set that this frame never touched). Keeps /// ByCell.Count / .Keys bit-identical to the old always-fresh- - /// Dictionary behavior — callers that inspect key presence/count + /// Dictionary behavior — callers that inspect key presence/count /// directly (not just TryGetValue) must see exactly the cells that /// actually received at least one static this frame. /// @@ -100,7 +100,7 @@ internal static class InteriorEntityPartition } /// - /// Allocating overload — always returns a brand-new . + /// Allocating overload — always returns a brand-new . /// Kept for tests and any one-shot caller; the per-frame render path /// uses the /// reuse overload instead (see ). @@ -121,7 +121,7 @@ internal static class InteriorEntityPartition /// in place (see ) and refills it, /// reusing each cell's existing List<WorldEntity> when the /// cell key survives from the previous frame instead of allocating a new - /// one — the per-cell dictionary entries persist across frames (cleared, + /// one — the per-cell dictionary entries persist across frames (cleared, /// never removed) since the visible-cell set is usually stable frame to /// frame. Identical partitioning output to the allocating overload; only /// the backing storage is reused. @@ -141,7 +141,7 @@ internal static class InteriorEntityPartition if (e.MeshRefs.Count == 0) continue; // Retail contract: every server-spawned entity is a DYNAMIC - // and draws in the last pass — indoor, outdoor, or unresolved. + // and draws in the last pass — indoor, outdoor, or unresolved. if (e.ServerGuid != 0) { result.Dynamics.Add(e); @@ -237,7 +237,7 @@ internal static class InteriorEntityPartition } } - /// Shared indoor classification — keep DrawDynamicsLast, the + /// Shared indoor classification — keep DrawDynamicsLast, the /// outside-stage assignment (#118), and the partition in lockstep. public static bool IsIndoorCellId(uint cellId) { diff --git a/src/AcDream.App/Rendering/NdcScissorRect.cs b/src/AcDream.App/Rendering/NdcScissorRect.cs index f764335a..f26eb0c6 100644 --- a/src/AcDream.App/Rendering/NdcScissorRect.cs +++ b/src/AcDream.App/Rendering/NdcScissorRect.cs @@ -1,29 +1,29 @@ -// NdcScissorRect.cs +// NdcScissorRect.cs // -// NDC AABB → framebuffer-pixel scissor box, CONSERVATIVE (outer bound). +// NDC AABB → framebuffer-pixel scissor box, CONSERVATIVE (outer bound). // The scissor that brackets a landscape/doorway slice is a fallback BOUND on // the slice's view region (AD-17 in the divergence register): it must CONTAIN // every fragment the per-fragment plane clip would keep. Under-inclusion is -// the bug class — the #130 doorway top-edge background strip was this box +// the bug class — the #130 doorway top-edge background strip was this box // computed as Floor(origin) + Ceiling(size), whose far edge -// floor(min)+ceil(max−min) lands up to one pixel SHORT of the true max edge +// floor(min)+ceil(max−min) lands up to one pixel SHORT of the true max edge // at unlucky fractional alignments, scissoring away the aperture's top/right // pixel row for the whole slice (sky, terrain, statics, weather) while the -// seal still stamps it — a strip of clear color no later pass can fill. +// seal still stamps it — a strip of clear color no later pass can fill. // // Correct outer bound: floor both mins, ceil both maxes, width = difference. // A fragment at pixel (i,j) rasterizes iff its CENTER (i+0.5, j+0.5) lies in -// the region ⊆ the NDC box [X0,X1]×[Y0,Y1] (pixel units). Center-inside ⇒ -// i ≥ X0−0.5 ⇒ i ≥ floor(X0) and i ≤ X1−0.5 ⇒ i < ceil(X1). So +// the region ⊆ the NDC box [X0,X1]×[Y0,Y1] (pixel units). Center-inside ⇒ +// i ≥ X0−0.5 ⇒ i ≥ floor(X0) and i ≤ X1−0.5 ⇒ i < ceil(X1). So // [floor(X0), ceil(X1)) admits every center-inside pixel, over-including by -// at most one pixel per edge — safe per AD-17's doctrine (the wall shell / +// at most one pixel per edge — safe per AD-17's doctrine (the wall shell / // plane clip repaints or kills the surplus). using System; using System.Numerics; namespace AcDream.App.Rendering; -internal static class NdcScissorRect +public static class NdcScissorRect { /// Convert an NDC AABB (minX, minY, maxX, maxY in [-1,1]) to a /// framebuffer-pixel scissor box that CONTAINS it. Inputs are clamped to diff --git a/src/AcDream.App/Rendering/OrbitCamera.cs b/src/AcDream.App/Rendering/OrbitCamera.cs index 1316222b..358dc9c2 100644 --- a/src/AcDream.App/Rendering/OrbitCamera.cs +++ b/src/AcDream.App/Rendering/OrbitCamera.cs @@ -2,7 +2,7 @@ namespace AcDream.App.Rendering; -internal sealed class OrbitCamera : ICamera +public sealed class OrbitCamera : ICamera { public Vector3 Target { get; set; } = new(96, 96, 0); // center of a 192x192 landblock public float Distance { get; set; } = 300f; diff --git a/src/AcDream.App/Rendering/OutdoorCellNode.cs b/src/AcDream.App/Rendering/OutdoorCellNode.cs index 7bbec56c..13ae4d0d 100644 --- a/src/AcDream.App/Rendering/OutdoorCellNode.cs +++ b/src/AcDream.App/Rendering/OutdoorCellNode.cs @@ -1,24 +1,24 @@ -using System.Numerics; +using System.Numerics; namespace AcDream.App.Rendering; /// -/// Factory for the OUTDOOR render root — the cell the render roots at when the camera eye is outdoors. -/// Retail roots every in-world frame at viewer_cell (SmartBox::RenderNormalMode → +/// Factory for the OUTDOOR render root — the cell the render roots at when the camera eye is outdoors. +/// Retail roots every in-world frame at viewer_cell (SmartBox::RenderNormalMode → /// DrawInside(viewer_cell), decomp:92635); when outdoors that is a CLandCell. acdream models it /// as a portal-less carrying only (so -/// seeds OutsideView FULL-SCREEN → terrain/sky/scenery draw +/// seeds OutsideView FULL-SCREEN → terrain/sky/scenery draw /// as the root's shell) and . /// /// R-A2 (2026-06-08): the node no longer carries reverse portals into nearby buildings. Retail -/// does NOT flood buildings from the land root — buildings flood SEPARATELY, per-building, during the -/// landscape draw (terrain BSP → DrawPortal → ConstructView(CBldPortal), decomp:326881/433895/433827). +/// does NOT flood buildings from the land root — buildings flood SEPARATELY, per-building, during the +/// landscape draw (terrain BSP → DrawPortal → ConstructView(CBldPortal), decomp:326881/433895/433827). /// acdream issues those via per nearby /// building inside . The pre-R-A2 design flooded all /// buildings from one root through reverse portals, coupling their interior membership to a single -/// root-level portal-side test that oscillated as the chase eye grazed a doorway — the indoor flap. +/// root-level portal-side test that oscillated as the chase eye grazed a doorway — the indoor flap. /// -internal static class OutdoorCellNode +public static class OutdoorCellNode { public static LoadedCell Build(uint outdoorCellId) => new LoadedCell { diff --git a/src/AcDream.App/Rendering/PaperdollFramePresenter.cs b/src/AcDream.App/Rendering/PaperdollFramePresenter.cs index 4ddfcb0e..33883803 100644 --- a/src/AcDream.App/Rendering/PaperdollFramePresenter.cs +++ b/src/AcDream.App/Rendering/PaperdollFramePresenter.cs @@ -191,16 +191,13 @@ internal sealed class RetailPaperdollFrameView : IPaperdollFrameView { private readonly UiViewport _viewport; private readonly IPaperdollInventoryVisibility _inventory; - private readonly ExternalViewportTextureBridge _textureBridge; public RetailPaperdollFrameView( UiViewport viewport, - IPaperdollInventoryVisibility inventory, - IGpuDevice gpuDevice) + IPaperdollInventoryVisibility inventory) { _viewport = viewport ?? throw new ArgumentNullException(nameof(viewport)); _inventory = inventory ?? throw new ArgumentNullException(nameof(inventory)); - _textureBridge = new ExternalViewportTextureBridge(gpuDevice); } public bool TryGetVisibleSize(out int width, out int height) @@ -218,7 +215,7 @@ internal sealed class RetailPaperdollFrameView : IPaperdollFrameView } public void SetTextureHandle(uint textureHandle) => - _viewport.TextureHandle = _textureBridge.Resolve(textureHandle); + _viewport.TextureHandle = textureHandle; } /// Narrow visibility adapter for the paperdoll's inventory host. diff --git a/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs b/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs index f0f168e3..b01395d3 100644 --- a/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs +++ b/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs @@ -1,4 +1,4 @@ -using AcDream.App.Rendering.Wb; +using AcDream.App.Rendering.Wb; using AcDream.App.UI; using AcDream.Core.Lighting; using AcDream.Core.World; @@ -10,7 +10,7 @@ namespace AcDream.App.Rendering; /// Paperdoll-specific facade over the shared private creature viewport. The /// fixed camera remains the verbatim retail gmPaperDollUI camera. /// -internal sealed class PaperdollViewportRenderer : +public sealed class PaperdollViewportRenderer : IUiViewportRenderer, IPaperdollDollRenderer, IDisposable diff --git a/src/AcDream.App/Rendering/ParticleRenderer.cs b/src/AcDream.App/Rendering/ParticleRenderer.cs index ecfa0ca8..81657d9a 100644 --- a/src/AcDream.App/Rendering/ParticleRenderer.cs +++ b/src/AcDream.App/Rendering/ParticleRenderer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using System.Runtime.CompilerServices; @@ -22,7 +22,7 @@ namespace AcDream.App.Rendering; /// compositing order is shared with ordinary translucent GfxObj parts. Sky and /// sealed off-screen passes retain their independent immediate path. /// -internal sealed unsafe class ParticleRenderer : IDisposable +public sealed unsafe class ParticleRenderer : IDisposable { // The texture is per instance through GL_ARB_bindless_texture. Only blend // state remains a draw-call boundary, so stable retail distance order no @@ -69,8 +69,8 @@ internal sealed unsafe class ParticleRenderer : IDisposable /// /// Vertex-instance ABI shared with particle.vert. Campaign V slice V2c /// (2026-07-27): TextureHandleLow/High (the split halves of a raw 64-bit - /// ARB_bindless_texture handle) became one TextureIndex — a slot into the - /// binding=9 handle table — so ordered particles using different textures + /// ARB_bindless_texture handle) became one TextureIndex — a slot into the + /// binding=9 handle table — so ordered particles using different textures /// still remain one instanced draw when their blend mode matches. /// [StructLayout(LayoutKind.Sequential)] @@ -121,8 +121,8 @@ internal sealed unsafe class ParticleRenderer : IDisposable // Campaign V slice V2c (2026-07-27): GL-only emulation of the eventual // Vulkan global texture descriptor array (binding=9, - // GpuBindingModel.StorageTextureTable). Owns its own table — see - // GlBindlessHandleTable's doc comment and the campaign doc's §5.2 for why + // GpuBindingModel.StorageTextureTable). Owns its own table — see + // GlBindlessHandleTable's doc comment and the campaign doc's §5.2 for why // particles don't share WbDrawDispatcher's/EnvCellRenderer's/ // TerrainModernRenderer's tables. There is no automated pixel-gate // coverage for particles (the offline gate's fixed outdoor view has none @@ -1157,7 +1157,7 @@ internal sealed unsafe class ParticleRenderer : IDisposable _gl.VertexAttribPointer(5, 4, VertexAttribPointerType.Float, false, instanceStride, (void*)(12 * sizeof(float))); _gl.VertexAttribDivisor(5, 1); // Campaign V slice V2c: one uint table slot (was uvec2 low/high - // handle halves) — BillboardGpuInstance shrank by 4 bytes. + // handle halves) — BillboardGpuInstance shrank by 4 bytes. _gl.EnableVertexAttribArray(6); _gl.VertexAttribIPointer( 6, diff --git a/src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs b/src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs index c2fb411c..23662168 100644 --- a/src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs +++ b/src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.Core.Physics; namespace AcDream.App.Rendering; @@ -12,7 +12,7 @@ namespace AcDream.App.Rendering; /// cell walls (FindEnvCollisions) AND outdoor/baked GfxObj shells /// (FindObjCollisions) in one faithful path. /// -internal sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe +public sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe { /// Retail viewer_sphere radius (acclient :93314). public const float ViewerSphereRadius = 0.3f; @@ -23,13 +23,13 @@ internal sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe public CameraSweepResult SweepEye(Vector3 pivot, Vector3 desiredEye, uint cellId, uint selfEntityId, Vector3 playerPos) { - // update_viewer: player->cell == 0 → set_viewer(player_pos, 1), viewer_cell = null - // (acclient_2013_pseudo_c.txt:92775). No cell to sweep against → snap to the player. + // update_viewer: player->cell == 0 → set_viewer(player_pos, 1), viewer_cell = null + // (acclient_2013_pseudo_c.txt:92775). No cell to sweep against → snap to the player. if (cellId == 0) return new CameraSweepResult(playerPos, 0u); // === Start cell (pc:92824-92844) === // Indoor (objcell_id >= 0x100): seat the sweep's start cell at the head-PIVOT via - // CPhysicsObj::AdjustPosition (pc:92832) — the head can sit in a different cell than + // CPhysicsObj::AdjustPosition (pc:92832) — the head can sit in a different cell than // the feet (the cellar lip: feet in the low connector, head up at floor level). On // failure retail falls back to player->cell. Outdoor: cell = player->cell (no AdjustPosition). uint startCell = cellId; @@ -39,10 +39,10 @@ internal sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe if (found) startCell = pivotCell; } - // === Sweep the viewer_sphere pivot → sought-eye from the start cell (pc:92860-92868) === + // === Sweep the viewer_sphere pivot → sought-eye from the start cell (pc:92860-92868) === // SpherePath.InitPath puts sphere0's center at pathPos + (0,0,radius) (the player // foot-capsule convention). Retail's viewer_sphere center is (0,0,0), so shift the - // path DOWN by the radius to make the SPHERE CENTER travel pivot→eye, then add it back. + // path DOWN by the radius to make the SPHERE CENTER travel pivot→eye, then add it back. Vector3 begin = ToSpherePath(pivot, ViewerSphereRadius); Vector3 end = ToSpherePath(desiredEye, ViewerSphereRadius); @@ -59,7 +59,7 @@ internal sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe // Retail SmartBox::update_viewer calls init_object(player, 0x5c) = // IsViewer | PathClipped | FreeRotate | PerfectClip (acclient // pseudo-C :92864; enum TransitionTypes.cs:24-33). PathClipped makes - // the sweep HARD-STOP at first contact (TransitionTypes.cs:811) — the + // the sweep HARD-STOP at first contact (TransitionTypes.cs:811) — the // spring-arm pull-in, not the player's edge-slide. IsViewer lets the // eye pass through creatures, colliding only with world geometry // (CollisionExemption.cs:83-85). FreeRotate/PerfectClip are no-ops in @@ -95,7 +95,7 @@ internal sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe // === Fallback 1 (pc:92878-92883): AdjustPosition at the sought eye === // The sweep found no valid position; try to seat the eye at its own cell. - // (Seed with the player cell — acdream's camera doesn't track the sought-eye's + // (Seed with the player cell — acdream's camera doesn't track the sought-eye's // cell separately; the eye is near the player so its stab-list is the right one.) var (eyeCell, eyeFound) = _physics.AdjustPosition(cellId, desiredEye); if (eyeFound) return new CameraSweepResult(desiredEye, eyeCell); @@ -104,11 +104,11 @@ internal sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe return new CameraSweepResult(playerPos, 0u); } - /// Eye/pivot point → InitPath path point (subtract the sphere-center offset). + /// Eye/pivot point → InitPath path point (subtract the sphere-center offset). internal static Vector3 ToSpherePath(Vector3 spherePoint, float radius) => spherePoint - new Vector3(0f, 0f, radius); - /// InitPath path point → eye point (add the sphere-center offset back). + /// InitPath path point → eye point (add the sphere-center offset back). internal static Vector3 FromSpherePath(Vector3 pathPoint, float radius) => pathPoint + new Vector3(0f, 0f, radius); } diff --git a/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs b/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs index 9565044e..8de3bdaa 100644 --- a/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs +++ b/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Numerics; using AcDream.App.Rendering.Wb; @@ -8,15 +8,15 @@ namespace AcDream.App.Rendering; /// /// BR-2 (holistic building-render port): retail's invisible portal depth -/// writes — the port of D3DPolyRender::DrawPortalPolyInternal +/// writes — the port of D3DPolyRender::DrawPortalPolyInternal /// (Ghidra 0x0059bc90, pc:424490). /// /// Wired by T1 (BR-3, `579c8b0`): seal on interior roots, punch /// on outdoor / look-in roots, via RetailPViewPassExecutor.DrawPortalDepthWrite -/// (the DrawExitPortalMasks slice callback) — safe alongside the +/// (the DrawExitPortalMasks slice callback) — safe alongside the /// dynamics-drawn-LAST frame order (the first BR-2 attempt punched after /// dynamics and erased the player; reverted 88be519). #117 (2026-06-11) -/// added the two-pass stencil depth gate on the punch side — see +/// added the two-pass stencil depth gate on the punch side — see /// . /// /// Retail projects a portal polygon, software-clips it against the @@ -25,12 +25,12 @@ namespace AcDream.App.Rendering; /// /// Seal (retail maxZ2=6, bit0 clear, data 0x00820e14): /// z = the polygon's true projected depth. Drawn on portals leading OUTSIDE -/// (other_cell_id==0xFFFF) after the landscape pass — terrain seen +/// (other_cell_id==0xFFFF) after the landscape pass — terrain seen /// through a doorway keeps its pixels because farther interior geometry /// z-fails inside the aperture (PView::DrawCells loop 1, Ghidra 0x005a4840, /// pc:432783-432786). /// Punch (retail maxZ1=7, bit0 set, data 0x00820e18): -/// z forced to the far plane (0.99999988) — erases depth inside a building +/// z forced to the far plane (0.99999988) — erases depth inside a building /// aperture so the interior cells drawn next land cleanly /// (ConstructView(CBldPortal) mode-1, pc:433827). BR-2 commit 2 wires this /// side. @@ -38,7 +38,7 @@ namespace AcDream.App.Rendering; /// /// Where retail clips the polygon on the CPU against the view, we apply /// the SAME view region via gl_ClipDistance from the slice's clip-space -/// half-planes (≤8, the validated output) — the +/// half-planes (≤8, the validated output) — the /// depth write lands only inside the slice region, matching retail's clipped /// fan. /// @@ -46,7 +46,7 @@ namespace AcDream.App.Rendering; /// sets everything it depends on, restores the frame-global convention on /// exit, no early-outs between set and restore. /// -internal sealed class PortalDepthMaskRenderer : IDisposable +public sealed class PortalDepthMaskRenderer : IDisposable { private const string VertSrc = @"#version 430 core layout(location = 0) in vec3 aPos; @@ -215,28 +215,28 @@ void main() { } // depth-only: color writes are masked off by the caller state /// /// #117 (2026-06-11): the mark-pass depth bias, in NDC, toward the /// viewer. Retail's punch is DEPTHTEST_ALWAYS and is safe only because - /// retail's outdoor pass is painter's-ordered far→near (anything nearer + /// retail's outdoor pass is painter's-ordered far→near (anything nearer /// redraws AFTER the punch and re-covers it). Our z-buffered MDI frame /// has no such order, so an unconditional far-Z punch erased the depth /// of NEARER occluders (terrain hills, closer buildings) at aperture - /// pixels — doors/interiors painted through them (the T5 #117 report). + /// pixels — doors/interiors painted through them (the T5 #117 report). /// The z-buffer-correct equivalent: punch ONLY where the aperture /// polygon itself wins a depth test at its true depth (two-pass - /// stencil below). The bias keeps the #108 case covered — terrain + /// stencil below). The bias keeps the #108 case covered — terrain /// hugging the door plane (centimeters in front of the aperture) must /// still be punched; a hill or another house meters nearer must not. /// private const float PunchMarkDepthBias = 0.0005f; /// - /// #129 (2026-06-12): NDC depth is non-linear — a constant NDC bias b - /// spans ≈ b·d²/near meters of eye depth at eye distance d. With + /// #129 (2026-06-12): NDC depth is non-linear — a constant NDC bias b + /// spans ≈ b·d²/near meters of eye depth at eye distance d. With /// znear = 0.1, the 0.0005 constant alone spanned 0.125 m at 5 m but /// ~190 m at a landblock away: every hill/house in front of a distant - /// aperture passed the mark and got far-Z punched — door-shaped leaks + /// aperture passed the mark and got far-Z punched — door-shaped leaks /// through occluders. Fix: cap the bias's EYE-SPACE span at /// . Below the ~10 m crossover - /// (sqrt(cap·near/0.0005)) the constant-NDC term is smaller and wins — + /// (sqrt(cap·near/0.0005)) the constant-NDC term is smaller and wins — /// bit-identical to the T5-validated close-range behavior (#108 grass /// coverage untouched); beyond it the punch can never reach an occluder /// more than the cap in front of the aperture plane. @@ -245,7 +245,7 @@ void main() { } // depth-only: color writes are masked off by the caller state /// Retail Render::znear = 0.1 (decomp :342173, re-landed /// d4b5c71). The cap conversion below assumes the production camera near - /// plane; the small f/(f−n) factor (~1.00002 at far 5000) is ignored. + /// plane; the small f/(f−n) factor (~1.00002 at far 5000) is ignored. public const float CameraNearPlaneMeters = 0.1f; /// CPU mirror of the vertex-shader mark-bias expression (keep in @@ -261,13 +261,13 @@ void main() { } // depth-only: color writes are masked off by the caller state /// slice's clip-space half-planes. selects /// punch (true, retail maxZ1) vs seal (false, retail maxZ2 true depth). /// - /// Seal (interior root): one pass, retail-verbatim — + /// Seal (interior root): one pass, retail-verbatim — /// depth ALWAYS + true projected depth. It runs immediately after the /// gated full depth clear, so there is no nearer content to stomp. /// /// Punch (outdoor root / look-in): two passes (#117). /// Pass A marks stencil where the aperture fan passes a LEQUAL depth - /// test at its (biased) true depth — i.e. where the aperture is + /// test at its (biased) true depth — i.e. where the aperture is /// actually visible against everything drawn so far. Pass B writes the /// far-Z punch with depth ALWAYS but stencil-gated to the marked /// pixels, and zeroes the stencil as it goes (self-cleaning). This is @@ -301,7 +301,7 @@ void main() { } // depth-only: color writes are masked off by the caller state _gl.Disable(EnableCap.CullFace); // portal fans face either way _gl.Disable(EnableCap.ScissorTest); _gl.Enable(EnableCap.DepthTest); - _gl.ColorMask(false, false, false, false); // alpha-0 fan ≙ no color + _gl.ColorMask(false, false, false, false); // alpha-0 fan ≙ no color for (int i = 0; i < planeCount; i++) _gl.Enable(EnableCap.ClipDistance0 + i); @@ -351,7 +351,7 @@ void main() { } // depth-only: color writes are masked off by the caller state if (!forceFarZ) { - // ── SEAL: retail-verbatim single pass ── + // ── SEAL: retail-verbatim single pass ── _gl.DepthFunc(DepthFunction.Always); _gl.DepthMask(true); _gl.Uniform1(_locForceFarZ, 0); @@ -360,7 +360,7 @@ void main() { } // depth-only: color writes are masked off by the caller state } else { - // ── PUNCH pass A: stencil-mark visible aperture pixels ── + // ── PUNCH pass A: stencil-mark visible aperture pixels ── _gl.Enable(EnableCap.StencilTest); _gl.StencilFunc(StencilFunction.Always, 1, 0xFF); _gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Replace); @@ -373,8 +373,8 @@ void main() { } // depth-only: color writes are masked off by the caller state PunchMarkBiasEyeCapMeters * CameraNearPlaneMeters); _gl.DrawArrays(PrimitiveType.TriangleFan, firstVertex, (uint)n); - // ── PUNCH pass B: far-Z write on marked pixels only; - // zero the stencil as we go (self-cleaning) ── + // ── PUNCH pass B: far-Z write on marked pixels only; + // zero the stencil as we go (self-cleaning) ── _gl.StencilFunc(StencilFunction.Equal, 1, 0xFF); _gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Zero); _gl.DepthFunc(DepthFunction.Always); diff --git a/src/AcDream.App/Rendering/PortalProjection.cs b/src/AcDream.App/Rendering/PortalProjection.cs index b9163def..d6092ded 100644 --- a/src/AcDream.App/Rendering/PortalProjection.cs +++ b/src/AcDream.App/Rendering/PortalProjection.cs @@ -1,11 +1,11 @@ -// PortalProjection.cs +// PortalProjection.cs // // Phase A8.F: project a cell-local portal polygon to NDC screen space. Homogeneous frustum clip // in CLIP SPACE (before the perspective divide): first the IN-FRONT-OF-EYE half-space (keep where // w > MinW) so a portal straddling the camera does not invert under the divide and the divide -// stays bounded away from the w=0 eye singularity, then the 4 SIDE planes (x,y within ±w) so every +// stays bounded away from the w=0 eye singularity, then the 4 SIDE planes (x,y within ±w) so every // surviving vertex lands on the screen [-1,1] by construction. The side-plane clip is the R1 -// void-flap fix (2026-06-05) — see ProjectToNdc. +// void-flap fix (2026-06-05) — see ProjectToNdc. // // The clip is NEAR-INDEPENDENT on purpose. We only use the projected x/y for the visibility clip // REGION, so a vertex in front of the eye is meaningful even if it is closer than the projection's @@ -23,7 +23,7 @@ using System.Numerics; namespace AcDream.App.Rendering; -internal static class PortalProjection +public static class PortalProjection { internal ref struct ClipPolygonLease { @@ -118,17 +118,17 @@ internal static class PortalProjection Vector4[]? second = null; // Homogeneous frustum clip in CLIP SPACE, before the perspective divide. First the - // in-front-of-eye half-space (w > MinW) — near-INDEPENDENT, so a portal the camera is - // standing in still projects (see header); then the 4 SIDE planes (x,y within ±w). The + // in-front-of-eye half-space (w > MinW) — near-INDEPENDENT, so a portal the camera is + // standing in still projects (see header); then the 4 SIDE planes (x,y within ±w). The // side clip is the R1 void-flap fix (2026-06-05): without it, a portal WITHIN the near // plane projected small-w verts to wildly off-screen NDC (the probe saw (10.2,-67.4)), // which corrupted the downstream 2D ScreenPolygonClip into an EMPTY region -> OutsideView // empty -> terrain Skip -> the bluish doorway "void". Clipping the side planes here bounds // every surviving vertex to the screen [-1,1] by construction, so a screen-covering doorway // clips to the screen (non-empty) instead of collapsing. The eye plane is clipped FIRST so - // all survivors have w > 0, making the side-plane functionals (w ± x, w ± y) well defined. + // all survivors have w > 0, making the side-plane functionals (w ± x, w ± y) well defined. // Near/far are intentionally NOT clipped (near-independence). Retail PView::GetClip - // (decomp:0x005a4320) projects + frustum-clips the portal poly likewise (research doc A §3.5). + // (decomp:0x005a4320) projects + frustum-clips the portal poly likewise (research doc A §3.5). try { second = vectorPool.Rent(capacity); @@ -155,7 +155,7 @@ internal static class PortalProjection (current, output) = (output, current); } - // Perspective divide → NDC xy. This is the only result allocation. + // Perspective divide → NDC xy. This is the only result allocation. var ndc = new Vector2[currentCount]; for (int i = 0; i < currentCount; i++) { @@ -174,21 +174,21 @@ internal static class PortalProjection /// Faithful homogeneous projection (retail PrimD3DRender::xformStart + the W=0 clip of /// ACRender::polyClipFinish, decomp 424310 / 702749): transform the portal to clip space and clip - /// ONLY the eye plane (w >= 0, EXACT), keeping homogeneous coords — NO perspective divide, NO + /// ONLY the eye plane (w >= 0, EXACT), keeping homogeneous coords — NO perspective divide, NO /// frustum side-plane clamp. The screen bound is applied later by /// against the view region (the root region is the full screen), exactly as retail clips the portal /// against the accumulated portal_view rather than fixed side planes. /// /// The W=0 clip is exact on purpose (the knife-edge port, 2026-06-11; pseudocode at /// docs/research/2026-06-11-polyclipfinish-w0-clip-pseudocode.md): boundary intersections land - /// at w == 0 — homogeneous DIRECTIONS — so a portal the eye is crossing (stair openings, decks) + /// at w == 0 — homogeneous DIRECTIONS — so a portal the eye is crossing (stair openings, decks) /// yields the correct UNBOUNDED half-region, which the bounded view-region clip then cuts to the /// screen. The previous EyePlaneW = 1e-4 produced finite ~1e4-NDC boundary verts whose region - /// intersections sat at the dedup/merge degeneracy threshold — the climb-strobe class. A w=0 + /// intersections sat at the dedup/merge degeneracy threshold — the climb-strobe class. A w=0 /// vertex can never survive ClipToRegion into its divide (a nonzero direction fails at least one /// edge test of any BOUNDED convex region), so no divide-by-zero path exists; the measure-zero /// corner case is guarded in ClipToRegion. Matches polyClipFinish part 1: clip pass runs only - /// when some vertex has w < 0; <3 survivors → reject (empty). + /// when some vertex has w < 0; <3 survivors → reject (empty). public static Vector4[] ProjectToClip(IReadOnlyList localPoly, Matrix4x4 cellToWorld, Matrix4x4 viewProj) { using ClipPolygonLease lease = ProjectToClipLease(localPoly, cellToWorld, viewProj); @@ -274,7 +274,7 @@ internal static class PortalProjection /// (CCW convex) with w-aware Sutherland-Hodgman edge tests, then divide the survivors to NDC and /// normalize to CCW. Ports retail ACRender::polyClipFinish's view-region clip (decomp 702749): the /// edge test multiplies through w (which is > 0 after the eye-plane clip) so it never divides a - /// near-eye vertex, and the final divide runs only on survivors already bounded to the region — + /// near-eye vertex, and the final divide runs only on survivors already bounded to the region — /// stable by construction. Returns <3 verts when the portal does not intersect the region. public static Vector2[] ClipToRegion(IReadOnlyList subjectClip, IReadOnlyList regionCcwNdc) { @@ -339,7 +339,7 @@ internal static class PortalProjection // Homogeneous Sutherland-Hodgman: clip the (w > 0) subject against each CCW edge of the NDC // region. f(P) below is the NDC inside test cross(edge, P_ndc - a) multiplied through P.W, - // which is > 0 after the eye-plane clip — so the sign is the NDC sign yet no near-eye vertex + // which is > 0 after the eye-plane clip — so the sign is the NDC sign yet no near-eye vertex // is ever divided (retail polyClipFinish, decomp 702749). int regionCount = regionCcwNdc.Count; int capacity = checked(subjectClip.Length + regionCount); @@ -370,7 +370,7 @@ internal static class PortalProjection if (currentCount < 3) return System.Array.Empty(); - // Divide survivors → NDC. They are already inside the bounded region. A w=0 + // Divide survivors → NDC. They are already inside the bounded region. A w=0 // measure-zero corner remains the same empty knife-edge result as the prior path. ndcScratch = vector2Pool.Rent(currentCount); Span ndc = ndcScratch.AsSpan(0, currentCount); @@ -408,8 +408,8 @@ internal static class PortalProjection // Retail copy_view's ~1-pixel vertex merge (see ClipToRegion). Collapses // runs of consecutive near-identical vertices, including across the // wrap-around. A polygon that collapses below 3 distinct vertices is - // degenerate (sub-pixel sliver) and returns empty — exactly retail's - // "<3 surviving verts → output count 0". + // degenerate (sub-pixel sliver) and returns empty — exactly retail's + // "<3 surviving verts → output count 0". private const float VertexMergeEpsilonNdc = 2f / 1080f; private static int MergeSubPixelVertices(Span poly) @@ -428,7 +428,7 @@ internal static class PortalProjection } poly[kept++] = vertex; } - // Wrap-around: last ≈ first. + // Wrap-around: last ≈ first. while (kept >= 2) { Vector2 first = poly[0]; @@ -442,9 +442,9 @@ internal static class PortalProjection return kept; } - // One Sutherland-Hodgman half-plane against the directed NDC edge a→b, keeping the CCW-inside + // One Sutherland-Hodgman half-plane against the directed NDC edge a→b, keeping the CCW-inside // (left) part of a HOMOGENEOUS polygon. Inside test for vertex P (clip space): the NDC cross - // product cross(b-a, P/P.W - a) scaled by P.W (> 0): ex·(P.Y - P.W·a.Y) - ey·(P.X - P.W·a.X) ≥ 0. + // product cross(b-a, P/P.W - a) scaled by P.W (> 0): ex·(P.Y - P.W·a.Y) - ey·(P.X - P.W·a.X) ≥ 0. // Crossings interpolate in homogeneous coords (perspective-correct), via the shared Lerp. private static int ClipHomogeneousEdge( ReadOnlySpan polygon, @@ -489,7 +489,7 @@ internal static class PortalProjection if (area2 < 0f) System.Array.Reverse(poly); } - // Minimum clip-space w (≈ metres in front of the eye) to keep a vertex. Excludes the eye + // Minimum clip-space w (≈ metres in front of the eye) to keep a vertex. Excludes the eye // (w=0) singularity and the ~5 cm right at it (bounding the perspective divide), but is // INTENTIONALLY far closer than the projection's 1.0 m near plane so a doorway the camera is // standing in still projects and the cell behind it stays visible. See the file header. diff --git a/src/AcDream.App/Rendering/PortalTunnelCamera.cs b/src/AcDream.App/Rendering/PortalTunnelCamera.cs index 07aa34ab..7126b4a9 100644 --- a/src/AcDream.App/Rendering/PortalTunnelCamera.cs +++ b/src/AcDream.App/Rendering/PortalTunnelCamera.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; namespace AcDream.App.Rendering; @@ -8,7 +8,7 @@ namespace AcDream.App.Rendering; /// (0x00453760). Identity looks along AC +Y with +Z up; the animated /// angle rolls that view around its own +Y forward axis. /// -internal sealed class PortalTunnelCamera : ICamera +public sealed class PortalTunnelCamera : ICamera { public static readonly Vector3 RetailEye = new(0.24f, -2.7f, 0.88f); diff --git a/src/AcDream.App/Rendering/PortalTunnelPresentation.cs b/src/AcDream.App/Rendering/PortalTunnelPresentation.cs index e9781784..8b10eb24 100644 --- a/src/AcDream.App/Rendering/PortalTunnelPresentation.cs +++ b/src/AcDream.App/Rendering/PortalTunnelPresentation.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.App.Rendering.Wb; using AcDream.App.UI; using AcDream.Content.Vfx; @@ -22,7 +22,7 @@ namespace AcDream.App.Rendering; /// 40 frames/second and drawn as a replacement 3-D viewport beneath the /// retained gameplay UI. /// -internal sealed class PortalTunnelPresentation : IDisposable +public sealed class PortalTunnelPresentation : IDisposable { public const uint SetupClientEnum = 0x10000001u; public const uint AnimationClientEnum = 0x10000002u; diff --git a/src/AcDream.App/Rendering/PortalView.cs b/src/AcDream.App/Rendering/PortalView.cs index 8915296e..9dd082cd 100644 --- a/src/AcDream.App/Rendering/PortalView.cs +++ b/src/AcDream.App/Rendering/PortalView.cs @@ -1,4 +1,4 @@ -// PortalView.cs +// PortalView.cs // // Phase A8.F: GL-free 2D screen-space (NDC) clip-region data model. // Mirrors retail view_poly (acclient.h:32465) and view_type (acclient.h:32338): @@ -10,7 +10,7 @@ using System.Numerics; namespace AcDream.App.Rendering; /// One convex polygon in NDC screen space (xy in [-1,1]), plus its bounding rect. -internal readonly struct ViewPolygon +public readonly struct ViewPolygon { public readonly Vector2[] Vertices; public readonly float MinX, MinY, MaxX, MaxY; @@ -101,7 +101,7 @@ internal sealed class PortalPolygonVertexStore } /// A cell's accumulated clip region: a set of convex view polygons + the union bounding rect. -internal sealed class CellView +public sealed class CellView { // ViewPolygon exposes its vertex array for the renderer, so this seed must // be owned by the CellView rather than shared globally. Pooling the @@ -170,7 +170,7 @@ internal sealed class CellView MaxY = float.MinValue; } - /// A region covering the entire NDC viewport — the camera cell's seed region + /// A region covering the entire NDC viewport — the camera cell's seed region /// (mirrors retail PView::DrawInside copy_view(..., 4) at decomp:433814). public static CellView FullScreen() { @@ -192,7 +192,7 @@ internal sealed class CellView // Drift-tolerant, rotation-invariant dedup (2026-06-06 hang fix). PortalVisibilityBuilder.Build // re-queues a cell every time its CellView GROWS, so the flood only terminates when Add // recognises a re-clipped region as a duplicate. Across BFS rounds the SAME region returns - // float-drifted, vertex-rotated, and/or with a ±1 vertex count (homogeneous Sutherland-Hodgman + + // float-drifted, vertex-rotated, and/or with a ±1 vertex count (homogeneous Sutherland-Hodgman + // EnsureCcw); the old exact index-by-index match (eps 1e-4) caught none of those, so the region // grew without bound -> O(n^2) CPU-spin hang in this method. We instead key each polygon by its // vertices SNAPPED to a small NDC grid, consecutive snap-duplicates removed, rotated to a @@ -206,14 +206,14 @@ internal sealed class CellView // #120 convergence (2026-06-11): reject a polygon CONTAINED in one already // stored. The reciprocal ping-pong (eye within PortalSideEpsilon of a - // portal plane → BOTH side tests pass → views lap A→B→A…) re-emits, each - // lap, a region that is — in exact arithmetic — a SUBSET of the polygon + // portal plane → BOTH side tests pass → views lap A→B→A…) re-emits, each + // lap, a region that is — in exact arithmetic — a SUBSET of the polygon // that originated it; near-edge-on apertures make the re-clip wobble by // more than the 1e-3 key grid, so every lap keyed as "new" and the // in-place growth recursed to the depth-128 tripwire (chain dumps: - // 0xA9B4015C↔0x0162, 0xA9B30103↔0x010F; Issue120ReciprocalPingPongTests + // 0xA9B4015C↔0x0162, 0xA9B30103↔0x010F; Issue120ReciprocalPingPongTests // reproduces deterministically). Containment rejection makes growth - // strictly area-increasing — no new visible area, no propagation. The + // strictly area-increasing — no new visible area, no propagation. The // key stays recorded so the exact emission also short-circuits later. // Bonus: back-emission into a full-screen view (the root cell) is now // always rejected outright. @@ -228,7 +228,7 @@ internal sealed class CellView } // #120: is polygon p entirely inside ONE stored polygon (with DedupGridNdc - // slack)? Single-polygon containment is sufficient for the ping-pong class — + // slack)? Single-polygon containment is sufficient for the ping-pong class — // a round-trip re-emission descends from exactly one originator. Stored // polygons are convex (Sutherland-Hodgman / full-screen seed outputs); the // edge test adapts to either winding via the polygon's signed area. @@ -252,7 +252,7 @@ internal sealed class CellView { if (convex.Length < 3) return false; - // signed area → winding (CCW positive); inside = left of every CCW edge. + // signed area → winding (CCW positive); inside = left of every CCW edge. float area2 = 0f; for (int i = 0; i < convex.Length; i++) { @@ -268,10 +268,10 @@ internal sealed class CellView var b = convex[(i + 1) % convex.Length]; var ab = b - a; float len = ab.Length(); - if (len < 1e-9f) continue; // degenerate edge — no constraint + if (len < 1e-9f) continue; // degenerate edge — no constraint foreach (var pt in pts) { - // signed perpendicular distance of pt from edge a→b (positive = inside for CCW) + // signed perpendicular distance of pt from edge a→b (positive = inside for CCW) float cross = sign * (ab.X * (pt.Y - a.Y) - ab.Y * (pt.X - a.X)); if (cross < -eps * len) return false; // a vertex lies outside this edge by more than eps @@ -280,7 +280,7 @@ internal sealed class CellView return true; } - // NDC dedup grid. 1e-3 is ~0.5 px at 1080p — finer than the gap between distinct portal openings + // NDC dedup grid. 1e-3 is ~0.5 px at 1080p — finer than the gap between distinct portal openings // (so real regions stay distinct) yet far coarser than the per-round float drift of a re-clipped // region (so a drifted duplicate snaps onto its predecessor). The finite grid is what bounds growth. private const float DedupGridNdc = 1e-3f; @@ -293,24 +293,24 @@ internal sealed class CellView // // W=0 port (2026-06-11): an ALL-COLLINEAR polygon (zero area) keys as its snapped segment // ("L:" + extreme points) instead of null. A portal whose plane contains the eye projects to - // exactly this — and retail PROPAGATES it: PView::ClipPortals (decomp:433651-433711) forwards + // exactly this — and retail PROPAGATES it: PView::ClipPortals (decomp:433651-433711) forwards // any GetClip output with count != 0 to copy_view/OtherPortalClip with no area gate anywhere, // so the neighbour cell stays in the draw list (cells draw whole; onward floods die naturally // against the zero-area region). Rejecting these views dropped the whole chain behind an - // exactly-in-plane portal for the frame — the parked-eye knife-edge band (tower deck, spiral + // exactly-in-plane portal for the frame — the parked-eye knife-edge band (tower deck, spiral // landings). The segment key space is finite like the area-key space, so dedup + the strict // growth convergence invariant are unchanged. Degenerate is returned only when fewer than 2 - // distinct snapped points survive (a true sub-grid point — not a real region OR segment). + // distinct snapped points survive (a true sub-grid point — not a real region OR segment). // - // §4 corner/doorway fix (2026-06-10) — the collinear pass: the homogeneous region clipper - // (PortalProjection.ClipToRegion, used by the forward AND — as of today — the reciprocal hop) + // §4 corner/doorway fix (2026-06-10) — the collinear pass: the homogeneous region clipper + // (PortalProjection.ClipToRegion, used by the forward AND — as of today — the reciprocal hop) // legitimately inserts intersection vertices ON a subject edge when a region edge grazes it, so // BFS re-clip rounds re-emit the SAME geometric region with 1-2 extra collinear edge vertices. // Without collinear canonicalization those re-emissions key as distinct, defeating the dedup and // accumulating duplicate polygons (the pre-2026-06-06 unbounded-growth hang in miniature, and the // exact reason the reciprocal clip was previously parked on the unstable divide-first path). // Dropping collinear snapped points makes the key purely a function of the region's CORNERS, so - // any re-emission of the same shape — drifted, rotated, vertex-count-inflated — deduplicates. + // any re-emission of the same shape — drifted, rotated, vertex-count-inflated — deduplicates. private CanonicalKeyResult TryAddCanonicalKey(Vector2[]? verts) { if (verts is null || verts.Length < 3) diff --git a/src/AcDream.App/Rendering/PortalVisibilityBuilder.cs b/src/AcDream.App/Rendering/PortalVisibilityBuilder.cs index 2f37a238..2edaaf2a 100644 --- a/src/AcDream.App/Rendering/PortalVisibilityBuilder.cs +++ b/src/AcDream.App/Rendering/PortalVisibilityBuilder.cs @@ -1,4 +1,4 @@ -// PortalVisibilityBuilder.cs +// PortalVisibilityBuilder.cs // // Phase A8.F: recursive portal-clip visibility (the builder). Port of retail // PView::ConstructView (decomp:433750) -> ClipPortals (433572) -> AddViewToPortals @@ -12,7 +12,7 @@ using System.Numerics; namespace AcDream.App.Rendering; /// Per-frame output of the portal-frame BFS. -internal sealed class PortalVisibilityFrame +public sealed class PortalVisibilityFrame { private const int MaxRetainedCellViews = 512; internal const int MaxRetainedBuildCollectionCapacity = 512; @@ -31,7 +31,7 @@ internal sealed class PortalVisibilityFrame internal int PolygonVertexAllocationCount => _polygonVertices.AllocationCount; internal int RetainedPolygonVertexArrayCount => _polygonVertices.RetainedArrayCount; - /// Screen region (NDC) where outdoor terrain/scenery may draw — exit portals + /// Screen region (NDC) where outdoor terrain/scenery may draw — exit portals /// recursively clipped to their portal chain. The cellar-flap fix. public CellView OutsideView { get; private set; } = new(); @@ -39,7 +39,7 @@ internal sealed class PortalVisibilityFrame public Dictionary CellViews { get; } = new(); /// Visible interior cells in the exact order they were first dequeued from the - /// distance-priority work list — closest-first (Phase U.2a). Mirrors retail's + /// distance-priority work list — closest-first (Phase U.2a). Mirrors retail's /// PView::cell_draw_list, appended in PView::ConstructView (decomp:433783) as each cell pops /// off the nearest-vertex-sorted cell_todo_list (InsCellTodoList 433183). Deduplicated: a cell /// appears exactly once, on its first dequeue. The camera cell is always first. @@ -243,7 +243,7 @@ internal sealed class PortalVisibilityFrame } } -internal static class PortalVisibilityBuilder +public static class PortalVisibilityBuilder { // Side-classification epsilon. Retail's is F_EPSILON = 0.000199999995 // (const @0x007c8c70; PView::InitCell Ghidra 0x005a4b70). T2 (BR-4) @@ -253,20 +253,20 @@ internal static class PortalVisibilityBuilder // side test never sees a stale root more than F_EPSILON past a plane. Our // root can lag the eye by up to ~1 cm at pressed corners (the harness's // fixed-root sweep models this), and 0.01 is that documented root-lag - // tolerance — NOT a retail constant. Tighten to F_EPSILON only together + // tolerance — NOT a retail constant. Tighten to F_EPSILON only together // with eye-exact viewer-cell tracking verification (the #108-membership // family) + the cdstW near-clip pin. private const float PortalSideEpsilon = 0.01f; - // Retail F_EPSILON proper — used where the semantic is knife-edge - // REJECTION (ConstructView(CBldPortal) Sidedness IN_PLANE → return 0, + // Retail F_EPSILON proper — used where the semantic is knife-edge + // REJECTION (ConstructView(CBldPortal) Sidedness IN_PLANE → return 0, // Ghidra 0x005a59a0), which must NOT inherit the root-lag tolerance above // (a 1 cm-wide in-plane band would reject look-in seeds whenever the eye // stands near a doorway plane). private const float SeedInPlaneEpsilon = 0.0002f; // TEMP diagnostic (Phase A8.F visual-gate triage; strip after): ACDREAM_A8_DUMP_PV=1 dumps the - // local→NDC→clipped portal geometry for the first 2 Build calls per distinct camera cell. + // local→NDC→clipped portal geometry for the first 2 Build calls per distinct camera cell. private static readonly bool s_pvDump = Environment.GetEnvironmentVariable("ACDREAM_A8_DUMP_PV") == "1"; private static readonly Dictionary s_pvDumpCount = new(); @@ -275,14 +275,14 @@ internal static class PortalVisibilityBuilder /// #120 observable: total convergence-tripwire firings across both the /// interior and the exterior look-in propagation. /// The tripwire firing means the in-place growth's fixpoint invariant - /// broke (T2/BR-4) — tests reset this and assert it stays 0. + /// broke (T2/BR-4) — tests reset this and assert it stays 0. /// public static int ConvergenceTripwireCount; /// /// #120 self-attribution dump: the growth-recursion path that exceeded - /// the tripwire, as a per-cell frequency summary plus the chain tail — - /// the cycle's structure (e.g. 0174↔0175 ping-pong vs a 3-cycle lap) + /// the tripwire, as a per-cell frequency summary plus the chain tail — + /// the cycle's structure (e.g. 0174↔0175 ping-pong vs a 3-cycle lap) /// reads directly off the output. /// private static void DumpPropagationChain(uint[] chain, int depth, uint rootCellId, Vector3 eye) @@ -309,11 +309,11 @@ internal static class PortalVisibilityBuilder /// The +Z world lift applied to DRAWN cell shells (z-fighting vs /// terrain; applied in GameWindow's cell registration). The visibility - /// graph stays in PHYSICS (unlifted) space — feeding the lift into portal + /// graph stays in PHYSICS (unlifted) space — feeding the lift into portal /// planes broke horizontal-portal side tests (#119-residual, f35cb8b). /// Draw-space consumers of portal polygons (the OutsideView color gate /// here, the seal/punch depth fans in GameWindow) must apply this lift so - /// they meet the drawn shell's aperture edge — the unlifted gate left a + /// they meet the drawn shell's aperture edge — the unlifted gate left a /// 2 cm background strip under the drawn lintel (#130). public const float ShellDrawLiftZ = 0.02f; @@ -347,7 +347,7 @@ internal static class PortalVisibilityBuilder // Render unification (outdoor-as-cell, 2026-06-07): when the root IS the synthetic outdoor // node, the landscape is visible FULL-SCREEN, so seed OutsideView with the full-screen NDC // quad. ClipFrameAssembler turns that into a full-screen OutsideView slice, so DrawInside's - // DrawLandscapeThroughOutsideView draws terrain/sky/scenery/weather as the node's "shell" — + // DrawLandscapeThroughOutsideView draws terrain/sky/scenery/weather as the node's "shell" — // the very same callback that already draws the doorway slice when an INTERIOR root reaches // outdoors. Keyed on the explicit IsOutdoorNode flag (set by OutdoorCellNode.Build), NOT a // cell-id heuristic: production EnvCell ids are >= 0x100 but test fixtures use low interior @@ -357,7 +357,7 @@ internal static class PortalVisibilityBuilder frame.OutsideView.Add(frame.CopyPolygon(FullScreenQuad)); // Distance-priority work list (retail PView::cell_todo_list). Cells pop closest-first; - // each cell carries the camera→nearest-portal-vertex distance that put it on the list + // each cell carries the camera→nearest-portal-vertex distance that put it on the list // (retail keys on InitCell's per-portal min-vertex distance, decomp 432988-433004). The // camera cell seeds at distance 0 (retail InsCellTodoList(this, arg2, 0f) at 433758) so it // always pops first. @@ -367,10 +367,10 @@ internal static class PortalVisibilityBuilder // Fixpoint termination replacing the old MaxReprocessPerCell hard cap. This mirrors the // retail portal_view slice offset 0x44 (last-incorporated view-poly watermark) vs 0x38 // (current view_count) decision in AddViewToPortals (433446): a cell is INSERTED into the - // todo list exactly once — on first discovery (retail's ecx_5==0 branch calls + // todo list exactly once — on first discovery (retail's ecx_5==0 branch calls // InsCellTodoList; the ecx_5!=eax_2 growth branch calls AddToCell IN PLACE and never // re-enqueues). Later growth into an already-discovered cell is unioned into its CellView but - // does NOT re-enqueue it — the `cell_view_done` guarantee (ConstructView sets it at 433784 + // does NOT re-enqueue it — the `cell_view_done` guarantee (ConstructView sets it at 433784 // the instant a cell is popped). Enqueue-once across the cell set is the hard termination // guarantee for cyclic / hub / diamond graphs: at most N cells are ever processed. The // camera cell is pre-marked so a portal looping back to it can never re-enqueue it. @@ -398,11 +398,11 @@ internal static class PortalVisibilityBuilder { Console.WriteLine($"[pv-dump] camCell=0x{cameraCell.CellId:X8} portals={cameraCell.Portals.Count} polyLists={cameraCell.PortalPolygons.Count} vp[M11={viewProj.M11:F3} M22={viewProj.M22:F3} M33={viewProj.M33:F3} M34={viewProj.M34:F3} M43={viewProj.M43:F3} M44={viewProj.M44:F3}]"); // Camera-cell portal census (A8.F triage 2026-05-29): report, for EVERY - // portal, the exact inputs the BFS guards read — BEFORE the guards run, so + // portal, the exact inputs the BFS guards read — BEFORE the guards run, so // a portal the loop silently `continue`s past is still visible here. An // empty OUTSIDEVIEW can then be traced to the precise gate: polyLen<3 (empty // polygon from EnvCellLandblockBuildBuilder), interiorSide=false (camera back-facing the - // portal — a legitimately-empty result, not a bug), or (if both OK) a + // portal — a legitimately-empty result, not a bug), or (if both OK) a // downstream projection/clip failure shown by the EXIT-PROJ/EXIT-CLIP lines. for (int ci = 0; ci < cameraCell.Portals.Count; ci++) { @@ -417,7 +417,7 @@ internal static class PortalVisibilityBuilder } // T2 (BR-4): retail's growth propagation is IN PLACE, never by re-enqueue - // — PView::AddViewToPortals (Ghidra 0x005a52d0, pc:433446): first + // — PView::AddViewToPortals (Ghidra 0x005a52d0, pc:433446): first // discovery enqueues via InsCellTodoList; growth into a cell whose // cell_view_done is set calls AdjustCellView (pc:433741-433745), which // re-clips ONLY the new views (the update_count watermark) through that @@ -426,7 +426,7 @@ internal static class PortalVisibilityBuilder // neighbour; it processes exactly the new tail and recurses further // growth. Termination is physical: recursion fires only when AddRegion // added a DISTINCT polygon (CanonicalKey dedup) that survived the 1-px - // vertex merge — the finite fixpoint floor that replaced the old + // vertex merge — the finite fixpoint floor that replaced the old // MaxReprocessPerCell=16 drift cap (deleted). The depth tripwire below // is a loud failsafe, not control flow: it firing means the convergence // invariant broke and must be fixed, not tuned. @@ -434,7 +434,7 @@ internal static class PortalVisibilityBuilder // #120 self-attribution: the recursion path (cell id per depth), so a // tripwire firing names the growth CYCLE instead of just the tip. // Harness sweeps (CornerFloodReplayTests *Converges tests) could not - // reproduce the T5 firing — production-only ingredients (full lookup + // reproduce the T5 firing — production-only ingredients (full lookup // graph / real camera path) are suspected; this dump pins them on the // next natural occurrence. uint[] propagationChain = frame.PropagationChainScratch; @@ -444,7 +444,7 @@ internal static class PortalVisibilityBuilder if (depth >= RecursionTripwire) { System.Threading.Interlocked.Increment(ref ConvergenceTripwireCount); - Console.WriteLine($"[pv-ERROR] in-place propagation tripwire at depth {depth} on cell=0x{cell.CellId:X8} — convergence invariant broken, investigate"); + Console.WriteLine($"[pv-ERROR] in-place propagation tripwire at depth {depth} on cell=0x{cell.CellId:X8} — convergence invariant broken, investigate"); DumpPropagationChain(propagationChain, depth, cameraCell.CellId, cameraPos); return; } @@ -497,7 +497,7 @@ internal static class PortalVisibilityBuilder // Portal-side test (retail PView::InitCell side test, decomp:432962): only traverse a portal // the camera is on the INTERIOR side of. Retail culls the back-facing portal (the doorway just - // flooded through) by this test ALONE — there is NO eye-in-opening bypass. R-A2b: the old + // flooded through) by this test ALONE — there is NO eye-in-opening bypass. R-A2b: the old // `&& !eyeInsideOpening` bypass let a back portal within 1.75 m through, forming the // 0171<->0173 flood cycle -> re-enqueue churn -> the doorway flap (pinned in flap-sidechk.log: // back portals show camInterior=False eyeIn=True). @@ -529,8 +529,8 @@ internal static class PortalVisibilityBuilder if (dx) Console.WriteLine($"[pv-dump] EXIT-PROJ cell=0x{cell.CellId:X8} p{i} localN={poly.Length} clipN={clipVerts} local0=({poly[0].X:F2},{poly[0].Y:F2},{poly[0].Z:F2})"); if (dx) Console.WriteLine($"[pv-dump] EXIT-CLIP cell=0x{cell.CellId:X8} p{i} currentViewPolys={currentView.Polygons.Count} clipResult={clippedRegion.Count}"); - // Empty clip = no flood through this portal, period — retail's empty-GetClip rule - // (polyClipFinish <3 survivors → reject; ClipPortals adds no view). The + // Empty clip = no flood through this portal, period — retail's empty-GetClip rule + // (polyClipFinish <3 survivors → reject; ClipPortals adds no view). The // EyeInsidePortalOpening rescue that used to substitute the current view here was // the documented compensation for ProjectToClip's old EyePlaneW=1e-4 divergence // from polyClipFinish's exact W=0 clip; with the W=0 port (2026-06-11, pseudocode @@ -554,7 +554,7 @@ internal static class PortalVisibilityBuilder // Exit portal -> outdoors visible through this (clipped) opening. // OutsideView gates DRAWN color (terrain/sky/scissor), and the // shell that rasterizes this aperture draws +drawLiftZ above - // the physics transform — project the region in the SAME + // the physics transform — project the region in the SAME // lifted space or terrain stops a lift-height short of the // drawn lintel (#130 strip). Flood semantics keep the // unlifted clippedRegion path above. @@ -607,20 +607,20 @@ internal static class PortalVisibilityBuilder continue; } - // Phase U.2b — neighbour-side OtherPortalClip (retail PView::OtherPortalClip + // Phase U.2b — neighbour-side OtherPortalClip (retail PView::OtherPortalClip // decomp:433524). The portal opening seen from THIS cell may be wider than the // SAME opening seen from the neighbour (skewed/oblique apertures), so retail // re-clips the already-near-side-clipped region against the neighbour's matching - // (reciprocal) portal polygon — the propagated region is the intersection of the + // (reciprocal) portal polygon — the propagated region is the intersection of the // opening "seen from A" AND "seen from B". This can only TIGHTEN, never widen, and // degrades to the prior near-side-only region when the reciprocal is unresolvable // (over-include is the safe default). The reciprocal is the portal at index - // `portal.OtherPortalId` in the NEIGHBOUR's portal list — retail's direct back-link + // `portal.OtherPortalId` in the NEIGHBOUR's portal list — retail's direct back-link // (arg2->other_portal_id, 433557), NOT a scan for the first OtherCellId match. The // direct index is what lets a cell with TWO portals to the same neighbour clip each // opening against its OWN reciprocal instead of the first one. Mutates clippedRegion // in place before the union below. - // T2 (BR-4): reciprocal-empty culls — retail OtherPortalClip + // T2 (BR-4): reciprocal-empty culls — retail OtherPortalClip // returning nothing means the opening is invisible from the // neighbour's side; the old eye-in-opening restore was part of // the deleted rescue. @@ -645,8 +645,8 @@ internal static class PortalVisibilityBuilder if (grew) { - // First discovery → enqueue once (retail InsCellTodoList in - // the ecx_5==0 branch). Distance = camera→nearest portal- + // First discovery → enqueue once (retail InsCellTodoList in + // the ecx_5==0 branch). Distance = camera→nearest portal- // opening vertex (retail InitCell min-vertex distance, // pc:432988-433004). if (queued.Add(neighbourId)) @@ -655,10 +655,10 @@ internal static class PortalVisibilityBuilder InsertTodo(todo, neighbour, dist); inserted = true; } - // Growth into an already-POPPED cell → retail AdjustCellView: + // Growth into an already-POPPED cell → retail AdjustCellView: // process only the new views, immediately, in place. A cell // discovered but still pending in the todo list needs nothing - // — its pop processes everything to date via the watermark. + // — its pop processes everything to date via the watermark. else if (drawListed.Contains(neighbourId)) { inPlace = true; @@ -678,8 +678,8 @@ internal static class PortalVisibilityBuilder // draw position (retail appends to cell_draw_list once per pop, // pc:433783). Note: retail also RE-SORTS the draw list when a // late-grown cell's dependency order changes (AdjustCellPlace, - // pc:433247); we keep first-pop order — under T1's whole-cell - // far→near draws + depth testing, order affects only transparent- + // pc:433247); we keep first-pop order — under T1's whole-cell + // far→near draws + depth testing, order affects only transparent- // pass compositing in exotic chains (documented residual for T5). if (drawListed.Add(cell.CellId)) frame.OrderedVisibleCells.Add(cell.CellId); @@ -691,7 +691,7 @@ internal static class PortalVisibilityBuilder if (pvDump) Console.WriteLine($"[pv-dump] OUTSIDEVIEW polys={frame.OutsideView.Polygons.Count} bfsCellViews={frame.CellViews.Count} crossBldg={frame.CrossBuildingViews.Count}"); - // Phase U.4c flap probe (ACDREAM_PROBE_FLAP) — read-only per-frame snapshot of the + // Phase U.4c flap probe (ACDREAM_PROBE_FLAP) — read-only per-frame snapshot of the // root cell's per-portal side-test + projection + the frame's exit/visible counts. if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled) EmitFlapProbe(cameraCell, cameraPos, viewProj, frame); @@ -715,7 +715,7 @@ internal static class PortalVisibilityBuilder /// camera cell. It keeps the same retail distance-priority traversal and /// neighbour reciprocal clipping once inside the building. /// - /// Optional NDC region the seed apertures clip against — + /// Optional NDC region the seed apertures clip against — /// retail's GetClip runs under the CURRENTLY INSTALLED view (PView::GetClip /// 0x005a4320): full screen when the viewer is outdoors, the accumulated /// outside (doorway) view when a building is looked into from an interior @@ -756,7 +756,7 @@ internal static class PortalVisibilityBuilder // If the camera is on the cell-interior side, the normal indoor // DrawInside path owns this portal instead. T2 (BR-4): a seed // portal the eye is IN-PLANE with (|dist| <= F_EPSILON) rejects - // OUTRIGHT — retail ConstructView(CBldPortal) returns 0 on + // OUTRIGHT — retail ConstructView(CBldPortal) returns 0 on // Sidedness IN_PLANE (Ghidra 0x005a59a0); no degenerate view is // ever built from a knife-edge aperture. if (i < cell.ClipPlanes.Count) @@ -788,7 +788,7 @@ internal static class PortalVisibilityBuilder // T2 (BR-4): empty clip = no seed, no exceptions (retail's // empty-GetClip rule; the full-screen substitute rescue is - // deleted — see Build()). + // deleted — see Build()). if (clippedRegion.Count == 0) continue; @@ -800,19 +800,19 @@ internal static class PortalVisibilityBuilder } } - // T2 (BR-4): in-place growth propagation — mirrors Build()'s + // T2 (BR-4): in-place growth propagation — mirrors Build()'s // ProcessCellPortals (retail AdjustCellView via the watermark); the // re-enqueue + MaxReprocessPerCell cap and the eye-in-opening rescues // are deleted (empty clip culls, period). const int RecursionTripwire = 128; - uint[] propagationChain = frame.PropagationChainScratch; // #120 self-attribution — see Build() + uint[] propagationChain = frame.PropagationChainScratch; // #120 self-attribution — see Build() void ProcessCellPortals(LoadedCell cell, int depth) { if (depth >= RecursionTripwire) { System.Threading.Interlocked.Increment(ref ConvergenceTripwireCount); - Console.WriteLine($"[pv-ERROR] look-in in-place propagation tripwire at depth {depth} on cell=0x{cell.CellId:X8} — convergence invariant broken, investigate"); + Console.WriteLine($"[pv-ERROR] look-in in-place propagation tripwire at depth {depth} on cell=0x{cell.CellId:X8} — convergence invariant broken, investigate"); DumpPropagationChain(propagationChain, depth, 0u, cameraPos); return; } @@ -841,7 +841,7 @@ internal static class PortalVisibilityBuilder if (portal.OtherCellId == 0xFFFF) continue; // already outdoors; exterior terrain was drawn by the caller. - // R-A2b: cull back portals by the side test alone — see Build(). + // R-A2b: cull back portals by the side test alone — see Build(). if (i < cell.ClipPlanes.Count && !CameraOnInteriorSide(cell, i, cameraPos)) continue; @@ -905,15 +905,15 @@ internal static class PortalVisibilityBuilder } /// - /// Retail per-building flood — PView::ConstructView(CBldPortal*, …) (decomp:433827), - /// reached from BSPPORTAL::portal_draw_portals_only (0x53d870) → DrawPortal + /// Retail per-building flood — PView::ConstructView(CBldPortal*, …) (decomp:433827), + /// reached from BSPPORTAL::portal_draw_portals_only (0x53d870) → DrawPortal /// (0x5a5ab0) during the terrain BSP walk. Floods ONE building's cells from its outside-facing /// entrance portal(s). Identical machinery to , but the CONTRACT is /// per-building: the caller passes exactly one building's cells, so the seed is that building's - /// FINITE entrance opening (bounded flood depth → the stable ~2-cell view retail draws per visible - /// building, measured live §3.4). This differs from the synthetic outdoor node's single unified - /// flood whose full-screen-ish seed reaches variable depth into a building as the eye moves — the - /// 2↔6 oscillation. Robustness is validated by the conformance test, not assumed. + /// FINITE entrance opening (bounded flood depth → the stable ~2-cell view retail draws per visible + /// building, measured live §3.4). This differs from the synthetic outdoor node's single unified + /// flood whose full-screen-ish seed reaches variable depth into a building as the eye moves — the + /// 2↔6 oscillation. Robustness is validated by the conformance test, not assumed. /// public static PortalVisibilityFrame ConstructViewBuilding( IEnumerable buildingCells, @@ -1053,12 +1053,12 @@ internal static class PortalVisibilityBuilder } // Phase U.4c flap probe. One [flap] line per Build: the root cell's per-portal - // signed distance D (eye→portal plane), traverse/cull decision, and NDC projection + // signed distance D (eye→portal plane), traverse/cull decision, and NDC projection // vertex count, plus the frame's OutsideView polygon count + visible-cell count. - // `localEye` is the eye in root-local space — its component along an interior portal + // `localEye` is the eye in root-local space — its component along an interior portal // plane reveals when the eye has crossed past that plane (the stale-root region that // makes the side test cull a still-needed portal). Read-only recompute; no effect on - // the returned frame. Throwaway apparatus — strip with the probe. + // the returned frame. Throwaway apparatus — strip with the probe. private static void EmitFlapProbe( LoadedCell cameraCell, Vector3 cameraPos, Matrix4x4 viewProj, PortalVisibilityFrame frame) { @@ -1080,9 +1080,9 @@ internal static class PortalVisibilityBuilder d = Vector3.Dot(pl.Normal, localEye) + pl.D; side = CameraOnInteriorSide(cameraCell, i, cameraPos); } - // Replicate the walk's faithful path exactly (ProjectToClip → ClipToRegion(FullScreen)) so + // Replicate the walk's faithful path exactly (ProjectToClip → ClipToRegion(FullScreen)) so // proj/clip mean the same as production: proj = clip-space verts in front of the eye, - // clip = verts surviving the screen-region clip. clip=0 with proj>=3 ⇒ the portal is + // clip = verts surviving the screen-region clip. clip=0 with proj>=3 ⇒ the portal is // genuinely off-screen; the ndc coords (post-clip, bounded) show where on screen it lands. int projN = -1, clipN = -1; string ndcText = ""; @@ -1115,21 +1115,21 @@ internal static class PortalVisibilityBuilder } // Mirrors CellVisibility's portal-side test (InsideSide convention). - // In-plane (|dot| <= PortalSideEpsilon) counts as interior-side — retail + // In-plane (|dot| <= PortalSideEpsilon) counts as interior-side — retail // InitCell leaves the in-plane case a CANDIDATE for cell portals (Ghidra // 0x005a4b70); building/exterior SEED portals additionally reject in-plane - // via EyeInPlaneOfPortal (retail ConstructView(CBldPortal) IN_PLANE → 0). + // via EyeInPlaneOfPortal (retail ConstructView(CBldPortal) IN_PLANE → 0). private static bool CameraOnInteriorSide(LoadedCell cell, int portalIndex, Vector3 cameraPos) { var plane = cell.ClipPlanes[portalIndex]; - if (plane.Normal.LengthSquared() < 1e-8f) return true; // no usable plane → allow + if (plane.Normal.LengthSquared() < 1e-8f) return true; // no usable plane → allow var localCam = Vector3.Transform(cameraPos, cell.InverseWorldTransform); float dot = Vector3.Dot(plane.Normal, localCam) + plane.D; return plane.InsideSide == 0 ? dot >= -PortalSideEpsilon : dot <= PortalSideEpsilon; } // T2 (BR-4): retail ConstructView(CBldPortal)'s Sidedness IN_PLANE reject - // (Ghidra 0x005a59a0): |eye·N + d| <= F_EPSILON → the building/exterior + // (Ghidra 0x005a59a0): |eye·N + d| <= F_EPSILON → the building/exterior // portal contributes nothing this frame (knife-edge aperture). Uses the // true retail epsilon, NOT the side test's root-lag tolerance. private static bool EyeInPlaneOfPortal(LoadedCell cell, int portalIndex, Vector3 cameraPos) @@ -1153,22 +1153,22 @@ internal static class PortalVisibilityBuilder if (area2 < 0f) Array.Reverse(poly); } - // Phase U.2b — reciprocal OtherPortalClip (retail PView::OtherPortalClip decomp:433524). + // Phase U.2b — reciprocal OtherPortalClip (retail PView::OtherPortalClip decomp:433524). // Resolves the neighbour's reciprocal back-portal by DIRECT INDEX (`otherPortalId`), projects // that reciprocal polygon through the NEIGHBOUR's world transform to NDC, and intersects it into // every polygon of `clippedRegion` (already clipped against the near-side opening + current - // view). The net region is "opening seen from the near cell" ∩ "opening seen from the - // neighbour" — a strict tightening that prevents over-inclusion through skewed apertures. + // view). The net region is "opening seen from the near cell" ∩ "opening seen from the + // neighbour" — a strict tightening that prevents over-inclusion through skewed apertures. // // `otherPortalId` is the near-side portal's reciprocal back-link, straight from the dat's - // CellPortal.OtherPortalId. Retail indexes the neighbour's portal array with it directly — - // `portals->portal[arg2->other_portal_id ...]` at 005a54b2/005a54f6 — rather than scanning for + // CellPortal.OtherPortalId. Retail indexes the neighbour's portal array with it directly — + // `portals->portal[arg2->other_portal_id ...]` at 005a54b2/005a54f6 — rather than scanning for // the first OtherCellId match. A scan picks the FIRST back-portal for EVERY near-side portal to // the same neighbour, so a cell with two openings into one neighbour clips both against the same - // (first) reciprocal — hiding the second opening when the apertures are disjoint (under-inclusion + // (first) reciprocal — hiding the second opening when the apertures are disjoint (under-inclusion // bug #102 M-4). The direct index gives each opening its own reciprocal. // - // GUARDS — degrade to over-include (leave `clippedRegion` untouched), NEVER clip against a + // GUARDS — degrade to over-include (leave `clippedRegion` untouched), NEVER clip against a // guessed polygon: the index is out of range, OR the indexed polygon is missing/degenerate // (< 3 verts), OR it projects entirely behind the camera. Over-inclusion is the safe default; // mis-resolution is the bug this method exists to remove. PortalPolygons is in lockstep with @@ -1184,38 +1184,38 @@ internal static class PortalVisibilityBuilder { if (clippedRegion.Count == 0) return; - // Retail skips OtherPortalClip entirely for exact-match portals — both cells share + // Retail skips OtherPortalClip entirely for exact-match portals — both cells share // the SAME opening polygon, so re-clipping against the reciprocal can only re-derive // the near-side clip: PView::ClipPortals decomp:433689 // `if (exact_match != 0 || other_portal_id < 0) goto propagate-without-reciprocal`. if ((portalFlags & PortalFlagExactMatch) != 0) return; - // Direct back-link index (retail arg2->other_portal_id). Out-of-range → over-include. + // Direct back-link index (retail arg2->other_portal_id). Out-of-range → over-include. if (otherPortalId >= neighbour.PortalPolygons.Count) return; Vector3[]? reciprocalPoly = neighbour.PortalPolygons[otherPortalId]; - if (reciprocalPoly == null || reciprocalPoly.Length < 3) return; // missing/degenerate → over-include + if (reciprocalPoly == null || reciprocalPoly.Length < 3) return; // missing/degenerate → over-include - // §4 corner/doorway fix (2026-06-10): the reciprocal clip now runs the SAME homogeneous - // pipeline as the forward clip — retail PView::OtherPortalClip (decomp:433524-433563) routes - // the reciprocal polygon through the very same GetClip(finish=1) → ACRender::polyClipFinish + // §4 corner/doorway fix (2026-06-10): the reciprocal clip now runs the SAME homogeneous + // pipeline as the forward clip — retail PView::OtherPortalClip (decomp:433524-433563) routes + // the reciprocal polygon through the very same GetClip(finish=1) → ACRender::polyClipFinish // homogeneous clipper as the near-side portal; there is no divide-first special case. // // HISTORY: this used to be ProjectToNdc + 2D ScreenPolygonClip.Intersect, justified by "the - // reciprocal is a back-portal one hop away — never near the eye". That assumption is FALSE + // reciprocal is a back-portal one hop away — never near the eye". That assumption is FALSE // exactly at doorways/corners: the reciprocal IS the same opening whose plane the eye presses // against (2-60 cm). ProjectToNdc's MinW=0.05 eye-clip + side-plane clip + divide is knife-edge - // there — 2 cm eye moves flipped its output between "covers the region" and a duplicated-vertex - // hairline, which CellView.Add's snap-dedup then rejected → the neighbour room dropped from the - // flood for isolated frames → the corner/transition background strobe (CornerFloodReplayTests + // there — 2 cm eye moves flipped its output between "covers the region" and a duplicated-vertex + // hairline, which CellView.Add's snap-dedup then rejected → the neighbour room dropped from the + // flood for isolated frames → the corner/transition background strobe (CornerFloodReplayTests // pins this deterministically; the glitch steps die with this change). The old path's other - // rationale — per-round float drift defeating the exact-match CellView dedup — is obsolete: + // rationale — per-round float drift defeating the exact-match CellView dedup — is obsolete: // CanonicalKey's 1e-3-grid snap dedup (2026-06-06) absorbs re-clip drift by construction. using PortalProjection.ClipPolygonLease reciprocalClip = PortalProjection.ProjectToClipLease( reciprocalPoly, neighbour.WorldTransform, viewProj); - if (reciprocalClip.Count < 3) return; // reciprocal entirely behind the eye → no constraint (over-include) + if (reciprocalClip.Count < 3) return; // reciprocal entirely behind the eye → no constraint (over-include) // Intersect the reciprocal opening into each near-side polygon; drop any that fall away. // ClipToRegion(subject=homogeneous reciprocal, region=near-side NDC polygon) = the same @@ -1252,7 +1252,7 @@ internal static class PortalVisibilityBuilder return grew; } - // Camera→nearest-vertex distance for a portal polygon, in world space. Mirrors the per-portal + // Camera→nearest-vertex distance for a portal polygon, in world space. Mirrors the per-portal // min-distance loop retail runs in PView::InitCell (decomp:432988-433004) to key the todo list: // it walks the portal's vertices, transforms each to world space, and keeps the smallest // straight-line distance to the camera viewpoint. Keying on the portal opening (not the cell @@ -1272,10 +1272,10 @@ internal static class PortalVisibilityBuilder /// /// Distance-sorted work list for the portal BFS, ported from retail PView::cell_todo_list + /// InsCellTodoList (decomp:433183). Insertion keeps the list ordered so the NEAREST cell sits at - /// the tail; removes the tail — giving closest-first traversal exactly + /// the tail; removes the tail — giving closest-first traversal exactly /// as ConstructView's pop-from-(cell_todo_num-1) does (433767-433769). The insertion only shifts /// entries strictly farther than the newcomer (retail's flag test breaks on the first - /// not-greater entry), so an equal-distance newcomer lands at the tail and pops FIRST — + /// not-greater entry), so an equal-distance newcomer lands at the tail and pops FIRST — /// LIFO on ties, matching retail's break-on-first-not-greater + pop-from-tail. /// private static void InsertTodo( diff --git a/src/AcDream.App/Rendering/RenderBootstrap.cs b/src/AcDream.App/Rendering/RenderBootstrap.cs index 39817ab0..1b3949b2 100644 --- a/src/AcDream.App/Rendering/RenderBootstrap.cs +++ b/src/AcDream.App/Rendering/RenderBootstrap.cs @@ -1,4 +1,4 @@ -using System.Collections.Concurrent; +using System.Collections.Concurrent; using AcDream.Content; using DatReaderWriter; using Microsoft.Extensions.Logging.Abstractions; @@ -12,7 +12,7 @@ namespace AcDream.App.Rendering; /// classes and the same order as , minus /// terrain / sky / physics / streaming. /// -internal sealed record RenderStack( +public sealed record RenderStack( GL Gl, IDatReaderWriter Dats, string ShaderDir, @@ -28,13 +28,15 @@ internal sealed record RenderStack( AcDream.App.UI.UiDatFont? LargeDatFont) : System.IDisposable { internal GpuFrameFlightController FrameFlights { get; init; } = null!; - internal IGpuDevice GpuDevice { get; init; } = null!; - internal GpuDeviceFrameLifetime FrameLifetime { get; init; } = null!; private ResourceShutdownTransaction? _shutdown; - internal void BeginFrame() => FrameLifetime.BeginFrame(); + internal void BeginFrame() + { + FrameFlights.BeginFrame(); + UiHost.TextRenderer.BeginFrame(FrameFlights.CurrentSlot); + } - internal void EndFrame() => FrameLifetime.EndFrame(); + internal void EndFrame() => FrameFlights.EndFrame(); /// Dispose the GL pieces this stack OWNS (everything created in /// ). + are caller-owned @@ -61,10 +63,6 @@ internal sealed record RenderStack( new("lighting UBO", LightingUbo.Dispose), new("UI host", UiHost.Dispose), ]), - new ResourceShutdownStage("GPU device (RHI)", - [ - new("GPU device", GpuDevice.Dispose), - ]), new ResourceShutdownStage("frame flight owner", [ new("frame flights", FrameFlights.Dispose), @@ -73,17 +71,17 @@ internal sealed record RenderStack( } /// - /// Resolves a sprite id (0x06xxxxxx) to a (texture-table slot, width, height) triple. - /// Copied verbatim from GameWindow's ResolveChrome closure — it calls + /// Resolves a sprite id (0x06xxxxxx) to a (GL handle, width, height) triple. + /// Copied verbatim from GameWindow's ResolveChrome closure — it calls /// TextureCache.GetOrUploadRenderSurface(id, out w, out h). /// - public (GpuTextureSlot handle, int width, int height) ResolveChrome(uint spriteId) + public (uint handle, int width, int height) ResolveChrome(uint spriteId) { - GpuTextureSlot t = TextureCache.GetOrUploadRenderSurface(spriteId, out int w, out int h); + uint t = TextureCache.GetOrUploadRenderSurface(spriteId, out int w, out int h); return (t, w, h); } - // ── Font cache (per-stack, keyed by FontDid) ───────────────────────────── + // ── Font cache (per-stack, keyed by FontDid) ───────────────────────────── /// /// Cache of loaded dat fonts keyed by FontDid (0x40000000-range). @@ -95,7 +93,7 @@ internal sealed record RenderStack( /// /// Lazily load and cache a dat font by its FontDid. Returns null (and - /// caches null) when the Font DBObj is absent or has no foreground surface — + /// caches null) when the Font DBObj is absent or has no foreground surface — /// callers fall back to the global font in that case. /// /// Pre-seeds (0x40000000) and @@ -125,7 +123,7 @@ internal sealed record RenderStack( } /// Options for . -internal sealed record RenderBootstrapOptions( +public sealed record RenderBootstrapOptions( AcDream.UI.Abstractions.Settings.QualitySettings Quality, string DiagnosticsDirectory); @@ -133,12 +131,12 @@ internal sealed record RenderBootstrapOptions( /// Constructs the UI Studio's render stack from the production classes, /// in the same order as . /// -internal static class RenderBootstrap +public static class RenderBootstrap { /// /// Build the studio's render stack. Throws /// (same message as GameWindow) if GL_ARB_bindless_texture or - /// GL_ARB_shader_draw_parameters are absent — the modern path is mandatory. + /// GL_ARB_shader_draw_parameters are absent — the modern path is mandatory. /// public static RenderStack Create( GL gl, @@ -167,15 +165,8 @@ internal static class RenderBootstrap // --- TextureCache (GameWindow ~1774) --- var frameFlights = new GpuFrameFlightController(gl); - // Campaign V slice V4a: the RHI device UI Studio's TextureCache/UiHost - // now need for their retained-UI texture/pipeline path, constructed - // the same way HostInputCameraCompositionPhase does for the main - // GameWindow (V1). - var gpuDevice = new AcDream.App.Rendering.Gpu.Gl.GlGpuDevice(gl, frameFlights, shaderDir); - var frameLifetime = new GpuDeviceFrameLifetime(gpuDevice); var textureCache = new TextureCache( gl, - gpuDevice, dats, bindless, frameFlights, @@ -210,7 +201,7 @@ internal static class RenderBootstrap return new AcDream.Core.Physics.AnimationSequencer( setup, mtable, capturedAnimLoader); } - // Setup exists but no motion table — no-op sequencer. + // Setup exists but no motion table — no-op sequencer. return new AcDream.Core.Physics.AnimationSequencer( setup, new DatReaderWriter.DBObjs.MotionTable(), @@ -228,10 +219,10 @@ internal static class RenderBootstrap var entitySpawnAdapter = new Wb.EntitySpawnAdapter( textureCache, SequencerFactory, meshAdapter); - // --- EntityClassificationCache (GameWindow ~217 — field initializer, new()) --- + // --- EntityClassificationCache (GameWindow ~217 — field initializer, new()) --- var classificationCache = new Wb.EntityClassificationCache(); - // --- TranslucencyFadeManager (GameWindow — field initializer, new()) --- + // --- TranslucencyFadeManager (GameWindow — field initializer, new()) --- var translucencyFades = new AcDream.Core.Rendering.TranslucencyFadeManager(); // --- WbDrawDispatcher (GameWindow ~2377-2381) --- @@ -246,13 +237,13 @@ internal static class RenderBootstrap // --- Larger retail font (0x40000001, MaxCharHeight=18) for attribute row text. // The default font (0x40000000, 16px) renders the row names too small; the 18px // variant (confirmed in client_portal.dat 2026-06-26) matches the retail character - // window list more closely (≈ icon height ≈ 24px target, 18px is best available). + // window list more closely (≈ icon height ≈ 24px target, 18px is best available). var largeDatFont = AcDream.App.UI.UiDatFont.Load(dats, textureCache, 0x40000001u); // --- UiHost (GameWindow ~1790); pass null for debugFont (only used as - // a fallback BitmapFont for the world-space HUD — not needed for the + // a fallback BitmapFont for the world-space HUD — not needed for the // UI Studio, and BitmapFont requires a system font byte array) --- - var uiHost = new AcDream.App.UI.UiHost(gpuDevice, () => frameLifetime.Current, defaultFont: null); + var uiHost = new AcDream.App.UI.UiHost(gl, shaderDir, defaultFont: null); var stack = new RenderStack( Gl: gl, @@ -270,8 +261,6 @@ internal static class RenderBootstrap LargeDatFont: largeDatFont) { FrameFlights = frameFlights, - GpuDevice = gpuDevice, - FrameLifetime = frameLifetime, }; // Pre-seed the font cache with the two already-uploaded atlas instances diff --git a/src/AcDream.App/Rendering/RenderFrameOrchestrator.cs b/src/AcDream.App/Rendering/RenderFrameOrchestrator.cs index a3531334..df5f0e0a 100644 --- a/src/AcDream.App/Rendering/RenderFrameOrchestrator.cs +++ b/src/AcDream.App/Rendering/RenderFrameOrchestrator.cs @@ -38,52 +38,6 @@ internal interface IRenderFrameLifetime void EndFrame(); } -/// -/// Campaign V slice V4a: the current frame's , readable -/// by any ported renderer that needs to allocate a ring or open a pass — the -/// structural piece this slice adds so TextRenderer/DebugLineRenderer -/// have somewhere to reach the frame lifecycle already bracketing every render -/// callback ('s ). -/// -internal interface ICurrentGpuFrameSource -{ - /// The frame opened by the most recent . Throws if none is open. - IGpuFrame Current { get; } -} - -/// -/// Wires / into -/// the same bracket -/// occupied before this slice — additive, not a frame-graph restructuring: -/// already calls the frame-flight -/// controller's BeginFrame internally, so this type OWNS the -/// slot rather than running alongside the -/// controller (which would double-begin the same flight fence). Clears, -/// framebuffer management, and every non-ported renderer's own per-frame -/// bracket are untouched — slice V4h formalizes the rest of the frame spine. -/// -internal sealed class GpuDeviceFrameLifetime : IRenderFrameLifetime, ICurrentGpuFrameSource -{ - private readonly IGpuDevice _device; - private IGpuFrame? _current; - - public GpuDeviceFrameLifetime(IGpuDevice device) => - _device = device ?? throw new ArgumentNullException(nameof(device)); - - public IGpuFrame Current => _current - ?? throw new InvalidOperationException( - "No GPU frame is open — BeginFrame must run before Current is read."); - - public void BeginFrame() => _current = _device.BeginFrame(); - - public void EndFrame() - { - IGpuFrame frame = Current; - _current = null; - frame.End(); - } -} - internal interface IRenderFrameResourcePhase { void Prepare(RenderFrameInput input); diff --git a/src/AcDream.App/Rendering/RenderFrameResourceController.cs b/src/AcDream.App/Rendering/RenderFrameResourceController.cs index db714e99..03d4792d 100644 --- a/src/AcDream.App/Rendering/RenderFrameResourceController.cs +++ b/src/AcDream.App/Rendering/RenderFrameResourceController.cs @@ -88,6 +88,8 @@ internal sealed class RuntimeRenderFrameBeginResources : IRenderFrameBeginResour private readonly WbDrawDispatcher? _dispatcher; private readonly EnvCellRenderer? _environmentCells; private readonly PortalDepthMaskRenderer? _portalDepth; + private readonly TextRenderer? _worldText; + private readonly TextRenderer? _uiText; private readonly ClipFrame? _clip; private readonly TerrainModernRenderer? _terrain; private readonly SceneLightingUboBinding? _lighting; @@ -96,6 +98,8 @@ internal sealed class RuntimeRenderFrameBeginResources : IRenderFrameBeginResour WbDrawDispatcher? dispatcher, EnvCellRenderer? environmentCells, PortalDepthMaskRenderer? portalDepth, + TextRenderer? worldText, + TextRenderer? uiText, ClipFrame? clip, TerrainModernRenderer? terrain, SceneLightingUboBinding? lighting) @@ -104,6 +108,8 @@ internal sealed class RuntimeRenderFrameBeginResources : IRenderFrameBeginResour _dispatcher = dispatcher; _environmentCells = environmentCells; _portalDepth = portalDepth; + _worldText = worldText; + _uiText = uiText; _clip = clip; _terrain = terrain; _lighting = lighting; @@ -116,11 +122,8 @@ internal sealed class RuntimeRenderFrameBeginResources : IRenderFrameBeginResour _dispatcher?.BeginFrame(gpuSlot); _environmentCells?.BeginFrame(gpuSlot); _portalDepth?.BeginFrame(gpuSlot); - // TextRenderer (world-hud + retained UI) is off this per-slot int - // pattern as of Campaign V slice V4a — it allocates rings directly - // from the current IGpuFrame at Flush time, with no separate begin - // step (GpuDeviceFrameLifetime resets the device's ring watermark - // when IGpuDevice.BeginFrame runs). + _worldText?.BeginFrame(gpuSlot); + _uiText?.BeginFrame(gpuSlot); _clip?.BeginFrame(gpuSlot); _terrain?.BeginFrame(gpuSlot); _lighting?.BeginFrame(gpuSlot); diff --git a/src/AcDream.App/Rendering/Residency/ResidencyBudgetOptions.cs b/src/AcDream.App/Rendering/Residency/ResidencyBudgetOptions.cs index 4acb4474..121802d6 100644 --- a/src/AcDream.App/Rendering/Residency/ResidencyBudgetOptions.cs +++ b/src/AcDream.App/Rendering/Residency/ResidencyBudgetOptions.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; namespace AcDream.App.Rendering.Residency; @@ -6,7 +6,7 @@ namespace AcDream.App.Rendering.Residency; /// Startup-time residency ceilings. Values preserve the pre-Slice-D cache /// behavior by default and are changed atomically as one profile. /// -internal sealed record ResidencyBudgetOptions( +public sealed record ResidencyBudgetOptions( long ObjectMeshGpuBytes, int ObjectMeshUnownedEntries, long PreparedMeshCpuBytes, diff --git a/src/AcDream.App/Rendering/RetailChaseCamera.cs b/src/AcDream.App/Rendering/RetailChaseCamera.cs index af2115f5..c2eefc8b 100644 --- a/src/AcDream.App/Rendering/RetailChaseCamera.cs +++ b/src/AcDream.App/Rendering/RetailChaseCamera.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; using AcDream.Core.Rendering; @@ -10,7 +10,7 @@ namespace AcDream.App.Rendering; /// docs/research/named-retail/acclient_2013_pseudo_c.txt:95505): /// a STATEFUL sought position that converges from the current swept /// viewer toward the desired boom pose (CameraManager::UpdateCamera -/// 0x00456660 → viewer_sought_position, the #180 fix), 5-frame +/// 0x00456660 → viewer_sought_position, the #180 fix), 5-frame /// velocity-averaged slope-aligned heading frame, mouse-input low-pass /// filter. Pseudocode: /// docs/research/2026-07-06-camera-sought-position-pseudocode.md. @@ -27,7 +27,7 @@ namespace AcDream.App.Rendering; /// Spec: docs/superpowers/specs/2026-05-18-retail-chase-camera-design.md. /// /// -internal sealed class RetailChaseCamera : ICamera +public sealed class RetailChaseCamera : ICamera { // ICamera surface. public Vector3 Position { get; private set; } @@ -35,19 +35,19 @@ internal sealed class RetailChaseCamera : ICamera /// /// The cell the collided viewer-sphere ended in (retail viewer_cell = /// sphere_path.curr_cell). Roots the render mode + indoor visibility + the portal - /// side-test in (Phase W single-viewpoint V1) — the ONE viewpoint. + /// side-test in (Phase W single-viewpoint V1) — the ONE viewpoint. /// Equals the passed player cell when camera collision is off / the probe is null. /// public uint ViewerCellId { get; private set; } public float Aspect { get; set; } = 16f / 9f; public float FovY { get; set; } = MathF.PI / 3f; public Matrix4x4 View { get; private set; } = Matrix4x4.Identity; - // Near plane = retail Render::znear = 0.1 m (decomp :342130/:342173/:1101867 — - // Render::SetFOVRad sets 0.1 flat; the legacy set_vdst variant is max(0.1, vdst·0.25)). + // Near plane = retail Render::znear = 0.1 m (decomp :342130/:342173/:1101867 — + // Render::SetFOVRad sets 0.1 flat; the legacy set_vdst variant is max(0.1, vdst·0.25)). // MUST be smaller than the 0.3 m camera-collision sphere (PhysicsCameraCollisionProbe. // ViewerSphereRadius): with a 1.0 m near, a wall the collided eye sits 0.3 m from - // falls INSIDE the near plane and is clipped away — pressing the camera into a corner - // let you see straight through the wall (§4 corner residual). History: 0.1 landed + // falls INSIDE the near plane and is clipped away — pressing the camera into a corner + // let you see straight through the wall (§4 corner residual). History: 0.1 landed // (137b4f2), was reverted (8bd3492) after correlating with missing indoor textures, // and re-landed once #110 resolved: the textures were the pre-existing #105 // staged-texture-flush drop (WbMeshAdapter.Tick), and 0.1 merely raised its trigger @@ -55,12 +55,12 @@ internal sealed class RetailChaseCamera : ICamera public Matrix4x4 Projection => Matrix4x4.CreatePerspectiveFieldOfView(FovY, Aspect, 0.1f, 5000f); - // ── Public tunables (per-instance) ────────────────────────────── + // ── Public tunables (per-instance) ────────────────────────────── - /// Length of the viewer_offset vector. Retail default ≈ 2.61. + /// Length of the viewer_offset vector. Retail default ≈ 2.61. public float Distance { get; set; } = 2.61f; - /// Angle of the camera above the heading-frame XY plane. Retail default ≈ 0.291 rad (16.7°). + /// Angle of the camera above the heading-frame XY plane. Retail default ≈ 0.291 rad (16.7°). public float Pitch { get; set; } = 0.291f; /// @@ -92,27 +92,27 @@ internal sealed class RetailChaseCamera : ICamera public const float PitchMax = 1.4f; // Retail CameraManager::UpdateCamera convergence-snap thresholds (decomp - // acclient_2013_pseudo_c.txt, 0x00456fcd–0x00457035). SnapEpsilon = 2 × - // 0.000199999995 m ≈ 0.0004 m — the per-frame translation step below which retail + // acclient_2013_pseudo_c.txt, 0x00456fcd–0x00457035). SnapEpsilon = 2 × + // 0.000199999995 m ≈ 0.0004 m — the per-frame translation step below which retail // freezes the boom at an exact fixed point (0x00456fe1). RotCloseEpsilon = - // 0.000199999995 — the Frame::close_rotation tolerance (0x00456fdd). Without the + // 0.000199999995 — the Frame::close_rotation tolerance (0x00456fdd). Without the // snap, Vector3.Lerp asymptotes forever and the boom drifts at rest, walking the eye - // across a portal plane and flipping the viewer cell → the indoor flicker. + // across a portal plane and flipping the viewer cell → the indoor flicker. private const float SnapEpsilon = 0.000199999995f * 2f; private const float RotCloseEpsilon = 0.000199999995f; - // ── Stateful camera state (retail SmartBox's two Positions) ───── + // ── Stateful camera state (retail SmartBox's two Positions) ───── // - // _soughtEye = retail viewer_sought_position — the persisted sweep + // _soughtEye = retail viewer_sought_position — the persisted sweep // TARGET, re-derived each frame from the current swept // viewer (NOT from itself). - // _publishedEye = retail viewer — the swept, published eye; the base + // _publishedEye = retail viewer — the swept, published eye; the base // of next frame's interpolation (SmartBox:: // PlayerPhysicsUpdatedCallback passes &this->viewer // into UpdateCamera, 0x00452d75). // _dampedForward = the sought's look direction. Sweeps translate but // never rotate, so the viewer's rotation is always the - // previous sought rotation — one field serves both. + // previous sought rotation — one field serves both. private readonly Vector3[] _velocityRing = new Vector3[5]; private int _velocityCount; @@ -121,12 +121,12 @@ internal sealed class RetailChaseCamera : ICamera private Vector3 _dampedForward = new(1f, 0f, 0f); private bool _initialised; - // Mouse-filter state — shared by FilterMouseDelta entrypoint. + // Mouse-filter state — shared by FilterMouseDelta entrypoint. private float _lastMouseDeltaX; private float _lastMouseDeltaY; private float _lastFilterTimeSec; - // ── Per-frame entry point ──────────────────────────────────────── + // ── Per-frame entry point ──────────────────────────────────────── /// /// Advance the camera one frame. Caller passes the player's current @@ -177,7 +177,7 @@ internal sealed class RetailChaseCamera : ICamera // 5. Stateful sought position (#180). Retail CameraManager::UpdateCamera // (0x00456660) interpolates FROM THE CURRENT SWEPT VIEWER toward the // desired pose and assigns the result to viewer_sought_position - // (SmartBox::PlayerPhysicsUpdatedCallback 0x00452d60) — the sweep + // (SmartBox::PlayerPhysicsUpdatedCallback 0x00452d60) — the sweep // target converges onto whatever the collision produced last frame // and re-extends gradually. The full-length ideal boom is never swept // directly. Pseudocode: @@ -193,15 +193,15 @@ internal sealed class RetailChaseCamera : ICamera { float tAlpha = ComputeDampingAlpha(CameraDiagnostics.TranslationStiffness, dt); float rAlpha = ComputeDampingAlpha(CameraDiagnostics.RotationStiffness, dt); - // interpolate_origin(viewer.frame → desired, t) — the lerp base is the + // interpolate_origin(viewer.frame → desired, t) — the lerp base is the // VIEWER (0x00456fae), not the previous sought. The forward base is the - // viewer's rotation ≡ the previous sought forward (sweeps never rotate). + // viewer's rotation ≡ the previous sought forward (sweeps never rotate). Vector3 candidateEye = Vector3.Lerp(_publishedEye, targetEye, tAlpha); Vector3 candidateForward = Vector3.Normalize(Vector3.Lerp(_dampedForward, targetForward, rAlpha)); - // Retail UpdateCamera dead-band (0x00456fcd–0x00457035): once the step + // Retail UpdateCamera dead-band (0x00456fcd–0x00457035): once the step // off the viewer is sub-epsilon in translation AND rotation, the sought - // parks EXACTLY ON the viewer — an exact fixed point instead of an + // parks EXACTLY ON the viewer — an exact fixed point instead of an // asymptote. Kills the at-rest drift AND the residual micro-jitter when // pressed against a wall. See ApplyConvergenceSnap + SnapEpsilon. (_soughtEye, _dampedForward, _) = @@ -209,13 +209,13 @@ internal sealed class RetailChaseCamera : ICamera } // 5b. Spring-arm collision (A8.F / #180). Retail SmartBox::update_viewer - // (0x00453ce0) sweeps the viewer_sphere pivot → viewer_sought_position + // (0x00453ce0) sweeps the viewer_sphere pivot → viewer_sought_position // and publishes the swept result as the viewer (set_viewer(curr_pos, 0) - // — the sought is NOT reset on success). Pressed against a wall, the + // — the sought is NOT reset on success). Pressed against a wall, the // sweep ray extends only one interpolation step past the contact, so a - // knife-edge r±ε graze can move the eye by at most that step (sub-mm at + // knife-edge r±ε graze can move the eye by at most that step (sub-mm at // high fps) instead of re-solving the full-length boom with its 0.27 m - // bistable contact pair — the #180 strobe fix. + // bistable contact pair — the #180 strobe fix. Vector3 publishedEye = _soughtEye; // The viewer cell defaults to the player cell (collision off / null probe); the sweep // overwrites it with the swept cell (retail viewer_cell). Always set so GameWindow has a @@ -227,13 +227,13 @@ internal sealed class RetailChaseCamera : ICamera publishedEye = swept.Eye; ViewerCellId = swept.ViewerCellId; // Total-failure fallback = retail set_viewer(player_pos, reset_sought=1) - // (update_viewer :92886 and the cell==0 bail :92775 — both surface here as + // (update_viewer :92886 and the cell==0 bail :92775 — both surface here as // ViewerCellId == 0): the sought resets to the returned position and // re-extends from there. if (swept.ViewerCellId == 0) _soughtEye = swept.Eye; } - // Retail viewer — the base of next frame's interpolation (step 5). + // Retail viewer — the base of next frame's interpolation (step 5). _publishedEye = publishedEye; // 6. Publish renderer surface (from the collided eye; rotation stays the @@ -241,7 +241,7 @@ internal sealed class RetailChaseCamera : ICamera Position = publishedEye; View = Matrix4x4.CreateLookAt(publishedEye, publishedEye + _dampedForward, new Vector3(0f, 0f, 1f)); - // 7. Auto-fade translucency — uses the published (collided) eye so the + // 7. Auto-fade translucency — uses the published (collided) eye so the // player fades once the eye is pulled in close. float d = Vector3.Distance(publishedEye, pivotWorld); PlayerTranslucency = ComputeTranslucency(d); @@ -295,12 +295,12 @@ internal sealed class RetailChaseCamera : ICamera /// public (float outX, float outY) FilterMouseDelta(float rawX, float rawY, float weight, float nowSec) { - // X first — advances the shared timestamp. + // X first — advances the shared timestamp. float x = FilterMouseAxis(rawX, weight, nowSec, ref _lastMouseDeltaX, ref _lastFilterTimeSec, CameraDiagnostics.MouseLowPassWindowSec); // Y uses a throwaway timestamp so the within-window check still uses the original delta // (X already advanced _lastFilterTimeSec to nowSec; if Y reused it, the within-window - // check would be 0 < windowSec which is always true — which is what we want here, since + // check would be 0 < windowSec which is always true — which is what we want here, since // both axes are sampled simultaneously and should both blend.). float yTimeShadow = _lastFilterTimeSec - 1f; // force within-window path for the Y axis float y = FilterMouseAxis(rawY, weight, nowSec, @@ -308,7 +308,7 @@ internal sealed class RetailChaseCamera : ICamera return (x, y); } - // Math primitives — pure, internal-static for unit-testability. + // Math primitives — pure, internal-static for unit-testability. /// /// Pick the heading vector that drives the camera basis. Mirrors @@ -316,8 +316,8 @@ internal sealed class RetailChaseCamera : ICamera /// path (decomp acclient_2013_pseudo_c.txt:95644-95795): /// /// Base heading is the player's facing - /// direction in world space — (cos yaw, sin yaw, 0) - /// — not the velocity vector. Velocity only gates whether + /// direction in world space — (cos yaw, sin yaw, 0) + /// — not the velocity vector. Velocity only gates whether /// slope-alignment fires. /// If is off /// OR the player's horizontal velocity is below epsilon (i.e. @@ -337,7 +337,7 @@ internal sealed class RetailChaseCamera : ICamera /// /// 5-frame averaged player velocity in world space. /// Player facing yaw + any orbit offset, radians. - /// Player's transient_state & 1 — does describe a valid contact plane? + /// Player's transient_state & 1 — does describe a valid contact plane? /// Player's current contact plane normal in world space; ignored when is false. /// User-tunable; when false skips the projection and returns the flat facing direction. internal static Vector3 ComputeHeading( @@ -356,15 +356,15 @@ internal sealed class RetailChaseCamera : ICamera // |vx| > 0.0002 AND |vy| > 0.0002 (decomp :95704, :95713). The // horizontal-magnitude-squared form is a cleaner equivalent. // Without this, the airborne path would still project against - // world up (no-op) which is fine — but the standing-jump case + // world up (no-op) which is fine — but the standing-jump case // wants the historical `direction` fallback that retail uses. float hMagSq = avgVelocity.X * avgVelocity.X + avgVelocity.Y * avgVelocity.Y; if (hMagSq < 1e-4f) return baseHeading; // Pick the projection plane normal: - // grounded → contact_plane.N (slope-aligned camera basis) - // airborne → world up (projection becomes a no-op because - // baseHeading is already in the XY plane — but + // grounded → contact_plane.N (slope-aligned camera basis) + // airborne → world up (projection becomes a no-op because + // baseHeading is already in the XY plane — but // keeping the code path uniform makes the airborne // case impossible to swing vertically). Vector3 normal; @@ -375,13 +375,13 @@ internal sealed class RetailChaseCamera : ICamera // Project baseHeading onto plane perpendicular to normal: // projected = forward - normal * dot(forward, normal) - // On flat ground this is a no-op (dot ≈ 0). On a slope the + // On flat ground this is a no-op (dot ≈ 0). On a slope the // projected vector gains a Z component matching the slope angle, // which tilts the camera basis with the terrain. float dot = Vector3.Dot(baseHeading, normal); Vector3 projected = baseHeading - normal * dot; - // Degenerate: facing nearly parallel to normal (rare — would + // Degenerate: facing nearly parallel to normal (rare — would // require player rotated to face into the ground). Fall back to // the unprojected base heading. if (projected.LengthSquared() < 1e-4f) return baseHeading; @@ -449,7 +449,7 @@ internal sealed class RetailChaseCamera : ICamera Vector3 right; if (MathF.Abs(forward.Z) > 0.99f) { - // Near-vertical forward — use world +X as the secondary axis. + // Near-vertical forward — use world +X as the secondary axis. right = Vector3.Normalize(Vector3.Cross(forward, new Vector3(1f, 0f, 0f))); } else @@ -495,7 +495,7 @@ internal sealed class RetailChaseCamera : ICamera /// /// Exponential-damping rate per frame. /// alpha = clamp(stiffness * dt * 10, 0, 1). At - /// stiffness=0.45, dt=1/60 → ~0.075 + /// stiffness=0.45, dt=1/60~0.075 /// (~150 ms half-life). Matches retail's /// x_1 = stiffness * dt * 10 formulation. /// @@ -508,11 +508,11 @@ internal sealed class RetailChaseCamera : ICamera } /// - /// Retail CameraManager::UpdateCamera dead-band (decomp 0x00456fcd–0x00457035). + /// Retail CameraManager::UpdateCamera dead-band (decomp 0x00456fcd–0x00457035). /// After the per-frame lerp, if the translation step from /// (the interpolation base = the current swept viewer) to /// is below AND the rotation step is below - /// , retail returns the VIEWER unchanged — the sought + /// , retail returns the VIEWER unchanged — the sought /// parks exactly on it (return viewer, 0x00457025). Returns frozen=true /// with the viewer state in that case; otherwise frozen=false with the candidate. /// Both conditions are required (retail couples origin + rotation in the test), @@ -562,7 +562,7 @@ internal sealed class RetailChaseCamera : ICamera /// distance. 0 = fully opaque, 1 = fully transparent. /// Opaque at and beyond 0.45 m; fully transparent at and within /// 0.20 m; linear ramp between. Matches retail's CameraSet:: - /// UpdateCamera distance check (decomp :97703–97725). + /// UpdateCamera distance check (decomp :97703–97725). /// internal static float ComputeTranslucency(float distance) { diff --git a/src/AcDream.App/Rendering/RetailCursorManager.cs b/src/AcDream.App/Rendering/RetailCursorManager.cs index 1c49d6f9..1a73b39d 100644 --- a/src/AcDream.App/Rendering/RetailCursorManager.cs +++ b/src/AcDream.App/Rendering/RetailCursorManager.cs @@ -1,4 +1,4 @@ -using AcDream.App.UI; +using AcDream.App.UI; using AcDream.Core.Textures; using DatReaderWriter; using AcDream.Content; @@ -9,7 +9,7 @@ using Silk.NET.Input; namespace AcDream.App.Rendering; /// Applies retail cursor feedback to Silk using dat MediaDescCursor art when available. -internal sealed class RetailCursorManager +public sealed class RetailCursorManager { private readonly IDatReaderWriter _dats; private readonly object _datLock; diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs index e0d37eab..9b515852 100644 --- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs +++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering.Scene; @@ -11,7 +11,7 @@ namespace AcDream.App.Rendering; /// SmartBox::RenderNormalMode -> RenderDeviceD3D::DrawInside -> /// PView::DrawInside -> ConstructView -> DrawCells. /// -internal sealed class RetailPViewRenderer +public sealed class RetailPViewRenderer { private readonly InteriorEntityPartition.IObserver? _partitionObserver; private readonly ICurrentRenderPViewObserver? _candidateObserver; @@ -44,7 +44,7 @@ internal sealed class RetailPViewRenderer private readonly PortalVisibilityFrame _outdoorBuildingFrameScratch = new(); // #124: per-building look-in frames under an INTERIOR root, drawn as a - // landscape-stage sub-pass (DrawBuildingLookIns) — never merged into the + // landscape-stage sub-pass (DrawBuildingLookIns) — never merged into the // main frame (see DrawInside). Rebuilt each interior-root frame. private readonly List _lookInFrames = new(); private readonly Stack _lookInFramePool = new(); @@ -59,7 +59,7 @@ internal sealed class RetailPViewRenderer // MP-Alloc (2026-07-05): the frame's entity partition (ByCell/OutdoorStatic/ // Dynamics), reused across frames instead of `new`ing a Result (a Dictionary // + 2 Lists, plus one List per visible cell) every DrawInside - // call. See InteriorEntityPartition.Partition(Result, ...) — clears in + // call. See InteriorEntityPartition.Partition(Result, ...) — clears in // place and reuses each cell's list across frames when the cell stays // visible. // Slice G4: this is now a diagnostic/fallback oracle only. Normal @@ -88,7 +88,7 @@ internal sealed class RetailPViewRenderer } // T2 (BR-4): retail has NO distance constant on the flood-admission chain - // (DrawBuilding → portal walk → ConstructView: viewconeCheck + side test + + // (DrawBuilding → portal walk → ConstructView: viewconeCheck + side test + // GetClip + GetVisible only). The old 48 m seed cap is replaced by the // caller's per-building frustum pre-gate on aperture bounds (GameWindow's // gather); seeds themselves are unbounded. @@ -113,28 +113,28 @@ internal sealed class RetailPViewRenderer drawLiftZ: PortalVisibilityBuilder.ShellDrawLiftZ, reuseFrame: _mainPortalFrameScratch); - // R-A2: outdoor root — flood each nearby building SEPARATELY from its own entrance and merge + // R-A2: outdoor root — flood each nearby building SEPARATELY from its own entrance and merge // the small (~2-cell) per-building views into the frame. Retail reaches building interiors via // the terrain BSP -> DrawPortal -> ConstructView(CBldPortal) (decomp:326881/433895/433827); the // land root itself has no portals (it floods nothing into buildings). Per-building seeding is - // robust to the eye's ~36 µm rest jitter where the pre-R-A2 single reverse-portal flood + // robust to the eye's ~36 µm rest jitter where the pre-R-A2 single reverse-portal flood // oscillated as the chase eye grazed a doorway (the indoor flap). if (ctx.RootCell.IsOutdoorNode && ctx.NearbyBuildingCells is not null) MergeNearbyBuildingFloods(ctx, pvFrame); // #124: interior-root building look-ins. Retail runs the look-in INSIDE - // the landscape stage for ANY root — LScape::draw is the FIRST call of + // the landscape stage for ANY root — LScape::draw is the FIRST call of // DrawCells' outside-view branch (pc:432719), strictly BEFORE the depth // clear (pc:432732) and the exit-portal seals (pc:432785); a far // building seen through our doorway floods clipped to the INSTALLED // outside view (GetClip vs current view, ConstructView(CBldPortal) // 0x005a59a0). These frames therefore draw in DrawBuildingLookIns - // (inside the landscape stage), NEVER merged into the main frame — a + // (inside the landscape stage), NEVER merged into the main frame — a // merged cell would draw post-clear and z-fail against the root's seal // (its geometry is beyond the door plane). The eye-side seed test // self-excludes the root's own building (the eye is on its interior // side). Outdoor roots keep the MergeNearbyBuildingFloods path above - // (no depth clear under outdoor roots — the merged form is equivalent + // (no depth clear under outdoor roots — the merged form is equivalent // there). if (!ctx.RootCell.IsOutdoorNode && ctx.NearbyBuildingCells is not null @@ -149,7 +149,7 @@ internal sealed class RetailPViewRenderer // R1: draw EVERY visible cell (retail cell_draw_list), not only the cells the // assembler handed a clip-slot. This feeds the Prepare filter + entity partition, - // so every visible cell's shell has a prepared batch and seals — killing the grey + // so every visible cell's shell has a prepared batch and seals — killing the grey // (the old clipAssembly.CellIdToSlot.Keys filter silently dropped slot-less cells). // Per-slice trim still applies in DrawEnvCellShells (Task 4 makes it self-contained). _drawableCellsScratch.Clear(); @@ -158,7 +158,7 @@ internal sealed class RetailPViewRenderer passes.UseIndoorMembershipOnlyRouting(); // #124: look-in cells need prepared shell batches + their statics routed - // into partition.ByCell (consumed ONLY by DrawBuildingLookIns — the main + // into partition.ByCell (consumed ONLY by DrawBuildingLookIns — the main // cell-object pass iterates pvFrame.OrderedVisibleCells, which never // contains them). drawableCells itself stays the MAIN flood: it feeds the // seals, the outside-stage predicate, and the frame result. @@ -174,22 +174,22 @@ internal sealed class RetailPViewRenderer } // (#176 correction, 2026-07-06: the flood-scoped light-pool rebuild that ran - // here was the seam-floor flicker mechanism — retail's visible_cell_table is - // the RESIDENT-cell registry, not the frame flood — and is deleted. The pool + // here was the seam-floor flicker mechanism — retail's visible_cell_table is + // the RESIDENT-cell registry, not the frame flood — and is deleted. The pool // is built once per frame in GameWindow, player-anchored.) passes.PrepareCellBatches(ctx, prepareCells); - // T1 (fused BR-2/3): retail's frame order — static world, then the - // aperture depth writes, then interior cells WHOLE far→near, then + // T1 (fused BR-2/3): retail's frame order — static world, then the + // aperture depth writes, then interior cells WHOLE far→near, then // per-cell statics, then ALL dynamics last (retail draws objects after // cells: PView::DrawCells Ghidra 0x005a4840; DrawBuilding 0x0059f2a0). // The geometric shell chop (gl_ClipDistance crop, 927fd8f/9ce335e) is - // DELETED — retail never clips cell geometry; aperture exactness comes + // DELETED — retail never clips cell geometry; aperture exactness comes // from the punch/seal depth writes + the z-buffer, and the dynamics- // last order is what makes the punch safe (the first BR-2 attempt // punched after dynamics and erased the player, reverted 88be519). - // T3 (BR-5): retail viewconeCheck — meshes are sphere-CULLED per view, + // T3 (BR-5): retail viewconeCheck — meshes are sphere-CULLED per view, // never clipped (Ghidra 0x0054c250). Built once per frame from the // assembled slices + this frame's view-projection. var viewcone = ViewconeCuller.Build( @@ -256,19 +256,19 @@ internal sealed class RetailPViewRenderer passes.EmitDiagnostics(ctx, result); // #118: stage assignment for dynamics under an INTERIOR root. Retail - // draws the OUTSIDE world's objects inside the landscape stage — + // draws the OUTSIDE world's objects inside the landscape stage — // PView::DrawCells runs LScape::draw FIRST (pc:432719), then the gated // full depth clear (pc:432731-432732) and the exit-portal SEALS // (pc:432785-432786); DrawBlock draws every landcell's objects via // DrawSortCell (0x005a17c0, pc:430124). A dynamic deferred to our // single last pass instead z-fails against the seal's true-depth stamp - // the moment it stands beyond the door plane — the house-exit + // the moment it stands beyond the door plane — the house-exit // clip+vanish (pinned by HouseExitWalkReplayTests). So under an // interior root: outdoor-classified dynamics draw in the outside // stage; an indoor dynamic whose sphere STRADDLES an exit portal // draws in BOTH stages (retail's per-overlapped-cell shadow-part // draw, DrawBlock pc:430056-430064) so neither body half clips at the - // plane. Outdoor roots keep ALL dynamics in the last pass — our + // plane. Outdoor roots keep ALL dynamics in the last pass — our // z-buffered equivalent of retail's painter-ordered outdoor pass (the // BR-2 punch-after-dynamics lesson, reverted 88be519). _outsideStageDynamics.Clear(); @@ -362,11 +362,11 @@ internal sealed class RetailPViewRenderer // on the cell (Render::copy_view appends + view_count++, Ghidra 0x0054dfc0; // a cell visible through two apertures holds two views, all consumed // downstream). The old first-wins (`ContainsKey -> continue`) dropped the - // second building flood's views whenever a cell was already in the frame — + // second building flood's views whenever a cell was already in the frame — // the multiview-loss-first-wins divergence (a named #109 suspect: per-frame // winner flips between apertures). CellView.Add dedups exact/collinear // re-emissions (the dac8f6a CanonicalKey), so unioning is convergent. - // OutsideView is NOT merged — the outdoor root already seeds full-screen + // OutsideView is NOT merged — the outdoor root already seeds full-screen // terrain, and ConstructViewBuilding (BuildFromExterior) leaves OutsideView // empty (it stops at exit portals once inside the building). private static void MergeBuildingFrame(PortalVisibilityFrame target, PortalVisibilityFrame src) @@ -393,8 +393,8 @@ internal sealed class RetailPViewRenderer } // #124: per-building look-in floods for an INTERIOR root, seeded clipped - // against the OutsideView (retail: GetClip runs under the INSTALLED view — - // the accumulated doorway region — so a far building floods only within the + // against the OutsideView (retail: GetClip runs under the INSTALLED view — + // the accumulated doorway region — so a far building floods only within the // doorway, ConstructView(CBldPortal) 0x005a59a0 via PView::GetClip // 0x005a4320). Same grouping as MergeNearbyBuildingFloods; the root's own // building self-excludes via the seed eye-side test. @@ -445,15 +445,15 @@ internal sealed class RetailPViewRenderer private void ResetBuildingGroups() => _buildingGroups.Reset(); - // #124: draw the interior-root look-ins INSIDE the landscape stage — - // retail's placement (LScape::draw → DrawBlock → DrawSortCell → + // #124: draw the interior-root look-ins INSIDE the landscape stage — + // retail's placement (LScape::draw → DrawBlock → DrawSortCell → // DrawBuilding runs as the FIRST call of DrawCells' outside-view branch, // pc:432719, before the depth clear + seals). Per building: punch ALL - // apertures first (retail finishes build_draw_portals_only pass 1 — the - // far-Z maxZ1 punch — across the whole building BSP before pass 2 floods), - // then draw the flooded cells' shells + statics far→near (the nested + // apertures first (retail finishes build_draw_portals_only pass 1 — the + // far-Z maxZ1 punch — across the whole building BSP before pass 2 floods), + // then draw the flooded cells' shells + statics far→near (the nested // DrawCells' DrawEnvCell + DrawObjCellForDummies; its outside_view is - // empty by construction — PView ctor draw_landscape=0 — so no recursive + // empty by construction — PView ctor draw_landscape=0 — so no recursive // landscape/clear/seal). Anything rasterized outside an aperture is // repainted by the root's own shells after the depth clear, so over-draw // here is color-safe; statics draw whole (the main viewcone has no entry @@ -491,12 +491,12 @@ internal sealed class RetailPViewRenderer } } - // Pass 2: shells + statics, far→near. + // Pass 2: shells + statics, far→near. passes.UseIndoorMembershipOnlyRouting(); // Opaque shells batched per building into ONE Render (this building's // aperture punches above already ran; z-buffer handles order and - // lighting is per-instance CellId-keyed) — was one heavy per-frame + // lighting is per-instance CellId-keyed) — was one heavy per-frame // Render per cell. Per-cell entity/particle work stays in the loop. _shellBatch.Clear(); foreach (uint cid in frame.OrderedVisibleCells) @@ -509,7 +509,7 @@ internal sealed class RetailPViewRenderer uint cellId = frame.OrderedVisibleCells[i]; _oneCell.Clear(); _oneCell.Add(cellId); - // Opaque shell batched above. Transparent stays per-cell (far→near) + // Opaque shell batched above. Transparent stays per-cell (far→near) // for correct compositing; skipped for opaque-only cells. if (passes.CellHasTransparentShell(cellId)) passes.DrawTransparentCellShells(_oneCell); @@ -523,7 +523,7 @@ internal sealed class RetailPViewRenderer // #131 ROOT CAUSE: DYNAMICS living in a look-in cell (the // Holtburg hall-porch PORTAL, pCell 0xA9B4017A) draw NOWHERE - // under an interior root — DrawDynamicsLast viewcone-culls + // under an interior root — DrawDynamicsLast viewcone-culls // them (the main cone has no entries for look-in cells), and // post-clear they would z-fail against the root's seal anyway // (the #118 lesson). Retail draws a look-in cell's objects @@ -576,7 +576,7 @@ internal sealed class RetailPViewRenderer _cellStaticScratch, _oneCell); - // The cell-particles pass for look-in cells — retail's + // The cell-particles pass for look-in cells — retail's // nested DrawCells draws objects WITH their emitters. foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId)) passes.DrawCellParticles(ctx, new RetailPViewCellSliceContext( @@ -600,7 +600,7 @@ internal sealed class RetailPViewRenderer // #131/#132 (the FlushAlphaList deferral): retail collects ALL alpha // draws of the landscape stage and flushes them ONCE after LScape::draw - // (D3DPolyRender::FlushAlphaList, DrawCells pc:432722) — so translucent + // (D3DPolyRender::FlushAlphaList, DrawCells pc:432722) — so translucent // landscape content (portal swirl meshes, flame particles) composites // AFTER the building look-ins. Our dispatcher draws translucency inside // each Draw call, so the stage is split in TWO phases instead: EARLY = @@ -609,12 +609,12 @@ internal sealed class RetailPViewRenderer // LATE = outside-stage dynamics' meshes + ALL scene particles + // weather. Content drawn early and overlapped by a look-in aperture // was otherwise overpainted by the far interior (translucents write no - // depth to protect themselves) — the portal-swirl/candle-flame class. + // depth to protect themselves) — the portal-swirl/candle-flame class. int probeSliceIndex = 0; foreach (var slice in clipAssembly.OutsideViewSlices) { passes.SetTerrainClip(slice.Planes); - // T3 (BR-5): entities are never hard-clipped — retail viewcone- + // T3 (BR-5): entities are never hard-clipped — retail viewcone- // CHECKS each mesh's sphere against the view (Ghidra 0x0054c250) // and draws it whole. The old per-slice entity clip routing // (gl_ClipDistance via SetClipRouting) is replaced by the sphere @@ -663,7 +663,7 @@ internal sealed class RetailPViewRenderer }); } - // #124: far-building look-ins draw HERE — still inside the landscape + // #124: far-building look-ins draw HERE — still inside the landscape // stage (their punches mark against the terrain/exterior depth just // drawn), strictly BEFORE the depth clear + seals below, matching // retail's LScape::draw placement (DrawCells pc:432719 vs 432732/432785). @@ -675,11 +675,11 @@ internal sealed class RetailPViewRenderer frameEntityPasses, in frameView); - // LATE phase (per slice): outside-stage dynamics' meshes (#118 — drawn + // LATE phase (per slice): outside-stage dynamics' meshes (#118 — drawn // pre-clear so the seal protects their aperture pixels; AFTER the // look-ins so a translucent portal mesh blends over a far interior // instead of being overpainted) + the scene-particle owners (statics + - // dynamics cone survivors — flames ride here for the same reason). + // dynamics cone survivors — flames ride here for the same reason). probeSliceIndex = 0; foreach (var slice in clipAssembly.OutsideViewSlices) { @@ -700,7 +700,7 @@ internal sealed class RetailPViewRenderer if (ownerPass) _lateParticleOwnerScratch.Add(e.Id); // #131 owner watchlist (throwaway): ACDREAM_DUMP_ENTITY ids - // double as an ENTITY-id watchlist here — one line per watched + // double as an ENTITY-id watchlist here — one line per watched // outdoor-static owner per CHANGE of its cone verdict. passes.EmitOutStageOwner( e, @@ -764,11 +764,11 @@ internal sealed class RetailPViewRenderer }); } - // #131: UNATTACHED emitters (AttachedObjectId == 0 — portal swirls, + // #131: UNATTACHED emitters (AttachedObjectId == 0 — portal swirls, // campfires, ground effects anchored at a position) have no owner id // to ride any of the id-filtered particle passes. The outdoor root // has the dedicated T3 pass for them; an INTERIOR root had NO pass - // at all. Draw them ONCE per frame (not per slice — alpha particles + // at all. Draw them ONCE per frame (not per slice — alpha particles // must not double-draw, the #121 lesson), at the END of the landscape // stage: after the clear they would z-fail against the doorway seal. if (!ctx.RootCell.IsOutdoorNode) @@ -780,7 +780,7 @@ internal sealed class RetailPViewRenderer passes.FlushLandscapeAlpha(); // T1: retail clears the FULL depth buffer ONCE between the outside - // stage and the interior stage (PView::DrawCells, Ghidra 0x005a4840 — + // stage and the interior stage (PView::DrawCells, Ghidra 0x005a4840 — // Clear gated on portalsDrawnCount; exact gate semantics is a plan // open question, staged as "any outside slice drawn"), then re-stamps // every outside-leading portal's TRUE depth (the seals, @@ -819,20 +819,20 @@ internal sealed class RetailPViewRenderer IRetailPViewPassExecutor passes, PortalVisibilityFrame pvFrame) { - // T1 (fused BR-2/3): retail DrawCells Loop 2 — every visible cell's - // shell drawn WHOLE, reverse cell_draw_list (far→near), drawn once. + // T1 (fused BR-2/3): retail DrawCells Loop 2 — every visible cell's + // shell drawn WHOLE, reverse cell_draw_list (far→near), drawn once. // Retail NEVER clips cell geometry: the production path is the // prebuilt mesh (DrawEnvCell use_built_mesh, pc:427905; the // planeMask=0xffffffff legacy submit means skip-all-edges), and // aperture exactness comes from the punch/seal depth writes + the // z-buffer + this order. The former gl_ClipDistance chop // (927fd8f/9ce335e, #114) is deleted with this rewrite. - // Per-cell opaque+transparent keeps the far→near transparent + // Per-cell opaque+transparent keeps the far→near transparent // compositing the per-cell loop already provided. passes.UseIndoorMembershipOnlyRouting(); // Opaque: ONE batched Render for all shell cells (was one heavy per-frame - // Render call PER cell — the dense-town FPS sink, ~94 calls/24.75ms at + // Render call PER cell — the dense-town FPS sink, ~94 calls/24.75ms at // Arwic). Opaque needs no draw order (z-buffer), and lighting is // per-instance (CellId-keyed light SSBO in EnvCellRenderer.RenderModernMDI- // Internal), so cross-cell batching is visually identical. The filtered @@ -856,16 +856,16 @@ internal sealed class RetailPViewRenderer passes.DrawTransparentCellShellsOrdered(_orderedTransparentShellCells); } - // T1: the frame's single LAST entity pass — ALL server-spawned dynamics + // T1: the frame's single LAST entity pass — ALL server-spawned dynamics // (player, NPCs, doors, items), indoor or out, drawn after the static // world + punches + interior cells. Depth-tested, never hard-clipped - // (retail draws objects per cell AFTER cells and viewcone-culls them — + // (retail draws objects per cell AFTER cells and viewcone-culls them — // PView::DrawCells epilogue Ghidra 0x005a4840; the sphere-vs-view cull is // T3). Drawing dynamics last is what makes the aperture punch safe. - // T3 (BR-5): each dynamic is viewcone-culled like retail — sphere vs its + // T3 (BR-5): each dynamic is viewcone-culled like retail — sphere vs its // cell's views; outdoor/unresolved vs the outside views (pass-all under // the outdoor root's full-screen outside view). A dynamic in a NON-flooded - // room culls HERE — retail never reaches an object whose cell is not in + // room culls HERE — retail never reaches an object whose cell is not in // the draw list; the partition keeps routing it so the CULL (not the // visibility set) drops it, exactly retail's shape. private void DrawDynamicsLast( @@ -928,12 +928,12 @@ internal sealed class RetailPViewRenderer && AcDream.App.Streaming.EntityVanishProbe.PlayerGuid != 0 && e.ServerGuid == AcDream.App.Streaming.EntityVanishProbe.PlayerGuid; // #118: under an interior root, outdoor-classified dynamics drew in - // the outside stage (pre-clear, seal-protected) — retail draws them + // the outside stage (pre-clear, seal-protected) — retail draws them // via LScape::draw's per-landcell DrawSortCell, never in the // post-seal cell-object epilogue (PView::DrawCells pc:432719 vs // pc:432878). Drawing them here instead z-fails them against the // seal. Indoor dynamics (incl. exit-portal straddlers, which drew - // in BOTH stages) stay — this pass is retail's loop C. + // in BOTH stages) stay — this pass is retail's loop C. if (!rootIsOutdoor && !indoor) { if (isProbePlayer) @@ -976,12 +976,12 @@ internal sealed class RetailPViewRenderer visibleCellIds: null); // #121: dynamics' attached emitters (portal swirls, creature effects) - // gate through the SAME cone-surviving owner set as their meshes — + // gate through the SAME cone-surviving owner set as their meshes — // retail draws emitters with the owner object. Before this callback, // dynamics' emitters fell through EVERY particle filter under the pview // path (the landscape slice carries outdoor statics + #118 outside- // stage dynamics; the cell callback carries cell statics; T4 deleted - // the old clipRoot==null global pass from normal frames) — all world + // the old clipRoot==null global pass from normal frames) — all world // portals went invisible. Outside-stage dynamics are excluded here: // their emitters already drew in the landscape slice (alpha-blended // particles must not double-draw, unlike the depth-idempotent meshes). @@ -1049,29 +1049,29 @@ internal sealed class RetailPViewRenderer return; } - // T1: per-cell STATIC object lists only (dat-baked 0x40 statics) — - // dynamics moved to DrawDynamicsLast. Far→near with the cells, after - // the shells (retail DrawCells epilogue: PortalList = cell's views → + // T1: per-cell STATIC object lists only (dat-baked 0x40 statics) — + // dynamics moved to DrawDynamicsLast. Far→near with the cells, after + // the shells (retail DrawCells epilogue: PortalList = cell's views → // DrawObjCell, Ghidra 0x005a4840). T3 (BR-5): each static's sphere is - // tested against ITS CELL's views (retail viewconeCheck) — the + // tested against ITS CELL's views (retail viewconeCheck) — the // statics-through-walls fix: a static whose sphere is outside every // view of its cell no longer paints through the wall (the cottage // phantom staircase's draw path). // Dense-town FPS iteration-1 (spec 2026-06-23-cellobject-draw-batching): // the per-cell DrawEntityBucket calls below were the top CPU sink at Arwic // (cellobjects ~3.5 ms/frame; each WbDrawDispatcher.Draw orphans 6 SSBOs + - // full state setup). Collapse them into ONE cross-cell batched draw — the + // full state setup). Collapse them into ONE cross-cell batched draw — the // shipped cells-shell batching pattern applied to cell OBJECTS. Two loops // preserve the statics-before-particles depth order: loop 1 culls + // accumulates every cell's survivors and draws them once; loop 2 runs the // per-cell particle passes AFTER the statics own the depth buffer (particles // depth-test but write no depth). The dispatcher sorts opaque front-to-back // and transparent back-to-front by group distance (WbDrawDispatcher.cs: - // 1469-1470), so cross-cell batching composites correctly — equal-or-better + // 1469-1470), so cross-cell batching composites correctly — equal-or-better // than the old per-cell-bucketed order. visibleCellIds = the union of cells, // so the dispatcher admits exactly the same survivor set. - // Loop 1: per-cell viewcone cull → accumulate survivors + the union of cells. + // Loop 1: per-cell viewcone cull → accumulate survivors + the union of cells. _allCellStatics.Clear(); _cellObjCells.Clear(); for (int i = pvFrame.OrderedVisibleCells.Count - 1; i >= 0; i--) @@ -1099,7 +1099,7 @@ internal sealed class RetailPViewRenderer } // ONE batched static-object draw for every visible cell (was N per-cell - // WbDrawDispatcher.Draw calls). T1: per-cell STATIC lists only — dynamics + // WbDrawDispatcher.Draw calls). T1: per-cell STATIC lists only — dynamics // draw in DrawDynamicsLast. T3 (BR-5): each static was sphere-tested against // ITS cell's views above (the statics-through-walls fix is preserved by the // cull; only the draw is batched). @@ -1124,20 +1124,20 @@ internal sealed class RetailPViewRenderer _cellObjCells); } - // Cell-particle pass — consolidated across ALL visible cells into ONE + // Cell-particle pass — consolidated across ALL visible cells into ONE // draw. Was per-cell, and each call re-walked the ENTIRE live particle set - // (RetailPViewPassExecutor.DrawCellParticles → ParticleRenderer.Draw enumerates every - // live emitter), i.e. O(cells × particles) — the dense-town cellobjects + // (RetailPViewPassExecutor.DrawCellParticles → ParticleRenderer.Draw enumerates every + // live emitter), i.e. O(cells × particles) — the dense-town cellobjects // sink (~5 ms at Arwic). Static owners are disjoint per cell, so the UNION // (= _allCellStatics, already accumulated above for the batched draw) draws // EXACTLY the same emitters: the callback gates on owner id (the cone- // surviving set), the renderer sorts globally back-to-front, and the per- // cell slice was never used for clipping (the scissor gate was deleted in - // T3 — RetailPViewPassExecutor.DrawCellParticles disables clip distances). Runs after + // T3 — RetailPViewPassExecutor.DrawCellParticles disables clip distances). Runs after // the batched static draw so emitters depth-test against the statics now in // the buffer (the statics-before-particles order). cellId/slice are unused - // by the particle pass — pass NoClipSlice + the union owner list. This also - // drops the per-cell BuildDrawList allocations (N → 1). + // by the particle pass — pass NoClipSlice + the union owner list. This also + // drops the per-cell BuildDrawList allocations (N → 1). if (frameEntityPasses is not null || _allCellStatics.Count > 0) { @@ -1199,7 +1199,7 @@ internal sealed class RetailPViewRenderer private readonly List _cellStaticScratch = new(); private readonly List _dynamicsScratch = new(); // #118: dynamics assigned to the OUTSIDE stage this frame (interior roots - // only) — outdoor-classified + exit-portal straddlers. Cleared per frame. + // only) — outdoor-classified + exit-portal straddlers. Cleared per frame. private readonly List _outsideStageDynamics = new(); // Dense-town FPS iteration-1 (cellobject batching): all visible cells' // viewcone-surviving statics accumulated for ONE batched DrawEntityBucket, @@ -1274,17 +1274,17 @@ internal sealed class RetailPViewRenderer /// /// #118 stage assignment for a dynamic under an INTERIOR root: does it draw - /// in the OUTSIDE (landscape) stage — before the gated depth clear and the - /// exit-portal seals — like retail's per-landcell object draw - /// (LScape::draw → DrawBlock 0x005a17c0 → DrawSortCell pc:430124, run at + /// in the OUTSIDE (landscape) stage — before the gated depth clear and the + /// exit-portal seals — like retail's per-landcell object draw + /// (LScape::draw → DrawBlock 0x005a17c0 → DrawSortCell pc:430124, run at /// the top of PView::DrawCells pc:432719)? /// /// True for outdoor-classified dynamics (their fragments lie beyond the /// door plane and would z-fail the seal in the last pass), and for INDOOR /// dynamics whose sphere straddles an exit-portal plane of their flood- - /// visible cell — retail draws an object once per overlapped shadow cell + /// visible cell — retail draws an object once per overlapped shadow cell /// (DrawBlock pc:430056-430064), so a threshold-straddling body draws in - /// both stages and neither half clips at the plane. Pure — also driven + /// both stages and neither half clips at the plane. Pure — also driven /// headlessly by HouseExitWalkReplayTests as the ordering contract. /// public static bool DynamicDrawsInOutsideStage( @@ -1299,7 +1299,7 @@ internal sealed class RetailPViewRenderer uint cellId = parentCellId!.Value; if (!drawableCells.Contains(cellId)) - return false; // not in the flood — the last-pass cone cull owns it + return false; // not in the flood — the last-pass cone cull owns it var cell = cells.Find(cellId); if (cell is null) return false; @@ -1320,7 +1320,7 @@ internal sealed class RetailPViewRenderer return false; } - // Conservative bounding sphere from the entity's cached AABB — the same + // Conservative bounding sphere from the entity's cached AABB — the same // bounds source the dispatcher's frustum cull uses. private static void EntitySphere(WorldEntity e, out Vector3 center, out float radius) { @@ -1343,7 +1343,7 @@ internal sealed class RetailPViewRenderer } -internal interface IRetailPViewCellSource +public interface IRetailPViewCellSource { LoadedCell? Find(uint cellId); } @@ -1354,7 +1354,7 @@ internal interface IRetailPViewCellSource /// pass only; visibility construction and draw ordering remain renderer-owned. /// All frame inputs and results are borrowed for the duration of the call. /// -internal interface IRetailPViewPassExecutor +public interface IRetailPViewPassExecutor { void AbortFrame(); void BeginFrame(); @@ -1565,7 +1565,7 @@ internal sealed class BuildingGroupScratch } } -internal sealed class RetailPViewFrameInput +public sealed class RetailPViewFrameInput { public LoadedCell RootCell { get; private set; } = null!; @@ -1669,7 +1669,7 @@ internal sealed class RetailPViewFrameInput /// frame objects are deliberately reused to keep the render loop allocation /// free; consumers must copy any state they need to retain asynchronously. /// -internal sealed class RetailPViewFrameResult +public sealed class RetailPViewFrameResult { public PortalVisibilityFrame PortalFrame { get; private set; } = null!; public ClipFrameAssembly ClipAssembly { get; private set; } = null!; @@ -1712,17 +1712,17 @@ internal sealed class RetailPViewFrameResult diagnosticPartition); } -internal readonly record struct RetailPViewLandscapeSliceContext( +public readonly record struct RetailPViewLandscapeSliceContext( ClipViewSlice Slice, IReadOnlyList OutdoorEntities) { internal RenderFrameEntityDrawRequest? EntityDraw { get; init; } } -/// #131/#132: the late landscape phase's per-slice payload — +/// #131/#132: the late landscape phase's per-slice payload — /// outside-stage dynamics to mesh-draw, plus the full scene-particle owner /// set (statics + dynamics cone survivors) the attached-emitter filter keys on. -internal readonly record struct RetailPViewLandscapeLateSliceContext( +public readonly record struct RetailPViewLandscapeLateSliceContext( ClipViewSlice Slice, IReadOnlyList Dynamics, IReadOnlySet ParticleOwnerIds) @@ -1730,7 +1730,7 @@ internal readonly record struct RetailPViewLandscapeLateSliceContext( internal RenderFrameEntityDrawRequest? EntityDraw { get; init; } } -internal readonly record struct RetailPViewCellSliceContext( +public readonly record struct RetailPViewCellSliceContext( uint CellId, ClipViewSlice Slice, IReadOnlySet ParticleOwnerIds); diff --git a/src/AcDream.App/Rendering/RetailParticleGeometryClassifier.cs b/src/AcDream.App/Rendering/RetailParticleGeometryClassifier.cs index 484fef9e..2d221c27 100644 --- a/src/AcDream.App/Rendering/RetailParticleGeometryClassifier.cs +++ b/src/AcDream.App/Rendering/RetailParticleGeometryClassifier.cs @@ -1,4 +1,4 @@ -using System; +using System; using AcDream.Core.Meshing; using DatReaderWriter.DBObjs; @@ -10,7 +10,7 @@ namespace AcDream.App.Rendering; /// degrade has mode 1, is drawn as its authored 3D mesh. Every other first /// degrade mode uses the camera-facing 2D presentation. /// -internal static class RetailParticleGeometryClassifier +public static class RetailParticleGeometryClassifier { public static RetailParticleGeometryKind Classify(uint? firstDegradeMode) => firstDegradeMode is uint mode && mode != 1u diff --git a/src/AcDream.App/Rendering/SamplerCache.cs b/src/AcDream.App/Rendering/SamplerCache.cs index fc874764..91647bab 100644 --- a/src/AcDream.App/Rendering/SamplerCache.cs +++ b/src/AcDream.App/Rendering/SamplerCache.cs @@ -1,4 +1,4 @@ -using System; +using System; using Silk.NET.OpenGL; namespace AcDream.App.Rendering; @@ -7,7 +7,7 @@ namespace AcDream.App.Rendering; /// Two persistent GL sampler objects (Repeat + ClampToEdge) created once /// per GL context. Renderers the appropriate /// one to a texture unit instead of mutating per-texture -/// GL_TEXTURE_WRAP_S/T state — sampler state overrides the +/// GL_TEXTURE_WRAP_S/T state — sampler state overrides the /// texture's own wrap parameters, so two renderers can share the same /// texture handle but sample it with different wrap modes safely. /// @@ -16,7 +16,7 @@ namespace AcDream.App.Rendering; /// references/WorldBuilder/Chorizite.OpenGLSDLBackend/OpenGLGraphicsDevice.cs:115-132. /// Filter modes match 's upload defaults /// (Linear / Linear, no mipmaps) so binding either sampler doesn't -/// change the visual filtering behavior — only the wrap behavior at +/// change the visual filtering behavior — only the wrap behavior at /// UVs outside [0, 1]. /// /// @@ -28,7 +28,7 @@ namespace AcDream.App.Rendering; /// per-texture wrap state. /// /// -internal sealed class SamplerCache : IDisposable +public sealed class SamplerCache : IDisposable { private readonly GL _gl; private readonly ResourceCleanupGroup _resources; diff --git a/src/AcDream.App/Rendering/SceneLightingUboBinding.cs b/src/AcDream.App/Rendering/SceneLightingUboBinding.cs index d5ae01e6..72b02277 100644 --- a/src/AcDream.App/Rendering/SceneLightingUboBinding.cs +++ b/src/AcDream.App/Rendering/SceneLightingUboBinding.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Runtime.InteropServices; using AcDream.App.Rendering.Wb; using AcDream.Core.Lighting; @@ -13,7 +13,7 @@ namespace AcDream.App.Rendering; /// consistent data without per-shader re-upload. /// /// -/// Usage (r12 §13.2 + r13 §12.3): +/// Usage (r12 §13.2 + r13 §12.3): /// /// Instantiate once at startup, after the GL context exists. /// Each frame, after , call with a freshly-built . @@ -21,7 +21,7 @@ namespace AcDream.App.Rendering; /// /// /// -internal sealed unsafe class SceneLightingUboBinding : IDisposable +public sealed unsafe class SceneLightingUboBinding : IDisposable { private readonly GL _gl; private uint _ubo; diff --git a/src/AcDream.App/Rendering/ScreenPolygonClip.cs b/src/AcDream.App/Rendering/ScreenPolygonClip.cs index 16617645..666b5fff 100644 --- a/src/AcDream.App/Rendering/ScreenPolygonClip.cs +++ b/src/AcDream.App/Rendering/ScreenPolygonClip.cs @@ -1,4 +1,4 @@ -// ScreenPolygonClip.cs +// ScreenPolygonClip.cs // // Phase A8.F: 2D convex-polygon intersection (Sutherland-Hodgman). // Ports the BEHAVIOR of retail ACRender::polyClipFinish (the screen-space @@ -11,7 +11,7 @@ using System.Numerics; namespace AcDream.App.Rendering; -internal static class ScreenPolygonClip +public static class ScreenPolygonClip { private const float Eps = 1e-7f; diff --git a/src/AcDream.App/Rendering/Shader.cs b/src/AcDream.App/Rendering/Shader.cs index 1a431ce4..b74960ae 100644 --- a/src/AcDream.App/Rendering/Shader.cs +++ b/src/AcDream.App/Rendering/Shader.cs @@ -1,9 +1,9 @@ -using System.Numerics; +using System.Numerics; using Silk.NET.OpenGL; namespace AcDream.App.Rendering; -internal sealed class Shader : IDisposable +public sealed class Shader : IDisposable { private readonly GL _gl; private readonly Dictionary _uniformLocations = new(StringComparer.Ordinal); @@ -15,10 +15,10 @@ internal sealed class Shader : IDisposable } /// - /// Campaign V slice V2 (docs/plans/2026-07-27-vulkan-campaign.md §3.4): when + /// Campaign V slice V2 (docs/plans/2026-07-27-vulkan-campaign.md §3.4): when /// is true, the text of - /// Shaders/common.glsl — sitting alongside - /// — is spliced into both sources right after their leading + /// Shaders/common.glsl — sitting alongside + /// — is spliced into both sources right after their leading /// #version/#extension block. GL has no #include, so this /// is plain string concatenation at load time rather than a GLSL-level /// mechanism. Every existing two-argument-path caller is unaffected: the @@ -49,14 +49,10 @@ internal sealed class Shader : IDisposable /// Inserts right after the shader's leading /// #version/#extension/blank-line block. GLSL requires /// #version to be the very first statement in the source, so the - /// preamble cannot simply be prepended — it has to land after that block, + /// preamble cannot simply be prepended — it has to land after that block, /// before the first real declaration. - /// - /// Internal (not private) so - /// can reuse the exact same splice for RHI-created pipelines rather than - /// a second copy of this parsing. /// - internal static string InjectPreamble(string source, string preamble) + private static string InjectPreamble(string source, string preamble) { int insertAt = 0; int lineStart = 0; diff --git a/src/AcDream.App/Rendering/Shaders/debug_line.vert b/src/AcDream.App/Rendering/Shaders/debug_line.vert index 6c7eb398..f6340133 100644 --- a/src/AcDream.App/Rendering/Shaders/debug_line.vert +++ b/src/AcDream.App/Rendering/Shaders/debug_line.vert @@ -2,16 +2,12 @@ layout(location = 0) in vec3 aPos; layout(location = 1) in vec3 aColor; -// Campaign V slice V4a: the shared GpuPushConstants block carries ONE -// combined view-projection matrix (uViewProjection), the same convention -// every other ported shader uses (mesh_modern.vert, terrain_modern.vert, -// particle.vert) rather than the separate uView/uProjection this shader used -// pre-migration. -uniform mat4 uViewProjection; +uniform mat4 uView; +uniform mat4 uProjection; out vec3 vColor; void main() { vColor = aColor; - gl_Position = uViewProjection * vec4(aPos, 1.0); + gl_Position = uProjection * uView * vec4(aPos, 1.0); } diff --git a/src/AcDream.App/Rendering/Shaders/ui_text.frag b/src/AcDream.App/Rendering/Shaders/ui_text.frag index 09a94ec1..75c9cd3d 100644 --- a/src/AcDream.App/Rendering/Shaders/ui_text.frag +++ b/src/AcDream.App/Rendering/Shaders/ui_text.frag @@ -1,36 +1,19 @@ #version 430 core -#extension GL_ARB_bindless_texture : require in vec2 vUv; in vec4 vColor; out vec4 FragColor; -// Campaign V slice V4a: the bound sprite/glyph texture arrives as a -// texture-table slot index (GpuTextureSlot.Index) via the shared -// push-constant block, rather than a raw sampler2D bound to a fixed texture -// unit. common.glsl's ACDREAM_TEXTURE_HANDLE macro (spliced in below this -// shader's leading #version/#extension block) resolves the slot to the GL -// bindless handle; the Vulkan backend will index its set-2 sampled-texture -// descriptor array with the same integer. -uniform uint uTextureIndexA; - -// uUseTexture reuses the shared block's uRenderPass scalar: the block has no -// other spare per-draw int, and "shaders declare only the fields they read, -// interpreted as they need" is the documented design -// (docs/plans/2026-07-27-vulkan-campaign.md §3.4) — mesh_modern's -// "0=opaque,1=translucent" meaning for this field does not apply here. -uniform int uRenderPass; -#define uUseTexture uRenderPass +uniform sampler2D uTex; +uniform int uUseTexture; void main() { if (uUseTexture == 1) { // Font atlas is a single-channel R8 texture; red = coverage alpha. - sampler2D tex = sampler2D(ACDREAM_TEXTURE_HANDLE(uTextureIndexA)); - float coverage = texture(tex, vUv).r; + float coverage = texture(uTex, vUv).r; FragColor = vec4(vColor.rgb, vColor.a * coverage); } else if (uUseTexture == 2) { // RGBA dat sprite (decoded to RGBA8); modulate by tint/alpha. - sampler2D tex = sampler2D(ACDREAM_TEXTURE_HANDLE(uTextureIndexA)); - FragColor = texture(tex, vUv) * vColor; + FragColor = texture(uTex, vUv) * vColor; } else { FragColor = vColor; } diff --git a/src/AcDream.App/Rendering/Shaders/ui_text.vert b/src/AcDream.App/Rendering/Shaders/ui_text.vert index 037b2809..0cc6c932 100644 --- a/src/AcDream.App/Rendering/Shaders/ui_text.vert +++ b/src/AcDream.App/Rendering/Shaders/ui_text.vert @@ -3,19 +3,12 @@ layout(location = 0) in vec2 aPos; // screen pixels, origin top-left layout(location = 1) in vec2 aUv; layout(location = 2) in vec4 aColor; -// Campaign V slice V4a: this vertex shader has no projection matrix to carry -// (V3 audit, docs/plans/2026-07-27-vulkan-campaign.md §4.10: ui_text converts -// pixel coordinates straight to NDC with a constant z=0), so the screen size -// travels through the shared push-constant block's two spare scalars instead -// of a dedicated uScreenSize uniform. -uniform float uParamA; // screen width in pixels -uniform float uParamB; // screen height in pixels +uniform vec2 uScreenSize; out vec2 vUv; out vec4 vColor; void main() { - vec2 uScreenSize = vec2(uParamA, uParamB); // Convert pixel coords (origin top-left, +Y down) to NDC (origin center, +Y up). vec2 ndc = vec2( aPos.x / uScreenSize.x * 2.0 - 1.0, diff --git a/src/AcDream.App/Rendering/Sky/SkyRenderer.cs b/src/AcDream.App/Rendering/Sky/SkyRenderer.cs index 5173ad04..26520bb8 100644 --- a/src/AcDream.App/Rendering/Sky/SkyRenderer.cs +++ b/src/AcDream.App/Rendering/Sky/SkyRenderer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Numerics; @@ -16,7 +16,7 @@ namespace AcDream.App.Rendering.Sky; /// /// Port of references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/SkyboxRenderManager.cs. /// Draws the retail sky as a stack of independent celestial meshes (the -/// "it's not a dome" insight from r12 §2) rather than a cube/sphere +/// "it's not a dome" insight from r12 §2) rather than a cube/sphere /// with a gradient texture. Each is /// visible in a window of day-fraction space, sweeps from /// BeginAngle to EndAngle across the sky, and samples its @@ -25,11 +25,11 @@ namespace AcDream.App.Rendering.Sky; /// /// GL state delta per frame: /// -/// Depth mask OFF, depth test OFF, cull OFF — the sky +/// Depth mask OFF, depth test OFF, cull OFF — the sky /// should never occlude scene geometry. -/// Separate projection matrix with a 0.1–1e6 near/far +/// Separate projection matrix with a 0.1–1e6 near/far /// so mesh vertices at large distance don't clip. -/// View matrix with translation zeroed — sky is +/// View matrix with translation zeroed — sky is /// always camera-centred; moving doesn't get you closer to the /// sun. /// @@ -38,12 +38,12 @@ namespace AcDream.App.Rendering.Sky; /// /// Meshes are built lazily per GfxObj id on first reference. The /// per-object arc transform matches WorldBuilder's composition: -/// scale × RotZ(-heading) × RotY(-rotation) — the negative signs +/// scale × RotZ(-heading) × RotY(-rotation) — the negative signs /// come from AC's Z-up right-handed convention where heading is /// measured clockwise from north. /// /// -internal sealed unsafe class SkyRenderer : IDisposable +public sealed unsafe class SkyRenderer : IDisposable { private readonly GL _gl; private readonly IDatReaderWriter _dats; @@ -54,11 +54,11 @@ internal sealed unsafe class SkyRenderer : IDisposable // Lazily-built GPU resources per sky-GfxObj. private readonly Dictionary> _gpuByGfxObj = new(); - // When did we start running — used to accumulate TexVelocityX/Y over + // When did we start running — used to accumulate TexVelocityX/Y over // real time (independent of the day-fraction clock). private readonly DateTime _startedAt = DateTime.UtcNow; - // Configurable render distance — retail uses ~1e6; anything larger + // Configurable render distance — retail uses ~1e6; anything larger // than the scene far plane works. public float Near { get; set; } = 0.1f; public float Far { get; set; } = 1_000_000f; @@ -73,7 +73,7 @@ internal sealed unsafe class SkyRenderer : IDisposable } /// - /// Draw all NON-WEATHER sky objects (dome, sun, moon, stars, clouds — + /// Draw all NON-WEATHER sky objects (dome, sun, moon, stars, clouds — /// every SkyObject with Properties & 0x04 == 0). /// Called BEFORE the scene; terrain / meshes / debug lines / overlay /// land on top via depth-test. @@ -82,7 +82,7 @@ internal sealed unsafe class SkyRenderer : IDisposable /// Mirrors the first half of retail's LScape::draw at /// 0x00506330: that function calls GameSky::Draw(0) /// (sky pass) before the landblock loop, then GameSky::Draw(1) - /// (weather pass) after. acdream splits the same way — see + /// (weather pass) after. acdream splits the same way — see /// for the post-scene companion. /// /// @@ -90,17 +90,17 @@ internal sealed unsafe class SkyRenderer : IDisposable /// Each submesh renders with retail's per-vertex lighting formula: /// tint = clamp(emissive + ambient + max(dot(N, -sunDir), 0) * sunColor, 0, 1) /// where emissive is the submesh's Surface.Luminosity - /// float (1.0 for dome + sun + moon → texture passthrough via - /// saturation; 0.0 for clouds → get the full time-of-day tint). + /// float (1.0 for dome + sun + moon → texture passthrough via + /// saturation; 0.0 for clouds → get the full time-of-day tint). /// supplies the AmbientColor and SunColor /// already pre-multiplied by AmbBright / DirBright (loader-side). /// /// - /// See docs/research/2026-04-23-sky-retail-verbatim.md §6 for + /// See docs/research/2026-04-23-sky-retail-verbatim.md §6 for /// the full decompile citation. The empirical Dereth dump ( /// ACDREAM_DUMP_SKY=1, logged 2026-04-23) confirmed the /// SurfaceType.Luminous flag bit is NOT set on any Dereth sky - /// mesh — the differentiator is the Surface.Luminosity FLOAT + /// mesh — the differentiator is the Surface.Luminosity FLOAT /// field. /// /// @@ -118,13 +118,13 @@ internal sealed unsafe class SkyRenderer : IDisposable /// Draw the POST-SCENE sky objects (the foreground rain mesh /// 0x01004C44 on Rainy DayGroups, plus any other SkyObject with /// Properties & 0x01 != 0). Called AFTER the scene so these - /// meshes paint on top of terrain and entities — retail-faithful order + /// meshes paint on top of terrain and entities — retail-faithful order /// from LScape::draw at 0x00506330, where /// GameSky::Draw(1) fires after the DrawBlock loop and /// renders the after_sky_cell contents. With depth-test /// disabled and additive blend (the rain Surface flag includes /// Additive), the 815m-tall rain cylinder's bright streak texels add - /// over the scene — making rain appear in the air between camera and + /// over the scene — making rain appear in the air between camera and /// character instead of only at the horizon. /// /// Method name kept as RenderWeather for API stability; the @@ -149,7 +149,7 @@ internal sealed unsafe class SkyRenderer : IDisposable /// Sets up the same GL state for both (depth-test off, additive + /// alpha-blend per submesh, camera-anchored translation) and iterates /// only the SkyObjects matching the requested partition by - /// — bit 0x01 per the + /// — bit 0x01 per the /// retail decomp at GameSky::MakeObject (0x00506ee0). /// private void RenderPass( @@ -171,7 +171,7 @@ internal sealed unsafe class SkyRenderer : IDisposable // that FOV here, including the near-180-degree teleport transition. var skyProj = SkyProjection.WithDepthRange(camera.Projection, Near, Far); - // View with translation zeroed — keeps the sky at camera origin + // View with translation zeroed — keeps the sky at camera origin // regardless of camera position in the world. var skyView = camera.View; skyView.M41 = 0f; @@ -183,7 +183,7 @@ internal sealed unsafe class SkyRenderer : IDisposable _shader.SetMatrix4("uSkyProjection", skyProj); // Retail per-vertex lighting inputs (AdjustPlanes formula). - // AmbColor/SunColor are already × AmbBright/DirBright from + // AmbColor/SunColor are already × AmbBright/DirBright from // SkyDescLoader. SunDir is the unit vector FROM surface TO sun // derived from the keyframe's DirHeading/DirPitch. _shader.SetVec3("uAmbientColor", keyframe.AmbientColor); @@ -204,14 +204,14 @@ internal sealed unsafe class SkyRenderer : IDisposable bool wasCullFace = _gl.IsEnabled(EnableCap.CullFace); _gl.Disable(EnableCap.CullFace); _gl.Enable(EnableCap.Blend); - // Default blend — overridden per-submesh inside the inner loop. + // Default blend — overridden per-submesh inside the inner loop. // Additive surfaces (sun/moon/stars via SurfaceType.Additive = // 0x10000) get GL_SRC_ALPHA / GL_ONE; alpha-blended (clouds, dome // with Alpha flag) get GL_SRC_ALPHA / GL_ONE_MINUS_SRC_ALPHA. _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); // Look up the keyframe's override list so we can apply - // SkyObjReplace (r12 §2.3): per-keyframe GfxObj swaps + rotation + // SkyObjReplace (r12 §2.3): per-keyframe GfxObj swaps + rotation // override + transparency fade + luminosity cap. var replaces = PickReplaces(group, dayFraction); @@ -220,19 +220,19 @@ internal sealed unsafe class SkyRenderer : IDisposable for (int i = 0; i < group.SkyObjects.Count; i++) { var obj = group.SkyObjects[i]; - // Partition by post-scene flag (Properties bit 0x01) — the + // Partition by post-scene flag (Properties bit 0x01) — the // caller chose either the pre-scene sky pass (bit clear) or // the post-scene pass (bit set). Mirrors retail // GameSky::CreateDeletePhysicsObjects at 0x005073c0 / decomp // line 269036 which routes (Properties & 1) into // before_sky_cell vs after_sky_cell, and GameSky::Draw at // 0x00506ff0 which renders those cells in the two passes. - // NOTE: bit 0x04 (IsWeather) is independent — it gates whether + // NOTE: bit 0x04 (IsWeather) is independent — it gates whether // the object is instantiated when weather_enabled is false. // Earlier acdream incorrectly used IsWeather for this // partition, putting the outer rain cylinder 0x01004C42 // (Props=0x04, NO bit 0x01) into the post-scene pass with the - // foreground rain — double-thick rain not matching retail. + // foreground rain — double-thick rain not matching retail. if (obj.IsPostScene != postScenePass) continue; if (!obj.IsVisible(dayFraction)) continue; // Retail GameSky::Draw (0x00506ff0) skips Properties bit 0x02 @@ -254,7 +254,7 @@ internal sealed unsafe class SkyRenderer : IDisposable // fallback at the inner loop never fired (1f is always > 0). // RainMeshProbe (committed b8e0857) confirmed empirically that // NO Dereth sky surface carries the SurfaceType.Luminous flag - // bit (0x40) — the differentiator is purely the float field. + // bit (0x40) — the differentiator is purely the float field. float replaceLuminosity = float.NaN; float replaceDiffuse = float.NaN; if (replaces.TryGetValue((uint)i, out var rep)) @@ -289,7 +289,7 @@ internal sealed unsafe class SkyRenderer : IDisposable // int32_t var_4_1 = 0xc2f00000; // 0xc2f00000 == -120.0f // // Gate: bit 0x04 (weather) set AND bit 0x08 unset. NOT every - // post-scene SkyObject — bit 0x01 (post-scene) is independent + // post-scene SkyObject — bit 0x01 (post-scene) is independent // of bit 0x04 (weather). Today's Dereth ships every post-scene // entry as also weather-flagged so the previous unconditional // offset was a no-op divergence, but a future DayGroup with a @@ -302,15 +302,15 @@ internal sealed unsafe class SkyRenderer : IDisposable // cylinder bottom sits at z=0.11 ABOVE the camera (skyView // translation is zeroed so model-origin == camera); looking // horizontally shows nothing. With -120m the cylinder spans z - // = (camera-119.89)..(camera+694.90) — camera is inside, - // looking in any direction shows surrounding walls — the + // = (camera-119.89)..(camera+694.90) — camera is inside, + // looking in any direction shows surrounding walls — the // volumetric foreground-rain look retail has. if (postScenePass && obj.IsWeather && (obj.Properties & 0x08u) == 0u) model = model * Matrix4x4.CreateTranslation(0f, 0f, -120f); _shader.SetMatrix4("uModel", model); - // UV scroll accumulates real-time × velocity. Wrap to [0, 1] + // UV scroll accumulates real-time × velocity. Wrap to [0, 1] // so long-running sessions don't accumulate float precision // loss in the fragment UV. float uOffset = (obj.TexVelocityX * secondsSinceStart) % 1f; @@ -328,7 +328,7 @@ internal sealed unsafe class SkyRenderer : IDisposable // sky dome is Base1Image (Opaque, mapped to // SrcAlpha/InvSrcAlpha for a no-op blend at alpha=1). // See FUN_00508010 (chunk_00500000.c:7535) for the retail - // pattern — retail routes sky meshes through the normal + // pattern — retail routes sky meshes through the normal // mesh pipeline where Surface flags dictate state. if (sub.IsAdditive) _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One); @@ -338,24 +338,24 @@ internal sealed unsafe class SkyRenderer : IDisposable // Emissive source picks the surface's authored Luminosity by // default; the per-keyframe replace data can OVERRIDE // (rep.Luminosity > 0) or CAP (rep.MaxBright). This matches - // retail's FUN_0059da60: surface.Luminosity → D3DMATERIAL.Emissive + // retail's FUN_0059da60: surface.Luminosity → D3DMATERIAL.Emissive // (via material cache +0x3c), with the keyframe replace // promoting bright-keyframe clouds when the keyframe asks. // // Empirical Dereth sky surfaces (RainMeshProbe, b8e0857): - // dome/sun/moon → Lum=1.0 → vTint saturates → texture + // dome/sun/moon → Lum=1.0 → vTint saturates → texture // passthrough (correct retail look); - // stars/clouds → Lum=0.0 → vTint = ambient + diffuse → + // stars/clouds → Lum=0.0 → vTint = ambient + diffuse → // picks up the time-of-day tint; - // rain → Lum=0.1484 → faint emissive baseline, + // rain → Lum=0.1484 → faint emissive baseline, // ambient+diffuse adds atmospheric tint. // // Pre-fix: the replace-override variable defaulted to 1f and // the fallback `(luminosity > 0) ? luminosity : sub.SurfLuminosity` - // never fired — every sky mesh got effEmissive=1.0, + // never fired — every sky mesh got effEmissive=1.0, // saturating vTint. That made stars/clouds look full-bright // instead of time-of-day-tinted, and made rain streaks - // 6.7× too bright (one of two factors compounding the + // 6.7× too bright (one of two factors compounding the // foreground-rim visibility bug). float effEmissive = float.IsNaN(replaceLuminosity) ? sub.SurfLuminosity @@ -374,7 +374,7 @@ internal sealed unsafe class SkyRenderer : IDisposable // Retail D3DPolyRender::SetSurface at 0x59c882 calls // SetFFFogAlphaDisabled(1) when the Additive flag (0x10000) - // is set on the Surface — so the sun, moon, stars, and any + // is set on the Surface — so the sun, moon, stars, and any // additive cloud sheet are drawn WITHOUT fog. Skipping fog // on additive surfaces keeps the sun bright at horizon // dusk/dawn (where fog would otherwise dim it to fog color). @@ -477,13 +477,13 @@ internal sealed unsafe class SkyRenderer : IDisposable /// Lazy mesh build for a sky object. Handles two cases: /// /// - /// 0x010xxxxx — direct . Reuses + /// 0x010xxxxx — direct . Reuses /// so the pos/neg polygon /// splitting logic stays consistent with the main static-mesh /// pipeline. Most sky meshes are single-surface. /// /// - /// 0x020xxxxx — . The agent at + /// 0x020xxxxx. The agent at /// 2026-04-27 found these Setup-backed sky objects (e.g. /// 0x02000588, 0x02000589, 0x02000714, /// 0x02000BA6) were silently dropped: every cache miss @@ -496,7 +496,7 @@ internal sealed unsafe class SkyRenderer : IDisposable /// Setup.Parts at the default placement frame and /// produces submeshes for each /// part. Per-part transforms are baked into vertex positions - /// (sky setups are static — no animation needed for the static + /// (sky setups are static — no animation needed for the static /// mesh half of the visual). /// /// @@ -504,7 +504,7 @@ internal sealed unsafe class SkyRenderer : IDisposable /// Even with this fix the visible aurora-style sheen most retail /// rainy/cloudy setups produce comes from the pes_id field /// on each (a Particle - /// Effect Schedule) — that's a separate Phase-level feature. + /// Effect Schedule) — that's a separate Phase-level feature. /// Rendering the Setup's static parts here is the geometry half; /// the dynamic particle half is deferred. /// @@ -550,8 +550,8 @@ internal sealed unsafe class SkyRenderer : IDisposable // Phase 1 diagnostic: dump Surface.Type flags on every sky GfxObj // once, so we can determine which submeshes carry Luminous (0x40) // vs plain-lit. This settles the retail "cloud tint = per-vertex - // lighting on non-Luminous meshes" hypothesis — see - // docs/research/2026-04-23-sky-retail-verbatim.md §6. + // lighting on non-Luminous meshes" hypothesis — see + // docs/research/2026-04-23-sky-retail-verbatim.md §6. if (System.Environment.GetEnvironmentVariable("ACDREAM_DUMP_SKY") == "1") DumpGfxObjSurfaces(gfxObjId, gfx, subMeshes); @@ -565,14 +565,14 @@ internal sealed unsafe class SkyRenderer : IDisposable /// Setup-backed sky object loader. Walks at /// the default placement frame, builds submeshes via /// , and bakes the per-part transform - /// into the vertex positions before upload. Static-pose only — sky + /// into the vertex positions before upload. Static-pose only — sky /// setups don't animate in any meaningful way for the visual we care /// about (the dynamic look comes from pes_id particles, not /// the underlying mesh). /// /// Mirrors retail's at - /// decomp 280484 dispatching type 7 → CPartArray::CreateSetup - /// → CSetup::SetSetupID, which loads the setup and instantiates + /// decomp 280484 dispatching type 7 → CPartArray::CreateSetup + /// → CSetup::SetSetupID, which loads the setup and instantiates /// each part as a separate CPhysicsObj child. We collapse the /// children into a flat submesh list because the sky pass renders /// without per-part transforms anyway. @@ -654,13 +654,13 @@ internal sealed unsafe class SkyRenderer : IDisposable continue; } - // SurfaceType is a flag enum — `ToString()` gives the + // SurfaceType is a flag enum — `ToString()` gives the // comma-joined names (e.g. "Base1Image, Additive"). uint rawType = (uint)surface.Type; string names = surface.Type.ToString(); uint origTex = surface.OrigTextureId?.DataId ?? 0u; var trans = TranslucencyKindExtensions.FromSurfaceType(surface.Type); - // Surface's own Luminosity (0..1 fraction per test fixture — + // Surface's own Luminosity (0..1 fraction per test fixture — // different from SkyObjectReplace.Luminosity which lives in the keyframe). Console.WriteLine( $"[sky-dump] Surface[{i}] 0x{surfaceId:X8} Type=0x{rawType:X8} ({names}) " + @@ -703,7 +703,7 @@ internal sealed unsafe class SkyRenderer : IDisposable // // NOTE: earlier revision also treated `SurfaceType.Luminous = 0x40` // as additive, but that flag is present on the sky DOME itself and - // on cloud sheets — turning those additive blew the whole sky to + // on cloud sheets — turning those additive blew the whole sky to // white. `Luminous` means "self-illuminated / unshaded" in retail's // render pipeline, not "additive blend". Only the Additive bit // toggles the blend mode. @@ -753,18 +753,18 @@ internal sealed unsafe class SkyRenderer : IDisposable /// public bool IsAdditive; /// - /// Surface.Luminosity float (0..1 — NOT the SurfaceType.Luminous + /// Surface.Luminosity float (0..1 — NOT the SurfaceType.Luminous /// flag bit). Passed to the sky fragment shader as uEmissive; /// when 1.0 it saturates the lighting math so the mesh renders at /// full texture brightness (dome, sun). When 0.0 the mesh picks up /// the time-of-day ambient+diffuse tint (clouds). See - /// docs/research/2026-04-23-sky-retail-verbatim.md §6. + /// docs/research/2026-04-23-sky-retail-verbatim.md §6. /// public float SurfLuminosity; public float SurfDiffuse; /// /// True when the source mesh's authored UVs exceed [0,1] (e.g. - /// the inner sky/star layer 0x010015EF and the cloud meshes — + /// the inner sky/star layer 0x010015EF and the cloud meshes — /// they tile their texture across the geometry). The renderer /// must use GL_REPEAT for these or only the small region /// where UVs fall in [0,1] samples the actual texture; the rest diff --git a/src/AcDream.App/Rendering/TeleportViewPlaneController.cs b/src/AcDream.App/Rendering/TeleportViewPlaneController.cs index 1af9fabc..01c50711 100644 --- a/src/AcDream.App/Rendering/TeleportViewPlaneController.cs +++ b/src/AcDream.App/Rendering/TeleportViewPlaneController.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.Core.World; namespace AcDream.App.Rendering; @@ -12,7 +12,7 @@ namespace AcDream.App.Rendering; /// from portal space to the destination world at the transition projection; /// there is no black-alpha compositor between them. /// -internal sealed class TeleportViewPlaneController +public sealed class TeleportViewPlaneController { public const float TransitionViewPlaneDistance = 0.001f; diff --git a/src/AcDream.App/Rendering/TerrainAtlas.cs b/src/AcDream.App/Rendering/TerrainAtlas.cs index 4d8860d8..e4c7ad59 100644 --- a/src/AcDream.App/Rendering/TerrainAtlas.cs +++ b/src/AcDream.App/Rendering/TerrainAtlas.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Textures; +using AcDream.Core.Textures; using DatReaderWriter; using AcDream.Content; using DatReaderWriter.DBObjs; @@ -13,21 +13,21 @@ namespace AcDream.App.Rendering; /// Holds both texture arrays the terrain renderer samples from: /// /// -/// Terrain atlas — one GL_TEXTURE_2D_ARRAY layer per terrain type +/// Terrain atlas — one GL_TEXTURE_2D_ARRAY layer per terrain type /// (grass, dirt, sand, forest...), sourced from /// Region.TerrainInfo.LandSurfaces.TexMerge.TerrainDesc. /// /// -/// Alpha atlas — one GL_TEXTURE_2D_ARRAY layer per blend mask, +/// Alpha atlas — one GL_TEXTURE_2D_ARRAY layer per blend mask, /// sourced from CornerTerrainMaps / SideTerrainMaps / RoadMaps in the /// same TexMerge. Used by the fragment shader to blend up to three /// terrain overlays and two roads on top of a base cell texture. /// /// -/// The alpha atlas is built but not yet sampled by any shader — that wiring +/// The alpha atlas is built but not yet sampled by any shader — that wiring /// lands in Phase 3c.4 along with the shader rewrite. /// -internal sealed unsafe class TerrainAtlas : IDisposable +public sealed unsafe class TerrainAtlas : IDisposable { private readonly GL _gl; @@ -276,7 +276,7 @@ internal sealed unsafe class TerrainAtlas : IDisposable /// /// Load corner, side, and road alpha maps from the TexMerge into a second - /// GL_TEXTURE_2D_ARRAY. AC ships these as 512×512 PFID_A8 textures; + /// GL_TEXTURE_2D_ARRAY. AC ships these as 512×512 PFID_A8 textures; /// expands each alpha byte /// into all four RGBA channels so the shader can sample from any channel. /// @@ -368,7 +368,7 @@ internal sealed unsafe class TerrainAtlas : IDisposable cornerTCodes, sideTCodes, roadRCodes); } - // Alpha maps should all be uniform size (WorldBuilder asserts 512×512). + // Alpha maps should all be uniform size (WorldBuilder asserts 512×512). // Fall back to the max observed so a stray mismatch doesn't crash us. int aMaxW = 1, aMaxH = 1; foreach (var d in decoded) @@ -433,7 +433,7 @@ internal sealed unsafe class TerrainAtlas : IDisposable // Alpha maps ship as PFID_CUSTOM_LSCAPE_ALPHA (AC's landscape-alpha // format) or the more generic PFID_A8; terrain blending alpha masks - // MUST use isAdditive=true so R=G=B=A=val — the terrain fragment shader + // MUST use isAdditive=true so R=G=B=A=val — the terrain fragment shader // reads .r for the blend weight. Palette is not used. var d = SurfaceDecoder.DecodeRenderSurface(rs, palette: null, isClipMap: false, isAdditive: true); if (ReferenceEquals(d, DecodedTexture.Magenta)) @@ -515,7 +515,7 @@ internal sealed unsafe class TerrainAtlas : IDisposable /// A.5 T22.5: update GL_TEXTURE_MAX_ANISOTROPY on the terrain atlas at /// runtime (called by /// when - /// the user changes Quality preset mid-session). Idempotent — calling with + /// the user changes Quality preset mid-session). Idempotent — calling with /// the same level as the current setting is safe and produces no visual /// change. The texture must not be resident-bindless when its parameters /// are mutated; we temporarily make it non-resident if needed. diff --git a/src/AcDream.App/Rendering/TerrainModernRenderer.cs b/src/AcDream.App/Rendering/TerrainModernRenderer.cs index d5e1f6e4..643a2d1a 100644 --- a/src/AcDream.App/Rendering/TerrainModernRenderer.cs +++ b/src/AcDream.App/Rendering/TerrainModernRenderer.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.App.Rendering.Gpu; using AcDream.App.Rendering.Wb; using AcDream.Core.Terrain; @@ -8,7 +8,7 @@ namespace AcDream.App.Rendering; /// /// Phase N.5b modern terrain dispatcher. Single global VBO/EBO with a slot -/// allocator (one slot per landblock, 384 verts × 40 bytes = 15,360 bytes +/// allocator (one slot per landblock, 384 verts × 40 bytes = 15,360 bytes /// per slot). Per-frame: build a DrawElementsIndirectCommand array from /// visible slots, upload, dispatch via glMultiDrawElementsIndirect. Atlas /// textures bound via bindless handles set per-frame as sampler uniforms. @@ -16,9 +16,9 @@ namespace AcDream.App.Rendering; /// Total ~6-8 GL calls per frame for terrain regardless of visible /// landblock count. /// -internal sealed unsafe class TerrainModernRenderer : IDisposable +public sealed unsafe class TerrainModernRenderer : IDisposable { - // VertsPerLandblock MUST stay divisible by 6 — terrain_modern.vert uses + // VertsPerLandblock MUST stay divisible by 6 — terrain_modern.vert uses // `gl_VertexID % 6` to pick the cell-corner index (BL/BR/TR/TL), and // because we bake `slot * VertsPerLandblock` into indices CPU-side and // pass BaseVertex=0 to MultiDrawElementsIndirect, gl_VertexID becomes @@ -83,7 +83,7 @@ internal sealed unsafe class TerrainModernRenderer : IDisposable private uint _fallbackClipUbo; // Campaign V slice V2b (2026-07-27): uTerrainHandle/uAlphaHandle (uvec2) - // became uTextureIndexA/uTextureIndexB (uint table slots) — cached + // became uTextureIndexA/uTextureIndexB (uint table slots) — cached // uniform locations (matrix uniforms are set by name via Shader.SetMatrix4). private int _uTextureIndexALoc; private int _uTextureIndexBLoc; @@ -91,8 +91,8 @@ internal sealed unsafe class TerrainModernRenderer : IDisposable private bool _textureTilingUploaded; // GL-only emulation of the eventual Vulkan global texture descriptor array - // (binding=9, GpuBindingModel.StorageTextureTable). Owns its own table — - // see GlBindlessHandleTable's doc comment and the campaign doc's §5.2 for + // (binding=9, GpuBindingModel.StorageTextureTable). Owns its own table — + // see GlBindlessHandleTable's doc comment and the campaign doc's §5.2 for // why terrain doesn't share WbDrawDispatcher's/EnvCellRenderer's tables. private readonly GlBindlessHandleTable _textureTable = new(); private uint _textureTableSsbo; @@ -495,7 +495,7 @@ internal sealed unsafe class TerrainModernRenderer : IDisposable // Bind shader + uniforms + atlas handles. // Verified Phase W Stage 4 (T4.2): terrain projects from the camera view-proj; // no separate landscape viewpoint to sync. Both uView and uProjection derive - // from the ICamera passed into this method — the same camera used for all other + // from the ICamera passed into this method — the same camera used for all other // renderers in the unified pipeline. Retail's LScape::update_viewpoint // pre-positions terrain to the outdoor landcell, but acdream uses the // unified camera matrix everywhere, so no separate viewpoint divergence can occur. @@ -508,7 +508,7 @@ internal sealed unsafe class TerrainModernRenderer : IDisposable // Campaign V slice V2b: pass each handle's binding=9 table slot // instead of the raw uvec2 handle. GLSL reconstructs // sampler2DArray(ACDREAM_TEXTURE_HANDLE(uTextureIndexA)) at the use - // site — see terrain_modern.frag. + // site — see terrain_modern.frag. uint terrainSlot = _textureTable.GetOrAdd(terrainHandle); uint alphaSlot = _textureTable.GetOrAdd(alphaHandle); FlushAndBindTextureTable(); @@ -519,7 +519,7 @@ internal sealed unsafe class TerrainModernRenderer : IDisposable // when wired, else the no-clip fallback (count 0 = ungated terrain). BindClipUboBinding2(); - // #108-residual: retail terrain is SINGLE-SIDED — ACRender::landPolysDraw + // #108-residual: retail terrain is SINGLE-SIDED — ACRender::landPolysDraw // (0x006b7040) draws each land triangle ONLY when the camera is on the // POSITIVE (upper) side of its plane (Plane::which_side2 vs // Render::FrameCurrent, zFightTerrainAdjust bias). GL backface culling @@ -527,13 +527,13 @@ internal sealed unsafe class TerrainModernRenderer : IDisposable // LandblockMesh emits every triangle CCW in world XY seen from above // (LandblockMeshTests winding pin), which the unified camera chain // (CreateLookAt up=+Z + Numerics perspective) maps to CCW window - // winding from above / CW from below (TerrainCullOrientationTests) — + // winding from above / CW from below (TerrainCullOrientationTests) — // so FrontFace(Ccw)+Cull(Back) keeps the top side and culls the // underside. WB drew the whole world with culling DISABLED - // frame-globally (WB GameScene.cs:841 — an editor camera goes + // frame-globally (WB GameScene.cs:841 — an editor camera goes // underground); inheriting that drew terrain DOUBLE-SIDED, and a // below-grade eye (cellar ascent) saw the UNDERSIDE of the grade - // sheet through the exit-door aperture — the #108 grass window. + // sheet through the exit-door aperture — the #108 grass window. // Self-contained state per feedback_render_self_contained_gl_state; // the frame-global CW + cull-off baseline is restored after the draw. _gl.Enable(EnableCap.CullFace); diff --git a/src/AcDream.App/Rendering/TextRenderGlStateScope.cs b/src/AcDream.App/Rendering/TextRenderGlStateScope.cs new file mode 100644 index 00000000..b65123b7 --- /dev/null +++ b/src/AcDream.App/Rendering/TextRenderGlStateScope.cs @@ -0,0 +1,156 @@ +using Silk.NET.OpenGL; + +namespace AcDream.App.Rendering; + +internal interface ITextRenderGlStateApi +{ + bool IsEnabled(EnableCap capability); + + int GetInteger(GetPName parameter); + + bool GetBoolean(GetPName parameter); + + void SetCapability(EnableCap capability, bool enabled); + + void DepthMask(bool enabled); + + void BlendFuncSeparate( + BlendingFactor sourceRgb, + BlendingFactor destinationRgb, + BlendingFactor sourceAlpha, + BlendingFactor destinationAlpha); + + void UseProgram(uint program); + + void BindVertexArray(uint vertexArray); + + void BindBuffer(BufferTargetARB target, uint buffer); + + void ActiveTexture(TextureUnit unit); + + void BindTexture(TextureTarget target, uint texture); +} + +internal sealed class SilkTextRenderGlStateApi : ITextRenderGlStateApi +{ + private readonly GL _gl; + + public SilkTextRenderGlStateApi(GL gl) => + _gl = gl ?? throw new ArgumentNullException(nameof(gl)); + + public bool IsEnabled(EnableCap capability) => _gl.IsEnabled(capability); + + public int GetInteger(GetPName parameter) => _gl.GetInteger(parameter); + + public bool GetBoolean(GetPName parameter) => _gl.GetBoolean(parameter); + + public void SetCapability(EnableCap capability, bool enabled) + { + if (enabled) + _gl.Enable(capability); + else + _gl.Disable(capability); + } + + public void DepthMask(bool enabled) => _gl.DepthMask(enabled); + + public void BlendFuncSeparate( + BlendingFactor sourceRgb, + BlendingFactor destinationRgb, + BlendingFactor sourceAlpha, + BlendingFactor destinationAlpha) => + _gl.BlendFuncSeparate( + sourceRgb, + destinationRgb, + sourceAlpha, + destinationAlpha); + + public void UseProgram(uint program) => _gl.UseProgram(program); + + public void BindVertexArray(uint vertexArray) => _gl.BindVertexArray(vertexArray); + + public void BindBuffer(BufferTargetARB target, uint buffer) => + _gl.BindBuffer(target, buffer); + + public void ActiveTexture(TextureUnit unit) => _gl.ActiveTexture(unit); + + public void BindTexture(TextureTarget target, uint texture) => + _gl.BindTexture(target, texture); +} + +/// +/// Exact, focused state transaction for . It +/// captures every GL value that Flush or DrawLayer mutates, while avoiding the +/// dozens of unrelated synchronous reads made by the broad diagnostic scope. +/// +internal readonly struct TextRenderGlStateScope : IDisposable +{ + private readonly ITextRenderGlStateApi _gl; + private readonly bool _depthTest; + private readonly bool _blend; + private readonly bool _cullFace; + private readonly bool _alphaToCoverage; + private readonly bool _multisample; + private readonly bool _depthWrite; + private readonly int _blendSourceRgb; + private readonly int _blendDestinationRgb; + private readonly int _blendSourceAlpha; + private readonly int _blendDestinationAlpha; + private readonly int _program; + private readonly int _vertexArray; + private readonly int _arrayBuffer; + private readonly int _activeTexture; + private readonly int _texture0Binding2D; + + public TextRenderGlStateScope(ITextRenderGlStateApi gl) + { + _gl = gl ?? throw new ArgumentNullException(nameof(gl)); + _depthTest = gl.IsEnabled(EnableCap.DepthTest); + _blend = gl.IsEnabled(EnableCap.Blend); + _cullFace = gl.IsEnabled(EnableCap.CullFace); + _alphaToCoverage = gl.IsEnabled(EnableCap.SampleAlphaToCoverage); + _multisample = gl.IsEnabled(EnableCap.Multisample); + _depthWrite = gl.GetBoolean(GetPName.DepthWritemask); + _blendSourceRgb = gl.GetInteger(GetPName.BlendSrcRgb); + _blendDestinationRgb = gl.GetInteger(GetPName.BlendDstRgb); + _blendSourceAlpha = gl.GetInteger(GetPName.BlendSrcAlpha); + _blendDestinationAlpha = gl.GetInteger(GetPName.BlendDstAlpha); + _program = gl.GetInteger(GetPName.CurrentProgram); + _vertexArray = gl.GetInteger(GetPName.VertexArrayBinding); + _arrayBuffer = gl.GetInteger(GetPName.ArrayBufferBinding); + _activeTexture = gl.GetInteger(GetPName.ActiveTexture); + + try + { + gl.ActiveTexture(TextureUnit.Texture0); + _texture0Binding2D = gl.GetInteger(GetPName.TextureBinding2D); + } + finally + { + gl.ActiveTexture((TextureUnit)_activeTexture); + } + } + + public void Dispose() + { + _gl.UseProgram((uint)_program); + _gl.BindVertexArray((uint)_vertexArray); + _gl.BindBuffer(BufferTargetARB.ArrayBuffer, (uint)_arrayBuffer); + + _gl.ActiveTexture(TextureUnit.Texture0); + _gl.BindTexture(TextureTarget.Texture2D, (uint)_texture0Binding2D); + _gl.ActiveTexture((TextureUnit)_activeTexture); + + _gl.DepthMask(_depthWrite); + _gl.BlendFuncSeparate( + (BlendingFactor)_blendSourceRgb, + (BlendingFactor)_blendDestinationRgb, + (BlendingFactor)_blendSourceAlpha, + (BlendingFactor)_blendDestinationAlpha); + _gl.SetCapability(EnableCap.DepthTest, _depthTest); + _gl.SetCapability(EnableCap.Blend, _blend); + _gl.SetCapability(EnableCap.CullFace, _cullFace); + _gl.SetCapability(EnableCap.SampleAlphaToCoverage, _alphaToCoverage); + _gl.SetCapability(EnableCap.Multisample, _multisample); + } +} diff --git a/src/AcDream.App/Rendering/TextRenderer.cs b/src/AcDream.App/Rendering/TextRenderer.cs index 7358a76d..18f8862b 100644 --- a/src/AcDream.App/Rendering/TextRenderer.cs +++ b/src/AcDream.App/Rendering/TextRenderer.cs @@ -1,7 +1,11 @@ -using System; +using System; using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Numerics; using System.Runtime.InteropServices; +using AcDream.App.Rendering.Wb; +using Silk.NET.OpenGL; namespace AcDream.App.Rendering; @@ -11,39 +15,36 @@ namespace AcDream.App.Rendering; /// at the start of a HUD pass, queue geometry via /// / , then . /// -/// Campaign V slice V4a: ported onto . One pipeline -/// (blend, depth-disable, and the MSAA/alpha-to-coverage isolation the prior -/// TextRenderGlStateScope restored by hand are now baked into the -/// pipeline description) and one pass per , with each -/// bucket/segment getting its own per-frame ring allocation instead of a -/// shared, growable VBO. Rects are drawn first so text sits on top of -/// background panels — bucket ORDER is unchanged, only how each bucket -/// reaches the GPU. +/// Uses two internal vertex buffers (text and rect) flushed in two draw calls +/// to avoid a per-vertex "use texture" flag. Rects are drawn first so text +/// sits on top of background panels. /// -internal sealed class TextRenderer : IDisposable +public sealed unsafe class TextRenderer : IDisposable { private const int FloatsPerVertex = 8; // pos(2) + uv(2) + color(4) - private const int VertexStrideBytes = FloatsPerVertex * sizeof(float); - private static readonly GpuVertexLayout VertexLayout = new( - StrideBytes: VertexStrideBytes, - [ - new GpuVertexAttribute(0, GpuVertexFormat.Float2, 0), - new GpuVertexAttribute(1, GpuVertexFormat.Float2, 8), - new GpuVertexAttribute(2, GpuVertexFormat.Float4, 16), - ]); + private readonly GL _gl; + private readonly ITextRenderGlStateApi _glState; + private readonly Shader _shader; + private readonly ResourceCleanupGroup _resources; + private uint _vao; + private uint _vbo; + private readonly uint _whiteTex; // 1×1 white, for solid fills routed through the sprite bucket + private int _vboCapacityBytes; - // uUseTexture values the ui_text.frag shader branches on (reusing the - // shared push-constant block's uRenderPass scalar — see the shader's own - // comment for why there is no dedicated field). - private const int UseTextureNone = 0; - private const int UseTextureFont = 1; - private const int UseTextureSprite = 2; + private sealed class FrameBufferSet + { + public uint Vao; + public uint Vbo; + public int CapacityBytes; + public int UsedBytes; + } - private readonly IGpuDevice _device; - private readonly IGpuPipeline _pipeline; + private readonly FrameBufferSet[] _frameBuffers; + private FrameBufferSet? _activeFrameBuffer; - private sealed class SpriteSeg { public GpuTextureSlot TextureSlot; public readonly List Verts = new(256); } + internal long DynamicBufferCapacityBytes => + _frameBuffers.Sum(set => (long)set.CapacityBytes); private readonly List _textBuf = new(8192); private readonly List _rectBuf = new(1024); @@ -52,14 +53,16 @@ internal sealed class TextRenderer : IDisposable // Drawing segments in submission order preserves painter z-order for // sprite-on-sprite UI. (The old per-texture dictionary drew a REUSED texture // at its FIRST-insertion point, so later bar sprites covered glyphs emitted - // earlier via the shared dat-font atlas — the stamina/mana numbers vanished.) + // earlier via the shared dat-font atlas — the stamina/mana numbers vanished.) + private sealed class SpriteSeg { public uint Texture; public readonly List Verts = new(256); } + private readonly List _spriteSegs = new(); private int _segUsed; private int _textVerts; private int _rectVerts; private Vector2 _screenSize; - // Overlay layer — a parallel set of buckets drawn AFTER the normal sprite/rect/text + // Overlay layer — a parallel set of buckets drawn AFTER the normal sprite/rect/text // buckets, so open popups/menus composite on top of EVERYTHING, including translucent // rect panel backgrounds (which otherwise always win because rects flush after // sprites). Routed by OverlayMode; the UI root sets it for the popup traversal. @@ -74,38 +77,142 @@ internal sealed class TextRenderer : IDisposable /// of all normal-layer geometry). Set by the UI root around the popup/overlay pass. public bool OverlayMode { get; set; } - /// - /// No longer meaningful post-V4a: per-frame vertex data comes from the - /// device's shared ring rather than a VBO this class owns. Kept (returning - /// 0) so RenderFrameDiagnosticSources's telemetry read still compiles; - /// the dynamic-buffer dimension it reported is now a device-wide, not a - /// per-renderer, concern. - /// - internal long DynamicBufferCapacityBytes => 0; - - public TextRenderer(IGpuDevice device) + public TextRenderer(GL gl, string shaderDir) { - _device = device ?? throw new ArgumentNullException(nameof(device)); - _pipeline = _device.CreatePipeline(new GpuPipelineDescription + _gl = gl; + _glState = new SilkTextRenderGlStateApi(gl); + var resources = new ResourceCleanupGroup(); + Shader? shader = null; + var frameBuffers = new FrameBufferSet[3]; + uint whiteTexture = 0; + try { - Name = "ui-text", - Shaders = new GpuShaderSet("ui_text"), - VertexLayout = VertexLayout, - Topology = GpuPrimitiveTopology.TriangleList, - Blend = GpuBlendMode.StraightAlpha, - // The retained UI is a self-contained 2-D pass: depth is - // irrelevant (feedback_render_self_contained_gl_state) and the - // world pass's alpha-to-coverage/multisample state must not leak - // in — SampleCount=1 drives the GL backend's GL_MULTISAMPLE - // toggle off when this pipeline binds (GlGpuDevice.ApplyRenderState), - // which is what the deleted TextRenderGlStateScope used to restore - // by hand around every Flush. - Depth = GpuDepthState.Disabled, - Cull = GpuCullMode.None, - AlphaToCoverage = false, - ColorWrite = true, - SampleCount = 1, - }); + shader = new Shader(gl, + Path.Combine(shaderDir, "ui_text.vert"), + Path.Combine(shaderDir, "ui_text.frag")); + resources.Add("text shader", shader.Dispose); + + for (int i = 0; i < frameBuffers.Length; i++) + frameBuffers[i] = CreateFrameBufferSet(resources); + + // 1×1 white texture so DrawFill can route solid-colour quads through the SPRITE + // bucket (the shader multiplies texel×color → white×color = color). Lets a panel + // background draw UNDER its text in painter order, which DrawRect's separate + // bucket cannot (it always composites after all sprites). + whiteTexture = GlResourceCommand.CreateTexture( + _gl, + "TextRenderer white texture"); + uint ownedWhiteTexture = whiteTexture; + resources.Add( + "white texture", + () => GlResourceCommand.DeleteTexture( + _gl, + ownedWhiteTexture, + $"delete TextRenderer white texture {ownedWhiteTexture}")); + GlResourceCommand.Execute( + _gl, + "initialize TextRenderer white texture", + () => + { + _gl.BindTexture(TextureTarget.Texture2D, whiteTexture); + Span whitePixel = stackalloc byte[] { 255, 255, 255, 255 }; + fixed (byte* wp = whitePixel) + _gl.TexImage2D(TextureTarget.Texture2D, 0, (int)InternalFormat.Rgba8, 1, 1, 0, + PixelFormat.Rgba, PixelType.UnsignedByte, wp); + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest); + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMinFilter.Nearest); + _gl.BindTexture(TextureTarget.Texture2D, 0); + }); + } + catch (Exception constructionFailure) + { + try + { + resources.RetryCleanup(); + } + catch (Exception cleanupFailure) + { + throw new GlResourceConstructionException( + "TextRenderer construction failed and its published GL resources did not cleanly roll back.", + resources, + [constructionFailure, cleanupFailure]); + } + + throw; + } + + _resources = resources; + _shader = shader; + _frameBuffers = frameBuffers; + _whiteTex = whiteTexture; + } + + /// + /// Selects the GPU-fenced frame slot and resets its append cursor. Every + /// UI segment rendered during the frame receives a distinct byte range; + /// later text or sprite batches cannot overwrite an earlier in-flight draw. + /// + public void BeginFrame(int frameSlot) + { + if ((uint)frameSlot >= (uint)_frameBuffers.Length) + throw new ArgumentOutOfRangeException(nameof(frameSlot)); + + FrameBufferSet set = _frameBuffers[frameSlot]; + set.UsedBytes = 0; + _activeFrameBuffer = set; + _vao = set.Vao; + _vbo = set.Vbo; + _vboCapacityBytes = set.CapacityBytes; + } + + private FrameBufferSet CreateFrameBufferSet(ResourceCleanupGroup resources) + { + uint vao = TrackedGlResource.CreateVertexArray( + _gl, + "TextRenderer frame VAO creation"); + RetryableGpuResourceRelease vaoRelease = + TrackedGlResource.CreateRetryableVertexArrayDeletion( + _gl, + vao, + "TextRenderer frame VAO disposal"); + resources.Add("frame VAO", vaoRelease.Run); + var set = new FrameBufferSet { Vao = vao }; + + uint vbo = TrackedGlResource.CreateBuffer( + _gl, + "TextRenderer frame VBO creation"); + set.Vbo = vbo; + RetryableGpuResourceRelease? vboRelease = null; + resources.Add( + "frame VBO", + () => + { + vboRelease ??= TrackedGlResource.CreateRetryableBufferDeletion( + _gl, + vbo, + set.CapacityBytes, + "TextRenderer frame VBO disposal"); + vboRelease.Run(); + }); + + GlResourceCommand.Execute( + _gl, + "initialize TextRenderer frame VAO and VBO", + () => + { + _gl.BindVertexArray(set.Vao); + _gl.BindBuffer(BufferTargetARB.ArrayBuffer, set.Vbo); + uint stride = FloatsPerVertex * sizeof(float); + _gl.EnableVertexAttribArray(0); + _gl.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, stride, (void*)0); + _gl.EnableVertexAttribArray(1); + _gl.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, stride, (void*)(2 * sizeof(float))); + _gl.EnableVertexAttribArray(2); + _gl.VertexAttribPointer(2, 4, VertexAttribPointerType.Float, false, stride, (void*)(4 * sizeof(float))); + _gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0); + _gl.BindVertexArray(0); + }); + return set; } /// Begin a HUD pass. Call once per frame before any Draw* calls. @@ -134,11 +241,11 @@ internal sealed class TextRenderer : IDisposable /// Draw a solid-colour quad through the SPRITE bucket (and the overlay layer /// when active), so it composites in painter order with sprites + dat-font text. Use - /// this — not — for a panel BACKGROUND that text draws on top of: + /// this — not — for a panel BACKGROUND that text draws on top of: /// DrawRect's bucket always flushes after all sprites, so a rect background would cover /// the text instead. public void DrawFill(float x, float y, float w, float h, Vector4 color) - => DrawSprite(_device.DefaultTextureSlot, x, y, w, h, 0f, 0f, 1f, 1f, color); + => DrawSprite(_whiteTex, x, y, w, h, 0f, 0f, 1f, 1f, color); /// Draw a 1-pixel-thick outline rect. public void DrawRectOutline(float x, float y, float w, float h, Vector4 color, float thickness = 1f) @@ -206,7 +313,7 @@ internal sealed class TextRenderer : IDisposable } if (!font.TryGetGlyph(c, out var g)) { - // Unknown glyph — skip its advance width if '?' exists. + // Unknown glyph — skip its advance width if '?' exists. if (font.TryGetGlyph('?', out var q)) cursorX += q.Advance; continue; @@ -237,9 +344,9 @@ internal sealed class TextRenderer : IDisposable /// /// Draw a textured sprite quad in screen pixel space with an explicit /// source-UV rectangle (for 9-slice / atlas sub-regions). Batched per - /// texture-table slot, flushed with uUseTexture=2 (RGBA modulate). + /// GL texture handle; flushed with uUseTexture=2 (RGBA modulate). /// - public void DrawSprite(GpuTextureSlot texture, float x, float y, float w, float h, + public void DrawSprite(uint texture, float x, float y, float w, float h, float u0, float v0, float u1, float v1, Vector4 tint) { SpriteSeg seg = OverlayMode @@ -251,18 +358,18 @@ internal sealed class TextRenderer : IDisposable /// Pick the sprite segment for : extend the current /// same-texture run, else reuse a pooled segment, else allocate. Submission order is /// preserved (painter z-order for sprite-on-sprite UI). - private static SpriteSeg NextSpriteSeg(List segs, ref int used, GpuTextureSlot texture) + private static SpriteSeg NextSpriteSeg(List segs, ref int used, uint texture) { - if (used > 0 && segs[used - 1].TextureSlot == texture) + if (used > 0 && segs[used - 1].Texture == texture) return segs[used - 1]; if (used < segs.Count) { var s = segs[used++]; - s.TextureSlot = texture; + s.Texture = texture; s.Verts.Clear(); return s; } - var ns = new SpriteSeg { TextureSlot = texture }; + var ns = new SpriteSeg { Texture = texture }; segs.Add(ns); used++; return ns; @@ -274,10 +381,10 @@ internal sealed class TextRenderer : IDisposable { // Two triangles (6 verts). CCW in pixel space is clockwise in NDC // because the vertex shader flips Y, so OpenGL's default front-face - // is GL_CCW — we rely on cull-face being disabled during HUD pass. - // (x, y) ─ (x+w, y) - // │ │ - // (x, y+h) ─ (x+w, y+h) + // is GL_CCW — we rely on cull-face being disabled during HUD pass. + // (x, y) ─ (x+w, y) + // │ │ + // (x, y+h) ─ (x+w, y+h) // // Triangle 1: (x,y) (x+w,y+h) (x+w,y) // Triangle 2: (x,y) (x,y+h) (x+w,y+h) @@ -295,105 +402,131 @@ internal sealed class TextRenderer : IDisposable V(x + w, y + h, u1, v1); } - /// Upload + draw accumulated rects + text against the current frame. font may - /// be null if only DrawRect was used. - public void Flush(BitmapFont? font, IGpuFrame frame) + /// Upload + draw accumulated rects + text. font may be null if only DrawRect was used. + public void Flush(BitmapFont? font) { bool anyNormal = _segUsed > 0 || _textVerts > 0 || _rectVerts > 0; bool anyOverlay = _overlaySegUsed > 0 || _overlayTextVerts > 0 || _overlayRectVerts > 0; if (!anyNormal && !anyOverlay) return; - ArgumentNullException.ThrowIfNull(frame); - using IGpuPassEncoder pass = frame.BeginPass(new GpuPassDescription - { - Name = "ui-text", - // GL's BeginPass deliberately does not touch viewport/scissor for - // a Target:null pass, and Load/Store against the backbuffer - // reproduces exactly what this pass did before the RHI existed — - // the frame spine still owns clears until slice V4h. - Color = new GpuColorAttachment( - Target: null, - Load: GpuLoadOp.Load, - Store: GpuStoreOp.Store, - ClearColor: default), - Depth = null, - SampleCount = 1, - }); - pass.BindPipeline(_pipeline); + // Retained UI is a private render pass: an upload or draw failure must + // not leak its depth/cull/blend/MSAA state into a later recoverable + // frame. The focused scope restores from Flush's generated finally, + // including when either DrawLayer call throws. + using var stateScope = new TextRenderGlStateScope(_glState); - GpuPushConstants baseConstants = GpuPushConstants.Default; - baseConstants.ParamA = _screenSize.X; - baseConstants.ParamB = _screenSize.Y; + _shader.Use(); + _shader.SetVec2("uScreenSize", _screenSize); - // LAYERED compositing for the UI (background → fill → text): - // 1. RGBA dat sprites — window chrome / panel backgrounds (behind) - // 2. Untextured rects — widget fills (e.g. vital bars) on the chrome - // 3. Text glyphs — on top + _gl.BindVertexArray(_vao); + _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo); + + // Establish the self-contained UI pass state. + // The world pass leaves alpha-to-coverage + multisample enabled (WbDrawDispatcher, + // QualitySettings MSAA). If they bleed into the UI pass, each glyph's soft alpha + // EDGE is converted to dithered MSAA coverage instead of a clean alpha blend — + // the "text not sharp / fuzzy" artifact. The UI composites with straight alpha + // blending and must own this state (feedback_render_self_contained_gl_state). + _gl.Disable(EnableCap.SampleAlphaToCoverage); + _gl.Disable(EnableCap.Multisample); + _gl.Disable(EnableCap.DepthTest); + _gl.Disable(EnableCap.CullFace); + _gl.DepthMask(false); + _gl.Enable(EnableCap.Blend); + _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); + + // LAYERED compositing for the UI (background → fill → text): + // 1. RGBA dat sprites — window chrome / panel backgrounds (behind) + // 2. Untextured rects — widget fills (e.g. vital bars) on the chrome + // 3. Text glyphs — on top // Bucket 1 (sprites) draws in SUBMISSION (painter) order via _spriteSegs, // so sprite-on-sprite z is preserved. Buckets 2 (rects) + 3 (debug text) // composite on top, in that order. The OVERLAY layer repeats all three // AFTER the normal layer, so open popups beat even the rect backgrounds. - DrawLayer(pass, frame, in baseConstants, _spriteSegs, _segUsed, _rectBuf, _rectVerts, _textBuf, _textVerts, font); - DrawLayer(pass, frame, in baseConstants, _overlaySpriteSegs, _overlaySegUsed, _overlayRectBuf, _overlayRectVerts, _overlayTextBuf, _overlayTextVerts, font); + DrawLayer(_spriteSegs, _segUsed, _rectBuf, _rectVerts, _textBuf, _textVerts, font); + DrawLayer(_overlaySpriteSegs, _overlaySegUsed, _overlayRectBuf, _overlayRectVerts, _overlayTextBuf, _overlayTextVerts, font); + } - /// Draw one compositing layer: sprites (submission order, one draw per - /// segment) → untextured rects → debug-font text. Shared by the normal and overlay - /// layers; pipeline + pass are already bound by . + /// Draw one compositing layer: sprites (submission order, one call per + /// texture) → untextured rects → debug-font text. Shared by the normal and overlay + /// layers; GL state + shader are set up by . private void DrawLayer( - IGpuPassEncoder pass, - IGpuFrame frame, - in GpuPushConstants baseConstants, List spriteSegs, int segUsed, List rectBuf, int rectVerts, List textBuf, int textVerts, BitmapFont? font) { - // 1. RGBA dat sprites — one draw per distinct texture-table slot. - for (int i = 0; i < segUsed; i++) + // 1. RGBA dat sprites — one draw call per distinct GL texture. + if (segUsed > 0) { - SpriteSeg seg = spriteSegs[i]; - if (seg.Verts.Count == 0) continue; - DrawBucket(pass, frame, in baseConstants, seg.Verts, UseTextureSprite, seg.TextureSlot); + _shader.SetInt("uUseTexture", 2); + _gl.ActiveTexture(TextureUnit.Texture0); + _shader.SetInt("uTex", 0); + for (int i = 0; i < segUsed; i++) + { + var seg = spriteSegs[i]; + if (seg.Verts.Count == 0) continue; + _gl.BindTexture(TextureTarget.Texture2D, seg.Texture); + int firstVertex = UploadBuffer(seg.Verts); + _gl.DrawArrays(PrimitiveType.Triangles, firstVertex, (uint)(seg.Verts.Count / FloatsPerVertex)); + } } - // 2. Untextured rects — widget fills on top of the chrome. + // 2. Untextured rects — widget fills on top of the chrome. if (rectVerts > 0) - DrawBucket(pass, frame, in baseConstants, rectBuf, UseTextureNone, GpuTextureSlot.Unassigned); + { + _shader.SetInt("uUseTexture", 0); + int firstVertex = UploadBuffer(rectBuf); + _gl.DrawArrays(PrimitiveType.Triangles, firstVertex, (uint)rectVerts); + } // 3. Textured debug-font text glyphs on top. if (textVerts > 0 && font is not null) - DrawBucket(pass, frame, in baseConstants, textBuf, UseTextureFont, font.TextureId); + { + _shader.SetInt("uUseTexture", 1); + _gl.ActiveTexture(TextureUnit.Texture0); + _gl.BindTexture(TextureTarget.Texture2D, font.TextureId); + _shader.SetInt("uTex", 0); + int firstVertex = UploadBuffer(textBuf); + _gl.DrawArrays(PrimitiveType.Triangles, firstVertex, (uint)textVerts); + } } - private static void DrawBucket( - IGpuPassEncoder pass, - IGpuFrame frame, - in GpuPushConstants baseConstants, - List verts, - int useTexture, - GpuTextureSlot textureSlot) + private int UploadBuffer(List buf) { - int byteCount = verts.Count * sizeof(float); - if (byteCount == 0) return; + int bytes = buf.Count * sizeof(float); + if (bytes == 0) return 0; + FrameBufferSet set = _activeFrameBuffer + ?? throw new InvalidOperationException("BeginFrame must be called before rendering text."); + int byteOffset = set.UsedBytes; + int requiredBytes = checked(byteOffset + bytes); - GpuRingAllocation allocation = frame.AllocateRing(byteCount, GpuRingUsage.Vertex); - CollectionsMarshal.AsSpan(verts).CopyTo(allocation.AsSpan()); + if (requiredBytes > _vboCapacityBytes) + { + int newCapacity = DynamicBufferCapacity.Grow( + _vboCapacityBytes, + requiredBytes); + TrackedGlResource.AllocateBufferStorage( + _gl, + GLEnum.ArrayBuffer, + _vbo, + _vboCapacityBytes, + newCapacity, + GLEnum.DynamicDraw, + "TextRenderer frame VBO growth"); + _vboCapacityBytes = newCapacity; + } - GpuPushConstants constants = baseConstants; - constants.RenderPass = useTexture; - // Unassigned (the untextured-rect bucket) never reaches the shader as - // a slot index — uUseTexture==0 skips the sample entirely — but a - // defined value is still written so no stale slot lingers in the - // uniform between draws. - constants.TextureIndexA = textureSlot.IsAssigned ? textureSlot.Index : 0u; + fixed (float* p = CollectionsMarshal.AsSpan(buf)) + _gl.BufferSubData(BufferTargetARB.ArrayBuffer, (nint)byteOffset, (nuint)bytes, p); - pass.BindVertexBuffer(allocation.Buffer, allocation.OffsetBytes); - pass.SetPushConstants(in constants); - pass.Draw((uint)(verts.Count / FloatsPerVertex), instanceCount: 1, firstVertex: 0, firstInstance: 0); + set.UsedBytes = requiredBytes; + set.CapacityBytes = _vboCapacityBytes; + return byteOffset / (FloatsPerVertex * sizeof(float)); } public void Dispose() { - _pipeline.Dispose(); + _resources.RetryCleanup(); } } diff --git a/src/AcDream.App/Rendering/TextureCache.cs b/src/AcDream.App/Rendering/TextureCache.cs index adab5810..c4bfb1fd 100644 --- a/src/AcDream.App/Rendering/TextureCache.cs +++ b/src/AcDream.App/Rendering/TextureCache.cs @@ -1,4 +1,4 @@ -// src/AcDream.App/Rendering/TextureCache.cs +// src/AcDream.App/Rendering/TextureCache.cs using AcDream.Core.Textures; using AcDream.Core.World; using AcDream.Content; @@ -12,12 +12,11 @@ using AcDream.App.Rendering.Residency; namespace AcDream.App.Rendering; -internal sealed unsafe class TextureCache +public sealed unsafe class TextureCache : Wb.IEntityTextureLifetime, IDisposable { private readonly GL _gl; - private readonly IGpuDevice _device; private readonly IDatReaderWriter _dats; private readonly string _diagnosticsDirectory; // Handle and decoded dimensions are one atomic cache entry. Keeping them @@ -29,40 +28,17 @@ internal sealed unsafe class TextureCache _decodedDimensionsByTexture = new(); private uint _magentaHandle; - /// - /// Campaign V slice V4a: one registered plus its - /// device texture-table and decoded pixel - /// size. Direct-RenderSurface caches for UI sprites: 0x06xxxxxx - /// RenderSurface ids decoded directly (Portal/HighRes → - /// DecodeRenderSurface), bypassing the Surface→SurfaceTexture chain that - /// GetOrUpload uses for world materials — that world path stays on raw GL - /// () until its own campaign slice. - /// - private readonly record struct GpuUiTextureEntry( - GpuTextureSlot Slot, - IGpuTexture Texture, - int Width, - int Height); + // Direct-RenderSurface caches for UI sprites: 0x06xxxxxx RenderSurface ids + // decoded directly (Portal/HighRes → DecodeRenderSurface), bypassing the + // Surface→SurfaceTexture chain that GetOrUpload uses for world materials. + private readonly Dictionary _handlesByRenderSurfaceId = new(); + private readonly Dictionary _rsSizeById = new(); - private readonly Dictionary _renderSurfaceGpuTextures = new(); - - // Ad-hoc GPU textures produced by the public UploadRgba8(byte[],int,int,bool) - // wrapper (used by IconComposer for composited item icons). These are NOT - // stored in the keyed cache above, so Dispose must sweep this list to avoid - // leaking GPU texture-table slots until process exit. - private readonly List _adhocGpuTextures = new(); - - // Every UI-path upload uses REPEAT addressing (existing behaviour: panel - // fills and tiled chrome sample UVs > 1) with the caller-selected filter. - // Nearest+Repeat has no predefined GpuSamplerDescription (UiNearest clamps), - // so it is constructed once here; CreateSampler de-duplicates by value. - private static readonly GpuSamplerDescription UiSpriteNearestRepeat = new( - GpuFilter.Nearest, - GpuFilter.Nearest, - GpuMipFilter.None, - GpuAddressMode.Repeat, - GpuAddressMode.Repeat, - MaxAnisotropy: 1f); + // Ad-hoc handles produced by the public UploadRgba8(byte[],int,int,bool) wrapper + // (used by IconComposer for composited item icons). These are NOT stored in any + // of the keyed caches above, so Dispose must sweep this list to avoid leaking + // GL texture objects until process exit. + private readonly List _adhocHandles = new(); private readonly Wb.BindlessSupport? _bindless; private readonly CompositeTextureArrayCache? _compositeTextures; @@ -106,14 +82,13 @@ internal sealed unsafe class TextureCache // Frame counter for the one-shot ACDREAM_DUMP_SURFACES=1 trigger. // Increments per Tick call; fires the dump once at frame index 600 - // and never again for the session. See spec §5. + // and never again for the session. See spec §5. private int _dumpFrameCounter; private bool _surfaceHistogramAlreadyDumped; - public TextureCache(GL gl, IGpuDevice device, IDatReaderWriter dats, Wb.BindlessSupport? bindless = null) + public TextureCache(GL gl, IDatReaderWriter dats, Wb.BindlessSupport? bindless = null) : this( gl, - device, dats, bindless, ImmediateGpuResourceRetirementQueue.Instance, @@ -126,7 +101,6 @@ internal sealed unsafe class TextureCache internal TextureCache( GL gl, - IGpuDevice device, IDatReaderWriter dats, Wb.BindlessSupport? bindless, IGpuResourceRetirementQueue retirementQueue, @@ -135,7 +109,6 @@ internal sealed unsafe class TextureCache { budgets ??= ResidencyBudgetOptions.Default; _gl = gl; - _device = device ?? throw new ArgumentNullException(nameof(device)); _dats = dats; _bindless = bindless; ArgumentException.ThrowIfNullOrWhiteSpace(diagnosticsDirectory); @@ -246,22 +219,23 @@ internal sealed unsafe class TextureCache /// /// Upload a UI sprite by its RenderSurface DataId (0x06xxxxxx), decoded - /// DIRECTLY (Portal/HighRes → DecodeRenderSurface) rather than through the - /// Surface→SurfaceTexture chain that uses + /// DIRECTLY (Portal/HighRes → DecodeRenderSurface) rather than through the + /// Surface→SurfaceTexture chain that uses /// for world-geometry materials. This is the correct path for retail UI /// chrome + font glyph sheets, which reference RenderSurface directly. - /// Paletted (PFID_P8 / PFID_INDEX16) UI sprites — e.g. the selected-object - /// health-bar track 0x0600193E — are decoded against the RenderSurface's own + /// Paletted (PFID_P8 / PFID_INDEX16) UI sprites — e.g. the selected-object + /// health-bar track 0x0600193E — are decoded against the RenderSurface's own /// DefaultPaletteId (same starting palette - /// uses); non-paletted formats have DefaultPaletteId==0 → palette null. Returns + /// uses); non-paletted formats have DefaultPaletteId==0 → palette null. Returns /// a 1x1 magenta handle on miss. /// - public GpuTextureSlot GetOrUploadRenderSurface(uint renderSurfaceId, out int width, out int height, bool nearest = false) + public uint GetOrUploadRenderSurface(uint renderSurfaceId, out int width, out int height, bool nearest = false) { - if (_renderSurfaceGpuTextures.TryGetValue(renderSurfaceId, out GpuUiTextureEntry existing)) + if (_handlesByRenderSurfaceId.TryGetValue(renderSurfaceId, out var existing) + && _rsSizeById.TryGetValue(renderSurfaceId, out var sz)) { - width = existing.Width; height = existing.Height; - return existing.Slot; + width = sz.w; height = sz.h; + return existing; } DecodedTexture decoded; @@ -282,43 +256,16 @@ internal sealed unsafe class TextureCache decoded = DecodedTexture.Magenta; } - GpuUiTextureEntry entry = UploadUiTexture(decoded, nearest, $"ui-rendersurface-0x{renderSurfaceId:X8}"); - _renderSurfaceGpuTextures[renderSurfaceId] = entry; + uint h = UploadRgba8(decoded, nearest); + _handlesByRenderSurfaceId[renderSurfaceId] = h; + _rsSizeById[renderSurfaceId] = (decoded.Width, decoded.Height); width = decoded.Width; height = decoded.Height; - return entry.Slot; - } - - /// - /// Campaign V slice V4a: creates an for one - /// decoded UI sprite/atlas and registers it into the device's global - /// texture table. Every UI-path texture uses REPEAT addressing (existing - /// behaviour — panel fills and tiled chrome sample UVs greater than 1) and - /// a single mip level (UI sprites never mip). - /// selects point sampling for pixel-crisp glyphs/icons versus bilinear for - /// everything else, matching the GL path's prior per-call choice. - /// - private GpuUiTextureEntry UploadUiTexture(DecodedTexture decoded, bool nearest, string debugName) - { - int width = Math.Max(1, decoded.Width); - int height = Math.Max(1, decoded.Height); - IGpuTexture texture = _device.CreateTexture(new GpuTextureDescription( - debugName, - GpuTextureKind.Texture2D, - GpuTextureFormat.Rgba8Unorm, - Width: width, - Height: height, - LayerCount: 1, - MipLevelCount: 1)); - texture.Upload(0, 0, decoded.Rgba8); - IGpuSampler sampler = _device.CreateSampler( - nearest ? UiSpriteNearestRepeat : GpuSamplerDescription.WorldRepeat); - GpuTextureSlot slot = _device.RegisterTexture(texture, sampler); - return new GpuUiTextureEntry(slot, texture, decoded.Width, decoded.Height); + return h; } /// /// Alpha-channel histogram for one decoded texture. Used to diagnose - /// "why are clouds not transparent" — if cloud textures come out with + /// "why are clouds not transparent" — if cloud textures come out with /// alpha = 1.0 everywhere we know the decode path strips the alpha /// channel somewhere. Printed once per unique surfaceId under /// ACDREAM_DUMP_SKY=1. Adds ~2ms per texture upload, negligible. @@ -583,7 +530,7 @@ internal sealed unsafe class TextureCache { if (_bindless is null) throw new InvalidOperationException( - "TextureCache constructed without BindlessSupport — cannot generate bindless handles. " + + "TextureCache constructed without BindlessSupport — cannot generate bindless handles. " + "WbDrawDispatcher requires the bindless-aware ctor overload (pass non-null BindlessSupport)."); } @@ -638,7 +585,7 @@ internal sealed unsafe class TextureCache /// internal static ulong HashPaletteOverride(PaletteOverride p) { - // Not cryptographic — just needs to distinguish override setups + // Not cryptographic — just needs to distinguish override setups // for caching. Start with base palette id, fold in each entry. ulong h = 0xCBF29CE484222325UL; // FNV-1a offset basis const ulong prime = 0x100000001B3UL; @@ -659,17 +606,17 @@ internal sealed unsafe class TextureCache /// Phase N.6 slice 1: one-shot surface-format histogram dump for the /// atlas-opportunity audit. Activated by ACDREAM_DUMP_SURFACES=1; fires /// once after BOTH gates pass: - /// 1. _dumpFrameCounter >= 600 — at least 600 OnRender ticks + /// 1. _dumpFrameCounter >= 600 — at least 600 OnRender ticks /// have elapsed (catches the "we're already past startup boilerplate" /// bound; ~10s at 60fps, ~3s at 200fps). - /// 2. _uploadMetadata.Count >= 100 — the cache contains at + /// 2. _uploadMetadata.Count >= 100 — the cache contains at /// least 100 uploaded textures, indicating streaming has actually /// pulled in world content (not just sky/UI/font). The original /// frame-only gate fired during the login/handshake phase where /// OnRender ticks at GUI rates but no world has streamed in. /// Output goes to the host-provided portable diagnostics directory. /// Zero cost - /// when off. See spec §5 in + /// when off. See spec §5 in /// docs/superpowers/specs/2026-05-11-phase-n6-slice1-design.md. /// public void TickSurfaceHistogramDumpIfEnabled() @@ -694,7 +641,7 @@ internal sealed unsafe class TextureCache { // Diagnostic-only path. If the dump file can't be written // (disk full, permission denied, antivirus lock, path too - // long) we must NOT crash OnRender — that would invalidate + // long) we must NOT crash OnRender — that would invalidate // the very measurement pass this diagnostic is meant to // support. Log to stderr and let the caller mark the dump // as "already done" so it doesn't retry every frame. @@ -710,7 +657,7 @@ internal sealed unsafe class TextureCache "n6-surfaces.txt"); var sb = new System.Text.StringBuilder(); - sb.AppendLine($"# acdream surface-format histogram — generated {DateTime.UtcNow:yyyy-MM-ddTHH:mm:ssZ}"); + sb.AppendLine($"# acdream surface-format histogram — generated {DateTime.UtcNow:yyyy-MM-ddTHH:mm:ssZ}"); sb.AppendLine("# Per-entry: surfaceId(hex), width, height, format, byteCount"); sb.AppendLine(); @@ -763,7 +710,7 @@ internal sealed unsafe class TextureCache foreach (var kv in bucketsByFormat.OrderByDescending(kv => kv.Value)) sb.AppendLine($"# {kv.Key}: {kv.Value}"); - sb.AppendLine("# Top 10 (W,H,format) triples — atlas-opportunity input:"); + sb.AppendLine("# Top 10 (W,H,format) triples — atlas-opportunity input:"); foreach (var kv in bucketsByTriple.OrderByDescending(kv => kv.Value).Take(10)) sb.AppendLine($"# {kv.Key.W}x{kv.Key.H} {kv.Key.F}: {kv.Value}"); @@ -782,8 +729,8 @@ internal sealed unsafe class TextureCache } // Base1Solid surfaces (and any with OrigTextureId==0) carry a ColorValue - // instead of a texture chain. Overrides are irrelevant here — there's - // no texture chain to swap — so the override is ignored for solid-color + // instead of a texture chain. Overrides are irrelevant here — there's + // no texture chain to swap — so the override is ignored for solid-color // surfaces. Translucency is honored so Base1Solid|Translucent surfaces // with Translucency=1.0 become alpha=0, which the mesh shader's discard // cutout makes invisible. @@ -812,7 +759,7 @@ internal sealed unsafe class TextureCache // Start with the texture's default palette, then apply overlays. // ACViewer's Render/TextureCache.IndexToColor does the same and never - // consults ObjDesc.BasePaletteId for palette-indexed textures — the + // consults ObjDesc.BasePaletteId for palette-indexed textures — the // RenderSurface's own default palette is the starting point. Palette? basePalette = rs.DefaultPaletteId != 0 ? _dats.Get(rs.DefaultPaletteId) @@ -870,13 +817,12 @@ internal sealed unsafe class TextureCache /// to upload CPU-composited icon layers. /// The returned handle is tracked in and deleted by /// . Callers must NOT also store the handle in any of the - /// keyed caches — that would cause a double-delete on Dispose. - public GpuTextureSlot UploadRgba8(byte[] rgba, int width, int height, bool nearest = false) + /// keyed caches — that would cause a double-delete on Dispose. + public uint UploadRgba8(byte[] rgba, int width, int height, bool nearest = false) { - GpuUiTextureEntry entry = UploadUiTexture( - new DecodedTexture(rgba, width, height), nearest, "ui-adhoc-icon"); - _adhocGpuTextures.Add(entry); - return entry.Slot; + uint h = UploadRgba8(new DecodedTexture(rgba, width, height), nearest); + _adhocHandles.Add(h); + return h; } private uint UploadRgba8(DecodedTexture decoded, bool nearest = false) @@ -900,7 +846,7 @@ internal sealed unsafe class TextureCache PixelType.UnsignedByte, p); - // Point (nearest) sampling for pixel-exact UI text — bilinear softens the dat + // Point (nearest) sampling for pixel-exact UI text — bilinear softens the dat // font's small glyphs. Other surfaces use bilinear. int filter = nearest ? (int)TextureMinFilter.Nearest : (int)TextureMinFilter.Linear; _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, filter); @@ -1016,28 +962,16 @@ internal sealed unsafe class TextureCache _magentaHandle = 0; } - // RenderSurface (UI sprite) GPU textures — pre-existing gap: this dict was - // populated by GetOrUploadRenderSurface but was not swept here before that fix. - foreach (GpuUiTextureEntry entry in _renderSurfaceGpuTextures.Values) - DisposeUiTexture(entry); - _renderSurfaceGpuTextures.Clear(); + // RenderSurface (UI sprite) handles — pre-existing gap: this dict was populated + // by GetOrUploadRenderSurface but was not swept here before this fix. + foreach (var h in _handlesByRenderSurfaceId.Values) + DeleteUploadedTexture(h); + _handlesByRenderSurfaceId.Clear(); - // Ad-hoc GPU textures from the public UploadRgba8(byte[],int,int,bool) wrapper + // Ad-hoc handles from the public UploadRgba8(byte[],int,int,bool) wrapper // (IconComposer composited icons). Not stored in any keyed cache. - foreach (GpuUiTextureEntry entry in _adhocGpuTextures) - DisposeUiTexture(entry); - _adhocGpuTextures.Clear(); - } - - /// - /// Releases a UI-path texture's table slot before disposing the backing - /// . Both route through the device's retirement - /// queue, so releasing the slot first is purely bookkeeping order, not a - /// use-after-free concern. - /// - private void DisposeUiTexture(GpuUiTextureEntry entry) - { - _device.ReleaseTextureSlot(entry.Slot); - entry.Texture.Dispose(); + foreach (var h in _adhocHandles) + DeleteUploadedTexture(h); + _adhocHandles.Clear(); } } diff --git a/src/AcDream.App/Rendering/Vfx/AnimationHookFrameQueue.cs b/src/AcDream.App/Rendering/Vfx/AnimationHookFrameQueue.cs index 18f31c44..578a3b65 100644 --- a/src/AcDream.App/Rendering/Vfx/AnimationHookFrameQueue.cs +++ b/src/AcDream.App/Rendering/Vfx/AnimationHookFrameQueue.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.Core.Physics; using AcDream.Core.Vfx; using DatReaderWriter.Types; @@ -16,7 +16,7 @@ namespace AcDream.App.Rendering.Vfx; /// here preserves that observable order and avoids firing a hand or weapon /// effect against the previous animation frame. /// -internal sealed class AnimationHookFrameQueue +public sealed class AnimationHookFrameQueue { private readonly AnimationHookRouter _router; private readonly IEntityEffectPoseSource _poses; diff --git a/src/AcDream.App/Rendering/Vfx/EntityEffectController.cs b/src/AcDream.App/Rendering/Vfx/EntityEffectController.cs index f11df828..1d42cb5d 100644 --- a/src/AcDream.App/Rendering/Vfx/EntityEffectController.cs +++ b/src/AcDream.App/Rendering/Vfx/EntityEffectController.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.App.World; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; @@ -23,7 +23,7 @@ namespace AcDream.App.Rendering.Vfx; /// (0x00513260) and both play_default_script overloads /// (0x005132B0, 0x00513300). /// -internal sealed class EntityEffectController : IAnimationHookSink, +public sealed class EntityEffectController : IAnimationHookSink, IEntityEffectAdvanceSource { private readonly LiveEntityRuntime _liveEntities; diff --git a/src/AcDream.App/Rendering/Vfx/EntityEffectPoseRegistry.cs b/src/AcDream.App/Rendering/Vfx/EntityEffectPoseRegistry.cs index 88677b61..0e9a6362 100644 --- a/src/AcDream.App/Rendering/Vfx/EntityEffectPoseRegistry.cs +++ b/src/AcDream.App/Rendering/Vfx/EntityEffectPoseRegistry.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.Core.Vfx; using AcDream.Core.World; @@ -15,7 +15,7 @@ namespace AcDream.App.Rendering.Vfx; /// (0x0051D180). This registry is the modern, read-only seam exposing /// those same final frames without coupling Core effects to the renderer. /// -internal sealed class EntityEffectPoseRegistry : +public sealed class EntityEffectPoseRegistry : IEntityEffectPoseSource, IEntityEffectCellSource, IEntityEffectPoseChangeSource, @@ -314,7 +314,7 @@ internal sealed class EntityEffectPoseRegistry : } } -internal interface IEntityEffectPoseLifetimeSource +public interface IEntityEffectPoseLifetimeSource { ulong GetPoseOwnerLifetimeVersion(uint localEntityId); } diff --git a/src/AcDream.App/Rendering/Vfx/EntityEffectProfile.cs b/src/AcDream.App/Rendering/Vfx/EntityEffectProfile.cs index 4696558e..304b44bf 100644 --- a/src/AcDream.App/Rendering/Vfx/EntityEffectProfile.cs +++ b/src/AcDream.App/Rendering/Vfx/EntityEffectProfile.cs @@ -1,4 +1,4 @@ -using AcDream.App.World; +using AcDream.App.World; using AcDream.Core.Net.Messages; using AcDream.Core.Vfx; using DatReaderWriter.DBObjs; @@ -16,7 +16,7 @@ namespace AcDream.App.Rendering.Vfx; /// a network PhysicsDesc then unconditionally replaces the typed table, even /// when PeTable was absent or zero. /// -internal sealed class EntityEffectProfile : ILiveEntityEffectProfile +public sealed class EntityEffectProfile : ILiveEntityEffectProfile { private EntityEffectProfile(Setup setup) { diff --git a/src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs b/src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs index 97b23c27..d998be06 100644 --- a/src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs +++ b/src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.Core.Vfx; using AcDream.Core.World; @@ -8,7 +8,7 @@ namespace AcDream.App.Rendering.Vfx; /// Setup script/profile data resolved once when a logical effect owner is /// registered. Part transforms are indexed and root-local. /// -internal sealed record ScriptActivationInfo( +public sealed record ScriptActivationInfo( uint ScriptId, IReadOnlyList PartTransforms, EntityEffectProfile? EffectProfile = null, @@ -26,7 +26,7 @@ internal sealed record ScriptActivationInfo( /// initialization. Live registration invokes this class once per logical /// generation; spatial rebucketing never replays it. /// -internal sealed class EntityScriptActivator +public sealed class EntityScriptActivator { private sealed class StaticOwnerState { diff --git a/src/AcDream.App/Rendering/Vfx/LiveEntityLightController.cs b/src/AcDream.App/Rendering/Vfx/LiveEntityLightController.cs index 65e6582f..97a74b7e 100644 --- a/src/AcDream.App/Rendering/Vfx/LiveEntityLightController.cs +++ b/src/AcDream.App/Rendering/Vfx/LiveEntityLightController.cs @@ -1,4 +1,4 @@ -using AcDream.App.World; +using AcDream.App.World; using AcDream.Core.Lighting; using AcDream.Core.Physics; using AcDream.Runtime.Entities; @@ -17,7 +17,7 @@ namespace AcDream.App.Rendering.Vfx; /// ; leaving the world removes only this /// cell-scoped presentation and re-entry registers it again. /// -internal sealed class LiveEntityLightController : IDisposable +public sealed class LiveEntityLightController : IDisposable { private readonly LiveEntityRuntime _liveEntities; private readonly EntityEffectPoseRegistry _poses; diff --git a/src/AcDream.App/Rendering/Vfx/ParticleVisibilityController.cs b/src/AcDream.App/Rendering/Vfx/ParticleVisibilityController.cs index db95c41f..f922dd29 100644 --- a/src/AcDream.App/Rendering/Vfx/ParticleVisibilityController.cs +++ b/src/AcDream.App/Rendering/Vfx/ParticleVisibilityController.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.Core.Vfx; namespace AcDream.App.Rendering.Vfx; @@ -18,7 +18,7 @@ internal interface IWorldSceneParticleVisibility /// frame meaning: one completed viewer position plus the AC cells admitted by /// that completed view. It neither creates emitters nor performs rendering. /// -internal sealed class ParticleVisibilityController : IWorldSceneParticleVisibility +public sealed class ParticleVisibilityController : IWorldSceneParticleVisibility { public const float ExtendedRangeMultiplier = 2f; diff --git a/src/AcDream.App/Rendering/ViewconeCuller.cs b/src/AcDream.App/Rendering/ViewconeCuller.cs index 2bbbbc2d..d686deeb 100644 --- a/src/AcDream.App/Rendering/ViewconeCuller.cs +++ b/src/AcDream.App/Rendering/ViewconeCuller.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; @@ -6,31 +6,31 @@ namespace AcDream.App.Rendering; /// /// T3 (BR-5): the port of retail's Render::viewconeCheck (Ghidra -/// 0x0054c250) — meshes (characters, statics, emitters) are CULLED per portal +/// 0x0054c250) — meshes (characters, statics, emitters) are CULLED per portal /// view by a bounding-sphere test against the view's edge planes, never /// clipped. Retail stores each view vertex with its 3D eye-edge plane /// (view_vertex { Vec2D pt; Plane plane }, acclient.h:32483) and tests /// the object's drawing sphere against the installed view's plane set; -/// OUTSIDE → skipped (RenderDeviceD3D::DrawMesh per-view loop pc:429290-429310, +/// OUTSIDE → skipped (RenderDeviceD3D::DrawMesh per-view loop pc:429290-429310, /// and the DrawCells per-cell object epilogue, Ghidra 0x005a4840). /// -/// Our views are clip-space half-planes (≤8 per slice, +/// Our views are clip-space half-planes (≤8 per slice, /// output: (nx,ny,0,d) satisfied when -/// nx·Cx + ny·Cy + d·Cw ≥ 0 for clip-space C). Lifting one to world space — -/// the view_vertex.plane analog, a plane through the EYE and the view edge — +/// nx·Cx + ny·Cy + d·Cw ≥ 0 for clip-space C). Lifting one to world space — +/// the view_vertex.plane analog, a plane through the EYE and the view edge — /// is one matrix fold: with row-vector convention (System.Numerics), -/// C = world·VP, so C·P = world·(VP·P); L = VP·P (rows of VP dotted with P) +/// C = world·VP, so C·P = world·(VP·P); L = VP·P (rows of VP dotted with P) /// is the world-space homogeneous half-plane. Sphere-vs-half-plane keeps the -/// sphere when L.xyz·c + L.w ≥ −r·|L.xyz| (not entirely outside). +/// sphere when L.xyz·c + L.w ≥ −r·|L.xyz| (not entirely outside). /// /// A sphere is visible through a SLICE when it is not entirely outside /// any of the slice's planes (convex region); visible for a CELL when any of /// the cell's slices passes. A slice with zero planes is pass-all (the -/// NoClipSlice / full-screen outdoor case). A cell with no views culls — in +/// NoClipSlice / full-screen outdoor case). A cell with no views culls — in /// retail an object whose cell is not in the draw list is simply never /// reached. /// -internal sealed class ViewconeCuller +public sealed class ViewconeCuller { private const int MaxRetainedCellPlaneSets = 512; private const int MaxRetainedPlanesPerCell = 256; @@ -65,7 +65,7 @@ internal sealed class ViewconeCuller } /// True when the outside view is a full-screen pass-all (the - /// synthetic outdoor root) — every outside-test passes. + /// synthetic outdoor root) — every outside-test passes. public bool OutsideIsFullScreen { get; private set; } public static ViewconeCuller Build( @@ -158,7 +158,7 @@ internal sealed class ViewconeCuller Vector4 l = plane.Equation; float nLen = plane.NormalLength; if (nLen < 1e-12f) - continue; // degenerate plane — no constraint + continue; // degenerate plane — no constraint float dist = l.X * center.X + l.Y * center.Y + l.Z * center.Z + l.W; if (dist < -radius * nLen) return false; // entirely outside this edge plane @@ -167,7 +167,7 @@ internal sealed class ViewconeCuller } /// Sphere-vs-the-cell's-views: visible when any slice passes. - /// A cell with no views culls (not in the draw list ⇒ never reached in + /// A cell with no views culls (not in the draw list ⇒ never reached in /// retail). A zero-plane slice is pass-all. public bool SphereVisibleInCell(uint cellId, in Vector3 center, float radius) { diff --git a/src/AcDream.App/Rendering/Wb/AnimatedEntityState.cs b/src/AcDream.App/Rendering/Wb/AnimatedEntityState.cs index 168c7612..913b7bf1 100644 --- a/src/AcDream.App/Rendering/Wb/AnimatedEntityState.cs +++ b/src/AcDream.App/Rendering/Wb/AnimatedEntityState.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using AcDream.Core.Physics; namespace AcDream.App.Rendering.Wb; @@ -8,7 +8,7 @@ namespace AcDream.App.Rendering.Wb; /// equipped items). Holds AC-specific per-instance customizations the WB /// atlas cache doesn't carry: AnimPartChange override map + /// HiddenParts bitmask. Also holds a reference to acdream's existing -/// — Phase N.4 explicitly does not touch +/// — Phase N.4 explicitly does not touch /// the sequencer; we just route through it at draw time. /// /// @@ -16,11 +16,11 @@ namespace AcDream.App.Rendering.Wb; /// a server CreateObject is processed; destroyed by /// EntitySpawnAdapter.OnRemove on RemoveObject. The mesh /// data backing each part is cached in WB's ObjectMeshManager; -/// per-instance customizations don't go through the atlas — they overlay +/// per-instance customizations don't go through the atlas — they overlay /// at draw time. /// /// -internal sealed class AnimatedEntityState +public sealed class AnimatedEntityState { private readonly Dictionary _partGfxObjOverrides = new(); private ulong _hiddenMask = 0; @@ -49,7 +49,7 @@ internal sealed class AnimatedEntityState } /// Override the GfxObj id for a Setup part. Used for - /// AnimPartChange — e.g. wielding a weapon swaps the hand-part's + /// AnimPartChange — e.g. wielding a weapon swaps the hand-part's /// GfxObj. public void SetPartOverride(int partIdx, ulong gfxObjId) => _partGfxObjOverrides[partIdx] = gfxObjId; diff --git a/src/AcDream.App/Rendering/Wb/BindlessSupport.cs b/src/AcDream.App/Rendering/Wb/BindlessSupport.cs index 299e0350..cde447c2 100644 --- a/src/AcDream.App/Rendering/Wb/BindlessSupport.cs +++ b/src/AcDream.App/Rendering/Wb/BindlessSupport.cs @@ -1,4 +1,4 @@ -using Silk.NET.OpenGL; +using Silk.NET.OpenGL; using Silk.NET.OpenGL.Extensions.ARB; using AcDream.App.Rendering; @@ -9,7 +9,7 @@ namespace AcDream.App.Rendering.Wb; /// for the modern rendering path. Constructed once at startup via /// , which returns false if the extension isn't present. /// -internal sealed class BindlessSupport +public sealed class BindlessSupport { private readonly GL _gl; private readonly ArbBindlessTexture _ext; @@ -63,7 +63,7 @@ internal sealed class BindlessSupport /// make it resident. Idempotent per (texture, sampler) pair. /// /// Added for Campaign V slice V1's GlGpuDevice.RegisterTexture, - /// which registers a (texture, sampler) pair per the RHI contract — "the + /// which registers a (texture, sampler) pair per the RHI contract — "the /// same texture registered with two samplers occupies two slots." The /// texture-only above cannot express /// that; ManagedGLTextureArray already calls the equivalent @@ -117,7 +117,7 @@ internal sealed class BindlessSupport // and removed when terrain rendering surfaced GL_INVALID_OPERATION on // NVIDIA Windows for the `uniform sampler2DArray` + glProgramUniformHandleARB // combination. The replacement pattern (uvec2 handle uniform + GLSL - // sampler-from-handle constructor — see terrain_modern.frag) lives at the + // sampler-from-handle constructor — see terrain_modern.frag) lives at the // call site via plain `_gl.ProgramUniform2(program, loc, low, high)`. If // you re-introduce a sampler-handle helper, restrict it to drivers known // to accept the direct sampler-uniform path. diff --git a/src/AcDream.App/Rendering/Wb/BufferUsageExtensions.cs b/src/AcDream.App/Rendering/Wb/BufferUsageExtensions.cs index 47d08f38..19788818 100644 --- a/src/AcDream.App/Rendering/Wb/BufferUsageExtensions.cs +++ b/src/AcDream.App/Rendering/Wb/BufferUsageExtensions.cs @@ -1,4 +1,4 @@ -using Chorizite.Core.Render.Enums; +using Chorizite.Core.Render.Enums; using Silk.NET.OpenGL; using System; using System.Collections.Generic; @@ -7,7 +7,7 @@ using System.Text; using System.Threading.Tasks; namespace AcDream.App.Rendering.Wb { - internal static class BufferUsageExtensions { + public static class BufferUsageExtensions { /// /// Converts a BufferUsage to a GL BufferUsageARB /// diff --git a/src/AcDream.App/Rendering/Wb/Building.cs b/src/AcDream.App/Rendering/Wb/Building.cs index e43d5134..aaff1b96 100644 --- a/src/AcDream.App/Rendering/Wb/Building.cs +++ b/src/AcDream.App/Rendering/Wb/Building.cs @@ -1,12 +1,12 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Numerics; namespace AcDream.App.Rendering.Wb; /// -/// Phase A8 (2026-05-26): a logical building — one or more EnvCells linked +/// Phase A8 (2026-05-26): a logical building — one or more EnvCells linked /// via the dat-level LandBlockInfo.Buildings entry. Building shells (cottage -/// walls, inn walls — IsBuildingShell=true entities) are scoped to this +/// walls, inn walls — IsBuildingShell=true entities) are scoped to this /// building's cells via their dat-derived anchor. The exit portal polygons are /// stencil-marked so outdoor visibility leaks through portal silhouettes only. /// @@ -19,7 +19,7 @@ namespace AcDream.App.Rendering.Wb; /// Retail reference: docs/research/named-retail/acclient.h:32035 /// (BuildInfo) + 32094 (CBldPortal). /// -internal sealed class Building +public sealed class Building { /// Unique within a landblock; allocated sequentially by /// starting at 1 (0 is reserved for "no building" semantics on LoadedCell). diff --git a/src/AcDream.App/Rendering/Wb/BuildingLoader.cs b/src/AcDream.App/Rendering/Wb/BuildingLoader.cs index b5e6c978..bcb6c586 100644 --- a/src/AcDream.App/Rendering/Wb/BuildingLoader.cs +++ b/src/AcDream.App/Rendering/Wb/BuildingLoader.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering; using DatReaderWriter.DBObjs; @@ -14,11 +14,11 @@ namespace AcDream.App.Rendering.Wb; /// Algorithm (mirrors WB's PortalService.GetPortalsByBuilding at /// WorldBuilder.Shared/Services/PortalService.cs:43-97): /// -/// Step A — seed the cell set from BuildingInfo.Portals entry portals. -/// Step B — BFS through to discover all +/// Step A — seed the cell set from BuildingInfo.Portals entry portals. +/// Step B — BFS through to discover all /// interior cells reachable from the entry portals (interior portals only; -/// exit portals — OtherCellId == 0xFFFF — terminate each BFS branch). -/// Step C — collect exit portal polygons in world space for the stencil +/// exit portals — OtherCellId == 0xFFFF — terminate each BFS branch). +/// Step C — collect exit portal polygons in world space for the stencil /// pipeline (Phase A8 Steps 1+2, RR7 scope). /// /// @@ -57,7 +57,7 @@ internal sealed class BuildingRegistryPublication internal bool PublicationCommitted { get; set; } } -internal static class BuildingLoader +public static class BuildingLoader { /// /// Builds a from the supplied landblock data. diff --git a/src/AcDream.App/Rendering/Wb/BuildingRegistry.cs b/src/AcDream.App/Rendering/Wb/BuildingRegistry.cs index f2826008..998f64bd 100644 --- a/src/AcDream.App/Rendering/Wb/BuildingRegistry.cs +++ b/src/AcDream.App/Rendering/Wb/BuildingRegistry.cs @@ -1,16 +1,16 @@ -using System; +using System; using System.Collections.Generic; namespace AcDream.App.Rendering.Wb; /// /// Phase A8 (2026-05-26): per-landblock registry of s. -/// Two-way indexed for O(1) cell→building and building-id→building lookups. +/// Two-way indexed for O(1) cell→building and building-id→building lookups. /// Built once per landblock at load time by ; /// no mutations occur after initial population. /// -/// The cell→building index uses a List<Building> value type -/// to handle the (rare but valid) case where two buildings share an EnvCell — +/// The cell→building index uses a List<Building> value type +/// to handle the (rare but valid) case where two buildings share an EnvCell — /// each building performs its own BFS so a shared boundary cell ends up in both /// EnvCellIds sets. returns all /// owners so RR7's render path can pick the correct one. @@ -19,13 +19,13 @@ namespace AcDream.App.Rendering.Wb; /// (BuildingPortalGroup). Design: /// docs/superpowers/specs/2026-05-26-phase-a8-wb-full-port-design.md. /// -internal sealed class BuildingRegistry +public sealed class BuildingRegistry { - // Index 1: cell-id → list of buildings containing that cell. + // Index 1: cell-id → list of buildings containing that cell. // Cells may belong to multiple buildings (rare; handled via List). private readonly Dictionary> _byCellId = new(); - // Index 2: building-id → Building. + // Index 2: building-id → Building. private readonly Dictionary _byBuildingId = new(); /// diff --git a/src/AcDream.App/Rendering/Wb/DebugRenderSettings.cs b/src/AcDream.App/Rendering/Wb/DebugRenderSettings.cs index a8b28c68..66bdd06a 100644 --- a/src/AcDream.App/Rendering/Wb/DebugRenderSettings.cs +++ b/src/AcDream.App/Rendering/Wb/DebugRenderSettings.cs @@ -1,10 +1,10 @@ -using System.Numerics; +using System.Numerics; namespace AcDream.App.Rendering.Wb { // Extracted verbatim from WorldBuilder.Shared/Models/DebugRenderSettings.cs. // LandscapeColorsSettings dependency (editor-only, CommunityToolkit.Mvvm) stripped; // default color values inlined from LandscapeColorsSettings field initializers. - internal class DebugRenderSettings { + public class DebugRenderSettings { public bool ShowBoundingBoxes { get; set; } = false; public bool SelectVertices { get; set; } = true; public bool SelectBuildings { get; set; } = true; diff --git a/src/AcDream.App/Rendering/Wb/DrawElementsIndirectCommand.cs b/src/AcDream.App/Rendering/Wb/DrawElementsIndirectCommand.cs index ee4a4d4f..80d1119d 100644 --- a/src/AcDream.App/Rendering/Wb/DrawElementsIndirectCommand.cs +++ b/src/AcDream.App/Rendering/Wb/DrawElementsIndirectCommand.cs @@ -1,4 +1,4 @@ -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; namespace AcDream.App.Rendering.Wb; @@ -7,7 +7,7 @@ namespace AcDream.App.Rendering.Wb; /// Total size 20 bytes; arrays are typically uploaded with stride = sizeof(this). /// [StructLayout(LayoutKind.Sequential, Pack = 4)] -internal struct DrawElementsIndirectCommand +public struct DrawElementsIndirectCommand { public uint Count; // index count for this draw public uint InstanceCount; // number of instances diff --git a/src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs b/src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs index 35d75023..1caaa343 100644 --- a/src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs +++ b/src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using AcDream.Core.Physics; using AcDream.Core.World; @@ -11,7 +11,7 @@ namespace AcDream.App.Rendering.Wb; /// logical removal has been queued and must be retried by the live-entity /// teardown owner after the transition unwinds. /// -internal sealed class EntityPresentationRemovalDeferredException(uint serverGuid) +public sealed class EntityPresentationRemovalDeferredException(uint serverGuid) : InvalidOperationException( $"Live entity 0x{serverGuid:X8} presentation removal is deferred until its active reference transition completes."); @@ -43,7 +43,7 @@ internal sealed class EntityPresentationRemovalDeferredException(uint serverGuid /// both to the created . /// /// -internal sealed class EntitySpawnAdapter +public sealed class EntitySpawnAdapter { private readonly IEntityTextureLifetime _textureLifetime; private readonly Func _sequencerFactory; @@ -161,14 +161,14 @@ internal sealed class EntitySpawnAdapter } // A.5 T18: populate cached AABB so WalkEntities reads from the cache - // rather than recomputing Position±5 per frame. Called here because + // rather than recomputing Position±5 per frame. Called here because // all entity-state initialization (position, rotation) is complete // by this point via the WorldEntity passed in. entity.RefreshAabb(); // Build the per-entity AnimatedEntityState. The sequencer factory // may return a stub (in tests) or a fully-constructed sequencer from - // the MotionTable (in production). Factory must not return null — + // the MotionTable (in production). Factory must not return null — // if the entity has no motion table the factory should construct a // no-op sequencer (Setup + empty MotionTable + NullAnimationLoader). var sequencer = _sequencerFactory(entity); @@ -185,7 +185,7 @@ internal sealed class EntitySpawnAdapter // Snapshot each unique GfxObj id for the shorter presentation lifetime. // Includes both the entity's natural MeshRefs AND any server-sent - // PartOverride GfxObjs (weapons, clothing, helmets) — those replace the + // PartOverride GfxObjs (weapons, clothing, helmets) — those replace the // Setup default and need their own mesh data uploaded. // Construct the replacement completely before displacing a live owner. // Sequencer/appearance construction is allowed to fail; in that case diff --git a/src/AcDream.App/Rendering/Wb/EnvCellLandblockBuild.cs b/src/AcDream.App/Rendering/Wb/EnvCellLandblockBuild.cs index 9fbbfe81..9a1c02a0 100644 --- a/src/AcDream.App/Rendering/Wb/EnvCellLandblockBuild.cs +++ b/src/AcDream.App/Rendering/Wb/EnvCellLandblockBuild.cs @@ -1,4 +1,4 @@ -using System.Collections.Immutable; +using System.Collections.Immutable; using System.Numerics; using AcDream.Core.Rendering.Wb; using DatReaderWriter.DBObjs; @@ -12,7 +12,7 @@ namespace AcDream.App.Rendering.Wb; /// placement data; the render thread schedules mesh preparation when it commits /// the containing . /// -internal sealed record EnvCellShellPlacement( +public sealed record EnvCellShellPlacement( uint CellId, ulong GeometryId, uint EnvironmentId, @@ -29,7 +29,7 @@ internal sealed record EnvCellShellPlacement( /// both portal-visibility cells and drawable shell placements so neither can be /// drained by, or mixed with, another streaming completion. /// -internal sealed class EnvCellLandblockBuild +public sealed class EnvCellLandblockBuild { public EnvCellLandblockBuild( uint landblockId, @@ -57,7 +57,7 @@ internal sealed class EnvCellLandblockBuild /// global pending bags, instances of this class are never shared between jobs or /// observed by the render thread before returns. /// -internal sealed class EnvCellLandblockBuildBuilder +public sealed class EnvCellLandblockBuildBuilder { private readonly uint _landblockId; private readonly List _visibilityCells = new(); diff --git a/src/AcDream.App/Rendering/Wb/EnvCellLandblockPublication.cs b/src/AcDream.App/Rendering/Wb/EnvCellLandblockPublication.cs index a40d625b..1b39656f 100644 --- a/src/AcDream.App/Rendering/Wb/EnvCellLandblockPublication.cs +++ b/src/AcDream.App/Rendering/Wb/EnvCellLandblockPublication.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; namespace AcDream.App.Rendering.Wb; @@ -7,7 +7,7 @@ namespace AcDream.App.Rendering.Wb; /// one private shell at a time; commit publishes the completed immutable /// landblock snapshot with one owner dictionary replacement. /// -internal interface IEnvCellLandblockPublisher +public interface IEnvCellLandblockPublisher { EnvCellLandblockPublication PreparePublication( EnvCellLandblockBuild build); @@ -17,7 +17,7 @@ internal interface IEnvCellLandblockPublisher void CommitPublication(EnvCellLandblockPublication publication); } -internal sealed class EnvCellLandblockPublication +public sealed class EnvCellLandblockPublication { internal EnvCellLandblockPublication( object owner, diff --git a/src/AcDream.App/Rendering/Wb/EnvCellMeshPreparationScheduler.cs b/src/AcDream.App/Rendering/Wb/EnvCellMeshPreparationScheduler.cs index 9f8d2f6c..725c0137 100644 --- a/src/AcDream.App/Rendering/Wb/EnvCellMeshPreparationScheduler.cs +++ b/src/AcDream.App/Rendering/Wb/EnvCellMeshPreparationScheduler.cs @@ -1,4 +1,4 @@ -namespace AcDream.App.Rendering.Wb; +namespace AcDream.App.Rendering.Wb; /// /// Starts CPU mesh extraction after the completed EnvCell build has been @@ -7,7 +7,7 @@ /// current streaming generation; stale portal destinations never keep decoder /// jobs or surface lists alive. /// -internal static class EnvCellMeshPreparationScheduler +public static class EnvCellMeshPreparationScheduler { public static void Schedule( EnvCellLandblockBuild build, diff --git a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs index e4983b56..4c4054a1 100644 --- a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs +++ b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs @@ -1,4 +1,4 @@ -// Phase A8 (2026-05-28): port of WB's EnvCellRenderManager. This is the +// Phase A8 (2026-05-28): port of WB's EnvCellRenderManager. This is the // production cell-rendering pipeline for indoor visibility, replacing the // broken "cell as WorldEntity with MeshRef(envCellId)" approach that the // four reverted RR7 variants couldn't fix. @@ -12,7 +12,7 @@ // // Note: we do NOT inherit from WB's ObjectRenderManagerBase. That base // class owns the landblock-streaming loop (Update, _pendingGeneration, -// _uploadQueue). acdream's StreamingController already does that work — +// _uploadQueue). acdream's StreamingController already does that work — // running a parallel loop would compete for dat I/O. Instead, streaming builds // a private EnvCellLandblockBuild and CommitLandblock publishes the completed // snapshot on the render thread. @@ -29,7 +29,7 @@ using Silk.NET.OpenGL; namespace AcDream.App.Rendering.Wb; -internal sealed unsafe class EnvCellRenderer : +public sealed unsafe class EnvCellRenderer : IDisposable, IEnvCellLandblockPublisher { @@ -39,7 +39,7 @@ internal sealed unsafe class EnvCellRenderer : private readonly WbFrustum _frustum; // Per-landblock storage. Key = full 32-bit landblock dat id (e.g. 0xA9B4FFFF). - // WB EnvCellRenderManager.cs:75 uses ConcurrentDictionary _landblocks — + // WB EnvCellRenderManager.cs:75 uses ConcurrentDictionary _landblocks — // we use uint (full LB id) because acdream uses 32-bit landblock keys throughout. private readonly ConcurrentDictionary _landblocks = new(); @@ -64,7 +64,7 @@ internal sealed unsafe class EnvCellRenderer : private Matrix4x4 _lastViewProjection = Matrix4x4.Identity; private bool _initialized; - // List pool — copied from WB ObjectRenderManagerBase. + // List pool — copied from WB ObjectRenderManagerBase. // WB ObjectRenderManagerBase.cs:83-86: protected readonly List> _listPool = new(); protected int _poolIndex = 0; private readonly List> _listPool = new(); private int _poolIndex = 0; @@ -78,7 +78,7 @@ internal sealed unsafe class EnvCellRenderer : private readonly ThreadLocal _prepareScratch = new(() => new PrepareScratch(), trackAllValues: true); - // Modern-MDI scratch buffers (single slot — we re-upload every frame). + // Modern-MDI scratch buffers (single slot — we re-upload every frame). // WB BaseObjectRenderManager.cs:43-48: _scratchMdiCommandBuffers, _scratchModernBatchBuffers, _modernInstanceBuffers // We collapse the ring-of-3 to a single slot since we have no persistent/consolidated draws. private uint _mdiCommandBuffer; @@ -95,7 +95,7 @@ internal sealed unsafe class EnvCellRenderer : // Phase U.3: per-instance clip-slot SSBO (binding=3), parallel to // _modernInstanceBuffer. One uint per instance selecting its CellClip slot, // indexed by the same BaseInstance + gl_InstanceID the shader uses for - // binding=0. ALL ZEROS in U.3 ⇒ slot 0 ⇒ no-clip. U.4 populates real slots. + // binding=0. ALL ZEROS in U.3 ⇒ slot 0 ⇒ no-clip. U.4 populates real slots. private uint _clipSlotBuffer; private int _clipSlotCapacity; private uint[] _clipSlotData = Array.Empty(); @@ -156,17 +156,17 @@ internal sealed unsafe class EnvCellRenderer : // Campaign V slice V2 (2026-07-27): GL-only emulation of the eventual // Vulkan global texture descriptor array (binding=9, // GpuBindingModel.StorageTextureTable). Owns its own table rather than - // sharing WbDrawDispatcher's — EnvCellRenderer never had a TextureCache + // sharing WbDrawDispatcher's — EnvCellRenderer never had a TextureCache // dependency and nothing requires index agreement between renderers (each // rebinds its own buffer to binding=9 immediately before its own draw // call). See GlBindlessHandleTable's doc comment and the campaign doc's - // §5.2. Lazily created; grown/uploaded only when a genuinely new handle - // appears (rare — see FlushAndBindTextureTable). + // §5.2. Lazily created; grown/uploaded only when a genuinely new handle + // appears (rare — see FlushAndBindTextureTable). private readonly GlBindlessHandleTable _textureTable = new(); private uint _textureTableSsbo; private int _textureTableSsboCapacityBytes; - // Reusable scratch arrays — avoid per-frame allocation. + // Reusable scratch arrays — avoid per-frame allocation. // WB BaseObjectRenderManager.cs:58-59: private DrawElementsIndirectCommand[] _commands = Array.Empty<...>() private DrawElementsIndirectCommand[] _commands = Array.Empty(); private ModernBatchData[] _modernBatches = Array.Empty(); @@ -192,7 +192,7 @@ internal sealed unsafe class EnvCellRenderer : private readonly Dictionary> _activeSnapshotGlobalGroups = new(); private readonly List _activeSnapshotGlobalGfxObjIds = new(); - // Static render-state tracking — matches WB BaseObjectRenderManager.cs:24-28. + // Static render-state tracking — matches WB BaseObjectRenderManager.cs:24-28. // Shared across all manager instances on the same GL context. private static uint _currentVao; private static CullMode? _currentCullMode; @@ -204,8 +204,8 @@ internal sealed unsafe class EnvCellRenderer : // inputs changed: landblock commits/removals (NeedsPrepare), the visible-cell // filter, the trim window, mesh render-data availability (the snapshot bakes // per-cell transparency from TryGetRenderData), or the view-projection. - // NeedsPrepare existed since A8 but was never read — this wires it. The VP - // tolerance must swallow the ~36 µm eye rest jitter (RetailPViewRenderer + // NeedsPrepare existed since A8 but was never read — this wires it. The VP + // tolerance must swallow the ~36 µm eye rest jitter (RetailPViewRenderer // R-A2 note) while any real camera motion crosses it in the same frame. private Matrix4x4 _preparedViewProjection; private Vector3 _preparedCameraPosition; @@ -223,14 +223,14 @@ internal sealed unsafe class EnvCellRenderer : public bool IsDisposed { get; private set; } public LastFrameStats Stats => _lastFrameStats; - internal struct LastFrameStats { public int CellsRendered; public int TrianglesDrawn; } + public struct LastFrameStats { public int CellsRendered; public int TrianglesDrawn; } private LastFrameStats _lastFrameStats; /// /// Diagnostic accessor for the [envcells] probe (Phase A8 apparatus 2026-05-28). /// Returns (pool-list count total, snapshot's PostPreparePoolIndex high-water). /// A divergence between expected and actual values would indicate a pool- - /// management regression — exactly the bug class the 2026-05-28 audit caught. + /// management regression — exactly the bug class the 2026-05-28 audit caught. /// public (int PoolTotal, int SnapshotPoolHwm) GetPoolDiagnostics() { @@ -338,20 +338,20 @@ internal sealed unsafe class EnvCellRenderer : public void SetClipRegionSsbo(uint sharedClipRegionSsbo) => _sharedClipRegionSsbo = sharedClipRegionSsbo; - // Phase U.4: per-frame cellId→CellClip-slot map for the cell shells. When + // Phase U.4: per-frame cellId→CellClip-slot map for the cell shells. When // non-null, RenderModernMDIInternal writes instanceClipSlot[i] = // _cellIdToSlot[allInstances[i].CellId] so each cell's shell instances are // gated to that cell's portal-clip region. When null (U.3 path), every // instance maps to slot 0 (no-clip). A cell absent from the map writes slot 0 - // (no-clip) — but the caller's Render filter already restricts the draw to the + // (no-clip) — but the caller's Render filter already restricts the draw to the // map's keys, so that fallback should not fire in practice. private IReadOnlyDictionary? _cellIdToSlot; /// - /// Phase U.4: install the per-frame cellId→slot map used to gate cell shells + /// Phase U.4: install the per-frame cellId→slot map used to gate cell shells /// to their portal-clip regions. Call once per frame BEFORE /// . Pass null to revert to - /// the U.3 no-clip behavior (every shell instance → slot 0). + /// the U.3 no-clip behavior (every shell instance → slot 0). /// public void SetClipRouting(IReadOnlyDictionary? cellIdToSlot) => _cellIdToSlot = cellIdToSlot; @@ -386,7 +386,7 @@ internal sealed unsafe class EnvCellRenderer : surfaces); // --------------------------------------------------------------------------- - // CommitLandblock — render-thread transaction boundary + // CommitLandblock — render-thread transaction boundary // --------------------------------------------------------------------------- /// @@ -586,7 +586,7 @@ internal sealed unsafe class EnvCellRenderer : int? renderRadius = null) { // Phase U.4 fix: stash the view-projection so Render() can upload it itself. - // Stashed even when the gate below skips the rebuild — Render must always + // Stashed even when the gate below skips the rebuild — Render must always // project with the CURRENT frame's matrix (the U.4 stale-matrix root cause). _lastViewProjection = viewProjection; @@ -614,7 +614,7 @@ internal sealed unsafe class EnvCellRenderer : return; } - // Prepare gate: every snapshot input unchanged → keep the active snapshot. + // Prepare gate: every snapshot input unchanged → keep the active snapshot. // (Same-thread discipline makes the version sample exact: publish, release // tickets, and this method all run on the render thread.) if (_hasPreparedSnapshot @@ -633,7 +633,7 @@ internal sealed unsafe class EnvCellRenderer : lock (_renderLock) { _poolIndex = 0; } // WB skips _cameraLbX/Y update (from LandscapeDoc.Region) here in our variant - // because we don't need camera-LB tracking for the snapshot — just frustum tests. + // because we don't need camera-LB tracking for the snapshot — just frustum tests. // WB EnvCellRenderManager.cs:262: // Filter loaded landblocks by GpuReady + Instances non-empty. @@ -674,7 +674,7 @@ internal sealed unsafe class EnvCellRenderer : PrepareScratch scratch = _prepareScratch.Value!; - // WB EnvCellRenderManager.cs:279-295: fast path — LB fully inside. + // WB EnvCellRenderManager.cs:279-295: fast path — LB fully inside. if (testResult == FrustumTestResult.Inside) { foreach (var (gfxObjId, instances) in lb.BuildingPartGroups) @@ -692,7 +692,7 @@ internal sealed unsafe class EnvCellRenderer : return; } - // WB EnvCellRenderManager.cs:298-324: slow path — per-cell frustum test. + // WB EnvCellRenderManager.cs:298-324: slow path — per-cell frustum test. HashSet visibleCells = scratch.VisibleCells; visibleCells.Clear(); foreach (var kvp in lb.EnvCellBounds) @@ -804,13 +804,13 @@ internal sealed unsafe class EnvCellRenderer : /// /// Pure half of the prepare gate's camera test (regression-tested without a /// GL context, same pattern as ). - /// Eye position uses a 1 mm ABSOLUTE epsilon: it swallows the ~36 µm rest + /// Eye position uses a 1 mm ABSOLUTE epsilon: it swallows the ~36 µm rest /// jitter but dirties on any real movement (a slow walk moves 20+ mm/frame). - /// Position must not be tested through the matrix — the view-projection's + /// Position must not be tested through the matrix — the view-projection's /// translation row scales with world coordinates (~5e4 in AC), where a - /// relative tolerance would mask sub-meter motion. Rows 1–3 of - /// view × projection are position-independent (rotation × projection), so a - /// relative 1e-5 there dirties at ≈0.001° of rotation and on any + /// relative tolerance would mask sub-meter motion. Rows 1–3 of + /// view × projection are position-independent (rotation × projection), so a + /// relative 1e-5 there dirties at ≈0.001° of rotation and on any /// projection (FOV/aspect/near/far) change. /// internal static bool CameraApproximatelyEqual( @@ -926,7 +926,7 @@ internal sealed unsafe class EnvCellRenderer : // Verbatim port of WB EnvCellRenderManager.cs:395-511. // Deviations from WB (all documented): // - Drop the _useModernRendering branch (our codebase asserts modern at startup per Phase N.5). - // - Drop SelectedInstance/HoveredInstance highlight block (lines 486-510) — no editor state. + // - Drop SelectedInstance/HoveredInstance highlight block (lines 486-510) — no editor state. // - Replace RenderModernMDI(base) with private RenderModernMDIInternal. // - shader.Bind() / SetUniform API: mapped to acdream's legacy Shader // class (Use() + SetInt/SetVec4/SetMatrix4) to match the existing @@ -947,7 +947,7 @@ internal sealed unsafe class EnvCellRenderer : /// filter (the drawable visible cells from the PView traversal; each cell's /// shell instances are clip-gated to its CellClip slot by the caller's /// binding=3 map). NOTE: this is NOT the old two-pipe RenderInsideOut approach - /// — that flat camera-inside-building stencil pass was deleted in Phase U.1. + /// — that flat camera-inside-building stencil pass was deleted in Phase U.1. /// Source: WB EnvCellRenderManager.cs:399-511 (verbatim minus selection highlights). /// public void Render(WbRenderPass renderPass, HashSet? filter) @@ -979,7 +979,7 @@ internal sealed unsafe class EnvCellRenderer : // WB EnvCellRenderManager.cs:403-404: _shader.Use(); // FIX 2026-05-28 (pool aliasing root cause): mirror WB - // EnvCellRenderManager.cs:405 — restore the pool cursor to the + // EnvCellRenderManager.cs:405 — restore the pool cursor to the // high-water mark Prepare's merge phase reached, so any // GetPooledList calls below return lists past the snapshot's // owned region. Original code used `snapshot.BatchedByCell.Count` @@ -999,7 +999,7 @@ internal sealed unsafe class EnvCellRenderer : // RenderInsideOutAcdream stencil pipeline) change the actual GL // state without updating these caches. The cache then lies, and // the per-batch SetCullMode in RenderModernMDIInternal skips its - // glCullFace call — leaving stale cull state from the prior + // glCullFace call — leaving stale cull state from the prior // consumer. For a cottage with mixed CullMode batches, half the // walls end up culled and the user sees "missing walls". // @@ -1012,15 +1012,15 @@ internal sealed unsafe class EnvCellRenderer : _shader.SetInt("uRenderPass", (int)renderPass); _shader.SetInt("uFilterByCell", 0); _shader.SetInt("uLightingMode", 1); // A7 Fix D D-3/D-4: EnvCell bake (wrap points, no sun) - // #176 stripe-hunt isolation (ACDREAM_LIGHT_DEBUG) — throwaway diagnostic. + // #176 stripe-hunt isolation (ACDREAM_LIGHT_DEBUG) — throwaway diagnostic. _shader.SetInt("uLightDebug", AcDream.Core.Rendering.RenderingDiagnostics.LightDebugMode); // Phase U.4 ROOT-CAUSE FIX (cell-shell flicker / "transparent walls when // moving"): upload uViewProjection HERE rather than inheriting it from // WbDrawDispatcher. The opaque shell pass runs BEFORE the dispatcher's // Draw (GameWindow ~7411 vs ~7418, the only other setter), so without - // this the opaque shells used the PREVIOUS frame's matrix — a stale - // gl_Position against this frame's clip planes → pose-dependent clipping, + // this the opaque shells used the PREVIOUS frame's matrix — a stale + // gl_Position against this frame's clip planes → pose-dependent clipping, // worst while moving. Same self-contained-GL-state precedent as the // 2026-05-28 cull-state cache fix above. _shader.SetMatrix4("uViewProjection", _lastViewProjection); @@ -1059,7 +1059,7 @@ internal sealed unsafe class EnvCellRenderer : else if (filter is null) { RebuildUnfilteredGroups(snapshot); - // WB EnvCellRenderManager.cs:418-429: optimized path — global groups. + // WB EnvCellRenderManager.cs:418-429: optimized path — global groups. foreach (var gfxObjId in _activeSnapshotGlobalGfxObjIds) { if (_activeSnapshotGlobalGroups.TryGetValue(gfxObjId, out var transforms)) @@ -1144,7 +1144,7 @@ internal sealed unsafe class EnvCellRenderer : renderPass); } - // WB EnvCellRenderManager.cs:486-510: selection/hover highlights — DROPPED (no editor state). + // WB EnvCellRenderManager.cs:486-510: selection/hover highlights — DROPPED (no editor state). // WB EnvCellRenderManager.cs:506-509: cleanup. _shader.SetVec4("uHighlightColor", new System.Numerics.Vector4(0, 0, 0, 0)); @@ -1170,12 +1170,12 @@ internal sealed unsafe class EnvCellRenderer : ? dc.renderData.Batches[0].IndexCount / 3 : 0) * dc.count; - // Issue #78 (2026-05-31) [shell] probe (ACDREAM_PROBE_SHELL) — THROWAWAY. + // Issue #78 (2026-05-31) [shell] probe (ACDREAM_PROBE_SHELL) — THROWAWAY. // Per opaque-pass call: totals + per visible (filtered) cell whether it is // present in the prepared snapshot, and its geometry/flags. Answers why the - // interior walls/ceiling don't appear: NOSNAP / gfx=0 ⇒ no shell geometry - // prepared for the cell; idx>0 + zh>0 ⇒ prepared but missing bindless texture - // (invisible); idx>0 + zh=0 + tr=0 ⇒ opaque geometry drawn (fault is depth/ + // interior walls/ceiling don't appear: NOSNAP / gfx=0 ⇒ no shell geometry + // prepared for the cell; idx>0 + zh>0 ⇒ prepared but missing bindless texture + // (invisible); idx>0 + zh=0 + tr=0 ⇒ opaque geometry drawn (fault is depth/ // occlusion or the geometry isn't the wall). Opaque pass only (halves noise). if (renderPass == WbRenderPass.Opaque && AcDream.Core.Rendering.RenderingDiagnostics.ProbeShellEnabled) @@ -1217,7 +1217,7 @@ internal sealed unsafe class EnvCellRenderer : /// /// True if the cell's prepared snapshot has any transparent render batch. /// The pview shell pass uses this to skip the (heavy per-frame) transparent - /// call for opaque-only cells — most cell geometry is + /// call for opaque-only cells — most cell geometry is /// opaque walls/floors/ceilings, so this removes the bulk of the per-cell /// transparent draws. Read-only; mirrors the [shell] probe's batch scan. /// @@ -1227,7 +1227,7 @@ internal sealed unsafe class EnvCellRenderer : // --------------------------------------------------------------------------- // GetCellLightSet (A7 Fix D D-2 helper) // Per-cell up-to-8 point lights, cached per frame. Camera-independent, like - // WbDrawDispatcher.ComputeEntityLightSet — keyed on the cell's world bounds. + // WbDrawDispatcher.ComputeEntityLightSet — keyed on the cell's world bounds. // --------------------------------------------------------------------------- // A7 Fix D (D-2): the up-to-8 point lights reaching a cell, by the cell's world @@ -1248,21 +1248,21 @@ internal sealed unsafe class EnvCellRenderer : var snap = _pointSnapshot; // Landblocks are keyed by the streaming landblock id 0xXXYYFFFF - // (GameWindow: (x<<24)|(y<<16)|0xFFFF), NOT 0xXXYY0000 — so the landblock + // (GameWindow: (x<<24)|(y<<16)|0xFFFF), NOT 0xXXYY0000 — so the landblock // key is (cellId & 0xFFFF0000) | 0xFFFF. The old `cellId & 0xFFFF0000` key // (0xXXYY0000) NEVER matched a registered landblock, so this lookup always // missed: SelectForObject never ran and every EnvCell wall received ZERO // point lights (the entire "indoor torches/lanterns don't light the room" - // bug — confirmed by the [cell-light] probe: inBounds=False for every cell). + // bug — confirmed by the [cell-light] probe: inBounds=False for every cell). if (snap is { Count: > 0 } && _landblocks.TryGetValue((cellId & 0xFFFF0000u) | 0xFFFFu, out var lb) && lb.EnvCellBounds.TryGetValue(cellId, out var b)) { Vector3 center = (b.Min + b.Max) * 0.5f; float radius = (b.Max - b.Min).Length() * 0.5f; - // #176 flap fix: cells use SelectForCell (retail minimize_envcell_lighting) — ALL + // #176 flap fix: cells use SelectForCell (retail minimize_envcell_lighting) — ALL // dynamic lights on every cell (stable), not the per-object sphere-overlap cull that - // let the portal set flip as the flood shifted → floor-lighting flap. + // let the portal set flip as the flood shifted → floor-lighting flap. AcDream.Core.Lighting.LightManager.SelectForCell(snap, center, radius, set); } cached.FrameGeneration = _lightFrameGeneration; @@ -1414,9 +1414,9 @@ internal sealed unsafe class EnvCellRenderer : int passIdx = (int)renderPass; if (passIdx < 0 || passIdx > 2) return; - // §4 outdoor full-world flap (2026-06-10): hoisted from below the SSBO uploads. + // §4 outdoor full-world flap (2026-06-10): hoisted from below the SSBO uploads. // Without the global VAO nothing can draw, and returning AFTER the pass state - // was established leaked it (same early-out shape as the totalDraws==0 leak — + // was established leaked it (same early-out shape as the totalDraws==0 leak — // see the comment on the state-establish block below). var globalVao = _meshManager.GlobalBuffer?.VAO ?? 0u; if (globalVao == 0) return; @@ -1468,14 +1468,14 @@ internal sealed unsafe class EnvCellRenderer : // transparent). Restored to opaque defaults at the end of the draw loop so a // Transparent pass can't leak into later draws. // - // §4 outdoor full-world flap fix (2026-06-10): this block MOVED below the + // §4 outdoor full-world flap fix (2026-06-10): this block MOVED below the // totalDraws==0 early-out above. It used to run before the batch grouping, so a // Transparent pass over a cell whose batches are ALL opaque (a plain cottage // interior) set Blend-on/DepthMask-off and then returned at the count check // WITHOUT reaching the restore. The frame ended with dmask=0; the NEXT frame's // glClear(DEPTH) silently no-oped (depth clears honor glDepthMask), every world // fragment failed GL_LESS against its own previous-frame depth ghost, and the - // whole screen dropped to the fog-tinted clear color — onset-locked to the + // whole screen dropped to the fog-tinted clear color — onset-locked to the // building-flood merge (the first frame a flooded building shell draws), holding // until camera rotation dropped the cell from the flood. From here down every // path reaches the end-of-pass restore. @@ -1689,14 +1689,14 @@ internal sealed unsafe class EnvCellRenderer : // Phase U.4: upload the per-instance clip-slot buffer (binding=3). When // _cellIdToSlot is set (indoor routing), each cell shell instance is gated // to its cell's CellClip slot via allInstances[i].CellId; cells absent from - // the map (shouldn't happen — the Render filter is the map's keys) and the + // the map (shouldn't happen — the Render filter is the map's keys) and the // U.3 path both map to slot 0 (no-clip). allInstances is laid out in the // SAME order as the binding=0 transforms (_gpuInstanceTransforms below), so // instanceClipSlot[i] tracks Instances[i] through the MDI BaseInstance. if (_clipSlotData.Length < uniqueInstanceCount) _clipSlotData = new uint[Math.Max(_clipSlotData.Length * 2, uniqueInstanceCount)]; // #176 stripe-hunt isolation (ACDREAM_CLIP_DEBUG=1): force every shell - // instance to slot 0 (no-clip) — retail draws cell shells WHOLE. + // instance to slot 0 (no-clip) — retail draws cell shells WHOLE. if (_cellIdToSlot is null || AcDream.Core.Rendering.RenderingDiagnostics.ClipDebugNoShellTrim) { @@ -1728,7 +1728,7 @@ internal sealed unsafe class EnvCellRenderer : // #176 seam-draw probe: emitted HERE (not in Render) so the per-cell light // sets read through the just-cleared cache against THIS frame's - // _pointSnapshot — the exact data the SSBO upload below carries. + // _pointSnapshot — the exact data the SSBO upload below carries. if (renderPass == WbRenderPass.Opaque && AcDream.Core.Rendering.RenderingDiagnostics.ProbeSeamDrawEnabled) EmitSeamDrawProbe(drawCalls, allInstances, _seamProbeFilter); @@ -1766,7 +1766,7 @@ internal sealed unsafe class EnvCellRenderer : PersistActiveDynamicBufferCapacities(); // WB BaseObjectRenderManager.cs:807-818: bind VAO + SSBOs + barrier. - // (globalVao validated at the top of the method — a return here would leak the + // (globalVao validated at the top of the method — a return here would leak the // pass state established above.) if (_currentVao != globalVao) { @@ -1865,16 +1865,16 @@ internal sealed unsafe class EnvCellRenderer : } // --------------------------------------------------------------------------- - // #176 seam-draw probe (ACDREAM_PROBE_SEAMDRAW) — throwaway apparatus. + // #176 seam-draw probe (ACDREAM_PROBE_SEAMDRAW) — throwaway apparatus. // The in-engine replacement for the RenderDoc pixel-history the pipeline - // can't have (RenderDoc hides GL_ARB_bindless_texture → our mandatory-modern - // startup gate throws). Per opaque pass: for each target cell — flood + // can't have (RenderDoc hides GL_ARB_bindless_texture → our mandatory-modern + // startup gate throws). Per opaque pass: for each target cell — flood // membership, every shell instance (count + translation, F3 z shows the - // +0.02 lift; n≥2 for one (cell,gfx) = the runtime double-draw), and the + // +0.02 lift; n≥2 for one (cell,gfx) = the runtime double-draw), and the // cell's 8-light set resolved to stable IDENTITIES (owner-cell low16 + // intensity; raw indices shuffle when the pool rebuilds). Plus the - // snapshot's HOT lights (intensity ≥ 50 — the portal purples; fixtures are - // ~1–2). Change-deduped block with a 2 s heartbeat: a purple identity + // snapshot's HOT lights (intensity ≥ 50 — the portal purples; fixtures are + // ~1–2). Change-deduped block with a 2 s heartbeat: a purple identity // flipping with flood membership = the snapshot-scope mechanism; two // coincident instances = the z-fight. See RenderingDiagnostics. // --------------------------------------------------------------------------- @@ -1995,8 +1995,8 @@ internal sealed unsafe class EnvCellRenderer : /// Uploads 's handles to /// when a new one was registered since the last flush, then (re)binds it at /// . - /// A genuinely new handle is rare — new dat surfaces/atlases, not every - /// frame — so this is not part of the ring-buffered per-frame SSBO set; + /// A genuinely new handle is rare — new dat surfaces/atlases, not every + /// frame — so this is not part of the ring-buffered per-frame SSBO set; /// see GlBindlessHandleTable's doc comment. /// private void FlushAndBindTextureTable() @@ -2068,7 +2068,7 @@ internal sealed unsafe class EnvCellRenderer : GLEnum.DynamicDraw, "allocating EnvCell fallback clip SSBO"); allocated = true; - // One CellClip slot, all zeros: count 0 ⇒ shader passes every plane. + // One CellClip slot, all zeros: count 0 ⇒ shader passes every plane. Span zero = stackalloc byte[AcDream.App.Rendering.ClipFrame.CellClipStrideBytes]; zero.Clear(); fixed (byte* p = zero) @@ -2103,12 +2103,12 @@ internal sealed unsafe class EnvCellRenderer : private List GetPooledList() { - // Mirrors WB ObjectRenderManagerBase.cs:1221-1233 — the reuse + // Mirrors WB ObjectRenderManagerBase.cs:1221-1233 — the reuse // branch MUST clear the list before returning. PrepareRenderBatches' // merge phase pattern is `gfxDict[k] = list; list.AddRange(...)`, // which assumes the list is empty. Without the clear, lists grow // unbounded across frames and each frame's draw includes all prior - // frames' stale data. Original port omitted the Clear() call — root + // frames' stale data. Original port omitted the Clear() call — root // cause of post-Wave-5 visual chaos (FIX 2026-05-28). See // docs/research/2026-05-28-a8-env-cell-renderer-audit-findings.md. lock (_listPool) diff --git a/src/AcDream.App/Rendering/Wb/EnvCellSceneryInstance.cs b/src/AcDream.App/Rendering/Wb/EnvCellSceneryInstance.cs index aec47b79..55f0b34f 100644 --- a/src/AcDream.App/Rendering/Wb/EnvCellSceneryInstance.cs +++ b/src/AcDream.App/Rendering/Wb/EnvCellSceneryInstance.cs @@ -1,4 +1,4 @@ -// Ported from references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/SceneryInstance.cs +// Ported from references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/SceneryInstance.cs // Phase A8 extraction (2026-05-28). Verbatim port; adaptations: // - SceneryInstance -> EnvCellSceneryInstance (scope-narrow to env-cell rendering) // - ObjectLandblock -> EnvCellLandblock @@ -17,7 +17,7 @@ namespace AcDream.App.Rendering.Wb; /// Lightweight data for a single placed env-cell scenery object. /// Source: references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/SceneryInstance.cs (lines 11-56) /// -internal struct EnvCellSceneryInstance +public struct EnvCellSceneryInstance { /// GfxObj or Setup ID from DAT. public ulong ObjectId; @@ -67,7 +67,7 @@ internal struct EnvCellSceneryInstance /// Shared by both scenery and static object render managers. /// Source: references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/SceneryInstance.cs (lines 62-160) /// -internal class EnvCellLandblock +public class EnvCellLandblock { /// Grid X coordinate of this landblock. public int GridX { get; set; } diff --git a/src/AcDream.App/Rendering/Wb/EnvCellVisibilitySnapshot.cs b/src/AcDream.App/Rendering/Wb/EnvCellVisibilitySnapshot.cs index e3eeeaa1..77120b39 100644 --- a/src/AcDream.App/Rendering/Wb/EnvCellVisibilitySnapshot.cs +++ b/src/AcDream.App/Rendering/Wb/EnvCellVisibilitySnapshot.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace AcDream.App.Rendering.Wb; @@ -9,14 +9,14 @@ namespace AcDream.App.Rendering.Wb; /// narrowed to the fields actually consumes /// (BatchedByCell + VisibleLandblocks + PostPreparePoolIndex). /// The scenery-side VisibleGroups / VisibleGfxObjIds / -/// IntersectingLandblocks are dropped — we render scenery through +/// IntersectingLandblocks are dropped — we render scenery through /// , not through this snapshot. /// /// Used as an immutable snapshot atomically swapped under the /// renderer's render lock so PrepareRenderBatches (worker-driven) and /// Render (render-thread-driven) can't race on a half-populated dict. /// -internal sealed class EnvCellVisibilitySnapshot +public sealed class EnvCellVisibilitySnapshot { /// Landblocks fully or partially inside the frustum at prepare time. public List VisibleLandblocks { get; init; } = new(); @@ -38,7 +38,7 @@ internal sealed class EnvCellVisibilitySnapshot /// cursor to a safe region past the snapshot's owned lists, so any /// GetPooledList calls inside Render don't trample data the /// snapshot still references. Dropping this field caused the post-Wave-5 - /// visual chaos — see + /// visual chaos — see /// docs/research/2026-05-28-a8-env-cell-renderer-audit-findings.md. /// public int PostPreparePoolIndex { get; init; } diff --git a/src/AcDream.App/Rendering/Wb/GLHelpers.cs b/src/AcDream.App/Rendering/Wb/GLHelpers.cs index 7b85e242..8e0640d6 100644 --- a/src/AcDream.App/Rendering/Wb/GLHelpers.cs +++ b/src/AcDream.App/Rendering/Wb/GLHelpers.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Silk.NET.Core.Native; using Silk.NET.OpenGL; using System; @@ -6,7 +6,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace AcDream.App.Rendering.Wb { - internal static class GLHelpers { + public static class GLHelpers { public static OpenGLGraphicsDevice? Device { get; set; } public static ILogger? Logger { get; set; } diff --git a/src/AcDream.App/Rendering/Wb/GLSLShader.cs b/src/AcDream.App/Rendering/Wb/GLSLShader.cs index fa610dd7..f4eabecf 100644 --- a/src/AcDream.App/Rendering/Wb/GLSLShader.cs +++ b/src/AcDream.App/Rendering/Wb/GLSLShader.cs @@ -1,4 +1,4 @@ -using Chorizite.Core.Render; +using Chorizite.Core.Render; using AcDream.App.Rendering; using Microsoft.Extensions.Logging; using Silk.NET.OpenGL; @@ -14,7 +14,7 @@ using System.Xml.Linq; namespace AcDream.App.Rendering.Wb { - internal unsafe class GLSLShader : BaseShader, IDisposable { + public unsafe class GLSLShader : BaseShader, IDisposable { private OpenGLGraphicsDevice _device; private Dictionary _uniformLocations = []; private Dictionary _uniformValues = []; diff --git a/src/AcDream.App/Rendering/Wb/GeometryUtils.cs b/src/AcDream.App/Rendering/Wb/GeometryUtils.cs index 09369907..5b5ea099 100644 --- a/src/AcDream.App/Rendering/Wb/GeometryUtils.cs +++ b/src/AcDream.App/Rendering/Wb/GeometryUtils.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; // Phase O-T7: verbatim copy of WorldBuilder.Shared.Lib.GeometryUtils into // the AcDream.App.Rendering.Wb namespace so the WorldBuilder.Shared project @@ -8,7 +8,7 @@ namespace AcDream.App.Rendering.Wb; -internal static class GeometryUtils { +public static class GeometryUtils { public static bool RayIntersectsBox(Vector3 rayOrigin, Vector3 rayDirection, Vector3 min, Vector3 max, out float distance) { distance = 0; diff --git a/src/AcDream.App/Rendering/Wb/GlobalMeshBuffer.cs b/src/AcDream.App/Rendering/Wb/GlobalMeshBuffer.cs index 0510761c..ba074462 100644 --- a/src/AcDream.App/Rendering/Wb/GlobalMeshBuffer.cs +++ b/src/AcDream.App/Rendering/Wb/GlobalMeshBuffer.cs @@ -1,4 +1,4 @@ -using AcDream.Content; +using AcDream.Content; using Chorizite.Core.Render.Enums; using Silk.NET.OpenGL; using AcDream.App.Rendering; @@ -73,7 +73,7 @@ internal enum GlobalMeshCapacityResult /// ObjectMeshManager owns allocation lifetime and releases a mesh's ranges /// when its zero-reference LRU entry is evicted. /// -internal sealed class GlobalMeshBuffer : IDisposable +public sealed class GlobalMeshBuffer : IDisposable { internal const int InitialVertexCapacity = 1024 * 1024; internal const int InitialIndexCapacity = 3 * 1024 * 1024; diff --git a/src/AcDream.App/Rendering/Wb/GpuMemoryTracker.cs b/src/AcDream.App/Rendering/Wb/GpuMemoryTracker.cs index 8151e0c9..2b4862c0 100644 --- a/src/AcDream.App/Rendering/Wb/GpuMemoryTracker.cs +++ b/src/AcDream.App/Rendering/Wb/GpuMemoryTracker.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -8,7 +8,7 @@ namespace AcDream.App.Rendering.Wb { /// /// Resource types for GPU memory tracking. /// - internal enum GpuResourceType { + public enum GpuResourceType { Texture, Buffer, Shader, @@ -21,17 +21,17 @@ namespace AcDream.App.Rendering.Wb { /// /// Details about a GPU resource type. /// - internal record GpuResourceDetails(GpuResourceType Type, int Count, long Bytes); + public record GpuResourceDetails(GpuResourceType Type, int Count, long Bytes); /// /// Details about a specific named buffer. /// - internal record NamedBufferDetails(string Name, long CapacityBytes, long UsedBytes); + public record NamedBufferDetails(string Name, long CapacityBytes, long UsedBytes); /// /// Tracks manual VRAM allocations for buffers and textures. /// - internal static class GpuMemoryTracker { + public static class GpuMemoryTracker { private static long _allocatedBytes; private static readonly long[] _allocatedBytesByType = new long[Enum.GetValues().Length]; private static readonly int[] _resourceCountsByType = new int[Enum.GetValues().Length]; diff --git a/src/AcDream.App/Rendering/Wb/IEntityTextureLifetime.cs b/src/AcDream.App/Rendering/Wb/IEntityTextureLifetime.cs index 8faf54d9..dc6c3745 100644 --- a/src/AcDream.App/Rendering/Wb/IEntityTextureLifetime.cs +++ b/src/AcDream.App/Rendering/Wb/IEntityTextureLifetime.cs @@ -1,11 +1,11 @@ -namespace AcDream.App.Rendering.Wb; +namespace AcDream.App.Rendering.Wb; /// /// Logical-owner lifetime seam for per-entity texture composites. Retail /// CSurface::Destroy (0x005361F0) releases its current ImgTex; /// live-object teardown must do the same for modern bindless composites. /// -internal interface IEntityTextureLifetime +public interface IEntityTextureLifetime { /// Release every composite acquired by one local entity id. void ReleaseOwner(uint localEntityId); diff --git a/src/AcDream.App/Rendering/Wb/IWbMeshAdapter.cs b/src/AcDream.App/Rendering/Wb/IWbMeshAdapter.cs index f20a7e33..3ade216b 100644 --- a/src/AcDream.App/Rendering/Wb/IWbMeshAdapter.cs +++ b/src/AcDream.App/Rendering/Wb/IWbMeshAdapter.cs @@ -1,4 +1,4 @@ -namespace AcDream.App.Rendering.Wb; +namespace AcDream.App.Rendering.Wb; /// /// Reports the physical outcome when a mesh-reference callback cannot provide @@ -6,7 +6,7 @@ /// a transactional owner reconcile its marker without guessing whether a /// throwing backend already changed the reference count. /// -internal sealed class MeshReferenceMutationException : Exception +public sealed class MeshReferenceMutationException : Exception { public MeshReferenceMutationException( string message, @@ -25,7 +25,7 @@ internal sealed class MeshReferenceMutationException : Exception /// drive ref-count lifecycle (e.g. LandblockSpawnAdapter, EntitySpawnAdapter) /// can be unit-tested without a real WB pipeline behind them. /// -internal interface IWbMeshAdapter +public interface IWbMeshAdapter { /// /// Acquires one logical reference. A normal exception guarantees that no diff --git a/src/AcDream.App/Rendering/Wb/InstanceData.cs b/src/AcDream.App/Rendering/Wb/InstanceData.cs index 993403bf..bf911ecd 100644 --- a/src/AcDream.App/Rendering/Wb/InstanceData.cs +++ b/src/AcDream.App/Rendering/Wb/InstanceData.cs @@ -1,9 +1,9 @@ -using System.Numerics; +using System.Numerics; using System.Runtime.InteropServices; namespace AcDream.App.Rendering.Wb { [StructLayout(LayoutKind.Sequential, Pack = 16)] - internal struct InstanceData { + public struct InstanceData { public const uint INSTANCE_FLAG_DISQUALIFIED = 1u; public Matrix4x4 Transform; // 64 bytes diff --git a/src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs b/src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs index 242824c3..2e5949a4 100644 --- a/src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs +++ b/src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using AcDream.Core.World; namespace AcDream.App.Rendering.Wb; @@ -8,9 +8,9 @@ namespace AcDream.App.Rendering.Wb; /// reference-count lifecycle. Tier-aware by design: only atlas-tier /// entities (procedural / dat-hydrated, identified by /// ServerGuid == 0) drive ref counts. Server-spawned entities -/// (per-instance tier) are skipped — those go through +/// (per-instance tier) are skipped — those go through /// EntitySpawnAdapter and the owner-scoped texture path -/// (see Phase N.4 spec, Architecture → Two-tier rendering split). +/// (see Phase N.4 spec, Architecture → Two-tier rendering split). /// /// /// On load: walks the landblock's atlas-tier entities, collects unique @@ -44,7 +44,7 @@ namespace AcDream.App.Rendering.Wb; /// on the owning render/update thread. /// /// -internal sealed class LandblockSpawnAdapter +public sealed class LandblockSpawnAdapter { private sealed class ReferenceRegistration { diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLFrameBuffer.cs b/src/AcDream.App/Rendering/Wb/ManagedGLFrameBuffer.cs index 107dcab3..7a3b190a 100644 --- a/src/AcDream.App/Rendering/Wb/ManagedGLFrameBuffer.cs +++ b/src/AcDream.App/Rendering/Wb/ManagedGLFrameBuffer.cs @@ -1,4 +1,4 @@ -using Chorizite.Core.Render; +using Chorizite.Core.Render; using Silk.NET.OpenGL; using System; using System.Collections.Generic; @@ -10,7 +10,7 @@ namespace AcDream.App.Rendering.Wb { /// /// Implementation of a framebuffer for OpenGL ES 3.0 using Silk.NET. /// - internal class ManagedGLFramebuffer : IFramebuffer { + public class ManagedGLFramebuffer : IFramebuffer { private readonly OpenGLGraphicsDevice _device; private GL _gl => _device.GL; private readonly uint _fboId; diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs b/src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs index 95a6aae3..7443fc58 100644 --- a/src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs +++ b/src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs @@ -1,4 +1,4 @@ -using Chorizite.Core.Render.Enums; +using Chorizite.Core.Render.Enums; using Chorizite.Core.Render.Vertex; using Silk.NET.OpenGL; using BufferUsage = Chorizite.Core.Render.Enums.BufferUsage; @@ -7,7 +7,7 @@ namespace AcDream.App.Rendering.Wb { /// /// OpenGL index buffer /// - internal unsafe class ManagedGLIndexBuffer : IIndexBuffer { + public unsafe class ManagedGLIndexBuffer : IIndexBuffer { private uint bufferId; private readonly OpenGLGraphicsDevice _device; private void* _mappedPtr; diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs b/src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs index a3ff538a..31e252e4 100644 --- a/src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs +++ b/src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs @@ -1,9 +1,9 @@ -using Chorizite.Core.Render; +using Chorizite.Core.Render; using Chorizite.Core.Render.Enums; using Silk.NET.OpenGL; namespace AcDream.App.Rendering.Wb { - internal unsafe class ManagedGLTexture : ITexture { + public unsafe class ManagedGLTexture : ITexture { private uint _texture; private readonly OpenGLGraphicsDevice _device; diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs b/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs index 1520645a..1115cede 100644 --- a/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs +++ b/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs @@ -1,7 +1,7 @@ -using AcDream.Core.Rendering.Wb; +using AcDream.Core.Rendering.Wb; using Chorizite.Core.Render; using Chorizite.Core.Render.Enums; -// Use our extracted TextureHelpers (T3), not the WB original — disambiguate explicitly +// Use our extracted TextureHelpers (T3), not the WB original — disambiguate explicitly using TextureHelpers = AcDream.Core.Rendering.Wb.TextureHelpers; using Microsoft.Extensions.Logging; using Silk.NET.OpenGL; @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; using AcDream.App.Rendering; namespace AcDream.App.Rendering.Wb { - internal class ManagedGLTextureArray : ITextureArray { + public class ManagedGLTextureArray : ITextureArray { private readonly bool[] _usedLayers; private readonly GL GL; private readonly OpenGLGraphicsDevice _device; @@ -46,7 +46,7 @@ namespace AcDream.App.Rendering.Wb { /// #105 diagnostic: staged layer updates (retained decoded payloads) not yet /// applied to the GL texture by . Layers with /// a pending update sample UNDEFINED content (TexStorage3D contents) until the - /// flush runs — a stuck non-zero count at standstill is the white-walls mechanism. + /// flush runs — a stuck non-zero count at standstill is the white-walls mechanism. /// public int PendingUpdateCount { get { lock (_mipmapLock) { return _pendingUpdates.Count; } } diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLUniformBuffer.cs b/src/AcDream.App/Rendering/Wb/ManagedGLUniformBuffer.cs index 4ab11f4c..ebcc56d2 100644 --- a/src/AcDream.App/Rendering/Wb/ManagedGLUniformBuffer.cs +++ b/src/AcDream.App/Rendering/Wb/ManagedGLUniformBuffer.cs @@ -1,4 +1,4 @@ -using Chorizite.Core.Render; +using Chorizite.Core.Render; using Chorizite.Core.Render.Enums; using Silk.NET.OpenGL; using System.Runtime.InteropServices; @@ -10,7 +10,7 @@ namespace AcDream.App.Rendering.Wb { /// /// OpenGL uniform buffer /// - internal unsafe class ManagedGLUniformBuffer : IUniformBuffer { + public unsafe class ManagedGLUniformBuffer : IUniformBuffer { private uint bufferId; private readonly OpenGLGraphicsDevice _device; private GL GL => _device.GL; diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLVertexArray.cs b/src/AcDream.App/Rendering/Wb/ManagedGLVertexArray.cs index 27abfc4d..583e82d9 100644 --- a/src/AcDream.App/Rendering/Wb/ManagedGLVertexArray.cs +++ b/src/AcDream.App/Rendering/Wb/ManagedGLVertexArray.cs @@ -1,4 +1,4 @@ -using Chorizite.Core.Render.Enums; +using Chorizite.Core.Render.Enums; using Chorizite.Core.Render.Vertex; using Silk.NET.OpenGL; using System; @@ -9,7 +9,7 @@ using System.Threading.Tasks; using VertexAttribType = Silk.NET.OpenGL.VertexAttribType; namespace AcDream.App.Rendering.Wb { - internal unsafe class ManagedGLVertexArray : IVertexArray { + public unsafe class ManagedGLVertexArray : IVertexArray { private readonly OpenGLGraphicsDevice _device; private GL GL => _device.GL; private uint _vaoId = 0; diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLVertexBuffer.cs b/src/AcDream.App/Rendering/Wb/ManagedGLVertexBuffer.cs index 92b09777..d9a3752a 100644 --- a/src/AcDream.App/Rendering/Wb/ManagedGLVertexBuffer.cs +++ b/src/AcDream.App/Rendering/Wb/ManagedGLVertexBuffer.cs @@ -1,4 +1,4 @@ -using Chorizite.Core.Render.Enums; +using Chorizite.Core.Render.Enums; using Chorizite.Core.Render.Vertex; using Microsoft.Extensions.Logging; using Silk.NET.OpenGL; @@ -10,7 +10,7 @@ namespace AcDream.App.Rendering.Wb { /// /// OpenGL vertex buffer /// - internal unsafe class ManagedGLVertexBuffer : IVertexBuffer { + public unsafe class ManagedGLVertexBuffer : IVertexBuffer { private uint bufferId; private readonly OpenGLGraphicsDevice _device; private void* _mappedPtr; diff --git a/src/AcDream.App/Rendering/Wb/ModernRenderData.cs b/src/AcDream.App/Rendering/Wb/ModernRenderData.cs index 4d8192aa..a3b497eb 100644 --- a/src/AcDream.App/Rendering/Wb/ModernRenderData.cs +++ b/src/AcDream.App/Rendering/Wb/ModernRenderData.cs @@ -1,4 +1,4 @@ -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; using DatReaderWriter.Enums; using Chorizite.Core.Render; @@ -9,17 +9,17 @@ namespace AcDream.App.Rendering.Wb { /// bindless handle) and the layer index within the shared/pooled array. /// Indexed by gl_DrawIDARB in the vertex shader. Same 16-byte std430 shape /// as mesh_modern.vert's BatchData: TextureTableIndex/Reserved/TextureIndex/ - /// Flags at offsets 0/4/8/12 — see ModernBatchDataLayoutTests. + /// Flags at offsets 0/4/8/12 — see ModernBatchDataLayoutTests. /// [StructLayout(LayoutKind.Sequential, Pack = 4)] - internal struct ModernBatchData { - public uint TextureTableIndex; // 4 bytes — slot into the binding=9 handle table - public uint Reserved; // 4 bytes — pad, keeps TextureIndex/Flags at offsets 8/12 - public uint TextureIndex; // 4 bytes — layer within the texture array - public uint Flags; // 4 bytes — reserved, matches mesh_modern.vert's BatchData.flags + public struct ModernBatchData { + public uint TextureTableIndex; // 4 bytes — slot into the binding=9 handle table + public uint Reserved; // 4 bytes — pad, keeps TextureIndex/Flags at offsets 8/12 + public uint TextureIndex; // 4 bytes — layer within the texture array + public uint Flags; // 4 bytes — reserved, matches mesh_modern.vert's BatchData.flags } - internal struct LandblockMdiCommand { + public struct LandblockMdiCommand { public ulong SortKey; public ulong ObjectId; public DrawElementsIndirectCommand Command; diff --git a/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs b/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs index 6ecc75b9..d3d97c4f 100644 --- a/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs +++ b/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs @@ -1,4 +1,4 @@ -using Chorizite.Core.Lib; +using Chorizite.Core.Lib; using Chorizite.Core.Render; using Chorizite.Core.Render.Enums; using DatReaderWriter.DBObjs; @@ -26,7 +26,7 @@ namespace AcDream.App.Rendering.Wb /// /// GPU-side render data created on the main thread. /// - internal class ObjectRenderData + public class ObjectRenderData { public uint VAO { get; set; } public uint VBO { get; set; } @@ -74,7 +74,7 @@ namespace AcDream.App.Rendering.Wb /// /// A single GPU draw batch: IBO + texture array layer. /// - internal class ObjectRenderBatch + public class ObjectRenderBatch { public uint IBO { get; set; } public int IndexCount { get; set; } @@ -101,7 +101,7 @@ namespace AcDream.App.Rendering.Wb /// Key design: mesh data is prepared on background threads via PrepareMeshData(), /// then GPU resources are created on the main thread via UploadMeshData(). /// - internal class ObjectMeshManager : IDisposable + public class ObjectMeshManager : IDisposable { private readonly OpenGLGraphicsDevice _graphicsDevice; private readonly IPreparedAssetSource _preparedAssets; @@ -171,7 +171,7 @@ namespace AcDream.App.Rendering.Wb private volatile bool _arenaBackpressured; /// #125: how many times a failed GL upload is re-staged before - /// giving up loudly. Small — a transient GL error clears on the next + /// giving up loudly. Small — a transient GL error clears on the next /// frame; anything that fails this many times is a genuine defect to /// surface, not retry forever. See . public const int MaxUploadRetries = 3; @@ -179,8 +179,8 @@ namespace AcDream.App.Rendering.Wb /// /// #125: drain one staged upload, returning whether it should be /// re-staged for a later frame. The caller (the per-frame Tick drain) - /// collects the re-stages and re-enqueues them AFTER the drain loop — - /// never inside it — so a deterministic failure can't spin the queue in + /// collects the re-stages and re-enqueues them AFTER the drain loop — + /// never inside it — so a deterministic failure can't spin the queue in /// a single frame. increments the mesh /// data's own counter only when new upload work actually starts (not /// while a prior rollback waits); this drain gives up loudly past @@ -197,7 +197,7 @@ namespace AcDream.App.Rendering.Wb if (UploadMeshData(meshData) is not null) { _stagedMeshData.Complete(item); - return false; // success (incl. legitimate 0-vertex → empty render data) + return false; // success (incl. legitimate 0-vertex → empty render data) } if (HasRenderData(meshData.ObjectId)) { @@ -215,7 +215,7 @@ namespace AcDream.App.Rendering.Wb if (meshData.UploadAttempts < MaxUploadRetries) return true; // re-stage for next frame _stagedMeshData.Complete(item); - Console.WriteLine($"[up-retry] 0x{meshData.ObjectId:X10} upload failed {meshData.UploadAttempts}x — giving up (was the #125 silent sticky drop; a GL error is being surfaced, not hidden)"); + Console.WriteLine($"[up-retry] 0x{meshData.ObjectId:X10} upload failed {meshData.UploadAttempts}x — giving up (was the #125 silent sticky drop; a GL error is being surfaced, not hidden)"); return false; } @@ -570,7 +570,7 @@ namespace AcDream.App.Rendering.Wb /// /// #105 diagnostic: counts staged-but-unflushed texture layer updates across all /// shared atlases (see ). - /// Render thread only — _globalAtlases is render-thread-owned. + /// Render thread only — _globalAtlases is render-thread-owned. /// public (int PendingUpdates, int ArraysWithPending, int TotalArrays) GetPendingTextureUpdateStats() { @@ -838,7 +838,7 @@ namespace AcDream.App.Rendering.Wb _cpuMeshCache.Clear(); } - internal struct EnvCellGeomRequest + public struct EnvCellGeomRequest { public uint SourceCellId; public uint EnvironmentId; @@ -1520,14 +1520,14 @@ namespace AcDream.App.Rendering.Wb { // 0-vertex mesh: every polygon was gated out at extraction. #119 // (2026-06-11) dat-verified this is LEGITIMATE for all-no-draw - // models (all polys NoPos + Base1Solid surfaces — retail's + // models (all polys NoPos + Base1Solid surfaces — retail's // skipNoTexture never draws them either; 0x010002B4/0x010008A8 // are this class, Issue119UpNullGfxObjDumpTests). The empty // cache is the correct terminal state for those. The line stays // as a tripwire for the OTHER way to get here (extraction - // dropped textured polys — a real defect; dat-verify with the + // dropped textured polys — a real defect; dat-verify with the // dump test before treating as one). - Console.WriteLine($"[up-null] 0x{meshData.ObjectId:X10} produced a 0-vertex mesh — caching empty render data (legitimate for all-no-draw models; dat-verify via Issue119UpNullGfxObjDumpTests)"); + Console.WriteLine($"[up-null] 0x{meshData.ObjectId:X10} produced a 0-vertex mesh — caching empty render data (legitimate for all-no-draw models; dat-verify via Issue119UpNullGfxObjDumpTests)"); renderData = new ObjectRenderData(); } @@ -1611,7 +1611,7 @@ namespace AcDream.App.Rendering.Wb /// /// Plans the actual GL work the next object would trigger against the /// current atlas inventory. This includes array storage, global-buffer - /// growth/copies, and one full mip generation per newly-dirtied array—not merely the + /// growth/copies, and one full mip generation per newly-dirtied array—not merely the /// source byte arrays held by ObjectMeshData. /// internal MeshUploadCost PlanUploadCost( @@ -1852,7 +1852,7 @@ namespace AcDream.App.Rendering.Wb #region Private: Background Preparation /// - /// #113: the set of polygon ids referenced by the GfxObj's drawing BSP — + /// #113: the set of polygon ids referenced by the GfxObj's drawing BSP — /// the polys retail actually renders (D3DPolyRender traverses the BSP; /// dictionary-orphaned polys are physics/no-draw geometry). Returns null /// when the model has no drawing BSP (caller draws everything). @@ -1985,7 +1985,7 @@ namespace AcDream.App.Rendering.Wb } atlasManager.LastUseSequence = ++_atlasUseSequence; - // MP1a: AcDream.Content is Silk.NET-free — the extraction records + // MP1a: AcDream.Content is Silk.NET-free — the extraction records // carry Content-owned UploadPixelFormat/UploadPixelType enums whose // underlying values are the GL ABI constants (numerically identical // to Silk.NET.OpenGL.PixelFormat/PixelType), so this lifted nullable @@ -2591,7 +2591,7 @@ namespace AcDream.App.Rendering.Wb // disposes the DatCollection right after this adapter chain, which // unmaps the dats' memory-mapped views. A worker still inside // MemoryMappedBlockAllocator.ReadBlock at that point dereferences the - // dead view pointer — an uncatchable, process-fatal AccessViolation + // dead view pointer — an uncatchable, process-fatal AccessViolation // (dat-race investigation 2026-06-09). Setting IsDisposed under the // queue lock publishes it to workers, which re-check it before every // dequeue; draining the queue means each worker exits after at most diff --git a/src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs b/src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs index 997c39f4..38bf7e1d 100644 --- a/src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs +++ b/src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs @@ -1,4 +1,4 @@ -using Chorizite.Core.Render; +using Chorizite.Core.Render; using Chorizite.Core.Render.Enums; using Chorizite.Core.Render.Vertex; using AcDream.App.Rendering; @@ -20,7 +20,7 @@ namespace AcDream.App.Rendering.Wb { /// /// OpenGL graphics device /// - internal unsafe class OpenGLGraphicsDevice : BaseGraphicsDevice { + public unsafe class OpenGLGraphicsDevice : BaseGraphicsDevice { private readonly ILogger _log; private readonly DebugRenderSettings _renderSettings; private readonly AcDream.App.Rendering.IGpuResourceRetirementQueue _resourceRetirement; diff --git a/src/AcDream.App/Rendering/Wb/RenderStateCache.cs b/src/AcDream.App/Rendering/Wb/RenderStateCache.cs index fb62cd2c..6453ed7e 100644 --- a/src/AcDream.App/Rendering/Wb/RenderStateCache.cs +++ b/src/AcDream.App/Rendering/Wb/RenderStateCache.cs @@ -1,4 +1,4 @@ -namespace AcDream.App.Rendering.Wb; +namespace AcDream.App.Rendering.Wb; /// /// Tracks currently-bound GL state to skip redundant rebinds across the @@ -7,9 +7,9 @@ /// here in Phase O-T7 to eliminate the WorldBuilder project reference. /// /// Semantics are identical to the WB originals: -/// CurrentAtlas — slot index of the currently bound texture atlas. -/// CurrentVAO — OpenGL name of the currently bound vertex array object. -/// CurrentIBO — OpenGL name of the currently bound index buffer object. +/// CurrentAtlas — slot index of the currently bound texture atlas. +/// CurrentVAO — OpenGL name of the currently bound vertex array object. +/// CurrentIBO — OpenGL name of the currently bound index buffer object. /// Sentinel value 0 means "no valid binding cached." /// /// @@ -18,7 +18,7 @@ /// corresponding glBind* call and read by the next dispatch on the /// same thread. /// -internal static class RenderStateCache +public static class RenderStateCache { public static uint CurrentAtlas = 0; public static uint CurrentVAO = 0; diff --git a/src/AcDream.App/Rendering/Wb/SceneData.cs b/src/AcDream.App/Rendering/Wb/SceneData.cs index 76ba5a19..2923404f 100644 --- a/src/AcDream.App/Rendering/Wb/SceneData.cs +++ b/src/AcDream.App/Rendering/Wb/SceneData.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using System.Runtime.InteropServices; namespace AcDream.App.Rendering.Wb { @@ -6,7 +6,7 @@ namespace AcDream.App.Rendering.Wb { /// Global scene data for Uniform Buffer Object (UBO) /// [StructLayout(LayoutKind.Sequential, Pack = 16)] - internal struct SceneData { + public struct SceneData { public Matrix4x4 View; // 64 bytes public Matrix4x4 Projection; // 64 bytes public Matrix4x4 ViewProjection; // 64 bytes diff --git a/src/AcDream.App/Rendering/Wb/TextureAtlasManager.cs b/src/AcDream.App/Rendering/Wb/TextureAtlasManager.cs index 66bda875..ee9ed87f 100644 --- a/src/AcDream.App/Rendering/Wb/TextureAtlasManager.cs +++ b/src/AcDream.App/Rendering/Wb/TextureAtlasManager.cs @@ -1,4 +1,4 @@ -using AcDream.Content; +using AcDream.Content; using Chorizite.Core.Render; using Chorizite.Core.Render.Enums; using DatReaderWriter.Enums; @@ -42,7 +42,7 @@ namespace AcDream.App.Rendering.Wb { /// Manages texture arrays grouped by (Width, Height, Format). /// Deduplicates textures by a TextureKey and supports reference counting. /// - internal class TextureAtlasManager : IDisposable { + public class TextureAtlasManager : IDisposable { private static uint _nextSlot = 1; private readonly OpenGLGraphicsDevice _graphicsDevice; private readonly int _textureWidth; diff --git a/src/AcDream.App/Rendering/Wb/TextureFormatExtensions.cs b/src/AcDream.App/Rendering/Wb/TextureFormatExtensions.cs index 32d7b008..d3b00bb3 100644 --- a/src/AcDream.App/Rendering/Wb/TextureFormatExtensions.cs +++ b/src/AcDream.App/Rendering/Wb/TextureFormatExtensions.cs @@ -1,9 +1,9 @@ -using Chorizite.Core.Render.Enums; +using Chorizite.Core.Render.Enums; using Silk.NET.OpenGL; using System; namespace AcDream.App.Rendering.Wb { - internal static class TextureFormatExtensions { + public static class TextureFormatExtensions { public static SizedInternalFormat ToGL(this Chorizite.Core.Render.Enums.TextureFormat format) { return format switch { TextureFormat.RGBA8 => SizedInternalFormat.Rgba8, diff --git a/src/AcDream.App/Rendering/Wb/TextureParameters.cs b/src/AcDream.App/Rendering/Wb/TextureParameters.cs index f7c49af4..30a7be14 100644 --- a/src/AcDream.App/Rendering/Wb/TextureParameters.cs +++ b/src/AcDream.App/Rendering/Wb/TextureParameters.cs @@ -1,10 +1,10 @@ -using Silk.NET.OpenGL; +using Silk.NET.OpenGL; namespace AcDream.App.Rendering.Wb { /// /// Configurable OpenGL texture parameters for wrap mode, filtering, mipmaps, and anisotropic filtering. /// - internal struct TextureParameters { + public struct TextureParameters { public TextureWrapMode WrapS; public TextureWrapMode WrapT; public TextureMinFilter MinFilter; @@ -12,7 +12,7 @@ namespace AcDream.App.Rendering.Wb { public bool EnableMipmaps; public bool EnableAnisotropicFiltering; - /// Standard tiling textures — Repeat + trilinear + aniso. + /// Standard tiling textures — Repeat + trilinear + aniso. public static readonly TextureParameters Default = new() { WrapS = TextureWrapMode.Repeat, WrapT = TextureWrapMode.Repeat, @@ -22,7 +22,7 @@ namespace AcDream.App.Rendering.Wb { EnableAnisotropicFiltering = true, }; - /// Non-tiling textures (alpha maps, fonts, UI, object atlases) — ClampToEdge + trilinear + aniso. + /// Non-tiling textures (alpha maps, fonts, UI, object atlases) — ClampToEdge + trilinear + aniso. public static readonly TextureParameters ClampToEdge = new() { WrapS = TextureWrapMode.ClampToEdge, WrapT = TextureWrapMode.ClampToEdge, diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.PackedOracle.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.PackedOracle.cs index 81a2325a..b04df854 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.PackedOracle.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.PackedOracle.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.Core.Rendering; using AcDream.App.Rendering.Scene; using AcDream.App.Rendering.Selection; @@ -16,7 +16,7 @@ namespace AcDream.App.Rendering.Wb; /// routes reuse the dispatcher's exact mesh, texture, light, translucency, /// selection, upload, and draw owners. /// -internal sealed unsafe partial class WbDrawDispatcher +public sealed unsafe partial class WbDrawDispatcher { private readonly Dictionary _packedEntityById = []; diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs index 0484da78..bb82c7e1 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using System.Runtime.CompilerServices; @@ -34,12 +34,12 @@ namespace AcDream.App.Rendering.Wb; /// WB. Native surfaces still reuse the WB atlas; only actual indexed-palette /// and original-texture replacements resolve through owner-scoped /// composites. is currently -/// unused at draw time — GameWindow's spawn path already bakes AnimPartChanges + +/// unused at draw time — GameWindow's spawn path already bakes AnimPartChanges + /// GfxObjDegradeResolver (Issue #47 close-detail mesh) into MeshRefs. /// /// /// -/// GL strategy (N.5 — mandatory): glMultiDrawElementsIndirect with SSBOs +/// GL strategy (N.5 — mandatory): glMultiDrawElementsIndirect with SSBOs /// and GL_ARB_bindless_texture + GL_ARB_shader_draw_parameters. /// All visible (entity, batch) pairs are bucketed by ; /// each group becomes one DrawElementsIndirectCommand. Three GPU buffers @@ -54,7 +54,7 @@ namespace AcDream.App.Rendering.Wb; /// /// Shader: mesh_modern (bindless + gl_DrawIDARB / /// gl_BaseInstanceARB). Missing bindless/draw-parameters throws -/// at startup — there is no legacy fallback. +/// at startup — there is no legacy fallback. /// /// /// @@ -65,7 +65,7 @@ namespace AcDream.App.Rendering.Wb; /// glMultiDrawElementsIndirect. /// /// -internal sealed unsafe partial class WbDrawDispatcher : IDisposable +public sealed unsafe partial class WbDrawDispatcher : IDisposable { /// /// Which subset of entities to walk in a single Draw call. @@ -78,10 +78,10 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// the unified pass can re-introduce partitioning later without re-threading /// the call sites. /// - internal enum EntitySet + public enum EntitySet { /// Every entity walked, gated only by the existing - /// ParentCellId ∈ visibleCellIds filter. + /// ParentCellId ∈ visibleCellIds filter. All, } @@ -100,7 +100,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable private readonly BindlessSupport _bindless; private ICurrentRenderDispatcherObserver? _currentRenderSceneObserver; - internal readonly record struct DrawStats( + public readonly record struct DrawStats( EntitySet Set, int EntitiesWalked, int MeshRefs, @@ -419,16 +419,16 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // Tier 1 cache (#53): per-entity classification results for static // entities (those NOT in GameWindow._animatedEntities). Wired here in - // Task 7 for plumbing only — Tasks 9-10 wire the per-entity + // Task 7 for plumbing only — Tasks 9-10 wire the per-entity // miss-populate / hit-fast-path through the loop. private readonly EntityClassificationCache _cache; - // #188 — per-(entity, Setup-part) translucency ramp state (fading doors / + // #188 — per-(entity, Setup-part) translucency ramp state (fading doors / // secret-passage walls). ClassifyBatches reads this per part to compute // the instance's opacity multiplier; never mutated here. private readonly AcDream.Core.Rendering.TranslucencyFadeManager _translucencyFades; - // ACDREAM_DISABLE_TIER1_CACHE=1 A/B diagnostic — forces every static + // ACDREAM_DISABLE_TIER1_CACHE=1 A/B diagnostic — forces every static // entity through the slow path. Read once in ctor. private readonly bool _tier1CacheDisabled = string.Equals(Environment.GetEnvironmentVariable("ACDREAM_DISABLE_TIER1_CACHE"), "1", StringComparison.Ordinal); @@ -453,7 +453,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // Phase U.3: per-instance clip-slot SSBO (binding=3), parallel to // _instanceSsbo. One uint per instance selecting its CellClip slot. In U.3 - // this is ALL ZEROS (every instance → slot 0 → no-clip), so the render is + // this is ALL ZEROS (every instance → slot 0 → no-clip), so the render is // identical to pre-U.3. U.4 populates real slot indices. private uint _clipSlotSsbo; private int _clipSlotSsboCapacityBytes; @@ -461,7 +461,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // Fix B (A7 #3): per-OBJECT light selection (minimize_object_lighting). Two // SSBOs replace the single global nearest-8-to-CAMERA UBO set for point/spot - // lights — see mesh_modern.vert binding=4/5. _globalLightsSsbo (binding=4) + // lights — see mesh_modern.vert binding=4/5. _globalLightsSsbo (binding=4) // holds the per-frame point-light snapshot (LightManager.PointSnapshot); // _instLightSetSsbo (binding=5) holds MaxLightsPerObject int indices per // instance INTO it (-1 = unused), laid out parallel to _instanceSsbo. @@ -495,10 +495,10 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // Campaign V slice V2 (2026-07-27): GL-only emulation of the eventual // Vulkan global texture descriptor array (binding=9, - // GpuBindingModel.StorageTextureTable). A genuinely new handle is rare — - // new dat surfaces/atlases, not every frame — so this single buffer is + // GpuBindingModel.StorageTextureTable). A genuinely new handle is rare — + // new dat surfaces/atlases, not every frame — so this single buffer is // NOT part of the ring-buffered DynamicBufferSet below; see - // GlBindlessHandleTable's doc comment and the campaign doc's §5.2 for why + // GlBindlessHandleTable's doc comment and the campaign doc's §5.2 for why // this table is owned here rather than by GlGpuDevice. Lazily created. private readonly GlBindlessHandleTable _textureTable = new(); private uint _textureTableSsbo; @@ -537,15 +537,15 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable _dynamicBufferSetsByFrame.Sum(frameSets => frameSets.Count); private Vector2[] _selectionLightingData = new Vector2[256]; // This frame's point-light snapshot, handed in by GameWindow before Draw via - // SetSceneLights. Null/empty ⇒ only ambient + sun render (all instance sets -1). + // SetSceneLights. Null/empty ⇒ only ambient + sun render (all instance sets -1). private IReadOnlyList? _pointSnapshot; - // This entity's selected point/spot light set — computed ONCE per entity at + // This entity's selected point/spot light set — computed ONCE per entity at // the isNewEntity site (constant across the entity's parts/tuples), exactly // like _currentEntitySlot. -1 = unused slot. private readonly int[] _currentEntityLightSetScratch = new int[LightManager.MaxLightsPerObject]; private InstanceLightSet _currentEntityLightSet = InstanceLightSet.Disabled; - // #142: per-entity "indoor" flag — set once per entity in ComputeEntityLightSet, + // #142: per-entity "indoor" flag — set once per entity in ComputeEntityLightSet, // parallel to _currentEntityLightSet. True when IndoorObjectReceivesTorches fires // (ParentCellId is an EnvCell). Appended to InstanceGroup.IndoorFlags in // AppendCurrentLightSet; uploaded as binding=6 instanceIndoor[]. @@ -563,7 +563,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // Phase U.4: per-frame clip-slot routing handed in via SetClipRouting before // each Draw. When _clipRoutingActive is false (the U.3 path / outdoor root / // no portal frame), every instance maps to slot 0 (no-clip) and no instance is - // culled — identical to U.3. When active, each instance's slot is resolved by + // culled — identical to U.3. When active, each instance's slot is resolved by // ResolveEntitySlot per the U.4 policy (cell-owned entities to their cell slot; // outdoor-owned entities to OutsideView; non-visible/unresolved indoors culled). private bool _clipRoutingActive; @@ -583,7 +583,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // across the entity's tuples; the per-tuple body skips all instance emission. private bool _currentEntityCulled; - // Per-frame scratch arrays — Tasks 9-10 fully wire these. + // Per-frame scratch arrays — Tasks 9-10 fully wire these. private float[] _instanceData = new float[256 * 16]; // mat4 floats per instance private BatchData[] _batchData = new BatchData[256]; private DrawElementsIndirectCommand[] _indirectCommands = new DrawElementsIndirectCommand[256]; @@ -600,7 +600,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // Campaign V slice V2 (2026-07-27): std430 layout: uint TextureIndex at // offset 0, uint Reserved (pad) at offset 4, uint TextureLayer at offset 8, - // uint Flags at offset 12. Total 16 bytes — unchanged from before V2, so + // uint Flags at offset 12. Total 16 bytes — unchanged from before V2, so // every existing CPU writer's offsets are unchanged (see // GpuBindingModel.GpuBatchDataStrideBytes). TextureIndex used to be a // 64-bit ulong TextureHandle (an ARB_bindless_texture handle, uvec2 in @@ -611,7 +611,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable private struct BatchData { public uint TextureIndex; // slot into the binding=9 handle table - public uint Reserved; // pad — keeps TextureLayer/Flags at offsets 8/12 + public uint Reserved; // pad — keeps TextureLayer/Flags at offsets 8/12 public uint TextureLayer; public uint Flags; } @@ -674,7 +674,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable => owner.ResetDeferredAlphaSubmissions(); } - // Per-frame scratch — reused across frames to avoid per-frame allocation. + // Per-frame scratch — reused across frames to avoid per-frame allocation. private readonly Dictionary _groups = new(); private readonly List _opaqueDraws = new(); private readonly List _translucentDraws = new(); @@ -704,7 +704,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable * Unsafe.SizeOf()); // A.5 T26 follow-up (Bug B): WalkEntities populates this scratch list // instead of allocating a fresh List<(WorldEntity, int)> per frame. At - // ~10K entities × ~3 mesh refs = ~30K tuples × 16 bytes = ~480 KB / frame + // ~10K entities × ~3 mesh refs = ~30K tuples × 16 bytes = ~480 KB / frame // of GC pressure on the render thread under the original T17 shape. private readonly List<(WorldEntity Entity, int MeshRefIndex, uint LandblockId)> _walkScratch = new(); // G2: the dispatcher consumes this acdream-owned value boundary, never @@ -712,16 +712,16 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // retained list; G4 will replace only that producer with RenderFrameView. private readonly List _candidateTupleScratch = new(); - // Tier 1 cache (#53) — per-entity classification collector. Reused across + // Tier 1 cache (#53) — per-entity classification collector. Reused across // frames; cleared at flush time when the per-entity loop crosses an entity // boundary in _walkScratch (and once more at end-of-loop for the last // entity). _walkScratch is in entity-order, so all MeshRefs of one entity - // are contiguous — accumulate them all before flushing one Populate call. + // are contiguous — accumulate them all before flushing one Populate call. // Animated entities skip this scratch entirely (collector = null). private readonly List _populateScratch = new(); private readonly List _populateSelectionScratch = new(); - // Per-entity-cull AABB radius. Conservative — covers most entities; large + // Per-entity-cull AABB radius. Conservative — covers most entities; large // outliers (long banners, tall columns) are still landblock-culled. private const float PerEntityCullRadius = 5.0f; @@ -763,7 +763,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable private long _lastLogTick; // #128 self-heal: per-Draw dedup of point-of-use load re-requests - // (PrepareMeshDataAsync is idempotent while pending — the dedup just + // (PrepareMeshDataAsync is idempotent while pending — the dedup just // avoids redundant dictionary probes within one pass) + the once-per-id // [mesh-miss] diagnostic set (never cleared; diag-gated emission). private readonly HashSet _missRequested = new(); @@ -772,7 +772,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // #119 decisive probe (2026-06-11): ACDREAM_DUMP_ENTITY one-shot entity // dump. Keyed by entity Id; the stored signature re-emits the header line // whenever (MeshRefs count, cache batch count, zero-translation count, - // culled) changes — e.g. the Tier-1 populate landing one frame after the + // culled) changes — e.g. the Tier-1 populate landing one frame after the // first slow-path draw. The full per-part listing prints only on first // sight. Inert (one Count==0 check per new entity) when the env var is // unset. Render-thread only. @@ -798,12 +798,12 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable private readonly uint[] _gpuQueryOpaque = new uint[GpuQueryRingDepth]; private readonly uint[] _gpuQueryTransparent = new uint[GpuQueryRingDepth]; // #125: a glGenQueries name does not become a QUERY OBJECT until its first - // glBeginQuery — GetQueryObject on a never-begun name is GL_INVALID_OPERATION. + // glBeginQuery — GetQueryObject on a never-begun name is GL_INVALID_OPERATION. // The N.6 ring assumed ONE Draw per frame with both passes always non-empty; // the pview pipeline issues MANY small Draws per frame (landscape slices, // per-cell buckets, dynamics), where zero-draw passes routinely skip // BeginQuery. Under ACDREAM_WB_DIAG=1 the slot read then queued an - // InvalidOperation EVERY frame — silently, until WB's diligent texture-path + // InvalidOperation EVERY frame — silently, until WB's diligent texture-path // glGetError checks ate the stale errors and treated their own successful // uploads as failures ([wb-error] + sticky drop) and ProcessDirtyUpdates' // check threw (process death; tower-wbdiag3.log). Track which slots were @@ -816,7 +816,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable private bool _gpuQueriesInitialized; // Constructor accessibility is internal because EntityClassificationCache - // is internal — a public ctor with an internal-typed parameter would be + // is internal — a public ctor with an internal-typed parameter would be // an inconsistent-accessibility error. The dispatcher is constructed // exclusively from GameWindow (same assembly), so internal is fine. internal WbDrawDispatcher( @@ -898,7 +898,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// (). Call once per frame BEFORE /// . The dispatcher uploads it to binding=4 and selects each /// object's up-to-8 lights from it () - /// by the object's bounding sphere — camera-independent. Pass null/empty to + /// by the object's bounding sphere — camera-independent. Pass null/empty to /// disable per-object point lights (only ambient + sun render). /// public void SetSceneLights(IReadOnlyList? pointSnapshot) @@ -923,11 +923,11 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// Pair with on outdoor-root frames so the /// dispatcher reverts to the U.3 no-clip-everything behavior. /// - /// cellId → CellClip slot. A cell absent from the map - /// is NOT visible → its cell-static instances are culled. + /// cellId → CellClip slot. A cell absent from the map + /// is NOT visible → its cell-static instances are culled. /// Slot for outdoor scenery / building shells while /// indoors (the OutsideView slot, or 0 for no-clip over-include). - /// False ⇒ cull outdoor scenery / shells this frame + /// False ⇒ cull outdoor scenery / shells this frame /// (the OutsideView is empty). public void SetClipRouting(IReadOnlyDictionary cellIdToSlot, int outdoorSlot, bool outdoorVisible) { @@ -939,7 +939,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable } /// - /// Phase U.4: revert to U.3 behavior — every instance maps to slot 0 (no-clip), + /// Phase U.4: revert to U.3 behavior — every instance maps to slot 0 (no-clip), /// nothing is culled by clip routing. Call on outdoor-root frames (camera /// outdoors) and any frame without a portal-visibility result. /// @@ -951,16 +951,16 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable _outdoorVisible = false; } - // §4 flap [clip-route-disp] probe state (2026-06-10, throwaway): print-on-change + // §4 flap [clip-route-disp] probe state (2026-06-10, throwaway): print-on-change // signature + monotonic sequence + reusable histogram. See RenderingDiagnostics // .ProbeClipRouteEnabled for the full probe contract. private string? _lastClipRouteDispSig; private long _clipRouteDispSeq; private readonly SortedDictionary _clipRouteHist = new(); - // §4 flap apparatus (2026-06-10): per-slot instance histogram as staged for binding=3. + // §4 flap apparatus (2026-06-10): per-slot instance histogram as staged for binding=3. // grp.Slots is laid out 1:1 with grp.Matrices (binding=0), so this IS the slot content - // the GPU reads per instance — if outdoor instances land on the wrong slot (or vanish + // the GPU reads per instance — if outdoor instances land on the wrong slot (or vanish // into cullEnt) when the building flood merges, this line shows it directly. private void EmitClipRouteDispatchProbe(int culledEntities) { @@ -1001,7 +1001,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // are dropped entirely (not emitted into the binding=0 instance buffer NOR the // binding=3 slot buffer), matching the existing frustum / visible-cell cull. // Internal (not private) so the clip-slot unit tests can assert against it - // directly — see WbDrawDispatcherClipSlotTests. + // directly — see WbDrawDispatcherClipSlotTests. internal const int ClipSlotCull = -1; /// @@ -1014,14 +1014,14 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// ServerGuid != 0 with ParentCellId == null: CULL while routing is active. /// /// Only called when _clipRoutingActive (indoor root). On the U.3 / outdoor - /// path every instance is slot 0 and nothing is culled — see + /// path every instance is slot 0 and nothing is culled — see /// , which gates on that flag. /// /// INVARIANT: and the keys of /// MUST live in the same FULL cell-id space /// (lbMask | OtherCellId, e.g. 0xA9B40164). A bare-low-byte /// ParentCellId (e.g. 0x64) would never match a full-id key and would - /// silently CULL every indoor stab — cf. the L.2e bare-low-byte finding in + /// silently CULL every indoor stab — cf. the L.2e bare-low-byte finding in /// CLAUDE.md where player CellId was tracked without its landblock prefix. /// /// @@ -1073,12 +1073,12 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// (Slot, Culled) pair the per-entity loop body consumes. Wraps /// with the /// gate: when routing is INACTIVE (outdoor root / no portal frame), every entity - /// is slot 0 and nothing is clip-culled — the bit-identical-to-U.3 property, so + /// is slot 0 and nothing is clip-culled — the bit-identical-to-U.3 property, so /// the resolver (and ) is bypassed entirely. - /// When active, a CULL sentinel maps to (0, culled=true) — the slot value + /// When active, a CULL sentinel maps to (0, culled=true) — the slot value /// is never emitted for a culled entity. /// internal static + pure so the whole policy (including the routing- - /// inactive branch) is unit-testable — see WbDrawDispatcherClipSlotTests. + /// inactive branch) is unit-testable — see WbDrawDispatcherClipSlotTests. /// internal static (uint Slot, bool Culled) ResolveSlotForFrame( bool clipRoutingActive, @@ -1106,7 +1106,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// Entry for per-landblock iteration. /// Mirrors the shape yielded by GpuWorldState.LandblockEntries. /// - internal readonly record struct LandblockEntry( + public readonly record struct LandblockEntry( uint LandblockId, Vector3 AabbMin, Vector3 AabbMax, @@ -1114,10 +1114,10 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable IReadOnlyDictionary? AnimatedById); /// - /// Result of — the list of (entity, meshRef index) + /// Result of — the list of (entity, meshRef index) /// pairs that passed all visibility filters, plus a diagnostic walk count. /// - internal struct WalkResult + public struct WalkResult { public int EntitiesWalked; public int BuildingShellAnchorPass; @@ -1142,7 +1142,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// A.5 T18 Change #2: per-entity AABB cull reads from the cached /// / /// (refreshed lazily if ), instead of - /// recomputing Position±5 each frame. + /// recomputing Position±5 each frame. /// /// /// @@ -1177,7 +1177,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// visibleCellIds or frustum filters, and [indoor-walk] lines for /// cell entities that pass all filters. Rate-limited by /// . Pass (the default) - /// to disable all probe emission — used by the test-friendly + /// to disable all probe emission — used by the test-friendly /// overload. /// /// @@ -1238,8 +1238,8 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable if (!EntityMatchesSet(entity, set)) continue; if (entity.MeshRefs.Count == 0) continue; - // Detect cell entity for indoor probes — first MeshRef.GfxObjId - // is an EnvCell id (low 16 bits ≥ 0x0100). Cheap to compute; + // Detect cell entity for indoor probes — first MeshRef.GfxObjId + // is an EnvCell id (low 16 bits ≥ 0x0100). Cheap to compute; // result reused for all probe checks below. ulong cellProbeId = (ulong)entity.MeshRefs[0].GfxObjId; bool isCellEntity = indoorProbeState is not null @@ -1265,7 +1265,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable } if (shellScoped) result.BuildingShellAnchorPass++; - // Per-entity AABB frustum cull (perf #3). Animated entities bypass — + // Per-entity AABB frustum cull (perf #3). Animated entities bypass — // they're tracked at landblock level + need per-frame work regardless. // A.5 T18 Change #2: read cached AABB, refresh lazily on AabbDirty. bool isAnimated = animatedEntityIds?.Contains(entity.Id) == true; @@ -1291,7 +1291,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable continue; } - // Passed all filters — emit walk probe. + // Passed all filters — emit walk probe. if (isCellEntity && RenderingDiagnostics.ProbeIndoorWalkEnabled && indoorProbeState!.ShouldEmit(cellProbeId)) { @@ -1315,16 +1315,16 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// #119 ROOT-CAUSE FIX (2026-06-11): the Tier-1 cache hint must identify the /// entity's OWNING landblock, not the Draw call's tuple landblock. /// RetailPViewRenderer.DrawEntityBucket fabricates its tuple with the - /// PLAYER's landblock id, so every bucket entity that frame shared one hint — + /// PLAYER's landblock id, so every bucket entity that frame shared one hint — /// and colliding entity ids from different landblocks (the pre-fix /// 0x40YYFF00 interior namespace discarded the landblock X byte) mapped /// to the SAME cache key and served each other's batches: the AAB3 tower's /// 43-part staircase drew a 1-part entity's 3 zero-RestPose batches - /// (captured live, tower-dump-launch1.log) — the session-sticky "broken + /// (captured live, tower-dump-launch1.log) — the session-sticky "broken /// stairs + water barrel". Interior statics carry their owning cell; derive /// the hint from it, canonicalized to the same 0xXXYYFFFF key format /// the streaming entries and - /// use — which also makes owner-unload invalidation actually hit these + /// use — which also makes owner-unload invalidation actually hit these /// entries (bucket-hinted entries were previously orphaned forever). /// Entities without a ParentCellId (outdoor stabs / scenery / building /// shells via GpuWorldState entries) keep the tuple id, which IS their @@ -1404,7 +1404,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// (meshRefs, cacheBatches, zeroTranslations, culled) signature changes. /// Discriminates H-A (hydration-time MeshRef corruption: translations /// collapsed to ~zero / missing parts) from H-B (Tier-1 cache holding a - /// partial or stale batch set) from H-C (both healthy ⇒ draw-side compose). + /// partial or stale batch set) from H-C (both healthy ⇒ draw-side compose). /// private void MaybeEmitEntityDump( in RenderInstanceCandidate entity, @@ -1503,7 +1503,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable out Matrix4x4 vp, out Vector3 camPos); - // ── Phase 1: clear groups, walk entities, build groups ────────────── + // ── Phase 1: clear groups, walk entities, build groups ────────────── // Draw is invoked several times per frame (landscape slices, late // dynamics, paperdoll). Per-dispatch payloads reset here, while group // retirement happens once in BeginFrame from whole-frame liveness. @@ -1534,7 +1534,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable if (RenderingDiagnostics.ProbeIndoorCullEnabled || RenderingDiagnostics.ProbeIndoorWalkEnabled) { // _currentFrame is snapped at construction time. Construct - // once per Draw() call only — a second construction within + // once per Draw() call only — a second construction within // the same frame would stamp the dictionary with the // (already-advanced) counter value, suppressing the second // pass's emissions for IndoorProbeRateLimitFrames frames. @@ -1572,35 +1572,35 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // end of the loop for the last entity. Flushing per-tuple would // overwrite earlier MeshRefs (the cache is keyed by entity.Id), so // multi-part Setup-backed entities would only retain their LAST - // MeshRef's batches — bug fixed in commit after 2f489a8. + // MeshRef's batches — bug fixed in commit after 2f489a8. uint? populateEntityId = null; uint populateLandblockId = 0; - // §4 flap [clip-route-disp] probe (2026-06-10, throwaway): entities dropped by - // ResolveSlotForFrame's CULL sentinel this Draw. One increment per culled entity — + // §4 flap [clip-route-disp] probe (2026-06-10, throwaway): entities dropped by + // ResolveSlotForFrame's CULL sentinel this Draw. One increment per culled entity — // cheap enough to count unconditionally; emission below is probe-gated. int probeCulledEntities = 0; - // Tier 1 cache (#53) — fast-path one-shot tracker. The cache stores a + // Tier 1 cache (#53) — fast-path one-shot tracker. The cache stores a // FLAT list of batches across all MeshRefs of an entity, so a single // ApplyCacheHit call already drew every batch. _walkScratch yields // one tuple per (entity, MeshRefIndex), so without this guard a // 3-MeshRef static entity on a frame-2 cache hit would call - // ApplyCacheHit 3 times — appending all 6 batches × 3 = 18 instances - // to _groups instead of 6. Result: severe Z-fighting + 3× perf hit + // ApplyCacheHit 3 times — appending all 6 batches × 3 = 18 instances + // to _groups instead of 6. Result: severe Z-fighting + 3× perf hit // on every multi-part static entity (buildings, statues, multi-MeshRef // NPCs). The fast path must fire only on the FIRST tuple of each // entity; subsequent tuples skip via this tracker. uint? lastHitEntityId = null; - // Tier 1 cache (#53) — incomplete-entity guard. When any MeshRef of + // Tier 1 cache (#53) — incomplete-entity guard. When any MeshRef of // the current entity has _meshAdapter.TryGetRenderData return null // (mesh still async-decoding via ObjectMeshManager.PrepareMeshDataAsync), // we mark the entity incomplete and DROP the accumulated populate // scratch at entity boundary instead of writing it to the cache. // Otherwise the cache would hold a partial classification (some parts // missing), and frame-2 cache hits would persist that partial render - // even after the missing mesh loads — every subsequent frame sees the + // even after the missing mesh loads — every subsequent frame sees the // cache hit and skips re-classification, so the missing parts never // recover. User-visible symptom: the drudge statue on top of the // Foundry (multi-part Setup entity with AnimPartChange) renders with @@ -1642,9 +1642,9 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable lastHitEntityId = null; } - // Tier 1 cache (#53) — drop the previous entity's accumulated + // Tier 1 cache (#53) — drop the previous entity's accumulated // populate scratch BEFORE MaybeFlushOnEntityChange runs. If the - // previous entity ended incomplete (≥1 null renderData), we MUST + // previous entity ended incomplete (≥1 null renderData), we MUST // NOT cache its partial classification: clear scratch and null // the tracker so MaybeFlushOnEntityChange sees the cleaned state // and no-ops for this entity. Reset the incomplete flag for the @@ -1653,11 +1653,11 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // CRITICAL: the flag reset must fire ONLY on entity change, not // every tuple. Resetting per-tuple within the same entity would // undo a null-renderData flag set by a previous tuple of the same - // entity → if the missing MeshRef sits in the MIDDLE of the + // entity → if the missing MeshRef sits in the MIDDLE of the // entity's MeshRefs list, a later valid tuple's reset would // re-mark the entity "complete" and let partial data populate // the cache. Trees with [trunk valid, branches null, leaves - // valid] hit this exactly — branches never recover. + // valid] hit this exactly — branches never recover. // #119 root-cause fix: cache operations key on the entity's OWNING // landblock, never the Draw call's tuple landblock (which is the // PLAYER's landblock on the bucket path). See ResolveCacheLandblockHint. @@ -1687,7 +1687,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // Fix B: select this entity's up-to-8 point/spot lights ONCE (the set // is constant across the entity's parts/tuples), by the entity's - // bounding sphere — camera-INDEPENDENT (minimize_object_lighting). + // bounding sphere — camera-INDEPENDENT (minimize_object_lighting). ComputeEntityLightSet(entity); _currentEntitySelectionLighting = _selectionLighting?.TryGetLighting( @@ -1749,7 +1749,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // Placed AFTER the entity-change flush above so that, on a // hit, this iteration also finishes flushing any pending // populate state from a previous entity. Animated entities never - // enter this branch — the !isAnimated guard makes that explicit. + // enter this branch — the !isAnimated guard makes that explicit. // // Fires ONCE per entity: the first tuple reaches here, runs // ApplyCacheHit, sets lastHitEntityId, and continues. Subsequent @@ -1769,7 +1769,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // anyVao recovery: when the first visible entity in the frame // takes the fast path, no slow-path lookup has populated // anyVao yet. Look up THIS entity's first MeshRef once via - // the mesh adapter — cheap dict lookup, not a re-classify. + // the mesh adapter — cheap dict lookup, not a re-classify. if (anyVao == 0) { MeshRef firstMeshRef = tuple.MeshRef; @@ -1783,7 +1783,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable #if DEBUG // Cross-check guard: assert the membership predicate held at hit time. // The full re-classification cross-check (spec section 6.5) is a stretch - // goal; this simpler assert catches the prior Tier 1 bug class — a + // goal; this simpler assert catches the prior Tier 1 bug class — a // static entity that turns out to actually be animated would fire here. // // Structurally redundant with the `if (!isAnimated && ...)` branch @@ -1793,7 +1793,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // entities into the fast path; the assert catches that immediately. System.Diagnostics.Debug.Assert( !isAnimated, - $"EntityClassificationCache hit on animated entity {entity.Id} — invariant violated"); + $"EntityClassificationCache hit on animated entity {entity.Id} — invariant violated"); #endif continue; @@ -1806,7 +1806,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable paletteIdentity = TextureCache.GetPaletteIdentity(entity.PaletteOverride); // Note: GameWindow's spawn path already applies - // AnimPartChanges + GfxObjDegradeResolver (Issue #47 fix — + // AnimPartChanges + GfxObjDegradeResolver (Issue #47 fix — // close-detail mesh swap for humanoids) to MeshRefs. We // trust MeshRefs as the source of truth here. AnimatedEntityState's // overrides become relevant only for hot-swap (0xF625 @@ -1816,7 +1816,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable var renderData = _meshAdapter.TryGetRenderData(gfxObjId); - // [indoor-lookup] probe — emit once per cell entity per sec. + // [indoor-lookup] probe — emit once per cell entity per sec. // Fires BEFORE the null-renderData early-continue so a miss still // emits hit=false, distinguishing H2 (empty batches) from H6 // (dispatcher fails to traverse Setup). @@ -1863,7 +1863,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable currentEntityIncomplete = true; if (diag) _meshesMissing++; // #128 self-heal: a missing-but-referenced mesh re-requests - // its load HERE — the one site that touches it every frame — + // its load HERE — the one site that touches it every frame — // so a preparation lost to landblock churn (cancelled after // the last registration event) can never stay lost. Deduped // per Draw; PrepareMeshDataAsync is idempotent while pending. @@ -1888,11 +1888,11 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable if (renderData.IsSetup && renderData.SetupParts.Count > 0) { // #188: setupPartIndex is the SAME index space - // TransparentPartHook.PartIndex addresses — retail's CPartArray + // TransparentPartHook.PartIndex addresses — retail's CPartArray // numbers parts by their ordinal position in the Setup's own // part list (SetupPartTransforms.Compute is the other verified // consumer of this exact indexing: one rigid pose per - // Setup.Parts[i]). NOT the outer per-MeshRef loop index — a + // Setup.Parts[i]). NOT the outer per-MeshRef loop index — a // MeshRef is acdream's own decomposition of top-level // attachments (weapon/shield/etc), a different concept. for (int setupPartIndex = 0; setupPartIndex < renderData.SetupParts.Count; setupPartIndex++) @@ -1903,7 +1903,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable { // #128 self-heal + #53: a missing Setup PART must mark // the entity incomplete (else a partial batch set - // caches permanently — the same bug class one level + // caches permanently — the same bug class one level // deeper) and re-request its load like the MeshRef // path above. currentEntityIncomplete = true; @@ -1920,11 +1920,11 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable var model = ComposePartWorldMatrix( entityWorld, meshRef.PartTransform, partTransform); - // [indoor-xform] probe — only for the cell's synthetic + // [indoor-xform] probe — only for the cell's synthetic // geometry part (bit 32 set, per WB's PrepareEnvCellMeshData // cellGeomId convention). One line per part per sec. - // Disambiguates hypothesis H5 (transform double-apply — - // composedT lands at 2 × cellOrigin). + // Disambiguates hypothesis H5 (transform double-apply — + // composedT lands at 2 × cellOrigin). if ((partGfxObjId & 0x1_0000_0000UL) != 0 && RenderingDiagnostics.ProbeIndoorXformEnabled && ShouldEmitIndoorProbe(partGfxObjId)) @@ -1941,7 +1941,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // #188 retail CPhysicsPart::Draw (0x0050d7a0) early-out: once a // part's translucency hits EXACTLY 1.0 (fully invisible), retail - // sets draw_state|=1 and skips the whole part outright — not a + // sets draw_state|=1 and skips the whole part outright — not a // blend to nothing. TranslucencyFadeManager.AdvanceAll guarantees // t=1 commits the bitwise-exact value so this check is safe. float opacityMultiplier = 1.0f; @@ -1968,7 +1968,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable } else { - // #188: a bare (non-Setup) GfxObj entity has exactly one part — + // #188: a bare (non-Setup) GfxObj entity has exactly one part — // retail's CPartArray for such an object is a single-entry array, // so TransparentPartHook.PartIndex for it is always 0. float opacityMultiplier = 1.0f; @@ -2013,8 +2013,8 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable if (diag && drewAny) _entitiesDrawn++; } - // Tier 1 cache (#53) — drop the accumulated populate scratch if the - // LAST entity in the loop ended incomplete (had ≥1 null renderData). + // Tier 1 cache (#53) — drop the accumulated populate scratch if the + // LAST entity in the loop ended incomplete (had ≥1 null renderData). // Same reason as the entity-boundary handling above: avoid caching a // partial classification. The slow path will retry on the next frame // and populate correctly once all meshes have loaded. @@ -2033,7 +2033,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable populateEntityId, populateLandblockId, _cache, _populateScratch, _populateSelectionScratch); - // §4 flap [clip-route-disp] probe (2026-06-10, throwaway): the per-slot instance + // §4 flap [clip-route-disp] probe (2026-06-10, throwaway): the per-slot instance // histogram exactly as it will be uploaded to binding=3 (grp.Slots) plus the // culled-entity count. Routed draws only (the landscape pass under DrawInside) so the // unrouted per-cell bucket draws don't oscillate the print-on-change signature. @@ -2102,7 +2102,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable bool diag, bool observeCurrentPath) { - // Nothing visible — skip the GL pass entirely. + // Nothing visible — skip the GL pass entirely. if (anyVao == 0) { LastDrawStats = new DrawStats(set, entitiesWalked, tupleCount, 0, 0, 0, 0, 0, 0); @@ -2116,7 +2116,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable return; } - // ── Phase 3: assign FirstInstance per group, lay matrices contiguously, sort opaque ── + // ── Phase 3: assign FirstInstance per group, lay matrices contiguously, sort opaque ── bool deferTransparent = _alphaQueue?.IsCollecting == true; var instanceCounts = PartitionInstanceGroups( groups, @@ -2152,7 +2152,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // Phase U.4: size the per-instance clip-slot buffer to match the instance // count and lay it out in the SAME group order / cursor as _instanceData, // so instanceClipSlot[i] (binding=3) tracks Instances[i] (binding=0). On - // the U.3 / outdoor path every Slots entry is 0 ⇒ identical to U.3. + // the U.3 / outdoor path every Slots entry is 0 ⇒ identical to U.3. if (_clipSlotData.Length < immediateInstances) _clipSlotData = new uint[immediateInstances + 256]; @@ -2198,8 +2198,8 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // submission so the shared queue can interleave it with particles. // Immediate mode remains for sealed off-screen consumers such as the // paperdoll and UI Studio render stack. - // ── Phase 4: build IndirectGroupInput list (opaque sorted, then translucent), - // fill via BuildIndirectArrays ────────────────────────────────── + // ── Phase 4: build IndirectGroupInput list (opaque sorted, then translucent), + // fill via BuildIndirectArrays ────────────────────────────────── int immediateTransparentCount = deferTransparent ? 0 : _translucentDraws.Count; int totalDraws = _opaqueDraws.Count + immediateTransparentCount; TrackScratchDemand(Math.Max(totalInstances, totalDraws)); @@ -2260,7 +2260,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable deferTransparent, camPos); - // ── Phase 5: upload four buffers ──────────────────────────────────── + // ── Phase 5: upload four buffers ──────────────────────────────────── ActivateNextDynamicBufferSet(); fixed (float* ip = _instanceData) UploadSsbo(_instanceSsbo, 0, ref _instanceSsboCapacityBytes, @@ -2273,9 +2273,9 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // Phase U.4: per-instance clip-slot buffer (binding=3), one uint per // instance, laid out parallel to _instanceData in Phase 3's group loop so // instanceClipSlot[instanceIndex] tracks Instances[instanceIndex]. On the - // U.3 / outdoor path every entry is 0 ⇒ slot 0 ⇒ no-clip (identical to + // U.3 / outdoor path every entry is 0 ⇒ slot 0 ⇒ no-clip (identical to // U.3); under indoor routing it holds the per-instance slot from - // ResolveEntitySlot. No clear here — Phase 3 wrote exactly immediateInstances + // ResolveEntitySlot. No clear here — Phase 3 wrote exactly immediateInstances // entries; only [0..immediateInstances) is uploaded, so any stale tail is // never read by the shader. fixed (uint* sp = _clipSlotData) @@ -2284,14 +2284,14 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // #142: per-instance indoor flag buffer (binding=6), one uint per instance, // laid out parallel to _instanceData in Phase 3. Only [0..immediateInstances) - // is uploaded — stale tail never read (same guarantee as clip-slot above). + // is uploaded — stale tail never read (same guarantee as clip-slot above). fixed (uint* dp = _indoorData) UploadSsbo(_instIndoorSsbo, 6, ref _instIndoorSsboCapacityBytes, dp, immediateInstances * sizeof(uint)); // #188: per-instance opacity buffer (binding=7), one float per instance, // laid out parallel to _instanceData in Phase 3. Only [0..immediateInstances) - // is uploaded — stale tail never read (same guarantee as clip-slot above). + // is uploaded — stale tail never read (same guarantee as clip-slot above). fixed (float* ap = _alphaData) UploadSsbo(_instAlphaSsbo, 7, ref _instAlphaSsboCapacityBytes, ap, immediateInstances * sizeof(float)); @@ -2306,7 +2306,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // Fix B: global point-light buffer (binding=4) + per-instance light-set // buffer (binding=5). The global buffer is this frame's PointSnapshot; the // per-instance buffer holds 8 int indices into it per instance, laid out - // parallel to _instanceData in Phase 3. Both bound with ≥1 element so the + // parallel to _instanceData in Phase 3. Both bound with ≥1 element so the // shader never reads an unbound SSBO on a no-lights frame. UploadGlobalLights(); fixed (int* lp = _lightSetData) @@ -2336,7 +2336,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // shader never reads an unbound SSBO at binding=2. BindClipRegionBinding2(); - // ── Phase 6: bind global VAO once ─────────────────────────────────── + // ── Phase 6: bind global VAO once ─────────────────────────────────── _gl.BindVertexArray(anyVao); if (string.Equals(Environment.GetEnvironmentVariable("ACDREAM_NO_CULL"), "1", StringComparison.Ordinal)) @@ -2346,11 +2346,11 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // result (the oldest data in the ring) before overwriting it with // frame N's queries. Hoisted to function scope so both the opaque // and transparent passes below can reference gpuQuerySlot. See spec - // §3 Q1/Q2 + §4 in + // §3 Q1/Q2 + §4 in // docs/superpowers/specs/2026-05-11-phase-n6-slice1-design.md. int gpuQuerySlot = _gpuQueryFrameIndex % GpuQueryRingDepth; // diag is part of the gate so the read/issue/increment trio stays - // symmetric — without it, toggling ACDREAM_WB_DIAG mid-session would + // symmetric — without it, toggling ACDREAM_WB_DIAG mid-session would // freeze the frame counter (gated by diag below) while the read kept // re-reading the same slot, producing duplicate stale samples. if (diag && _gpuQueriesInitialized && _gpuQueryFrameIndex >= GpuQueryRingDepth) @@ -2391,14 +2391,14 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable } } - // ── Phase 7: opaque pass ───────────────────────────────────────────── + // ── Phase 7: opaque pass ───────────────────────────────────────────── if (_opaqueDrawCount > 0) { _gl.Disable(EnableCap.Blend); _gl.DepthMask(true); - // A.5 T20: enable A2C for ClipMap foliage — GPU derives sample mask + // A.5 T20: enable A2C for ClipMap foliage — GPU derives sample mask // from the alpha written by mesh_modern.frag so foliage edges are - // smooth under MSAA 4x. A no-op for fully-opaque (α=1) batches. + // smooth under MSAA 4x. A no-op for fully-opaque (α=1) batches. // A.5 T22.5: gated by AlphaToCoverage property so Low/Medium presets // (no MSAA) skip the unnecessary GL state change. if (AlphaToCoverage) _gl.Enable(EnableCap.SampleAlphaToCoverage); @@ -2418,7 +2418,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable if (AlphaToCoverage) _gl.Disable(EnableCap.SampleAlphaToCoverage); } - // ── Phase 8: transparent pass ──────────────────────────────────────── + // ── Phase 8: transparent pass ──────────────────────────────────────── if (_transparentDrawCount > 0) { _gl.Enable(EnableCap.Blend); @@ -2426,8 +2426,8 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable _gl.DepthMask(false); // Phase Post-A.5 (ISSUE #52, 2026-05-10): transparent section of // Batches[] starts at index _opaqueDrawCount. Without this offset, - // each transparent draw reads BatchData[0..transparentCount) — the - // OPAQUE section — and the lifestone crystal's apparent texture + // each transparent draw reads BatchData[0..transparentCount) — the + // OPAQUE section — and the lifestone crystal's apparent texture // flickers to whatever opaque batch sorted first that frame. See // uDrawIDOffset comment in mesh_modern.vert. _shader.SetInt("uDrawIDOffset", _opaqueDrawCount); @@ -2481,7 +2481,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// explicit cell list (not the BFS-derived visibility set). The semantic /// difference is at the caller: cellIds = the camera-buildings' EnvCellIds, /// not the portal BFS result. The dispatcher's internal logic is identical - /// — it filters indoor entities by membership in the provided set. + /// — it filters indoor entities by membership in the provided set. /// public void Draw( ICamera camera, @@ -2494,7 +2494,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable HashSet? animatedEntityIds = null, EntitySet set = EntitySet.All) { - // Adapt IReadOnlyCollection → HashSet for the existing path. + // Adapt IReadOnlyCollection → HashSet for the existing path. // If the caller already passed a HashSet, avoid re-wrapping. HashSet cellIdSet = cellIds is HashSet hs ? hs : new HashSet(cellIds); Draw(camera, landblockEntries, @@ -3442,8 +3442,8 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// /// Fix B: pack into the binding=4 global light /// buffer (one GlobalLight = 4 vec4 = 16 floats, std430 stride 64 bytes, - /// matching mesh_modern.vert's GlobalLight). Always uploads ≥1 element - /// so the shader never reads an unbound SSBO — on a no-lights frame index 0 is + /// matching mesh_modern.vert's GlobalLight). Always uploads ≥1 element + /// so the shader never reads an unbound SSBO — on a no-lights frame index 0 is /// a zeroed dummy that no instance set references (all sets are -1). /// private unsafe void UploadGlobalLights() @@ -3461,8 +3461,8 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// when a new one was registered since the /// last flush (by or ), /// then (re)binds it at . - /// A genuinely new handle is rare — new dat surfaces/composite overrides, - /// not every frame — so unlike the SSBOs above this is not part of the + /// A genuinely new handle is rare — new dat surfaces/composite overrides, + /// not every frame — so unlike the SSBOs above this is not part of the /// ring-buffered ; see /// 's doc comment. /// @@ -3511,7 +3511,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable if (_fallbackClipRegionSsbo == 0) { _fallbackClipRegionSsbo = _gl.GenBuffer(); - // One CellClip slot, all zeros: count 0 ⇒ shader passes every plane. + // One CellClip slot, all zeros: count 0 ⇒ shader passes every plane. var zero = stackalloc byte[ClipFrame.CellClipStrideBytes]; for (int i = 0; i < ClipFrame.CellClipStrideBytes; i++) zero[i] = 0; _gl.BindBuffer(BufferTargetARB.ShaderStorageBuffer, _fallbackClipRegionSsbo); @@ -3532,7 +3532,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable long gpuMed = MedianMicros(_gpuSamples); long gpuP95 = Percentile95Micros(_gpuSamples); // A.5 T23: flag when entity dispatcher median exceeds 2.0ms budget - // (Phase A.5 spec §2 acceptance criterion 6). Grep-friendly prefix. + // (Phase A.5 spec §2 acceptance criterion 6). Grep-friendly prefix. const long BudgetUs = 2000; string budgetFlag = cpuMed > BudgetUs ? " BUDGET_OVER" : ""; Console.WriteLine( @@ -3540,7 +3540,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable $"cpu_us={cpuMed}m/{cpuP95}p95 gpu_us={gpuMed}m/{gpuP95}p95"); _entitiesSeen = _entitiesDrawn = _meshesMissing = _drawsIssued = _instancesIssued = 0; _lastLogTick = now; - // Don't reset the sample buffers — they're a moving window of the + // Don't reset the sample buffers — they're a moving window of the // last 256 frames; clearing per 5s flush would lose recent history. } } @@ -3553,7 +3553,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable foreach (var v in copy) if (v > 0) nz++; if (nz == 0) return 0; // Sorted ascending: zero-padding front, samples at the back. (nz - 1) / 2 - // from the end keeps the offset >= 0 for all nz >= 1 — the original + // from the end keeps the offset >= 0 for all nz >= 1 — the original // nz / 2 form indexed copy[copy.Length] (crash) on the first diag flush // when exactly 1 sample was recorded. Same fix as GameWindow's // TerrainDiagMedianMicros twin. @@ -3571,7 +3571,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable return copy[idx]; } - // ── Tier 1 cache (#53) helpers extracted for testability ───────────────── + // ── Tier 1 cache (#53) helpers extracted for testability ───────────────── // // Three pure-CPU static helpers carved out of Draw's per-entity loop so // unit tests can exercise the populate/flush algorithm + cache-hit fast @@ -3669,7 +3669,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// AND differs from , the previous /// entity's accumulated batches are committed to /// and is cleared. Returns the - /// updated tracker tuple — pass these back into the field locals in the + /// updated tracker tuple — pass these back into the field locals in the /// caller's loop. /// /// @@ -3711,7 +3711,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// so commit its accumulated batches here. No-op when no populate is /// pending (the last entity was animated, or the scratch is empty). /// - /// End-of-loop only — does NOT reset the caller's tracker locals + /// End-of-loop only — does NOT reset the caller's tracker locals /// (intentional, since they go out of scope immediately after). /// /// @@ -3784,11 +3784,11 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable grp.Matrices.Add(model); grp.LocalSortCenters.Add(localSortCenter); grp.SubmissionOrders.Add(_nextInstanceSubmissionOrder++); - grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices - AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, parallel to Matrices + grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices + AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, parallel to Matrices // #188: cache-hit entities are always non-animated (the Tier-1 cache // gates on !isAnimated), and TranslucencyFadeManager only ever holds - // state for entities whose animation hooks fired — so a cached + // state for entities whose animation hooks fired — so a cached // instance can never be mid-fade. Always unmodified opacity. grp.Opacities.Add(1.0f); grp.SelectionLighting.Add(_currentEntitySelectionLighting); @@ -3800,11 +3800,11 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// sphere. Camera-independent (), so /// a static building's torches stay constant as the viewer moves. Fills /// ; unused slots are -1. On the no-lights - /// path (no snapshot handed in) every slot is -1 ⇒ shader adds no point light. + /// path (no snapshot handed in) every slot is -1 ⇒ shader adds no point light. /// /// /// A7 Fix D round 2 (2026-06-19): retail lights OUTDOOR objects with the SUN + - /// ambient ONLY — never the static wall torches. The per-object torch step + /// ambient ONLY — never the static wall torches. The per-object torch step /// (minimize_object_lighting, 0x0054d480) runs ONLY in the indoor stage: /// RenderDeviceD3D::DrawMeshInternal (0x0059f398) calls it under /// if (Render::useSunlight == 0), and the outdoor landscape stage runs @@ -3815,16 +3815,16 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// creatures get the sun, not torches. We mirror that: only objects parented to /// an EnvCell (indoor) select torches; outdoor objects keep the all-(-1) set so /// the sun path alone lights them. This is what made the Holtburg meeting-hall - /// facade wash out warm — the dat's intensity-100 wall torches (range - /// Falloff×1.3) were flooding the exterior shell that retail never torch-lights. + /// facade wash out warm — the dat's intensity-100 wall torches (range + /// Falloff×1.3) were flooding the exterior shell that retail never torch-lights. /// The indoor "no sun" half is already handled by the global sun kill when the /// player is inside a cell (UpdateSunFromSky). See the divergence register /// (AP-43) and docs/research/2026-06-19-lighting-a7-fixD-round2-*. /// /// - // #176 seam-draw probe (ACDREAM_PROBE_SEAMDRAW) — throwaway apparatus. One + // #176 seam-draw probe (ACDREAM_PROBE_SEAMDRAW) — throwaway apparatus. One // [seam-ent] line per target-cell entity, re-emitted on state change: world - // position (F3 z — entities do NOT get the +0.02 shell lift), cull/slot, + // position (F3 z — entities do NOT get the +0.02 shell lift), cull/slot, // and the SelectForObject light set resolved to identities (owner-cell // low16 + intensity). Sig dict is bounded by the handful of entities that // ever live in the target cells. @@ -3865,7 +3865,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable { // #142: set the indoor flag first so it's available even when the early-return // fires below. Both the torch selection and the sun gate use the same predicate, - // so they can't disagree — one call, one truth. + // so they can't disagree — one call, one truth. _currentEntityIndoor = IndoorObjectReceivesTorches(entity.ParentCell); @@ -3889,7 +3889,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// Retail's useSunlight gate for per-object torch lighting, as a pure /// predicate. An object receives the static wall torches (the indoor /// minimize_object_lighting pass) ONLY when it is parented to an EnvCell - /// — an interior cell, by the AC convention (cellId & 0xFFFF) >= 0x0100. + /// — an interior cell, by the AC convention (cellId & 0xFFFF) >= 0x0100. /// Outdoor objects (building shells with null , /// outdoor scenery in a land sub-cell 0x0001..0x00FF, outdoor creatures) /// are sun-lit only and return false. Mirrors @@ -3899,7 +3899,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable internal static bool IndoorObjectReceivesTorches(uint? parentCellId) => parentCellId.HasValue && (parentCellId.Value & 0xFFFFu) >= 0x0100u - && (parentCellId.Value & 0xFFFFu) != 0xFFFFu; // 0xFFFF = landblock marker, not an EnvCell → outdoor + && (parentCellId.Value & 0xFFFFu) != 0xFFFFu; // 0xFFFF = landblock marker, not an EnvCell → outdoor /// /// Fix B: append the current entity's 8-slot light set to a group's @@ -3931,7 +3931,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // #188: a mid-fade instance whose surface is otherwise Opaque/ClipMap // must route through the alpha-blend pass so mesh_modern.frag's - // (blend-enabled) shader actually composites the reduced alpha — + // (blend-enabled) shader actually composites the reduced alpha — // the no-blend opaque pass would ignore it. if (opacityMultiplier < 1.0f && IsOpaque(translucency)) translucency = TranslucencyKind.AlphaBlend; @@ -3956,9 +3956,9 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable grp.Matrices.Add(model); grp.LocalSortCenters.Add(renderData.SortCenter); grp.SubmissionOrders.Add(_nextInstanceSubmissionOrder++); - grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices - AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, parallel to Matrices - grp.Opacities.Add(opacityMultiplier); // #188 — parallel to Matrices + grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices + AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, parallel to Matrices + grp.Opacities.Add(opacityMultiplier); // #188 — parallel to Matrices grp.SelectionLighting.Add(_currentEntitySelectionLighting); collector?.Add(new CachedBatch( key, @@ -4085,7 +4085,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable EntitySet set) { // No cell filter (outdoor root, or a bucket drawn unfiltered like live-dynamics / outdoor - // scenery) ⇒ every entity passes; clip-slot routing (ResolveEntitySlot) does the gating. + // scenery) ⇒ every entity passes; clip-slot routing (ResolveEntitySlot) does the gating. if (visibleCellIds is null) return true; @@ -4094,7 +4094,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable return visibleCellIds.Contains(entity.ParentCellId.Value); // ParentCellId == null (outdoor scenery / building shell): NOT a member of any interior cell, - // so it does NOT pass a cell-membership filter (R1: the bleed fix — was an unconditional + // so it does NOT pass a cell-membership filter (R1: the bleed fix — was an unconditional // `return true`). When such entities must draw (through the doorway), the caller passes // visibleCellIds: null and relies on ResolveEntitySlot's OutsideView routing instead. return false; @@ -4272,24 +4272,24 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable _gpuQueriesInitialized = false; } - // ── Public types + helpers for BuildIndirectArrays (Task 9) ───────────── + // ── Public types + helpers for BuildIndirectArrays (Task 9) ───────────── // // These are public so the pure-CPU unit tests in AcDream.Core.Tests can // exercise BuildIndirectArrays without needing a GL context. /// /// Stride in bytes of DrawElementsIndirectCommand in the indirect buffer. - /// 5 × uint = 20 bytes. Tests and callers reference this symbolically + /// 5 × uint = 20 bytes. Tests and callers reference this symbolically /// rather than hard-coding 20 so a layout change produces a compile error. /// - public const int DrawCommandStride = 20; // sizeof(DrawElementsIndirectCommand): 5 × uint + public const int DrawCommandStride = 20; // sizeof(DrawElementsIndirectCommand): 5 × uint /// - /// Public view of the per-group inputs to — used in tests. + /// Public view of the per-group inputs to — used in tests. /// Campaign V slice V2: TextureIndex is a slot into the binding=9 /// handle table (was a raw 64-bit bindless TextureHandle). /// - internal readonly record struct IndirectGroupInput( + public readonly record struct IndirectGroupInput( int IndexCount, uint FirstIndex, int BaseVertex, @@ -4305,7 +4305,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// Tests verify the layout. Same field shape as the private BatchData. /// [StructLayout(LayoutKind.Sequential, Pack = 4)] - internal struct BatchDataPublic + public struct BatchDataPublic { public uint TextureIndex; public uint Reserved; @@ -4314,7 +4314,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable } /// Result of . - internal readonly record struct IndirectLayoutResult( + public readonly record struct IndirectLayoutResult( int OpaqueCount, int TransparentCount, int TransparentByteOffset); @@ -4325,8 +4325,8 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// Pure CPU, no GL state. Caller passes pre-sized scratch arrays. /// /// - /// Classification: Opaque + ClipMap → opaque pass (ClipMap uses discard, not - /// blending). Everything else (AlphaBlend, Additive, InvAlpha) → transparent pass. + /// Classification: Opaque + ClipMap → opaque pass (ClipMap uses discard, not + /// blending). Everything else (AlphaBlend, Additive, InvAlpha) → transparent pass. /// public static IndirectLayoutResult BuildIndirectArrays( IReadOnlyList groups, @@ -4385,15 +4385,15 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// /// Public test shim for . Locks in the N.5 Decision 2 - /// translucency partition: Opaque + ClipMap → opaque indirect; AlphaBlend + - /// Additive + InvAlpha → transparent indirect. + /// translucency partition: Opaque + ClipMap → opaque indirect; AlphaBlend + + /// Additive + InvAlpha → transparent indirect. /// public static bool IsOpaquePublic(TranslucencyKind t) => IsOpaque(t); private static bool IsOpaque(TranslucencyKind t) => t == TranslucencyKind.Opaque || t == TranslucencyKind.ClipMap; - // ──────────────────────────────────────────────────────────────────────── + // ──────────────────────────────────────────────────────────────────────── /// /// Thin wrapper around an instance's rate-limit dictionary + frame @@ -4496,7 +4496,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// appended in lockstep (one entry per drawn instance) during group build, so /// they MUST all be cleared together each frame. Keeping the reset in one /// method stops a newly-added parallel list from silently drifting out of the - /// frame lifecycle — which is exactly the #193 OOM: #188 added + /// frame lifecycle — which is exactly the #193 OOM: #188 added /// alongside the others but left it out of the old /// inline clear loop, so it grew one float per instance per frame forever and /// leaked ~1 GB/min of LOH float[] as its backing array doubled. diff --git a/src/AcDream.App/Rendering/Wb/WbFrustum.cs b/src/AcDream.App/Rendering/Wb/WbFrustum.cs index 7e22c5cb..96a254e0 100644 --- a/src/AcDream.App/Rendering/Wb/WbFrustum.cs +++ b/src/AcDream.App/Rendering/Wb/WbFrustum.cs @@ -1,4 +1,4 @@ -// Ported from references/WorldBuilder/Chorizite.OpenGLSDLBackend/Frustum.cs +// Ported from references/WorldBuilder/Chorizite.OpenGLSDLBackend/Frustum.cs // Phase A8 extraction (2026-05-28). Verbatim algorithm; adaptations: // - Namespace: AcDream.App.Rendering.Wb // - Class renamed Frustum -> WbFrustum @@ -10,7 +10,7 @@ using System.Numerics; namespace AcDream.App.Rendering.Wb; -internal struct WbBoundingBox +public struct WbBoundingBox { public Vector3 Min; public Vector3 Max; @@ -27,7 +27,7 @@ internal struct WbBoundingBox Vector3.Max(a.Max, b.Max)); } -internal enum FrustumTestResult +public enum FrustumTestResult { Outside, Inside, @@ -39,7 +39,7 @@ internal enum FrustumTestResult /// Source: references/WorldBuilder/Chorizite.OpenGLSDLBackend/Frustum.cs /// Phase A8 extraction (2026-05-28). /// -internal sealed class WbFrustum +public sealed class WbFrustum { private struct Plane { diff --git a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs index 7f59775b..1bc0fd88 100644 --- a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs +++ b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using AcDream.Content; using AcDream.App.Rendering.Residency; @@ -17,11 +17,11 @@ namespace AcDream.App.Rendering.Wb; /// /// /// As of Phase O-T7, all DAT I/O routes through the runtime-owned shared -/// facade — the separate +/// facade — the separate /// DefaultDatReaderWriter file-handle set has been removed. /// /// -internal sealed class WbMeshAdapter +public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter { @@ -291,12 +291,12 @@ internal sealed class WbMeshAdapter private WbMeshAdapter() { - // Uninitialized constructor — only for tests / flag-off cases where + // Uninitialized constructor — only for tests / flag-off cases where // the caller wants a Dispose-safe no-op instance. _isUninitialized = true; } - /// Test/init helper — produces a Dispose-safe instance with no + /// Test/init helper — produces a Dispose-safe instance with no /// underlying mesh manager. Public methods are all no-ops. public static WbMeshAdapter CreateUninitialized() => new(); @@ -310,7 +310,7 @@ internal sealed class WbMeshAdapter /// /// Returns the WB render data for , or null if not /// yet uploaded or if this adapter is uninitialized. Increments WB's - /// internal usage counter — use for + /// internal usage counter — use for /// render-loop lookups that should not affect lifecycle. /// public ObjectRenderData? GetRenderData(ulong id) @@ -353,15 +353,15 @@ internal sealed class WbMeshAdapter // auto-enqueues into _stagedMeshData (ObjectMeshManager line 510), // which Tick() drains onto the GPU. Until that completes, // TryGetRenderData(id) returns null and the dispatcher silently - // skips the entity — standard streaming flicker. + // skips the entity — standard streaming flicker. // // #128 (2026-06-11): Prepare must RE-ARM whenever the id has no render - // data — NOT only on the first-ever registration. A first-only gate + // data — NOT only on the first-ever registration. A first-only gate // permanently lost any id whose initial read was cancelled before completing - // (landblock unload → CancelStagedUploads during login/teleport + // (landblock unload → CancelStagedUploads during login/teleport // churn) or whose upload was later LRU-evicted: every subsequent // registration skipped Prepare, so the mesh stayed invisible for the - // session with zero log output — the dispatcher's slow path just + // session with zero log output — the dispatcher's slow path just // counted meshMissing forever (issue #55's 1.45M/5s mountain was this // bug's heartbeat). User-visible: the AAB3 tower staircase rendering // partially or not at all depending on the session's landblock @@ -370,7 +370,7 @@ internal sealed class WbMeshAdapter // on existing render data, returns the in-flight task when already // pending, and dedups via _preparationTasks. // - // isSetup: false — acdream's MeshRefs already carry expanded + // isSetup: false — acdream's MeshRefs already carry expanded // per-part GfxObj ids (0x01XXXXXX). WB's Setup-expansion path is // unused. if (_meshManager.TryGetRenderData(id) is null) @@ -419,15 +419,15 @@ internal sealed class WbMeshAdapter /// /// #128 self-heal (2026-06-11): re-request a mesh load at the POINT OF - /// USE. Registration-time re-arming was insufficient — a preparation + /// USE. Registration-time re-arming was insufficient — a preparation /// cancelled by landblock churn AFTER the last registration event /// (running across blocks loads/unloads them repeatedly) left the mesh /// permanently unloadable with no later event to re-fire it. The draw /// dispatcher touches every missing-but-referenced mesh every frame (the - /// meshMissing slow path) — that is the one place a retry can never be + /// meshMissing slow path) — that is the one place a retry can never be /// missed. Cheap and idempotent: PrepareMeshDataAsync early-outs on /// existing render data and returns the in-flight task when pending. - /// Retail-equivalence: retail loads content synchronously — geometry is + /// Retail-equivalence: retail loads content synchronously — geometry is /// never permanently absent; this converges our async pipeline to the /// same guarantee. /// @@ -466,7 +466,7 @@ internal sealed class WbMeshAdapter _graphicsDevice!.ProcessGLQueue(); // #125: drain staged uploads; a FAILED upload (UploadMeshData returned // null from its catch) is re-staged for a LATER frame, not dropped. The - // re-stages are collected and re-enqueued AFTER the loop — re-enqueuing + // re-stages are collected and re-enqueued AFTER the loop — re-enqueuing // inside the while would let a deterministic failure spin the queue in a // single frame. UploadOrRequeue bounds the retries (MaxUploadRetries) so // a genuine defect surfaces loudly instead of the old silent sticky drop. @@ -594,14 +594,14 @@ internal sealed class WbMeshAdapter : default; // #105 root cause (2026-06-10): TextureAtlasManager.AddTexture only STAGES - // immutable decoded payloads in ManagedGLTextureArray._pendingUpdates — the + // immutable decoded payloads in ManagedGLTextureArray._pendingUpdates — the // actual TexSubImage3D copies + mipmap regeneration happen in // ProcessDirtyUpdates, which WB drives ONCE PER FRAME from its render loop // (WB GameScene.cs:975 `_meshManager?.GenerateMipmaps()`, just before the // opaque pass). That call site lived in the GameScene file the N.4/O-T4 // extraction replaced with GameWindow, so the driver was silently dropped: // staged updates never reached TexSubImage3D, leaving undefined - // TexStorage3D content behind valid resident bindless handles — the + // TexStorage3D content behind valid resident bindless handles — the // intermittent white indoor walls (#105). Pre-fix evidence: 126 updates // stuck across 34/34 arrays at standstill (texflush-prefix.log). Tick() // runs before all draw passes (GameWindow OnRender), so this is the exact @@ -662,7 +662,7 @@ internal sealed class WbMeshAdapter meshManager.RejectUnsupportedStagedUpload(rejected, error); } - // #105 apparatus state — see RenderingDiagnostics.ProbeTexFlushEnabled. + // #105 apparatus state — see RenderingDiagnostics.ProbeTexFlushEnabled. private int _lastTexFlushBefore = -1; private int _texFlushHeartbeat; diff --git a/src/AcDream.App/Rendering/Wb/WbRenderPass.cs b/src/AcDream.App/Rendering/Wb/WbRenderPass.cs index 753bffff..746050f3 100644 --- a/src/AcDream.App/Rendering/Wb/WbRenderPass.cs +++ b/src/AcDream.App/Rendering/Wb/WbRenderPass.cs @@ -1,4 +1,4 @@ -namespace AcDream.App.Rendering.Wb; +namespace AcDream.App.Rendering.Wb; /// /// Phase A8 (2026-05-28): WB's RenderPass enum, extracted verbatim from @@ -9,7 +9,7 @@ /// Consumed by and matches the /// uRenderPass uniform in the modern mesh shaders. /// -internal enum WbRenderPass +public enum WbRenderPass { /// /// The opaque pass. Only non-transparent objects are rendered. diff --git a/src/AcDream.App/Rendering/WorldSceneDiagnosticsController.cs b/src/AcDream.App/Rendering/WorldSceneDiagnosticsController.cs index cc5eeb81..6a6d31c2 100644 --- a/src/AcDream.App/Rendering/WorldSceneDiagnosticsController.cs +++ b/src/AcDream.App/Rendering/WorldSceneDiagnosticsController.cs @@ -58,7 +58,6 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics private readonly IWorldScenePViewDiagnosticSource _pview; private readonly IWorldSceneDebugStateSource _state; private readonly DebugLineRenderer? _lines; - private readonly ICurrentGpuFrameSource _currentFrame; private readonly PhysicsEngine _physics; private readonly ILocalPlayerModeSource _mode; private readonly IRuntimeLocalPlayerControllerSource _player; @@ -71,7 +70,6 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics IWorldScenePViewDiagnosticSource pview, IWorldSceneDebugStateSource state, DebugLineRenderer? lines, - ICurrentGpuFrameSource currentFrame, PhysicsEngine physics, ILocalPlayerModeSource mode, IRuntimeLocalPlayerControllerSource player, @@ -82,7 +80,6 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics _pview = pview ?? throw new ArgumentNullException(nameof(pview)); _state = state ?? throw new ArgumentNullException(nameof(state)); _lines = lines; - _currentFrame = currentFrame ?? throw new ArgumentNullException(nameof(currentFrame)); _physics = physics ?? throw new ArgumentNullException(nameof(physics)); _mode = mode ?? throw new ArgumentNullException(nameof(mode)); _player = player ?? throw new ArgumentNullException(nameof(player)); @@ -250,7 +247,7 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics LogNearbyCollisionObjects(position, drawn); } - _lines.Flush(camera.Camera.View, camera.Projection, _currentFrame.Current); + _lines.Flush(camera.Camera.View, camera.Projection); } private void LogNearbyCollisionObjects(Vector3 playerPosition, int drawn) diff --git a/src/AcDream.App/RuntimeOptions.cs b/src/AcDream.App/RuntimeOptions.cs index 4beeda4b..8324c504 100644 --- a/src/AcDream.App/RuntimeOptions.cs +++ b/src/AcDream.App/RuntimeOptions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Globalization; using System.IO; using AcDream.App.Rendering.Residency; @@ -15,19 +15,19 @@ namespace AcDream.App; /// /// /// -/// Scope: startup-time only — values that don't change +/// Scope: startup-time only — values that don't change /// once the window is up. Runtime diagnostic toggles /// (e.g. ACDREAM_DUMP_MOTION, ACDREAM_PROBE_*) belong in /// diagnostic owner classes (see AcDream.Core.Physics.PhysicsDiagnostics /// for the template), not here. /// /// -/// See docs/architecture/code-structure.md §2 Rule 4 for the -/// rule that drove this extraction, and §4 Step 1 for the broader +/// See docs/architecture/code-structure.md §2 Rule 4 for the +/// rule that drove this extraction, and §4 Step 1 for the broader /// extraction sequence this is the first cut of. /// /// -internal sealed record RuntimeOptions( +public sealed record RuntimeOptions( string DatDir, string PreparedAssetPath, bool LiveMode, diff --git a/src/AcDream.App/Spells/MagicRuntime.cs b/src/AcDream.App/Spells/MagicRuntime.cs index 14fd5163..100b8fd5 100644 --- a/src/AcDream.App/Spells/MagicRuntime.cs +++ b/src/AcDream.App/Spells/MagicRuntime.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using AcDream.Core.Items; using AcDream.Core.Spells; @@ -8,7 +8,7 @@ using AcDream.Content; namespace AcDream.App.Spells; /// One resolved formula cell in retail's spell examination subview. -internal readonly record struct SpellExamineComponent( +public readonly record struct SpellExamineComponent( uint SpellComponentId, SpellComponentDescriptor Descriptor, bool Owned); @@ -18,7 +18,7 @@ internal readonly record struct SpellExamineComponent( /// owner. The server owns turning, motion, fizzle, mana/component consumption, /// effects, and results. /// -internal sealed class MagicRuntime : IDisposable +public sealed class MagicRuntime : IDisposable { private readonly SpellComponentRequirementService _requirements; private IDisposable? _castOperationsBinding; diff --git a/src/AcDream.App/Streaming/DungeonStreamingGate.cs b/src/AcDream.App/Streaming/DungeonStreamingGate.cs index 048a0158..86aaeb5d 100644 --- a/src/AcDream.App/Streaming/DungeonStreamingGate.cs +++ b/src/AcDream.App/Streaming/DungeonStreamingGate.cs @@ -1,16 +1,16 @@ -namespace AcDream.App.Streaming; +namespace AcDream.App.Streaming; /// Result of the per-frame dungeon streaming-gate decision. -/// Passed to — collapse +/// Passed to — collapse /// streaming to the single dungeon landblock. /// When non-null, override the streaming observer to this /// landblock key (the cell id's high 16 bits, 0xXXYY). Null leaves the caller's observer as-is. -internal readonly record struct DungeonGateResult(bool InsideDungeon, uint? ObserverLandblockKey); +public readonly record struct DungeonGateResult(bool InsideDungeon, uint? ObserverLandblockKey); /// /// AP-36: the dungeon streaming gate (#133 FPS). When the player stands in a SEALED /// EnvCell (an indoor cell that doesn't see outside), streaming collapses to the single -/// dungeon landblock — AC dungeons have no adjacent landblocks, so the normal 25×25 +/// dungeon landblock — AC dungeons have no adjacent landblocks, so the normal 25×25 /// window would pull in ~129 unrelated ocean-grid dungeons. The trigger is the player's /// CURRENT cell (CellGraph.CurrCell, set the moment the player is placed), and the /// observer is pinned to that cell's OWN landblock (the cell id high 16 bits) because a @@ -19,7 +19,7 @@ internal readonly record struct DungeonGateResult(bool InsideDungeon, uint? Obse /// Extracted from GameWindow.OnUpdate as a pure function so the /// teleport-hold rule (below) is unit-testable without the GL/dat/network stack. /// -internal static class DungeonStreamingGate +public static class DungeonStreamingGate { /// /// Decide the streaming gate from the player's current cell. @@ -33,11 +33,11 @@ internal static class DungeonStreamingGate bool isTeleportHold, bool currCellIsSealedDungeon, uint currCellId) { // #145/#138: during a teleport hold the player is NOT yet placed, so CurrCell is - // the frozen SOURCE cell — where the player IS, not where they're going. Streaming + // the frozen SOURCE cell — where the player IS, not where they're going. Streaming // must follow the DESTINATION, which the PortalSpace observer pin already does, so // the source-cell gate is suppressed. Otherwise a teleport OUT of a dungeon keeps - // streaming collapsed on the source dungeon (CurrCell still sealed) → the outdoor - // destination never hydrates → the TAS transit holds 600 frames → force-snap to + // streaming collapsed on the source dungeon (CurrCell still sealed) → the outdoor + // destination never hydrates → the TAS transit holds 600 frames → force-snap to // ocean. A teleport INTO a dungeon is handled explicitly upstream by // StreamingController.PreCollapseToDungeon (and the controller's _collapsed latch // holds it through the hold), so suppressing the gate here doesn't regress it. diff --git a/src/AcDream.App/Streaming/GpuLandblockRetirement.cs b/src/AcDream.App/Streaming/GpuLandblockRetirement.cs index 26c1f92c..0cfee190 100644 --- a/src/AcDream.App/Streaming/GpuLandblockRetirement.cs +++ b/src/AcDream.App/Streaming/GpuLandblockRetirement.cs @@ -1,4 +1,4 @@ -using AcDream.Core.World; +using AcDream.Core.World; namespace AcDream.App.Streaming; @@ -8,7 +8,7 @@ namespace AcDream.App.Streaming; /// asynchronously from the state transition, so a failed renderer cleanup /// cannot keep the old landblock logically resident. /// -internal sealed record GpuLandblockRetirement( +public sealed record GpuLandblockRetirement( uint LandblockId, LandblockRetirementKind Kind, IReadOnlyList Entities); @@ -24,7 +24,7 @@ internal sealed record GpuWorldRecenterRetirement( int SpatialOperationCount, Exception? ObserverFailure); -internal enum LandblockRetirementKind +public enum LandblockRetirementKind { Full, NearLayer, diff --git a/src/AcDream.App/Streaming/GpuWorldState.cs b/src/AcDream.App/Streaming/GpuWorldState.cs index bcbc857d..5b2a32cd 100644 --- a/src/AcDream.App/Streaming/GpuWorldState.cs +++ b/src/AcDream.App/Streaming/GpuWorldState.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; @@ -56,7 +56,7 @@ internal sealed record GpuLandblockSpatialPublication( /// Threading: not thread-safe. All calls must happen on the render thread. /// /// -internal sealed class GpuWorldState : ILiveEntitySpatialQuery +public sealed class GpuWorldState : ILiveEntitySpatialQuery { private readonly LandblockSpawnAdapter? _wbSpawnAdapter; private readonly EntityScriptActivator? _entityScriptActivator; @@ -194,7 +194,7 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery && IsLiveEntityProjectionResident(key); /// - /// Try to grab the loaded record for a landblock — useful for callers + /// Try to grab the loaded record for a landblock — useful for callers /// that need to enumerate entities before the landblock is dropped /// (e.g. unregistering dynamic lights on a RemoveLandblock). /// @@ -1053,7 +1053,7 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery } /// - /// Mark a server-GUID as persistent — this entity survives landblock unloads + /// Mark a server-GUID as persistent — this entity survives landblock unloads /// and gets re-parked as pending for its current canonical landblock. /// public void MarkPersistent(uint serverGuid) @@ -1093,7 +1093,7 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery uint canonical = (newCanonicalLb & 0xFFFF0000u) | 0xFFFFu; - // Fast path: already drawn in the correct loaded bucket → nothing to do + // Fast path: already drawn in the correct loaded bucket → nothing to do // (avoids per-frame list churn for a settled, stationary entity). bool hasCurrent = _projectionLocations.TryGetValue( entity, @@ -1110,17 +1110,17 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery return; } - // Remove the entity from wherever it currently lives — a loaded bucket - // OR a pending bucket — then re-append to its current landblock. + // Remove the entity from wherever it currently lives — a loaded bucket + // OR a pending bucket — then re-append to its current landblock. // // The projection-location index is the 2026-07-03 fix for the cold-spawn / // run-out "invisible player" bug: a persistent (server-spawned) entity // that spawned into a not-yet-loaded landblock sits in _pendingByLandblock, - // and the old code scanned ONLY _loaded — so it silently no-op'd and left + // and the old code scanned ONLY _loaded — so it silently no-op'd and left // the player stranded, hidden, even after its landblock finished loading // (the AddLandblock pending-drain had already run empty before the churn // re-parked the player, and the player is excluded from the server-object - // re-hydrate — so RebucketLiveEntity was the ONLY path that could recover it, + // re-hydrate — so RebucketLiveEntity was the ONLY path that could recover it, // and it couldn't reach a pending entity). The index now reaches either // residency class in O(1). Re-appending routes the entity // to _loaded (drawn) when its landblock is loaded, or back to pending to @@ -1274,8 +1274,8 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery // landblock often hasn't streamed in yet, so the player lands in // _pendingByLandblock. If that same landblock is then unloaded (a // streaming churn / re-teleport before it finishes loading), the - // pending entry was silently dropped here — violating the - // "persistent ⇒ survives unload" contract and making the avatar + // pending entry was silently dropped here — violating the + // "persistent ⇒ survives unload" contract and making the avatar // vanish after a couple round-trips. Rescue them so DrainRescued // re-parks them at the next valid landblock. if (_pendingByLandblock.TryGetValue(canonical, out var pendingForLb)) @@ -1580,7 +1580,7 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery /// world withdrawal, or logical teardown. It owns no renderer/script /// lifecycle and is safe for a still-live incarnation. /// - /// Safe to call with a server guid that's not currently present — no-op. + /// Safe to call with a server guid that's not currently present — no-op. /// public void RemoveLiveEntityProjection(uint serverGuid) { @@ -1707,7 +1707,7 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery if (_loaded.ContainsKey(canonicalLandblockId)) { - // Hot path — append directly to the render-thread-owned mutable + // Hot path — append directly to the render-thread-owned mutable // resident bucket and flat view. AddLoadedProjection(canonicalLandblockId, entity, key); if (probePersistent) @@ -1715,7 +1715,7 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery return; } - // Cold path — landblock not yet loaded. Park the entity in the + // Cold path — landblock not yet loaded. Park the entity in the // pending bucket; AddLandblock will pick it up when the streamer // delivers the matching landblock. if (!_pendingByLandblock.TryGetValue(canonicalLandblockId, out var bucket)) @@ -1737,8 +1737,8 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery /// /// Drop all entities from a landblock without removing the terrain. Used - /// by two-tier streaming when a landblock crosses Near→Far hysteresis. - /// Per Phase A.5 spec §4.4. + /// by two-tier streaming when a landblock crosses Near→Far hysteresis. + /// Per Phase A.5 spec §4.4. /// /// /// Only dat-static entity layers demote. Live server projections retain @@ -1798,7 +1798,7 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery // for this landblock BEFORE we drop the entity list. The cache stores // canonical landblock ids (the dispatcher's _walkScratch carries // entry.LandblockId from GpuWorldState.LandblockEntries, whose keys are - // canonicalized). Null when the cache isn't wired (tests). Per spec §5.3 W3b. + // canonicalized). Null when the cache isn't wired (tests). Per spec §5.3 W3b. // C.1.5b: stop DefaultScript for each dat-hydrated entity about to // be dropped. Demote-tier entities are always atlas-tier (ServerGuid==0 @@ -1852,14 +1852,14 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery /// /// Merge entities into an existing-loaded landblock. Used by two-tier - /// streaming for the Far→Near promotion case (terrain already loaded; + /// streaming for the Far→Near promotion case (terrain already loaded; /// entity layer streaming in). Falls back to the pending bucket if the /// landblock isn't loaded yet (handles the rare "promote arrives before /// far load completes" race). - /// Per Phase A.5 spec §4.4. + /// Per Phase A.5 spec §4.4. /// /// - /// Landblock id is canonicalized (low 16 bits forced to 0xFFFF) — + /// Landblock id is canonicalized (low 16 bits forced to 0xFFFF) — /// callers may pass cell-resolved ids and they will key correctly. /// /// @@ -1913,7 +1913,7 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery if (!parkIfMissing) return null; - // Park as pending — same pattern as live projections for not-yet-loaded LBs. + // Park as pending — same pattern as live projections for not-yet-loaded LBs. if (!_pendingByLandblock.TryGetValue(canonical, out var bucket)) { bucket = new List(); @@ -2023,7 +2023,7 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery private readonly HashSet _persistentInFlatProbe = new(); // TEMP (#138-B): log when a persistent (player) entity enters/leaves the - // drawn flat view. Transition-gated → low volume (fires at teleport + // drawn flat view. Transition-gated → low volume (fires at teleport // boundaries, not every rebuild). Strip with EntityVanishProbe. private void ProbeFlatViewTransitions() { diff --git a/src/AcDream.App/Streaming/LandblockBuild.cs b/src/AcDream.App/Streaming/LandblockBuild.cs index 7f66c554..4a04929d 100644 --- a/src/AcDream.App/Streaming/LandblockBuild.cs +++ b/src/AcDream.App/Streaming/LandblockBuild.cs @@ -1,4 +1,4 @@ -using AcDream.App.Rendering.Wb; +using AcDream.App.Rendering.Wb; using AcDream.Core.World; namespace AcDream.App.Streaming; @@ -8,7 +8,7 @@ namespace AcDream.App.Streaming; /// until it posts the corresponding completion; the render thread then applies /// the landblock and its cell transaction together. /// -internal sealed record LandblockBuild( +public sealed record LandblockBuild( LoadedLandblock Landblock, EnvCellLandblockBuild? EnvCells = null, LandblockBuildOrigin Origin = default, diff --git a/src/AcDream.App/Streaming/LandblockBuildFactory.cs b/src/AcDream.App/Streaming/LandblockBuildFactory.cs index 25a13fc1..5e1e04cc 100644 --- a/src/AcDream.App/Streaming/LandblockBuildFactory.cs +++ b/src/AcDream.App/Streaming/LandblockBuildFactory.cs @@ -1,4 +1,4 @@ -using DatReaderWriter; +using DatReaderWriter; using AcDream.Content; namespace AcDream.App.Streaming; @@ -14,7 +14,7 @@ namespace AcDream.App.Streaming; /// CObjCell::init_objects (0x0052B420). /// See docs/architecture/worldbuilder-inventory.md. /// -internal sealed class LandblockBuildFactory +public sealed class LandblockBuildFactory { private readonly IDatReaderWriter _dats; private readonly IPreparedCollisionSource _preparedCollisions; @@ -51,7 +51,7 @@ internal sealed class LandblockBuildFactory /// /// ISSUE #54 (post-A.5): far-tier loads (kind == LoadFar) skip /// LandBlockInfo + scenery + interior hydration. They return only the - /// LandBlock heightmap dat record + an empty entity list — enough for + /// LandBlock heightmap dat record + an empty entity list — enough for /// terrain-mesh build on the next phase. Near-tier loads run the full /// path. This replaces Bug A's post-load entity strip in /// with an @@ -70,7 +70,7 @@ internal sealed class LandblockBuildFactory // gate through mesh/bounds hydration prevents another consumer from // interleaving reader cursor/cache state with this build. // tp-probe (2026-06-22, REMOVABLE): measure lock-WAIT (the _datLock - // contention signal — large only when the render thread is hammering the + // contention signal — large only when the render thread is hammering the // lock during a CreateObject flood) AND lock-HOLD (the intrinsic build // cost). Identical work in both branches; the probe branch only adds the // stopwatch + log. No behavior change when ProbeTeleportEnabled is false. @@ -123,7 +123,7 @@ internal sealed class LandblockBuildFactory { uint landblockId = request.LandblockId; - // ISSUE #54: far-tier early-out — heightmap only, empty entities. + // ISSUE #54: far-tier early-out — heightmap only, empty entities. // Skips the LandBlockInfo dat read AND all entity hydration (stabs // + buildings) AND the SceneryGenerator AND interior cells. Cuts // worker-thread cost per far-tier LB from ~tens of ms to a single @@ -203,7 +203,7 @@ internal sealed class LandblockBuildFactory } /// /// Phase A.1 Task 8: generate scenery (trees, rocks, bushes) for a single - /// landblock on the worker thread. Pure CPU — no GL calls. + /// landblock on the worker thread. Pure CPU — no GL calls. /// /// Ported from the pre-streaming preload loop in GameWindow.OnLoad /// (pre-Task-7 version, lines 329-405). Adapted to operate on a single @@ -305,7 +305,7 @@ internal sealed class LandblockBuildFactory float localX = spawn.LocalPosition.X; float localY = spawn.LocalPosition.Y; // Prefer the physics engine's terrain sampler (TerrainSurface.SampleZ) - // — it uses the same AC2D render split-direction formula the + // — it uses the same AC2D render split-direction formula the // TerrainModernRenderer uses for the visible terrain mesh. This // guarantees trees are placed on the SAME Z height the player // walks on. If physics hasn't registered this landblock yet, @@ -313,14 +313,14 @@ internal sealed class LandblockBuildFactory var worldPx = localX + lbOffset.X; var worldPy = localY + lbOffset.Y; // FIX (trees-in-sky, 2026-06-22): scenery ground-Z comes from THIS - // landblock's OWN heightmap — the same triangle-aware Z the player walks on + // landblock's OWN heightmap — the same triangle-aware Z the player walks on // (TerrainSurface.SampleZFromHeightmap, lock-step with physics per #48), // scoped to the landblock being built. The former global // _physicsEngine.SampleTerrainZ(worldPx) query was structurally racy: at // build time this landblock is NOT registered in physics yet, so that query - // could only return null (→ this same own-heightmap) or a STALE neighbor's - // height — the previous location's terrain before the full old-window - // recenter retirement converges — planting scenery at the old location's + // could only return null (→ this same own-heightmap) or a STALE neighbor's + // height — the previous location's terrain before the full old-window + // recenter retirement converges — planting scenery at the old location's // altitude (trees-in-sky, deltaZ up to +500m; confirmed via the // [scenery-z-stale] probe 2026-06-22). Own-heightmap is correct in every // case, so the global query is removed (also drops its per-spawn cost). @@ -335,7 +335,7 @@ internal sealed class LandblockBuildFactory if (_dumpSceneryZ) { // groundZ now always comes from THIS landblock's own heightmap (the - // global physics query was removed — see the trees-in-sky fix above). + // global physics query was removed — see the trees-in-sky fix above). string source = "heightmap"; foreach (var mr in meshRefs) { @@ -422,7 +422,7 @@ internal sealed class LandblockBuildFactory /// /// Phase A.1 Task 8: walk a landblock's EnvCells and produce (a) the cell /// room-mesh entity (Phase 7.1) for each EnvCell with an EnvironmentId, and - /// (b) a WorldEntity per StaticObject in each cell. Pure CPU — no GL calls. + /// (b) a WorldEntity per StaticObject in each cell. Pure CPU — no GL calls. /// /// Portal cells and drawable shell placements are accumulated in the /// transaction-local . The render thread @@ -448,28 +448,28 @@ internal sealed class LandblockBuildFactory (lbY - origin.CenterY) * 192f, 0f); - // Per-landblock id namespace — see AcDream.Core.World.InteriorEntityIdAllocator + // Per-landblock id namespace — see AcDream.Core.World.InteriorEntityIdAllocator // for the full bit layout + history. Distinct from scenery (0x80000000+) and // landblock stabs (0xC0000000+, ids from LandblockLoader). // // #119 ROOT-CAUSE FIX (2026-06-11): this used to be // `0x40000000 | (landblockId & 0x00FFFF00)`, which for landblock keys 0xXXYYFFFF - // resolves to 0x40YYFF00 — the landblock X byte DISCARDED. Every landblock in a + // resolves to 0x40YYFF00 — the landblock X byte DISCARDED. Every landblock in a // map Y-row produced the same id base, so interior statics collided across // landblocks (Holtburg town A9B3's 9th stab == the AAB3 tower's 43-part spiral // staircase, both 0x40B3FF09). The Tier-1 classification cache then served one // entity's batches to the other (the cache hint at bucket-draw time was the - // player's landblock, identical for both) — the session-sticky "broken stairs + + // player's landblock, identical for both) — the session-sticky "broken stairs + // water barrel". // - // #190 (2026-07-09): the fix above LEFT a documented residual — "counter overflow + // #190 (2026-07-09): the fix above LEFT a documented residual — "counter overflow // past 0xFF still bleeds into the lbY byte." That residual manifested for real: // the Town Network hub (205 cells, one landblock) reached 277 interior entities // after the #79/#93 A7.L1 light-carrier hydration fix, aliasing into the NEXT // landblock's Y-byte (entity script/particle tracking is keyed on entity.Id - // directly — EntityScriptActivator — with no landblock-hint disambiguation, so + // directly — EntityScriptActivator — with no landblock-hint disambiguation, so // the fountain's water-spray script silently stopped firing). Widened the - // counter budget 8→12 bits (256→4096); see InteriorEntityIdAllocator's doc for + // counter budget 8→12 bits (256→4096); see InteriorEntityIdAllocator's doc for // why this is safe (nothing decodes X/Y back out of an entity id). uint interiorLbX = (landblockId >> 24) & 0xFFu; uint interiorLbY = (landblockId >> 16) & 0xFFu; @@ -484,7 +484,7 @@ internal sealed class LandblockBuildFactory { // TEMP diagnostic (dat-race investigation 2026-06-09, strip with fix): // every id in [0x0100, 0x0100+NumCells) is derived from LandBlockInfo and - // MUST exist in the cell dat — a null here is always a read anomaly. + // MUST exist in the cell dat — a null here is always a read anomaly. Console.WriteLine($"[cell-miss] EnvCell 0x{envCellId:X8} null during interior hydration (NumCells={lbInfo.NumCells})"); continue; } @@ -498,7 +498,7 @@ internal sealed class LandblockBuildFactory { // TEMP diagnostic (dat-race investigation 2026-06-09, strip with fix): // a null Environment means this cell's WALLS are silently never - // registered while its static objects still draw — the exact + // registered while its static objects still draw — the exact // white-walls geometry signature. Console.WriteLine($"[cell-miss] Environment 0x{0x0D000000u | envCell.EnvironmentId:X8} null for EnvCell 0x{envCellId:X8} -> walls not registered"); } @@ -510,14 +510,14 @@ internal sealed class LandblockBuildFactory // drawable-geometry predicate; the actual shell placement is now owned // by this streaming job's EnvCellLandblockBuild transaction. // Static objects inside the cell continue to flow through the dispatcher - // as WorldEntity records below — they have real GfxObj MeshRefs that work + // as WorldEntity records below — they have real GfxObj MeshRefs that work // fine; EnvCellRenderer receives only the completed shell transaction. - // Transforms — needed by the portal-visibility cell (unlifted) AND the + // Transforms — needed by the portal-visibility cell (unlifted) AND the // render/physics path. Computed for EVERY cell with a valid cellStruct, // not just drawable ones. Keep the small render lift out of physics; retail // BSP contact planes use the EnvCell origin verbatim. The lift constant is // shared with every draw-space consumer of portal polygons (OutsideView - // gate, seal/punch fans) — PortalVisibilityBuilder.ShellDrawLiftZ (#130). + // gate, seal/punch fans) — PortalVisibilityBuilder.ShellDrawLiftZ (#130). var physicsCellOrigin = envCell.Position.Origin + lbOffset; var cellOrigin = physicsCellOrigin + new System.Numerics.Vector3( 0f, 0f, AcDream.App.Rendering.PortalVisibilityBuilder.ShellDrawLiftZ); @@ -532,9 +532,9 @@ internal sealed class LandblockBuildFactory // of whether CellMesh.Build produced drawable sub-meshes. A portals-only // pass-through connector (a ramp / stair / cellar mouth) yields 0 render // sub-meshes but MUST be in the visibility graph so the flood can traverse it - // to the cells beyond — otherwise the flood lookup-misses the unregistered + // to the cells beyond — otherwise the flood lookup-misses the unregistered // neighbour and the grey clear shows through the opening (#133: ramp - // neighbour 0x0007014D had 0 sub-meshes → unregistered → vis=1 grey barrier + // neighbour 0x0007014D had 0 sub-meshes → unregistered → vis=1 grey barrier // at the ramp; confirmed via [cellreg] registered=204/205 + [pv-trace] // skip=lookup-miss). Retail keeps the whole landblock cell array resident // before the flood runs; the cell-build transaction reads portals, NOT @@ -558,7 +558,7 @@ internal sealed class LandblockBuildFactory foreach (var stab in envCell.StaticObjects) { // #119 decisive probe: HYDRATE-side dump for ACDREAM_DUMP_ENTITY- - // targeted stabs. This is the MOMENT MeshRefs are constructed — + // targeted stabs. This is the MOMENT MeshRefs are constructed — // a degraded dat read here (setup null / placement frames short / // part GfxObj null) permanently corrupts the entity (H-A), and // nothing downstream ever rebuilds it. Inert when the set is empty. @@ -568,7 +568,7 @@ internal sealed class LandblockBuildFactory // #136: skip an EDITOR-ONLY placement marker. Such a dat object degrades to // nothing (GfxObj id 0) at any runtime distance, so retail's distance-based - // degrade (CPhysicsPart::UpdateViewerDistance) never draws it — only the + // degrade (CPhysicsPart::UpdateViewerDistance) never draws it — only the // WorldBuilder editor shows it at the origin. acdream's render path came from // WB (no distance LOD), so without this skip it draws the marker forever (the // red/green dungeon "cone"). Bare-GfxObj stabs are checked here; Setup stabs @@ -581,7 +581,7 @@ internal sealed class LandblockBuildFactory var interiorBounds = new AcDream.Core.Meshing.LocalBoundsAccumulator(); // #79/#93 (2026-07-09): a Setup-sourced stab whose sole visual part is a // runtime-hidden marker (#136) flattens to zero mesh refs even though its - // Setup carries real Lights — a "light attach point" fixture (e.g. the Town + // Setup carries real Lights — a "light attach point" fixture (e.g. the Town // Network fountain room's ceiling light, Setup 0x02000365). Track the dat // Setup's Lights.Count here so the meshRefs==0 gate below doesn't also drop // the entity that otherwise carries those lights to the static @@ -618,7 +618,7 @@ internal sealed class LandblockBuildFactory { // #136: skip an editor-only marker PART (retail hides it at runtime // distance). The #136 dungeon "cone" is Setup 0x02000C39 whose sole - // part GfxObj 0x010028CA is such a marker — skipping it empties + // part GfxObj 0x010028CA is such a marker — skipping it empties // meshRefs and the whole stab drops below. if (AcDream.Core.Meshing.GfxObjDegradeResolver.IsRuntimeHiddenMarker(_dats, mr.GfxObjId)) continue; @@ -652,7 +652,7 @@ internal sealed class LandblockBuildFactory // Stabs inside EnvCells are already in landblock-local coordinates // (same space as LandBlockInfo.Objects stabs). Adding cellOrigin would - // be wrong — see Phase 2d comment in the pre-streaming preload. + // be wrong — see Phase 2d comment in the pre-streaming preload. var worldPos = stab.Frame.Origin + lbOffset; var worldRot = stab.Frame.Orientation; diff --git a/src/AcDream.App/Streaming/LandblockBuildRequest.cs b/src/AcDream.App/Streaming/LandblockBuildRequest.cs index 16d14e69..315753f4 100644 --- a/src/AcDream.App/Streaming/LandblockBuildRequest.cs +++ b/src/AcDream.App/Streaming/LandblockBuildRequest.cs @@ -1,11 +1,11 @@ -namespace AcDream.App.Streaming; +namespace AcDream.App.Streaming; /// /// Immutable world-origin center captured by the update thread when a /// landblock load is admitted. Worker construction and render publication use /// this same value, so a later recenter cannot combine two coordinate frames. /// -internal readonly record struct LandblockBuildOrigin +public readonly record struct LandblockBuildOrigin { public LandblockBuildOrigin(int centerX, int centerY) { @@ -29,7 +29,7 @@ internal readonly record struct LandblockBuildOrigin /// is carried for matching and diagnostics only; /// residency policy remains owned by . /// -internal readonly record struct LandblockBuildRequest( +public readonly record struct LandblockBuildRequest( uint LandblockId, LandblockStreamJobKind Kind, ulong Generation, diff --git a/src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs b/src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs index c4163f52..169d912c 100644 --- a/src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs +++ b/src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using System.Numerics; using AcDream.Core.Physics; using AcDream.Core.World; @@ -12,7 +12,7 @@ namespace AcDream.App.Streaming; /// publication. The render publisher commits buildings and EnvCells between /// these stages without recomputing the captured origin. /// -internal sealed class LandblockPhysicsPublication +public sealed class LandblockPhysicsPublication { internal LandblockPhysicsPublication( object owner, @@ -72,7 +72,7 @@ internal sealed class LandblockPhysicsPublication /// Cumulative update-thread diagnostics for physics publication and removal. /// Durations are ticks. /// -internal readonly record struct LandblockPhysicsPublisherDiagnostics( +public readonly record struct LandblockPhysicsPublisherDiagnostics( long BeginCount, long CompleteCount, long BasePublishTicks, @@ -103,7 +103,7 @@ internal readonly record struct LandblockPhysicsPublisherDiagnostics( /// use one multipart shadow owner or the mutually exclusive Setup /// cylinder/sphere fallback. /// -internal sealed class LandblockPhysicsPublisher +public sealed class LandblockPhysicsPublisher { private readonly object _receiptOwner = new(); private readonly RuntimePhysicsState _physics; @@ -953,7 +953,7 @@ internal sealed class LandblockPhysicsPublisher string sampleText = string.Join(",", samples.Select( value => $"0x{value:X8}")); Console.WriteLine( - $" → {missingCount} scenery entities had no visual bounds cached. " + + $" → {missingCount} scenery entities had no visual bounds cached. " + $"Samples: {sampleText}"); } } diff --git a/src/AcDream.App/Streaming/LandblockPresentationPipeline.cs b/src/AcDream.App/Streaming/LandblockPresentationPipeline.cs index cef987b0..f5ae4f9d 100644 --- a/src/AcDream.App/Streaming/LandblockPresentationPipeline.cs +++ b/src/AcDream.App/Streaming/LandblockPresentationPipeline.cs @@ -1,4 +1,4 @@ -using AcDream.App.Rendering.Wb; +using AcDream.App.Rendering.Wb; using AcDream.App.Rendering.Scene; using AcDream.Core.Terrain; using AcDream.Core.World; @@ -10,7 +10,7 @@ namespace AcDream.App.Streaming; /// Keeping this projection on the pipeline prevents the composition root from /// retaining each concrete publication owner independently. /// -internal readonly record struct LandblockPresentationDiagnostics( +public readonly record struct LandblockPresentationDiagnostics( LandblockRenderPublisherDiagnostics Render, LandblockPhysicsPublisherDiagnostics Physics, LandblockStaticPresentationDiagnostics Statics); @@ -37,7 +37,7 @@ internal readonly record struct LandblockPublicationAdvance( /// graph; the internal delegate constructor exists only for hermetic policy and /// retry characterization tests. /// -internal sealed class LandblockPresentationPipeline +public sealed class LandblockPresentationPipeline { private enum PublicationKind : byte { diff --git a/src/AcDream.App/Streaming/LandblockPresentationRetirementOwner.cs b/src/AcDream.App/Streaming/LandblockPresentationRetirementOwner.cs index 7536fb1f..f9e0e937 100644 --- a/src/AcDream.App/Streaming/LandblockPresentationRetirementOwner.cs +++ b/src/AcDream.App/Streaming/LandblockPresentationRetirementOwner.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Lighting; +using AcDream.Core.Lighting; using AcDream.Core.Rendering; using AcDream.Core.World; @@ -16,7 +16,7 @@ namespace AcDream.App.Streaming; /// detaches spatial reachability first, then advances these concrete owners /// from the retained ticket without replaying successful stages or entities. /// -internal sealed class LandblockPresentationRetirementOwner +public sealed class LandblockPresentationRetirementOwner { private readonly LandblockRenderPublisher _render; private readonly LandblockPhysicsPublisher _physics; diff --git a/src/AcDream.App/Streaming/LandblockRenderPublisher.cs b/src/AcDream.App/Streaming/LandblockRenderPublisher.cs index aa54bb89..5578bc1a 100644 --- a/src/AcDream.App/Streaming/LandblockRenderPublisher.cs +++ b/src/AcDream.App/Streaming/LandblockRenderPublisher.cs @@ -1,4 +1,4 @@ -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using System.Diagnostics; using System.Numerics; using AcDream.App.Rendering; @@ -13,7 +13,7 @@ namespace AcDream.App.Streaming; /// origin between and /// . /// -internal sealed class LandblockRenderPublication +public sealed class LandblockRenderPublication { private readonly IReadOnlyDictionary _visibilityCells; @@ -64,7 +64,7 @@ internal sealed class LandblockRenderPublication /// Times use ticks so callers can aggregate without /// losing sub-millisecond precision. /// -internal readonly record struct LandblockRenderPublisherDiagnostics( +public readonly record struct LandblockRenderPublisherDiagnostics( long BeginCount, long CompleteCount, long TerrainPublishTicks, @@ -90,7 +90,7 @@ internal readonly record struct LandblockRenderPublisherDiagnostics( /// WorldBuilder one-job/one-EnvCell-transaction seam documented in /// docs/architecture/worldbuilder-inventory.md. /// -internal sealed class LandblockRenderPublisher +public sealed class LandblockRenderPublisher { private readonly object _receiptOwner = new(); private readonly Action _publishTerrain; diff --git a/src/AcDream.App/Streaming/LandblockRetirementCoordinator.cs b/src/AcDream.App/Streaming/LandblockRetirementCoordinator.cs index 460f63cd..82046fa1 100644 --- a/src/AcDream.App/Streaming/LandblockRetirementCoordinator.cs +++ b/src/AcDream.App/Streaming/LandblockRetirementCoordinator.cs @@ -1,9 +1,9 @@ -using AcDream.Core.World; +using AcDream.Core.World; namespace AcDream.App.Streaming; [Flags] -internal enum LandblockRetirementStage : ushort +public enum LandblockRetirementStage : ushort { None = 0, MeshReferences = 1 << 0, @@ -31,7 +31,7 @@ internal enum LandblockRetirementOperationResult : byte /// One retryable landblock-retirement ledger. Successful owner operations are /// committed once; a later attempt resumes at the exact failed owner/entity. /// -internal sealed class LandblockRetirementTicket +public sealed class LandblockRetirementTicket { private readonly Dictionary _entityCursors = new(); private readonly Dictionary _failures = new(); @@ -240,7 +240,7 @@ internal sealed class LandblockRetirementTicket /// owner callback cannot recursively replay its active stage or mutate the /// pending-ticket map while it is being enumerated. /// -internal sealed class LandblockRetirementCoordinator +public sealed class LandblockRetirementCoordinator { private enum BudgetedAdvanceResult : byte { diff --git a/src/AcDream.App/Streaming/LandblockStaticPresentationPublisher.cs b/src/AcDream.App/Streaming/LandblockStaticPresentationPublisher.cs index 8ba7c031..ac9e0121 100644 --- a/src/AcDream.App/Streaming/LandblockStaticPresentationPublisher.cs +++ b/src/AcDream.App/Streaming/LandblockStaticPresentationPublisher.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Lighting; +using AcDream.Core.Lighting; using AcDream.Core.Plugins; using AcDream.Core.Rendering; using AcDream.Core.World; @@ -11,7 +11,7 @@ namespace AcDream.App.Streaming; /// publication. The physics publisher calls the light/translucency stage at /// the existing per-object point immediately before ordinary collision. /// -internal sealed class LandblockStaticPresentationPublication +public sealed class LandblockStaticPresentationPublication { private readonly Dictionary _entities; private readonly Dictionary _snapshots; @@ -63,7 +63,7 @@ internal sealed class LandblockStaticPresentationPublication public IReadOnlyDictionary Snapshots => _snapshots; } -internal readonly record struct LandblockStaticPresentationDiagnostics( +public readonly record struct LandblockStaticPresentationDiagnostics( long BeginCount, long CompleteCount, long LightReplacementCount, @@ -87,7 +87,7 @@ internal readonly record struct LandblockStaticPresentationDiagnostics( /// logical spawn per retained static ID, while reapply only refreshes current /// state. /// -internal sealed class LandblockStaticPresentationPublisher +public sealed class LandblockStaticPresentationPublisher { private readonly object _receiptOwner = new(); private readonly LightingHookSink _lighting; diff --git a/src/AcDream.App/Streaming/LandblockStreamJob.cs b/src/AcDream.App/Streaming/LandblockStreamJob.cs index d69bd421..17017cd2 100644 --- a/src/AcDream.App/Streaming/LandblockStreamJob.cs +++ b/src/AcDream.App/Streaming/LandblockStreamJob.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using AcDream.Core.Terrain; using AcDream.Core.World; @@ -12,7 +12,7 @@ namespace AcDream.App.Streaming; /// public abstract record LandblockStreamJob(uint LandblockId) { - internal sealed record Load( + public sealed record Load( uint LandblockId, LandblockStreamJobKind Kind, ulong Generation = 0, @@ -21,7 +21,7 @@ public abstract record LandblockStreamJob(uint LandblockId) public LandblockBuildRequest Request => new(LandblockId, Kind, Generation, Origin); } - internal sealed record Unload( + public sealed record Unload( uint LandblockId, ulong Generation = 0) : LandblockStreamJob(LandblockId); @@ -30,10 +30,10 @@ public abstract record LandblockStreamJob(uint LandblockId) /// priority queues, keeping Unloads. Posted by /// when the player enters a /// dungeon and the in-flight outdoor/neighbor window load must be cancelled - /// (#133 FPS — dungeons have no adjacent landblocks). LandblockId is 0 by + /// (#133 FPS — dungeons have no adjacent landblocks). LandblockId is 0 by /// convention; readers pattern-match on the type. /// - internal sealed record ClearLoads() : LandblockStreamJob(0); + public sealed record ClearLoads() : LandblockStreamJob(0); } /// @@ -49,7 +49,7 @@ public abstract record LandblockStreamResult(uint LandblockId, ulong Generation) /// (terrain only) from Near (terrain + entities). /// is built off the render thread on the streaming worker. /// - internal sealed record Loaded( + public sealed record Loaded( uint LandblockId, LandblockStreamTier Tier, LandblockBuild Build, @@ -78,7 +78,7 @@ public abstract record LandblockStreamResult(uint LandblockId, ulong Generation) /// GpuWorldState still merges only the entity layer so live entities already /// attached to the landblock are preserved. /// - internal sealed record Promoted( + public sealed record Promoted( uint LandblockId, LandblockBuild Build, LandblockMeshData MeshData, @@ -98,20 +98,20 @@ public abstract record LandblockStreamResult(uint LandblockId, ulong Generation) public IReadOnlyList Entities => Landblock.Entities; } - internal sealed record Failed( + public sealed record Failed( uint LandblockId, string Error, ulong Generation = 0) : LandblockStreamResult(LandblockId, Generation); - internal sealed record Unloaded( + public sealed record Unloaded( uint LandblockId, ulong Generation = 0) : LandblockStreamResult(LandblockId, Generation); /// /// The worker loop itself crashed with an unhandled exception. Not tied - /// to a specific landblock — distinguished from + /// to a specific landblock — distinguished from /// because consumers typically route this to a fatal-log path rather /// than retrying a single landblock later. LandblockId is 0 by /// convention; readers should pattern-match on the type, not the id. /// - internal sealed record WorkerCrashed(string Error) : LandblockStreamResult(0, 0); + public sealed record WorkerCrashed(string Error) : LandblockStreamResult(0, 0); } diff --git a/src/AcDream.App/Streaming/LandblockStreamResultCost.cs b/src/AcDream.App/Streaming/LandblockStreamResultCost.cs index 4d6b9dc0..b0f01426 100644 --- a/src/AcDream.App/Streaming/LandblockStreamResultCost.cs +++ b/src/AcDream.App/Streaming/LandblockStreamResultCost.cs @@ -1,4 +1,4 @@ -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; using AcDream.App.Rendering.Wb; using AcDream.Core.Terrain; using AcDream.Core.World; @@ -12,7 +12,7 @@ namespace AcDream.App.Streaming; /// the managed heap. Exact array payloads and logical retained entries are /// charged consistently so admission has a deterministic unit. /// -internal readonly record struct LandblockStreamCostEstimate( +public readonly record struct LandblockStreamCostEstimate( StreamingWorkCost Work, long TerrainPayloadBytes, int Entities, @@ -26,7 +26,7 @@ internal readonly record struct LandblockStreamCostEstimate( int PhysicsSetups, int PhysicsGfxObjects); -internal static class LandblockStreamResultCost +public static class LandblockStreamResultCost { private const int ReferenceChargeBytes = 8; private const int DictionaryEntryChargeBytes = 16; diff --git a/src/AcDream.App/Streaming/LandblockStreamTier.cs b/src/AcDream.App/Streaming/LandblockStreamTier.cs index 5daf4393..c4a9e5d7 100644 --- a/src/AcDream.App/Streaming/LandblockStreamTier.cs +++ b/src/AcDream.App/Streaming/LandblockStreamTier.cs @@ -1,11 +1,11 @@ -namespace AcDream.App.Streaming; +namespace AcDream.App.Streaming; /// /// Streaming-tier classification for a landblock. means /// terrain mesh only; means terrain + scenery + EnvCells + -/// entity registration with the WB dispatcher. Per Phase A.5 spec §3. +/// entity registration with the WB dispatcher. Per Phase A.5 spec §3. /// -internal enum LandblockStreamTier +public enum LandblockStreamTier { Far, Near, @@ -15,7 +15,7 @@ internal enum LandblockStreamTier /// What work the streaming worker should perform for a given job. Distinct /// from because /// reads only the entity layer (terrain mesh already loaded), while -/// reads everything from scratch. Per Phase A.5 spec §4.3. +/// reads everything from scratch. Per Phase A.5 spec §4.3. /// public enum LandblockStreamJobKind { @@ -23,6 +23,6 @@ public enum LandblockStreamJobKind LoadFar, /// Read LandBlock + LandBlockInfo, generate scenery, build mesh, full entity layer. LoadNear, - /// Read LandBlockInfo + scenery only — terrain already loaded for this LB. + /// Read LandBlockInfo + scenery only — terrain already loaded for this LB. PromoteToNear, } diff --git a/src/AcDream.App/Streaming/LandblockStreamer.cs b/src/AcDream.App/Streaming/LandblockStreamer.cs index ed6fa5f8..234b90e1 100644 --- a/src/AcDream.App/Streaming/LandblockStreamer.cs +++ b/src/AcDream.App/Streaming/LandblockStreamer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Channels; @@ -28,12 +28,12 @@ namespace AcDream.App.Streaming; /// GameWindow's _datLock (Phase A.5 T10) serialises all /// DatCollection.Get<T> calls. Both factory closures passed at /// construction acquire that lock before reading dats. The worker never -/// touches DatCollection directly — it only calls the factories. +/// touches DatCollection directly — it only calls the factories. /// /// /// /// Unloads pass through the outbox as -/// records so the render thread can release GPU state on the next drain — +/// records so the render thread can release GPU state on the next drain — /// the streamer never touches GPU resources directly. /// /// @@ -43,7 +43,7 @@ namespace AcDream.App.Streaming; /// methods are thread-safe. /// /// -internal sealed class LandblockStreamer : IDisposable, ILandblockCompletionSource +public sealed class LandblockStreamer : IDisposable, ILandblockCompletionSource { /// /// Default drain batch size. Tuned to cap GPU upload work the render @@ -78,7 +78,7 @@ internal sealed class LandblockStreamer : IDisposable, ILandblockCompletionSourc { _loadLandblock = loadLandblock; _supportsRequestOrigin = supportsRequestOrigin; - // Default: no mesh build (returns null → Failed result). Production + // Default: no mesh build (returns null → Failed result). Production // wires in LandblockMesh.Build via the T12 construction site. _buildMeshOrNull = buildMeshOrNull ?? ((_, _) => null); _inbox = Channel.CreateUnbounded( @@ -131,7 +131,7 @@ internal sealed class LandblockStreamer : IDisposable, ILandblockCompletionSourc } /// - /// Back-compat overload — wraps a kind-agnostic factory so existing test code + /// Back-compat overload — wraps a kind-agnostic factory so existing test code /// that doesn't care about the JobKind branch keeps compiling. The wrapper /// ignores the kind and calls the factory once per LB regardless of tier. /// New production code should use . @@ -239,7 +239,7 @@ internal sealed class LandblockStreamer : IDisposable, ILandblockCompletionSourc /// control job which the worker /// honours at read time, dropping all pending Loads from both priority /// queues (Unloads survive). Used on the dungeon-entry edge to abort the - /// in-flight 25×25 neighbor window so the ~129 ocean-grid dungeons never + /// in-flight 25×25 neighbor window so the ~129 ocean-grid dungeons never /// finish loading (#133 FPS). Loads the worker has ALREADY dequeued still /// complete; the StreamingController's collapsed-sweep unloads those few. /// diff --git a/src/AcDream.App/Streaming/StreamingCompletionQueue.cs b/src/AcDream.App/Streaming/StreamingCompletionQueue.cs index eb440650..147767aa 100644 --- a/src/AcDream.App/Streaming/StreamingCompletionQueue.cs +++ b/src/AcDream.App/Streaming/StreamingCompletionQueue.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; namespace AcDream.App.Streaming; @@ -7,7 +7,7 @@ namespace AcDream.App.Streaming; /// channel. Peek plus read lets the update thread price a payload before /// adopting it into the scheduler. /// -internal interface ILandblockCompletionSource +public interface ILandblockCompletionSource { int BacklogCount { get; } bool TryPeek(out LandblockStreamResult? result); diff --git a/src/AcDream.App/Streaming/StreamingController.cs b/src/AcDream.App/Streaming/StreamingController.cs index 01ae537d..08f20cc8 100644 --- a/src/AcDream.App/Streaming/StreamingController.cs +++ b/src/AcDream.App/Streaming/StreamingController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using AcDream.App.Rendering.Wb; @@ -17,7 +17,7 @@ namespace AcDream.App.Streaming; /// Threading: not thread-safe. All calls must happen on the render thread. /// /// -internal sealed class StreamingController +public sealed class StreamingController : IStreamingFrameBackend, IWorldRevealStreamingScheduler { @@ -88,14 +88,14 @@ internal sealed class StreamingController // True while streaming is collapsed to the single dungeon landblock the // player stands in (the dungeon gate, #133 FPS). AC dungeons have NO - // adjacent landblocks — neighbors are unrelated ocean-grid dungeons that - // are never visible, so we stop loading the 25×25 window entirely. + // adjacent landblocks — neighbors are unrelated ocean-grid dungeons that + // are never visible, so we stop loading the 25×25 window entirely. private bool _collapsed; // The dungeon landblock id we collapsed onto. Once collapsed we key the // gate on this STABLE landblock, not the per-frame insideDungeon signal: // CurrCell can momentarily resolve to null/outdoor mid-frame, and gating - // expand on that flicker thrashes collapse↔expand (reload storms + a light + // expand on that flicker thrashes collapse↔expand (reload storms + a light // leak). We only expand when the observer actually moves to a different // landblock (teleport/portal out). private uint _collapsedCenter; @@ -610,16 +610,16 @@ internal sealed class StreamingController /// /// Advance one frame. / - /// are landblock coordinates (0..255) of the current viewer — the camera + /// are landblock coordinates (0..255) of the current viewer — the camera /// in offline mode, the server-sent player position in live. /// /// Two-tier model (Phase A.5 T13): /// - /// → enqueue LoadFar (terrain only, no entities) - /// → enqueue LoadNear (terrain + entities) - /// → enqueue PromoteToNear (entity layer for already-loaded terrain) - /// → drop entities on render thread immediately (terrain stays) - /// → enqueue full unload + /// → enqueue LoadFar (terrain only, no entities) + /// → enqueue LoadNear (terrain + entities) + /// → enqueue PromoteToNear (entity layer for already-loaded terrain) + /// → drop entities on render thread immediately (terrain stays) + /// → enqueue full unload /// /// public void Tick(int observerCx, int observerCy, bool insideDungeon = false) @@ -699,15 +699,15 @@ internal sealed class StreamingController if (_collapsed) { // Hysteresis. Cases: - // - Still in the SAME dungeon landblock → hold (sweep stragglers). + // - Still in the SAME dungeon landblock → hold (sweep stragglers). // - In a DIFFERENT dungeon cell (multi-landblock dungeon / new dungeon) - // → re-collapse onto it. + // → re-collapse onto it. // - CurrCell flickered null but the player hasn't gone anywhere: the // observer landblock reverts to the position-derived value, which for a // dungeon is only ever the ADJACENT off-by-one landblock (negative cell- - // local Y). Hold — never expand on an adjacent flicker. + // local Y). Hold — never expand on an adjacent flicker. // - Genuinely left to a DISTANT landblock (portal/teleport out, always far - // from the ocean-grid dungeon block) → expand. + // from the ocean-grid dungeon block) → expand. if (insideDungeon && centerId != _collapsedCenter) EnterDungeonCollapse(observerCx, observerCy, centerId); else if (!insideDungeon && ChebyshevLandblocks(centerId, _collapsedCenter) > 1) @@ -910,23 +910,23 @@ internal sealed class StreamingController /// /// #135: collapse to a single dungeon landblock IMMEDIATELY, before the first - /// has a chance to bootstrap the full 25×25 window. Called + /// has a chance to bootstrap the full 25×25 window. Called /// from the login / teleport spawn path the instant the streaming center is /// recentered onto a SEALED dungeon landblock. /// /// The per-frame insideDungeon gate keys on the physics - /// CurrCell, which is only set once the player is PLACED — and placement + /// CurrCell, which is only set once the player is PLACED — and placement /// waits for the dungeon landblock to hydrate. So for the whole hydration window /// (tens of seconds for a ~200-cell dungeon) the gate reads false and /// would enqueue the ~24 unrelated ocean-grid neighbor /// dungeons (+ ~19k entities each); the collapse then only mops them up after - /// placement. That mop-up is the 10→high FPS ramp users see at a dungeon login. + /// placement. That mop-up is the 10→high FPS ramp users see at a dungeon login. /// /// Pre-collapsing means the EXPENSIVE dungeon-neighbour window is never /// enqueued. On teleport nothing is enqueued at all (this fires before the next /// Tick recenters). On login a brief Holtburg outdoor window may be enqueued by the /// frame-1 NormalTick (before the player's spawn arrives) and is immediately - /// cancelled by _clearPendingLoads here — cheap outdoor terrain, not the + /// cancelled by _clearPendingLoads here — cheap outdoor terrain, not the /// ocean-grid dungeons, and a handful of already-dequeued loads get swept next /// frame. Idempotent: a no-op when already collapsed onto this same landblock, so a /// re-sent spawn or a same-frame double call costs nothing. Render-thread only, @@ -954,7 +954,7 @@ internal sealed class StreamingController } /// - /// Outdoor / building-interior streaming — the original two-tier model. + /// Outdoor / building-interior streaming — the original two-tier model. /// private void NormalTick(int observerCx, int observerCy) { @@ -1012,11 +1012,11 @@ internal sealed class StreamingController /// /// Dungeon-entry edge: cancel the in-flight window load, unload every /// resident neighbor, and pin streaming to the player's single dungeon - /// landblock. Retail-faithful — AC dungeons have no adjacent landblocks + /// landblock. Retail-faithful — AC dungeons have no adjacent landblocks /// (ACE LandblockManager.GetAdjacentIDs returns empty for a dungeon); - /// the 25×25 window was pulling in ~129 unrelated ocean-grid dungeons and + /// the 25×25 window was pulling in ~129 unrelated ocean-grid dungeons and /// their thousands of emitters (#133 FPS). Unloading them also tears down - /// their lights, shrinking the static-light set toward retail's ≤40. + /// their lights, shrinking the static-light set toward retail's ≤40. /// private void EnterDungeonCollapse(int cx, int cy, uint centerId) { @@ -1049,7 +1049,7 @@ internal sealed class StreamingController /// /// While collapsed, unload any landblock that finished loading after the - /// collapse edge — a Load the worker had already dequeued before the + /// collapse edge — a Load the worker had already dequeued before the /// control job took /// effect. At steady state only the dungeon landblock is resident, so this /// is a no-op. @@ -1057,7 +1057,7 @@ internal sealed class StreamingController private void SweepCollapsed() { // Always preserve the true dungeon landblock (_collapsedCenter), never the - // per-frame observer landblock — a CurrCell flicker must not unload the dungeon. + // per-frame observer landblock — a CurrCell flicker must not unload the dungeon. foreach (var id in _state.LoadedLandblockIds) if (id != _collapsedCenter) EnqueueUnload(id); } diff --git a/src/AcDream.App/Streaming/StreamingMutationException.cs b/src/AcDream.App/Streaming/StreamingMutationException.cs index 70424ab1..5c74dc88 100644 --- a/src/AcDream.App/Streaming/StreamingMutationException.cs +++ b/src/AcDream.App/Streaming/StreamingMutationException.cs @@ -1,11 +1,11 @@ -namespace AcDream.App.Streaming; +namespace AcDream.App.Streaming; /// /// Reports whether a streaming queue/retirement mutation crossed its commit /// point before a callback failed. Retry ledgers use this to avoid submitting /// the same generation operation twice after a post-commit diagnostic error. /// -internal sealed class StreamingMutationException : Exception +public sealed class StreamingMutationException : Exception { public StreamingMutationException( string message, diff --git a/src/AcDream.App/Streaming/StreamingRegion.cs b/src/AcDream.App/Streaming/StreamingRegion.cs index 66264d9b..1118489f 100644 --- a/src/AcDream.App/Streaming/StreamingRegion.cs +++ b/src/AcDream.App/Streaming/StreamingRegion.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; @@ -7,9 +7,9 @@ namespace AcDream.App.Streaming; /// /// Pure data type describing the set of landblocks currently considered /// "visible" by the streaming system. Given a center landblock (x, y) and -/// a radius, builds the set of landblock IDs in the (2r+1)×(2r+1) window. +/// a radius, builds the set of landblock IDs in the (2r+1)×(2r+1) window. /// -internal sealed class StreamingRegion +public sealed class StreamingRegion { public int CenterX { get; private set; } public int CenterY { get; private set; } @@ -17,7 +17,7 @@ internal sealed class StreamingRegion public int NearRadius { get; } public int FarRadius { get; } - // Strictly the (2r+1)×(2r+1) window (clamped to world bounds). + // Strictly the (2r+1)×(2r+1) window (clamped to world bounds). private readonly HashSet _visible = new(); // Everything currently loaded: window + hysteresis-retained landblocks. @@ -42,7 +42,7 @@ internal sealed class StreamingRegion /// LandblockLoader. /// /// - /// This set is strictly the (2r+1)×(2r+1) window; it does NOT include + /// This set is strictly the (2r+1)×(2r+1) window; it does NOT include /// hysteresis-retained landblocks outside the window. Use /// to enumerate everything actually loaded. /// @@ -89,7 +89,7 @@ internal sealed class StreamingRegion /// /// Encode a landblock at (lbX, lbY) into the AC dat id form. Always uses /// the 0xFFFF terminator (LandBlock = terrain). The earlier - /// version of this method used 0xFFFE by mistake — that's the + /// version of this method used 0xFFFE by mistake — that's the /// LandBlockInfo id, and asking LandblockLoader.Load to read a /// LandBlock at the LandBlockInfo coords corrupts the dat reader's /// buffer position, returning a half-populated LandBlock.Height[] @@ -135,7 +135,7 @@ internal sealed class StreamingRegion /// /// Call once after to seed /// _tierResidence with the initial window. Every LB in the inner - /// ring (Chebyshev ≤ NearRadius) is marked Near; everything else Far. + /// ring (Chebyshev ≤ NearRadius) is marked Near; everything else Far. /// public void MarkResidentFromBootstrap() { @@ -191,8 +191,8 @@ internal sealed class StreamingRegion } /// - /// Two-tier recenter: computes the 5-list diff per Phase A.5 spec §4.2. - /// Hysteresis: NearRadius+2 for Near→Far demote; FarRadius+2 for Far→null + /// Two-tier recenter: computes the 5-list diff per Phase A.5 spec §4.2. + /// Hysteresis: NearRadius+2 for Near→Far demote; FarRadius+2 for Far→null /// unload. Requires (or a prior /// call to this method) to have seeded _tierResidence. /// @@ -214,7 +214,7 @@ internal sealed class StreamingRegion var toDemote = new List(); var toUnload = new List(); - // Pass 1: walk new far window — emit ToLoadFar / ToLoadNear / ToPromote. + // Pass 1: walk new far window — emit ToLoadFar / ToLoadNear / ToPromote. var newCenterIds = new HashSet(); for (int dx = -FarRadius; dx <= FarRadius; dx++) { @@ -231,18 +231,18 @@ internal sealed class StreamingRegion if (!_tierResidence.TryGetValue(id, out var current)) { - // Not resident at all — fresh load. + // Not resident at all — fresh load. if (inNear) toLoadNear.Add(id); else toLoadFar.Add(id); _tierResidence[id] = inNear ? TierResidence.Near : TierResidence.Far; } else if (current == TierResidence.Far && inNear) { - // Was Far, now inside Near ring — promote. + // Was Far, now inside Near ring — promote. toPromote.Add(id); _tierResidence[id] = TierResidence.Near; } - // Near→Near and Far→Far are no-ops. + // Near→Near and Far→Far are no-ops. } } @@ -259,7 +259,7 @@ internal sealed class StreamingRegion if (newCenterIds.Contains(id)) { - // Still in the far window — only Near→Far demote possible here. + // Still in the far window — only Near→Far demote possible here. if (current == TierResidence.Near && (absDx > NearRadius || absDy > NearRadius)) { if (distance > nearUnloadThreshold) @@ -271,7 +271,7 @@ internal sealed class StreamingRegion continue; } - // Outside the new window — demote / unload by threshold. + // Outside the new window — demote / unload by threshold. if (current == TierResidence.Near) { if (distance > nearUnloadThreshold) @@ -336,7 +336,7 @@ internal sealed class StreamingRegion toUnload.Add(id); } - // Update resident: (oldResident ∪ newVisible) ∖ toUnload. + // Update resident: (oldResident ∪ newVisible) ∖ toUnload. _resident.UnionWith(_visible); foreach (var id in toUnload) _resident.Remove(id); @@ -352,7 +352,7 @@ internal sealed class StreamingRegion /// Both lists are disjoint from the current /// set; the caller hands them to LandblockStreamer as jobs. /// -internal readonly record struct RegionDiff( +public readonly record struct RegionDiff( IReadOnlyList ToLoad, IReadOnlyList ToUnload); diff --git a/src/AcDream.App/Streaming/StreamingWorkBudget.cs b/src/AcDream.App/Streaming/StreamingWorkBudget.cs index 68618945..1d04f352 100644 --- a/src/AcDream.App/Streaming/StreamingWorkBudget.cs +++ b/src/AcDream.App/Streaming/StreamingWorkBudget.cs @@ -1,11 +1,11 @@ -using System.Diagnostics; +using System.Diagnostics; namespace AcDream.App.Streaming; /// /// Immutable, validated per-frame streaming work profile. /// -internal readonly record struct StreamingWorkBudget +public readonly record struct StreamingWorkBudget { public StreamingWorkBudget( TimeSpan maxUpdateTime, @@ -75,7 +75,7 @@ internal readonly record struct StreamingWorkBudget /// /// Conservative, known cost of one atomic streaming operation. /// -internal readonly record struct StreamingWorkCost( +public readonly record struct StreamingWorkCost( int CompletionAdmissions = 0, long AdoptedCpuBytes = 0, int EntityOperations = 0, @@ -117,14 +117,14 @@ internal readonly record struct StreamingWorkCost( left > long.MaxValue - right ? long.MaxValue : left + right; } -internal enum StreamingWorkAdmission : byte +public enum StreamingWorkAdmission : byte { Admitted, OversizedProgress, Yielded, } -internal enum StreamingWorkLimit : byte +public enum StreamingWorkLimit : byte { None, Time, @@ -144,7 +144,7 @@ internal enum StreamingWorkLane : byte /// /// Immutable observation of one frame's streaming work. /// -internal readonly record struct StreamingWorkMeterSnapshot( +public readonly record struct StreamingWorkMeterSnapshot( StreamingWorkCost Used, StreamingWorkCost DestinationUsed, StreamingWorkCost NonDestinationUsed, @@ -163,7 +163,7 @@ internal readonly record struct StreamingWorkMeterSnapshot( /// /// Read-only streaming scheduler facts published to lifecycle artifacts. /// -internal readonly record struct StreamingWorkDiagnostics( +public readonly record struct StreamingWorkDiagnostics( StreamingWorkMeterSnapshot LastFrame, long LifetimeFrameOverrunCount, long LifetimeOversizedProgressCount, @@ -187,7 +187,7 @@ internal readonly record struct StreamingWorkDiagnostics( /// Single-thread, frame-scoped admission meter. Callers reserve a known cost /// before an atomic operation and complete or fail that reservation afterward. /// -internal sealed class StreamingWorkMeter +public sealed class StreamingWorkMeter { private readonly StreamingWorkBudget _budget; private readonly Func _timestamp; diff --git a/src/AcDream.App/Streaming/StreamingWorkBudgetOptions.cs b/src/AcDream.App/Streaming/StreamingWorkBudgetOptions.cs index d61532ce..edc4800c 100644 --- a/src/AcDream.App/Streaming/StreamingWorkBudgetOptions.cs +++ b/src/AcDream.App/Streaming/StreamingWorkBudgetOptions.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; namespace AcDream.App.Streaming; @@ -7,7 +7,7 @@ namespace AcDream.App.Streaming; /// These values limit work per frame; they never reduce the amount or /// quality of content that eventually becomes resident. /// -internal sealed record StreamingWorkBudgetOptions( +public sealed record StreamingWorkBudgetOptions( double MaxUpdateMilliseconds, int MaxCompletionAdmissions, long MaxAdoptedCpuBytes, diff --git a/src/AcDream.App/Streaming/TwoTierDiff.cs b/src/AcDream.App/Streaming/TwoTierDiff.cs index 7d08c9b9..2a24dab9 100644 --- a/src/AcDream.App/Streaming/TwoTierDiff.cs +++ b/src/AcDream.App/Streaming/TwoTierDiff.cs @@ -1,15 +1,15 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace AcDream.App.Streaming; /// /// Output of for the two-tier model. /// Five disjoint lists describe what changed since the previous Tick. Per -/// Phase A.5 spec §4.2. +/// Phase A.5 spec §4.2. /// -internal readonly record struct TwoTierDiff( +public readonly record struct TwoTierDiff( IReadOnlyList ToLoadFar, // entered far window from null (terrain only) - IReadOnlyList ToLoadNear, // entered near window from null (terrain + entities — first-tick or teleport) + IReadOnlyList ToLoadNear, // entered near window from null (terrain + entities — first-tick or teleport) IReadOnlyList ToPromote, // entered near window from far-resident (entities only) IReadOnlyList ToDemote, // exited near window past hysteresis (drop entities) IReadOnlyList ToUnload); // exited far window past hysteresis (drop terrain) diff --git a/src/AcDream.App/Streaming/WorldGenerationQuiescence.cs b/src/AcDream.App/Streaming/WorldGenerationQuiescence.cs index 41fa676c..5ebfd3e1 100644 --- a/src/AcDream.App/Streaming/WorldGenerationQuiescence.cs +++ b/src/AcDream.App/Streaming/WorldGenerationQuiescence.cs @@ -1,4 +1,4 @@ -using AcDream.App.Audio; +using AcDream.App.Audio; using AcDream.Core.Selection; using AcDream.Runtime.Entities; using AcDream.Runtime.World; @@ -10,7 +10,7 @@ namespace AcDream.App.Streaming; /// A hard login/portal reveal boundary makes the prior world unavailable /// immediately while its physical owners retire over later frames. /// -internal interface IWorldGenerationAvailability +public interface IWorldGenerationAvailability { bool IsWorldAvailable { get; } long QuiescedGeneration { get; } diff --git a/src/AcDream.App/Studio/DumpLayout.cs b/src/AcDream.App/Studio/DumpLayout.cs index 17279d2f..1f90f724 100644 --- a/src/AcDream.App/Studio/DumpLayout.cs +++ b/src/AcDream.App/Studio/DumpLayout.cs @@ -1,27 +1,27 @@ -using System.Numerics; +using System.Numerics; using AcDream.App.UI; namespace AcDream.App.Studio; -// ───────────────────────────────────────────────────────────────────────────── -// DumpLayout — load a panel from the retail UI layout dump +// ───────────────────────────────────────────────────────────────────────────── +// 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 +// 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: +// 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. -// ───────────────────────────────────────────────────────────────────────────── +// ───────────────────────────────────────────────────────────────────────────── /// /// Builds a static tree from the retail UI layout dump @@ -30,11 +30,11 @@ namespace AcDream.App.Studio; /// node's set to the dump's element_id and /// set to the widget_kind string. /// -/// This source is STATIC — no controllers, no FixtureProvider, no live +/// 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. /// -internal static class DumpLayout +public static class DumpLayout { /// /// Parse the dump at , find the panel whose slug @@ -42,7 +42,7 @@ internal static class DumpLayout /// /// /// maps a RenderSurface id (0x06xxxxxx) to a - /// (GL texture handle, native width, native height) triple — pass + /// (GL texture handle, native width, native height) triple — pass /// RenderStack.ResolveChrome from the studio, or a stub returning /// (1,1,1) for tests. /// @@ -52,10 +52,10 @@ internal static class DumpLayout public static UiElement? Load( string dumpPath, string slug, - Func resolve, + Func resolve, out string? error) { - // ── 1. Parse the dump JSON ──────────────────────────────────────── + // ── 1. Parse the dump JSON ──────────────────────────────────────── var dump = UiDumpModel.Parse(dumpPath); if (dump is null) { @@ -63,7 +63,7 @@ internal static class DumpLayout return null; } - // ── 2. Find the requested panel ─────────────────────────────────── + // ── 2. Find the requested panel ─────────────────────────────────── var panel = dump.Panels.FirstOrDefault( p => string.Equals(p.Slug, slug, StringComparison.OrdinalIgnoreCase)); if (panel is null) @@ -79,12 +79,12 @@ internal static class DumpLayout return null; } - // ── 3. Build a traversal-index → node lookup ────────────────────── + // ── 3. Build a traversal-index → node lookup ────────────────────── var byIndex = new Dictionary(panel.Nodes.Count); foreach (var n in panel.Nodes) byIndex[n.TraversalIndex] = n; - // ── 4. Create UiElement objects for every node ──────────────────── + // ── 4. Create UiElement objects for every node ──────────────────── var elements = new Dictionary(panel.Nodes.Count); foreach (var node in panel.Nodes) { @@ -92,7 +92,7 @@ internal static class DumpLayout elements[node.TraversalIndex] = el; } - // ── 5. Wire parent–child relationships + set parent-relative coords ─ + // ── 5. Wire parent–child relationships + set parent-relative coords ─ UiElement? root = null; foreach (var node in panel.Nodes) { @@ -100,7 +100,7 @@ internal static class DumpLayout if (node.ParentTraversalIndex is null) { - // Root node — place at (0,0) so the tree sits at the UiHost origin. + // 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; @@ -109,7 +109,7 @@ internal static class DumpLayout } else { - // Non-root: convert absolute → parent-relative by subtracting parent rect. + // 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 @@ -137,11 +137,11 @@ internal static class DumpLayout return root; } - // ── Private helpers ─────────────────────────────────────────────────────── + // ── Private helpers ─────────────────────────────────────────────────────── private static UiElement BuildElement( DumpNode node, - Func resolve) + Func resolve) { uint imageId = UiDumpModel.PickImageId(node); var kind = node.WidgetKind ?? "Group"; @@ -149,7 +149,7 @@ internal static class DumpLayout UiElement el; if (imageId != 0 && !string.Equals(kind, "Group", StringComparison.OrdinalIgnoreCase)) { - // Sprite/Button/Scrollbar/Slider — create a sprite-drawing element. + // Sprite/Button/Scrollbar/Slider — create a sprite-drawing element. el = new DumpSpriteElement(imageId, resolve) { Name = kind, @@ -159,7 +159,7 @@ internal static class DumpLayout } else { - // Group (or sprite without an image) — plain container, no own draw. + // Group (or sprite without an image) — plain container, no own draw. el = new DumpGroupElement() { Name = kind, @@ -168,7 +168,7 @@ internal static class DumpLayout }; } - // EventId is set from the dump's element_id (cast to uint — the decimal + // 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 @@ -180,27 +180,27 @@ internal static class DumpLayout } } -// ───────────────────────────────────────────────────────────────────────────── -// DumpSpriteElement — minimal element that draws a single sprite -// ───────────────────────────────────────────────────────────────────────────── +// ───────────────────────────────────────────────────────────────────────────── +// DumpSpriteElement — minimal element that draws a single sprite +// ───────────────────────────────────────────────────────────────────────────── /// /// Draws a single sprite at its native size tiled to fill -/// × . Used for Sprite/Button/Scrollbar/Slider nodes from +/// × . Used for Sprite/Button/Scrollbar/Slider nodes from /// the retail UI dump. /// /// We do NOT reuse here because /// that class requires an ElementInfo with a populated StateMedia -/// dictionary — the dat-import plumbing — which is not needed for a static dump +/// 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. /// internal sealed class DumpSpriteElement : UiElement { private readonly uint _imageId; - private readonly Func _resolve; + private readonly Func _resolve; - public DumpSpriteElement(uint imageId, Func resolve) + public DumpSpriteElement(uint imageId, Func resolve) { _imageId = imageId; _resolve = resolve; @@ -211,25 +211,25 @@ internal sealed class DumpSpriteElement : UiElement if (_imageId == 0) return; var (tex, tw, th) = _resolve(_imageId); - if (!tex.IsAssigned || tw == 0 || th == 0) return; + if (tex == 0 || tw == 0 || th == 0) return; - // Tile at native resolution (same as UiDatElement.OnDraw — UV-repeat on both + // 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) -// ───────────────────────────────────────────────────────────────────────────── +// ───────────────────────────────────────────────────────────────────────────── +// DumpGroupElement — pure container (Group nodes from the dump) +// ───────────────────────────────────────────────────────────────────────────── /// -/// Container element for dump Group nodes — no own draw, just hosts children. +/// 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. /// internal sealed class DumpGroupElement : UiElement { - // No OnDraw — completely transparent container. + // No OnDraw — completely transparent container. } diff --git a/src/AcDream.App/Studio/FixtureProvider.cs b/src/AcDream.App/Studio/FixtureProvider.cs index 8489e688..eb375b40 100644 --- a/src/AcDream.App/Studio/FixtureProvider.cs +++ b/src/AcDream.App/Studio/FixtureProvider.cs @@ -1,4 +1,4 @@ -using AcDream.App.Rendering; +using AcDream.App.Rendering; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Core.Combat; @@ -15,7 +15,7 @@ namespace AcDream.App.Studio; /// controller Bind methods against . /// /// -/// The studio is intentionally thin — there is no live game session, no +/// 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 @@ -23,7 +23,7 @@ namespace AcDream.App.Studio; /// /// /// -/// IconIds approach: raw-resolve stub — resolve the base iconId +/// IconIds approach: raw-resolve stub — resolve the base iconId /// via and return the GL handle /// directly. This is intentionally simpler than GameWindow's full /// (5-layer composite). The raw icon is enough @@ -31,7 +31,7 @@ namespace AcDream.App.Studio; /// compositor is the live-game concern, not the layout preview concern. /// /// -internal static class FixtureProvider +public static class FixtureProvider { /// /// Populate with sample data appropriate for @@ -131,7 +131,7 @@ internal static class FixtureProvider { // 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). + // 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); @@ -177,10 +177,10 @@ internal static class FixtureProvider return new RetainedPanelControllerGroup(inventory, paperdoll); } - case 0x2100002Eu: // gmStatManagementUI — Attributes/Skills/Titles window (LayoutDesc 0x2100002E) + 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). + // 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( @@ -192,12 +192,12 @@ internal static class FixtureProvider return null; default: - // Unknown layout — no-op; the panel renders structurally. + // Unknown layout — no-op; the panel renders structurally. return null; } } - // ── Helpers ───────────────────────────────────────────────────────────── + // ── Helpers ───────────────────────────────────────────────────────────── /// /// Build the iconIds delegate for toolbar / inventory controllers. @@ -206,15 +206,15 @@ internal static class FixtureProvider /// Raw-resolve stub: resolve the base (arg 2) /// via 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. + /// for the studio — a single-layer icon is sufficient for layout preview. /// /// /// This is what the task spec calls "v1 raw-resolve stub". /// - private static Func MakeIconIds(RenderStack stack) + private static Func MakeIconIds(RenderStack stack) => (_, iconId, _, _, _) => { - if (iconId == 0u) return GpuTextureSlot.Unassigned; + if (iconId == 0u) return 0u; var (handle, _, _) = stack.ResolveChrome(iconId); return handle; }; diff --git a/src/AcDream.App/Studio/LayoutSource.cs b/src/AcDream.App/Studio/LayoutSource.cs index ba61f2b8..78e53383 100644 --- a/src/AcDream.App/Studio/LayoutSource.cs +++ b/src/AcDream.App/Studio/LayoutSource.cs @@ -1,4 +1,4 @@ -using AcDream.App.UI; +using AcDream.App.UI; using AcDream.App.UI.Layout; using DatReaderWriter; using AcDream.Content; @@ -6,21 +6,21 @@ using AcDream.Content; namespace AcDream.App.Studio; /// Which kind of source the studio is currently previewing. -internal enum LayoutSourceKind { DatLayout, Markup } +public enum LayoutSourceKind { DatLayout, Markup } /// /// 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). +/// a LayoutDesc dat id, or a KSML markup file path (Task 6 — unsupported now). /// /// Call with the current to /// import the layout and get the root . The result is also /// cached in so can re-run the same /// source without re-reading the options. /// -internal sealed class LayoutSource +public sealed class LayoutSource { private readonly IDatReaderWriter _dats; - private readonly Func _resolve; + private readonly Func _resolve; private readonly UiDatFont? _datFont; private readonly Func? _fontResolve; @@ -33,17 +33,17 @@ internal sealed class LayoutSource /// /// Create a LayoutSource. /// - /// Optional per-element font resolver: FontDid → + /// Optional per-element font resolver: FontDid → /// (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 global. Controllers that /// explicitly set after /// still override the build-time value. - /// Pass null (default) for the original single-font behavior — the live + /// Pass null (default) for the original single-font behavior — the live /// path passes null so it is provably unchanged. public LayoutSource( IDatReaderWriter dats, - Func resolve, + Func resolve, UiDatFont? datFont, Func? fontResolve = null) { @@ -107,7 +107,7 @@ internal sealed class LayoutSource return LoadDat(LayoutId.Value); } - // ── Private ────────────────────────────────────────────────────────────────── + // ── Private ────────────────────────────────────────────────────────────────── private UiElement? LoadDat(uint layoutId) { diff --git a/src/AcDream.App/Studio/PanelFbo.cs b/src/AcDream.App/Studio/PanelFbo.cs index d8295aa1..4f61ec55 100644 --- a/src/AcDream.App/Studio/PanelFbo.cs +++ b/src/AcDream.App/Studio/PanelFbo.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; using AcDream.App.Rendering.Wb; using AcDream.App.UI; @@ -19,11 +19,11 @@ namespace AcDream.App.Studio; /// 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. /// -internal sealed unsafe class PanelFbo : IDisposable +public sealed unsafe class PanelFbo : IDisposable { private readonly GL _gl; - // Off-screen target — lazily (re)created when the requested size changes. + // Off-screen target — lazily (re)created when the requested size changes. private uint _fbo; private uint _colorTex; private uint _depthRbo; @@ -37,7 +37,7 @@ internal sealed unsafe class PanelFbo : IDisposable /// /// Render (a full draw pass) into a - /// private FBO at × pixels. + /// private FBO at × pixels. /// Returns the GL color texture handle (0 on failure). The texture is valid until /// the next call to with a different size, or until . /// @@ -50,7 +50,7 @@ internal sealed unsafe class PanelFbo : IDisposable // 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. + // — set up by BeginFrame and expected intact by Render — is untouched. using var scope = new GLStateScope(_gl); _gl.BindFramebuffer(FramebufferTarget.Framebuffer, _fbo); @@ -95,7 +95,7 @@ internal sealed unsafe class PanelFbo : IDisposable return buf; } - // ── FBO lifecycle (mirrors PaperdollViewportRenderer.EnsureFramebuffer) ────── + // ── FBO lifecycle (mirrors PaperdollViewportRenderer.EnsureFramebuffer) ────── private void EnsureFramebuffer(int width, int height) { diff --git a/src/AcDream.App/Studio/SampleData.cs b/src/AcDream.App/Studio/SampleData.cs index 2422ba59..e523fc8e 100644 --- a/src/AcDream.App/Studio/SampleData.cs +++ b/src/AcDream.App/Studio/SampleData.cs @@ -1,4 +1,4 @@ -using AcDream.App.UI.Layout; +using AcDream.App.UI.Layout; using AcDream.Core.Items; namespace AcDream.App.Studio; @@ -25,18 +25,18 @@ namespace AcDream.App.Studio; /// Equipped chest armor : 0x060011CFu /// Equipped melee weapon : 0x060011CBu /// -/// These are the icon *base* RenderSurface ids — the same ids GameWindow passes +/// These are the icon *base* RenderSurface ids — the same ids GameWindow passes /// as `iconId` into the iconIds lambda. FixtureProvider resolves them via /// and returns the raw GL handle. /// -internal static class SampleData +public static class SampleData { - // ── Guids ──────────────────────────────────────────────────────────────── + // ── Guids ──────────────────────────────────────────────────────────────── /// Fake server guid for the studio's synthetic player. public const uint PlayerGuid = 0x50000001u; - // Items in main pack (slots 0–5). + // Items in main pack (slots 0–5). private const uint SwordGuid = 0x50000010u; private const uint ChestGuid = 0x50000011u; private const uint GlovesGuid = 0x50000012u; @@ -53,7 +53,7 @@ internal static class SampleData private const uint ChestEqGuid = 0x50000031u; private const uint WeaponEqGuid = 0x50000032u; - // ── Icon ids (0x06xxxxxx RenderSurface dat ids) ─────────────────────────── + // ── 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 @@ -62,7 +62,7 @@ internal static class SampleData private const uint IconJewelry = 0x060011D5u; // jewelry underlay private const uint IconMisc = 0x060011D4u; // misc / fallback underlay - // ── Public API ────────────────────────────────────────────────────────── + // ── Public API ────────────────────────────────────────────────────────── /// /// Build a fresh populated with the @@ -74,7 +74,7 @@ internal static class SampleData { var t = new ClientObjectTable(); - // ── Player object ───────────────────────────────────────────────── + // ── Player object ───────────────────────────────────────────────── t.AddOrUpdate(new ClientObject { ObjectId = PlayerGuid, @@ -89,7 +89,7 @@ internal static class SampleData AetheriaUnlocks.PropertyId, (int)AetheriaUnlockState.All); - // ── Loose items in main pack (slots 0–5) ────────────────────────── + // ── 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); @@ -98,14 +98,14 @@ internal static class SampleData 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) ───────── + // ── 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) ── + // ── 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); @@ -114,13 +114,13 @@ internal static class SampleData return t; } - // ── Sample vital constants (used by FixtureProvider) ──────────────────── + // ── 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) ─── + // ── Sample character sheet (used by CharacterController in the Studio) ─── /// /// Returns a representative for the studio's @@ -145,12 +145,12 @@ internal static class SampleData XpToNextLevel = 42_000_000, XpFraction = 0.63f, - // Vitals: retail screenshot spec (Pass 1 acceptance criteria §Goal). + // 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). + // Attributes: Strength + Quickness = 200; all others = 10 (retail screenshot spec §Goal). Strength = 200, Endurance = 10, Quickness = 200, @@ -171,9 +171,9 @@ internal static class SampleData // 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). + // 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 }, @@ -207,7 +207,7 @@ internal static class SampleData BurdenMax = 4500, }; - // ── Helpers ───────────────────────────────────────────────────────────── + // ── Helpers ───────────────────────────────────────────────────────────── private static void AddItem( ClientObjectTable t, diff --git a/src/AcDream.App/Studio/StudioInspector.cs b/src/AcDream.App/Studio/StudioInspector.cs index 76f2f9a9..26e6dc0d 100644 --- a/src/AcDream.App/Studio/StudioInspector.cs +++ b/src/AcDream.App/Studio/StudioInspector.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.App.UI; using ImGuiNET; @@ -8,7 +8,7 @@ namespace AcDream.App.Studio; /// All canvas mouse events gathered by in one frame. /// All coordinates are already mapped to panel-local pixels (origin top-left, same as UiRoot). /// -internal readonly struct CanvasInputEvent +public readonly struct CanvasInputEvent { /// Mouse is currently hovering the canvas image. When false all other fields are 0 / false. public readonly bool IsHovered; @@ -37,13 +37,13 @@ internal readonly struct CanvasInputEvent /// /// Four-pane ImGui IDE for the acdream UI Studio: /// -/// Toolbar — panel picker (slug combo) across the top. -/// Canvas — shows the panel FBO texture; in Interact mode mouse events +/// Toolbar — panel picker (slug combo) across the top. +/// Canvas — 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. -/// Tree — recursive ImGui tree of the element hierarchy; clicking a node +/// Tree — recursive ImGui tree of the element hierarchy; clicking a node /// sets . -/// Properties — shows the element's geometry, +/// Properties — shows the element's geometry, /// anchors, and z-order. /// /// @@ -52,20 +52,20 @@ internal readonly struct CanvasInputEvent /// sub-window. After ImGui.Image we call ImGui.GetItemRectMin() 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 +/// panel-local pixels directly — no additional scale factor is needed because the image is /// drawn 1:1. /// -/// V-flip — no extra Y inversion needed: +/// V-flip — no extra Y inversion needed: /// The FBO origin is bottom-left (GL convention), so we pass uv0=(0,1), uv1=(1,0) to /// ImGui.Image 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. +/// without further inversion — do NOT flip Y again. /// /// Layout: the four panes call SetNextWindowPos + SetNextWindowSize /// with ImGuiCond.FirstUseEver so they start docked but can be freely dragged. /// -internal sealed class StudioInspector +public sealed class StudioInspector { /// Currently selected element (set by tree-click or canvas-click in Inspect mode). public UiElement? Selected { get; set; } @@ -77,7 +77,7 @@ internal sealed class StudioInspector /// public bool InteractMode { get; set; } = true; - // ── Toolbar ─────────────────────────────────────────────────────────────────── + // ── Toolbar ─────────────────────────────────────────────────────────────────── /// /// Draw the "Studio" toolbar window (top strip) containing a slug combo-box and @@ -124,7 +124,7 @@ internal sealed class StudioInspector return result; } - // ── Canvas ──────────────────────────────────────────────────────────────────── + // ── Canvas ──────────────────────────────────────────────────────────────────── /// /// Draw the "Canvas" ImGui window containing the panel FBO texture and return all @@ -133,13 +133,13 @@ internal sealed class StudioInspector /// Coordinate mapping: After ImGui.Image, GetItemRectMin() /// 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 + /// mouse position gives panel-local pixels directly — no scale factor because the /// image is drawn 1:1. /// - /// V-flip — no extra Y inversion: we pass uv0=(0,1) / uv1=(1,0) so the + /// V-flip — no extra Y inversion: 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. + /// top-left origin — do NOT flip Y again. /// /// If is non-null a bright-green 2-pixel outline is /// drawn over it using the window draw list. @@ -163,7 +163,7 @@ internal sealed class StudioInspector // This is what lets us translate raw mouse screen coords into panel-local pixels. var rectMin = ImGui.GetItemRectMin(); - // ── Selection highlight ─────────────────────────────────────────────── + // ── Selection highlight ─────────────────────────────────────────────── var el = Selected; if (el is not null && el.Width > 0f && el.Height > 0f) { @@ -176,7 +176,7 @@ internal sealed class StudioInspector 0f, ImDrawFlags.None, 2f); } - // ── Gather canvas mouse events ──────────────────────────────────────── + // ── 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; @@ -187,7 +187,7 @@ internal sealed class StudioInspector { 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. + // 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). @@ -202,7 +202,7 @@ internal sealed class StudioInspector } else { - // Mouse is over ImGui chrome (title bar, padding) adjacent to image — not over the panel. + // Mouse is over ImGui chrome (title bar, padding) adjacent to image — not over the panel. hovered = false; } } @@ -211,7 +211,7 @@ internal sealed class StudioInspector return new CanvasInputEvent(hovered, mx, my, leftDown, leftUp, scroll); } - // ── Tree ────────────────────────────────────────────────────────────────────── + // ── Tree ────────────────────────────────────────────────────────────────────── /// Draw the "Tree" ImGui window. Clicking a node sets . public void DrawTree(UiElement root, int windowX, int windowY, int windowW, int windowH) @@ -252,7 +252,7 @@ internal sealed class StudioInspector } } - // ── Properties ─────────────────────────────────────────────────────────────── + // ── Properties ─────────────────────────────────────────────────────────────── /// Draw the "Properties" ImGui window for . public void DrawProperties(int windowX, int windowY, int windowW, int windowH) diff --git a/src/AcDream.App/Studio/StudioOptions.cs b/src/AcDream.App/Studio/StudioOptions.cs index 1af8c075..8e2a3c2e 100644 --- a/src/AcDream.App/Studio/StudioOptions.cs +++ b/src/AcDream.App/Studio/StudioOptions.cs @@ -1,11 +1,11 @@ -namespace AcDream.App.Studio; +namespace AcDream.App.Studio; /// /// Parsed options for the acdream UI Studio standalone tool. /// Constructed by from the command-line tokens that follow /// the ui-studio dispatch token. /// -internal sealed record StudioOptions( +public sealed record StudioOptions( string DatDir, uint? LayoutId, string? MarkupPath, @@ -24,11 +24,11 @@ internal sealed record StudioOptions( /// --layout 0xNNNN: hex LayoutDesc dat id to preview. /// --markup <path>: path to a KSML markup file (Task 6, unsupported for now). /// --dump <slug>: load a panel from the retail UI dump JSON by slug - /// (e.g. inventory, radar, toolbar). Static mockup — no controllers. + /// (e.g. inventory, radar, toolbar). Static mockup — no controllers. /// --dump-file <path>: override the default dump file path /// (docs/research/2026-06-25-retail-ui-layout-dump.json from the solution root). /// Only meaningful when --dump is also given. - /// --screenshot <path>: headless mode — render the loaded panel to a PNG + /// --screenshot <path>: headless mode — render the loaded panel to a PNG /// at and exit without showing an interactive window. /// Combines with --dump or --layout. /// When neither --layout, --markup, nor --dump is given the @@ -96,7 +96,7 @@ internal sealed record StudioOptions( if (string.IsNullOrWhiteSpace(datDir)) throw new InvalidOperationException( - "ui-studio: dat directory required — pass as first arg or set ACDREAM_DAT_DIR."); + "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) diff --git a/src/AcDream.App/Studio/StudioWindow.cs b/src/AcDream.App/Studio/StudioWindow.cs index 4817f943..be43d853 100644 --- a/src/AcDream.App/Studio/StudioWindow.cs +++ b/src/AcDream.App/Studio/StudioWindow.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.Content; using AcDream.App.Platform; using AcDream.App.Audio; @@ -26,18 +26,18 @@ namespace AcDream.App.Studio; /// /// Task 3 adds an ImGui IDE on top of the panel FBO: /// -/// Canvas pane — the panel rendered off-screen via . -/// Tree pane — the element hierarchy; click-to-select. -/// Properties pane — geometry/anchors/flags of the selected element. -/// Click-to-inspect — a left-click in the canvas selects the topmost +/// Canvas pane — the panel rendered off-screen via . +/// Tree pane — the element hierarchy; click-to-select. +/// Properties pane — geometry/anchors/flags of the selected element. +/// Click-to-inspect — a left-click in the canvas selects the topmost /// element under the cursor via . /// /// -/// The window is intentionally thin: no game world, no physics, no streaming — +/// 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 GameWindow. /// -internal sealed class StudioWindow : IDisposable +public sealed class StudioWindow : IDisposable { private readonly StudioOptions _opts; private readonly ApplicationPathSet _applicationPaths; @@ -63,7 +63,7 @@ internal sealed class StudioWindow : IDisposable private string? _dumpFile; // resolved dump file path (once, in OnLoad) private IReadOnlyList _dumpSlugs = Array.Empty(); // all slugs from the dump - // Task 4: sample data table — built once in OnLoad and kept alive for the window's lifetime + // 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; @@ -114,7 +114,7 @@ internal sealed class StudioWindow : IDisposable { _platformServices.ConfigureWindowBackend(); // Resolve quality settings the same way GameWindow.Run() does - // (SettingsStore → QualitySettings.From → WithEnvOverrides). + // (SettingsStore → QualitySettings.From → WithEnvOverrides). var startupStore = new AcDream.UI.Abstractions.Panels.Settings.SettingsStore( _applicationPaths.SettingsFile); var startupDisplay = startupStore.LoadDisplay(); @@ -131,7 +131,7 @@ internal sealed class StudioWindow : IDisposable ContextFlags.ForwardCompatible, new APIVersion(4, 3)), VSync = false, - // MSAA from quality preset — must be baked into the GL context at creation. + // 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. @@ -195,7 +195,7 @@ internal sealed class StudioWindow : IDisposable _dats = RuntimeDatCollectionFactory.OpenReadOnly(_opts.DatDir); - // Build QualitySettings for RenderBootstrap (same as Run() above — re-read + // 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); @@ -265,7 +265,7 @@ internal sealed class StudioWindow : IDisposable // 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. + // Dump source is static — no FixtureProvider needed. if (_opts.DumpSlug is null && _source.CurrentLayout is not null) { uint layoutId = _opts.LayoutId ?? 0x2100006Cu; @@ -275,7 +275,7 @@ internal sealed class StudioWindow : IDisposable } } - // Task 3: ImGui IDE — interactive mode only. + // 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) @@ -337,14 +337,14 @@ internal sealed class StudioWindow : IDisposable try { - // ── HEADLESS SCREENSHOT PATH ────────────────────────────────────────────── + // ── 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. + // Fall back to 1280×720 when the root has no explicit size. int w = 1280, h = 720; if (!_opts.Mockup && _panelRoot is not null) { @@ -372,7 +372,7 @@ internal sealed class StudioWindow : IDisposable return; } - // Flip rows vertically: FBO bottom-left → PNG top-left. + // 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++) @@ -394,7 +394,7 @@ internal sealed class StudioWindow : IDisposable return; } - // ── INTERACTIVE PATH ────────────────────────────────────────────────────── + // ── INTERACTIVE PATH ────────────────────────────────────────────────────── if (_opts.Mockup) { var mockupGl = _stack.Gl; @@ -415,12 +415,12 @@ internal sealed class StudioWindow : IDisposable 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). + // 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. + // 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). @@ -430,8 +430,8 @@ internal sealed class StudioWindow : IDisposable // 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 + // ── 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. @@ -445,7 +445,7 @@ internal sealed class StudioWindow : IDisposable int paneY = kMenuBarH; int paneH = Math.Max(1, ih - kMenuBarH); - // 5. Main menu bar — panel picker combo pinned to the window top. + // 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; @@ -471,7 +471,7 @@ internal sealed class StudioWindow : IDisposable if (pickedSlug is not null) LoadDumpPanel(pickedSlug); - // 6. Canvas pane — show the FBO texture; gather canvas mouse events. + // 6. Canvas pane — show the FBO texture; gather canvas mouse events. var canvasEvt = default(CanvasInputEvent); if (panelTex != 0) canvasEvt = _inspector.DrawCanvas( @@ -501,10 +501,10 @@ internal sealed class StudioWindow : IDisposable if (_inspector.InteractMode) { - // ── Interact: live panel interaction ────────────────────────────── + // ── Interact: live panel interaction ────────────────────────────── if (canvasEvt.LeftDown) { - Console.WriteLine($"[studio] canvas click → panel ({mx}, {my})"); + Console.WriteLine($"[studio] canvas click → panel ({mx}, {my})"); root.OnMouseDown(UiMouseButton.Left, mx, my); } if (canvasEvt.LeftUp) @@ -514,7 +514,7 @@ internal sealed class StudioWindow : IDisposable } else { - // ── Inspect: click selects an element in the tree ───────────────── + // ── Inspect: click selects an element in the tree ───────────────── if (canvasEvt.LeftDown) { var hit = root.Pick(mx, my); @@ -634,7 +634,7 @@ internal sealed class StudioWindow : IDisposable _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 anyway — the review flagged that disposing only UiHost here leaked the rest. _stack?.Dispose(); _dats?.Dispose(); _audio?.Dispose(); diff --git a/src/AcDream.App/Studio/UiDumpModel.cs b/src/AcDream.App/Studio/UiDumpModel.cs index 174e34a5..c33e4b9e 100644 --- a/src/AcDream.App/Studio/UiDumpModel.cs +++ b/src/AcDream.App/Studio/UiDumpModel.cs @@ -1,10 +1,10 @@ -using System.Text.Json; +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 +// ───────────────────────────────────────────────────────────────────────────── +// UiDumpModel — POCOs for docs/research/2026-06-25-retail-ui-layout-dump.json // // Schema (v1): // { "version":1, "panels":[ { "id":int, "slug":string, "title":string, @@ -25,10 +25,10 @@ namespace AcDream.App.Studio; // 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. -// ───────────────────────────────────────────────────────────────────────────── +// ───────────────────────────────────────────────────────────────────────────── /// Top-level container for the retail UI layout dump. -internal sealed class UiDump +public sealed class UiDump { [JsonPropertyName("version")] public int Version { get; set; } @@ -38,7 +38,7 @@ internal sealed class UiDump } /// One panel (window) exported from the retail UI. -internal sealed class DumpPanel +public sealed class DumpPanel { [JsonPropertyName("id")] public long Id { get; set; } @@ -66,7 +66,7 @@ internal sealed class DumpPanel } /// One element node within a panel's traversal list. -internal sealed class DumpNode +public sealed class DumpNode { [JsonPropertyName("traversal_index")] public int TraversalIndex { get; set; } @@ -96,8 +96,8 @@ internal sealed class DumpNode public DumpStateSet StateSet { get; set; } = new(); } -/// Absolute screen-space rect (see comment above — must subtract parent rect for UiElement). -internal sealed class DumpRect +/// Absolute screen-space rect (see comment above — must subtract parent rect for UiElement). +public sealed class DumpRect { [JsonPropertyName("x")] public float X { get; set; } @@ -112,8 +112,8 @@ internal sealed class DumpRect public float Height { get; set; } } -/// State set for a node — default image plus per-state overrides. -internal sealed class DumpStateSet +/// State set for a node — default image plus per-state overrides. +public sealed class DumpStateSet { [JsonPropertyName("default_image")] public DumpImage? DefaultImage { get; set; } @@ -123,7 +123,7 @@ internal sealed class DumpStateSet } /// Image reference (RenderSurface dat id + optional separate alpha surface). -internal sealed class DumpImage +public sealed class DumpImage { [JsonPropertyName("image_id")] public long ImageId { get; set; } @@ -133,7 +133,7 @@ internal sealed class DumpImage } /// A named state override. -internal sealed class DumpState +public sealed class DumpState { [JsonPropertyName("state_id")] public int StateId { get; set; } @@ -142,14 +142,14 @@ internal sealed class DumpState public DumpImage Image { get; set; } = new(); } -// ───────────────────────────────────────────────────────────────────────────── +// ───────────────────────────────────────────────────────────────────────────── // Helper statics -// ───────────────────────────────────────────────────────────────────────────── +// ───────────────────────────────────────────────────────────────────────────── /// /// Parsing helpers for the retail UI dump JSON. /// -internal static class UiDumpModel +public static class UiDumpModel { private static readonly JsonSerializerOptions _opts = new() { diff --git a/src/AcDream.App/UI/ClientCommandController.cs b/src/AcDream.App/UI/ClientCommandController.cs index 2e1510ac..08eef64d 100644 --- a/src/AcDream.App/UI/ClientCommandController.cs +++ b/src/AcDream.App/UI/ClientCommandController.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Physics; +using AcDream.Core.Physics; using AcDream.Core.Ui; using AcDream.Core.Social; using AcDream.UI.Abstractions; @@ -10,9 +10,9 @@ namespace AcDream.App.UI; /// remain backend- and network-agnostic; this controller owns the boundary /// between verified command behavior and live session/UI services. /// -internal sealed class ClientCommandController +public sealed class ClientCommandController { - internal sealed record Bindings( + public sealed record Bindings( Action TeleportToLifestone, Action TeleportToMarketplace, Action TeleportToPkArena, diff --git a/src/AcDream.App/UI/ControlsIni.cs b/src/AcDream.App/UI/ControlsIni.cs index 42e03e74..2812d696 100644 --- a/src/AcDream.App/UI/ControlsIni.cs +++ b/src/AcDream.App/UI/ControlsIni.cs @@ -1,16 +1,16 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Globalization; using System.Numerics; namespace AcDream.App.UI; /// -/// Minimal reader for retail's controls.ini — a flat INI with one +/// Minimal reader for retail's controls.ini — a flat INI with one /// [section] per element type. Colors are #AARRGGBB (alpha /// first). Optional: a missing file yields an empty sheet (callers fall back -/// to hardcoded defaults). See the D.2b spec §7. +/// to hardcoded defaults). See the D.2b spec §7. /// -internal sealed class ControlsIni +public sealed class ControlsIni { private readonly Dictionary> _sections; diff --git a/src/AcDream.App/UI/CursorFeedbackController.cs b/src/AcDream.App/UI/CursorFeedbackController.cs index fbfa3597..babab5d6 100644 --- a/src/AcDream.App/UI/CursorFeedbackController.cs +++ b/src/AcDream.App/UI/CursorFeedbackController.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Combat; +using AcDream.Core.Combat; using AcDream.Runtime.Gameplay; namespace AcDream.App.UI; @@ -62,7 +62,7 @@ public enum RetailGlobalCursorKind TargetInvalid, } -internal readonly record struct CursorFeedback( +public readonly record struct CursorFeedback( CursorFeedbackKind Kind, UiCursorMedia Cursor = default, RetailGlobalCursorKind GlobalKind = RetailGlobalCursorKind.Default) @@ -72,7 +72,7 @@ internal readonly record struct CursorFeedback( public readonly record struct CursorFeedbackSnapshot( object? DragPayload = null, - DragAcceptState DragAccept = DragAcceptState.None, + UiItemSlot.DragAcceptState DragAccept = UiItemSlot.DragAcceptState.None, ResizeEdges ActiveResizeEdges = ResizeEdges.None, ResizeEdges HoverResizeEdges = ResizeEdges.None, bool WindowMoveActive = false, @@ -85,7 +85,7 @@ public readonly record struct CursorFeedbackSnapshot( RetailCursorTargetMode TargetMode = RetailCursorTargetMode.None, CombatMode CombatMode = CombatMode.NonCombat); -internal sealed class CursorFeedbackController +public sealed class CursorFeedbackController { private readonly ItemInteractionController? _itemInteraction; private readonly Func? _worldTargetProvider; @@ -110,8 +110,8 @@ internal sealed class CursorFeedbackController UiElement? hover = root.Pick(root.MouseX, root.MouseY); // Retail UpdateCursorState (0x00564630) keys the target-mode cursor off - // the SmartBox found object — the WORLD entity under the cursor. A UI - // window occludes the world (no found object → pending). The one + // the SmartBox found object — the WORLD entity under the cursor. A UI + // window occludes the world (no found object → pending). The one // UI-side source retail-style cells contribute is an occupied item // slot's own item. RetailCursorTargetMode targetMode = ModeFromInteraction(_itemInteraction); @@ -127,7 +127,7 @@ internal sealed class CursorFeedbackController var snapshot = new CursorFeedbackSnapshot( DragPayload: root.DragPayload, - DragAccept: FindHoveredItemSlot(hover)?.DragAcceptVisual ?? DragAcceptState.None, + DragAccept: FindHoveredItemSlot(hover)?.DragAcceptVisual ?? UiItemSlot.DragAcceptState.None, ActiveResizeEdges: root.ActiveResizeEdges, HoverResizeEdges: root.HoverResizeEdges, WindowMoveActive: root.IsWindowMoveActive, @@ -168,8 +168,8 @@ internal sealed class CursorFeedbackController { return snapshot.DragAccept switch { - DragAcceptState.Accept => CursorFeedbackKind.DragAccept, - DragAcceptState.Reject => CursorFeedbackKind.DragReject, + UiItemSlot.DragAcceptState.Accept => CursorFeedbackKind.DragAccept, + UiItemSlot.DragAcceptState.Reject => CursorFeedbackKind.DragReject, _ => CursorFeedbackKind.Drag, }; } @@ -197,10 +197,10 @@ internal sealed class CursorFeedbackController } // Retail UpdateCursorState (0x00564630), TARGET_MODE 3: no found - // object → the 0x27 four-arrows pending cursor — INCLUDING over + // object → the 0x27 four-arrows pending cursor — INCLUDING over // UI chrome. Valid/invalid exist only with a target under the - // cursor. (The earlier HoverUi → Invalid arm was a non-retail - // invention — 2026-07-03 visual gate.) + // cursor. (The earlier HoverUi → Invalid arm was a non-retail + // invention — 2026-07-03 visual gate.) return CursorFeedbackKind.TargetPending; } diff --git a/src/AcDream.App/UI/GameplayConfirmationController.cs b/src/AcDream.App/UI/GameplayConfirmationController.cs index 2ef0ebf7..b23686d0 100644 --- a/src/AcDream.App/UI/GameplayConfirmationController.cs +++ b/src/AcDream.App/UI/GameplayConfirmationController.cs @@ -1,4 +1,4 @@ -using AcDream.App.UI.Layout; +using AcDream.App.UI.Layout; using AcDream.Core.Net.Messages; namespace AcDream.App.UI; @@ -9,7 +9,7 @@ namespace AcDream.App.UI; /// completion; this semantic owner retains the server type/context and sends the /// matching confirmation response. /// -internal sealed class GameplayConfirmationController : IDisposable +public sealed class GameplayConfirmationController : IDisposable { private readonly RetailDialogFactory _dialogs; private readonly Action _sendResponse; diff --git a/src/AcDream.App/UI/IItemListDragHandler.cs b/src/AcDream.App/UI/IItemListDragHandler.cs index 64543527..3c149142 100644 --- a/src/AcDream.App/UI/IItemListDragHandler.cs +++ b/src/AcDream.App/UI/IItemListDragHandler.cs @@ -1,11 +1,11 @@ -namespace AcDream.App.UI; +namespace AcDream.App.UI; /// /// Visual result of a drag-over query. Retail handlers can consume a drag-over /// message without setting either accept or reject art; shortcut aliases over a /// physical item list use that neutral path. /// -internal enum ItemDragAcceptance +public enum ItemDragAcceptance { None, Accept, @@ -18,13 +18,13 @@ internal enum ItemDragAcceptance /// (RegisterItemListDragHandler, decomp 230461; confirmed acclient /// 0x004a539e + the gmToolbarUI block 0x004bdd89). /// decides the neutral/accept/reject overlay only (advisory). -/// is authoritative — it performs the action, or +/// is authoritative — it performs the action, or /// no-ops to reject. /// -internal interface IItemListDragHandler +public interface IItemListDragHandler { - /// The drag STARTED from a cell in this list — retail's RecvNotice_ItemListBeginDrag - /// → RemoveShortcut (decomp 0x004bd930/0x004bd450): the handler removes the lifted item from its + /// The drag STARTED from a cell in this list — retail's RecvNotice_ItemListBeginDrag + /// → RemoveShortcut (decomp 0x004bd930/0x004bd450): the handler removes the lifted item from its /// model + wire so the source slot empties immediately. The item is "in hand" until /// HandleDropRelease (place) or the drag ends off-target (stays removed). No restore on cancel. void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload); diff --git a/src/AcDream.App/UI/IRetainedPanelController.cs b/src/AcDream.App/UI/IRetainedPanelController.cs index d75d0d33..4ea2ae40 100644 --- a/src/AcDream.App/UI/IRetainedPanelController.cs +++ b/src/AcDream.App/UI/IRetainedPanelController.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace AcDream.App.UI; @@ -7,7 +7,7 @@ namespace AcDream.App.UI; /// Implementations own panel-specific subscriptions and reactions; the window /// manager owns visibility, focus, capture, geometry, and teardown ordering. /// -internal interface IRetainedPanelController : IDisposable +public interface IRetainedPanelController : IDisposable { /// Called once for each hidden-to-shown transition. void OnShown() { } diff --git a/src/AcDream.App/UI/IRetainedWindowStateController.cs b/src/AcDream.App/UI/IRetainedWindowStateController.cs index 6ed1d086..cdb129bc 100644 --- a/src/AcDream.App/UI/IRetainedWindowStateController.cs +++ b/src/AcDream.App/UI/IRetainedWindowStateController.cs @@ -1,7 +1,7 @@ -namespace AcDream.App.UI; +namespace AcDream.App.UI; /// Panel state that is not completely described by outer-frame bounds. -internal readonly record struct RetainedWindowState( +public readonly record struct RetainedWindowState( bool Collapsed = false, bool Maximized = false, float? PersistedTop = null, @@ -11,7 +11,7 @@ internal readonly record struct RetainedWindowState( /// Optional state seam used by retained-window persistence. Bounds are restored /// first; the controller then reapplies collapsed/maximized presentation. /// -internal interface IRetainedWindowStateController +public interface IRetainedWindowStateController { RetainedWindowState CaptureWindowState(); void RestoreWindowState(RetainedWindowState state); diff --git a/src/AcDream.App/UI/IUiDatStateful.cs b/src/AcDream.App/UI/IUiDatStateful.cs index 6fb9768a..7a86e9b8 100644 --- a/src/AcDream.App/UI/IUiDatStateful.cs +++ b/src/AcDream.App/UI/IUiDatStateful.cs @@ -1,16 +1,16 @@ -namespace AcDream.App.UI; +namespace AcDream.App.UI; /// /// Narrow numeric-state bridge for widgets imported from retail LayoutDesc data. /// Controllers use retail state ids without knowing how names/media are stored. /// -internal interface IUiDatStateful +public interface IUiDatStateful { uint ActiveRetailStateId { get; } bool TrySetRetailState(uint stateId); } -internal static class RetailUiStateIds +public static class RetailUiStateIds { public const uint Closed = 11u; public const uint Open = 12u; diff --git a/src/AcDream.App/UI/IUiGlobalTimeListener.cs b/src/AcDream.App/UI/IUiGlobalTimeListener.cs index 54d5a3f1..36680887 100644 --- a/src/AcDream.App/UI/IUiGlobalTimeListener.cs +++ b/src/AcDream.App/UI/IUiGlobalTimeListener.cs @@ -1,10 +1,10 @@ -namespace AcDream.App.UI; +namespace AcDream.App.UI; /// /// Opt-in recipient of retail UI global message 3, broadcast once per UI frame /// after tooltip deadline processing. /// -internal interface IUiGlobalTimeListener +public interface IUiGlobalTimeListener { void OnGlobalUiTime(double nowSeconds); } diff --git a/src/AcDream.App/UI/IUiViewportRenderer.cs b/src/AcDream.App/UI/IUiViewportRenderer.cs index 0a2a182b..13023078 100644 --- a/src/AcDream.App/UI/IUiViewportRenderer.cs +++ b/src/AcDream.App/UI/IUiViewportRenderer.cs @@ -1,10 +1,10 @@ -namespace AcDream.App.UI; +namespace AcDream.App.UI; /// Renders a 3-D mini-scene into an off-screen buffer and returns the GL color-texture /// handle. Called by the per-frame pre-UI hook (GameWindow), NOT from UiViewport.OnDraw. Implemented /// by PaperdollViewportRenderer in AcDream.App.Rendering. Intra-App decoupling so the UI widget /// doesn't depend on WbDrawDispatcher/GameWindow. -internal interface IUiViewportRenderer +public interface IUiViewportRenderer { /// Render at (width,height); return the color-texture GL handle, or 0 if nothing rendered. uint Render(int width, int height); diff --git a/src/AcDream.App/UI/IconComposer.cs b/src/AcDream.App/UI/IconComposer.cs index 196678da..77dacf2b 100644 --- a/src/AcDream.App/UI/IconComposer.cs +++ b/src/AcDream.App/UI/IconComposer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering; @@ -17,9 +17,9 @@ namespace AcDream.App.UI; /// DBCache::GetDIDFromEnum (0x413940). Each layer is a 0x06 RenderSurface decoded /// DIRECTLY (the D.2b RenderSurface-vs-Surface rule). /// -/// Layer order (bottom → top), matching retail: +/// Layer order (bottom → top), matching retail: /// 1. type-default underlay (OPAQUE backing; resolved via EnumIDMap 0x10000004 from -/// the portal MasterMap) — +/// the portal MasterMap) — /// 2. item custom underlay (e.g. "magic" tint strip) /// 3. base icon /// 4. item custom overlay (e.g. "enchanted" sparkle) @@ -31,28 +31,28 @@ namespace AcDream.App.UI; /// /// Composited textures are cached by their (typeUnderlay, underlay, base, overlay) tuple. /// -internal sealed class IconComposer +public sealed class IconComposer { private readonly IDatReaderWriter _dats; private readonly TextureCache _cache; - private readonly Dictionary<(uint, uint, uint, uint, uint), GpuTextureSlot> _byTuple = new(); + private readonly Dictionary<(uint, uint, uint, uint, uint), uint> _byTuple = new(); private readonly Dictionary<(uint, uint, uint), ComposedIcon> _dragByTuple = new(); - private readonly Dictionary _spellIcons = new(); - private readonly Dictionary _componentIcons = new(); + private readonly Dictionary _spellIcons = new(); + private readonly Dictionary _componentIcons = new(); - private sealed record ComposedIcon(byte[] Rgba, int Width, int Height, GpuTextureSlot Texture); + private sealed record ComposedIcon(byte[] Rgba, int Width, int Height, uint Texture); - // ── type-default underlay resolve (EnumIDMap 0x10000004) ───────────────── - // Portal MasterMap (0x25000000) maps enum 0x10000004 → submap DID (0x25000008). - // Submap maps index → 0x06 RenderSurface DID. index = LSB(itemType)+1, or 0x21. - // Refs: IconData::RenderIcons 0058d214–0058d22c; DBCache::GetDIDFromEnum 0x413940. + // ── type-default underlay resolve (EnumIDMap 0x10000004) ───────────────── + // Portal MasterMap (0x25000000) maps enum 0x10000004 → submap DID (0x25000008). + // Submap maps index → 0x06 RenderSurface DID. index = LSB(itemType)+1, or 0x21. + // Refs: IconData::RenderIcons 0058d214–0058d22c; DBCache::GetDIDFromEnum 0x413940. private EnumIDMap? _underlaySubMap; private bool _underlayResolveTried; private readonly Dictionary _underlayDidByIndex = new(); - // ── effect overlay resolve (EnumIDMap 0x10000005) ──────────────────────── - // Portal MasterMap (0x25000000) maps enum 0x10000005 → submap DID (0x25000009). - // Submap maps index → 0x06 RenderSurface DID. index = LSB(effects)+1, fallback 0x21. + // ── effect overlay resolve (EnumIDMap 0x10000005) ──────────────────────── + // Portal MasterMap (0x25000000) maps enum 0x10000005 → submap DID (0x25000009). + // Submap maps index → 0x06 RenderSurface DID. index = LSB(effects)+1, fallback 0x21. // Refs: IconData::RenderIcons 0x0058d180 (effect path); the effect tile is a // ReplaceColor tint SOURCE, not a blit layer (see RESOLVED doc, divergence DR-1). private EnumIDMap? _effectSubMap; @@ -68,13 +68,13 @@ internal sealed class IconComposer /// /// Resolve the type-default underlay DID for via the - /// two-level EnumIDMap chain (retail: IconData::RenderIcons 0058d214–0058d22c + + /// two-level EnumIDMap chain (retail: IconData::RenderIcons 0058d214–0058d22c + /// DBCache::GetDIDFromEnum 0x413940). /// /// index = LowestSetBit(itemType) + 1, or 0x21 when itemType has no bits set. /// /// NOTE: retail RenderIcons (407546) has a special paperdoll IsThePlayer case - /// that uses GetDIDByEnum(0x10000004, 7) + TYPE_CONTAINER for the player doll — that + /// that uses GetDIDByEnum(0x10000004, 7) + TYPE_CONTAINER for the player doll — that /// path is out of scope here (paperdoll phase). /// internal uint ResolveUnderlayDid(ItemType itemType) @@ -97,7 +97,7 @@ internal sealed class IconComposer uint masterDid = (uint)_dats.Portal.Db.Header.MasterMapId; // = 0x25000000 if (masterDid == 0) return; if (!_dats.Portal.TryGet(masterDid, out var master)) return; - if (!master.ClientEnumToID.TryGetValue(0x10000004u, out var subDid)) return; // → 0x25000008 + if (!master.ClientEnumToID.TryGetValue(0x10000004u, out var subDid)) return; // → 0x25000008 if (_dats.Portal.TryGet(subDid, out var sub)) _underlaySubMap = sub; } @@ -105,7 +105,7 @@ internal sealed class IconComposer /// Resolve the effect-overlay DID for via the EnumIDMap /// 0x10000005 chain. index = LowestSetBit(effects)+1; if the entry is missing/zero, /// retail falls back to index 0x21 (the solid-black tile). NOTE: the effect path has - /// NO lsb==-1 pre-check (unlike the type underlay), so effects==0 → index 0 → miss → + /// NO lsb==-1 pre-check (unlike the type underlay), so effects==0 → index 0 → miss → /// fallback. (Retail IconData::RenderIcons 0x0058d180.) /// internal uint ResolveEffectDid(uint effects) @@ -129,16 +129,16 @@ internal sealed class IconComposer uint masterDid = (uint)_dats.Portal.Db.Header.MasterMapId; // = 0x25000000 if (masterDid == 0) return; if (!_dats.Portal.TryGet(masterDid, out var master)) return; - if (!master.ClientEnumToID.TryGetValue(0x10000005u, out var subDid)) return; // → 0x25000009 + if (!master.ClientEnumToID.TryGetValue(0x10000005u, out var subDid)) return; // → 0x25000009 if (_dats.Portal.TryGet(subDid, out var sub)) _effectSubMap = sub; } /// /// Retail SurfaceWindow::ReplaceColor SURFACE overload (0x004415b0): for every - /// pixel in that equals pure-white-opaque (RGBAColor(1,1,1,1) → + /// pixel in that equals pure-white-opaque (RGBAColor(1,1,1,1) → /// 0xFFFFFFFF), copy the SAME (x,y) pixel from the source effect tile. This preserves /// the effect tile's texture/gradient (NOT a flat color). Retail requires the source to - /// cover the dest (it does — both are 32x32); out-of-range pixels are left unchanged. + /// cover the dest (it does — both are 32x32); out-of-range pixels are left unchanged. /// Mutates in place. /// internal static void ReplaceWhiteFromSurface(byte[] dst, int dw, int dh, byte[] src, int sw, int sh) @@ -160,7 +160,7 @@ internal sealed class IconComposer /// /// The decoded effect tile for (enum 0x10000005). The tile is /// a 32x32 textured RenderSurface whose pixels ARE the per-effect coloring (blue=Magical, - /// green=Poisoned, …; the 0x21 fallback is solid black). Retail copies it per-pixel into + /// green=Poisoned, …; the 0x21 fallback is solid black). Retail copies it per-pixel into /// the icon's white pixels (gradient), so we need the whole tile, not a representative /// color. Cached per DID. /// @@ -224,27 +224,27 @@ internal sealed class IconComposer /// effects==0 resolves to the 0x21 solid-black fallback tile, so pure-white pixels become /// black (matching retail); magical items take the per-effect hue instead. /// - public GpuTextureSlot GetIcon(ItemType itemType, uint iconId, uint underlayId, uint overlayId, uint effects) + public uint GetIcon(ItemType itemType, uint iconId, uint underlayId, uint overlayId, uint effects) { - if (iconId == 0) return GpuTextureSlot.Unassigned; + if (iconId == 0) return 0; uint typeUnderlayDid = ResolveUnderlayDid(itemType); var key = (typeUnderlayDid, iconId, underlayId, overlayId, effects); if (_byTuple.TryGetValue(key, out var tex)) return tex; - // Stage 1 — retail m_pDragIcon: base + custom overlay, then the effect recolor. + // Stage 1 — retail m_pDragIcon: base + custom overlay, then the effect recolor. // RenderIcons retains this as a distinct Graphic because the cursor ghost must not // carry the type/custom underlay that fills an inventory cell. ComposedIcon? drag = GetOrCreateDragIcon(iconId, overlayId, effects); - // Stage 2 — retail m_pIcon: type-default underlay (opaque) + custom underlay + drag. + // Stage 2 — retail m_pIcon: type-default underlay (opaque) + custom underlay + drag. var layers = new List<(byte[] rgba, int w, int h)>(); AddLayer(layers, typeUnderlayDid); AddLayer(layers, underlayId); if (drag is not null) layers.Add((drag.Rgba, drag.Width, drag.Height)); - if (layers.Count == 0) return GpuTextureSlot.Unassigned; + if (layers.Count == 0) return 0; var (rgba, w, h) = Compose(layers); - GpuTextureSlot handle = _cache.UploadRgba8(rgba, w, h, nearest: true); + uint handle = _cache.UploadRgba8(rgba, w, h, nearest: true); _byTuple[key] = handle; return handle; } @@ -255,15 +255,13 @@ internal sealed class IconComposer /// UIElement_ItemList::PrepareDragIcon obtains exactly this graphic through /// ACCWeenieObject::GetDragIcon (0x004e2a50 / 0x0058d180). /// - public GpuTextureSlot GetDragIcon(ItemType itemType, uint iconId, uint underlayId, uint overlayId, uint effects) + public uint GetDragIcon(ItemType itemType, uint iconId, uint underlayId, uint overlayId, uint effects) { // itemType/underlayId are deliberately unused: keeping the resolver signature identical // to GetIcon lets every item-panel binding request the two retail siblings from one model. _ = itemType; _ = underlayId; - return iconId == 0 - ? GpuTextureSlot.Unassigned - : GetOrCreateDragIcon(iconId, overlayId, effects)?.Texture ?? GpuTextureSlot.Unassigned; + return iconId == 0 ? 0u : GetOrCreateDragIcon(iconId, overlayId, effects)?.Texture ?? 0u; } private ComposedIcon? GetOrCreateDragIcon(uint iconId, uint overlayId, uint effects) @@ -277,11 +275,11 @@ internal sealed class IconComposer if (dragLayers.Count == 0) return null; var composed = Compose(dragLayers); - // Effect recolor — ALWAYS, matching retail IconData::RenderIcons (0x0058d180): + // Effect recolor — ALWAYS, matching retail IconData::RenderIcons (0x0058d180): // the effect tile (enum 0x10000005, lsb(effects)+1, fallback 0x21) is non-null // even for effects==0 (the 0x21 SOLID-BLACK tile 0x060011C5). Retail's RenderIcons // calls the SURFACE overload of SurfaceWindow::ReplaceColor (0x004415b0), copying - // the textured effect tile per-pixel into the icon's pure-white pixels — so + // the textured effect tile per-pixel into the icon's pure-white pixels — so // magical items take the tile's GRADIENT hue and mundane items go solid black. // (Visually confirmed against retail 2026-06-17: the Energy Crystal's blue is a // gradient, not a flat tint, and the no-mana scroll's edges are black.) @@ -289,7 +287,7 @@ internal sealed class IconComposer ReplaceWhiteFromSurface(composed.rgba, composed.w, composed.h, tile.Rgba8, tile.Width, tile.Height); - GpuTextureSlot texture = _cache.UploadRgba8(composed.rgba, composed.w, composed.h, nearest: true); + uint texture = _cache.UploadRgba8(composed.rgba, composed.w, composed.h, nearest: true); var created = new ComposedIcon(composed.rgba, composed.w, composed.h, texture); _dragByTuple[key] = created; return created; @@ -309,12 +307,12 @@ internal sealed class IconComposer /// Retail ClientMagicSystem::CompositeSpellIcon (0x00567550): power-level /// backing, spell art, reversed/normal recolor, then self/fellow overlay. /// - public GpuTextureSlot GetSpellIcon(uint spellId) + public uint GetSpellIcon(uint spellId) { - if (_spellIcons.TryGetValue(spellId, out GpuTextureSlot cached)) return cached; + if (_spellIcons.TryGetValue(spellId, out uint cached)) return cached; DatReaderWriter.DBObjs.SpellTable? table = _dats.Get(0x0E00000Eu); - if (table is null || !table.Spells.TryGetValue(spellId, out var spell)) return GpuTextureSlot.Unassigned; + if (table is null || !table.Spells.TryGetValue(spellId, out var spell)) return 0u; uint power = spell.Components.Count == 0 ? 0u @@ -323,7 +321,7 @@ internal sealed class IconComposer var layers = new List<(byte[] rgba, int w, int h)>(); AddLayer(layers, powerBacking); AddLayer(layers, spell.Icon); - if (layers.Count == 0) return GpuTextureSlot.Unassigned; + if (layers.Count == 0) return 0u; var composed = Compose(layers); uint tintIndex = (spell.Bitfield & DatReaderWriter.Enums.SpellIndex.Reversed) != 0 @@ -346,7 +344,7 @@ internal sealed class IconComposer (overlay.Rgba8, overlay.Width, overlay.Height)]); } - GpuTextureSlot texture = _cache.UploadRgba8(composed.rgba, composed.w, composed.h, nearest: true); + uint texture = _cache.UploadRgba8(composed.rgba, composed.w, composed.h, nearest: true); _spellIcons[spellId] = texture; return texture; } @@ -355,11 +353,11 @@ internal sealed class IconComposer /// Retail ClientMagicSystem::CompositeSpellComponentIcon (0x00567720). /// Components use their raw DAT art with pure white replaced by black. /// - public GpuTextureSlot GetSpellComponentIcon(uint iconId) + public uint GetSpellComponentIcon(uint iconId) { - if (iconId == 0u) return GpuTextureSlot.Unassigned; - if (_componentIcons.TryGetValue(iconId, out GpuTextureSlot cached)) return cached; - if (!TryDecode(iconId, out DecodedTexture icon)) return GpuTextureSlot.Unassigned; + if (iconId == 0u) return 0u; + if (_componentIcons.TryGetValue(iconId, out uint cached)) return cached; + if (!TryDecode(iconId, out DecodedTexture icon)) return 0u; byte[] rgba = (byte[])icon.Rgba8.Clone(); for (int i = 0; i + 3 < rgba.Length; i += 4) { @@ -367,7 +365,7 @@ internal sealed class IconComposer continue; rgba[i] = rgba[i + 1] = rgba[i + 2] = 0; } - GpuTextureSlot texture = _cache.UploadRgba8(rgba, icon.Width, icon.Height, nearest: true); + uint texture = _cache.UploadRgba8(rgba, icon.Width, icon.Height, nearest: true); _componentIcons[iconId] = texture; return texture; } diff --git a/src/AcDream.App/UI/ItemDragPayload.cs b/src/AcDream.App/UI/ItemDragPayload.cs index eb0ff2c1..f4f93234 100644 --- a/src/AcDream.App/UI/ItemDragPayload.cs +++ b/src/AcDream.App/UI/ItemDragPayload.cs @@ -1,25 +1,25 @@ -using AcDream.Core.Items; +using AcDream.Core.Items; namespace AcDream.App.UI; /// -/// Where a dragged item came from — the retail InqDropIconInfo flag +/// Where a dragged item came from — the retail InqDropIconInfo flag /// distinction (flags & 0xE == 0 fresh-from-inventory vs /// flags & 4 within-list reorder) expressed as a typed enum. The drop /// handler maps SourceKind + target back to the fresh-vs-reorder decision. /// Decomp anchors: gmToolbarUI 0x004bd162 / 0x004bd1af; InqDropIconInfo 230533. /// -internal enum ItemDragSource { Inventory, ShortcutBar, Equipment, Ground } +public enum ItemDragSource { Inventory, ShortcutBar, Equipment, Ground } /// /// Snapshot of a drag-in-progress, taken at drag-begin (so a server move arriving /// mid-drag can't mutate it under us). Port of retail's m_dragElement + /// InqDropIconInfo out-params (objId/container/flags, decomp 230533). /// SourceContainer is intentionally NOT stored: the handler resolves the -/// LIVE container via ClientObjectTable.Get(ObjId).ContainerId at drop — the +/// LIVE container via ClientObjectTable.Get(ObjId).ContainerId at drop — the /// same container id retail reads off the dragged element, single source of truth. /// -internal sealed record ItemDragPayload( +public sealed record ItemDragPayload( uint ObjId, // dragged weenie guid (retail itemID, +0x5FC) ItemDragSource SourceKind, // what kind of slot it left int SourceSlot, // the source cell's SlotIndex (retail m_lastShortcutNumDragged) diff --git a/src/AcDream.App/UI/ItemInteractionController.cs b/src/AcDream.App/UI/ItemInteractionController.cs index 68f6cd0e..db968d08 100644 --- a/src/AcDream.App/UI/ItemInteractionController.cs +++ b/src/AcDream.App/UI/ItemInteractionController.cs @@ -1,4 +1,4 @@ -using System; +using System; using AcDream.Core.Combat; using AcDream.Core.Items; using AcDream.Runtime.Gameplay; @@ -6,14 +6,14 @@ using AcDream.Runtime.Gameplay; namespace AcDream.App.UI; /// Result of offering a primary click to active item-target mode. -internal enum ItemPrimaryClickResult +public enum ItemPrimaryClickResult { NotActive, ConsumedSuccess, ConsumedRejected, } -internal readonly record struct PendingBackpackPlacement( +public readonly record struct PendingBackpackPlacement( ulong Token, uint ItemId, uint ContainerId, @@ -25,7 +25,7 @@ internal readonly record struct PendingBackpackPlacement( /// target acquisition, and drag-out drops here instead of duplicating /// ItemHolder::UseObject fragments in each panel. /// -internal sealed class ItemInteractionController : IDisposable +public sealed class ItemInteractionController : IDisposable { internal const string InventoryRequestBusyMessage = "You can only move or use one item at a time"; @@ -1230,7 +1230,7 @@ internal sealed class ItemInteractionController : IDisposable failures); } - internal readonly record struct AppraisalResponseAcceptance( + public readonly record struct AppraisalResponseAcceptance( bool Accepted, bool FirstResponse); @@ -1308,7 +1308,7 @@ internal sealed class ItemInteractionController : IDisposable { var target = _objects.Get(targetGuid); if (target is null) - return false; // retail: GetWeenieObject(target) null → incompatible + return false; // retail: GetWeenieObject(target) null → incompatible return ItemInteractionPolicy.IsTargetCompatible( Snapshot(source), Snapshot(target), _playerGuid()); } diff --git a/src/AcDream.App/UI/Layout/AppraisalUiController.cs b/src/AcDream.App/UI/Layout/AppraisalUiController.cs index a2d2d0de..a3052bd7 100644 --- a/src/AcDream.App/UI/Layout/AppraisalUiController.cs +++ b/src/AcDream.App/UI/Layout/AppraisalUiController.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; using System.Text; using AcDream.App.Spells; using AcDream.Core.Combat; @@ -14,7 +14,7 @@ namespace AcDream.App.UI.Layout; /// appraisal response/subview lifecycle; the imported LayoutDesc owns all /// chrome, geometry, fonts, and scrollbars. /// -internal sealed class AppraisalUiController : IRetainedPanelController +public sealed class AppraisalUiController : IRetainedPanelController { public const uint LayoutId = 0x2100006Bu; public const uint RootId = 0x100005F2u; @@ -73,8 +73,8 @@ internal sealed class AppraisalUiController : IRetainedPanelController private readonly CreatureAppraisalRowTemplateFactory? _creatureRowTemplates; private readonly CreatureDisplayNameResolver _creatureNames; private readonly RetailAppraisalNameResolver _itemNames; - private readonly Func _resolveSpellIcon; - private readonly Func _resolveComponentIcon; + private readonly Func _resolveSpellIcon; + private readonly Func _resolveComponentIcon; private readonly Func> _spellComponents; private readonly Func _magicSkill; private readonly SpellExamineComponentTemplateFactory? _spellComponentTemplates; @@ -128,8 +128,8 @@ internal sealed class AppraisalUiController : IRetainedPanelController CreatureAppraisalRowTemplateFactory? creatureRowTemplates, CreatureDisplayNameResolver? creatureNames, RetailAppraisalNameResolver? itemNames, - Func? resolveSpellIcon, - Func? resolveComponentIcon, + Func? resolveSpellIcon, + Func? resolveComponentIcon, Func>? spellComponents, Func? magicSkill, SpellExamineComponentTemplateFactory? spellComponentTemplates) @@ -155,8 +155,8 @@ internal sealed class AppraisalUiController : IRetainedPanelController ?? new CreatureDisplayNameResolver( new Dictionary()); _itemNames = itemNames ?? RetailAppraisalNameResolver.Empty; - _resolveSpellIcon = resolveSpellIcon ?? (_ => GpuTextureSlot.Unassigned); - _resolveComponentIcon = resolveComponentIcon ?? (_ => GpuTextureSlot.Unassigned); + _resolveSpellIcon = resolveSpellIcon ?? (_ => 0u); + _resolveComponentIcon = resolveComponentIcon ?? (_ => 0u); _spellComponents = spellComponents ?? (_ => []); _magicSkill = magicSkill ?? (_ => 0u); _spellComponentTemplates = spellComponentTemplates; @@ -283,8 +283,8 @@ internal sealed class AppraisalUiController : IRetainedPanelController CreatureAppraisalRowTemplateFactory? creatureRowTemplates = null, CreatureDisplayNameResolver? creatureNames = null, RetailAppraisalNameResolver? itemNames = null, - Func? resolveSpellIcon = null, - Func? resolveComponentIcon = null, + Func? resolveSpellIcon = null, + Func? resolveComponentIcon = null, Func>? spellComponents = null, Func? magicSkill = null, SpellExamineComponentTemplateFactory? spellComponentTemplates = null) @@ -496,7 +496,7 @@ internal sealed class AppraisalUiController : IRetainedPanelController _characterObjectId = 0; _spellId = 0u; _refreshElapsed = 0; - _spellIcon.Texture = GpuTextureSlot.Unassigned; + _spellIcon.Texture = 0u; SetSpellText(_spellSchool, string.Empty); SetSpellText(_spellMana, string.Empty); SetSpellText(_spellDuration, string.Empty); @@ -1037,7 +1037,7 @@ internal sealed class AppraisalUiController : IRetainedPanelController } } -internal enum AppraisalView +public enum AppraisalView { Item, Creature, diff --git a/src/AcDream.App/UI/Layout/CharacterController.cs b/src/AcDream.App/UI/Layout/CharacterController.cs index 1074c83d..6c60dc15 100644 --- a/src/AcDream.App/UI/Layout/CharacterController.cs +++ b/src/AcDream.App/UI/Layout/CharacterController.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; using System.Text; using AcDream.Core.Items; @@ -9,7 +9,7 @@ namespace AcDream.App.UI.Layout; /// localized report into m_pMainText; it is not a second character /// sheet and therefore does not repeat name, level, title, or current vitals. /// -internal static class CharacterController +public static class CharacterController { public const uint LayoutId = 0x2100006Eu; public const uint RootId = 0x10000183u; @@ -343,7 +343,7 @@ internal static class CharacterController /// Event-invalidated report owner for retail's character-information text. /// Stable frames borrow the same shaped line collection. /// -internal sealed class CharacterInformationUiController : IRetainedPanelController +public sealed class CharacterInformationUiController : IRetainedPanelController { private readonly Func _data; private readonly CharacterInfoStrings _strings; @@ -402,7 +402,7 @@ internal sealed class CharacterInformationUiController : IRetainedPanelControlle } } -internal sealed record CharacterInfoStrings( +public sealed record CharacterInfoStrings( string[] Birth, string[] Played, string DeathsNone, diff --git a/src/AcDream.App/UI/Layout/CharacterSheet.cs b/src/AcDream.App/UI/Layout/CharacterSheet.cs index 94a78e12..2da63597 100644 --- a/src/AcDream.App/UI/Layout/CharacterSheet.cs +++ b/src/AcDream.App/UI/Layout/CharacterSheet.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; namespace AcDream.App.UI.Layout; @@ -17,9 +17,9 @@ namespace AcDream.App.UI.Layout; /// UpdateAugmentations (0x004b9000), and /// UpdateLoad (0x004b8a20). /// -internal sealed class CharacterSheet +public sealed class CharacterSheet { - // ── Identity ────────────────────────────────────────────────────────────── + // ── Identity ────────────────────────────────────────────────────────────── /// Character name (first line of the report). public string Name { get; init; } = string.Empty; @@ -39,8 +39,8 @@ internal sealed class CharacterSheet /// Title string, e.g. "the Adventurer". Null = omit. public string? Title { get; init; } - // ── Experience / PK (gmStatManagementUI::UpdateExperience 0x004f0a70, - // UpdatePKStatus 0x004f00a0) — the Attributes-tab header strip ────────── + // ── Experience / PK (gmStatManagementUI::UpdateExperience 0x004f0a70, + // UpdatePKStatus 0x004f00a0) — the Attributes-tab header strip ────────── /// Total accrued experience (retail PropertyInt64 1). Header value /// element 0x10000235 (m_pTotalXPText). @@ -50,7 +50,7 @@ internal sealed class CharacterSheet /// 0x10000238 (m_pXPToLevelText). public long XpToNextLevel { get; init; } - /// XP-to-next-level meter fill, 0..1 (retail (cur−base)/(cap−base)). + /// XP-to-next-level meter fill, 0..1 (retail (cur−base)/(cap−base)). /// Drives the header meter 0x10000236 (m_pXPToLevelMeter). public float XpFraction { get; init; } @@ -58,13 +58,13 @@ internal sealed class CharacterSheet /// 0x10000233 (m_pPKStatusText). Null = omit. public string? PkStatus { get; init; } - // ── Birth / age / deaths (UpdatePlayerBirthAgeDeaths 0x004b8cb0) ───────── + // ── Birth / age / deaths (UpdatePlayerBirthAgeDeaths 0x004b8cb0) ───────── - /// Formatted birth date string (retail InqInt(0x62) → strftime). + /// Formatted birth date string (retail InqInt(0x62) → strftime). /// Null = omit the birth line. public string? BirthDate { get; init; } - /// Formatted play-time duration (retail InqInt(0x7d) → QueryDuration). + /// Formatted play-time duration (retail InqInt(0x7d) → QueryDuration). /// Null = omit the age line. public string? PlayTime { get; init; } @@ -77,7 +77,7 @@ internal sealed class CharacterSheet /// Raw retail PropertyInt 0x7D seconds. Null means the quality was absent. public int? TotalPlayTimeSeconds { get; init; } - // ── Vitals (UpdateEnduranceInfo 0x004b8eb0) ───────────────────────────── + // ── Vitals (UpdateEnduranceInfo 0x004b8eb0) ───────────────────────────── public int HealthCurrent { get; init; } public int HealthMax { get; init; } @@ -86,7 +86,7 @@ internal sealed class CharacterSheet public int ManaCurrent { get; init; } public int ManaMax { get; init; } - // ── Innate attributes (UpdateInnateAttributeInfo 0x004b87e0) ──────────── + // ── Innate attributes (UpdateInnateAttributeInfo 0x004b87e0) ──────────── // InqAttribute order: 1,2,4,3,5,6 = Strength, Endurance, Quickness, Coordination, Focus, Self. public int Strength { get; init; } @@ -96,9 +96,9 @@ internal sealed class CharacterSheet public int Focus { get; init; } public int Self { get; init; } - // ── Skills (UpdateFakeSkills 0x004b8930) ──────────────────────────────── + // ── Skills (UpdateFakeSkills 0x004b8930) ──────────────────────────────── // Character Information uses 0xB5/0xC0 for Chess/Fishing; skill credits use 0x18. - // InqInt(0x18) = available skill credits — footer 0x10000245 in the Attributes tab. + // InqInt(0x18) = available skill credits — footer 0x10000245 in the Attributes tab. public int UnspentSkillCredits { get; init; } public int SpecializedSkillCredits { get; init; } @@ -111,21 +111,21 @@ internal sealed class CharacterSheet /// /// Available (unspent) skill credits shown in the Attributes tab footer State-A. - /// Retail InqInt(0x18) — gmStatManagementUI::DisplayDefaultFooter (0x0049cde0). + /// Retail InqInt(0x18) — gmStatManagementUI::DisplayDefaultFooter (0x0049cde0). /// Element 0x10000243 (footer line-1 value in the studio's 3-line layout). /// public int SkillCredits { get; init; } /// /// Unassigned (banked) experience points. - /// Retail InqInt64(2) — shown in footer line-2 in State-A display. + /// Retail InqInt64(2) — shown in footer line-2 in State-A display. /// Element 0x10000245 (footer line-2 value). /// public long UnassignedXp { get; init; } - // ── Attribute raise costs (ExperienceToAttributeLevel, gmAttributeUI::PostInit) ── - // Retail formula for x1: ExperienceToAttributeLevel(value + 1) − xpSpent. - // Retail formula for x10: ExperienceToAttributeLevel(value + min(10, remaining)) − xpSpent. + // ── Attribute raise costs (ExperienceToAttributeLevel, gmAttributeUI::PostInit) ── + // Retail formula for x1: ExperienceToAttributeLevel(value + 1) − xpSpent. + // Retail formula for x10: ExperienceToAttributeLevel(value + min(10, remaining)) − xpSpent. // Cost 0 means the attribute is at max or not trainable. Ordered to match AttrRows: // Strength, Endurance, Coordination, Quickness, Focus, Self, Health, Stamina, Mana. // Source: gmAttributeUI::GetCostToRaise/GetCostToRaise10 (0x0049cb80/0x0049cc70). @@ -150,7 +150,7 @@ internal sealed class CharacterSheet /// public IReadOnlyList Skills { get; init; } = Array.Empty(); - // ── Augmentations (UpdateAugmentations 0x004b9000) ───────────────────── + // ── Augmentations (UpdateAugmentations 0x004b9000) ───────────────────── // Retail InqInt(0x162) = AugmentationStat; string-switch 1..0xb. /// Augmentation name from the switch in UpdateAugmentations (0x004b9000), @@ -165,7 +165,7 @@ internal sealed class CharacterSheet public IReadOnlyDictionary CharacterInfoProperties { get; init; } = new Dictionary(); - // ── Burden / load (UpdateLoad 0x004b8a20) ─────────────────────────────── + // ── Burden / load (UpdateLoad 0x004b8a20) ─────────────────────────────── // Retail InqLoad + EncumbranceCapacity(Strength, AugEncumbrance). public int BurdenCurrent { get; init; } @@ -173,7 +173,7 @@ internal sealed class CharacterSheet public int EncumbranceAugmentations { get; init; } } -internal enum CharacterSkillAdvancementClass +public enum CharacterSkillAdvancementClass { Inactive = 0, Untrained = 1, @@ -181,7 +181,7 @@ internal enum CharacterSkillAdvancementClass Specialized = 3, } -internal sealed record CharacterSkill( +public sealed record CharacterSkill( uint Id, string Name, uint IconDid, diff --git a/src/AcDream.App/UI/Layout/CharacterSheetProvider.cs b/src/AcDream.App/UI/Layout/CharacterSheetProvider.cs index 56e05442..dc390f97 100644 --- a/src/AcDream.App/UI/Layout/CharacterSheetProvider.cs +++ b/src/AcDream.App/UI/Layout/CharacterSheetProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using AcDream.Core.Items; using AcDream.Core.Player; @@ -18,7 +18,7 @@ namespace AcDream.App.UI.Layout; /// cited there too (gmAttributeUI::GetCostToRaise 0x0049cb80 family). /// /// State ownership: optimistic debits go through the owning -/// store's eventful APIs — / +/// store's eventful APIs — / /// (fires ObjectUpdated) /// when the player object is in the table, else /// / @@ -26,9 +26,9 @@ namespace AcDream.App.UI.Layout; /// Never write the raw property dictionaries from UI code. The next server /// snapshot remains authoritative over every optimistic value. /// -internal sealed class CharacterSheetProvider +public sealed class CharacterSheetProvider { - /// PropertyInt64 2 = unassigned (banked) XP — CharacterSheet.UnassignedXp. + /// PropertyInt64 2 = unassigned (banked) XP — CharacterSheet.UnassignedXp. private const uint UnassignedXpPropertyId = 2u; /// @@ -48,10 +48,10 @@ internal sealed class CharacterSheetProvider private readonly Action? _sendRaiseSkill; private readonly Action? _sendTrainSkill; - /// Portal SkillTable (0x0E000004) — set by the host once dats load. + /// Portal SkillTable (0x0E000004) — set by the host once dats load. public DatReaderWriter.DBObjs.SkillTable? SkillTable { get; set; } - /// Portal ExperienceTable (0x0E000018) — set by the host once dats load. + /// Portal ExperienceTable (0x0E000018) — set by the host once dats load. public DatReaderWriter.DBObjs.ExperienceTable? ExperienceTable { get; set; } public CharacterSheetProvider( @@ -89,7 +89,7 @@ internal sealed class CharacterSheetProvider return new ChangeBinding(this, changed); } - // ── Sheet assembly ───────────────────────────────────────────────────── + // ── Sheet assembly ───────────────────────────────────────────────────── /// Best display name: active toon key, else the live object's name, else "Player". public string CharacterName() @@ -240,8 +240,8 @@ internal sealed class CharacterSheetProvider /// /// Load the portal ExperienceTable (0x0E000018), falling back to a - /// type scan for older or odd dat collections. Failures are logged — - /// never silently swallowed — and leave raise costs unavailable (0). + /// type scan for older or odd dat collections. Failures are logged — + /// never silently swallowed — and leave raise costs unavailable (0). /// public static DatReaderWriter.DBObjs.ExperienceTable? LoadExperienceTable( IDatReaderWriter dats, Action? log = null) @@ -275,7 +275,7 @@ internal sealed class CharacterSheetProvider } /// XP still needed for the next level + fill fraction of the - /// current level band (retail (cur−base)/(cap−base); CharacterSheet.XpFraction). + /// current level band (retail (cur−base)/(cap−base); CharacterSheet.XpFraction). private (long toNext, float fraction) ComputeLevelXp(int level, long totalXp) { var levels = ExperienceTable?.Levels; @@ -394,7 +394,7 @@ internal sealed class CharacterSheetProvider } /// Cost to advance ranks along a retail - /// cumulative-XP curve: curve[target] − xpAlreadySpent, clamped at the + /// cumulative-XP curve: curve[target] − xpAlreadySpent, clamped at the /// curve end (retail GetCostToRaise/GetCostToRaise10 0x0049cb80/0x0049cc70). private static long RaiseCostFromXpCurve(uint[]? curve, uint ranks, uint spentXp, int amount) { @@ -427,13 +427,13 @@ internal sealed class CharacterSheetProvider private int VitalMax(LocalPlayerState.VitalKind kind) => _localPlayer.GetMaxApprox(kind) is { } max ? checked((int)Math.Min(int.MaxValue, max)) : 0; - // ── Raise-request flow ───────────────────────────────────────────────── + // ── Raise-request flow ───────────────────────────────────────────────── /// /// Send a raise/train action to the server and, when a send delegate /// fired, optimistically apply the local effect so the sheet stays /// current during the round trip. The next server snapshot remains - /// authoritative (a rejected raise is corrected by the property echo — + /// authoritative (a rejected raise is corrected by the property echo — /// pending/rollback ledger tracked as a follow-up issue). /// public void HandleRaiseRequest(CharacterStatController.RaiseRequest request) diff --git a/src/AcDream.App/UI/Layout/CharacterStatController.cs b/src/AcDream.App/UI/Layout/CharacterStatController.cs index 6bfba90a..4ac8f737 100644 --- a/src/AcDream.App/UI/Layout/CharacterStatController.cs +++ b/src/AcDream.App/UI/Layout/CharacterStatController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.UI; @@ -6,14 +6,14 @@ using AcDream.App.UI; namespace AcDream.App.UI.Layout; /// -/// Controller for the Character window's Attributes tab — LayoutDesc 0x2100002E, +/// Controller for the Character window's Attributes tab — LayoutDesc 0x2100002E, /// whose tab-content slot 0x1000022B mounts sub-layout 0x2100002C (gmAttributeUI, root /// type 0x1000002A) which in turn chains into the gmStatManagementUI header content. /// /// Unlike (which targets the SEPARATE text-report /// sub-panel 0x2100001A, gmCharacterInfoUI, by creating its runtime m_pMainText element), /// this controller binds the real, statically-mounted header + list elements that the -/// importer already produces — every id below is confirmed present via +/// importer already produces — every id below is confirmed present via /// . /// /// Ported from gmStatManagementUI::UpdateCharacterInfo (0x004f0770) + @@ -23,7 +23,7 @@ namespace AcDream.App.UI.Layout; /// gmAttributeUI::PostInit (0x0049db70) + AttributeInfoRegion / Attribute2ndInfoRegion /// (0x004f1910 / 0x004f19e0). Row icons loaded via sub-element 0x10000129 in the retail /// dat template (each icon is a 0x06xxxxxx RenderSurface DataID from SubMap -/// 0x25000006 / 0x25000007, spec §2). +/// 0x25000006 / 0x25000007, spec §2). /// /// Footer State A (nothing selected) bound from /// DisplayDefaultFooter (0x0049cde0): title empty, line-1 value = @@ -39,13 +39,13 @@ namespace AcDream.App.UI.Layout; /// UIStateId.Closed (0x0B). The imported Type-12 tab owns its authored font color and /// propagates the state to its three chrome children through PassToChildren. /// -/// Raise buttons: 0x10000246 (×1) + 0x100005EB (×10). State "Normal" = affordable +/// Raise buttons: 0x10000246 (×1) + 0x100005EB (×10). State "Normal" = affordable /// (UIStateId.Normal, 0x01), state "Ghosted" = unaffordable or no selection /// (UIStateId.Ghosted, 0x0D). Source: gmAttributeUI::AttributeInfoRegion::Update (0x004f1910). /// -internal static class CharacterStatController +public static class CharacterStatController { - // ── gmStatManagementUI header element ids (sub-layout 0x2100002C content) ── + // ── gmStatManagementUI header element ids (sub-layout 0x2100002C content) ── public const uint NameId = 0x10000231u; // m_pNameText public const uint HeritageId = 0x10000232u; // m_pHeritageText public const uint PkStatusId = 0x10000233u; // m_pPKStatusText @@ -64,16 +64,16 @@ internal static class CharacterStatController public const uint ListScrollbarId = 0x1000023Eu; // m_pListBox vertical scrollbar gutter public const uint ListDividerId = 0x1000023Fu; // bottom divider above footer - // ── Footer STATE-A container id ────────────────────────────────────────── + // ── Footer STATE-A container id ────────────────────────────────────────── // 0x10000240 is the "nothing selected" footer group. Its children (0x1000024E label row, - // 0x10000242–0x10000245 labels+values) are the correct State-A versions with wider + // 0x10000242–0x10000245 labels+values) are the correct State-A versions with wider // label widths (195px vs 145px in State B). _byId stores the LAST duplicate, which - // is the narrower State-B/C copy — so we walk the tree to 0x10000240 and bind from there. + // is the narrower State-B/C copy — so we walk the tree to 0x10000240 and bind from there. public const uint FooterStateAId = 0x10000240u; // State-A footer container (nothing selected) public const uint FooterStateBId = 0x10000241u; // State-B footer container (row selected) public const uint FooterStateCId = 0x10000247u; // State-C footer container (hide inactive) - // ── Tab bar element ids (LayoutDesc 0x2100002E root) ──────────────────── + // ── Tab bar element ids (LayoutDesc 0x2100002E root) ──────────────────── // These are imported Type-12 UIElement_Text tabs. Their Closed/Open states carry // the caption color and PassToChildren=true; their three retained children carry // the authored left/center/right chrome. Character and Spellbook therefore share @@ -89,7 +89,7 @@ internal static class CharacterStatController public const uint SkillsPageId = 0x1000022Cu; public const uint TitlesPageId = 0x10000539u; - // ── Footer element ids (gmStatManagementUI struct fields) ──────────────── + // ── Footer element ids (gmStatManagementUI struct fields) ──────────────── // Source: acclient.h / DisplayDefaultFooter (0x0049cde0) public const uint FooterTitleId = 0x1000024eu; // GetFooterTitleLabel public const uint FooterLine1Label = 0x10000242u; // GetFooterLineOneLabel @@ -97,26 +97,26 @@ internal static class CharacterStatController public const uint FooterLine2Label = 0x10000244u; // GetFooterLineTwoLabel public const uint FooterLine2Value = 0x10000245u; // GetFooterLineTwoValue - // ── Raise button element ids ────────────────────────────────────────────── + // ── Raise button element ids ────────────────────────────────────────────── // Source: gmAttributeUI::PostInit (0x0049db70); CM_Train::Event_TrainAttribute. // Button state "Normal" (UIStateId 0x01) = affordable (green/active); // "Ghosted" (UIStateId 0x0D) = disabled. Hidden when nothing is selected. - public const uint RaiseOneId = 0x10000246u; // raise × 1 - public const uint RaiseTenId = 0x100005EBu; // raise × 10 + public const uint RaiseOneId = 0x10000246u; // raise × 1 + public const uint RaiseTenId = 0x100005EBu; // raise × 10 private static readonly Vector4 Body = new(0.92f, 0.90f, 0.82f, 1f); // parchment-white body text private static readonly Vector4 Gold = new(1f, 0.82f, 0.36f, 1f); // section / emphasis gold - /// Row highlight color — semi-translucent gold, matches retail + /// Row highlight color — semi-translucent gold, matches retail /// UIStateId.Highlight (0x06) sprite 0x06001397 visual intent. private static readonly Vector4 HighlightBg = new(1f, 0.75f, 0.2f, 0.25f); private static readonly Vector4 BuffedSkillGreen = new(0.55f, 1f, 0.55f, 1f); - // ── Row layout constants ───────────────────────────────────────────────── + // ── Row layout constants ───────────────────────────────────────────────── // RowHeight 22px + IconSize 16px: retail spec (2026-06-26) says icons ~icon-height // and rows tighter. 16px icon fits inside 22px row with 3px vertical padding each side. // The larger row font (0x40000001, MaxCharHeight=18) is clipped to the 22px height which - // gives a tight-but-readable line. Retail spec (2026-06-26 ref): "rows tighter, text ≈ icon height". + // gives a tight-but-readable line. Retail spec (2026-06-26 ref): "rows tighter, text ≈ icon height". private const float RowHeight = 22f; private const float IconSize = 16f; private const float RowPadX = 4f; @@ -146,7 +146,7 @@ internal static class CharacterStatController Skills, } - internal enum RaiseTargetKind + public enum RaiseTargetKind { Attribute, Vital, @@ -154,7 +154,7 @@ internal static class CharacterStatController TrainSkill, } - internal readonly record struct RaiseRequest( + public readonly record struct RaiseRequest( RaiseTargetKind Kind, uint StatId, long Cost, @@ -170,7 +170,7 @@ internal static class CharacterStatController private sealed record SkillRowBinding(UiClickablePanel Panel, CharacterSkill Skill); - // ── Attribute row descriptors — retail display order per spec §1 ───────── + // ── Attribute row descriptors — retail display order per spec §1 ───────── private static readonly (string name, uint iconDid, uint statId)[] AttrRows = new[] { ("Strength", 0x060002C8u, 1u), @@ -210,7 +210,7 @@ internal static class CharacterStatController Func data, UiDatFont? datFont = null, UiDatFont? rowDatFont = null, - Func? spriteResolve = null, + Func? spriteResolve = null, RaiseRequestHandler? onRaiseRequest = null, Action? onClose = null) { @@ -231,35 +231,35 @@ internal static class CharacterStatController UiElement? contentPage = FindDirectChildById(layout.Root, AttributesPageId); // Name (18px from dat FontDid), Heritage (14px), PkStatus (14px): - // Fix C: pass null → Label's null-guard keeps the build-time dat font. + // Fix C: pass null → Label's null-guard keeps the build-time dat font. // Controllers still own the text color and the LinesProvider. - // Name = WHITE (retail "Horan" is white — confirmed 2026-06-26). + // Name = WHITE (retail "Horan" is white — confirmed 2026-06-26). Label(layout, contentPage, NameId, null, Vector4.One, () => data().Name); Label(layout, contentPage, HeritageId, null, Body, () => CharacterIdentityText.StatHeaderLine(data())); Label(layout, contentPage, PkStatusId, null, Body, () => data().PkStatus ?? string.Empty); - // ── Header captions (new — retail labels above/left of each number) ────── - // LevelCaption (0x1000023A, 16px from dat): pass null → keep build-time dat font. + // ── Header captions (new — retail labels above/left of each number) ────── + // LevelCaption (0x1000023A, 16px from dat): pass null → keep build-time dat font. LabelTwoLine(layout, contentPage, LevelCaptionId, null, Body, "Character", "Level"); - // Level number: retail renders this as large gold centered text in the 65×50 element. + // Level number: retail renders this as large gold centered text in the 65×50 element. // Fix C: the dat FontDid for the level element (0x1000023B) is now applied at build // time when the font resolver is provided (studio path). We no longer force rowDatFont - // here for the level — the dat's own FontDid drives the font. The Gold color is still + // here for the level — the dat's own FontDid drives the font. The Gold color is still // set via LinesProvider. SYNTHESIZED elements (the 9 attribute rows built in // BuildAttributeRows) continue to use datFont directly since they have no dat origin. - // Source: spec §Level area (65,50) + decomp gmStatManagementUI::UpdateCharacterInfo 0x004f0770. + // Source: spec §Level area (65,50) + decomp gmStatManagementUI::UpdateCharacterInfo 0x004f0770. // runtime color, dat carries none. Label(layout, contentPage, LevelId, null, Gold, () => data().Level.ToString()); - // TotalXpLabel (16px from dat) + TotalXp (16px from dat): pass null → keep dat font. + // TotalXpLabel (16px from dat) + TotalXp (16px from dat): pass null → keep dat font. LabelLeft(layout, contentPage, TotalXpLabelId, null, Body, static () => "Total Experience (XP):"); LabelRight(layout, contentPage, TotalXpId, null, Body, () => data().TotalXp.ToString("N0")); // XP-to-level meter fill (gmStatManagementUI::UpdateExperience 0x004f0a70). // Fix 5: child elements 0x10000237 (label) and 0x10000238 (value) are now built by // the LayoutImporter as UiText children of the XP meter (non-Type-3 meter children - // are explicitly built and registered in byId — see LayoutImporter.BuildWidget). + // are explicitly built and registered in byId — see LayoutImporter.BuildWidget). // FindElement now returns them; the controller binds their LinesProvider. // The importer builds them as UiText via DatWidgetFactory.BuildText, applying their // dat-origin HJustify/VJustify/FontDid/FontColor at build time. The controller then @@ -272,21 +272,21 @@ internal static class CharacterStatController // Bind the dat-origin XP label (0x10000237) and value (0x10000238). // These are now real UiText children of the meter (built by the importer). // The retail layout places the caption + value ON TOP of the red bar - // (ref 2026-06-26: "value … with the red fill bar behind it"). - // Source: retail spec (2026-06-26-character-window-retail-reference.md §State 1). + // (ref 2026-06-26: "value … with the red fill bar behind it"). + // Source: retail spec (2026-06-26-character-window-retail-reference.md §State 1). if (FindTextByDatId(layout, contentPage, XpNextLabelId) is UiText xpLabel) { if (datFont is not null) xpLabel.DatFont = datFont; xpLabel.ClickThrough = true; xpLabel.Centered = false; // left-align (retail: aligns with Total XP label above) xpLabel.RightAligned = false; - xpLabel.Padding = 0f; // avoid scroll clip — meter bar is ~13px tall + xpLabel.Padding = 0f; // avoid scroll clip — meter bar is ~13px tall // Item 1: align the XP-next label's left edge to match the TotalXpLabel's // absolute left edge. The XP-next label is a child of the meter (local coords), - // so its Left = TotalXpLabel.Left − meter.Left. This accounts for the meter's + // so its Left = TotalXpLabel.Left − meter.Left. This accounts for the meter's // horizontal offset within the panel (the meter starts to the right of the - // "Total Experience (XP):" caption row). Source: retail spec §State 1 (the + // "Total Experience (XP):" caption row). Source: retail spec §State 1 (the // "XP for next level:" caption left-aligns with "Total Experience (XP):" above). if (FindElementByDatId(layout, contentPage, TotalXpLabelId) is { } totalXpLbl) { @@ -310,7 +310,7 @@ internal static class CharacterStatController // The tab visuals are already retained in the imported LayoutDesc. Controllers // bind only click behavior and the active Open/Closed state below. - // ── Attribute list — 9 rows in list box 0x1000023D ──────────────────── + // ── Attribute list — 9 rows in list box 0x1000023D ──────────────────── // Mutable selected-index box: -1 = nothing selected. // Gather EVERY copy of the raise buttons in the tree. The raise button ids @@ -322,7 +322,7 @@ internal static class CharacterStatController // At bind time the tree includes all three tab pages (the page-visibility pass // runs AFTER this). Collecting from the full tree is safe: once the page- // visibility pass hides the inactive pages their raise buttons are invisible - // regardless of the Visible flag we set here — but the Attributes page's + // regardless of the Visible flag we set here — but the Attributes page's // buttons (which are NOT hidden by the page pass) must be explicitly hidden. var allRaise1 = new List(); var allRaise10 = new List(); @@ -365,7 +365,7 @@ internal static class CharacterStatController foreach (var b in allRaise1) b.Visible = false; foreach (var b in allRaise10) b.Visible = false; - // ── Footer state visibility ─────────────────────────────────────────── + // ── Footer state visibility ─────────────────────────────────────────── // There are THREE footer state groups (A=0x10000240, B=0x10000241, C=0x10000247) // all stacked at the same position within the Attributes page. _byId stores only // the LAST copy of each id; the others live in the VISIBLE Attributes page and must @@ -373,13 +373,13 @@ internal static class CharacterStatController // // WHY this cannot be done in the importer (dat state-model audit 2026-06-26): // All three group elements have DefaultState = StatManagement_Footer_Default - // (0x10000011) — the dat does NOT differentiate them by visibility. The parent + // (0x10000011) — the dat does NOT differentiate them by visibility. The parent // element (0x1000022F) has a States map {Default, Text, Meter} with PassToChildren= // true, but each child group also registers all three states (IncFlags=None, - // Media=0) — meaning the state-propagation produces no media change on any group. + // Media=0) — meaning the state-propagation produces no media change on any group. // Retail's gmStatManagementUI uses hardcoded element-id dispatch // (GetChildRecursive(this, 0x10000240) for Default, 0x10000241 for Text, 0x10000247 - // for Meter) to access the right group's children at runtime — the groups themselves + // for Meter) to access the right group's children at runtime — the groups themselves // are never hidden/shown via the dat state mechanism. The controller is the correct // and only place for this visibility management. See retail decomp // gmStatManagementUI::GetFooterTitleLabel @0x004f0170. @@ -389,7 +389,7 @@ internal static class CharacterStatController // selected and owns the retail raise buttons; State C stays hidden for now. SetFooterSelected(false); - // ── Footer State A initial binding ──────────────────────────────────── + // ── Footer State A initial binding ──────────────────────────────────── // Walk to the State-A container directly (rather than _byId which returns the // last duplicate) so we get the wider-label copies (195px) for the unselected state. BindFooterDynamic(layout, datFont, data, activeTab, attrSel, skillSel, contentPage); @@ -428,10 +428,10 @@ internal static class CharacterStatController RetailTabBinding.SetClick(titlesTab, null); UpdateTabStates(); - // ── Active-page selection (fixes the dark-overlay) ───────────────────── + // ── Active-page selection (fixes the dark-overlay) ───────────────────── // WHY this cannot be done in the importer (dat state-model audit 2026-06-26): // The three tab-page content areas (0x1000022B Attributes, 0x1000022C Skills, - // 0x10000539 Titles) all have DefaultState = Undef (0) — the dat carries no + // 0x10000539 Titles) all have DefaultState = Undef (0) — the dat carries no // visibility encoding for tabs. Tab visibility is managed at runtime by gmTabUI // via SetVisible(bool) on the page containers. The controller is the correct // and only place for initial tab-page selection. @@ -565,7 +565,7 @@ internal static class CharacterStatController ImportedLayout layout, UiElement? contentPage, UiElement? statList, - Func? spriteResolve) + Func? spriteResolve) { if (spriteResolve is null) return null; @@ -624,7 +624,7 @@ internal static class CharacterStatController private static void ConfigureSkillScrollbar( UiScrollbar bar, - Func spriteResolve) + Func spriteResolve) { bar.SpriteResolve = id => { var (h, w, ht) = spriteResolve(id); return (h, w, ht); }; bar.TrackSprite = ScrollTrackSprite; @@ -649,12 +649,12 @@ internal static class CharacterStatController : SkillContentWidth; } - // ── 9-row attribute list ───────────────────────────────────────────────── + // ── 9-row attribute list ───────────────────────────────────────────────── private static List BuildAttributeRows( UiElement list, UiDatFont? datFont, - Func? spriteResolve, + Func? spriteResolve, Func data, int[] sel, List allRaise1, @@ -736,7 +736,7 @@ internal static class CharacterStatController private static List BuildSkillRows( UiElement list, UiDatFont? datFont, - Func? spriteResolve, + Func? spriteResolve, Func data, int[] sel, List allRaise1, @@ -792,7 +792,7 @@ internal static class CharacterStatController private static UiPanel AddSkillHeader( UiElement list, UiDatFont? datFont, - Func? spriteResolve, + Func? spriteResolve, float left, float top, float width, @@ -878,14 +878,14 @@ internal static class CharacterStatController : Vector4.One; /// - /// Handles a row click: toggle (same row → deselect), else select new row. + /// Handles a row click: toggle (same row → deselect), else select new row. /// Updates highlight, footer providers, and raise-button state. /// private static void HandleRowClick( int clickedIndex, int[] sel, List rows, - Func? spriteResolve, + Func? spriteResolve, Func data, List allRaise1, List allRaise10) @@ -895,10 +895,10 @@ internal static class CharacterStatController // Log for live test confirmation (user tests selection in the studio). string rowName = GetRowName(newSel); - Console.WriteLine($"[CharacterStat] Row click: index={clickedIndex} → selected={newSel} ({rowName})"); + Console.WriteLine($"[CharacterStat] Row click: index={clickedIndex} → selected={newSel} ({rowName})"); // Update highlight on all rows. - // Retail uses sprite 0x06001397 (Button state 6 — the dark horizontal bars) + // Retail uses sprite 0x06001397 (Button state 6 — the dark horizontal bars) // for the selected row background. When spriteResolve is available, apply the // sprite; otherwise fall back to the translucent gold tint. const uint HighlightSprite = 0x06001397u; @@ -936,7 +936,7 @@ internal static class CharacterStatController int clickedIndex, int[] sel, List rows, - Func? spriteResolve, + Func? spriteResolve, Func data, List allRaise1, List allRaise10) @@ -956,7 +956,7 @@ internal static class CharacterStatController private static void ApplySkillSelectionVisuals( int selectedIndex, IReadOnlyList rows, - Func? spriteResolve) + Func? spriteResolve) { for (int i = 0; i < rows.Count; i++) { @@ -1227,7 +1227,7 @@ internal static class CharacterStatController private static UiClickablePanel AddRow( UiElement list, UiDatFont? datFont, - Func? spriteResolve, + Func? spriteResolve, float left, float top, float width, float height, uint iconDid, string nameText, @@ -1317,7 +1317,7 @@ internal static class CharacterStatController return row; } - // ── Footer — dynamic (State A + State B via sel[]) ──────────────────────── + // ── Footer — dynamic (State A + State B via sel[]) ──────────────────────── /// /// Bind all 5 footer elements with providers that close over : @@ -1361,7 +1361,7 @@ internal static class CharacterStatController // IMPORTANT: The footer state id (0x10000240) appears once per tab-page sub-layout // (Attributes / Skills / Titles). layout._byId stores only the LAST registered copy, // which ends up in the LAST-imported tab page (Titles). The page-visibility pass - // hides the Titles page → the bound footer elements would be invisible. + // hides the Titles page → the bound footer elements would be invisible. // // Fix: find State A/B inside the explicit Attributes page, not via _byId. The // id dictionary stores the last duplicate and can point at a hidden Skills/Titles @@ -1401,19 +1401,19 @@ internal static class CharacterStatController // The dat title element is H=55 (the full footer box). The dat says VJustify=Center, so // without an override the text would center vertically in the 55px box, overlapping // line-1/line-2 below. We set VerticalJustify=Top explicitly so the text renders at the - // top of the 55px box (y≈Padding), keeping all three footer lines non-overlapping. - // The dat says HJustify=Center (Centered=true from BuildText) — the title is centered. + // top of the 55px box (y≈Padding), keeping all three footer lines non-overlapping. + // The dat says HJustify=Center (Centered=true from BuildText) — the title is centered. // BackgroundSprite cleared: its full-height sprite would cover line-1/line-2. titleEl.BackgroundSprite = 0; titleEl.VerticalJustify = VJustify.Top; // dat says Center; override to Top (see comment above) titleEl.OneLine = true; } - // Title (FooterTitle 0x1000024E, 20px from dat): pass null → keep dat font. + // Title (FooterTitle 0x1000024E, 20px from dat): pass null → keep dat font. // Fix C: the dat has a 20px font for the footer title. Let it drive. if (titleEl is not null) { - // DatFont: null → keep the build-time dat font (20px in studio, global fallback in live game). - // Centered=true comes from the dat (HJustify=Center) via BuildText — not overridden here. + // DatFont: null → keep the build-time dat font (20px in studio, global fallback in live game). + // Centered=true comes from the dat (HJustify=Center) via BuildText — not overridden here. // RightAligned stays false (BuildText default for a Center element). titleEl.ClickThrough = true; titleEl.LinesProvider = () => @@ -1440,7 +1440,7 @@ internal static class CharacterStatController }; } - // Footer lines (all dat-origin with their own font sizes): pass null → keep dat font. + // Footer lines (all dat-origin with their own font sizes): pass null → keep dat font. var l1L = ByPos(20f, 5f, FooterLine1Label); LabelProvider(l1L, null, Body, () => { @@ -1475,7 +1475,7 @@ internal static class CharacterStatController return cost > 0 ? cost.ToString("N0") : "Infinity!"; }); - // Line-2 elements: pass null → keep dat font. + // Line-2 elements: pass null → keep dat font. var l2L = ByPos(37f, 5f, FooterLine2Label); LabelProvider(l2L, null, Body, () => { @@ -1596,7 +1596,7 @@ internal static class CharacterStatController } } - // ── Helpers ────────────────────────────────────────────────────────────── + // ── Helpers ────────────────────────────────────────────────────────────── private static void SetCompatibilityAnchorsAllById( UiElement node, @@ -1707,7 +1707,7 @@ internal static class CharacterStatController /// renders multiple lines oldest-first (top-to-bottom), so /// line 0 = (top) and line 1 = (bottom). /// This replaces the single-line "Character Level" caption which truncated in the 65px element. - /// Source: retail spec (2026-06-26-character-window-retail-reference.md §State 1 level caption). + /// Source: retail spec (2026-06-26-character-window-retail-reference.md §State 1 level caption). private static void LabelTwoLine(ImportedLayout layout, uint id, UiDatFont? datFont, Vector4 color, string line1, string line2) => LabelTwoLine(layout, null, id, datFont, color, line1, line2); @@ -1719,7 +1719,7 @@ internal static class CharacterStatController { // Null = keep whatever the importer (dat FontDid resolver) set at build time. if (datFont is not null) t.DatFont = datFont; - t.Centered = false; // non-Centered → scroll/multi-line path + t.Centered = false; // non-Centered → scroll/multi-line path t.RightAligned = false; t.ClickThrough = true; t.Padding = 1f; @@ -1732,7 +1732,7 @@ internal static class CharacterStatController } /// Left-justified label (for captions that should be left-aligned, not centered). - /// Padding=0 so a single dat-font line (≈12px) fits cleanly in a small element without + /// Padding=0 so a single dat-font line (≈12px) fits cleanly in a small element without /// being clipped by the bottom-pin scroll math (top=Padding, bottom=H-Padding). private static void LabelLeft(ImportedLayout layout, uint id, UiDatFont? datFont, Vector4 color, Func text) => LabelLeft(layout, null, id, datFont, color, text); @@ -1776,8 +1776,8 @@ internal static class CharacterStatController /// Bind a directly-located widget with a provider. /// Used when the widget was found by subtree walk rather than FindElement. /// Sets Padding = 0 to prevent the scroll-clip from hiding text in small - /// (H≈17–18px) footer elements: with the default Padding=4 and a dat font line-height - /// of ~12px the bottom-pinned baseY ends up above the top clip boundary → blank. + /// (H≈17–18px) footer elements: with the default Padding=4 and a dat font line-height + /// of ~12px the bottom-pinned baseY ends up above the top clip boundary → blank. private static void LabelProvider(UiText? t, UiDatFont? datFont, Vector4 color, Func text) { if (t is null) return; @@ -1798,7 +1798,7 @@ internal static class CharacterStatController /// The standard returns only the LAST widget /// registered for a given id; for elements duplicated across tab-page sub-layouts /// (raise buttons, close buttons) we need ALL copies so that visibility changes are - /// reflected in every page — not just the last-mounted one. + /// reflected in every page — not just the last-mounted one. /// /// /// @@ -1810,14 +1810,14 @@ internal static class CharacterStatController /// We therefore walk the tree recursively and collect every whose /// ActiveState reflects the dat default (before our code sets it), which is not a /// reliable discriminator. Instead, we gather ALL instances from - /// the subtree at the known spatial position (bottom of the panel) — but positions can + /// the subtree at the known spatial position (bottom of the panel) — but positions can /// overlap across pages. /// /// /// /// The correct approach: since _byId stores only one instance per id, we use the /// for the canonical id, then do a FULL tree walk - /// to find ADDITIONAL instances that have identical Width×Height to + /// to find ADDITIONAL instances that have identical Width×Height to /// the known button. This works because the three page copies share the same dat template /// and thus the same geometry. Collected via reference-equality guard to avoid duplicates. /// @@ -1833,7 +1833,7 @@ internal static class CharacterStatController _ = layout; // Walk the tree and collect ALL UiButton instances matching the canonical geometry. - // The canonical copy itself will also be found — that's fine; use a HashSet to dedup. + // The canonical copy itself will also be found — that's fine; use a HashSet to dedup. var seen = new HashSet(ReferenceEqualityComparer.Instance); CollectMatchingButtons(node, targetId, seen, result); } diff --git a/src/AcDream.App/UI/Layout/ChatWindowController.cs b/src/AcDream.App/UI/Layout/ChatWindowController.cs index 7d577bf8..c41561dd 100644 --- a/src/AcDream.App/UI/Layout/ChatWindowController.cs +++ b/src/AcDream.App/UI/Layout/ChatWindowController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering; @@ -10,7 +10,7 @@ using AcDream.UI.Abstractions.Panels.Chat; namespace AcDream.App.UI.Layout; /// -/// Binds the imported chat LayoutDesc (0x21000006) to live behavior — the acdream +/// Binds the imported chat LayoutDesc (0x21000006) to live behavior — the acdream /// analogue of retail ChatInterface + gmMainChatUI::PostInit @0x4ce130. /// /// @@ -24,20 +24,20 @@ namespace AcDream.App.UI.Layout; /// and bound in place. /// /// -internal sealed class ChatWindowController : IRetainedWindowStateController, IRetainedPanelController +public sealed class ChatWindowController : IRetainedWindowStateController, IRetainedPanelController { public const uint LayoutId = 0x21000006u; private bool _disposed; // Element ids from chat LayoutDesc 0x21000006 (confirmed in Task D/G1). private const uint RootId = 0x1000000Eu; - private const uint ResizeBarId = 0x1000000Fu; // dat top resize bar (800px — dropped; nine-slice grips replace it) + private const uint ResizeBarId = 0x1000000Fu; // dat top resize bar (800px — dropped; nine-slice grips replace it) private const uint TranscriptPanelId = 0x10000010u; - private const uint TranscriptId = 0x10000011u; // Type-12 prototype — skipped by factory + private const uint TranscriptId = 0x10000011u; // Type-12 prototype — skipped by factory private const uint TrackId = 0x10000012u; private const uint InputBarId = 0x10000013u; private const uint MenuId = 0x10000014u; - private const uint InputId = 0x10000016u; // Type-12 Text + Editable 0x16 → UiField + private const uint InputId = 0x10000016u; // Type-12 Text + Editable 0x16 → UiField private const uint SendId = 0x10000019u; private const uint MaxMinId = 0x1000046Fu; @@ -48,7 +48,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe private const uint MenuItemRow = 0x0600124Eu; // item row bg (template 0x1000001E) private const uint MenuItemSelected = 0x0600124Du; // active channel row - // ── Public surface ───────────────────────────────────────────────────── + // ── Public surface ───────────────────────────────────────────────────── /// Root element of the imported layout (the chat window chrome). public UiElement Root { get; private set; } = null!; @@ -71,7 +71,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe public RetailWindowHandle? WindowHandle { get; private set; } public bool IsMaximized => _maximized; - // ── Private state ────────────────────────────────────────────────────── + // ── Private state ────────────────────────────────────────────────────── private ChatChannelKind _activeChannel = ChatChannelKind.Say; @@ -86,7 +86,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe private BitmapFont? _cachedTranscriptDebugFont; internal int TranscriptLayoutBuildCount { get; private set; } - // ── Channel knowledge (ported from old UiChannelMenu — gmMainChatUI::InitTalkFocusMenu @0x4cdc50) ── + // ── Channel knowledge (ported from old UiChannelMenu — gmMainChatUI::InitTalkFocusMenu @0x4cdc50) ── private static readonly (string Label, ChatChannelKind? Channel)[] ChannelItems = { @@ -133,7 +133,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe private bool _maximized; private UiButton? _maxMinButton; - // ── Factory ──────────────────────────────────────────────────────────── + // ── Factory ──────────────────────────────────────────────────────────── /// /// Bind an imported chat layout to live behavior. @@ -156,7 +156,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe /// Retail dat font for transcript + input rendering. /// Fallback debug bitmap font (used when /// is null). - /// Dat RenderSurface id → (GL tex handle, px width, px height). + /// Dat RenderSurface id → (GL tex handle, px width, px height). /// Forwarded to and . public static ChatWindowController? Bind( ElementInfo rootInfo, @@ -165,7 +165,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe Func busProvider, UiDatFont? datFont, BitmapFont? debugFont, - Func resolve) + Func resolve) { // Their parent panels must exist as real widgets in the layout tree. var transcriptPanel = layout.FindElement(TranscriptPanelId); @@ -177,14 +177,14 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe Console.WriteLine( $"[D.2b] ChatWindowController.Bind: missing required elements " + $"(input={input is not null}, " + - $"panel={transcriptPanel is not null}, bar={inputBar is not null}) — " + + $"panel={transcriptPanel is not null}, bar={inputBar is not null}) — " + $"chat window will not be interactive."); return null; } // LayoutDesc 0x21000006 has SEVERAL top-level elements: the gmMainChatUI window // (RootId 0x1000000E) PLUS stray auxiliary elements that are NOT part of the docked - // window — a separate Field+ListBox (0x1000001C/1D, the floaty scrollback), the + // window — a separate Field+ListBox (0x1000001C/1D, the floaty scrollback), the // talk-focus highlight strip (0x1000001E), and a scroll-button prototype (0x10000526). // LayoutImporter.ImportInfos wraps all top-level elements in a synthetic Type-3 root, // so using layout.Root would render the strays overlapping the real window (the @@ -210,9 +210,9 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe transcriptPanel.Top = 0f; transcriptPanel.Height += 9f; // dat resize-bar height (0x1000000F H=9) - // ── Transcript ─────────────────────────────────────────────────── + // ── Transcript ─────────────────────────────────────────────────── // The factory now builds the Type-12 transcript element (0x10000011) as a UiText. - // Find it in the widget tree and bind the live providers — no remove/add needed. + // Find it in the widget tree and bind the live providers — no remove/add needed. c.Transcript = layout.FindElement(TranscriptId) as UiText ?? throw new InvalidOperationException("chat transcript 0x10000011 not built as UiText"); c.Transcript.DatFont = datFont; @@ -228,7 +228,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe c.Transcript.BackgroundColor = new Vector4(0f, 0f, 0f, 0.35f); // retail translucent transcript c.Transcript.LinesProvider = () => c.GetTranscriptLines(vm); - // ── Input ──────────────────────────────────────────────────────── + // ── Input ──────────────────────────────────────────────────────── // Editable/selectable/one-line semantics and state sprites came from the // imported property/state bags. The controller supplies runtime services only. c.Input = input; @@ -238,9 +238,9 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe c.Input.SpriteResolve = resolve; c.Input.OnSubmit = text => ChatCommandRouter.Submit(text, vm, busProvider(), c._activeChannel); - // ── Scrollbar — bind the factory-built Type-11 track element ──────── + // ── Scrollbar — bind the factory-built Type-11 track element ──────── // The factory now builds the Type-11 track element (0x10000012) as a UiScrollbar - // directly. Find it, bind it in place — no remove/add needed. + // directly. Find it, bind it in place — no remove/add needed. var track = layout.FindElement(TrackId); if (track is UiScrollbar bar) { @@ -253,7 +253,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe c.Scrollbar = bar; } - // ── Channel menu — bind the factory-built Type-6 UiMenu ────────── + // ── Channel menu — bind the factory-built Type-6 UiMenu ────────── if (layout.FindElement(MenuId) is UiMenu menu) { menu.DatFont = datFont; menu.Font = debugFont; menu.SpriteResolve = resolve; @@ -268,7 +268,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe menu.EnabledProvider = p => p is not ChatChannelKind ch || ChannelAvailable(ch); menu.ButtonLabelProvider = () => ChannelButtonLabel(c._activeChannel); // The widget reports the pick; the controller owns Selected. Only a talk-channel - // payload updates the active channel + highlight — the null-payload specials are + // payload updates the active channel + highlight — the null-payload specials are // deferred no-ops (see the chat re-drive deferred list) and leave selection intact. menu.OnSelect = p => { @@ -277,18 +277,18 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe c.Menu = menu; } - // ── Send button — Enter-alternate submit trigger ────────────────── + // ── Send button — Enter-alternate submit trigger ────────────────── // Retail's gmMainChatUI wires the Send button to the same ProcessCommand path. if (layout.FindElement(SendId) is UiButton sendEl) { sendEl.OnClick = () => c.Input.Submit(); - // The Send sprite is a blank gold button — retail draws the caption as text. + // The Send sprite is a blank gold button — retail draws the caption as text. sendEl.Label = "Send"; sendEl.LabelFont = datFont; sendEl.LabelColor = new Vector4(1f, 0.92f, 0.72f, 1f); } - // ── Size the channel button to its label + reflow the input field ─ + // ── Size the channel button to its label + reflow the input field ─ // Retail's talk-focus button autosizes to the selected channel name; the input // field then fills the gap from the button's right edge to the Send button. The // dat authors the button at a fixed 46px (too narrow for "Chat" once the LED + @@ -310,12 +310,12 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe ReflowInputRow(); } - // ── Max/min toggle — gmMainChatUI::HandleMaximizeButton ── + // ── Max/min toggle — gmMainChatUI::HandleMaximizeButton ── if (layout.FindElement(MaxMinId) is UiButton maxMinEl) { // The dat puts max/min and the scrollbar up-button at the SAME X (both // right-anchored), so at content width they overlap. Retail shows max/min - // just LEFT of the scrollbar column — shift it one button-width left. + // just LEFT of the scrollbar column — shift it one button-width left. if (track is not null) maxMinEl.Left = track.Left - maxMinEl.Width; maxMinEl.ResetAnchorCapture(); @@ -326,7 +326,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe return c; } - // ── Max/min implementation ───────────────────────────────────────────── + // ── Max/min implementation ───────────────────────────────────────────── /// /// Attach the typed outer-frame handle after the controller's imported content @@ -419,7 +419,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe return null; } - // ── Helpers ──────────────────────────────────────────────────────────── + // ── Helpers ──────────────────────────────────────────────────────────── /// /// Convert the ChatVM's detailed lines to the transcript's @@ -449,7 +449,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe } // Word-wrap each message to the transcript's current pixel width (ports retail - // GlyphList::Recalculate @0x473800 — break at word boundaries when the line would + // GlyphList::Recalculate @0x473800 — break at word boundaries when the line would // exceed wrapWidth). The cache key re-evaluates it after window resize. Func measure = datFont is { } df ? s => df.MeasureWidth(s) @@ -486,8 +486,8 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe /// Greedy word-wrap: split into fragments that each fit in /// pixels (per ), breaking at spaces. /// A word that is itself wider than the line is broken at CHARACTER boundaries (no - /// hyphen), packed onto the current line first — so a long unbroken token (e.g. a URL - /// or "wwwww…") wraps instead of overflowing, and a "You say," prefix stays on the same + /// hyphen), packed onto the current line first — so a long unbroken token (e.g. a URL + /// or "wwwww…") wraps instead of overflowing, and a "You say," prefix stays on the same /// row as the start of the message. Mirrors retail GlyphList::Recalculate's per-GlyphLine /// emission (which breaks mid-glyph-run when a run exceeds the wrap width). /// @@ -510,7 +510,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe } if (line.Length > 0 && measure(word) <= maxW) { - yield return line.ToString(); // word fits alone → push to a new line + yield return line.ToString(); // word fits alone → push to a new line line.Clear(); line.Append(word); continue; @@ -532,7 +532,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe } /// - /// Per- text color — the EXACT retail RGBA values read from a + /// Per- text color — the EXACT retail RGBA values read from a /// live retail client via cdb (the named RGBAColor constants at acclient /// 0x81c4a8+, e.g. colorWhite/colorBrightPurple/colorLightBlue/ /// colorGreen, used by ChatInterface::BuildChatColorLookupTable @0x4f31c0). diff --git a/src/AcDream.App/UI/Layout/CombatUiController.cs b/src/AcDream.App/UI/Layout/CombatUiController.cs index 14c9e411..f2413e0d 100644 --- a/src/AcDream.App/UI/Layout/CombatUiController.cs +++ b/src/AcDream.App/UI/Layout/CombatUiController.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Combat; +using AcDream.Core.Combat; using AcDream.Runtime.Gameplay; using AcDream.UI.Abstractions.Panels.Settings; @@ -16,7 +16,7 @@ namespace AcDream.App.UI.Layout; /// ListenToElementMessage (0x004CC430), and /// RecvNotice_SetCombatMode (0x004CC620). /// -internal sealed class CombatUiController : IRetainedPanelController +public sealed class CombatUiController : IRetainedPanelController { public const uint LayoutId = 0x21000073u; public const uint BasicPanelId = 0x1000005Cu; @@ -231,7 +231,7 @@ internal sealed class CombatUiController : IRetainedPanelController } /// Localized labels assigned by retail gmCombatUI::PostInit. -internal sealed record CombatUiLabels( +public sealed record CombatUiLabels( string Speed, string Power, string RepeatAttacks, diff --git a/src/AcDream.App/UI/Layout/ComponentBookTemplateFactory.cs b/src/AcDream.App/UI/Layout/ComponentBookTemplateFactory.cs index bccea804..b37dd408 100644 --- a/src/AcDream.App/UI/Layout/ComponentBookTemplateFactory.cs +++ b/src/AcDream.App/UI/Layout/ComponentBookTemplateFactory.cs @@ -1,4 +1,4 @@ -using DatReaderWriter; +using DatReaderWriter; using AcDream.Content; namespace AcDream.App.UI.Layout; @@ -9,7 +9,7 @@ namespace AcDream.App.UI.Layout; /// UIElement_ListBox::AddItemFromTemplateList used by /// gmSpellComponentUI::UpdateComponents @ 0x0048A910. /// -internal sealed class ComponentBookTemplateFactory +public sealed class ComponentBookTemplateFactory { public const uint LayoutId = 0x21000033u; public const uint CategoryTemplateId = 0x10000466u; @@ -35,7 +35,7 @@ internal sealed class ComponentBookTemplateFactory private readonly ElementInfo _categoryTemplate; private readonly ElementInfo _componentTemplate; - private readonly Func _resolveSprite; + private readonly Func _resolveSprite; private readonly UiDatFont? _defaultFont; private readonly IReadOnlyDictionary _fonts; private readonly string[] _categoryNames; @@ -43,7 +43,7 @@ internal sealed class ComponentBookTemplateFactory public ComponentBookTemplateFactory( ElementInfo categoryTemplate, ElementInfo componentTemplate, - Func resolveSprite, + Func resolveSprite, UiDatFont? defaultFont, IReadOnlyDictionary? fonts = null, IReadOnlyList? categoryNames = null) @@ -64,7 +64,7 @@ internal sealed class ComponentBookTemplateFactory /// public static ComponentBookTemplateFactory? TryLoad( IDatReaderWriter dats, - Func resolveSprite, + Func resolveSprite, UiDatFont? defaultFont, Func? resolveFont) { @@ -111,7 +111,7 @@ internal sealed class ComponentBookTemplateFactory public ComponentRow CreateComponentRow( uint componentId, - GpuTextureSlot iconTexture, + uint iconTexture, string name, int ownedCount, uint desiredCount) @@ -183,7 +183,7 @@ internal sealed class ComponentBookTemplateFactory CaptureFonts(child, resolveFont, fonts); } - internal readonly record struct ComponentRow( + public readonly record struct ComponentRow( UiTemplateListSlot Slot, UiField DesiredField); } diff --git a/src/AcDream.App/UI/Layout/CreatureAppraisalRows.cs b/src/AcDream.App/UI/Layout/CreatureAppraisalRows.cs index 1b0cd189..a9eec101 100644 --- a/src/AcDream.App/UI/Layout/CreatureAppraisalRows.cs +++ b/src/AcDream.App/UI/Layout/CreatureAppraisalRows.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; using System.Numerics; using AcDream.Content; using AcDream.Core.Items; @@ -9,7 +9,7 @@ using DatReaderWriter.Types; namespace AcDream.App.UI.Layout; -internal enum CreatureAppraisalValueStyle +public enum CreatureAppraisalValueStyle { Normal, Positive, @@ -17,12 +17,12 @@ internal enum CreatureAppraisalValueStyle Incomplete, } -internal readonly record struct CreatureAppraisalRow( +public readonly record struct CreatureAppraisalRow( string Label, string Value, CreatureAppraisalValueStyle Style); -internal enum CreatureAppraisalRowLayer +public enum CreatureAppraisalRowLayer { Combined, Background, @@ -35,7 +35,7 @@ internal enum CreatureAppraisalRowLayer /// BasicCreatureExamineUI and the two Update overloads at /// 0x004F1D90/0x004F1E80. /// -internal static class CreatureAppraisalRows +public static class CreatureAppraisalRows { private const string Unknown = "???"; private const uint DamageRating = 0x133u; @@ -228,20 +228,20 @@ internal static class CreatureAppraisalRows /// Instantiates LayoutDesc 0x2100006B's InfoRegion token template /// 0x10000166, matching UIElement_ListBox::AddItemFromTemplateList. /// -internal sealed class CreatureAppraisalRowTemplateFactory +public sealed class CreatureAppraisalRowTemplateFactory { public const uint TemplateId = 0x10000166u; public const uint LabelId = 0x1000012Au; public const uint ValueId = 0x1000012Bu; private readonly ElementInfo _template; - private readonly Func _resolveSprite; + private readonly Func _resolveSprite; private readonly UiDatFont? _defaultFont; private readonly IReadOnlyDictionary _fonts; public CreatureAppraisalRowTemplateFactory( ElementInfo template, - Func resolveSprite, + Func resolveSprite, UiDatFont? defaultFont, IReadOnlyDictionary? fonts = null) { @@ -256,7 +256,7 @@ internal sealed class CreatureAppraisalRowTemplateFactory public static CreatureAppraisalRowTemplateFactory? TryLoad( IDatReaderWriter dats, - Func resolveSprite, + Func resolveSprite, UiDatFont? defaultFont, Func? resolveFont) { @@ -368,7 +368,7 @@ internal sealed class CreatureAppraisalRowTemplateFactory /// instance of the same authored template keeps label/value text above it. /// Both lists share one pixel scroll model so their rows cannot drift. /// -internal sealed class CreatureAppraisalLayeredList +public sealed class CreatureAppraisalLayeredList { public const float TextInset = 8f; @@ -488,7 +488,7 @@ internal sealed class CreatureAppraisalLayeredList /// creature enum 0x10000005 through portal EnumMapper 0x2200000E and replaces /// underscores with spaces. /// -internal sealed class CreatureDisplayNameResolver +public sealed class CreatureDisplayNameResolver { public const uint MapperDid = 0x2200000Eu; private readonly IReadOnlyDictionary _names; diff --git a/src/AcDream.App/UI/Layout/DatStringResolver.cs b/src/AcDream.App/UI/Layout/DatStringResolver.cs index 10aa6f72..999d4d17 100644 --- a/src/AcDream.App/UI/Layout/DatStringResolver.cs +++ b/src/AcDream.App/UI/Layout/DatStringResolver.cs @@ -1,4 +1,4 @@ -using DatReaderWriter; +using DatReaderWriter; using AcDream.Content; using DatReaderWriter.DBObjs; @@ -13,7 +13,7 @@ namespace AcDream.App.UI.Layout; /// compute_str_hash @ 0x00413110. A StringInfo's token selects one /// localized string variant; ordinary UI labels use token zero. /// -internal sealed class DatStringResolver +public sealed class DatStringResolver { private readonly IDatReaderWriter _dats; private readonly Dictionary _tables = new(); diff --git a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs index dd694b7f..2f28bb53 100644 --- a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs +++ b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using AcDream.App.UI; @@ -18,7 +18,7 @@ namespace AcDream.App.UI.Layout; /// /// /// The meter's back/front 3-slice sprite ids live on grandchild image elements, -/// NOT on the meter element itself (format doc §11). +/// NOT on the meter element itself (format doc §11). /// walks two layers down to extract them: the two Type-3 container children /// ordered by (back behind = lower, front /// on top = higher), then within each container the image children that carry @@ -28,44 +28,44 @@ namespace AcDream.App.UI.Layout; /// /// /// The expand-detail overlay present in the front container carries ONLY named -/// states ("HideDetail"/"ShowDetail") — no "" DirectState entry — so the +/// states ("HideDetail"/"ShowDetail") — no "" DirectState entry — so the /// TryGetValue("") filter in excludes it /// automatically. /// /// -internal static class DatWidgetFactory +public static class DatWidgetFactory { /// /// Creates the for , sets its /// rect (Left/Top/Width/Height) and Anchors, and returns it. /// /// Resolved, merged element snapshot from the LayoutDesc importer. - /// RenderSurface id → (GL tex handle, pixel width, pixel height). + /// RenderSurface id → (GL tex handle, pixel width, pixel height). /// Returns (0,0,0) when the texture is not yet uploaded. /// Retail UI font for the meter's "cur/max" number overlay. - /// May be null pre-load — the meter falls back to the debug bitmap font. - /// Optional font resolver: FontDid → + /// May be null pre-load — the meter falls back to the debug bitmap font. + /// Optional font resolver: FontDid → /// (or null when the font can't be loaded). When non-null, any element whose /// is non-zero gets ITS OWN dat font applied instead of /// the shared fallback. Null = original behavior (use /// for every element). - /// The widget for this element. Never null — every type produces a widget. + /// The widget for this element. Never null — every type produces a widget. public static UiElement? Create(ElementInfo info, - Func resolve, UiDatFont? datFont, + Func resolve, UiDatFont? datFont, Func? fontResolve = null, Func? stringResolve = null) { // Retail Type 3 = UIElement_Field (reg :126190), but in acdream's CURRENT layouts // (vitals 0x2100006C / chat 0x21000006) Type-3 elements are sprite-bearing chrome + // containers (the 8-piece bevel corners/edges, the transcript/input panels), NOT - // editable fields — retail draws those as inert media-bearing Fields, which our + // editable fields — retail draws those as inert media-bearing Fields, which our // UiDatElement reproduces pixel-for-pixel (and without the spurious focus/edit // affordance a UiField would add). The one true editable field, the chat input // (0x10000016), resolves to Type 12 and is controller-placed as a UiField. So Type 3 // stays on the generic fallback here; register it as UiField only when a window // actually carries a factory-built editable Type-3 field (and UiField grows a // background-media draw + an opt-in editable flag at that point). UiField (the widget) - // still ships — it just isn't wired into the factory switch yet. + // still ships — it just isn't wired into the factory switch yet. // Resolve this element's own dat font if a resolver is provided and the element // has a FontDid. Falls back to the shared datFont when not set (FontDid==0) or // when the resolver returns null (font missing from dats). @@ -86,11 +86,11 @@ internal static class DatWidgetFactory // gmUIElement_*Indicator custom button classes 6 => new UiMenu(), // UIElement_Menu (reg :120163) 7 => BuildMeter(info, resolve, elementFont), // UIElement_Meter - 0xD => new UiViewport(), // UIElement_Viewport — 3-D mini-scene blit leaf + 0xD => new UiViewport(), // UIElement_Viewport — 3-D mini-scene blit leaf 11 => BuildScrollbar(info, resolve), // UIElement_Scrollbar (reg :124137) 12 => BuildText(info, resolve, elementFont, stringResolve), // UIElement_Text 0x13 => new UiDialogRoot(), // ConfirmationDialog - 0x10000031u => new UiItemList(resolve), // UIElement_ItemList — toolbar/inventory/paperdoll slots + 0x10000031u => new UiItemList(resolve), // UIElement_ItemList — toolbar/inventory/paperdoll slots 0x10000035u => BuildCheckbox( info, resolve, elementFont, fontResolve, stringResolve), // UIOption_Checkbox _ => new UiDatElement(info, resolve), // generic fallback (incl. Type 3 chrome/containers) @@ -105,7 +105,7 @@ internal static class DatWidgetFactory e.Width = info.Width; e.Height = info.Height; - // Honor the dat's draw order. ZLevel is the primary layer (higher = further BACK — e.g. the + // Honor the dat's draw order. ZLevel is the primary layer (higher = further BACK — e.g. the // gmInventoryUI full-window backdrop at ZLevel 100 sits behind the ZLevel-0 panels, #145); // ReadOrder is the within-layer tiebreaker (higher = on top). K=10000 exceeds any window's // element count so ZLevel always dominates. Vitals (all ZLevel 0) keep ZOrder == ReadOrder. @@ -134,7 +134,7 @@ internal static class DatWidgetFactory /// private static UiScrollbar BuildScrollbar( ElementInfo info, - Func resolve) + Func resolve) { var bar = new UiScrollbar { @@ -302,39 +302,39 @@ internal static class DatWidgetFactory return stateName == "Normal" ? DefaultImage(info) : 0u; } - // ── Meter ──────────────────────────────────────────────────────────────── + // ── Meter ──────────────────────────────────────────────────────────────── /// /// Builds a and populates its sprite ids from the meter's - /// child/grandchild elements (format doc §11). Two shapes are handled: + /// child/grandchild elements (format doc §11). Two shapes are handled: /// /// - /// 3-slice shape (vitals meters — 2 Type-3 containers, each with 3 image grandchildren): + /// 3-slice shape (vitals meters — 2 Type-3 containers, each with 3 image grandchildren): /// /// meter (Type 7) - /// ├── back-layer container (Type 3, lower ReadOrder — drawn first / behind) - /// │ ├── left-cap image (DirectState "" → File = back-left sprite) - /// │ ├── center image (DirectState "" → File = back-tile sprite) - /// │ └── right-cap image (DirectState "" → File = back-right sprite) - /// ├── front-layer container (Type 3, higher ReadOrder — drawn on top) - /// │ ├── left-cap image (→ front-left sprite) - /// │ ├── center image (→ front-tile sprite) - /// │ ├── right-cap image (→ front-right sprite) - /// │ └── expand overlay (named "ShowDetail"/"HideDetail" only — NO DirectState — IGNORED) - /// └── text label (Type 0) (IGNORED — Fill/Label providers bound by VitalsController) + /// ├── back-layer container (Type 3, lower ReadOrder — drawn first / behind) + /// │ ├── left-cap image (DirectState "" → File = back-left sprite) + /// │ ├── center image (DirectState "" → File = back-tile sprite) + /// │ └── right-cap image (DirectState "" → File = back-right sprite) + /// ├── front-layer container (Type 3, higher ReadOrder — drawn on top) + /// │ ├── left-cap image (→ front-left sprite) + /// │ ├── center image (→ front-tile sprite) + /// │ ├── right-cap image (→ front-right sprite) + /// │ └── expand overlay (named "ShowDetail"/"HideDetail" only — NO DirectState — IGNORED) + /// └── text label (Type 0) (IGNORED — Fill/Label providers bound by VitalsController) /// /// /// /// - /// Single-image shape (toolbar selected-object meters 0x100001A1/0x100001A2 — 1 Type-3 + /// Single-image shape (toolbar selected-object meters 0x100001A1/0x100001A2 — 1 Type-3 /// child, no grandchildren): the back-track sprite is on the meter element's own DirectState; /// the fill sprite is on the single Type-3 child's own DirectState. Both are placed in the /// TILE slot (Back/FrontTile) with left/right caps 0, so tiles /// them across the full bar geometry (DrawMode=Normal) and clips the fill to the fraction. /// (retail: gmToolbarUI::HandleSelectionChanged :198635, UIElement_Meter::Initialize :123328) /// - /// meter (Type 7) [DirectState "" → back-track sprite, e.g. 0x0600193E] - /// └── fill container (Type 3) [DirectState "" → fill sprite, e.g. 0x0600193F] + /// meter (Type 7) [DirectState "" → back-track sprite, e.g. 0x0600193E] + /// └── fill container (Type 3) [DirectState "" → fill sprite, e.g. 0x0600193F] /// /// /// @@ -345,7 +345,7 @@ internal static class DatWidgetFactory /// /// private static UiMeter BuildMeter(ElementInfo info, - Func resolve, UiDatFont? datFont) + Func resolve, UiDatFont? datFont) { var m = new UiMeter { @@ -383,18 +383,18 @@ internal static class DatWidgetFactory // Single-image shape used by the toolbar selected-object meters // (health 0x100001A1, mana 0x100001A2). // - The back-track sprite lives on the meter ELEMENT's own DirectState ("" key of - // info.StateMedia) — not on any grandchild image. e.g. health back = 0x0600193E. + // info.StateMedia) — not on any grandchild image. e.g. health back = 0x0600193E. // - The fill sprite lives on the single Type-3 child's own DirectState ("" key of // containers[0].StateMedia). e.g. health fill = 0x0600193F. - // The fill child has NO image grandchildren, so SliceIds would return all-zero — + // The fill child has NO image grandchildren, so SliceIds would return all-zero — // read the container's StateMedia directly instead. // // These go in the TILE slot (not the left-cap slot): the sprites are DrawMode=Normal, // which retail renders as "tile at native width to fill the full element geometry" - // (format doc §6; the generic UiDatElement.OnDraw Normal path; UIElement_Meter:: + // (format doc §6; the generic UiDatElement.OnDraw Normal path; UIElement_Meter:: // DrawChildren :123574 clips the child's FULL 140px geometry box to the fill fraction). // With the sprite on BackLeft instead, UiMeter.DrawHBar would clamp the cap to the - // sprite's NATIVE width (capL = min(nativeW, 140)) — leaving a right-side gap and + // sprite's NATIVE width (capL = min(nativeW, 140)) — leaving a right-side gap and // mapping the fill fraction to native width when nativeW < 140. The tile slot makes // midW = full bar width, so the back tiles across all 140px and the front clips to // 140*fraction correctly for any native sprite width (left/right caps unused = 0). @@ -435,7 +435,7 @@ internal static class DatWidgetFactory } else { - Console.WriteLine($"[D.2b] meter 0x{info.Id:X8}: {containers.Count} Type-3 containers but no recognized 3-slice, direct-fill, or stateful-fill shape — bar may render as solid-color fallback."); + Console.WriteLine($"[D.2b] meter 0x{info.Id:X8}: {containers.Count} Type-3 containers but no recognized 3-slice, direct-fill, or stateful-fill shape — bar may render as solid-color fallback."); } return m; @@ -482,11 +482,11 @@ internal static class DatWidgetFactory return (left, tile, right); } - // ── Text ───────────────────────────────────────────────────────────────── + // ── Text ───────────────────────────────────────────────────────────────── /// Type-12 UIElement_Text: an editable field or colored-line text view, /// selected from the canonical property bag. The element's - /// own Direct/Normal media (if any) becomes the background sprite, drawn under the text — + /// own Direct/Normal media (if any) becomes the background sprite, drawn under the text — /// so a Type-12 element that previously rendered via UiDatElement keeps its sprite. Lines /// are bound later by the controller (LinesProvider). An unbound UiText draws nothing /// because defaults to transparent. @@ -497,15 +497,15 @@ internal static class DatWidgetFactory /// that subsequently call / /// on dat-origin elements can be simplified. Controllers that explicitly set those /// properties after still override the build-time - /// defaults — the build-time value is just the starting point, not a lock. + /// defaults — the build-time value is just the starting point, not a lock. /// /// /// The font to seed on the widget. When a font resolver was /// provided and the element's FontDid resolved successfully, this is that element-specific /// font; otherwise it is the shared global fallback. Controllers that call /// and set afterward - /// still override this — the build-time value is just the starting point. - private static UiElement BuildText(ElementInfo info, Func resolve, + /// still override this — the build-time value is just the starting point. + private static UiElement BuildText(ElementInfo info, Func resolve, UiDatFont? elementFont = null, Func? stringResolve = null) { @@ -547,7 +547,7 @@ internal static class DatWidgetFactory // Apply horizontal + vertical justification from the dat at build time. // Controllers that call FindElement and set Centered/RightAligned/VerticalJustify - // afterward will override these — this is only the dat-driven default. + // afterward will override these — this is only the dat-driven default. bool centered = info.HJustify == HJustify.Center; bool rightAligned = info.HJustify == HJustify.Right; var vJustify = info.VJustify switch @@ -580,7 +580,7 @@ internal static class DatWidgetFactory // Font color from dat property 0x1B (ColorBaseProperty). // When present, seed DefaultColor so controllers that read it don't have to hard-code colors. - // Controllers that supply explicit per-line colors via LinesProvider still win — this is only + // Controllers that supply explicit per-line colors via LinesProvider still win — this is only // the build-time default. if (info.FontColor.HasValue) t.DefaultColor = info.FontColor.Value; @@ -593,7 +593,7 @@ internal static class DatWidgetFactory private static UiButton BuildButton( ElementInfo info, - Func resolve, + Func resolve, UiDatFont? elementFont, Func? fontResolve, Func? stringResolve) @@ -663,7 +663,7 @@ internal static class DatWidgetFactory /// private static UiButton BuildCheckbox( ElementInfo info, - Func resolve, + Func resolve, UiDatFont? elementFont, Func? fontResolve, Func? stringResolve) diff --git a/src/AcDream.App/UI/Layout/EffectRowTemplateFactory.cs b/src/AcDream.App/UI/Layout/EffectRowTemplateFactory.cs index 3e750c04..80aeebde 100644 --- a/src/AcDream.App/UI/Layout/EffectRowTemplateFactory.cs +++ b/src/AcDream.App/UI/Layout/EffectRowTemplateFactory.cs @@ -1,4 +1,4 @@ -using DatReaderWriter; +using DatReaderWriter; using AcDream.Content; namespace AcDream.App.UI.Layout; @@ -9,16 +9,16 @@ namespace AcDream.App.UI.Layout; /// UIElement_ListBox::AddItemFromTemplateList for /// EffectInfoRegion. /// -internal sealed class EffectRowTemplateFactory +public sealed class EffectRowTemplateFactory { private readonly ElementInfo _template; - private readonly Func _resolveSprite; + private readonly Func _resolveSprite; private readonly UiDatFont? _defaultFont; private readonly IReadOnlyDictionary _fonts; public EffectRowTemplateFactory( ElementInfo template, - Func resolveSprite, + Func resolveSprite, UiDatFont? defaultFont, IReadOnlyDictionary? fonts = null) { @@ -33,7 +33,7 @@ internal sealed class EffectRowTemplateFactory public static EffectRowTemplateFactory? TryLoad( IDatReaderWriter dats, - Func resolveSprite, + Func resolveSprite, UiDatFont? defaultFont, Func? resolveFont) { @@ -57,7 +57,7 @@ internal sealed class EffectRowTemplateFactory public EffectRow Create( uint spellId, - GpuTextureSlot iconTexture, + uint iconTexture, string name, string remaining) { @@ -90,7 +90,7 @@ internal sealed class EffectRowTemplateFactory return new EffectRow(slot, duration, remaining); } - internal sealed class EffectRow + public sealed class EffectRow { private readonly UiText _duration; private string _remaining; diff --git a/src/AcDream.App/UI/Layout/EffectsUiController.cs b/src/AcDream.App/UI/Layout/EffectsUiController.cs index 85a9b62f..3ab84179 100644 --- a/src/AcDream.App/UI/Layout/EffectsUiController.cs +++ b/src/AcDream.App/UI/Layout/EffectsUiController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -7,7 +7,7 @@ using AcDream.Core.Spells; namespace AcDream.App.UI.Layout; /// Retail gmEffectsUI positive/negative instance binding. -internal sealed class EffectsUiController : IRetainedPanelController +public sealed class EffectsUiController : IRetainedPanelController { public const uint LayoutId = 0x2100001Bu; // gmPanelUI's 0x10000184/185 are panel-slot IDs, not roots in this LayoutDesc. @@ -27,7 +27,7 @@ internal sealed class EffectsUiController : IRetainedPanelController private readonly Spellbook _spellbook; private readonly bool _positive; private readonly Func _serverTime; - private readonly Func _resolveSpellIcon; + private readonly Func _resolveSpellIcon; private readonly EffectRowTemplateFactory _templates; private readonly string _selectPrompt; private readonly UiItemList _list; @@ -48,7 +48,7 @@ internal sealed class EffectsUiController : IRetainedPanelController Spellbook spellbook, bool positive, Func serverTime, - Func resolveSpellIcon, + Func resolveSpellIcon, EffectRowTemplateFactory templates, string selectPrompt, UiItemList list, @@ -81,8 +81,8 @@ internal sealed class EffectsUiController : IRetainedPanelController Spellbook spellbook, bool positive, Func serverTime, - Func spriteResolve, - Func resolveSpellIcon, + Func spriteResolve, + Func resolveSpellIcon, EffectRowTemplateFactory templates, string selectPrompt, Action? close = null) @@ -144,7 +144,7 @@ internal sealed class EffectsUiController : IRetainedPanelController uint identity = enchantment.Identity; EffectRowTemplateFactory.EffectRow row = _templates.Create( enchantment.SpellId, - metadata is null ? GpuTextureSlot.Unassigned : _resolveSpellIcon(enchantment.SpellId), + metadata is null ? 0u : _resolveSpellIcon(enchantment.SpellId), metadata?.Name ?? $"Spell {enchantment.SpellId}", FormatRemaining(enchantment, _serverTime())); row.Slot.Clicked = () => Select(enchantment.SpellId); diff --git a/src/AcDream.App/UI/Layout/ElementReader.cs b/src/AcDream.App/UI/Layout/ElementReader.cs index 0e7f32e7..b4976b42 100644 --- a/src/AcDream.App/UI/Layout/ElementReader.cs +++ b/src/AcDream.App/UI/Layout/ElementReader.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Numerics; namespace AcDream.App.UI.Layout; @@ -53,7 +53,7 @@ public sealed class ElementInfo /// /// Raw edge-anchor flag values from the dat (LeftEdge, TopEdge, /// RightEdge, BottomEdge fields of ElementDesc). - /// Values 0–4. Imported elements preserve these in ; + /// Values 0–4. Imported elements preserve these in ; /// is only the compatibility projection /// for older programmatic consumers. /// @@ -80,7 +80,7 @@ public sealed class ElementInfo /// /// Font dat object id inherited from the base element's Properties[0x1A] - /// (ArrayBaseProperty → DataIdBaseProperty). 0 = none / not inherited. + /// (ArrayBaseProperty → DataIdBaseProperty). 0 = none / not inherited. /// public uint FontDid; @@ -109,7 +109,7 @@ public sealed class ElementInfo public Vector4? FontColor; /// - /// Sprite per state: state name → (RenderSurface file id, DrawMode int). + /// Sprite per state: state name → (RenderSurface file id, DrawMode int). /// The "" key represents the unnamed DirectState (ElementDesc.StateDesc). /// Named states use the UIStateId.ToString() value as the key /// (e.g. "HideDetail", "ShowDetail"). @@ -220,16 +220,16 @@ public sealed class ElementInfo /// No OpenGL, no DatReaderWriter types, no rendering dependencies beyond /// the bit-flag enum from AcDream.App.UI. /// -internal static class ElementReader +public static class ElementReader { /// Compatibility projection from raw retail modes to the legacy /// flags. This projection cannot represent centered /// mode 3 or proportional mode 4 exactly. Imported DAT widgets therefore use /// ; call this only for legacy/programmatic paths. - /// LeftEdge dat field value (0–4). - /// TopEdge dat field value (0–4). - /// RightEdge dat field value (0–4). - /// BottomEdge dat field value (0–4). + /// LeftEdge dat field value (0–4). + /// TopEdge dat field value (0–4). + /// RightEdge dat field value (0–4). + /// BottomEdge dat field value (0–4). public static AnchorEdges ToAnchors(uint left, uint top, uint right, uint bottom) { var a = AnchorEdges.None; @@ -284,7 +284,7 @@ internal static class ElementReader X = derived.X, Y = derived.Y, // NOTE: 0 is the "not set, inherit from base" sentinel for Width/Height. This - // diverges from the format doc §12 rule 2 ("derived W/H win even if zero") but is + // diverges from the format doc §12 rule 2 ("derived W/H win even if zero") but is // indistinguishable for Plan 1 (all base elements are zero-size Type-12 prototypes). // If a real zero-size derived element ever needs to override a non-zero base in // switch Width/Height to nullable values and use presence-aware merging. @@ -301,11 +301,11 @@ internal static class ElementReader // HJustify/VJustify: derived wins when it carries an explicit non-Center value // (the dat property was present and read); otherwise inherit the base prototype's value. // Center is the default (= "not set by this element") so Center-derived never overrides - // a non-Center base — matching the FontDid "non-zero wins" convention. + // a non-Center base — matching the FontDid "non-zero wins" convention. HJustify = derived.HJustify != HJustify.Center ? derived.HJustify : base_.HJustify, VJustify = derived.VJustify != VJustify.Center ? derived.VJustify : base_.VJustify, // FontColor: derived wins when it has an explicit (non-null) color; otherwise inherit the base. - // Null means "dat carried no 0x1B property" — so null-derived does NOT override a non-null base. + // Null means "dat carried no 0x1B property" — so null-derived does NOT override a non-null base. FontColor = derived.FontColor ?? base_.FontColor, // DefaultStateName: derived wins if set; otherwise inherit the base's default. DefaultStateName = !string.IsNullOrEmpty(derived.DefaultStateName) ? derived.DefaultStateName : base_.DefaultStateName, diff --git a/src/AcDream.App/UI/Layout/ExternalContainerController.cs b/src/AcDream.App/UI/Layout/ExternalContainerController.cs index 7d2f1a47..d7bf4460 100644 --- a/src/AcDream.App/UI/Layout/ExternalContainerController.cs +++ b/src/AcDream.App/UI/Layout/ExternalContainerController.cs @@ -1,4 +1,4 @@ -using System; +using System; using AcDream.Core.Items; using AcDream.Core.Selection; @@ -9,7 +9,7 @@ namespace AcDream.App.UI.Layout; /// Chests, corpses, and other world containers share this bottom-screen strip; /// it is intentionally independent from the owned backpack window. /// -internal sealed class ExternalContainerController : IItemListDragHandler, IRetainedPanelController +public sealed class ExternalContainerController : IItemListDragHandler, IRetainedPanelController { public const uint LayoutId = 0x21000008u; public const uint RootId = 0x10000063u; @@ -31,8 +31,8 @@ internal sealed class ExternalContainerController : IItemListDragHandler, IRetai private readonly SelectionState _selection; private readonly ItemInteractionController _itemInteraction; private readonly StackSplitQuantityState _stackSplitQuantity; - private readonly Func _resolveIcon; - private readonly Func _resolveDragIcon; + private readonly Func _resolveIcon; + private readonly Func _resolveDragIcon; private readonly Action _sendUse; private readonly Action _sendPutItemInContainer; private readonly Action _sendSplitToContainer; @@ -53,8 +53,8 @@ internal sealed class ExternalContainerController : IItemListDragHandler, IRetai SelectionState selection, ItemInteractionController itemInteraction, StackSplitQuantityState stackSplitQuantity, - Func resolveIcon, - Func resolveDragIcon, + Func resolveIcon, + Func resolveDragIcon, Action sendUse, Action sendPutItemInContainer, Action sendSplitToContainer, @@ -128,8 +128,8 @@ internal sealed class ExternalContainerController : IItemListDragHandler, IRetai SelectionState selection, ItemInteractionController itemInteraction, StackSplitQuantityState stackSplitQuantity, - Func resolveIcon, - Func resolveDragIcon, + Func resolveIcon, + Func resolveDragIcon, Action sendUse, Action sendPutItemInContainer, Action sendSplitToContainer, @@ -354,9 +354,9 @@ internal sealed class ExternalContainerController : IItemListDragHandler, IRetai private UiItemSlot CreateCell(UiItemList owner, uint guid, ItemDragSource source) { ClientObject? item = _objects.Get(guid); - GpuTextureSlot icon = item is null ? GpuTextureSlot.Unassigned : _resolveIcon( + uint icon = item is null ? 0u : _resolveIcon( item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects); - GpuTextureSlot dragIcon = item is null ? GpuTextureSlot.Unassigned : _resolveDragIcon( + uint dragIcon = item is null ? 0u : _resolveDragIcon( item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects); var cell = new UiItemSlot { diff --git a/src/AcDream.App/UI/Layout/IndicatorBarController.cs b/src/AcDream.App/UI/Layout/IndicatorBarController.cs index 9b78e933..60e6b741 100644 --- a/src/AcDream.App/UI/Layout/IndicatorBarController.cs +++ b/src/AcDream.App/UI/Layout/IndicatorBarController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using AcDream.Core.Items; using AcDream.Core.Net; @@ -12,7 +12,7 @@ namespace AcDream.App.UI.Layout; /// sprites, and state names; this controller owns only live state selection and /// authored input actions. /// -internal sealed class IndicatorBarController : IRetainedPanelController +public sealed class IndicatorBarController : IRetainedPanelController { public const uint LayoutId = 0x21000071u; @@ -270,7 +270,7 @@ internal sealed class IndicatorBarController : IRetainedPanelController } } -internal sealed record IndicatorBarBindings( +public sealed record IndicatorBarBindings( Spellbook Spellbook, ClientObjectTable Objects, Func PlayerGuid, diff --git a/src/AcDream.App/UI/Layout/InventoryController.cs b/src/AcDream.App/UI/Layout/InventoryController.cs index 26ab7a52..4030a4b1 100644 --- a/src/AcDream.App/UI/Layout/InventoryController.cs +++ b/src/AcDream.App/UI/Layout/InventoryController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; using AcDream.App.UI; using AcDream.Core.Items; @@ -10,12 +10,12 @@ namespace AcDream.App.UI.Layout; /// Binds the imported gmInventoryUI tree (LayoutDesc 0x21000023) and populates it from /// . The acdream analogue of retail /// gmInventoryUI/gmBackpackUI/gm3DItemsUI ::PostInit (named-retail decomp 176236/176596/176728). -/// Container-switching is live (click a side bag → Use 0x0036 → ViewContents 0x0196 full-replace); +/// Container-switching is live (click a side bag → Use 0x0036 → ViewContents 0x0196 full-replace); /// drag-into-bag / wield-drop wire are later sub-phases. /// -internal sealed class InventoryController : IItemListDragHandler, IRetainedPanelController +public sealed class InventoryController : IItemListDragHandler, IRetainedPanelController { - // Element ids — spec §1 (dat dump of 0x21000022 / 0x21000021 + the *::PostInit binds). + // Element ids — spec §1 (dat dump of 0x21000022 / 0x21000021 + the *::PostInit binds). public const uint ContentsGridId = 0x100001C6u; // gm3DItemsUI m_itemList ("Contents of Backpack") public const uint ContainerListId = 0x100001CAu; // gmBackpackUI m_containerList (side-bag selector) public const uint TopContainerId = 0x100001C9u; // gmBackpackUI m_topContainer (main-pack cell) @@ -30,7 +30,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel private const uint PaperdollWindowId = 0x100001CDu; private const uint BackpackWindowId = 0x100001CEu; - // 3D-items grid: 192x96 → 6 cols x 3 rows of the 32x32 UIItem cell (template 0x21000037). + // 3D-items grid: 192x96 → 6 cols x 3 rows of the 32x32 UIItem cell (template 0x21000037). private const int ContentsColumns = 6; private const float ContentsCellPx = 32f; // gm3DItemsUI grid (192x96 = 6x3 of 32px) private const float BackpackCellPx = 36f; // gmBackpackUI column cells (0x100001C9/CA = 36px) @@ -40,8 +40,8 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel private readonly ClientObjectTable _objects; private readonly Func _playerGuid; - private readonly Func _iconIds; - private readonly Func? _dragIconIds; + private readonly Func _iconIds; + private readonly Func? _dragIconIds; private readonly Func _strength; private readonly Func? _ownerName; @@ -80,8 +80,8 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel ImportedLayout layout, ClientObjectTable objects, Func playerGuid, - Func iconIds, - Func? dragIconIds, + Func iconIds, + Func? dragIconIds, Func strength, SelectionState selection, Func? ownerName, @@ -169,7 +169,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel _topContainer.ExamineItemRequested = ExamineItem; } - // Burden meter: vertical 11×58 bar (gmBackpackUI m_burdenMeter, retail direction 4). + // Burden meter: vertical 11×58 bar (gmBackpackUI m_burdenMeter, retail direction 4). _burdenMeter = layout.FindElement(BurdenMeterId) as UiMeter; if (_burdenMeter is not null) { @@ -180,7 +180,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel // Captions: drive each host UiText directly with the known string (the caption // elements resolve to UiText). "Contents of Backpack" + "%d%%" are procedural in retail - // (gm3DItemsUI/gmBackpackUI PostInit/SetLoadLevel); "Burden" is the dat label. (Spec §5.) + // (gm3DItemsUI/gmBackpackUI PostInit/SetLoadLevel); "Burden" is the dat label. (Spec §5.) AttachCaption(layout.FindElement(TitleTextId), () => "Inventory of " + OwnerName(), datFont); AttachCaption(layout.FindElement(BurdenCaptionId), () => "Burden", datFont); AttachCaption(layout.FindElement(ContentsCaptionId), () => "Contents of " + OpenContainerName(), datFont); @@ -228,7 +228,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel ImportedLayout layout, ClientObjectTable objects, Func playerGuid, - Func iconIds, + Func iconIds, Func strength, SelectionState selection, UiDatFont? datFont, @@ -244,7 +244,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel ItemInteractionController? itemInteraction = null, Action? onClose = null, StackSplitQuantityState? stackSplitQuantity = null, - Func? dragIconIds = null) + Func? dragIconIds = null) => new InventoryController(layout, objects, playerGuid, iconIds, dragIconIds, strength, selection, ownerName, datFont, contentsEmptySprite, sideBagEmptySprite, mainPackEmptySprite, @@ -342,7 +342,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel Populate(); } - /// True if the object is in (or wielded by) the player — i.e. a rebuild is warranted. + /// True if the object is in (or wielded by) the player — i.e. a rebuild is warranted. private bool Concerns(ClientObject o) { uint p = _playerGuid(); @@ -425,14 +425,14 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel while (_containerList.GetNumUIItems() < slots) AddEmptyCell(_containerList); } - // Main-pack cell: the player's own container — clicking it opens/selects the main pack. + // Main-pack cell: the player's own container — clicking it opens/selects the main pack. // Retail draws a CONSTANT backpack icon here, NOT the player's body icon: IconData::RenderIcons // (0x0058d1ee) has an IsThePlayer() branch that draws a fixed backpack with m_itemType = // TYPE_CONTAINER. Compose that backpack base over the Container type-underlay (the player // object's own IconId is the character body, which would render wrong here). The backpack // RenderSurface is 0x0600127E, VISUALLY CONFIRMED at the live gate 2026-06-22 (the earlier // 0x060011F4 from a research dat-dump of GetDIDByEnum(0x10000004,7) was a green tile, not the - // pack — the index value was misreported). Retires AP-51. + // pack — the index value was misreported). Retires AP-51. if (_topContainer is not null) { const uint PlayerPackBaseIcon = 0x0600127Eu; // constant main-pack backpack (visual gate) @@ -442,7 +442,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel p, _iconIds(ItemType.Container, PlayerPackBaseIcon, 0u, 0u, 0u), dragIconTexture: _dragIconIds?.Invoke( - ItemType.Container, PlayerPackBaseIcon, 0u, 0u, 0u)); + ItemType.Container, PlayerPackBaseIcon, 0u, 0u, 0u) ?? 0u); main.DragAcceptSprite = 0x060011F7u; main.DragRejectSprite = 0x060011F8u; main.Clicked = () => OpenContainer(p); main.DoubleClicked = () => _itemInteraction?.ActivateItem(p); @@ -454,10 +454,10 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel RefreshBurden(); } - /// The effective open container — the explicit one, or the player (main pack) by default. + /// The effective open container — the explicit one, or the player (main pack) by default. /// Resolved live (not cached at ctor) so a late-arriving player guid is handled. The default /// sentinel is 0; once the main pack is explicitly opened, _openContainer holds the player - /// guid instead — both resolve here to the same main-pack container, so the paths are equivalent. + /// guid instead — both resolve here to the same main-pack container, so the paths are equivalent. private static bool IsBag(ClientObject item) => item.ContainerTypeHint != 0u || item.Type.HasFlag(ItemType.Container) @@ -468,17 +468,17 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel /// The owned destination retail PlaceInBackpack currently uses. public uint CurrentOpenContainerId => EffectiveOpen(); - /// Add a populated cell wired to its click role: container cell → open+select, - /// item cell → select-only. + /// Add a populated cell wired to its click role: container cell → open+select, + /// item cell → select-only. private void AddCell(UiItemList? list, uint guid, bool isContainer, bool waiting = false) { if (list is null) return; var item = _objects.Get(guid); - GpuTextureSlot tex = item is null ? GpuTextureSlot.Unassigned + uint tex = item is null ? 0u : _iconIds(item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects); - GpuTextureSlot? dragTex = item is null ? null + uint dragTex = item is null ? 0u : _dragIconIds?.Invoke( - item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects); + item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects) ?? 0u; var cell = new UiItemSlot { SpriteResolve = list.SpriteResolve }; cell.SetItem(guid, tex, dragIconTexture: dragTex); cell.SetWaitingState(waiting); @@ -520,9 +520,9 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel /// /// Bind the exact ItemList_DragOver state for this destination. A normal contents-grid - /// insertion uses ItemSlot_DragOver_Accept (0x10000041 → green circle 0x060011F9). + /// insertion uses ItemSlot_DragOver_Accept (0x10000041 → green circle 0x060011F9). /// An occupied container selector uses ItemSlot_DragOver_DropIn - /// (0x10000046 → green arrow 0x060011F7). Retail: 0x004e3400. + /// (0x10000046 → green arrow 0x060011F7). Retail: 0x004e3400. /// private void ConfigureDropFeedback(UiItemList list, UiItemSlot cell) { @@ -532,11 +532,11 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel cell.DragRejectSprite = 0x060011F8u; } - /// Set the per-cell container capacity bar — retail UIElement_UIItem::UpdateCapacityDisplay + /// Set the per-cell container capacity bar — retail UIElement_UIItem::UpdateCapacityDisplay /// (0x004e16e0): visible only for a container with itemsCapacity > 0; fill = /// GetNumContainedItems / itemsCapacity, clamped [0,1]. -1 hides the bar (non-container / unknown /// capacity). For a CLOSED side bag the contents aren't indexed until it's opened (ViewContents), - /// so the bar reads empty until then — faithful to retail's known-children count. + /// so the bar reads empty until then — faithful to retail's known-children count. private void SetCapacityBar(UiItemSlot cell, uint containerGuid) { int cap = _objects.Get(containerGuid)?.ItemsCapacity ?? 0; @@ -545,7 +545,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel cell.CapacityFill = Math.Clamp(n / (float)cap, 0f, 1f); } - // ── IItemListDragHandler (B-Drag) — drop an item to move it (optimistic + wire) ────────────── + // ── IItemListDragHandler (B-Drag) — drop an item to move it (optimistic + wire) ────────────── /// Retail ItemList_BeginDrag selects an unselected item before enabling its waiting /// mesh. Inventory items do not lift-remove (unlike the toolbar): the item stays in its slot /// until the server confirms the eventual drop. @@ -778,7 +778,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel ?? dispatch(); /// True only when we KNOW the container is full (capacity known + contents indexed). A - /// closed bag (unknown count) returns false → advisory accept; the server is authoritative. + /// closed bag (unknown count) returns false → advisory accept; the server is authoritative. private bool IsContainerFull(uint container) { int cap = _objects.Get(container)?.ItemsCapacity ?? 0; @@ -815,7 +815,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel if (guid == 0) return; _selection.Select(guid, SelectionChangeSource.Inventory); uint open = EffectiveOpen(); - if (guid == open) { ApplyIndicators(); return; } // already open — just move the square + if (guid == open) { ApplyIndicators(); return; } // already open — just move the square uint p = _playerGuid(); _openContainer = guid; @@ -872,7 +872,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel if (host is null) return; // The caption elements (0x100001D7 "Burden", 0x100001C5 "Contents of Backpack", - // 0x100001D8 "%") resolve to UiText (Type-0 inheriting a text base — confirmed live). + // 0x100001D8 "%") resolve to UiText (Type-0 inheriting a text base — confirmed live). // Drive the host UiText DIRECTLY: it is already in the paint tree and renders, whereas // a nested child UiText did not paint. Set it to a static centered single-line label. if (host is UiText t) @@ -913,7 +913,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel } /// Recompute the burden fill + percent. Port of CACQualities::InqLoad - /// (decomp 0x0058f130) → gmBackpackUI::SetLoadLevel (0x004a6ea0). currentBurden: + /// (decomp 0x0058f130) → gmBackpackUI::SetLoadLevel (0x004a6ea0). currentBurden: /// player wire EncumbranceVal (PropertyInt 5) if present, else the carried-Burden sum. private void RefreshBurden() { diff --git a/src/AcDream.App/UI/Layout/ItemAppraisalReport.cs b/src/AcDream.App/UI/Layout/ItemAppraisalReport.cs index ea003944..3997e6ee 100644 --- a/src/AcDream.App/UI/Layout/ItemAppraisalReport.cs +++ b/src/AcDream.App/UI/Layout/ItemAppraisalReport.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using System.Text; namespace AcDream.App.UI.Layout; @@ -18,7 +18,7 @@ public enum ItemAppraisalFontStyle /// Separator inserted before an appraisal fragment. This is the retained /// equivalent of ItemExamineUI::AddItemInfo's final argument. /// -internal enum ItemAppraisalSeparator +public enum ItemAppraisalSeparator { None, Line, @@ -30,7 +30,7 @@ internal enum ItemAppraisalSeparator /// font-color index. Keeping these separate until shaping preserves style /// when a long fragment wraps. /// -internal readonly record struct ItemAppraisalFragment( +public readonly record struct ItemAppraisalFragment( string Text, ItemAppraisalSeparator Separator, ItemAppraisalFontStyle Style); @@ -38,7 +38,7 @@ internal readonly record struct ItemAppraisalFragment( /// /// Immutable item appraisal report in retail append order. /// -internal sealed class ItemAppraisalReport +public sealed class ItemAppraisalReport { public static ItemAppraisalReport Empty { get; } = new([]); diff --git a/src/AcDream.App/UI/Layout/ItemAppraisalTextFormatter.cs b/src/AcDream.App/UI/Layout/ItemAppraisalTextFormatter.cs index 7e3d444c..8aaa60fc 100644 --- a/src/AcDream.App/UI/Layout/ItemAppraisalTextFormatter.cs +++ b/src/AcDream.App/UI/Layout/ItemAppraisalTextFormatter.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; using System.Text; using AcDream.Core.Items; using AcDream.Core.Net.Messages; @@ -12,7 +12,7 @@ namespace AcDream.App.UI.Layout; /// and its Appraisal_Show* helpers. Static spell prose comes from the /// installed portal.dat spell table, like retail's ClientMagicSystem. /// -internal static class ItemAppraisalTextFormatter +public static class ItemAppraisalTextFormatter { private static readonly (uint Requirement, uint Stat, uint Difficulty)[] WieldRequirements = diff --git a/src/AcDream.App/UI/Layout/ItemCooldownAssets.cs b/src/AcDream.App/UI/Layout/ItemCooldownAssets.cs index cbef9793..76a635c1 100644 --- a/src/AcDream.App/UI/Layout/ItemCooldownAssets.cs +++ b/src/AcDream.App/UI/Layout/ItemCooldownAssets.cs @@ -1,4 +1,4 @@ -using AcDream.Content; +using AcDream.Content; using DatReaderWriter; namespace AcDream.App.UI.Layout; @@ -7,7 +7,7 @@ namespace AcDream.App.UI.Layout; /// DAT-authored ten-step radial cooldown art owned by the shared retail /// UIElement_UIItem prototype. /// -internal readonly record struct ItemCooldownAssets(IReadOnlyList Sprites) +public readonly record struct ItemCooldownAssets(IReadOnlyList Sprites) { public const uint CatalogLayoutId = 0x21000037u; public const uint SharedItemPrototypeId = 0x1000033Eu; diff --git a/src/AcDream.App/UI/Layout/ItemCooldownUiController.cs b/src/AcDream.App/UI/Layout/ItemCooldownUiController.cs index 9e7a4cc2..c4ed53a6 100644 --- a/src/AcDream.App/UI/Layout/ItemCooldownUiController.cs +++ b/src/AcDream.App/UI/Layout/ItemCooldownUiController.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Items; +using AcDream.Core.Items; using AcDream.Core.Spells; namespace AcDream.App.UI.Layout; @@ -8,7 +8,7 @@ namespace AcDream.App.UI.Layout; /// heartbeat before drawing and gives every current and future /// the same display projection. /// -internal sealed class ItemCooldownUiController +public sealed class ItemCooldownUiController { private readonly Spellbook _spellbook; private readonly ClientObjectTable _objects; diff --git a/src/AcDream.App/UI/Layout/ItemListCellTemplate.cs b/src/AcDream.App/UI/Layout/ItemListCellTemplate.cs index 651d2a71..ea6a53e5 100644 --- a/src/AcDream.App/UI/Layout/ItemListCellTemplate.cs +++ b/src/AcDream.App/UI/Layout/ItemListCellTemplate.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using DatReaderWriter; using AcDream.Content; using DatReaderWriter.DBObjs; @@ -13,7 +13,7 @@ namespace AcDream.App.UI.Layout; /// shared UIItem catalog LayoutDesc, = 0x21000037] -> CreateChildElement(catalog, id); the cloned /// prototype's ItemSlot_Empty (state 0x1000001c) media is the empty-cell background. /// -internal static class ItemListCellTemplate +public static class ItemListCellTemplate { /// The shared UIItem cell-template catalog. Hardcoded: retail resolves it via /// GetByEnum(0x10000038,5,0x23) through a master enum-map DAT object (no code literal); @@ -87,14 +87,14 @@ internal static class ItemListCellTemplate // child search alone returns nothing for containers. We deliberately do NOT use the // prototype's DirectState child overlay: on the container prototype that child is the // open/selected-container TRIANGLE indicator (0x06005D9C), which retail draws ONLY on the - // selected container (a deferred container-selection feature) — never as empty-cell art. + // selected container (a deferred container-selection feature) — never as empty-cell art. // (Live visual gate 2026-06-22: frame-first stamped the triangle onto every empty cell.) return FindIconEmpty(catalog, proto, new HashSet()); } - // ── attribute 0x1000000e: stored as EnumBaseProperty in the dat (Value is the prototype id) ── + // ── attribute 0x1000000e: stored as EnumBaseProperty in the dat (Value is the prototype id) ── // Note: the spec anticipated DataIdBaseProperty/ArrayBaseProperty based on the font-DID pattern, - // but the live dat uses EnumBaseProperty.Value (uint) — confirmed by runtime reflection. + // but the live dat uses EnumBaseProperty.Value (uint) — confirmed by runtime reflection. private static uint ReadCellTemplateId(ElementDesc elem) { uint id = ReadIdFromState(elem.StateDesc); @@ -121,7 +121,7 @@ internal static class ItemListCellTemplate return 0; } - // ── prototype media: the m_elem_Icon (0x1000033B) ItemSlot_Empty, resolved through inheritance ── + // ── prototype media: the m_elem_Icon (0x1000033B) ItemSlot_Empty, resolved through inheritance ── // Find the icon child's empty media within `element`'s subtree, following BaseElement edges // within the same catalog (cycle-guarded by `baseSeen`). The 32x32 contents prototype carries // 0x1000033B as a direct child; the 36x36 container prototype reaches it only via an inherited @@ -171,7 +171,7 @@ internal static class ItemListCellTemplate return 0; } - // ── depth-first element search by id (LayoutImporter.FindDesc is private there) ── + // ── depth-first element search by id (LayoutImporter.FindDesc is private there) ── private static ElementDesc? FindDesc(LayoutDesc ld, uint id) { foreach (var kv in ld.Elements) diff --git a/src/AcDream.App/UI/Layout/JumpPowerbarController.cs b/src/AcDream.App/UI/Layout/JumpPowerbarController.cs index 7f72ba2f..ecdbd9b4 100644 --- a/src/AcDream.App/UI/Layout/JumpPowerbarController.cs +++ b/src/AcDream.App/UI/Layout/JumpPowerbarController.cs @@ -1,4 +1,4 @@ -using AcDream.App.Input; +using AcDream.App.Input; namespace AcDream.App.UI.Layout; @@ -13,7 +13,7 @@ namespace AcDream.App.UI.Layout; /// RecvNotice_FinishPowerbar (0x004DA580), and /// ClientCombatSystem::CommenceJump (0x0056AF90). /// -internal sealed class JumpPowerbarController : IRetainedPanelController +public sealed class JumpPowerbarController : IRetainedPanelController { public const uint LayoutId = 0x21000072u; public const uint MeterId = 0x10000034u; diff --git a/src/AcDream.App/UI/Layout/LayoutImporter.cs b/src/AcDream.App/UI/Layout/LayoutImporter.cs index 5c6648e4..43418270 100644 --- a/src/AcDream.App/UI/Layout/LayoutImporter.cs +++ b/src/AcDream.App/UI/Layout/LayoutImporter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using AcDream.Content; using DatReaderWriter; @@ -12,7 +12,7 @@ namespace AcDream.App.UI.Layout; /// The result of importing a retail LayoutDesc: a tree with /// an O(1) lookup table for finding any element by its dat id. /// -internal sealed class ImportedLayout +public sealed class ImportedLayout { /// Root widget of the imported tree. public UiElement Root { get; } @@ -37,7 +37,7 @@ internal sealed class ImportedLayout /// /// Pure layer ( / ): /// converts a pre-resolved tree into a -/// tree via . Testable without dats or OpenGL — all tests +/// tree via . Testable without dats or OpenGL — all tests /// in LayoutImporterTests.cs exercise this layer only. /// /// @@ -55,9 +55,9 @@ internal sealed class ImportedLayout /// Every other element type recurses its children generically. /// /// -internal static class LayoutImporter +public static class LayoutImporter { - // ── Pure layer ──────────────────────────────────────────────────────────── + // ── Pure layer ──────────────────────────────────────────────────────────── /// /// Convenience for tests: attach to @@ -68,7 +68,7 @@ internal static class LayoutImporter public static ImportedLayout BuildFromInfos( ElementInfo rootInfo, IEnumerable children, - Func resolve, + Func resolve, UiDatFont? datFont, Func? fontResolve = null, Func? stringResolve = null) @@ -81,15 +81,15 @@ internal static class LayoutImporter /// Pure builder: produce the widget tree from a fully resolved /// tree (children already attached). /// - /// Optional per-element font resolver — FontDid → + /// Optional per-element font resolver — FontDid → /// (or null if the font can't be loaded). When supplied, /// elements with a non-zero get their own dat /// font at build time instead of the shared fallback. /// Null preserves the original single-font behavior for all callers that don't - /// pass it — no behavior change for the live game path. + /// pass it — no behavior change for the live game path. public static ImportedLayout Build( ElementInfo rootInfo, - Func resolve, + Func resolve, UiDatFont? datFont, Func? fontResolve = null, Func? stringResolve = null) @@ -100,7 +100,7 @@ internal static class LayoutImporter var root = BuildWidget(rootInfo, resolve, datFont, fontResolve, stringResolve, byId); if (root is null) { - Console.WriteLine($"[D.2b] LayoutImporter: root element 0x{rootInfo.Id:X8} (type {rootInfo.Type}) produced no widget — using empty container fallback."); + Console.WriteLine($"[D.2b] LayoutImporter: root element 0x{rootInfo.Id:X8} (type {rootInfo.Type}) produced no widget — using empty container fallback."); root = new UiDatElement(rootInfo, resolve); } return new ImportedLayout(root, byId); @@ -108,20 +108,20 @@ internal static class LayoutImporter private static UiElement? BuildWidget( ElementInfo info, - Func resolve, + Func resolve, UiDatFont? datFont, Func? fontResolve, Func? stringResolve, Dictionary byId) { var w = DatWidgetFactory.Create(info, resolve, datFont, fontResolve, stringResolve); - if (w is null) return null; // Type-12 style prototype — skip + if (w is null) return null; // Type-12 style prototype — skip if (info.Id != 0) byId[info.Id] = w; // Behavioral widgets that draw their full appearance + reproduce their dat // sub-elements procedurally (Meter's 3-slice, Menu's label/rows, Field/Text caps, - // Button labels, Scrollbar arrows) CONSUME their dat children — building those as + // Button labels, Scrollbar arrows) CONSUME their dat children — building those as // separate widgets double-draws and lets an invisible child steal pointer/focus // from the behavioral widget (e.g. the channel Menu's label child intercepting the // button click). Only generic containers (UiDatElement, panels) recurse. See @@ -152,7 +152,7 @@ internal static class LayoutImporter // double-draw the bar art). All other child types are built normally. // // Safe for vitals: the health/stamina/mana meters have ONLY Type-3 slice children - // (no text children). This loop finds nothing for them → no change to vitals. + // (no text children). This loop finds nothing for them → no change to vitals. foreach (var child in info.Children) { if (child.Type == 3) continue; // slice containers: already consumed by BuildMeter @@ -171,7 +171,7 @@ internal static class LayoutImporter return w; } - // ── Dat shell ───────────────────────────────────────────────────────────── + // ── Dat shell ───────────────────────────────────────────────────────────── /// /// Dat shell, ElementInfo half: load the layout + resolve inheritance + build the @@ -187,7 +187,7 @@ internal static class LayoutImporter // Collect the set of element ids that are referenced as a BaseElement by ANY // element in THIS layout (where BaseLayoutId == layoutId). Such elements are - // purely inheritance templates ("prototypes") — retail never instantiates them + // purely inheritance templates ("prototypes") — retail never instantiates them // as live widgets. Example: the toolbar slot prototype 0x100001B2 in LayoutDesc // 0x21000016, which all 18 slot elements inherit from and which has no own media. // @@ -258,7 +258,7 @@ internal static class LayoutImporter /// state an element starts in (e.g., Normal, Minimized). It does /// NOT encode visibility of sibling Group containers. The StateDesc's /// contains X/Y/Width/Height/ - /// ZLevel/PassToChildren — there is no Visible flag. + /// ZLevel/PassToChildren — there is no Visible flag. /// /// /// @@ -276,7 +276,7 @@ internal static class LayoutImporter public static ImportedLayout? Import( IDatReaderWriter dats, uint layoutId, - Func resolve, + Func resolve, UiDatFont? datFont, Func? fontResolve = null) { @@ -291,7 +291,7 @@ internal static class LayoutImporter IDatReaderWriter dats, uint layoutId, uint rootElementId, - Func resolve, + Func resolve, UiDatFont? datFont, Func? fontResolve = null) { @@ -301,7 +301,7 @@ internal static class LayoutImporter return Build(rootInfo, resolve, datFont, fontResolve, strings.Resolve); } - // ── Inheritance resolution ──────────────────────────────────────────────── + // ── Inheritance resolution ──────────────────────────────────────────────── /// True when a pure-container inheritor needs the mounted-base Z-layer /// correction. Child inheritance itself is unconditional and follows retail @@ -353,9 +353,9 @@ internal static class LayoutImporter // The mounted slot's layer WITHIN THE FRAME is its OWN ZLevel, not the mounted // sub-window root's. The gm*UI sub-window roots carry ZLevel 1000 (their standalone // top-window layer); ElementReader.Merge's zero-wins-base rule made the slot (own - // ZLevel 0) inherit that 1000, and the #145 ZOrder fold (ReadOrder − ZLevel·10000) - // turns 1000 into ZOrder ≈ −10,000,000 — sinking the whole panel BEHIND the frame's - // Alphablend backdrop (ZLevel 100 → ≈ −1,000,000). The backdrop then overpaints the + // ZLevel 0) inherit that 1000, and the #145 ZOrder fold (ReadOrder − ZLevel·10000) + // turns 1000 into ZOrder ≈ −10,000,000 — sinking the whole panel BEHIND the frame's + // Alphablend backdrop (ZLevel 100 → ≈ −1,000,000). The backdrop then overpaints the // panel's captions/meter/cells (the wash-out bug; the paperdoll root happens to be // ZLevel 0 so it escaped). Restore the slot's own frame-layer so the panel sits in // FRONT of the backdrop. (B-Controller debug 2026-06-21; continuation of #145.) @@ -452,7 +452,7 @@ internal static class LayoutImporter if (d.StateDesc is not null) ReadState(d.StateDesc, UiStateInfo.DirectStateId, "", info); - // Named states (e.g. UIStateId.HideDetail → "HideDetail"). + // Named states (e.g. UIStateId.HideDetail → "HideDetail"). foreach (var s in d.States) ReadState(s.Value, (uint)s.Key, s.Key.ToString(), info); @@ -464,7 +464,7 @@ internal static class LayoutImporter /// Read the first from into /// info.StateMedia[name], read any into /// info.StateCursors[name], and extract the font DID from property 0x1A - /// (ArrayBaseProperty → DataIdBaseProperty) if not yet set. + /// (ArrayBaseProperty → DataIdBaseProperty) if not yet set. /// private static void ReadState(StateDesc sd, uint stateId, string name, ElementInfo info) { @@ -504,7 +504,7 @@ internal static class LayoutImporter info.States[stateId] = state; // Font DID: Properties[0x1A] is ArrayBaseProperty{ DataIdBaseProperty }. - // Format doc §3: "ArrayBaseProperty containing ONE DataIdBaseProperty". + // Format doc §3: "ArrayBaseProperty containing ONE DataIdBaseProperty". if (info.FontDid == 0 && sd.Properties is not null && sd.Properties.TryGetValue(0x1Au, out var raw) && raw is ArrayBaseProperty arr && arr.Value.Count > 0 @@ -547,15 +547,15 @@ internal static class LayoutImporter }; } - // ColorBaseProperty (0x1B): ARGB bytes → normalized [0,1] Vector4 (R,G,B,A). + // ColorBaseProperty (0x1B): ARGB bytes → normalized [0,1] Vector4 (R,G,B,A). // Only read when not already set (first dat state wins; Merge propagates from base). if (info.FontColor is null && sd.Properties.TryGetValue(0x1Bu, out var cRaw) && cRaw is ColorBaseProperty cProp) { var c = cProp.Value; - // ColorARGB stores components as bytes (0–255); normalize to [0,1] for Vector4. - // Alpha=0 in the dat typically means fully opaque (retail convention: 0 → 255). + // ColorARGB stores components as bytes (0–255); normalize to [0,1] for Vector4. + // Alpha=0 in the dat typically means fully opaque (retail convention: 0 → 255). float a = c.Alpha == 0 ? 1f : c.Alpha / 255f; info.FontColor = new System.Numerics.Vector4(c.Red / 255f, c.Green / 255f, c.Blue / 255f, a); } @@ -638,7 +638,7 @@ internal static class LayoutImporter return value; } - // ── Prototype detection helpers ─────────────────────────────────────────── + // ── Prototype detection helpers ─────────────────────────────────────────── /// /// Recursively walks and all its children, adding to @@ -656,7 +656,7 @@ internal static class LayoutImporter } /// - /// Returns true when carries no own state media — i.e. its + /// Returns true when carries no own state media — i.e. its /// StateDesc (DirectState) and States (named states) yield no /// entries with a non-zero file id. /// Such elements are pure inheritance templates with no rendering content. @@ -669,7 +669,7 @@ internal static class LayoutImporter return info.StateMedia.Count == 0; } - // ── Element tree search ─────────────────────────────────────────────────── + // ── Element tree search ─────────────────────────────────────────────────── /// /// Find an by id anywhere in the top-level tree of @@ -696,7 +696,7 @@ internal static class LayoutImporter return null; } - // ── Raw-edge layout provenance ──────────────────────────────────────────── + // ── Raw-edge layout provenance ──────────────────────────────────────────── private static void SetOriginalParentSize(ElementInfo child, float width, float height) { diff --git a/src/AcDream.App/UI/Layout/LinkStatusUiController.cs b/src/AcDream.App/UI/Layout/LinkStatusUiController.cs index 03c3f75a..4d461fc1 100644 --- a/src/AcDream.App/UI/Layout/LinkStatusUiController.cs +++ b/src/AcDream.App/UI/Layout/LinkStatusUiController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using AcDream.Core.Net; @@ -10,7 +10,7 @@ namespace AcDream.App.UI.Layout; /// chrome; this controller owns its five-second text refresh and 120-second /// ping cadence. /// -internal sealed class LinkStatusUiController : IRetainedPanelController +public sealed class LinkStatusUiController : IRetainedPanelController { public const uint LayoutId = 0x2100001Du; public const uint RootId = 0x10000167u; @@ -126,7 +126,7 @@ internal sealed class LinkStatusUiController : IRetainedPanelController } } -internal sealed record LinkStatusStrings( +public sealed record LinkStatusStrings( string Description, string Legend, string DisconnectWarning, diff --git a/src/AcDream.App/UI/Layout/MiniGameUiController.cs b/src/AcDream.App/UI/Layout/MiniGameUiController.cs index b0e2c41c..6fcda3f3 100644 --- a/src/AcDream.App/UI/Layout/MiniGameUiController.cs +++ b/src/AcDream.App/UI/Layout/MiniGameUiController.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace AcDream.App.UI.Layout; @@ -7,7 +7,7 @@ namespace AcDream.App.UI.Layout; /// ghosted until a future game-state owner calls SetMiniGameActive; /// mounting the page now keeps its window lifecycle on the shared retail host. /// -internal sealed class MiniGameUiController : IRetainedPanelController +public sealed class MiniGameUiController : IRetainedPanelController { public const uint LayoutId = 0x2100001Eu; public const uint RootId = 0x1000016Au; diff --git a/src/AcDream.App/UI/Layout/PaperdollClickMap.cs b/src/AcDream.App/UI/Layout/PaperdollClickMap.cs index 99de82b3..aed5a3f5 100644 --- a/src/AcDream.App/UI/Layout/PaperdollClickMap.cs +++ b/src/AcDream.App/UI/Layout/PaperdollClickMap.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Items; +using AcDream.Core.Items; using AcDream.Core.Textures; using AcDream.Content; using DatReaderWriter; @@ -11,7 +11,7 @@ namespace AcDream.App.UI.Layout; /// gmPaperDollUI::CreateClickMap @ 0x004A4850 and /// gmPaperDollUI::GetPaperDollItemUnderMouse @ 0x004A4920. /// -internal sealed class PaperdollClickMap +public sealed class PaperdollClickMap { public const uint ClickMapEnum = 0x1000000Cu; public const uint InterfaceEnumCategory = 7u; diff --git a/src/AcDream.App/UI/Layout/PaperdollController.cs b/src/AcDream.App/UI/Layout/PaperdollController.cs index 48599a29..9d84f10b 100644 --- a/src/AcDream.App/UI/Layout/PaperdollController.cs +++ b/src/AcDream.App/UI/Layout/PaperdollController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using AcDream.App.UI; using AcDream.Core.Items; @@ -10,19 +10,19 @@ namespace AcDream.App.UI.Layout; /// Binds the 24 equip slots mounted under the paperdoll (gmPaperDollUI 0x21000024, nested in the /// inventory frame 0x21000023) to live equipped-item data and makes them drag-drop WIELD targets. /// The acdream analogue of gmPaperDollUI::PostInit + GetLocationInfoFromElementID (named-retail decomp -/// 175480 / 173620). Slice 1: equip slots only — no 3D doll viewport (that's Slice 2). +/// 175480 / 173620). Slice 1: equip slots only — no 3D doll viewport (that's Slice 2). /// Unwield is handled by InventoryController (dragging an equipped item onto the pack grid). /// -internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanelController +public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelController { public const uint DollViewportId = 0x100001D5u; public const uint DollDragMaskId = 0x100001D6u; - // ── Slots-toggle public surface ─────────────────────────────────────────────────────────────── + // ── Slots-toggle public surface ─────────────────────────────────────────────────────────────── /// /// The 9 armor-slot element-ids whose Visible state the Slots button (0x100005BE) toggles. /// Doll-view: hidden. Slot-view: shown. Source: gmPaperDollUI::ListenToElementMessage decomp - /// 175674-175706 — these are the only 9 ids that element flips. The 12 ordinary non-armor + /// 175674-175706 — these are the only 9 ids that element flips. The 12 ordinary non-armor /// lists remain visible in both views; the three Aetheria lists are independently unlock-gated. /// public static readonly uint[] ArmorSlotElementIds = @@ -36,7 +36,7 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel /// Default is doll-view (SlotView == false): the 3-D character is visible, /// the 9 armor slots are hidden. Calling Toggle() alternates between views. /// - internal sealed class PaperdollViewState + public sealed class PaperdollViewState { public bool SlotView { get; private set; } // false = doll-view (default) public bool DollVisible => !SlotView; @@ -46,8 +46,8 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel private readonly ClientObjectTable _objects; private readonly Func _playerGuid; - private readonly Func _iconIds; - private readonly Func? _dragIconIds; + private readonly Func _iconIds; + private readonly Func? _dragIconIds; private readonly ItemInteractionController _itemInteraction; private readonly bool _ownsItemInteraction; private readonly SelectionState _selection; @@ -55,7 +55,7 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel private readonly List<(EquipMask Mask, UiItemList List)> _slots = new(); private readonly List<(AetheriaUnlockState Bit, UiItemList List)> _aetheriaSlots = new(); - // ── Slots-toggle state ──────────────────────────────────────────────────────────────────────── + // ── Slots-toggle state ──────────────────────────────────────────────────────────────────────── private readonly PaperdollViewState _viewState = new(); private readonly List _armorSlots = new(); private UiElement? _dollViewport; // UiViewport wired in Slice 3; UiElement? keeps this task independent @@ -64,11 +64,11 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel private PaperdollController( ImportedLayout layout, ClientObjectTable objects, Func playerGuid, - Func iconIds, SelectionState selection, + Func iconIds, SelectionState selection, ItemInteractionController itemInteraction, uint emptySlotSprite, UiDatFont? datFont, PaperdollClickMap? clickMap, - Func? dragIconIds, + Func? dragIconIds, IReadOnlyDictionary? emptySlotSprites, bool ownsItemInteraction) { @@ -97,10 +97,10 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel if (list.Cell.ItemId != 0) _itemInteraction?.ActivateItem(list.Cell.ItemId); }; - // 3D character (the "figure" — Slice 1/2 correction: NOT a per-slot + // 3D character (the "figure" — Slice 1/2 correction: NOT a per-slot // silhouette) is the doll viewport, which arrives in Slice 2 with the Slots toggle. // Cell.SpriteResolve + the default accept/reject sprites (ItemSlot_DragOver_Accept ring - // 0x060011F9 / reject circle 0x060011F8 — the discrete-slot frames, NOT the inventory grid's + // 0x060011F9 / reject circle 0x060011F8 — the discrete-slot frames, NOT the inventory grid's // insert-arrow 0x060011F7) are already wired by DatWidgetFactory when it built the UiItemList; // no need to re-set them here. _slots.Add((mask, list)); @@ -115,7 +115,7 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel _objects.Cleared += OnObjectsCleared; _selection.Changed += OnSelectionChanged; - // ── Slots-toggle wiring ─────────────────────────────────────────────────────────────────── + // ── Slots-toggle wiring ─────────────────────────────────────────────────────────────────── foreach (var id in ArmorSlotElementIds) if (layout.FindElement(id) is UiItemList armor) _armorSlots.Add(armor); @@ -166,11 +166,11 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel public static PaperdollController Bind( ImportedLayout layout, ClientObjectTable objects, Func playerGuid, - Func iconIds, SelectionState selection, + Func iconIds, SelectionState selection, ItemInteractionController itemInteraction, uint emptySlotSprite = 0u, UiDatFont? datFont = null, PaperdollClickMap? clickMap = null, - Func? dragIconIds = null, + Func? dragIconIds = null, IReadOnlyDictionary? emptySlotSprites = null, bool ownsItemInteraction = false) => new PaperdollController( @@ -221,7 +221,7 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel Populate(); } - /// The object belongs to the player (wielded gear or pack contents) — so a change to it may + /// The object belongs to the player (wielded gear or pack contents) — so a change to it may /// add/remove/repaint a doll slot. Player-scoped: an NPC's or vendor's wielded item (which also carries /// CurrentlyEquippedLocation from the wire) must NOT trigger a repaint. A player-equipped item always /// has WielderId==p (login, from CreateObject) or ContainerId==p (live/optimistic wield, set by @@ -251,9 +251,9 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel if ((o.CurrentlyEquippedLocation & mask) != EquipMask.None) { worn = o; break; } if (worn is null) { list.Cell.Clear(); continue; } - GpuTextureSlot tex = _iconIds(worn.Type, worn.IconId, worn.IconUnderlayId, worn.IconOverlayId, worn.Effects); - GpuTextureSlot? dragTex = _dragIconIds?.Invoke( - worn.Type, worn.IconId, worn.IconUnderlayId, worn.IconOverlayId, worn.Effects); + uint tex = _iconIds(worn.Type, worn.IconId, worn.IconUnderlayId, worn.IconOverlayId, worn.Effects); + uint dragTex = _dragIconIds?.Invoke( + worn.Type, worn.IconId, worn.IconUnderlayId, worn.IconOverlayId, worn.Effects) ?? 0u; list.Cell.SetItem(worn.ObjectId, tex, dragIconTexture: dragTex); } ApplyAetheriaVisibility(); @@ -287,10 +287,10 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel return EquipMask.None; } - // ── IItemListDragHandler ────────────────────────────────────────────────────────────────────── + // ── IItemListDragHandler ────────────────────────────────────────────────────────────────────── /// Selects the wielded item before the waiting mesh appears. The item itself stays put /// until the server confirms, like the inventory grid and unlike the toolbar's remove-on-lift. - /// Unwield happens on DROP onto the pack grid — InventoryController. + /// Unwield happens on DROP onto the pack grid — InventoryController. public void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload) { // UIElement_ItemList::ItemList_BeginDrag @ 0x004E32D0. diff --git a/src/AcDream.App/UI/Layout/PaperdollSlotBackgrounds.cs b/src/AcDream.App/UI/Layout/PaperdollSlotBackgrounds.cs index 7994838a..6fef9fdf 100644 --- a/src/AcDream.App/UI/Layout/PaperdollSlotBackgrounds.cs +++ b/src/AcDream.App/UI/Layout/PaperdollSlotBackgrounds.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using AcDream.Core.Items; using AcDream.Content; @@ -16,7 +16,7 @@ namespace AcDream.App.UI.Layout; /// Cell creation: UIElement_ItemList::InternalCreateItem @ 0x004E3570. /// Prototype media: live DAT catalog LayoutDesc 0x21000037. /// -internal static class PaperdollSlotBackgrounds +public static class PaperdollSlotBackgrounds { internal readonly record struct Definition( uint Element, diff --git a/src/AcDream.App/UI/Layout/RadarController.cs b/src/AcDream.App/UI/Layout/RadarController.cs index 23ec5f8c..1097691b 100644 --- a/src/AcDream.App/UI/Layout/RadarController.cs +++ b/src/AcDream.App/UI/Layout/RadarController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.UI; @@ -11,7 +11,7 @@ namespace AcDream.App.UI.Layout; /// LayoutDesc 0x21000074 tree. Like the other gm* controllers, this class only /// finds children by retail id and attaches live providers; it does not recreate DAT chrome. /// -internal sealed class RadarController : IRetainedPanelController +public sealed class RadarController : IRetainedPanelController { public const uint LayoutId = 0x21000074u; /// Production layout property 0x1000002D, recovered directly from the retail DAT. diff --git a/src/AcDream.App/UI/Layout/RadarSnapshotProvider.cs b/src/AcDream.App/UI/Layout/RadarSnapshotProvider.cs index 995c8875..004a357b 100644 --- a/src/AcDream.App/UI/Layout/RadarSnapshotProvider.cs +++ b/src/AcDream.App/UI/Layout/RadarSnapshotProvider.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.App.World; using AcDream.Core.Items; using AcDream.Core.Net; @@ -15,7 +15,7 @@ namespace AcDream.App.UI.Layout; /// AC-specific classification and math decision, while the retained widget remains a /// backend-only renderer. /// -internal sealed class RadarSnapshotProvider +public sealed class RadarSnapshotProvider { private static readonly Vector2 ProductionCenter = new(60f, 60f); diff --git a/src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs b/src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs index fbc79c00..7546525f 100644 --- a/src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs +++ b/src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs @@ -1,4 +1,4 @@ -using AcDream.Content; +using AcDream.Content; using AcDream.Core.Items; using DatReaderWriter; using DatReaderWriter.DBObjs; @@ -14,7 +14,7 @@ namespace AcDream.App.UI.Layout; /// AppraisalSystem::InqCreatureDisplayName @ 0x005B59E0 and /// InqHeritageGroupDisplayName @ 0x005B4710. /// -internal sealed class RetailAppraisalNameResolver +public sealed class RetailAppraisalNameResolver { private const uint MaterialClientEnum = 0x10000001u; private const uint MaterialSubEnum = 1u; diff --git a/src/AcDream.App/UI/Layout/RetailDialogData.cs b/src/AcDream.App/UI/Layout/RetailDialogData.cs index 82d47584..debec420 100644 --- a/src/AcDream.App/UI/Layout/RetailDialogData.cs +++ b/src/AcDream.App/UI/Layout/RetailDialogData.cs @@ -1,10 +1,10 @@ -namespace AcDream.App.UI.Layout; +namespace AcDream.App.UI.Layout; /// /// Property identifiers consumed by retail's Dialog and /// DialogFactory implementations. /// -internal static class RetailDialogProperty +public static class RetailDialogProperty { public const uint Priority = 0x8Du; public const uint Type = 0x8Eu; @@ -43,7 +43,7 @@ public enum RetailDialogType : uint /// The raw numeric keys remain visible because type-specific presenters and semantic /// callbacks both extend the same collection in retail. /// -internal sealed class RetailDialogData +public sealed class RetailDialogData { private readonly Dictionary _values = new(); diff --git a/src/AcDream.App/UI/Layout/RetailDialogFactory.cs b/src/AcDream.App/UI/Layout/RetailDialogFactory.cs index 56b6e3f1..f4c71c2f 100644 --- a/src/AcDream.App/UI/Layout/RetailDialogFactory.cs +++ b/src/AcDream.App/UI/Layout/RetailDialogFactory.cs @@ -1,11 +1,11 @@ -namespace AcDream.App.UI.Layout; +namespace AcDream.App.UI.Layout; /// /// Retained-mode port of retail DialogFactory @ 0x004773C0..0x00478470. /// It owns dialog contexts, independent FIFO queue groups, nonqueued dialogs, /// priority preemption, callback delivery, close notices, and fresh catalog roots. /// -internal sealed class RetailDialogFactory : IDisposable +public sealed class RetailDialogFactory : IDisposable { public const uint DefaultQueueKey = 2u; public const uint NonQueuedKey = 1u; diff --git a/src/AcDream.App/UI/Layout/RetailFpsController.cs b/src/AcDream.App/UI/Layout/RetailFpsController.cs index c1cf3fa2..b3a299b2 100644 --- a/src/AcDream.App/UI/Layout/RetailFpsController.cs +++ b/src/AcDream.App/UI/Layout/RetailFpsController.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; namespace AcDream.App.UI.Layout; @@ -13,7 +13,7 @@ namespace AcDream.App.UI.Layout; /// string 0x0DCFFF73 supplies the two labels FPS: and DEG:; /// retail inserts both floating-point values with two decimal places. /// -internal sealed class RetailFpsController +public sealed class RetailFpsController { public const uint LayoutId = 0x2100000Fu; public const uint DisplayElementId = 0x10000047u; diff --git a/src/AcDream.App/UI/Layout/RetailPanelUiController.cs b/src/AcDream.App/UI/Layout/RetailPanelUiController.cs index 80b84812..c5e9c8d5 100644 --- a/src/AcDream.App/UI/Layout/RetailPanelUiController.cs +++ b/src/AcDream.App/UI/Layout/RetailPanelUiController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; namespace AcDream.App.UI.Layout; @@ -9,7 +9,7 @@ namespace AcDream.App.UI.Layout; /// deferred child; this owner applies the same lifecycle to registered retained /// windows without making the toolbar authoritative for non-toolbar panels. /// -internal sealed class RetailPanelUiController : IDisposable +public sealed class RetailPanelUiController : IDisposable { public const uint RestorePreviousPropertyId = 0x10000049u; diff --git a/src/AcDream.App/UI/Layout/RetailWindowFrame.cs b/src/AcDream.App/UI/Layout/RetailWindowFrame.cs index f94176fe..d9b22a22 100644 --- a/src/AcDream.App/UI/Layout/RetailWindowFrame.cs +++ b/src/AcDream.App/UI/Layout/RetailWindowFrame.cs @@ -1,9 +1,9 @@ -using System; +using System; namespace AcDream.App.UI.Layout; /// How a production retained window obtains its outer chrome. -internal enum RetailWindowChrome +public enum RetailWindowChrome { /// The imported root already is the complete retail outer frame. Imported, @@ -21,9 +21,9 @@ internal enum RetailWindowChrome /// shared nine-slice wrapper, applies exact resize/opacity/visibility policy, mounts /// the outer frame, and returns its registered typed handle. /// -internal static class RetailWindowFrame +public static class RetailWindowFrame { - internal sealed record Options + public sealed record Options { public required string WindowName { get; init; } public RetailWindowChrome Chrome { get; init; } = RetailWindowChrome.NineSlice; @@ -87,7 +87,7 @@ internal static class RetailWindowFrame public static RetailWindowHandle Mount( UiRoot root, UiElement content, - Func resolveChrome, + Func resolveChrome, Options options) { ArgumentNullException.ThrowIfNull(root); diff --git a/src/AcDream.App/UI/Layout/SelectedObjectController.cs b/src/AcDream.App/UI/Layout/SelectedObjectController.cs index 58544f8c..224f9de8 100644 --- a/src/AcDream.App/UI/Layout/SelectedObjectController.cs +++ b/src/AcDream.App/UI/Layout/SelectedObjectController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; using AcDream.App.UI; using AcDream.Core.Items; @@ -7,7 +7,7 @@ using AcDream.Core.Selection; namespace AcDream.App.UI.Layout; /// -/// Controller for the action bar's selected-object strip (ids 0x1000019E–0x100001A4). +/// Controller for the action bar's selected-object strip (ids 0x1000019E–0x100001A4). /// Analogue of retail gmToolbarUI::HandleSelectionChanged /// (docs/research/named-retail/acclient_2013_pseudo_c.txt:198635) + /// RecvNotice_UpdateObjectHealth (:196213) + @@ -18,29 +18,29 @@ namespace AcDream.App.UI.Layout; /// guid is provided it sets the name, flashes the selection overlay briefly, and sends /// either QueryHealth (0x01BF) for health-bearing targets or /// QueryItemMana (0x0263) for owned non-stack items. The Health meter -/// becomes visible only when the server actually reports health for the selected guid — +/// becomes visible only when the server actually reports health for the selected guid — /// either an UpdateHealth (0x01C0) arrives (retail -/// RecvNotice_UpdateObjectHealth → SetVisible(1)) or the value is already +/// RecvNotice_UpdateObjectHealthSetVisible(1)) or the value is already /// cached. So a friendly NPC you have not assessed shows name-only (no bar), and a -/// monster's bar appears after damage / a successful assess — matching retail. +/// monster's bar appears after damage / a successful assess — matching retail. /// /// /// /// Retail element roles (PostInit, :198119): m_pSelObjectField /// is the container 0x1000019E whose SetState(0x1000000b/0c) drives a -/// 0.25s Pause→Normal flash that cascades to the overlay child's green frame. +/// 0.25s Pause→Normal flash that cascades to the overlay child's green frame. /// acdream has no state-cascade / transition-animation system, so this controller drives /// the overlay element 0x100001A0 directly and reverts it after the same /// to reproduce the brief flash. The name element /// 0x1000019F is bumped to the top of the strip's z-order so it draws OVER the -/// overlay frame and the health bar (retail draws the name over the bar — see the +/// overlay frame and the health bar (retail draws the name over the bar — see the /// "Drudge Slinker" reference shot). /// /// /// -internal sealed class SelectedObjectController : IRetainedPanelController +public sealed class SelectedObjectController : IRetainedPanelController { - // ── Element ids (toolbar LayoutDesc 0x21000016) ───────────────────────── + // ── Element ids (toolbar LayoutDesc 0x21000016) ───────────────────────── /// Selected-object container / field element id (retail m_pSelObjectField). public const uint ContainerId = 0x1000019E; /// Selected-object name element id (retail m_pSelObjectName, UIElement_Text). @@ -56,24 +56,24 @@ internal sealed class SelectedObjectController : IRetainedPanelController /// Horizontal stack quantity slider (retail m_pStackSizeSlider). public const uint StackSizeSliderId = 0x100001A4; - /// Selection-overlay flash duration — retail's container ObjectSelected state is a - /// Pause(0.25s)→Normal transition (toolbar dump, element 0x1000019E). + /// Selection-overlay flash duration — retail's container ObjectSelected state is a + /// Pause(0.25s)→Normal transition (toolbar dump, element 0x1000019E). private const double FlashSeconds = 0.25; /// Z-order for the name so it draws OVER the overlay frame + health bar. - /// The strip's other children sit at ReadOrder 1–4; this floats the name to the top. + /// The strip's other children sit at ReadOrder 1–4; this floats the name to the top. private const int NameZOrderOnTop = 1_000_000; - /// Z-order for the selection-flash overlay — above the health meter (so the green + /// Z-order for the selection-flash overlay — above the health meter (so the green /// flash isn't hidden by the bar) but below the name (so the name stays readable). private const int OverlayZOrder = NameZOrderOnTop - 1; /// Height (px) of the black name band at the top of the 31px bar sprite. The name - /// label is constrained to this band (top-aligned) so the health bar shows below it — + /// label is constrained to this band (top-aligned) so the health bar shows below it — /// retail "name on the black, bar below". The bar sprite's colored region starts ~y14. private const float NameBandHeight = 15f; - // ── Found elements (any may be null for partial/test layouts) ─────────── + // ── Found elements (any may be null for partial/test layouts) ─────────── private readonly UiElement? _name; private readonly UiDatElement? _overlay; private readonly UiMeter? _healthMeter; @@ -81,7 +81,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController private readonly UiField? _stackSizeEntry; private readonly UiScrollbar? _stackSizeSlider; - // ── Captured delegates ─────────────────────────────────────────────────── + // ── Captured delegates ─────────────────────────────────────────────────── private readonly Func _isHealthTarget; private readonly Func _isOwnedByPlayer; private readonly Func _resolveName; @@ -97,7 +97,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController private readonly Action> _unsubscribeItemManaChanged; private readonly Action> _unsubscribeObjectUpdated; - // ── Live state (read by closures on the per-frame draw path) ──────────── + // ── Live state (read by closures on the per-frame draw path) ──────────── private uint? _current; private string? _currentName; private double _flashRemaining; // > 0 while the selection overlay is flashing @@ -143,7 +143,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController _unsubscribeItemManaChanged = unsubscribeItemManaChanged; _unsubscribeObjectUpdated = unsubscribeObjectUpdated; - // Find elements — silently skip absent ones (partial/test layouts). + // Find elements — silently skip absent ones (partial/test layouts). _name = layout.FindElement(NameId); _overlay = layout.FindElement(OverlayId) as UiDatElement; _healthMeter = layout.FindElement(HealthMeterId) as UiMeter; @@ -152,7 +152,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController _stackSizeSlider = layout.FindElement(StackSizeSliderId) as UiScrollbar; // The selection-flash overlay must draw OVER the health meter (which spans the whole - // strip) — otherwise the meter hides the green flash whenever a bar is visible (i.e. + // strip) — otherwise the meter hides the green flash whenever a bar is visible (i.e. // for players/monsters). Float it just below the name so the name stays readable. if (_overlay is not null) _overlay.ZOrder = OverlayZOrder; @@ -195,7 +195,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController // // The bar sprite (0x0600193E/F, 146x31) carries a ~14px BLACK name band across its // TOP with the colored bar in the lower portion (confirmed from the dat). Retail - // draws the object name in that black band with the health bar BELOW it — so the + // draws the object name in that black band with the health bar BELOW it — so the // label is TOP-aligned by constraining its height to the band, not centered over the // whole 31px strip (which overlapped the bar's middle). if (_name is not null) @@ -240,7 +240,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController /// Imported toolbar layout (LayoutDesc 0x21000016). /// The single Core selected-object owner. /// Called once with - /// (typical host: h => Combat.HealthChanged += h) — drives meter visibility. + /// (typical host: h => Combat.HealthChanged += h) — drives meter visibility. /// Returns true for guids that may show a health meter /// (proxy for retail's IsPlayer() || pet_owner || ObjectIsAttackable()). /// Returns retail's NAME_APPROPRIATE display name for a guid. @@ -296,8 +296,8 @@ internal sealed class SelectedObjectController : IRetainedPanelController _sendQueryItemMana(0); } - // ── 1. Clear first (retail: SetText("") + m_pSelObjectField->SetState(0) - // + SetVisible(0) on the meters). ────────────────────────────────────── + // ── 1. Clear first (retail: SetText("") + m_pSelObjectField->SetState(0) + // + SetVisible(0) on the meters). ────────────────────────────────────── if (selectionChanged) { if (_healthMeter is not null) _healthMeter.Visible = false; @@ -319,15 +319,15 @@ internal sealed class SelectedObjectController : IRetainedPanelController uint g = guid.Value; - // ── 2. Name (displayed via the UiText child's LinesProvider reading _currentName). ── + // ── 2. Name (displayed via the UiText child's LinesProvider reading _currentName). ── uint stackSize = _stackSize(g); string? objectName = _resolveName(g); _currentName = stackSize > 1u && !string.IsNullOrEmpty(objectName) ? $"{stackSize} {objectName}" : objectName; - // ── 3. Selection overlay: brief flash (retail container ObjectSelected - // = Pause(0.25s)→Normal). "StackedItemSelected" for stacks. ────────────── + // ── 3. Selection overlay: brief flash (retail container ObjectSelected + // = Pause(0.25s)→Normal). "StackedItemSelected" for stacks. ────────────── SetOverlayState(stackSize > 1u ? RetailUiStateIds.StackedItemSelected : RetailUiStateIds.ObjectSelected); @@ -344,9 +344,9 @@ internal sealed class SelectedObjectController : IRetainedPanelController if (_stackSizeSlider is not null) _stackSizeSlider.Visible = true; } - // ── 4. Health: query, and show the meter only if real health is already known. + // ── 4. Health: query, and show the meter only if real health is already known. // Otherwise the meter appears when OnHealthChanged fires for this guid - // (retail RecvNotice_UpdateObjectHealth :196213). ────────────────────────── + // (retail RecvNotice_UpdateObjectHealth :196213). ────────────────────────── if (stackSize <= 1u && _isHealthTarget(g)) { if (selectionChanged) @@ -378,7 +378,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController if (_flashRemaining <= 0) return; _flashRemaining -= deltaSeconds; if (_flashRemaining <= 0) - SetOverlayState(UiStateInfo.DirectStateId); // flash done → overlay back to blank + SetOverlayState(UiStateInfo.DirectStateId); // flash done → overlay back to blank } private void SetOverlayState(uint state) diff --git a/src/AcDream.App/UI/Layout/SpellExamineComponentTemplateFactory.cs b/src/AcDream.App/UI/Layout/SpellExamineComponentTemplateFactory.cs index 768ab17f..9d592114 100644 --- a/src/AcDream.App/UI/Layout/SpellExamineComponentTemplateFactory.cs +++ b/src/AcDream.App/UI/Layout/SpellExamineComponentTemplateFactory.cs @@ -1,4 +1,4 @@ -using AcDream.Content; +using AcDream.Content; using DatReaderWriter; namespace AcDream.App.UI.Layout; @@ -9,19 +9,19 @@ namespace AcDream.App.UI.Layout; /// the component icon as the template root's own UIRegion image while /// retaining child 0x10000330 as the missing-component overlay. /// -internal sealed class SpellExamineComponentTemplateFactory +public sealed class SpellExamineComponentTemplateFactory { public const uint TemplateId = 0x1000032Eu; public const uint MissingOverlayId = 0x10000330u; private readonly ElementInfo _template; - private readonly Func _resolveSprite; + private readonly Func _resolveSprite; private readonly UiDatFont? _defaultFont; private readonly IReadOnlyDictionary _fonts; public SpellExamineComponentTemplateFactory( ElementInfo template, - Func resolveSprite, + Func resolveSprite, UiDatFont? defaultFont, IReadOnlyDictionary? fonts = null) { @@ -33,7 +33,7 @@ internal sealed class SpellExamineComponentTemplateFactory public static SpellExamineComponentTemplateFactory? TryLoad( IDatReaderWriter dats, - Func resolveSprite, + Func resolveSprite, UiDatFont? defaultFont, Func? resolveFont) { @@ -53,7 +53,7 @@ internal sealed class SpellExamineComponentTemplateFactory fonts); } - public UiElement Create(GpuTextureSlot iconTexture, bool owned) + public UiElement Create(uint iconTexture, bool owned) { ImportedLayout content = LayoutImporter.Build( _template, diff --git a/src/AcDream.App/UI/Layout/SpellbookRowStyle.cs b/src/AcDream.App/UI/Layout/SpellbookRowStyle.cs index 39a9bc6b..668709fa 100644 --- a/src/AcDream.App/UI/Layout/SpellbookRowStyle.cs +++ b/src/AcDream.App/UI/Layout/SpellbookRowStyle.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.Content; using DatReaderWriter; @@ -9,7 +9,7 @@ namespace AcDream.App.UI.Layout; /// list points at this prototype through ItemList attribute 0x1000000E; /// retail clones it for every learned spell. /// -internal readonly record struct SpellbookRowStyle( +public readonly record struct SpellbookRowStyle( float Width, float Height, float IconLeft, diff --git a/src/AcDream.App/UI/Layout/SpellbookWindowController.cs b/src/AcDream.App/UI/Layout/SpellbookWindowController.cs index b6461f5e..7a408d8d 100644 --- a/src/AcDream.App/UI/Layout/SpellbookWindowController.cs +++ b/src/AcDream.App/UI/Layout/SpellbookWindowController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -10,7 +10,7 @@ using AcDream.Content; namespace AcDream.App.UI.Layout; -internal enum SpellbookWindowPage { Spells, Components } +public enum SpellbookWindowPage { Spells, Components } /// /// Binds retail's combined spellbook/component-book LayoutDesc 0x21000034. @@ -19,7 +19,7 @@ internal enum SpellbookWindowPage { Spells, Components } /// two authored tabs are stateful UIElement_Text controls, matching /// gmSpellbookUI::PostInit @ 0x0048B2B0 and the resolved retail layout. /// -internal sealed class SpellbookWindowController : IRetainedPanelController +public sealed class SpellbookWindowController : IRetainedPanelController { public const uint LayoutId = 0x21000034u; public const uint RootId = 0x100002A8u; @@ -50,8 +50,8 @@ internal sealed class SpellbookWindowController : IRetainedPanelController private readonly Func _playerGuid; private readonly IReadOnlyDictionary _components; private readonly SelectionState _selection; - private readonly Func _resolveSpellIcon; - private readonly Func _resolveComponentIcon; + private readonly Func _resolveSpellIcon; + private readonly Func _resolveComponentIcon; private readonly Func _spellLevel; private readonly Action _selectObject; private readonly Action _addFavorite; @@ -86,8 +86,8 @@ internal sealed class SpellbookWindowController : IRetainedPanelController Func playerGuid, IReadOnlyDictionary components, SelectionState selection, - Func resolveSpellIcon, - Func resolveComponentIcon, + Func resolveSpellIcon, + Func resolveComponentIcon, Func spellLevel, Action selectObject, Action addFavorite, @@ -170,8 +170,8 @@ internal sealed class SpellbookWindowController : IRetainedPanelController Func playerGuid, IReadOnlyDictionary components, SelectionState selection, - Func resolveSpellIcon, - Func resolveComponentIcon, + Func resolveSpellIcon, + Func resolveComponentIcon, Func spellLevel, Action selectObject, Action addFavorite, diff --git a/src/AcDream.App/UI/Layout/SpellcastingUiController.cs b/src/AcDream.App/UI/Layout/SpellcastingUiController.cs index 90ecf1ee..216338b7 100644 --- a/src/AcDream.App/UI/Layout/SpellcastingUiController.cs +++ b/src/AcDream.App/UI/Layout/SpellcastingUiController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using AcDream.App.Spells; @@ -16,7 +16,7 @@ namespace AcDream.App.UI.Layout; /// LayoutDesc 0x21000073. Favorites remain server-persisted; casting emits one /// request through . /// -internal sealed class SpellcastingUiController : IRetainedPanelController +public sealed class SpellcastingUiController : IRetainedPanelController { public const uint PageId = 0x10000061u; public const uint SpellNameId = 0x1000048Bu; @@ -42,8 +42,8 @@ internal sealed class SpellcastingUiController : IRetainedPanelController private readonly SelectionState _selection; private readonly ClientObjectTable _objects; private readonly Func _playerGuid; - private readonly Func _resolveSpellIcon; - private readonly Func _resolveItemDragIcon; + private readonly Func _resolveSpellIcon; + private readonly Func _resolveItemDragIcon; private readonly Action _useItem; private readonly Action? _examineSpell; private readonly Action? _addFavorite; @@ -73,8 +73,8 @@ internal sealed class SpellcastingUiController : IRetainedPanelController RuntimeSpellCastState casting, ClientObjectTable objects, Func playerGuid, - Func resolveSpellIcon, - Func resolveItemDragIcon, + Func resolveSpellIcon, + Func resolveItemDragIcon, Action useItem, Action? examineSpell, SelectionState selection, @@ -162,8 +162,8 @@ internal sealed class SpellcastingUiController : IRetainedPanelController RuntimeSpellCastState casting, ClientObjectTable objects, Func playerGuid, - Func resolveSpellIcon, - Func resolveItemDragIcon, + Func resolveSpellIcon, + Func resolveItemDragIcon, Action useItem, SelectionState selection, Action? addFavorite, @@ -351,7 +351,7 @@ internal sealed class SpellcastingUiController : IRetainedPanelController var slot = new UiCatalogSlot { EntryId = id, - CatalogIconTexture = metadata is null ? GpuTextureSlot.Unassigned : _resolveSpellIcon(id), + CatalogIconTexture = metadata is null ? 0u : _resolveSpellIcon(id), Label = metadata?.Name ?? $"Spell {id}", SpriteResolve = list.SpriteResolve, CatalogDragPayload = new SpellFavoriteDragPayload(tab, position, id), @@ -530,9 +530,9 @@ internal sealed class SpellcastingUiController : IRetainedPanelController _endowmentHost.Visible = endowment is not null; _endowmentSlot.EntryId = _endowmentItemId; _endowmentSlot.CatalogIconTexture = _endowmentSpellId == 0u - ? GpuTextureSlot.Unassigned : _resolveSpellIcon(_endowmentSpellId); + ? 0u : _resolveSpellIcon(_endowmentSpellId); _endowmentSlot.CatalogOverlayTexture = endowment is null - ? GpuTextureSlot.Unassigned : _resolveItemDragIcon(endowment); + ? 0u : _resolveItemDragIcon(endowment); string spellName = _spellbook.TryGetMetadata(_endowmentSpellId, out SpellMetadata metadata) ? metadata.Name : $"Spell {_endowmentSpellId}"; _endowmentSlot.Label = endowment is null @@ -603,13 +603,13 @@ internal sealed class SpellcastingUiController : IRetainedPanelController } } -internal sealed record SpellFavoriteDragPayload(int SourceTab, int SourcePosition, uint SpellId); +public sealed record SpellFavoriteDragPayload(int SourceTab, int SourcePosition, uint SpellId); /// /// A learned-spell shortcut carried from the spellbook. Unlike a favorite drag, /// lifting this payload never removes anything from its source collection. /// -internal sealed record SpellbookShortcutDragPayload(uint SpellId); +public sealed record SpellbookShortcutDragPayload(uint SpellId); internal static class FavoriteListExtensions { diff --git a/src/AcDream.App/UI/Layout/ToolbarController.cs b/src/AcDream.App/UI/Layout/ToolbarController.cs index 56d05415..9d880d84 100644 --- a/src/AcDream.App/UI/Layout/ToolbarController.cs +++ b/src/AcDream.App/UI/Layout/ToolbarController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using AcDream.Core.Combat; using AcDream.Core.Items; @@ -8,7 +8,7 @@ using AcDream.Core.Selection; namespace AcDream.App.UI.Layout; /// -/// Binds the imported gmToolbarUI window (LayoutDesc 0x21000016) to live data — +/// Binds the imported gmToolbarUI window (LayoutDesc 0x21000016) to live data — /// the gm*UI::PostInit analogue. Finds the 18 shortcut slots (UiItemList) by id, /// populates them from the persisted PlayerDescription shortcuts /// (UpdateFromPlayerDesc), re-binds deferred slots when an item's CreateObject @@ -24,7 +24,7 @@ namespace AcDream.App.UI.Layout; /// CreateObject resolves a formerly-unknown guid. /// /// -internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelController +public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelController { // Slot element ids in slot-index order (toolbar LayoutDesc 0x21000016, pre-dump). // Row 1 = slots 0-8 (0x100001A7..0x100001AF), Row 2 = slots 9-17 (0x100006B7..0x100006BF). @@ -39,7 +39,7 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo // SelectedObjectController owns the health/mana meters and both stack controls, // including retail's initial-hidden state and selection-driven visibility. - // Four mutually-exclusive combat-mode indicator elements — exactly one visible at a time. + // Four mutually-exclusive combat-mode indicator elements — exactly one visible at a time. // Index 0 = NonCombat (peace), 1 = Melee, 2 = Missile, 3 = Magic. // Retail ref: gmToolbarUI::RecvNotice_SetCombatMode (acclient_2013_pseudo_c.txt:196632-196669) // SetVisible's exactly one element depending on the incoming mode. @@ -64,9 +64,9 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo private readonly ClientObjectTable _repo; private readonly CombatState? _combatState; private readonly ShortcutStore _store; - private readonly Func _iconIds; // (itemType, icon, underlay, overlay, effects) → GL tex - private readonly Func? _dragIconIds; - private readonly Action _useItem; // guid → fire UseObject + private readonly Func _iconIds; // (itemType, icon, underlay, overlay, effects) → GL tex + private readonly Func? _dragIconIds; + private readonly Action _useItem; // guid → fire UseObject private readonly Action? _sendAddShortcut; private readonly Action? _sendRemoveShortcut; // (index) private readonly ItemInteractionController? _itemInteraction; @@ -83,8 +83,8 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo // Retail ref: UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465); // gmToolbarUI::RecvNotice_SetCombatMode (196610-196621) re-stamps ghosting. // Occupancy branch (decomp 229481): - // occupied → regular 0x10000042 / ghosted 0x10000043 - // empty → background digit 0x1000005e (stance-independent) + // occupied → regular 0x10000042 / ghosted 0x10000043 + // empty → background digit 0x1000005e (stance-independent) private uint[]? _regularDigits; private uint[]? _ghostedDigits; private uint[]? _emptyDigits; @@ -94,7 +94,7 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo ImportedLayout layout, ClientObjectTable repo, ShortcutStore shortcuts, - Func iconIds, + Func iconIds, Action useItem, CombatState? combatState, uint[]? regularDigits, @@ -110,7 +110,7 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo Func? playerGuid = null, Action? sendPutItemInContainer = null, UiDatFont? ammoFont = null, - Func? dragIconIds = null) + Func? dragIconIds = null) { _repo = repo; _combatState = combatState; @@ -193,7 +193,7 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo _examineButton.OnClick = () => _itemInteraction?.ExamineSelectedOrEnterMode(_selectedObjectId()); // Port of gmToolbarUI::RecvNotice_SetCombatMode (acclient_2013_pseudo_c.txt:196632-196669): - // exactly one indicator visible at a time. Default to NonCombat (peace) — the player + // exactly one indicator visible at a time. Default to NonCombat (peace) — the player // always spawns in peace mode; retail has not yet called SetVisible when PostInit runs. SetCombatMode(CombatMode.NonCombat); @@ -204,7 +204,7 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo _selection.Changed += OnSelectionChanged; // D.5.4: the table now holds ALL objects (creatures, NPCs, etc.), so filter - // to our 18 shortcut guids — else every creature spawn in a busy zone + // to our 18 shortcut guids — else every creature spawn in a busy zone // needlessly re-populates the bar (gmToolbarUI::SetDelayedShortcutNum pattern). repo.ObjectAdded += OnRepositoryObjectChanged; repo.ObjectUpdated += OnRepositoryObjectChanged; @@ -310,22 +310,22 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo /// if the shortcut list is refreshed outside the repo-event path. /// /// Imported toolbar layout (LayoutDesc 0x21000016). - /// Live item repository — must stay alive for the controller's lifetime. + /// Live item repository — must stay alive for the controller's lifetime. /// /// Runtime-owned retail shortcut manager. The toolbar borrows this exact /// mutable owner; it never reconstructs another slot map. /// - /// Resolves (itemType, iconId, underlayId, overlayId, effects) → GL texture handle. + /// Resolves (itemType, iconId, underlayId, overlayId, effects) → GL texture handle. /// Callback fired when a bound slot is clicked; receives the item guid. /// - /// Optional live combat state — when provided, the toolbar subscribes to + /// Optional live combat state — when provided, the toolbar subscribes to /// and updates the four mutually-exclusive /// combat-mode indicator elements accordingly. /// Pass null to skip live wiring (e.g. in unit tests that don't exercise the indicator). /// /// /// Regular digit DID array (property 0x10000042 from LayoutDesc 0x21000037 element - /// 0x1000034A under composite 0x10000346). Index i → slot label digit (i+1) RenderSurface id. + /// 0x1000034A under composite 0x10000346). Index i → slot label digit (i+1) RenderSurface id. /// Null if the dat lookup failed (no digits drawn). Retail reference: /// UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465). /// @@ -333,14 +333,14 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo /// /// Empty-slot background digit DID array (property 0x1000005e, stance-independent). /// Used when a slot is EMPTY (ItemId == 0). Retail ref: UIElement_UIItem::SetShortcutNum - /// (decomp 229481) — else branch when m_elem_Icon->m_state == 0x1000001c (empty state). + /// (decomp 229481) — else branch when m_elem_Icon->m_state == 0x1000001c (empty state). /// Null if the dat lookup failed (empty slots draw no digit, which is safe). /// public static ToolbarController Bind( ImportedLayout layout, ClientObjectTable repo, ShortcutStore shortcuts, - Func iconIds, + Func iconIds, Action useItem, CombatState? combatState = null, uint[]? regularDigits = null, @@ -356,7 +356,7 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo Func? playerGuid = null, Action? sendPutItemInContainer = null, UiDatFont? ammoFont = null, - Func? dragIconIds = null) + Func? dragIconIds = null) { var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState, regularDigits, ghostedDigits, emptyDigits, itemInteraction, @@ -427,15 +427,15 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo if (list is null) continue; var item = _repo.Get(guid); if (item is null) continue; // deferred: ObjectAdded re-calls Populate - GpuTextureSlot tex = _iconIds(item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects); - GpuTextureSlot? dragTex = _dragIconIds?.Invoke( - item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects); + uint tex = _iconIds(item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects); + uint dragTex = _dragIconIds?.Invoke( + item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects) ?? 0u; list.Cell.SetItem(guid, tex, entry, dragTex); } // Re-stamp slot number labels after any item change. // Digit SPRITE SOURCE depends on occupancy (decomp UIElement_UIItem::SetShortcutNum:229481): - // occupied → regular 0x10000042 / ghosted 0x10000043; empty → background 0x1000005e. + // occupied → regular 0x10000042 / ghosted 0x10000043; empty → background 0x1000005e. // The digit is ALWAYS shown on top-row slots (SetVisible(1) at decomp 229511). RestampShortcutNumbers(); } @@ -450,7 +450,7 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo /// public void SetCombatMode(CombatMode mode) { - // Index → mode mapping matches CombatIndicatorIds declaration order: + // Index → mode mapping matches CombatIndicatorIds declaration order: // 0 = NonCombat (peace), 1 = Melee, 2 = Missile, 3 = Magic. bool[] show = { @@ -475,14 +475,14 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo /// /// Push digit-array references and shortcut-number state into every slot cell. - /// Top row (indices 0–8): SetShortcutNum(i, _shortcutsGhosted) — numbers 1–9 always shown + /// Top row (indices 0–8): SetShortcutNum(i, _shortcutsGhosted) — numbers 1–9 always shown /// (the digit is ALWAYS visible, SetVisible(1) at decomp 229511; only the sprite - /// SOURCE differs by occupancy — see UIElement_UIItem::SetShortcutNum decomp 229481). - /// Bottom row (indices 9–17): ClearShortcutNum() — retail shows no numbers there. + /// SOURCE differs by occupancy — see UIElement_UIItem::SetShortcutNum decomp 229481). + /// Bottom row (indices 9–17): ClearShortcutNum() — retail shows no numbers there. /// Retail ref: UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465); /// gmToolbarUI::RecvNotice_SetCombatMode (196610-196621). - /// Occupancy → source: occupied → regular 0x10000042 / ghosted 0x10000043; - /// empty → background 0x1000005e (decomp 229481/229493). + /// Occupancy → source: occupied → regular 0x10000042 / ghosted 0x10000043; + /// empty → background 0x1000005e (decomp 229481/229493). /// private void RestampShortcutNumbers() { @@ -494,7 +494,7 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo cell.GhostedDigits = _ghostedDigits; cell.EmptyDigits = _emptyDigits; if (i < 9) - cell.SetShortcutNum(i, _shortcutsGhosted); // top row: slot labels 1–9 always shown + cell.SetShortcutNum(i, _shortcutsGhosted); // top row: slot labels 1–9 always shown else cell.ClearShortcutNum(); // bottom row: no slot labels } @@ -503,7 +503,7 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo /// /// Wire the callback on a slot cell so that /// clicking a bound item fires with the slot's current guid. - /// Mirrors retail's gmToolbarUI click → UseShortcut dispatch. + /// Mirrors retail's gmToolbarUI click → UseShortcut dispatch. /// private void WireClick(UiItemList list) { @@ -662,12 +662,12 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo return true; } - // ── IItemListDragHandler (B.2 live handler) ────────────────────────────── + // ── IItemListDragHandler (B.2 live handler) ────────────────────────────── // Retail: gmToolbarUI is the m_dragHandler for every shortcut slot list. // Retail model (remove-on-lift / place-on-drop / no-restore): - // lift → RemoveShortcut (0x019D) + store.Remove (slot empties immediately) - // drop → AddShortcut (0x019C) + optional swap of evicted item into source - // off-bar release → the lift's removal stands (no restore) + // lift → RemoveShortcut (0x019D) + store.Remove (slot empties immediately) + // drop → AddShortcut (0x019C) + optional swap of evicted item into source + // off-bar release → the lift's removal stands (no restore) // Retail ref: gmToolbarUI::HandleDropRelease acclient_2013_pseudo_c.txt:197971 /// @@ -678,7 +678,7 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo if (payload.ObjId != 0 && _selectedObjectId() != payload.ObjId) _selectItem?.Invoke(payload.ObjId); - // Retail RecvNotice_ItemListBeginDrag → RemoveShortcut (0x004bd930/0x004bd450): the lifted + // Retail RecvNotice_ItemListBeginDrag → RemoveShortcut (0x004bd930/0x004bd450): the lifted // shortcut leaves the bar (+ wire) the instant the drag starts; it's re-placed only on a // drop onto a slot. Off-bar release leaves it removed. _sendRemoveShortcut?.Invoke((uint)payload.SourceSlot); diff --git a/src/AcDream.App/UI/Layout/ToolbarInputController.cs b/src/AcDream.App/UI/Layout/ToolbarInputController.cs index 964762c9..d298da41 100644 --- a/src/AcDream.App/UI/Layout/ToolbarInputController.cs +++ b/src/AcDream.App/UI/Layout/ToolbarInputController.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Selection; +using AcDream.Core.Selection; using AcDream.UI.Abstractions.Input; namespace AcDream.App.UI.Layout; @@ -8,7 +8,7 @@ namespace AcDream.App.UI.Layout; /// dispatcher owns chords/scopes; this class only maps semantic actions to the /// exact gmToolbarUI::ListenToGlobalMessage @ 0x004BE4E0 slot intent. /// -internal sealed class ToolbarInputController +public sealed class ToolbarInputController { private readonly ToolbarController _toolbar; private readonly SelectionState _selection; diff --git a/src/AcDream.App/UI/Layout/UiDatElement.cs b/src/AcDream.App/UI/Layout/UiDatElement.cs index 88b08f99..b67a6c70 100644 --- a/src/AcDream.App/UI/Layout/UiDatElement.cs +++ b/src/AcDream.App/UI/Layout/UiDatElement.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; namespace AcDream.App.UI.Layout; @@ -10,28 +10,28 @@ namespace AcDream.App.UI.Layout; /// faithful because retail's base element render is exactly "stamp the media per draw-mode". /// /// -/// For Plan 1, all observed draw modes produce the same alpha-blended tiled quad — the +/// For Plan 1, all observed draw modes produce the same alpha-blended tiled quad — the /// sprite shader already alpha-blends, so no per-mode branch is needed here. The named /// constants document the real enum for Plan 2. /// /// /// /// DrawModeType (DatReaderWriter.Enums), stored as int in to -/// keep this dat-free. See docs/research/2026-06-15-layoutdesc-format.md §6: +/// keep this dat-free. See docs/research/2026-06-15-layoutdesc-format.md §6: /// Undefined=0, Normal=1, Overlay=2, Alphablend=3. There is no Stretch mode. /// /// /// /// Tiling uses UV-repeat on BOTH axes (Width/tw, Height/th) so vertical -/// chrome edges (e.g. a 5×10 sprite drawn over a 5×48 rect) tile vertically too. +/// chrome edges (e.g. a 5×10 sprite drawn over a 5×48 rect) tile vertically too. /// sets /// GL_REPEAT on both S and T, so vertical tiling is always active. /// /// -internal sealed class UiDatElement : UiElement, IUiDatStateful +public sealed class UiDatElement : UiElement, IUiDatStateful { // DrawModeType enum values from DatReaderWriter.Enums. - // See docs/research/2026-06-15-layoutdesc-format.md §6. + // See docs/research/2026-06-15-layoutdesc-format.md §6. #pragma warning disable IDE0051 // private constants kept for documentation / Plan 2 private const int DrawUndefined = 0; private const int DrawNormal = 1; @@ -40,7 +40,7 @@ internal sealed class UiDatElement : UiElement, IUiDatStateful #pragma warning restore IDE0051 private readonly ElementInfo _info; - private readonly Func _resolve; + private readonly Func _resolve; /// The dat element id from . Exposed so controllers /// can identify which logical element a UiDatElement represents when walking subtrees @@ -102,9 +102,9 @@ internal sealed class UiDatElement : UiElement, IUiDatStateful } /// Merged for this element. - /// Dat file-id → (GL texture handle, native px width, native px height). + /// Dat file-id → (GL texture handle, native px width, native px height). /// Returns (0,0,0) when the texture is not yet uploaded. - public UiDatElement(ElementInfo info, Func resolve) + public UiDatElement(ElementInfo info, Func resolve) { _info = info; _resolve = resolve; @@ -172,13 +172,13 @@ internal sealed class UiDatElement : UiElement, IUiDatStateful /// The image remains this element's own media, so authored descendants retain /// their normal foreground relationship. /// - public GpuTextureSlot? RuntimeImageTexture { get; set; } + public uint? RuntimeImageTexture { get; set; } protected override void OnDraw(UiRenderContext ctx) { - if (MediaVisible && RuntimeImageTexture is { } runtimeTexture) + if (MediaVisible && RuntimeImageTexture is uint runtimeTexture) { - if (runtimeTexture.IsAssigned) + if (runtimeTexture != 0u) { ctx.DrawSprite( runtimeTexture, @@ -200,9 +200,9 @@ internal sealed class UiDatElement : UiElement, IUiDatStateful if (MediaVisible && file != 0) { var (tex, tw, th) = _resolve(file); - if (tex.IsAssigned && tw != 0 && th != 0) + if (tex != 0 && tw != 0 && th != 0) { - // Normal → TILE at native size on both axes (UV-repeat; GL_REPEAT-wrapped UI + // Normal → TILE at native size on both axes (UV-repeat; GL_REPEAT-wrapped UI // texture), matching ImgTex::TileCSI. Overlay/Alphablend use the same blit (the // sprite shader already alpha-blends). No Stretch mode exists in DrawModeType. ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Vector4.One); diff --git a/src/AcDream.App/UI/Layout/UiPropertyBag.cs b/src/AcDream.App/UI/Layout/UiPropertyBag.cs index 2e5c56e8..de97aa43 100644 --- a/src/AcDream.App/UI/Layout/UiPropertyBag.cs +++ b/src/AcDream.App/UI/Layout/UiPropertyBag.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Numerics; namespace AcDream.App.UI.Layout; diff --git a/src/AcDream.App/UI/Layout/UiTemplateListSlot.cs b/src/AcDream.App/UI/Layout/UiTemplateListSlot.cs index 961aaf29..97793596 100644 --- a/src/AcDream.App/UI/Layout/UiTemplateListSlot.cs +++ b/src/AcDream.App/UI/Layout/UiTemplateListSlot.cs @@ -1,11 +1,11 @@ -namespace AcDream.App.UI.Layout; +namespace AcDream.App.UI.Layout; /// /// One runtime listbox entry instantiated from an authored LayoutDesc template. /// Retail UIElement_ListBox::AddItemFromTemplateList creates exactly this /// ownership shape: the list owns the entry while the template owns its visuals. /// -internal sealed class UiTemplateListSlot : UiItemSlot +public sealed class UiTemplateListSlot : UiItemSlot { private readonly IUiDatStateful? _statefulRoot; private readonly uint _normalState; diff --git a/src/AcDream.App/UI/Layout/VitaeUiController.cs b/src/AcDream.App/UI/Layout/VitaeUiController.cs index 487c58c7..518205b3 100644 --- a/src/AcDream.App/UI/Layout/VitaeUiController.cs +++ b/src/AcDream.App/UI/Layout/VitaeUiController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -11,7 +11,7 @@ namespace AcDream.App.UI.Layout; /// Retained port of retail gmVitaeUI. It presents the active vitae /// modifier and the authoritative experience pool needed to regain one percent. /// -internal sealed class VitaeUiController : IRetainedPanelController +public sealed class VitaeUiController : IRetainedPanelController { public const uint LayoutId = 0x21000020u; public const uint RootId = 0x100001C1u; @@ -151,7 +151,7 @@ internal sealed class VitaeUiController : IRetainedPanelController } } -internal sealed record VitaeStrings( +public sealed record VitaeStrings( string FullStrength, string LostPrefix, string LostSuffix, diff --git a/src/AcDream.App/UI/Layout/VitalsController.cs b/src/AcDream.App/UI/Layout/VitalsController.cs index acb74d2a..9668c479 100644 --- a/src/AcDream.App/UI/Layout/VitalsController.cs +++ b/src/AcDream.App/UI/Layout/VitalsController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; using AcDream.App.UI; @@ -8,7 +8,7 @@ namespace AcDream.App.UI.Layout; /// Per-window controller for the vitals layout (LayoutDesc 0x2100006C). /// Mirrors retail gmVitalsUI::PostInit: grab the three meter elements /// by their dat element ids and bind live data providers (fill fraction + cur/max -/// text) to each. This is the ONLY per-window code in the whole importer — pure +/// text) to each. This is the ONLY per-window code in the whole importer — pure /// data wiring, not graphics. /// /// The slice sprites + dat font on each are already @@ -16,10 +16,10 @@ namespace AcDream.App.UI.Layout; /// only binds the dynamic vitals data. Do not touch meter rendering fields here. /// /// Element ids confirmed from -/// docs/research/2026-06-15-layoutdesc-format.md §11 +/// docs/research/2026-06-15-layoutdesc-format.md §11 /// (vitals window 0x2100006C dump). /// -internal static class VitalsController +public static class VitalsController { /// Dat element id for the Health meter (0x100000E6). public const uint Health = 0x100000E6; @@ -34,7 +34,7 @@ internal static class VitalsController /// /// Bind live vitals data providers to the Health, Stamina, and Mana meter /// elements found in . Any meter whose id is absent - /// from the layout is silently skipped — partial layouts (e.g. test fakes) + /// from the layout is silently skipped — partial layouts (e.g. test fakes) /// do not cause errors. /// /// Imported vitals layout tree. @@ -58,7 +58,7 @@ internal static class VitalsController BindMeter(layout, Mana, ManaText, manaPct, manaText); } - /// White cur/max numbers — matches the former UiMeter.LabelColor default. + /// White cur/max numbers — matches the former UiMeter.LabelColor default. private static readonly Vector4 NumberColor = new(1f, 1f, 1f, 1f); private static void BindMeter( @@ -66,7 +66,7 @@ internal static class VitalsController Func pct, Func text) { - // Silently skip if the id is absent — missing meters are not an error (partial layouts). + // Silently skip if the id is absent — missing meters are not an error (partial layouts). if (layout.FindElement(id) is not UiMeter m) return; m.Fill = () => pct(); diff --git a/src/AcDream.App/UI/Layout/VividTargetIndicatorController.cs b/src/AcDream.App/UI/Layout/VividTargetIndicatorController.cs index 56573901..187163e4 100644 --- a/src/AcDream.App/UI/Layout/VividTargetIndicatorController.cs +++ b/src/AcDream.App/UI/Layout/VividTargetIndicatorController.cs @@ -1,17 +1,17 @@ -using System.Numerics; +using System.Numerics; using AcDream.Core.Selection; using AcDream.Core.Ui; using AcDream.Content; namespace AcDream.App.UI.Layout; -internal readonly record struct VividTargetInfo( +public readonly record struct VividTargetInfo( Vector3 SelectionSphereCenter, float SelectionSphereRadius, uint ItemType, uint ObjectDescriptionFlags); -internal sealed record VividTargetRuntimeBindings( +public sealed record VividTargetRuntimeBindings( SelectionState Selection, Func PlayerGuid, Func Enabled, @@ -25,7 +25,7 @@ internal sealed record VividTargetRuntimeBindings( /// this presentation from the live object's selection sphere, independently of /// whether the world renderer drew or occluded the object. /// -internal sealed class VividTargetIndicatorController +public sealed class VividTargetIndicatorController { private const uint ClientEnumCategory = 0x10000009u; private const uint FirstSourceImageEnum = 1u; @@ -85,7 +85,7 @@ internal sealed class VividTargetIndicatorController return null; var resolved = assets.ResolveSprite(did); - if (!resolved.Texture.IsAssigned || resolved.Width <= 0 || resolved.Height <= 0) + if (resolved.Texture == 0u || resolved.Width <= 0 || resolved.Height <= 0) return null; sources[i] = new VividTargetSource( resolved.Texture, resolved.Width, resolved.Height); @@ -387,6 +387,6 @@ internal readonly record struct VividTargetProjection( float AngleDegrees); internal readonly record struct VividTargetSource( - GpuTextureSlot Texture, + uint Texture, float Width, float Height); diff --git a/src/AcDream.App/UI/MarkupDocument.cs b/src/AcDream.App/UI/MarkupDocument.cs index ddd9364f..1132479b 100644 --- a/src/AcDream.App/UI/MarkupDocument.cs +++ b/src/AcDream.App/UI/MarkupDocument.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Globalization; using System.Numerics; using System.Reflection; @@ -10,16 +10,16 @@ namespace AcDream.App.UI; /// Parses our KSML-style panel markup (mirrors retail's ElementDesc fields) /// into a live subtree. {Binding} attribute /// values resolve against a supplied object by property name (reflection). -/// This is the format the future LayoutDesc importer will emit. See D.2b spec §7. +/// This is the format the future LayoutDesc importer will emit. See D.2b spec §7. /// -internal static class MarkupDocument +public static class MarkupDocument { /// Raw XML markup for a single panel. /// Object whose public properties are bound to {PropName} attributes. - /// Surface id → (GL handle, width, height) for chrome sprites. + /// Surface id → (GL handle, width, height) for chrome sprites. /// Optional controls.ini stylesheet for the title color. public static UiNineSlicePanel Build( - string xml, object binding, Func resolve, + string xml, object binding, Func resolve, ControlsIni? style = null) { var root = XDocument.Parse(xml).Root ?? throw new FormatException("empty markup"); @@ -86,7 +86,7 @@ internal static class MarkupDocument CultureInfo.InvariantCulture, out var v) ? v : 0f; /// - /// Parses #AARRGGBB → RGBA (alpha first, matching + /// Parses #AARRGGBB → RGBA (alpha first, matching /// controls.ini convention). Falls back to opaque white on bad input. /// private static Vector4 Color(string? hex) diff --git a/src/AcDream.App/UI/RetailChromeSprites.cs b/src/AcDream.App/UI/RetailChromeSprites.cs index de7aea4d..f2a80fd7 100644 --- a/src/AcDream.App/UI/RetailChromeSprites.cs +++ b/src/AcDream.App/UI/RetailChromeSprites.cs @@ -1,66 +1,66 @@ -namespace AcDream.App.UI; +namespace AcDream.App.UI; /// /// Retail window-chrome RenderSurface DataIds, CONFIRMED via the D.2b Step-0 /// prove-out (2026-06-14). These are RenderSurface objects (0x06xxxxxx) decoded /// DIRECTLY (), NOT -/// through the Surface→SurfaceTexture chain. +/// through the Surface→SurfaceTexture chain. /// /// /// The universal floating-window bevel is an 8-piece border (4 corners -/// 5×5 + 4 edges) drawn around a tiled center fill — it is NOT a single +/// 5×5 + 4 edges) drawn around a tiled center fill — it is NOT a single /// 9-slice texture. Decoded sizes are in the comments (from the prove-out). /// /// /// -/// The edge/corner → position mapping below is a reasonable guess pending the +/// The edge/corner → position mapping below is a reasonable guess pending the /// LayoutDesc 0x21000040 parse (sub-project 3) and is confirmed visually in the /// first vitals-panel render. If a corner's bevel highlight looks wrong, swap /// the four corner constants; if top/bottom or left/right look inverted, swap /// those edge pairs. /// /// -internal static class RetailChromeSprites +public static class RetailChromeSprites { - /// Tiled interior fill — the shared panel background (48×48). + /// Tiled interior fill — the shared panel background (48×48). public const uint CenterFill = 0x06004CC2; - /// Horizontal top edge (10×5, tiled across the top span). + /// Horizontal top edge (10×5, tiled across the top span). public const uint TopEdge = 0x060074BF; - /// Horizontal bottom edge (10×5). + /// Horizontal bottom edge (10×5). public const uint BottomEdge = 0x060074C1; - /// Vertical left edge (5×10). + /// Vertical left edge (5×10). public const uint LeftEdge = 0x060074C0; - /// Vertical right edge (5×10). + /// Vertical right edge (5×10). public const uint RightEdge = 0x060074C2; - /// Top-left corner (5×5). + /// Top-left corner (5×5). public const uint CornerTL = 0x060074C3; - /// Top-right corner (5×5). + /// Top-right corner (5×5). public const uint CornerTR = 0x060074C4; - /// Bottom-left corner (5×5). + /// Bottom-left corner (5×5). public const uint CornerBL = 0x060074C5; - /// Bottom-right corner (5×5). + /// Bottom-right corner (5×5). public const uint CornerBR = 0x060074C6; /// Border thickness in pixels = the corner/edge sprite size (5px). public const int Border = 5; - // ── Resize-grip overlay ────────────────────────────────────────────── + // ── Resize-grip overlay ────────────────────────────────────────────── // A second 8-piece layer drawn ON TOP of the bevel above: the gold ridged // accents + square corner studs that frame a resizable retail window. From - // the vitals LayoutDesc 0x2100006C (elements 0x1000063B–0x10000642): each - // corner is the same 5×5 stud (0x06006129); the edges are gold double-line + // the vitals LayoutDesc 0x2100006C (elements 0x1000063B–0x10000642): each + // corner is the same 5×5 stud (0x06006129); the edges are gold double-line // strips tiled along each side. These have transparent gaps, so the bevel - // shows through — both layers are needed. - /// Corner grip stud, all four corners (5×5). + // shows through — both layers are needed. + /// Corner grip stud, all four corners (5×5). public const uint GripCorner = 0x06006129; - /// Top edge grip (10×5, tiled across). + /// Top edge grip (10×5, tiled across). public const uint GripTop = 0x0600612A; - /// Left edge grip (5×10, tiled down). + /// Left edge grip (5×10, tiled down). public const uint GripLeft = 0x0600612B; - /// Bottom edge grip (10×5). + /// Bottom edge grip (10×5). public const uint GripBottom = 0x0600612C; - /// Right edge grip (5×10). + /// Right edge grip (5×10). public const uint GripRight = 0x0600612D; } diff --git a/src/AcDream.App/UI/RetailItemConfirmationController.cs b/src/AcDream.App/UI/RetailItemConfirmationController.cs index 325fb44e..e16c8134 100644 --- a/src/AcDream.App/UI/RetailItemConfirmationController.cs +++ b/src/AcDream.App/UI/RetailItemConfirmationController.cs @@ -1,4 +1,4 @@ -using AcDream.App.UI.Layout; +using AcDream.App.UI.Layout; using AcDream.Core.Items; namespace AcDream.App.UI; @@ -9,7 +9,7 @@ namespace AcDream.App.UI; /// ClientUISystem::UsageConfirmation_* @ 0x00566420..0x005669EA and /// completion mirrors ClientUISystem::UsageCallback @ 0x00565B20. /// -internal sealed class RetailItemConfirmationController : IDisposable +public sealed class RetailItemConfirmationController : IDisposable { internal const string PlayerKillerMessage = "Using this altar will make you a player killer, able to attack or be attacked by other player killers. Are you sure you want to do this?"; diff --git a/src/AcDream.App/UI/RetailPanelCatalog.cs b/src/AcDream.App/UI/RetailPanelCatalog.cs index 14c4b0e7..95dcae21 100644 --- a/src/AcDream.App/UI/RetailPanelCatalog.cs +++ b/src/AcDream.App/UI/RetailPanelCatalog.cs @@ -1,11 +1,11 @@ -namespace AcDream.App.UI; +namespace AcDream.App.UI; /// /// Retail gmPanelUI panel ids carried by DAT property 0x10000029. /// The toolbar exposes only a subset of these ids; Helpful/Harmful effects use /// the authored gmUIElement_EffectsIndicator buttons instead. /// -internal static class RetailPanelCatalog +public static class RetailPanelCatalog { public const uint CharacterInformation = 3u; public const uint PositiveEffects = 4u; diff --git a/src/AcDream.App/UI/RetailSkillTrainingConfirmationController.cs b/src/AcDream.App/UI/RetailSkillTrainingConfirmationController.cs index ff5eb963..dbd28ef7 100644 --- a/src/AcDream.App/UI/RetailSkillTrainingConfirmationController.cs +++ b/src/AcDream.App/UI/RetailSkillTrainingConfirmationController.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; using AcDream.App.UI.Layout; namespace AcDream.App.UI; @@ -10,7 +10,7 @@ namespace AcDream.App.UI; /// Already-trained skill XP raises deliberately bypass this controller, matching /// retail's separate gmSkillUI::RaiseSelection @ 0x0049C8C0 path. /// -internal sealed class RetailSkillTrainingConfirmationController +public sealed class RetailSkillTrainingConfirmationController { public const string MessageFormat = "Are you sure you want to spend {0} credits to train {1}?"; diff --git a/src/AcDream.App/UI/RetailUiRuntime.cs b/src/AcDream.App/UI/RetailUiRuntime.cs index 98437aef..4d436089 100644 --- a/src/AcDream.App/UI/RetailUiRuntime.cs +++ b/src/AcDream.App/UI/RetailUiRuntime.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using AcDream.App.Plugins; using AcDream.App.Combat; using AcDream.App.Input; @@ -24,41 +24,41 @@ using Silk.NET.Input; namespace AcDream.App.UI; -internal sealed record RetailUiAssets( +public sealed record RetailUiAssets( IDatReaderWriter Dats, object DatLock, - Func ResolveSprite, + Func ResolveSprite, Func ResolveFont, UiDatFont? DefaultFont, BitmapFont? DebugFont, ControlsIni Controls, IconComposer Icons); -internal sealed record VitalsRuntimeBindings(VitalsVM ViewModel); +public sealed record VitalsRuntimeBindings(VitalsVM ViewModel); -internal sealed record ChatRuntimeBindings(ChatVM ViewModel, Func CommandBus); +public sealed record ChatRuntimeBindings(ChatVM ViewModel, Func CommandBus); -internal sealed record RadarRuntimeBindings( +public sealed record RadarRuntimeBindings( Func Snapshot, SelectionState Selection, Action SetUiLocked); -internal sealed record CombatRuntimeBindings( +public sealed record CombatRuntimeBindings( CombatState State, RuntimeCombatAttackState Attacks, Func Gameplay, Action SetGameplay); -internal sealed record MagicRuntimeBindings( +public sealed record MagicRuntimeBindings( Spellbook Spellbook, RuntimeSpellCastState Casting, ClientObjectTable Objects, Func PlayerGuid, IReadOnlyDictionary Components, - Func ResolveIcon, - Func ResolveDragIcon, - Func ResolveSpellIcon, - Func ResolveComponentIcon, + Func ResolveIcon, + Func ResolveDragIcon, + Func ResolveSpellIcon, + Func ResolveComponentIcon, SelectionState Selection, Func SpellLevel, Func> SpellComponents, @@ -72,14 +72,14 @@ internal sealed record MagicRuntimeBindings( Action SetDesiredComponent, Func ServerTime); -internal sealed record JumpPowerbarRuntimeBindings(Func Snapshot); +public sealed record JumpPowerbarRuntimeBindings(Func Snapshot); -internal sealed record FpsRuntimeBindings( +public sealed record FpsRuntimeBindings( Func FramesPerSecond, Func DegradeMultiplier, Func IsVisible); -internal sealed record IndicatorRuntimeBindings( +public sealed record IndicatorRuntimeBindings( Spellbook Spellbook, ClientObjectTable Objects, Func PlayerGuid, @@ -89,11 +89,11 @@ internal sealed record IndicatorRuntimeBindings( Action RequestLinkStatusPing, Action EndCharacterSession); -internal sealed record ToolbarRuntimeBindings( +public sealed record ToolbarRuntimeBindings( ClientObjectTable Objects, ShortcutStore Shortcuts, - Func ResolveIcon, - Func ResolveDragIcon, + Func ResolveIcon, + Func ResolveDragIcon, Action UseItem, CombatState Combat, ItemManaState ItemMana, @@ -114,13 +114,13 @@ internal sealed record ToolbarRuntimeBindings( Func PlayerGuid, Action? SendPutItemInContainer); -internal sealed record CharacterRuntimeBindings(CharacterSheetProvider Provider); +public sealed record CharacterRuntimeBindings(CharacterSheetProvider Provider); -internal sealed record InventoryRuntimeBindings( +public sealed record InventoryRuntimeBindings( ClientObjectTable Objects, Func PlayerGuid, - Func ResolveIcon, - Func ResolveDragIcon, + Func ResolveIcon, + Func ResolveDragIcon, Func Strength, Action? SendUse, Action? SendPutItemInContainer, @@ -129,11 +129,11 @@ internal sealed record InventoryRuntimeBindings( ItemInteractionController ItemInteraction, SelectionState Selection); -internal sealed record ExternalContainerRuntimeBindings( +public sealed record ExternalContainerRuntimeBindings( ExternalContainerState State, ClientObjectTable Objects, - Func ResolveIcon, - Func ResolveDragIcon, + Func ResolveIcon, + Func ResolveDragIcon, ItemInteractionController ItemInteraction, SelectionState Selection, Action SendUse, @@ -141,12 +141,12 @@ internal sealed record ExternalContainerRuntimeBindings( Action SendStackableSplitToContainer, Func IsWithinUseRange); -internal sealed record RetailUiPersistenceBindings( +public sealed record RetailUiPersistenceBindings( SettingsStore Store, Func CharacterKey, Func<(int Width, int Height)> ScreenSize); -internal sealed record RetailUiProbeBindings( +public sealed record RetailUiProbeBindings( bool Enabled, string? ScriptPath, bool DumpOnStart, @@ -155,19 +155,19 @@ internal sealed record RetailUiProbeBindings( Func SetInputHeld, Testing.IRetailUiAutomationRuntime? Runtime = null); -internal sealed record RetailUiCursorBindings( +public sealed record RetailUiCursorBindings( CursorFeedbackController Feedback, RetailCursorManager Manager); -internal sealed record ConfirmationRuntimeBindings( +public sealed record ConfirmationRuntimeBindings( Action SendResponse); -internal sealed record AppraisalRuntimeBindings( +public sealed record AppraisalRuntimeBindings( Func PlayerName, Action SendSetInscription, Action DisplaySystemMessage); -internal sealed record RetailUiRuntimeBindings( +public sealed record RetailUiRuntimeBindings( UiHost Host, RetailUiAssets Assets, VitalsRuntimeBindings Vitals, @@ -196,7 +196,7 @@ internal sealed record RetailUiRuntimeBindings( /// state/action delegates and GL/DAT resolvers; this runtime imports layouts, binds /// controllers, mounts windows, owns persistence, and presents one tick/draw/dispose seam. /// -internal sealed class RetailUiRuntime : IDisposable +public sealed class RetailUiRuntime : IDisposable { private readonly RetailUiRuntimeBindings _bindings; private StackSplitQuantityState StackSplitQuantity => _bindings.StackSplitQuantity; @@ -566,7 +566,7 @@ internal sealed class RetailUiRuntime : IDisposable ImportedLayout? layout = Import(0x2100006Cu); if (layout is null) { - Console.WriteLine("[D.2b] vitals: LayoutDesc 0x2100006C not found — vitals unavailable."); + Console.WriteLine("[D.2b] vitals: LayoutDesc 0x2100006C not found — vitals unavailable."); return; } @@ -590,7 +590,7 @@ internal sealed class RetailUiRuntime : IDisposable MinWidth = 40f, ContentClickThrough = false, }); - Console.WriteLine("[D.2b] retail UI active — vitals window from LayoutDesc importer (0x2100006C)."); + Console.WriteLine("[D.2b] retail UI active — vitals window from LayoutDesc importer (0x2100006C)."); } private void MountRadar() diff --git a/src/AcDream.App/UI/RetailWindowHandle.cs b/src/AcDream.App/UI/RetailWindowHandle.cs index e021e6e0..fd7d12e6 100644 --- a/src/AcDream.App/UI/RetailWindowHandle.cs +++ b/src/AcDream.App/UI/RetailWindowHandle.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace AcDream.App.UI; @@ -7,7 +7,7 @@ namespace AcDream.App.UI; /// mutations flow through this handle so lifecycle observers see the same state /// regardless of whether a change came from input, restore, or controller logic. /// -internal sealed class RetailWindowHandle +public sealed class RetailWindowHandle { private readonly RetailWindowManager _owner; private bool _notifiedVisible; diff --git a/src/AcDream.App/UI/RetailWindowLayoutPersistence.cs b/src/AcDream.App/UI/RetailWindowLayoutPersistence.cs index 5d0ac356..9022c780 100644 --- a/src/AcDream.App/UI/RetailWindowLayoutPersistence.cs +++ b/src/AcDream.App/UI/RetailWindowLayoutPersistence.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using AcDream.UI.Abstractions.Panels.Settings; @@ -10,7 +10,7 @@ namespace AcDream.App.UI; /// default character key so startup layout cannot overwrite a real /// character's state. /// -internal sealed class RetailWindowLayoutPersistence : IDisposable +public sealed class RetailWindowLayoutPersistence : IDisposable { private readonly RetailWindowManager _manager; private readonly SettingsStore _store; diff --git a/src/AcDream.App/UI/RetailWindowManager.cs b/src/AcDream.App/UI/RetailWindowManager.cs index d38021a5..8dd9d228 100644 --- a/src/AcDream.App/UI/RetailWindowManager.cs +++ b/src/AcDream.App/UI/RetailWindowManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; @@ -9,7 +9,7 @@ namespace AcDream.App.UI; /// and controller teardown. remains responsible for raw /// input ownership and reports completed interactions to this manager. /// -internal sealed class RetailWindowManager : IDisposable +public sealed class RetailWindowManager : IDisposable { private readonly UiRoot _root; private readonly Dictionary _byName = diff --git a/src/AcDream.App/UI/RetainedPanelControllerGroup.cs b/src/AcDream.App/UI/RetainedPanelControllerGroup.cs index 161b9592..890fdba9 100644 --- a/src/AcDream.App/UI/RetainedPanelControllerGroup.cs +++ b/src/AcDream.App/UI/RetainedPanelControllerGroup.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace AcDream.App.UI; @@ -8,7 +8,7 @@ namespace AcDream.App.UI; /// inventory+paperdoll). Teardown runs in reverse construction order and is /// idempotent. /// -internal sealed class RetainedPanelControllerGroup : IRetainedPanelController +public sealed class RetainedPanelControllerGroup : IRetainedPanelController { private readonly IRetainedPanelController[] _controllers; private bool _disposed; diff --git a/src/AcDream.App/UI/Testing/RetailUiAutomationProbe.cs b/src/AcDream.App/UI/Testing/RetailUiAutomationProbe.cs index a11bf09a..42218789 100644 --- a/src/AcDream.App/UI/Testing/RetailUiAutomationProbe.cs +++ b/src/AcDream.App/UI/Testing/RetailUiAutomationProbe.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Text; @@ -10,7 +10,7 @@ namespace AcDream.App.UI.Testing; /// Snapshot row for the live retained-mode retail UI tree. Coordinates are /// absolute root/screen pixels. /// -internal sealed record RetailUiProbeElement( +public sealed record RetailUiProbeElement( int Index, int Depth, string Path, @@ -32,14 +32,14 @@ internal sealed record RetailUiProbeElement( public int CenterY => (int)MathF.Round(Y + Height * 0.5f); } -internal sealed record RetailUiProbeAssertion(bool Success, string Message); +public sealed record RetailUiProbeAssertion(bool Success, string Message); /// /// Test/diagnostic harness for the retail-style UI. It drives /// mouse events, never panel controllers directly, so automated checks exercise /// the same click, double-click, and drag-drop path as a player. /// -internal sealed class RetailUiAutomationProbe +public sealed class RetailUiAutomationProbe { private readonly UiRoot _root; private readonly ClientObjectTable _objects; diff --git a/src/AcDream.App/UI/Testing/RetailUiAutomationScriptRunner.cs b/src/AcDream.App/UI/Testing/RetailUiAutomationScriptRunner.cs index c3f61a64..4fc0e1f7 100644 --- a/src/AcDream.App/UI/Testing/RetailUiAutomationScriptRunner.cs +++ b/src/AcDream.App/UI/Testing/RetailUiAutomationScriptRunner.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -15,7 +15,7 @@ public enum RetailUiAutomationCheckpointStatus Cancelled, } -internal interface IRetailUiAutomationCheckpoint +public interface IRetailUiAutomationCheckpoint { int Sequence { get; } string Name { get; } @@ -28,7 +28,7 @@ internal interface IRetailUiAutomationCheckpoint /// diagnostics. Implementations run on the same update/render thread as the /// script; they must not mutate gameplay state or call GL from another thread. /// -internal interface IRetailUiAutomationRuntime +public interface IRetailUiAutomationRuntime { bool IsWorldReady { get; } bool IsWorldViewportVisible { get; } @@ -48,7 +48,7 @@ internal interface IRetailUiAutomationRuntime /// execute on render ticks. Pointer commands use the same /// path as physical mouse input; semantic input uses the production dispatcher. /// -internal sealed class RetailUiAutomationScriptRunner : IDisposable +public sealed class RetailUiAutomationScriptRunner : IDisposable { private readonly RetailUiAutomationProbe _probe; private readonly Action _log; diff --git a/src/AcDream.App/UI/UiButton.cs b/src/AcDream.App/UI/UiButton.cs index b2a02244..e642ccc4 100644 --- a/src/AcDream.App/UI/UiButton.cs +++ b/src/AcDream.App/UI/UiButton.cs @@ -1,11 +1,11 @@ -using System; +using System; using System.Numerics; using AcDream.App.UI.Layout; namespace AcDream.App.UI; /// -/// Generic dat-widget button — the production replacement for any dat element of +/// Generic dat-widget button — the production replacement for any dat element of /// Type 1 (UIElement_Button, registered via RegisterElementClass(1, UIElement_Button::Create) /// @ acclient_2013_pseudo_c.txt:125828). /// @@ -21,7 +21,7 @@ namespace AcDream.App.UI; /// /// State selection: picks if set, then /// "Normal" if the element has a Normal state sprite, then falls back to the unnamed -/// DirectState ("" key) — identical to . +/// DirectState ("" key) — identical to . /// /// /// @@ -30,12 +30,12 @@ namespace AcDream.App.UI; /// earlier dev-scaffold widget with no dat sprites. /// /// -internal sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful +public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful { private readonly ElementInfo _info; private readonly ElementInfo _mediaInfo; private readonly FaceSegment[] _faceSegments; - private readonly Func _resolve; + private readonly Func _resolve; private readonly HashSet _availableStates = new(); private bool _pressed; private bool _pointerOver; @@ -103,7 +103,7 @@ internal sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStatefu public LabelAlignment LabelAlign { get; set; } = LabelAlignment.Center; /// Label horizontal alignment options. - internal enum LabelAlignment { Center, Left } + public enum LabelAlignment { Center, Left } public bool ToggleBehavior { get; } public bool RolloverEnabled { get; } @@ -123,7 +123,7 @@ internal sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStatefu } /// - /// Active state name, runtime-settable (e.g. Max/Min toggling Normal ↔ Minimized). + /// Active state name, runtime-settable (e.g. Max/Min toggling Normal ↔ Minimized). /// Matches . /// public string ActiveState { get; set; } = ""; @@ -198,11 +198,11 @@ internal sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStatefu } /// Merged for this element. - /// Dat file-id → (GL texture handle, native px width, native px height). + /// Dat file-id → (GL texture handle, native px width, native px height). /// Returns (0,0,0) when the texture is not yet uploaded. public UiButton( ElementInfo info, - Func resolve, + Func resolve, ElementInfo? mediaInfo = null, IReadOnlyList? faceSegments = null) { @@ -212,7 +212,7 @@ internal sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStatefu ? [] : faceSegments.Select(static segment => new FaceSegment(segment)).ToArray(); _resolve = resolve; - ClickThrough = false; // buttons are interactive — opt OUT of click-through + ClickThrough = false; // buttons are interactive — opt OUT of click-through // Visual transitions can select only states with an actual button face. // Retail layouts commonly declare an empty Normal_pressed descriptor while @@ -254,7 +254,7 @@ internal sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStatefu /// procedurally, so the importer must not build the button's children as widgets. public override bool ConsumesDatChildren => true; - /// A button is interactive — it must receive its Click even inside a whole-window-Draggable + /// A button is interactive — it must receive its Click even inside a whole-window-Draggable /// frame (e.g. the paperdoll "Slots" toggle in the inventory window), so it opts out of the /// IA-12 whole-window-drag that would otherwise swallow the press. public override bool HandlesClick => true; @@ -282,9 +282,9 @@ internal sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStatefu if (file != 0) { var (tex, tw, th) = _resolve(file); - if (tex.IsAssigned && tw != 0 && th != 0) + if (tex != 0 && tw != 0 && th != 0) { - // Tiled draw — same call shape as UiDatElement.OnDraw (UV-repeat; GL_REPEAT-wrapped + // Tiled draw — same call shape as UiDatElement.OnDraw (UV-repeat; GL_REPEAT-wrapped // UI texture). Matches ImgTex::TileCSI; no Stretch mode exists. float faceWidth = FaceWidth > 0f ? FaceWidth : Width; float faceHeight = FaceHeight > 0f ? FaceHeight : Height; @@ -312,7 +312,7 @@ internal sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStatefu if (dragSprite != 0) { var (tex, _, _) = _resolve(dragSprite); - if (tex.IsAssigned) + if (tex != 0) ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); } } @@ -322,7 +322,7 @@ internal sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStatefu if (file == 0 || rect.Width <= 0 || rect.Height <= 0) return; var (texture, textureWidth, textureHeight) = _resolve(file); - if (!texture.IsAssigned || textureWidth == 0 || textureHeight == 0) + if (texture == 0 || textureWidth == 0 || textureHeight == 0) return; // Same tiled/cropped media path as UiDatElement. Segment geometry is diff --git a/src/AcDream.App/UI/UiButtonStateMachine.cs b/src/AcDream.App/UI/UiButtonStateMachine.cs index 71826fbb..4238df9d 100644 --- a/src/AcDream.App/UI/UiButtonStateMachine.cs +++ b/src/AcDream.App/UI/UiButtonStateMachine.cs @@ -1,4 +1,4 @@ -namespace AcDream.App.UI; +namespace AcDream.App.UI; /// Inputs to retail UIElement_Button::UpdateState_. public readonly record struct UiButtonVisualInput( @@ -12,7 +12,7 @@ public readonly record struct UiButtonVisualInput( /// GL-free retail button visual-state policy. State ids are the numeric /// UIStateId values from the DAT. /// -internal static class UiButtonStateMachine +public static class UiButtonStateMachine { public const uint Normal = 1u; public const uint NormalRollover = 2u; diff --git a/src/AcDream.App/UI/UiCatalogSlot.cs b/src/AcDream.App/UI/UiCatalogSlot.cs index 9a6aaae3..a6d36372 100644 --- a/src/AcDream.App/UI/UiCatalogSlot.cs +++ b/src/AcDream.App/UI/UiCatalogSlot.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; using AcDream.App.Rendering; @@ -9,12 +9,12 @@ namespace AcDream.App.UI; /// It reuses the retail UIItemList geometry without pretending a spell id is an /// object GUID; catalog drags stay distinct from physical item drag/drop. /// -internal sealed class UiCatalogSlot : UiItemSlot +public sealed class UiCatalogSlot : UiItemSlot { public uint EntryId { get; set; } - public GpuTextureSlot CatalogIconTexture { get; set; } = GpuTextureSlot.Unassigned; + public uint CatalogIconTexture { get; set; } /// Optional second 32x32 layer (retail spell endowment item icon). - public GpuTextureSlot CatalogOverlayTexture { get; set; } = GpuTextureSlot.Unassigned; + public uint CatalogOverlayTexture { get; set; } public string Label { get; set; } = string.Empty; public string? Detail { get; set; } public bool ShowLabel { get; init; } @@ -43,8 +43,8 @@ internal sealed class UiCatalogSlot : UiItemSlot public override bool IsDragSource => CatalogDragPayload is not null; public override object? GetDragPayload() => CatalogDragPayload; - public override (GpuTextureSlot tex, int w, int h)? GetDragGhost() => - CatalogDragPayload is not null && CatalogIconTexture.IsAssigned + public override (uint tex, int w, int h)? GetDragGhost() => + CatalogDragPayload is not null && CatalogIconTexture != 0u ? (CatalogIconTexture, 32, 32) : null; @@ -92,7 +92,7 @@ internal sealed class UiCatalogSlot : UiItemSlot if (BackgroundSprite != 0u && SpriteResolve is not null) { var (texture, _, _) = SpriteResolve(BackgroundSprite); - if (texture.IsAssigned) + if (texture != 0u) ctx.DrawSprite(texture, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); } @@ -101,16 +101,16 @@ internal sealed class UiCatalogSlot : UiItemSlot float iconWidth = IconWidth > 0f ? IconWidth : ShowLabel ? MathF.Min(32f, Height) : Width; float iconHeight = IconHeight > 0f ? IconHeight : ShowLabel ? MathF.Min(32f, Height) : Height; - if (CatalogIconTexture.IsAssigned) + if (CatalogIconTexture != 0) ctx.DrawSprite(CatalogIconTexture, IconLeft, IconTop, iconWidth, iconHeight, 0f, 0f, 1f, 1f, Vector4.One); else if (SpriteResolve is not null && EmptySprite != 0) { var (texture, _, _) = SpriteResolve(EmptySprite); - if (texture.IsAssigned) + if (texture != 0) ctx.DrawSprite(texture, IconLeft, IconTop, iconWidth, iconHeight, 0f, 0f, 1f, 1f, Vector4.One); } - if (CatalogOverlayTexture.IsAssigned) + if (CatalogOverlayTexture != 0) ctx.DrawSprite(CatalogOverlayTexture, IconLeft, IconTop, iconWidth, iconHeight, 0f, 0f, 1f, 1f, Vector4.One); // Spell favorites are UIElement_UIItems too. Retail's @@ -147,7 +147,7 @@ internal sealed class UiCatalogSlot : UiItemSlot { if (!Selected || SpriteResolve is null || SelectedSprite == 0u) return; var (texture, _, _) = SpriteResolve(SelectedSprite); - if (texture.IsAssigned) + if (texture != 0u) ctx.DrawSprite(texture, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); } diff --git a/src/AcDream.App/UI/UiCollapsibleFrame.cs b/src/AcDream.App/UI/UiCollapsibleFrame.cs index 2e6a4fac..97899243 100644 --- a/src/AcDream.App/UI/UiCollapsibleFrame.cs +++ b/src/AcDream.App/UI/UiCollapsibleFrame.cs @@ -1,18 +1,18 @@ -using System; +using System; using System.Collections.Generic; namespace AcDream.App.UI; /// -/// A toolbar-frame variant that snaps its height between two stops — collapsed (row 2 hidden) and -/// expanded (row 2 shown) — and toggles a set of "second-row" elements to match. Resized via the +/// A toolbar-frame variant that snaps its height between two stops — collapsed (row 2 hidden) and +/// expanded (row 2 shown) — and toggles a set of "second-row" elements to match. Resized via the /// bottom edge (the mount sets = Bottom); each tick it resolves -/// the dragged height to the nearer stop so the frame always rests collapsed or expanded — never a -/// half-row. Toolkit UX (keystone.dll has no decomp; the dat stacks both rows always) — see IA-17. +/// the dragged height to the nearer stop so the frame always rests collapsed or expanded — never a +/// half-row. Toolkit UX (keystone.dll has no decomp; the dat stacks both rows always) — see IA-17. /// -internal sealed class UiCollapsibleFrame : UiNineSlicePanel, IRetainedWindowStateController +public sealed class UiCollapsibleFrame : UiNineSlicePanel, IRetainedWindowStateController { - public UiCollapsibleFrame(Func resolve) : base(resolve) { } + public UiCollapsibleFrame(Func resolve) : base(resolve) { } public float CollapsedHeight { get; set; } public float ExpandedHeight { get; set; } @@ -25,13 +25,13 @@ internal sealed class UiCollapsibleFrame : UiNineSlicePanel, IRetainedWindowStat protected override void OnTick(double deltaSeconds) { base.OnTick(deltaSeconds); - if (ExpandedHeight <= CollapsedHeight) return; // not configured yet — no snap + if (ExpandedHeight <= CollapsedHeight) return; // not configured yet — no snap bool expanded = IsExpanded; Height = expanded ? ExpandedHeight : CollapsedHeight; // snap the dragged height to a stop for (int i = 0; i < SecondRow.Count; i++) SecondRow[i].Visible = expanded; } - /// Test hook — OnTick is protected. Drives one snap+visibility reconcile. + /// Test hook — OnTick is protected. Drives one snap+visibility reconcile. internal void TickForTest(double dt) => OnTick(dt); public RetainedWindowState CaptureWindowState() diff --git a/src/AcDream.App/UI/UiDatFont.cs b/src/AcDream.App/UI/UiDatFont.cs index ff17f89e..176cb3b0 100644 --- a/src/AcDream.App/UI/UiDatFont.cs +++ b/src/AcDream.App/UI/UiDatFont.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering; @@ -17,7 +17,7 @@ namespace AcDream.App.UI; /// as two textured quads exactly the way the retail client does. /// /// -/// Retail render model — SurfaceWindow::DrawCharacter +/// Retail render model — SurfaceWindow::DrawCharacter /// (acclient 0x00442bd0, Font::GetCharDesc + the two SurfaceWindow blits): for /// each glyph it copies the BACKGROUND atlas sub-rect first, tinted with the /// outline color (black), then the FOREGROUND atlas sub-rect, tinted with the @@ -30,27 +30,27 @@ namespace AcDream.App.UI; /// /// /// Atlas format: the foreground atlas (0x06005EE5 for Font 0x40000000) is -/// PFID_A8 — alpha-only. Our SurfaceDecoder expands A8 to RGBA as +/// PFID_A8 — alpha-only. Our SurfaceDecoder expands A8 to RGBA as /// (255,255,255, alpha). The UI sprite shader path (ui_text.frag, /// uUseTexture==2) MULTIPLIES the sampled texel by the per-vertex tint /// (texture(uTex,vUv) * vColor), so tinting a white+alpha glyph by a -/// color gives that color with the glyph's alpha — black for the outline pass, +/// color gives that color with the glyph's alpha — black for the outline pass, /// text color for the fill pass. No shader change was needed. /// /// -internal sealed class UiDatFont +public sealed class UiDatFont { /// Retail UI font id (Latin-1, 16x16 max, with outline atlas). public const uint DefaultFontId = 0x40000000u; - /// Foreground (glyph pixels) texture-table slot + atlas pixel size. - public GpuTextureSlot ForegroundTexture { get; } + /// Foreground (glyph pixels) GL texture handle + atlas pixel size. + public uint ForegroundTexture { get; } public int ForegroundWidth { get; } public int ForegroundHeight { get; } - /// Background (outline/shadow) texture-table slot + atlas pixel size. - /// Unassigned when the font has no background atlas (then the outline pass is skipped). - public GpuTextureSlot BackgroundTexture { get; } + /// Background (outline/shadow) GL texture handle + atlas pixel size. + /// 0 when the font has no background atlas (then the outline pass is skipped). + public uint BackgroundTexture { get; } public int BackgroundWidth { get; } public int BackgroundHeight { get; } @@ -63,8 +63,8 @@ internal sealed class UiDatFont private readonly Dictionary _glyphs; internal UiDatFont( - GpuTextureSlot fgTex, int fgW, int fgH, - GpuTextureSlot bgTex, int bgW, int bgH, + uint fgTex, int fgW, int fgH, + uint bgTex, int bgW, int bgH, float lineHeight, float baselineOffset, Dictionary glyphs) { @@ -78,7 +78,7 @@ internal sealed class UiDatFont /// True if this font carries a separate outline/shadow atlas /// (retail's m_pBackgroundSurface). When false the outline pass is /// skipped and only the foreground (fill) glyphs are drawn. - public bool HasBackground => BackgroundTexture.IsAssigned; + public bool HasBackground => BackgroundTexture != 0; /// Look up a glyph descriptor for a character. Returns false for /// characters not present in the font's table (callers skip them). @@ -88,7 +88,7 @@ internal sealed class UiDatFont /// Load Font from the dat collection and upload /// both atlases through the texture cache (the same direct-RenderSurface /// path the D.2b chrome sprites use). Returns null if the Font DBObj is - /// missing — callers fall back to the debug bitmap font. + /// missing — callers fall back to the debug bitmap font. /// public static UiDatFont? Load(IDatReaderWriter dats, TextureCache cache, uint fontId = DefaultFontId) { @@ -104,9 +104,9 @@ internal sealed class UiDatFont // Point-sample the glyph atlases (nearest) so small UI text stays pixel-crisp; // bilinear softens the dat font noticeably (the chat menu/button text "blur"). - GpuTextureSlot fgTex = cache.GetOrUploadRenderSurface(font.ForegroundSurfaceDataId, out int fgW, out int fgH, nearest: true); + uint fgTex = cache.GetOrUploadRenderSurface(font.ForegroundSurfaceDataId, out int fgW, out int fgH, nearest: true); - GpuTextureSlot bgTex = GpuTextureSlot.Unassigned; int bgW = 0, bgH = 0; + uint bgTex = 0; int bgW = 0, bgH = 0; if (font.BackgroundSurfaceDataId != 0) bgTex = cache.GetOrUploadRenderSurface(font.BackgroundSurfaceDataId, out bgW, out bgH, nearest: true); @@ -147,7 +147,7 @@ internal sealed class UiDatFont /// /// Pure pen-advance summation seam: total width of /// given a that maps each char to its descriptor - /// (null = not in the font → contributes nothing). Lets the advance math be + /// (null = not in the font → contributes nothing). Lets the advance math be /// unit-tested with synthetic glyphs, with no GL or dat dependency. /// public static float MeasureWidth(string? text, Func lookup) diff --git a/src/AcDream.App/UI/UiDialogRoot.cs b/src/AcDream.App/UI/UiDialogRoot.cs index 81a5fb47..3915b497 100644 --- a/src/AcDream.App/UI/UiDialogRoot.cs +++ b/src/AcDream.App/UI/UiDialogRoot.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; namespace AcDream.App.UI; @@ -6,7 +6,7 @@ namespace AcDream.App.UI; /// Full-device modal root for retail dialog element classes. The authored child with id /// 0x3D supplies the visible popup art; this root only owns modality and input capture. /// -internal sealed class UiDialogRoot : UiPanel +public sealed class UiDialogRoot : UiPanel { public Action? Cancel { get; set; } diff --git a/src/AcDream.App/UI/UiElement.cs b/src/AcDream.App/UI/UiElement.cs index 7c1d9d51..ea484b1e 100644 --- a/src/AcDream.App/UI/UiElement.cs +++ b/src/AcDream.App/UI/UiElement.cs @@ -1,13 +1,13 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; namespace AcDream.App.UI; /// Which parent edges a child keeps a fixed margin to on resize. -/// Left+Right ⇒ width stretches; Top+Bottom ⇒ height stretches. +/// Left+Right ⇒ width stretches; Top+Bottom ⇒ height stretches. [System.Flags] -internal enum AnchorEdges { None = 0, Left = 1, Top = 2, Right = 4, Bottom = 8 } +public enum AnchorEdges { None = 0, Left = 1, Top = 2, Right = 4, Bottom = 8 } /// Retail dat cursor media attached to a UI state. public readonly record struct UiCursorMedia(uint File, int HotspotX, int HotspotY) @@ -21,7 +21,7 @@ public readonly record struct UiCursorMedia(uint File, int HotspotX, int Hotspot /// Design notes: /// - Retail AC delegates widget semantics to the external /// keystone.dll library (see -/// docs/research/retail-ui/02-class-hierarchy.md — there is no +/// docs/research/retail-ui/02-class-hierarchy.md — there is no /// widget hierarchy inside acclient.exe itself). We implement /// our own retained-mode toolkit here, matching the behavior /// described in the decompile without trying to byte-match Keystone's @@ -35,13 +35,13 @@ public readonly record struct UiCursorMedia(uint File, int HotspotX, int Hotspot /// - Coordinates are in screen pixels, origin top-left. /// is in the parent's local coordinate space. /// -internal abstract class UiElement +public abstract class UiElement { - // ── Identity ───────────────────────────────────────────────────────── + // ── Identity ───────────────────────────────────────────────────────── /// /// Unique 32-bit event ID. Retail uses the range 0x10000000+ /// for custom app events (see - /// docs/research/retail-ui/04-input-events.md §3). Assigned + /// docs/research/retail-ui/04-input-events.md §3). Assigned /// by when the element is added to the tree. /// public uint EventId { get; internal set; } @@ -96,7 +96,7 @@ internal abstract class UiElement return default; } - // ── Geometry ──────────────────────────────────────────────────────── + // ── Geometry ──────────────────────────────────────────────────────── /// X in the parent's local pixel space. public float Left { get; set; } public float Top { get; set; } @@ -119,7 +119,7 @@ internal abstract class UiElement } } - // ── State flags ───────────────────────────────────────────────────── + // ── State flags ───────────────────────────────────────────────────── private bool _visible = true; public bool Visible { @@ -152,7 +152,7 @@ internal abstract class UiElement } /// - /// If true, skips this element — the event + /// If true, skips this element — the event /// passes through to whatever is behind. Used by decoration widgets /// (portrait frames, ornamental dividers). /// @@ -212,13 +212,13 @@ internal abstract class UiElement /// If true, a left-drag starting on this element is delivered to the /// element (e.g. text selection) instead of moving/resizing an ancestor window. - /// Edge resize on a resizable ancestor still wins — only the interior move / + /// Edge resize on a resizable ancestor still wins — only the interior move / /// drag-drop candidacy is suppressed in favour of the element's own handling. public bool CapturesPointerDrag { get; set; } /// If true, a left-press-and-move on this element starts a DRAG-DROP /// ( promotes to BeginDrag) rather than moving a Draggable - /// ancestor window — so an item cell inside the toolbar frame drags the item, not + /// ancestor window — so an item cell inside the toolbar frame drags the item, not /// the window. Distinct from (a self-driven /// interior drag like text selection, which does NOT promote to BeginDrag). Default /// false; overridden by drag sources (e.g. an occupied ). @@ -227,7 +227,7 @@ internal abstract class UiElement /// If true, a left-press on this element is handled BY the element (it receives the Click /// on release) instead of being captured as a whole-window move on a Draggable ancestor. Set by /// interactive leaf widgets (e.g. ) so they stay clickable inside a - /// whole-window-Draggable frame like the inventory window — where, without this, the IA-12 + /// whole-window-Draggable frame like the inventory window — where, without this, the IA-12 /// whole-window-drag swallows the press and the Click is never emitted. Distinct from /// (starts a drag-drop) and (a /// self-driven interior drag such as text selection). Default false. @@ -260,7 +260,7 @@ internal abstract class UiElement ResizeEdges.Left | ResizeEdges.Right | ResizeEdges.Top | ResizeEdges.Bottom; /// Edges this element anchors to in its parent. Default Left|Top - /// (pinned top-left, fixed size — no reflow). Left|Right stretches width. + /// (pinned top-left, fixed size — no reflow). Left|Right stretches width. private AnchorEdges _anchors = AnchorEdges.Left | AnchorEdges.Top; /// Edges this programmatic element anchors to in its parent. Assigning @@ -284,7 +284,7 @@ internal abstract class UiElement /// public UiLayoutPolicy? LayoutPolicy { get; set; } - // ── Tree structure ────────────────────────────────────────────────── + // ── Tree structure ────────────────────────────────────────────────── public UiElement? Parent { get; private set; } private readonly List _children = new(); @@ -355,7 +355,7 @@ internal abstract class UiElement /// /// True if this widget draws its full appearance itself and REPRODUCES its dat /// sub-elements procedurally (3-slice caps, button labels, scroll arrows, popup - /// rows…) — so the must NOT build + /// rows…) — so the must NOT build /// those dat child elements as separate widgets (they would double-draw and, worse, /// steal pointer/focus from the behavioral widget). All registered behavioral widgets /// (Meter/Menu/Button/Scrollbar/Text/Field) return true; the generic container @@ -365,7 +365,7 @@ internal abstract class UiElement /// public virtual bool ConsumesDatChildren => false; - // ── Virtual overrides ─────────────────────────────────────────────── + // ── Virtual overrides ─────────────────────────────────────────────── /// /// Draw THIS element (not its children). Children are composited by @@ -384,7 +384,7 @@ internal abstract class UiElement /// /// Draw content that must sit ON TOP of the ENTIRE UI, regardless of this - /// element's position in the tree — open menus, dropdowns, tooltips. Called in + /// element's position in the tree — open menus, dropdowns, tooltips. Called in /// a SECOND traversal after the whole tree's pass, with the /// same accumulated transform/alpha this element had during its normal draw. /// Retail spawns popups as ROOT elements (UIElement_Menu::MakePopup) for exactly @@ -425,9 +425,9 @@ internal abstract class UiElement public virtual object? GetDragPayload() => null; /// The texture paints at the cursor while this element - /// is the drag source: (texture-table slot, width, height). Null = no ghost. Keeps + /// is the drag source: (GL handle, width, height). Null = no ghost. Keeps /// item-agnostic. Retail analog: m_dragIcon (decomp 229738). - public virtual (GpuTextureSlot tex, int w, int h)? GetDragGhost() => null; + public virtual (uint tex, int w, int h)? GetDragGhost() => null; /// /// Notifies the source widget when the root starts or finishes carrying its drag payload. @@ -444,7 +444,7 @@ internal abstract class UiElement /// public virtual string? GetTooltipText() => null; - // ── Framework entry points (internal, called by UiRoot) ───────────── + // ── Framework entry points (internal, called by UiRoot) ───────────── internal void DrawSelfAndChildren(UiRenderContext ctx) { @@ -551,7 +551,7 @@ internal abstract class UiElement return null; // Children first, in reverse Z-order (topmost first). ClickThrough means - // THIS element is transparent to the pointer — but its children are NOT. + // THIS element is transparent to the pointer — but its children are NOT. // A ClickThrough container (e.g. a UiDatElement panel that hosts the chat // input / transcript) must still let the pointer reach its behavioral // children, so the ClickThrough check happens AFTER the child walk, gating @@ -571,7 +571,7 @@ internal abstract class UiElement return OnHitTest(localX, localY) ? this : null; } - // ── Anchor layout ──────────────────────────────────────────────────── + // ── Anchor layout ──────────────────────────────────────────────────── private bool _anchorCaptured; private float _amL, _amT, _amR, _amB, _aw0, _ah0; @@ -659,7 +659,7 @@ internal abstract class UiElement } /// Walk up to the owning (the top of the tree), or null - /// if this element is not attached. Lets a widget reach focus/capture services — e.g. + /// if this element is not attached. Lets a widget reach focus/capture services — e.g. /// a chat input blurring itself (exiting write mode) after submit. internal UiRoot? FindRoot() { @@ -668,8 +668,8 @@ internal abstract class UiElement return e as UiRoot; } - /// Compute an anchored child rect. Left&Right ⇒ stretch width - /// (keep both margins); Right only ⇒ pin to right at fixed width; otherwise + /// Compute an anchored child rect. Left&Right ⇒ stretch width + /// (keep both margins); Right only ⇒ pin to right at fixed width; otherwise /// pin left at fixed width. Same logic vertically. public static (float x, float y, float w, float h) ComputeAnchoredRect( AnchorEdges a, float mL, float mT, float mR, float mB, diff --git a/src/AcDream.App/UI/UiEvent.cs b/src/AcDream.App/UI/UiEvent.cs index 073936b5..044026b6 100644 --- a/src/AcDream.App/UI/UiEvent.cs +++ b/src/AcDream.App/UI/UiEvent.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; namespace AcDream.App.UI; @@ -10,16 +10,16 @@ namespace AcDream.App.UI; /// Layout from decompiled chunk_004A0000.c paperdoll handler /// FUN_004A5FA0: /// -/// int source_id; // param_2[0] — e.g. 0x100001d6 (drag source) +/// int source_id; // param_2[0] — e.g. 0x100001d6 (drag source) /// void* target_widget; // param_2[1] -/// int event_type; // param_2[2] — see UiEventType +/// int event_type; // param_2[2] — see UiEventType /// int data0; // param_2[3] -/// int data1; // param_2[4] — typically x in local coords -/// int data2; // param_2[5] — typically y +/// int data1; // param_2[4] — typically x in local coords +/// int data2; // param_2[5] — typically y /// int data3; // param_2[6] /// /// -internal readonly record struct UiEvent( +public readonly record struct UiEvent( uint SourceId, UiElement? Target, int Type, // see @@ -33,22 +33,22 @@ internal readonly record struct UiEvent( /// Retail AC UI event-type constants. Each value matches the decompiled /// switch-case in widgets' OnEvent handlers (e.g. 0x01 click, 0x15 drag /// begin, 0x3E drop released). Win32 WM_* numbers are reused for raw -/// button/key/mouse events (0x200 = WM_MOUSEMOVE etc.) — this matches +/// button/key/mouse events (0x200 = WM_MOUSEMOVE etc.) — this matches /// retail where internal event codes collide deliberately with WM_*. /// /// Evidence from decompile: -/// - 0x01 click — chunk_00470000.c ~11140, chunk_004C0000.c ~9270 -/// - 0x05/0x06 hover — chunk_00460000.c ~6253 -/// - 0x07 tooltip — chunk_00460000.c ~6253 (the UI manager polls the +/// - 0x01 click — chunk_00470000.c ~11140, chunk_004C0000.c ~9270 +/// - 0x05/0x06 hover — chunk_00460000.c ~6253 +/// - 0x07 tooltip — chunk_00460000.c ~6253 (the UI manager polls the /// hovered element's tooltip deadline before global time) -/// - 0x0A scroll — chunk_00470000.c ~11210 -/// - 0x0E right-click— chunk_004A0000.c ~2674 -/// - 0x15 drag begin — chunk_004A0000.c ~2707 -/// - 0x1C drag-over — chunk_004A0000.c ~2723 -/// - 0x21 drag-enter — chunk_004A0000.c ~2714 -/// - 0x3E drop-released — chunk_004A0000.c ~2754 +/// - 0x0A scroll — chunk_00470000.c ~11210 +/// - 0x0E right-click— chunk_004A0000.c ~2674 +/// - 0x15 drag begin — chunk_004A0000.c ~2707 +/// - 0x1C drag-over — chunk_004A0000.c ~2723 +/// - 0x21 drag-enter — chunk_004A0000.c ~2714 +/// - 0x3E drop-released — chunk_004A0000.c ~2754 /// -internal static class UiEventType +public static class UiEventType { public const int Click = 0x01; public const int HoverEnter = 0x05; @@ -82,7 +82,7 @@ internal static class UiEventType /// /// Mouse button enum matching retail's 1/2/3 encoding. /// -internal enum UiMouseButton +public enum UiMouseButton { Left = 1, Right = 2, diff --git a/src/AcDream.App/UI/UiField.cs b/src/AcDream.App/UI/UiField.cs index f802751a..8cb0af79 100644 --- a/src/AcDream.App/UI/UiField.cs +++ b/src/AcDream.App/UI/UiField.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; @@ -11,16 +11,16 @@ namespace AcDream.App.UI; /// as stubs for future item-window use. /// /// -/// Caret is a glyph index; the caret pixel-X is Σ glyph advances (UiDatFont) to the +/// Caret is a glyph index; the caret pixel-X is Σ glyph advances (UiDatFont) to the /// caret. Supports mouse + Shift-arrow SELECTION, clipboard cut/copy/paste, and /// held-key auto-repeat (hold Backspace deletes continuously). Submit (Enter / Send) /// fires , clears, and pushes history (100-entry cap, -/// sentinel 0xFFFFFFFF — port of ChatInterface::ProcessCommand @0x4f5100). +/// sentinel 0xFFFFFFFF — port of ChatInterface::ProcessCommand @0x4f5100). /// /// /// Decomp: UIElement_Text MoveCursor @0x468d00, FindPixelsFromPos @0x472b40. /// -internal sealed class UiField : UiElement +public sealed class UiField : UiElement { private readonly record struct WrappedLine(int Start, int Length, string Text); @@ -71,9 +71,9 @@ internal sealed class UiField : UiElement /// Wired by the host from . public Silk.NET.Input.IKeyboard? Keyboard { get; set; } - /// Dat sprite resolver (id → GL texture + size) for the focused-field + /// Dat sprite resolver (id → GL texture + size) for the focused-field /// background. Null = fall back to the flat rect. - public Func? SpriteResolve { get; set; } + public Func? SpriteResolve { get; set; } /// Unfocused/default state sprite imported from the DAT. public uint BackgroundSprite { get; set; } /// Gold "lit" field background drawn when focused (retail Normal_focussed @@ -120,7 +120,7 @@ internal sealed class UiField : UiElement /// are reproduced procedurally, so the importer must not build them as widgets. public override bool ConsumesDatChildren => true; - // ── Editing primitives ────────────────────────────────────────────── + // ── Editing primitives ────────────────────────────────────────────── public void InsertChar(char c) { @@ -169,7 +169,7 @@ internal sealed class UiField : UiElement private void MoveCaret(int delta, bool shift) => MoveCaretTo(_caret + delta, shift); - // ── Selection ──────────────────────────────────────────────────────── + // ── Selection ──────────────────────────────────────────────────────── private (int lo, int hi) SelSpan() { @@ -268,7 +268,7 @@ internal sealed class UiField : UiElement _historyIndex = -1; } - // ── Submit + history ───────────────────────────────────────────────── + // ── Submit + history ───────────────────────────────────────────────── public void Submit() { @@ -315,9 +315,9 @@ internal sealed class UiField : UiElement _selAnchor = null; } - // ── Geometry ───────────────────────────────────────────────────────── + // ── Geometry ───────────────────────────────────────────────────────── - /// Pixel-X of the caret (Σ glyph advances to ). + /// Pixel-X of the caret (Σ glyph advances to ). private float MeasureTo(int i) { if (i <= 0) return 0f; @@ -328,7 +328,7 @@ internal sealed class UiField : UiElement public float CaretPixelX() => MeasureTo(_caret); - /// Map a local X (click) to the nearest caret index — retail + /// Map a local X (click) to the nearest caret index — retail /// FindPixelsFromPos inverse. Accounts for the horizontal scroll offset. private int HitCharX(float localX) { @@ -344,25 +344,25 @@ internal sealed class UiField : UiElement return best; } - // ── Draw ───────────────────────────────────────────────────────────── + // ── Draw ───────────────────────────────────────────────────────────── protected override void OnDraw(UiRenderContext ctx) { // Focused = "write mode": draw the gold lit field sprite (retail Normal_focussed). // Unfocused: draw the imported default state, then the flat translucent fallback. // Both go through the sprite bucket - // (DrawFill / DrawSprite) so the text — also sprite-bucket — draws on top. + // (DrawFill / DrawSprite) so the text — also sprite-bucket — draws on top. bool lit = _focused && SpriteResolve is not null && FocusFieldSprite != 0; if (lit) { var (tex, tw, th) = SpriteResolve!(FocusFieldSprite); - if (tex.IsAssigned && tw > 0) ctx.DrawSprite(tex, 0, 0, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); + if (tex != 0 && tw > 0) ctx.DrawSprite(tex, 0, 0, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); else lit = false; } if (!lit && SpriteResolve is not null && BackgroundSprite != 0) { var (tex, tw, th) = SpriteResolve(BackgroundSprite); - if (tex.IsAssigned && tw > 0 && th > 0) + if (tex != 0 && tw > 0 && th > 0) { ctx.DrawSprite(tex, 0, 0, Width, Height, 0f, 0f, Width / tw, Height / th, Vector4.One); @@ -382,7 +382,7 @@ internal sealed class UiField : UiElement float visibleW = MathF.Max(1f, Width - 2f * Padding); // Horizontal scroll: keep the caret inside the field; clamp so we never scroll past - // the text. Then draw only the glyph window that lands inside the field — a single- + // the text. Then draw only the glyph window that lands inside the field — a single- // line text box clips + scrolls (retail UIElement_Text) rather than overflowing the // field (which previously spilled the text out into the 3D world). float caretX = MeasureTo(_caret); @@ -417,7 +417,7 @@ internal sealed class UiField : UiElement if (_focused) { - // Caret on TOP of the text → submitted after the text in the same bucket. + // Caret on TOP of the text → submitted after the text in the same bucket. float cx = Padding + alignX + (caretX - _scrollX); if (cx >= Padding - 1f && cx <= Width - Padding + 1f) ctx.DrawFill(cx, ty, 1f, lh, TextColor); @@ -620,7 +620,7 @@ internal sealed class UiField : UiElement return line.Start + best; } - // ── Auto-repeat ────────────────────────────────────────────────────── + // ── Auto-repeat ────────────────────────────────────────────────────── protected override void OnTick(double deltaSeconds) { @@ -649,7 +649,7 @@ internal sealed class UiField : UiElement && (Keyboard.IsKeyPressed(Silk.NET.Input.Key.ShiftLeft) || Keyboard.IsKeyPressed(Silk.NET.Input.Key.ShiftRight)); - // ── Events ─────────────────────────────────────────────────────────── + // ── Events ─────────────────────────────────────────────────────────── public override bool OnEvent(in UiEvent e) { diff --git a/src/AcDream.App/UI/UiHost.cs b/src/AcDream.App/UI/UiHost.cs index 41bae50a..6fb21eb6 100644 --- a/src/AcDream.App/UI/UiHost.cs +++ b/src/AcDream.App/UI/UiHost.cs @@ -1,8 +1,8 @@ -using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering; using Silk.NET.Input; +using Silk.NET.OpenGL; namespace AcDream.App.UI; @@ -14,7 +14,7 @@ namespace AcDream.App.UI; /// /// Usage (from GameWindow.OnLoad): /// -/// _uiHost = new UiHost(_gpuDevice, () => _gpuFrameLifetime.Current, _debugFont); +/// _uiHost = new UiHost(_gl, shadersDir, _debugFont); /// _uiHost.Root.WorldMouseFallThrough += (btn, x, y, f) => HandleWorldClick(btn, x, y); /// _uiHost.Root.WorldKeyFallThrough += (vk, lp) => HandleHotkey(vk); /// @@ -35,7 +35,7 @@ namespace AcDream.App.UI; /// (Keystone root, widget tree). We fuse them into a single host class /// because we're not linking to Keystone. /// -internal sealed class UiHost : System.IDisposable +public sealed class UiHost : System.IDisposable { public UiRoot Root { get; } = new(); public RetailWindowManager WindowManager => Root.WindowManager; @@ -44,13 +44,12 @@ internal sealed class UiHost : System.IDisposable /// The last wired keyboard. Exposed so widgets that need clipboard /// access () or modifier-key state - /// () — e.g. 's - /// Ctrl+C copy — can reach the device. One-keyboard desktop: last wins. + /// () — e.g. 's + /// Ctrl+C copy — can reach the device. One-keyboard desktop: last wins. public IKeyboard? Keyboard { get; private set; } private long _startTicks = System.Environment.TickCount64; private readonly HostQuiescenceGate _quiescence; - private readonly Func _currentFrame; private readonly List _inputBindings = new(); private ResourceShutdownTransaction? _inputShutdown; private ResourceShutdownTransaction? _shutdown; @@ -59,24 +58,23 @@ internal sealed class UiHost : System.IDisposable internal bool IsDisposalComplete => _disposed; - public UiHost(IGpuDevice device, Func currentFrame, BitmapFont? defaultFont = null) - : this(device, currentFrame, defaultFont, new HostQuiescenceGate()) + public UiHost(GL gl, string shaderDir, BitmapFont? defaultFont = null) + : this(gl, shaderDir, defaultFont, new HostQuiescenceGate()) { } internal UiHost( - IGpuDevice device, - Func currentFrame, + GL gl, + string shaderDir, BitmapFont? defaultFont, HostQuiescenceGate quiescence) { _quiescence = quiescence ?? throw new ArgumentNullException(nameof(quiescence)); - _currentFrame = currentFrame ?? throw new ArgumentNullException(nameof(currentFrame)); - TextRenderer = new TextRenderer(device); + TextRenderer = new TextRenderer(gl, shaderDir); DefaultFont = defaultFont; } - // ── Per-frame ────────────────────────────────────────────────────── + // ── Per-frame ────────────────────────────────────────────────────── public void Tick(double deltaSeconds) { @@ -92,10 +90,10 @@ internal sealed class UiHost : System.IDisposable var ctx = new UiRenderContext(TextRenderer, screenSize, DefaultFont); TextRenderer.Begin(screenSize); Root.Draw(ctx); - TextRenderer.Flush(DefaultFont, _currentFrame()); + TextRenderer.Flush(DefaultFont); } - // ── Input wiring helpers ─────────────────────────────────────────── + // ── Input wiring helpers ─────────────────────────────────────────── public void WireMouse(IMouse mouse) { @@ -185,7 +183,7 @@ internal sealed class UiHost : System.IDisposable } } - // ── Window manager forwarders (delegate to UiRoot) ───────────────── + // ── Window manager forwarders (delegate to UiRoot) ───────────────── /// Register a top-level window for Show/Hide/Toggle. See . public RetailWindowHandle RegisterWindow( diff --git a/src/AcDream.App/UI/UiItemList.cs b/src/AcDream.App/UI/UiItemList.cs index abdf9d86..d96b3654 100644 --- a/src/AcDream.App/UI/UiItemList.cs +++ b/src/AcDream.App/UI/UiItemList.cs @@ -1,10 +1,10 @@ -using System; +using System; using System.Collections.Generic; namespace AcDream.App.UI; /// Cell order for a multi-column . -internal enum UiItemListFlow +public enum UiItemListFlow { /// Retail listbox bit 0 set: index advances across columns, then down rows. RowMajor, @@ -19,7 +19,7 @@ internal enum UiItemListFlow /// LayoutImporter must NOT build dat children. The toolbar uses single-cell /// instances (one slot); the inventory phase will grow this to an N-cell grid. /// -internal sealed class UiItemList : UiElement +public sealed class UiItemList : UiElement { private readonly List _cells = new(); private int _layoutDeferralDepth; @@ -29,7 +29,7 @@ internal sealed class UiItemList : UiElement public UiScrollable Scroll { get; } public UiItemList( - Func? spriteResolve = null, + Func? spriteResolve = null, UiScrollable? scroll = null) { Scroll = scroll ?? new UiScrollable(); @@ -40,7 +40,7 @@ internal sealed class UiItemList : UiElement public override bool ConsumesDatChildren => true; - public Func? SpriteResolve { get; set; } + public Func? SpriteResolve { get; set; } private IReadOnlyList? _cooldownSprites; public IReadOnlyList? CooldownSprites @@ -165,7 +165,7 @@ internal sealed class UiItemList : UiElement cell.CooldownStepProvider ??= _cooldownStepProvider; if (_cellEmptySprite != 0) cell.EmptySprite = _cellEmptySprite; // The list lays cells out procedurally (grid offset + scroll clip), so cells must be - // EXEMPT from the parent anchor pass — UiElement.DrawSelfAndChildren runs ApplyAnchor + // EXEMPT from the parent anchor pass — UiElement.DrawSelfAndChildren runs ApplyAnchor // on every child after OnDraw, which would otherwise reset the scroll offset to each // cell's captured base position (the escaping-grid bug). Anchors=None makes LayoutCells // the sole authority over cell rects. @@ -197,7 +197,7 @@ internal sealed class UiItemList : UiElement /// public UiItemListFlow Flow { get; set; } = UiItemListFlow.RowMajor; - /// Fixed cell width in grid mode. 0 = "fill the list" — the single cell sizes + /// Fixed cell width in grid mode. 0 = "fill the list" — the single cell sizes /// to the whole list (the toolbar single-slot legacy). Set >0 (with CellHeight) for a grid. public float CellWidth { get; set; } /// Fixed cell height in grid mode (pairs with CellWidth). diff --git a/src/AcDream.App/UI/UiItemSlot.cs b/src/AcDream.App/UI/UiItemSlot.cs index faf2b0a2..898c14af 100644 --- a/src/AcDream.App/UI/UiItemSlot.cs +++ b/src/AcDream.App/UI/UiItemSlot.cs @@ -1,24 +1,16 @@ -using System; +using System; using System.Numerics; using AcDream.Core.Items; namespace AcDream.App.UI; -/// -/// Accept/reject overlay state while a drag hovers an item slot cell. Top-level -/// (not nested in , which is internal) because -/// CursorFeedbackSnapshot is a public test-visible type that carries this -/// value. -/// -public enum DragAcceptState { None, Accept, Reject } - /// /// One item-in-a-slot cell (port of retail UIElement_UIItem, class 0x10000032). /// A behavioral LEAF: it draws the empty-slot sprite when unbound, else a /// pre-composited icon texture (set by the controller). Holds the bound weenie /// guid (retail UIElement_UIItem::itemID, +0x5FC). /// -internal class UiItemSlot : UiElement +public class UiItemSlot : UiElement { public UiItemSlot() { ClickThrough = false; } @@ -27,15 +19,15 @@ internal class UiItemSlot : UiElement /// Bound weenie guid (0 = empty). Retail UIElement_UIItem::itemID. public uint ItemId { get; private set; } - /// Pre-composited icon texture-table slot for the bound item (Unassigned = none). - public GpuTextureSlot IconTexture { get; private set; } = GpuTextureSlot.Unassigned; + /// Pre-composited icon GL texture for the bound item (0 = none). + public uint IconTexture { get; private set; } /// /// Underlay-free cursor graphic for the bound item (retail IconData::m_pDragIcon). /// Falls back to only for callers that have not supplied the /// dedicated composite. /// - public GpuTextureSlot DragIconTexture { get; private set; } = GpuTextureSlot.Unassigned; + public uint DragIconTexture { get; private set; } /// /// Lossless shortcut record when this cell belongs to the toolbar. Physical item @@ -44,7 +36,7 @@ internal class UiItemSlot : UiElement public ShortcutEntry? Shortcut { get; private set; } /// This cell's own index within its panel (0..17 toolbar; container slot - /// for inventory). Distinct from (the 1–9 label, -1 on the + /// for inventory). Distinct from (the 1–9 label, -1 on the /// bottom row). Set by the controller; used as the drag payload's SourceSlot and to /// identify the drop TARGET slot. public int SlotIndex { get; set; } = -1; @@ -62,24 +54,24 @@ internal class UiItemSlot : UiElement /// True when this cell is the OPEN container (its contents fill the grid). Draws the /// open-container triangle. Port of UIElement_ItemList::UpdateOpenContainerIndicator - /// (0x004e3070) → SetOpenContainerState (0x004e1200): shown when item.itemID == openContainerId. + /// (0x004e3070) → SetOpenContainerState (0x004e1200): shown when item.itemID == openContainerId. public bool IsOpenContainer { get; set; } /// Open-container triangle sprite (element 0x10000450 on the container prototype /// 0x1000033F). Configurable; guard id != 0 before resolving. public uint OpenContainerSprite { get; set; } = 0x06005D9Cu; /// True when this cell is the SELECTED item. Draws the green/yellow selection square. - /// Port of UIElement_ItemList::ItemList_SetSelectedItem (0x004e2fe0) → SetSelectedState + /// Port of UIElement_ItemList::ItemList_SetSelectedItem (0x004e2fe0) → SetSelectedState /// (0x004e1240): shown when item.itemID == selectedItemId. Uniform across item + container cells. public bool Selected { get; set; } - /// Selected-item square sprite (element 0x10000342 on the 32×32 item prototype + /// Selected-item square sprite (element 0x10000342 on the 32×32 item prototype /// 0x10000341; pixel-confirmed green/yellow frame). Drawn as a procedural overlay so it renders - /// on the 36×36 container cell too (whose prototype lacks the square child). + /// on the 36×36 container cell too (whose prototype lacks the square child). public uint SelectedSprite { get; set; } = 0x06004D21u; /// Container fullness [0..1], or -1 = hidden (the cell is not a container, or its /// itemsCapacity is unknown/0). Port of UIElement_UIItem::UpdateCapacityDisplay (0x004e16e0): a - /// per-cell vertical UIElement_Meter (element 0x10000347, 5×30 at x=26,y=1) shown only when + /// per-cell vertical UIElement_Meter (element 0x10000347, 5×30 at x=26,y=1) shown only when /// isContainer && itemsCapacity > 0, fill = numContainedItems / itemsCapacity (meter attr 0x69). public float CapacityFill { get; set; } = -1f; /// Capacity-bar track sprite (meter 0x10000347 DirectState). @@ -87,8 +79,10 @@ internal class UiItemSlot : UiElement /// Capacity-bar fill sprite (meter 0x10000347 front child 0x00000002 DirectState). public uint CapacityFrontSprite { get; set; } = 0x06004D23u; + /// Accept/reject overlay state while a drag hovers this cell. + public enum DragAcceptState { None, Accept, Reject } private DragAcceptState _dragAccept = DragAcceptState.None; - /// Current overlay state — internal so unit tests can assert it (InternalsVisibleTo). + /// Current overlay state — internal so unit tests can assert it (InternalsVisibleTo). internal DragAcceptState DragAcceptVisual => _dragAccept; /// Empty-slot sprite. Default = the generic toolbar empty-slot border @@ -108,7 +102,7 @@ internal class UiItemSlot : UiElement private bool _primaryPressConsumed; /// RenderSurface id -> (GL texture, w, h). Set by the factory/controller. - public Func? SpriteResolve { get; set; } + public Func? SpriteResolve { get; set; } /// /// DAT-authored 10%..100% radial cooldown overlays from element ids @@ -124,21 +118,21 @@ internal class UiItemSlot : UiElement public void SetItem( uint itemId, - GpuTextureSlot iconTexture, + uint iconTexture, ShortcutEntry? shortcut = null, - GpuTextureSlot? dragIconTexture = null) + uint dragIconTexture = 0) { ItemId = itemId; IconTexture = iconTexture; - DragIconTexture = dragIconTexture ?? GpuTextureSlot.Unassigned; + DragIconTexture = dragIconTexture; Shortcut = shortcut; } public void Clear() { ItemId = 0; - IconTexture = GpuTextureSlot.Unassigned; - DragIconTexture = GpuTextureSlot.Unassigned; + IconTexture = 0; + DragIconTexture = 0; Shortcut = null; _waiting = false; _primaryPressConsumed = false; @@ -151,13 +145,13 @@ internal class UiItemSlot : UiElement : null; /// - public override (GpuTextureSlot tex, int w, int h)? GetDragGhost() + public override (uint tex, int w, int h)? GetDragGhost() { if (ItemId == 0) return null; - // RenderIcons creates m_pDragIcon on a fixed 0x20 × 0x20 surface, even when its - // containing bag cell is 36 × 36. The cursor hotspot is correspondingly (16,16). - if (DragIconTexture.IsAssigned) return (DragIconTexture, 32, 32); - return IconTexture.IsAssigned ? (IconTexture, (int)Width, (int)Height) : null; + // RenderIcons creates m_pDragIcon on a fixed 0x20 × 0x20 surface, even when its + // containing bag cell is 36 × 36. The cursor hotspot is correspondingly (16,16). + if (DragIconTexture != 0) return (DragIconTexture, 32, 32); + return IconTexture != 0 ? (IconTexture, (int)Width, (int)Height) : null; } /// @@ -177,7 +171,7 @@ internal class UiItemSlot : UiElement internal void SetWaitingState(bool waiting) => _waiting = waiting && ItemId != 0; - /// An OCCUPIED slot is a drag source — a press-and-move picks up the item + /// An OCCUPIED slot is a drag source — a press-and-move picks up the item /// rather than moving the toolbar window. An EMPTY slot is NOT a drag source, so a /// press-and-move there falls through to the IA-12 whole-window-drag, keeping the bar /// movable by its empty cells / chrome. Drives 's mousedown @@ -193,23 +187,23 @@ internal class UiItemSlot : UiElement return null; } - // ── Shortcut number (slot label) ───────────────────────────────────────── + // ── Shortcut number (slot label) ───────────────────────────────────────── // Port of UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465). // Retail draws the digit on the cell's ShortcutNum sub-element, picking the // digit image from a DID-array property: 0x10000042 (regular) / 0x10000043 (ghosted), - // indexed by slot position. Each digit is a 32×32 PFID_A8R8G8B8 RenderSurface + // indexed by slot position. Each digit is a 32×32 PFID_A8R8G8B8 RenderSurface // with the digit baked into the top-left corner (rest alpha=0), drawn Alphablend. /// Slot position in the shortcut bar (0-indexed). -1 = no number (retail - /// SetVisible(0) when edi < 0). Top row: 0..8 → digits 1..9. Bottom row: -1. + /// SetVisible(0) when edi < 0). Top row: 0..8 → digits 1..9. Bottom row: -1. public int ShortcutNum { get; private set; } = -1; /// True when retail marks the physical-item shortcut as ghosted. public bool ShortcutGhosted { get; private set; } - /// Regular digit DID array. Index i → digit (i+1) sprite RenderSurface id. + /// Regular digit DID array. Index i → digit (i+1) sprite RenderSurface id. /// Injected by the controller after reading LayoutDesc 0x21000037. - /// Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481) — occupied slot picks + /// Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481) — occupied slot picks /// property 0x10000042 when the shortcut is not ghosted. public uint[]? RegularDigits { get; set; } @@ -219,7 +213,7 @@ internal class UiItemSlot : UiElement /// Empty-slot digit DID array (property 0x1000005e, stance-independent). /// Used when the slot is EMPTY (ItemId == 0). Retail ref: UIElement_UIItem::SetShortcutNum - /// (decomp 229481) — else branch when m_elem_Icon->m_state == 0x1000001c (empty). + /// (decomp 229481) — else branch when m_elem_Icon->m_state == 0x1000001c (empty). public uint[]? EmptyDigits { get; set; } /// @@ -247,8 +241,8 @@ internal class UiItemSlot : UiElement /// /// Returns the digit DID array that OnDraw will use, following the retail occupancy /// branch in UIElement_UIItem::SetShortcutNum (decomp 229481): - /// occupied (ItemId != 0) → ShortcutGhosted ? GhostedDigits : RegularDigits (0x10000043/42) - /// empty (ItemId == 0) → EmptyDigits (0x1000005e, stance-independent) + /// occupied (ItemId != 0) → ShortcutGhosted ? GhostedDigits : RegularDigits (0x10000043/42) + /// empty (ItemId == 0) → EmptyDigits (0x1000005e, stance-independent) /// Exposed as an internal method so unit tests can assert array selection without /// needing a real render context. /// @@ -259,7 +253,7 @@ internal class UiItemSlot : UiElement : EmptyDigits; } - // ── Events / draw ───────────────────────────────────────────────────────── + // ── Events / draw ───────────────────────────────────────────────────────── /// Invoked by when a completed left click lands /// on a bound slot. Selection already occurred on MouseDown through the owning @@ -275,7 +269,7 @@ internal class UiItemSlot : UiElement { switch (e.Type) { - // Use fires on CLICK (mouse-up over the same cell), not MouseDown — so a + // Use fires on CLICK (mouse-up over the same cell), not MouseDown — so a // drag (press + >3px move) does NOT also use the item. UiRoot suppresses the // post-drag Click (UiRoot.cs FinishDrag returns before the Click emit). case UiEventType.MouseDown: @@ -302,13 +296,13 @@ internal class UiItemSlot : UiElement return true; case UiEventType.DragBegin: - // Notify the source list's handler so it can lift (remove + wire) — retail - // RecvNotice_ItemListBeginDrag → RemoveShortcut. UiRoot snapshotted the ghost first. + // Notify the source list's handler so it can lift (remove + wire) — retail + // RecvNotice_ItemListBeginDrag → RemoveShortcut. UiRoot snapshotted the ghost first. if (FindList() is { DragHandler: { } lh } liftList && e.Payload is ItemDragPayload lp) lh.OnDragLift(liftList, this, lp); return true; - case UiEventType.DragEnter: // pointer entered me mid-drag → ask the list's handler + case UiEventType.DragEnter: // pointer entered me mid-drag → ask the list's handler _dragAccept = FindList() is { DragHandler: { } h } list && e.Payload is ItemDragPayload p ? h.OnDragOver(list, this, p) switch @@ -320,7 +314,7 @@ internal class UiItemSlot : UiElement : DragAcceptState.Reject; return true; - case UiEventType.DragOver: // UiRoot fires this on LEAVE → neutral + case UiEventType.DragOver: // UiRoot fires this on LEAVE → neutral _dragAccept = DragAcceptState.None; return true; @@ -337,43 +331,43 @@ internal class UiItemSlot : UiElement { // Draw the icon (filled slot) or the empty-slot border. Both paths fall through // to the digit draw below; the slot label always shows on top-row slots. - if (ItemId != 0 && IconTexture.IsAssigned) + if (ItemId != 0 && IconTexture != 0) { ctx.DrawSprite(IconTexture, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); } else if (SpriteResolve is not null && EmptySprite != 0) { var (tex, _, _) = SpriteResolve(EmptySprite); - if (tex.IsAssigned) + if (tex != 0) ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); } // Digit overlay: UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465). // Occupancy branch (decomp 229481): - // occupied (ItemId != 0) → regular/ghosted digit set 0x10000042/43 - // empty (ItemId == 0) → background digit set 0x1000005e, stance-independent + // occupied (ItemId != 0) → regular/ghosted digit set 0x10000042/43 + // empty (ItemId == 0) → background digit set 0x1000005e, stance-independent // Each digit image is corner-baked (glyph in top-left, rest alpha=0); drawn // full-cell Alphablend so the transparent region is invisible. DrawShortcutOverlay(ctx); - // Container capacity bar — UIElement_UIItem::UpdateCapacityDisplay (0x004e16e0): a vertical - // UIElement_Meter (element 0x10000347, 5×30 at x=26,y=1) shown only for container cells + // Container capacity bar — UIElement_UIItem::UpdateCapacityDisplay (0x004e16e0): a vertical + // UIElement_Meter (element 0x10000347, 5×30 at x=26,y=1) shown only for container cells // (CapacityFill >= 0). Track drawn full; fill clipped to the fraction from the BOTTOM (the - // "how full" direction). Procedural — UiItemSlot is a behavioral leaf. Guard id != 0 first. + // "how full" direction). Procedural — UiItemSlot is a behavioral leaf. Guard id != 0 first. if (CapacityFill >= 0f && SpriteResolve is not null) { - const float by = 1f, bw = 5f, bh = 30f; // element 0x10000347 size (dat 5×30 at y=1) + const float by = 1f, bw = 5f, bh = 30f; // element 0x10000347 size (dat 5×30 at y=1) float bx = Width - bw; // flush to the cell's right edge (visual gate: the dat X=26 sat ~5px off the edge) if (CapacityBackSprite != 0) { var (bt, _, _) = SpriteResolve(CapacityBackSprite); - if (bt.IsAssigned) ctx.DrawSprite(bt, bx, by, bw, bh, 0f, 0f, 1f, 1f, Vector4.One); + if (bt != 0) ctx.DrawSprite(bt, bx, by, bw, bh, 0f, 0f, 1f, 1f, Vector4.One); } float f = Math.Clamp(CapacityFill, 0f, 1f); if (f > 0f && CapacityFrontSprite != 0) { var (ft, _, _) = SpriteResolve(CapacityFrontSprite); - if (ft.IsAssigned) + if (ft != 0) { // Bottom-up fill: draw the bottom f-fraction of the bar, sampling the matching // bottom slice of the front sprite (UV v from 1-f to 1). @@ -383,7 +377,7 @@ internal class UiItemSlot : UiElement } } - // Pending/drag-source mesh — retail UIElement_UIItem::SetWaitingState (0x004e11b0) + // Pending/drag-source mesh — retail UIElement_UIItem::SetWaitingState (0x004e11b0) // reveals m_elem_Icon_Ghosted (0x10000349). ItemList_BeginDrag (0x004e32d0) // sets it for physical inventory/equipment cells while leaving their icon in place. // Draw it before the persistent selected/open indicators: retail keeps selection visible @@ -391,7 +385,7 @@ internal class UiItemSlot : UiElement if (_waiting && SpriteResolve is not null && WaitingSprite != 0) { var (tex, _, _) = SpriteResolve(WaitingSprite); - if (tex.IsAssigned) + if (tex != 0) ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); } @@ -402,18 +396,18 @@ internal class UiItemSlot : UiElement if (IsOpenContainer && SpriteResolve is not null && OpenContainerSprite != 0) { var (tex, _, _) = SpriteResolve(OpenContainerSprite); - if (tex.IsAssigned) + if (tex != 0) ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); } if (Selected && SpriteResolve is not null && SelectedSprite != 0) { var (tex, _, _) = SpriteResolve(SelectedSprite); - if (tex.IsAssigned) + if (tex != 0) ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); } // Drag-rollover accept/reject frame (retail SetDragAcceptState 0x10000041/40). - // Guard id != 0 BEFORE resolving — resolve(0) returns the 1×1 magenta placeholder + // Guard id != 0 BEFORE resolving — resolve(0) returns the 1×1 magenta placeholder // with a non-zero GL handle (feedback_ui_resolve_zero_magenta). if (_dragAccept != DragAcceptState.None && SpriteResolve is not null) { @@ -421,7 +415,7 @@ internal class UiItemSlot : UiElement if (id != 0) { var (tex, _, _) = SpriteResolve(id); - if (tex.IsAssigned) + if (tex != 0) ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); } } @@ -434,7 +428,7 @@ internal class UiItemSlot : UiElement if (cooldownSprite != 0u && SpriteResolve is not null) { var (texture, _, _) = SpriteResolve(cooldownSprite); - if (texture.IsAssigned) + if (texture != 0u) ctx.DrawSprite( texture, 0f, @@ -466,7 +460,7 @@ internal class UiItemSlot : UiElement return; var (texture, _, _) = SpriteResolve(did); - if (texture.IsAssigned) + if (texture != 0) ctx.DrawSprite(texture, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); } diff --git a/src/AcDream.App/UI/UiLayoutPolicy.cs b/src/AcDream.App/UI/UiLayoutPolicy.cs index 5d5f2269..bd1af24e 100644 --- a/src/AcDream.App/UI/UiLayoutPolicy.cs +++ b/src/AcDream.App/UI/UiLayoutPolicy.cs @@ -1,7 +1,7 @@ -namespace AcDream.App.UI; +namespace AcDream.App.UI; /// An inclusive integer pixel box, matching retail Box2D. -internal readonly record struct UiPixelRect(int X0, int Y0, int X1, int Y1) +public readonly record struct UiPixelRect(int X0, int Y0, int X1, int Y1) { public int Width => X1 - X0 + 1; public int Height => Y1 - Y0 + 1; @@ -16,7 +16,7 @@ internal readonly record struct UiPixelRect(int X0, int Y0, int X1, int Y1) /// retain all four independent modes; programmatic widgets continue to use /// . /// -internal sealed class UiLayoutPolicy +public sealed class UiLayoutPolicy { public uint LeftMode { get; } public uint TopMode { get; } diff --git a/src/AcDream.App/UI/UiMenu.cs b/src/AcDream.App/UI/UiMenu.cs index b84f5f35..c10bd419 100644 --- a/src/AcDream.App/UI/UiMenu.cs +++ b/src/AcDream.App/UI/UiMenu.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; @@ -13,10 +13,10 @@ namespace AcDream.App.UI; /// knowledge are populated by the controller, not baked into this widget. Built /// by for Type-6 elements. /// -internal sealed class UiMenu : UiElement +public sealed class UiMenu : UiElement { /// One menu row: its label + an opaque payload the controller maps back. - internal readonly record struct MenuItem(string Label, object? Payload); + public readonly record struct MenuItem(string Label, object? Payload); /// The rows, populated by the controller. Laid out column-major: /// rows 0..RowsPerColumn-1 in column 0, then the next group in column 1, etc. @@ -28,10 +28,10 @@ internal sealed class UiMenu : UiElement /// Fired with the picked item's payload when a row is chosen. public Action? OnSelect { get; set; } - /// Per-payload enabled gate (disabled rows render greyed + are inert). Null ⇒ all enabled. + /// Per-payload enabled gate (disabled rows render greyed + are inert). Null ⇒ all enabled. public Func? EnabledProvider { get; set; } - /// Button-face caption (the active target). Null ⇒ blank face. + /// Button-face caption (the active target). Null ⇒ blank face. public Func? ButtonLabelProvider { get; set; } public int RowsPerColumn { get; set; } = 7; // items per column (dat item template) @@ -43,29 +43,29 @@ internal sealed class UiMenu : UiElement // square; the label starts just past it (box width + small gap) so text aligns with // the box instead of overlapping it. private const float TextIndent = 19f; - // The button face sprite (0x06004D65/66) bakes a status LED (red→green) into its - // left socket (~x4–20 of the 46px button); the caption starts past it so it doesn't + // The button face sprite (0x06004D65/66) bakes a status LED (red→green) into its + // left socket (~x4–20 of the 46px button); the caption starts past it so it doesn't // render over the LED. private const float ButtonTextIndent = 20f; public UiDatFont? DatFont { get; set; } public AcDream.App.Rendering.BitmapFont? Font { get; set; } - public Func? SpriteResolve { get; set; } + public Func? SpriteResolve { get; set; } // Button face sprites (dat menu element 0x10000014). public uint NormalSprite { get; set; } public uint PressedSprite { get; set; } // Popup chrome sprites (dat menu popup template, layout 0x21000006). - public uint PopupBgSprite { get; set; } // 0x0600124C — panel fill (191×2 tiles) - public uint ItemNormalSprite { get; set; } // 0x0600124E — a row background (191×17) - public uint ItemHighlightSprite { get; set; } // 0x0600124D — the active channel's row + public uint PopupBgSprite { get; set; } // 0x0600124C — panel fill (191×2 tiles) + public uint ItemNormalSprite { get; set; } // 0x0600124E — a row background (191×17) + public uint ItemHighlightSprite { get; set; } // 0x0600124D — the active channel's row public Vector4 TextColor { get; set; } = new(1f, 0.92f, 0.72f, 1f); - /// Available item text — retail white #FFFFFF (gmMainChatUI talk-focus + /// Available item text — retail white #FFFFFF (gmMainChatUI talk-focus /// enabled state). Confirmed via decomp: enabled items render white. public Vector4 TextColorAvailable { get; set; } = new(1f, 1f, 1f, 1f); - /// Disabled/unavailable item text — retail GREYS these (UIElement state 0xd - /// disabled StateDesc colour). NOT the salmon colorPink (0x81c528) we had before — that + /// Disabled/unavailable item text — retail GREYS these (UIElement state 0xd + /// disabled StateDesc colour). NOT the salmon colorPink (0x81c528) we had before — that /// belongs to the chat-MESSAGE palette and was misapplied. Exact float lives in the dat /// StateDesc (not a code symbol); ~0.5 neutral grey here pending a live cdb dump. public Vector4 TextColorGhosted { get; set; } = new(0.5f, 0.5f, 0.5f, 1f); @@ -92,7 +92,7 @@ internal sealed class UiMenu : UiElement if (resolve is not null) { var (tex, tw, _) = resolve(_open ? PressedSprite : NormalSprite); - if (tex.IsAssigned && tw > 0) DrawButtonFace(ctx, tex, tw); + if (tex != 0 && tw > 0) DrawButtonFace(ctx, tex, tw); } DrawLabel(ctx, ButtonLabelProvider?.Invoke() ?? "", ButtonTextIndent, (Height - LineH()) * 0.5f, TextColor); } @@ -102,7 +102,7 @@ internal sealed class UiMenu : UiElement // point. Slicing keeps the LED + arrow undistorted when the button widens to its label. private const float FaceCapL = 20f, FaceCapR = 12f; - private void DrawButtonFace(UiRenderContext ctx, GpuTextureSlot tex, float tw) + private void DrawButtonFace(UiRenderContext ctx, uint tex, float tw) { float uL = FaceCapL / tw, uR = (tw - FaceCapR) / tw; float midDest = Width - FaceCapL - FaceCapR; @@ -112,7 +112,7 @@ internal sealed class UiMenu : UiElement ctx.DrawSprite(tex, Width - FaceCapR, 0f, FaceCapR, Height, uR, 0f, 1f, 1f, Vector4.One); // arrow cap } - /// The button width that fits "LED cap + channel label + arrow cap" — retail + /// The button width that fits "LED cap + channel label + arrow cap" — retail /// sizes the talk-focus button to its selected label. The controller widens the button /// to this and reflows the input field to start after it. public float NaturalButtonWidth() @@ -123,7 +123,7 @@ internal sealed class UiMenu : UiElement } /// The open popup draws in the OVERLAY pass so it sits on top of the whole - /// UI — otherwise the translucent chat panel (drawn after this element in the main + /// UI — otherwise the translucent chat panel (drawn after this element in the main /// pass) greys out the part of the popup that overlaps it. protected override void OnDrawOverlay(UiRenderContext ctx) { @@ -133,7 +133,7 @@ internal sealed class UiMenu : UiElement // Column-major popup opening UPWARD from the button, wrapped in the universal // 8-piece window bevel (retail UIElement_Menu::MakePopup spawns the popup as a // bevelled floating window). Force OPAQUE (a menu reads solid even though the - // chat window is translucent). Draw bevel → panel fill → row sprites → labels, + // chat window is translucent). Draw bevel → panel fill → row sprites → labels, // all through the sprite bucket in submission order so labels land on top. ctx.PushAlphaAbsolute(1f); try @@ -170,7 +170,7 @@ internal sealed class UiMenu : UiElement /// ,). Reuses the same geometry + /// ids as ; no resize /// grips (a menu popup is not resizable). - private void DrawBevel(UiRenderContext ctx, Func resolve, + private void DrawBevel(UiRenderContext ctx, Func resolve, float x, float y, float w, float h) { var r = UiNineSlicePanel.ComputeFrameRects(w, h, Border); @@ -188,13 +188,13 @@ internal sealed class UiMenu : UiElement private float LineH() => DatFont?.LineHeight ?? Font?.LineHeight ?? 14f; - private void DrawSprite(UiRenderContext ctx, Func resolve, + private void DrawSprite(UiRenderContext ctx, Func resolve, uint id, float x, float y, float w, float h) { if (id == 0) return; var (tex, tw, th) = resolve(id); - if (!tex.IsAssigned || tw == 0 || th == 0) return; - // Tile at native size (the panel fill is 191×2; rows are 191×17 = 1:1). + if (tex == 0 || tw == 0 || th == 0) return; + // Tile at native size (the panel fill is 191×2; rows are 191×17 = 1:1). ctx.DrawSprite(tex, x, y, w, h, 0f, 0f, w / tw, h / th, Vector4.One); } @@ -230,7 +230,7 @@ internal sealed class UiMenu : UiElement // The widget REPORTS the pick; the controller owns Selected (it sets // Selected only for payloads it acts on). This mirrors retail // UIElement_Menu::NewSelection delegating to the owner rather than - // self-selecting — so a deferred/no-op item (e.g. the Squelch / + // self-selecting — so a deferred/no-op item (e.g. the Squelch / // Tell-to-Selected specials, null payload) leaves the current // selection + highlight unchanged when the controller ignores it. OnSelect?.Invoke(Items[idx].Payload); diff --git a/src/AcDream.App/UI/UiMeter.cs b/src/AcDream.App/UI/UiMeter.cs index 82813761..d4aac0cc 100644 --- a/src/AcDream.App/UI/UiMeter.cs +++ b/src/AcDream.App/UI/UiMeter.cs @@ -1,20 +1,20 @@ -using System.Numerics; +using System.Numerics; namespace AcDream.App.UI; /// /// A horizontal vital bar (retail HP/Stamina/Mana style): a background rect, a /// partial-width solid fill, and an optional centered "current/max" numeric -/// overlay. returns 0..1 (null = no data → empty bar); +/// overlay. returns 0..1 (null = no data → empty bar); /// returns the overlay text (null = no number). /// /// /// Solid-color fill + debug font for Spec 1. The retail gradient bar sprite -/// (glassy center highlight) and the retail dat font are a later polish pass — +/// (glassy center highlight) and the retail dat font are a later polish pass — /// retail's vitals are bars exactly like this, just sprited. /// /// -internal sealed class UiMeter : UiElement, IUiDatStateful +public sealed class UiMeter : UiElement, IUiDatStateful { private readonly Dictionary _stateFillSprites = new(); @@ -37,13 +37,13 @@ internal sealed class UiMeter : UiElement, IUiDatStateful /// Resolver from a RenderSurface DataId to (GL handle, w, h). When set /// with the 9-slice ids below, the bar draws the retail sprites instead of solid color. - public Func? SpriteResolve { get; set; } + public Func? SpriteResolve { get; set; } // Retail vital bars are a horizontal 3-slice: a fixed-width bevelled left-cap, - // a TILED gradient middle (the "fill-tile" repeats at native width — it does not + // a TILED gradient middle (the "fill-tile" repeats at native width — it does not // stretch), and a fixed-width right-cap. The "back" slice is the empty track // (drawn full width); the "front" slice is the coloured fill (drawn full-geometry - // but CLIPPED to the fill fraction — its own right-cap shows at 100%, the back's + // but CLIPPED to the fill fraction — its own right-cap shows at 100%, the back's // shows through when partial). Ids come from the stacked vitals LayoutDesc // (0x2100006C) via the dump-vitals-layout CLI; 0 = none. /// Empty-track left-cap RenderSurface id. @@ -110,8 +110,8 @@ internal sealed class UiMeter : UiElement, IUiDatStateful } /// Clamp to [0,1] and return the vertical fill rect - /// (local px). true → the fill occupies the bottom - /// h*pct px (retail direction 4); false → the top (direction 2). + /// (local px). true → the fill occupies the bottom + /// h*pct px (retail direction 4); false → the top (direction 2). public static (float x, float y, float w, float h) ComputeVFillRect( float pct, float w, float h, bool fromBottom) { @@ -142,7 +142,7 @@ internal sealed class UiMeter : UiElement, IUiDatStateful // empty track, drawn full width; the FRONT 3-slice is the coloured fill, // drawn at FULL width too but horizontally CLIPPED to the fill fraction. // The front carries its own right-cap (shown at 100%); clipping below 100% - // removes it and reveals the back track's right-cap — retail's scissor-fill. + // removes it and reveals the back track's right-cap — retail's scissor-fill. DrawHBar(ctx, resolve, BackLeft, BackTile, BackRight, Width); if (pct is not null && p > 0f) DrawHBar(ctx, resolve, FrontLeft, FrontTile, FrontRight, Width * p); @@ -191,7 +191,7 @@ internal sealed class UiMeter : UiElement, IUiDatStateful /// A 0 id skips that slice. /// private void DrawHBar( - UiRenderContext ctx, Func resolve, + UiRenderContext ctx, Func resolve, uint leftId, uint midId, uint rightId, float clipW) { if (clipW <= 0f) return; @@ -201,18 +201,18 @@ internal sealed class UiMeter : UiElement, IUiDatStateful // testing `tex != 0` would draw a 1px magenta cap. The single-image meter (toolbar // selected-object bar) has no left/right caps (ids 0); the 3-slice vitals meter sets // all six ids. Guard on the id, not the resolved handle. - var (lt, lw, _) = leftId != 0 ? resolve(leftId) : (GpuTextureSlot.Unassigned, 0, 0); - var (mt, mw, _) = midId != 0 ? resolve(midId) : (GpuTextureSlot.Unassigned, 0, 0); - var (rt, rw, _) = rightId != 0 ? resolve(rightId) : (GpuTextureSlot.Unassigned, 0, 0); + var (lt, lw, _) = leftId != 0 ? resolve(leftId) : (0u, 0, 0); + var (mt, mw, _) = midId != 0 ? resolve(midId) : (0u, 0, 0); + var (rt, rw, _) = rightId != 0 ? resolve(rightId) : (0u, 0, 0); - float capL = lt.IsAssigned ? MathF.Min(lw, w) : 0f; - float capR = rt.IsAssigned ? MathF.Min(rw, w - capL) : 0f; + float capL = lt != 0 ? MathF.Min(lw, w) : 0f; + float capR = rt != 0 ? MathF.Min(rw, w - capL) : 0f; float midW = w - capL - capR; // Each slice's texture repeats every NATIVE-width px (UV-repeat; the UI - // texture is GL_REPEAT-wrapped — TextureCache.UploadRgba8). Caps span their - // own native width → a single 1:1 copy. The wide middle spans many native - // widths → it TILES, matching retail's "fill-tile" + ImgTex::TileCSI rather + // texture is GL_REPEAT-wrapped — TextureCache.UploadRgba8). Caps span their + // own native width → a single 1:1 copy. The wide middle spans many native + // widths → it TILES, matching retail's "fill-tile" + ImgTex::TileCSI rather // than stretching one copy. (Same UV-repeat the chrome border already uses.) DrawPiece(ctx, lt, 0f, capL, lw, h, clipW); DrawPiece(ctx, mt, capL, midW, mw, h, clipW); @@ -224,12 +224,12 @@ internal sealed class UiMeter : UiElement, IUiDatStateful /// bottom () or top, UV-cropped to that fraction of the /// sprite so the fill reveals the matching part of the art (retail /// UIElement_Meter::DrawChildren Box2D clip, direction 2/4). A 0 id is a no-op. - private void DrawVBar(UiRenderContext ctx, Func resolve, + private void DrawVBar(UiRenderContext ctx, Func resolve, uint tileId, float visibleH, bool fromBottom, bool isFill) { if (tileId == 0 || visibleH <= 0f) return; var (tex, _, _) = resolve(tileId); - if (!tex.IsAssigned) return; + if (tex == 0) return; float w = Width, h = Height; if (visibleH > h) visibleH = h; float frac = h > 0f ? visibleH / h : 0f; @@ -243,17 +243,17 @@ internal sealed class UiMeter : UiElement, IUiDatStateful /// Draw a slice over local [, /// pieceX+], with the texture repeating every - /// px (UV-repeat — the UI texture is GL_REPEAT-wrapped). + /// px (UV-repeat — the UI texture is GL_REPEAT-wrapped). /// Clipped so nothing past shows. For a cap (span == native) /// this is one 1:1 copy; for the wide middle it tiles; a partial last copy is /// UV-cropped. private static void DrawPiece( - UiRenderContext ctx, GpuTextureSlot tex, float pieceX, float pieceW, float nativeW, float h, float clipW) + UiRenderContext ctx, uint tex, float pieceX, float pieceW, float nativeW, float h, float clipW) { - if (!tex.IsAssigned || pieceW <= 0f || nativeW <= 0f) return; + if (tex == 0 || pieceW <= 0f || nativeW <= 0f) return; float visibleW = MathF.Min(pieceW, clipW - pieceX); if (visibleW <= 0f) return; - float u1 = visibleW / nativeW; // >1 ⇒ texture repeats (tiles); ≤1 ⇒ a partial copy + float u1 = visibleW / nativeW; // >1 ⇒ texture repeats (tiles); ≤1 ⇒ a partial copy ctx.DrawSprite(tex, pieceX, 0f, visibleW, h, 0f, 0f, u1, 1f, Vector4.One); } } diff --git a/src/AcDream.App/UI/UiNineSlicePanel.cs b/src/AcDream.App/UI/UiNineSlicePanel.cs index 68a91e48..a87ce024 100644 --- a/src/AcDream.App/UI/UiNineSlicePanel.cs +++ b/src/AcDream.App/UI/UiNineSlicePanel.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; namespace AcDream.App.UI; @@ -10,17 +10,17 @@ namespace AcDream.App.UI; /// the widget is testable without GL. In production: /// id => { var t = cache.GetOrUploadRenderSurface(id, out var w, out var h); return (t, w, h); }. /// -internal class UiNineSlicePanel : UiPanel +public class UiNineSlicePanel : UiPanel { /// A placed chrome piece: destination rect in local pixel space. - internal readonly record struct Rect(float X, float Y, float W, float H); + public readonly record struct Rect(float X, float Y, float W, float H); /// The nine destination rects for an 8-piece border + center. - internal readonly record struct FrameRects( + public readonly record struct FrameRects( Rect Center, Rect Top, Rect Bottom, Rect Left, Rect Right, Rect TL, Rect TR, Rect BL, Rect BR); - private readonly System.Func _resolve; + private readonly System.Func _resolve; /// /// Whether this wrapper paints the shared center surface. Some retail child @@ -29,7 +29,7 @@ internal class UiNineSlicePanel : UiPanel /// public bool DrawCenterFill { get; set; } = true; - public UiNineSlicePanel(System.Func resolve) + public UiNineSlicePanel(System.Func resolve) { _resolve = resolve; BackgroundColor = Vector4.Zero; // suppress the base flat-rect fill @@ -46,7 +46,7 @@ internal class UiNineSlicePanel : UiPanel /// /// Destination rects (local px) for a frame of (, /// ) with border thickness : - /// b×b corners, top/bottom edges spanning the interior width at height b, + /// b×b corners, top/bottom edges spanning the interior width at height b, /// left/right edges spanning the interior height at width b, center fills /// the interior. /// @@ -68,7 +68,7 @@ internal class UiNineSlicePanel : UiPanel protected override void OnDraw(UiRenderContext ctx) { - // Center fill is the window BACKGROUND — it must sit UNDER the content, so it + // Center fill is the window BACKGROUND — it must sit UNDER the content, so it // draws here (before children). The bevel border + grip is the OUTERMOST layer // and draws in OnDrawAfterChildren (over the content's edges) so content can // never poke through the frame (e.g. the toolbar's 2px bottom-right cap overhang). @@ -91,7 +91,7 @@ internal class UiNineSlicePanel : UiPanel DrawStretched(ctx, RetailChromeSprites.CornerBR, r.BR); // Resize-grip overlay (gold ridged edges + square corner studs) on top of the - // bevel — the second border layer the vitals LayoutDesc carries (0x1000063B–0x10000642). + // bevel — the second border layer the vitals LayoutDesc carries (0x1000063B–0x10000642). DrawTiled(ctx, RetailChromeSprites.GripTop, r.Top); DrawTiled(ctx, RetailChromeSprites.GripBottom, r.Bottom); DrawTiled(ctx, RetailChromeSprites.GripLeft, r.Left); @@ -106,7 +106,7 @@ internal class UiNineSlicePanel : UiPanel { if (d.W <= 0 || d.H <= 0) return; var (tex, tw, th) = _resolve(id); - if (!tex.IsAssigned || tw == 0 || th == 0) return; + if (tex == 0 || tw == 0 || th == 0) return; ctx.DrawSprite(tex, d.X, d.Y, d.W, d.H, 0, 0, d.W / tw, d.H / th, Vector4.One); } @@ -114,7 +114,7 @@ internal class UiNineSlicePanel : UiPanel { if (d.W <= 0 || d.H <= 0) return; var (tex, _, _) = _resolve(id); - if (!tex.IsAssigned) return; + if (tex == 0) return; ctx.DrawSprite(tex, d.X, d.Y, d.W, d.H, 0, 0, 1, 1, Vector4.One); } } diff --git a/src/AcDream.App/UI/UiPanel.cs b/src/AcDream.App/UI/UiPanel.cs index 159ea2e4..71bf0c8b 100644 --- a/src/AcDream.App/UI/UiPanel.cs +++ b/src/AcDream.App/UI/UiPanel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; namespace AcDream.App.UI; @@ -14,7 +14,7 @@ namespace AcDream.App.UI; /// AcFont/UiSpriteBatch consumes those directly, we draw a /// simple translucent rectangle so panels are visible during development. /// -internal class UiPanel : UiElement +public class UiPanel : UiElement { /// Background fill color. Set to skip. public Vector4 BackgroundColor { get; set; } = new(0f, 0f, 0f, 0.55f); @@ -32,14 +32,14 @@ internal class UiPanel : UiElement /// Resolves a dat RenderSurface id to (GL tex handle, pixel width, pixel height). /// Required when is non-zero. - public Func? SpriteResolve { get; set; } + public Func? SpriteResolve { get; set; } protected override void OnDraw(UiRenderContext ctx) { if (BackgroundSprite != 0 && SpriteResolve is { } sr) { var (tex, tw, th) = sr(BackgroundSprite); - if (tex.IsAssigned && tw != 0 && th != 0) + if (tex != 0 && tw != 0 && th != 0) ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Vector4.One); } else if (BackgroundColor.W > 0f) @@ -64,7 +64,7 @@ internal class UiPanel : UiElement /// FUN_0040b8f0 then drawn by the widget's draw method through /// FUN_00698330. /// -internal class UiLabel : UiElement +public class UiLabel : UiElement { public string Text { get; set; } = string.Empty; public Vector4 TextColor { get; set; } = new(1f, 1f, 1f, 1f); @@ -81,10 +81,10 @@ internal class UiLabel : UiElement /// a StateDesc per UIStateId (normal / hot / pressed / /// disabled) from the panel layout. /// Note: the dat-widget button (Type 1 / UIElement_Button) is -/// in UiButton.cs — that is the production widget used by D.2b panels. +/// in UiButton.cs — that is the production widget used by D.2b panels. /// This class is the earlier dev-scaffold button (plain rect + text; no dat sprites). /// -internal class UiSimpleButton : UiPanel +public class UiSimpleButton : UiPanel { public string Text { get; set; } = string.Empty; public Vector4 TextColor { get; set; } = new(1f, 1f, 1f, 1f); @@ -120,7 +120,7 @@ internal class UiSimpleButton : UiPanel /// /// A that fires an callback when the user -/// left-clicks it. Used for the attribute-list rows in the Character window — each row +/// left-clicks it. Used for the attribute-list rows in the Character window — each row /// is a transparent container that needs to respond to pointer hits while its children /// (icon, name, value) are ClickThrough decorations. /// @@ -136,7 +136,7 @@ internal class UiSimpleButton : UiPanel /// with NO left/right end-caps. Bar height is pixels /// (default 3px). /// -internal class UiClickablePanel : UiPanel +public class UiClickablePanel : UiPanel { /// Called when the user releases the left mouse button over this panel. public Action? OnClick { get; set; } @@ -144,7 +144,7 @@ internal class UiClickablePanel : UiPanel /// When true and is non-zero, draws /// the sprite as a thin horizontal bar at the top AND bottom edges of the panel, /// NOT as a full-height stretched fill. Matches retail's selected-row highlight - /// (sprite 0x06001397 — 300×32 px — shown as bars, not a block fill). + /// (sprite 0x06001397 — 300×32 px — shown as bars, not a block fill). /// Default false (preserves legacy full-stretch behavior). public bool UseSelectionBars { get; set; } @@ -154,7 +154,7 @@ internal class UiClickablePanel : UiPanel public UiClickablePanel() { - // Rows must receive pointer events — override the UiPanel default (ClickThrough=false, + // Rows must receive pointer events — override the UiPanel default (ClickThrough=false, // which is the UiElement base default). Explicit for clarity. ClickThrough = false; } @@ -180,18 +180,18 @@ internal class UiClickablePanel : UiPanel if (UseSelectionBars && BackgroundSprite != 0 && SpriteResolve is { } sr) { // Draw the selection highlight as a thin bar at the TOP and BOTTOM of the row. - // The sprite (0x06001397) is 300×32 px — we draw it as horizontal strips at + // The sprite (0x06001397) is 300×32 px — we draw it as horizontal strips at // native height (SelectionBarHeight), stretched to full panel width (UV tile // horizontally). No left/right end-caps: u0=0, u1=Width/nativeW (UV repeat). var (tex, tw, th) = sr(BackgroundSprite); - if (tex.IsAssigned && tw > 0 && th > 0) + if (tex != 0 && tw > 0 && th > 0) { float barH = SelectionBarHeight; float uTile = tw > 0 ? Width / tw : 1f; - // Top bar: shows the top barH px of the sprite (v = 0 → barH/th). + // Top bar: shows the top barH px of the sprite (v = 0 → barH/th). float vBot = th > 0 ? barH / th : 1f; ctx.DrawSprite(tex, 0f, 0f, Width, barH, 0f, 0f, uTile, vBot, Vector4.One); - // Bottom bar: shows the bottom barH px of the sprite (v = 1−barH/th → 1). + // Bottom bar: shows the bottom barH px of the sprite (v = 1−barH/th → 1). float vTop2 = th > 0 ? 1f - barH / th : 0f; ctx.DrawSprite(tex, 0f, Height - barH, Width, barH, 0f, vTop2, uTile, 1f, Vector4.One); } diff --git a/src/AcDream.App/UI/UiRadar.cs b/src/AcDream.App/UI/UiRadar.cs index 8d6d72bc..77d718fa 100644 --- a/src/AcDream.App/UI/UiRadar.cs +++ b/src/AcDream.App/UI/UiRadar.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using AcDream.Core.Ui; @@ -10,7 +10,7 @@ namespace AcDream.App.UI; /// are local pixels in the 120x120 radar disc. World-to-player /// projection stays in the Core retail port; this record is the backend-facing draw seam. /// -internal readonly record struct UiRadarBlip( +public readonly record struct UiRadarBlip( uint ObjectId, string Name, float PixelX, @@ -25,7 +25,7 @@ internal readonly record struct UiRadarBlip( /// CoordinatesOnRadar is disabled). mirrors /// ClientUISystem::m_bRadarBlank: static chrome and the player marker remain visible. /// -internal sealed record UiRadarSnapshot( +public sealed record UiRadarSnapshot( float PlayerHeadingDegrees, IReadOnlyList Blips, string? CoordinatesText, @@ -50,7 +50,7 @@ internal sealed record UiRadarSnapshot( /// gmRadarUI::UseTime 0x004D98A0 (25 ms refresh cadence). /// /// -internal sealed class UiRadar : UiElement +public sealed class UiRadar : UiElement { public const uint RetailClassId = 0x10000010u; public const float RetailRefreshSeconds = RetailRadar.UpdateIntervalSeconds; diff --git a/src/AcDream.App/UI/UiRenderContext.cs b/src/AcDream.App/UI/UiRenderContext.cs index 1ba6eb63..9c191d40 100644 --- a/src/AcDream.App/UI/UiRenderContext.cs +++ b/src/AcDream.App/UI/UiRenderContext.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.App.Rendering; namespace AcDream.App.UI; @@ -33,19 +33,19 @@ internal readonly record struct UiClipRect(float Left, float Top, float Right, f /// when iterating the UI tree. Our version is explicit so it plugs /// cleanly into Silk.NET. /// -internal sealed class UiRenderContext +public sealed class UiRenderContext { public TextRenderer TextRenderer { get; } public BitmapFont? DefaultFont { get; set; } public Vector2 ScreenSize { get; } - // Transform stack — simple 2D translate (no rotation/scale for UI). + // Transform stack — simple 2D translate (no rotation/scale for UI). private readonly System.Collections.Generic.List _stack = new(); private Vector2 _current; private readonly System.Collections.Generic.List _clipStack = new(); private UiClipRect? _clip; - // Alpha (opacity) stack — a window pushes its Opacity so its background/sprite + // Alpha (opacity) stack — a window pushes its Opacity so its background/sprite // draws fade (retail's translucent-chat effect). Text draws bypass this (they go // straight to TextRenderer), so text stays sharp over a translucent background. private readonly System.Collections.Generic.List _alphaStack = new(); @@ -57,7 +57,7 @@ internal sealed class UiRenderContext /// Multiply into the running opacity. Pair with . public void PushAlpha(float a) { _alphaStack.Add(_alpha); _alpha *= a; } - /// Push an ABSOLUTE opacity (replaces, not multiplies) — for popups/overlays + /// Push an ABSOLUTE opacity (replaces, not multiplies) — for popups/overlays /// that must stay opaque even inside a translucent window. Pair with . public void PushAlphaAbsolute(float a) { _alphaStack.Add(_alpha); _alpha = a; } @@ -117,7 +117,7 @@ internal sealed class UiRenderContext public void BeginOverlayLayer() => TextRenderer.OverlayMode = true; public void EndOverlayLayer() => TextRenderer.OverlayMode = false; - // ── Pass-through draw helpers (add current translate) ────────────── + // ── Pass-through draw helpers (add current translate) ────────────── public void DrawRect(float x, float y, float w, float h, Vector4 color) { @@ -129,7 +129,7 @@ internal sealed class UiRenderContext /// Solid-colour fill drawn in the SPRITE bucket (painter order with text), for /// a panel BACKGROUND that text draws on top of. composites after - /// all sprites and would cover the text — use this for backgrounds, that for foreground + /// all sprites and would cover the text — use this for backgrounds, that for foreground /// fills (carets, vital bars). public void DrawFill(float x, float y, float w, float h, Vector4 color) { @@ -149,7 +149,7 @@ internal sealed class UiRenderContext DrawRect(x + w - t, y + t, t, h - 2f * t, color); } - public void DrawSprite(GpuTextureSlot texture, float x, float y, float w, float h, + public void DrawSprite(uint texture, float x, float y, float w, float h, float u0, float v0, float u1, float v1, Vector4 tint) { x += _current.X; @@ -158,7 +158,7 @@ internal sealed class UiRenderContext } private void DrawSpriteAbsolute( - GpuTextureSlot texture, float x, float y, float w, float h, + uint texture, float x, float y, float w, float h, float u0, float v0, float u1, float v1, Vector4 tint, bool applyAlpha) { if (_clip is { } clip @@ -213,7 +213,7 @@ internal sealed class UiRenderContext /// /// gates the black outline pass. Retail decides /// this PER text element: UIElement_Text::DrawSelf (acclient 0x00467aa0) - /// runs the outline pass only when m_bitField & 0x10 is set — i.e. the + /// runs the outline pass only when m_bitField & 0x10 is set — i.e. the /// element called SetOutline(true) (LayoutDesc property 0xd). The DEFAULT /// is OFF (one fill-only pass): the talk-focus menu items set no outline, so an /// always-on outline shows as a grey halo over the solid menu panel. Pass @@ -232,10 +232,10 @@ internal sealed class UiRenderContext // Snap the LINE baseline to a whole pixel ONCE. Retail's // SurfaceWindow::DrawCharacter (acclient 0x00442bd0) takes an int32 pen Y - // (arg3) and adds the glyph's integer m_VerticalOffsetBefore (a schar) — every + // (arg3) and adds the glyph's integer m_VerticalOffsetBefore (a schar) — every // glyph on a line shares one integer baseline. If we instead round EACH glyph's // Y independently and the caller passes a fractional line Y (e.g. a channel-menu - // item centered in a 17px row over a 16px font → y = 0.5), adjacent letters round + // item centered in a 17px row over a 16px font → y = 0.5), adjacent letters round // to different rows and the line looks crooked ("letters dip down"). The vitals // digits never showed it because their bar baseline lands on an integer; chat text // does. Snapping the baseline once, then adding the integer offset, keeps the whole @@ -251,7 +251,7 @@ internal sealed class UiRenderContext // Horizontal: snap each glyph's dest X to a whole pixel (the pen keeps its // true fractional advance). Vertical: integer baseline + integer per-glyph - // offset — never an independent per-glyph round (see baseY note above). + // offset — never an independent per-glyph round (see baseY note above). float gx = System.MathF.Round(pen + g.HorizontalOffsetBefore); float gy = baseY + g.VerticalOffsetBefore; float gw = g.Width; @@ -259,10 +259,10 @@ internal sealed class UiRenderContext if (gw > 0f && gh > 0f) { - // Background (outline) atlas pass, tinted black — drawn behind. Gated by + // Background (outline) atlas pass, tinted black — drawn behind. Gated by // `outline` (retail's per-element m_bitField & 0x10); off by default so UI // text is crisp fill-only and free of the grey halo over solid panels. - if (outline && font.BackgroundTexture.IsAssigned) + if (outline && font.BackgroundTexture != 0) { var (bu0, bv0, bu1, bv1) = AtlasUv( g.OffsetX, g.OffsetY, g.Width, g.Height, diff --git a/src/AcDream.App/UI/UiRoot.cs b/src/AcDream.App/UI/UiRoot.cs index 25db83c9..24c88b65 100644 --- a/src/AcDream.App/UI/UiRoot.cs +++ b/src/AcDream.App/UI/UiRoot.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; namespace AcDream.App.UI; @@ -22,7 +22,7 @@ public enum ResizeEdges { None = 0, Left = 1, Right = 2, Top = 4, Bottom = 8 } /// or event fires so the game world /// (camera, player controller) still receives input. /// -internal sealed class UiRoot : UiElement +public sealed class UiRoot : UiElement { public UiRoot() { @@ -32,7 +32,7 @@ internal sealed class UiRoot : UiElement /// Single owner for named retained-window lifecycle and raise policy. public RetailWindowManager WindowManager { get; } - // ── Device-level state ─────────────────────────────────────────────── + // ── Device-level state ─────────────────────────────────────────────── public int MouseX { get; private set; } public int MouseY { get; private set; } public bool LeftButtonDown { get; private set; } @@ -42,7 +42,7 @@ internal sealed class UiRoot : UiElement /// Widget currently receiving keyboard events. public UiElement? KeyboardFocus { get; private set; } - /// The edit control activated by Tab/Enter when nothing is focused — retail's + /// The edit control activated by Tab/Enter when nothing is focused — retail's /// chat input "write mode" toggle. Set by the host once the chat window is built. public UiElement? DefaultTextInput { get; set; } @@ -59,7 +59,7 @@ internal sealed class UiRoot : UiElement /// True when the pointer is over a widget OR a widget holds mouse capture. /// The host ORs this into the InputDispatcher's WantCaptureMouse gate so game /// actions (movement, world-pick) are suppressed while the user interacts with - /// a retail window — mirrors ImGui's WantCaptureMouse. + /// a retail window — mirrors ImGui's WantCaptureMouse. /// public bool WantsMouse => Captured is not null || HitTestTopDown(MouseX, MouseY).element is not null; @@ -111,9 +111,9 @@ internal sealed class UiRoot : UiElement && !target.HandlesClick; } } - private (GpuTextureSlot tex, int w, int h)? _dragGhost; + private (uint tex, int w, int h)? _dragGhost; /// Snapshotted drag-ghost (tex,w,h), exposed for tests. See BeginDrag. - internal (GpuTextureSlot tex, int w, int h)? DragGhostForTest => _dragGhost; + internal (uint tex, int w, int h)? DragGhostForTest => _dragGhost; private UiElement? _lastDragHoverTarget; private int _pressX, _pressY; private bool _dragCandidate; @@ -272,7 +272,7 @@ internal sealed class UiRoot : UiElement return false; } - // ── Per-frame pumping ──────────────────────────────────────────────── + // ── Per-frame pumping ──────────────────────────────────────────────── public void Tick(double dt, long nowMs) { @@ -303,7 +303,7 @@ internal sealed class UiRoot : UiElement public void Draw(UiRenderContext ctx) { - // Render children (panels) sorted by z-order — modal last so it + // Render children (panels) sorted by z-order — modal last so it // sits on top. DrawSelfAndChildren(ctx); // Second pass: open popups/menus draw ON TOP of the whole tree (so e.g. the @@ -324,12 +324,12 @@ internal sealed class UiRoot : UiElement /// never intercepts hit-tests. private void DrawDragGhost(UiRenderContext ctx) { - if (_dragGhost is not { } g || !g.tex.IsAssigned) return; + if (_dragGhost is not { } g || g.tex == 0) return; ctx.DrawSprite(g.tex, MouseX - g.w / 2f, MouseY - g.h / 2f, g.w, g.h, 0f, 0f, 1f, 1f, new Vector4(1f, 1f, 1f, GhostAlpha)); } - // ── Input entry points (called from GameWindow's Silk.NET handlers) ── + // ── Input entry points (called from GameWindow's Silk.NET handlers) ── public void OnMouseMove(int x, int y) { @@ -426,7 +426,7 @@ internal sealed class UiRoot : UiElement if (target is null) { // Clicking the 3D world exits write mode (no submit) and returns control to - // the character — retail blurs the chat input on an outside click. + // the character — retail blurs the chat input on an outside click. if (btn == UiMouseButton.Left) SetKeyboardFocus(null); WorldMouseFallThrough?.Invoke(btn, x, y, flags); return; @@ -473,7 +473,7 @@ internal sealed class UiRoot : UiElement } else if (target.CapturesPointerDrag || target.HandlesClick) { - // The pressed widget owns its pointer interaction — either an interior drag (e.g. text + // The pressed widget owns its pointer interaction — either an interior drag (e.g. text // selection, CapturesPointerDrag) or a click it must receive (e.g. a UiButton, // HandlesClick). Either way do NOT move the ancestor window. The already-dispatched // MouseDown + SetCapture(target) let the target handle it; on release OnMouseUp emits @@ -513,7 +513,7 @@ internal sealed class UiRoot : UiElement // Deliver TARGET-LOCAL coords (consistent with MouseMove/MouseUp, which use // target.ScreenPosition). HitTestTopDown's lx/ly are relative to the TOP-LEVEL // child, so for a nested target (e.g. the chat view inset inside its window) - // they'd be offset by the child's position — which mis-anchored drag-select. + // they'd be offset by the child's position — which mis-anchored drag-select. var sp = target.ScreenPosition; var e = new UiEvent(target.EventId, target, rawType, Data0: (int)flags, Data1: (int)(x - sp.X), Data2: (int)(y - sp.Y)); @@ -615,7 +615,7 @@ internal sealed class UiRoot : UiElement return; } - // No capture — give the world a chance. + // No capture — give the world a chance. WorldMouseFallThrough?.Invoke(btn, x, y, flags); } @@ -687,7 +687,7 @@ internal sealed class UiRoot : UiElement BubbleEvent(KeyboardFocus, in e); } - // ── Focus + capture ───────────────────────────────────────────────── + // ── Focus + capture ───────────────────────────────────────────────── public void SetKeyboardFocus(UiElement? e) { @@ -726,12 +726,12 @@ internal sealed class UiRoot : UiElement PointerCaptureChanged?.Invoke(previous, null); } - // ── Window manager (named top-level windows: Show / Hide / Toggle) ─── + // ── Window manager (named top-level windows: Show / Hide / Toggle) ─── // Registry state lives in RetailWindowManager; methods below are compatibility forwarders. /// Register a top-level window under a name for Show/Hide/Toggle. - /// Does NOT add it to the tree — the caller mounts via AddChild and controls + /// Does NOT add it to the tree — the caller mounts via AddChild and controls /// initial Visible. Idempotent registration returns the existing typed handle; /// replacement performs full lifecycle teardown of the prior registration. public RetailWindowHandle RegisterWindow( @@ -788,7 +788,7 @@ internal sealed class UiRoot : UiElement WindowResized?.Invoke(handle.Name, window); } - // ── Drag-drop (retail event chain 0x15 → 0x21 → 0x1C → 0x3E) ──────── + // ── Drag-drop (retail event chain 0x15 → 0x21 → 0x1C → 0x3E) ──────── private void BeginDrag(UiElement source) { @@ -796,7 +796,7 @@ internal sealed class UiRoot : UiElement if (payload is null) { _dragCandidate = false; return; } DragSource = source; DragPayload = payload; - _dragGhost = source.GetDragGhost(); // snapshot NOW — the DragBegin handler may empty the source cell + _dragGhost = source.GetDragGhost(); // snapshot NOW — the DragBegin handler may empty the source cell var e = new UiEvent(source.EventId, source, UiEventType.DragBegin, Payload: payload); source.OnEvent(in e); // Retail UIElement_ItemList::ItemList_BeginDrag @ 0x004E32D0 selects an @@ -843,7 +843,7 @@ internal sealed class UiRoot : UiElement var (t, lx, ly) = HitTestTopDown(x, y); if (t is not null) { - // Dropped on a real element — deliver DropReleased; the hit cell's handler places. + // Dropped on a real element — deliver DropReleased; the hit cell's handler places. // A non-item target's OnEvent ignores it, so an off-bar drop leaves the lift's removal. var e = new UiEvent(source!.EventId, t, UiEventType.DropReleased, Data1: (int)lx, Data2: (int)ly, Payload: payload); @@ -859,7 +859,7 @@ internal sealed class UiRoot : UiElement _lastDragHoverTarget = null; } - // ── Hover / tooltip ───────────────────────────────────────────────── + // ── Hover / tooltip ───────────────────────────────────────────────── private void UpdateHover(int x, int y) { @@ -892,7 +892,7 @@ internal sealed class UiRoot : UiElement } } - // ── Helpers ───────────────────────────────────────────────────────── + // ── Helpers ───────────────────────────────────────────────────────── public void FireEvent(int type, UiElement target, object? payload = null) { diff --git a/src/AcDream.App/UI/UiScrollable.cs b/src/AcDream.App/UI/UiScrollable.cs index 779264b0..cc12eaee 100644 --- a/src/AcDream.App/UI/UiScrollable.cs +++ b/src/AcDream.App/UI/UiScrollable.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace AcDream.App.UI; @@ -11,7 +11,7 @@ namespace AcDream.App.UI; /// Decomp anchors: SetScrollableXY @0x4740c0, UpdateScrollbarSize_ @0x4741a0, /// UpdateScrollbarPosition_ @0x473f20, UIElement_Text::InqScrollDelta @0x4689b0. /// -internal sealed class UiScrollable +public sealed class UiScrollable { /// Total wrapped content height in px (m_iScrollableHeight). public int ContentHeight { get; set; } @@ -30,7 +30,7 @@ internal sealed class UiScrollable /// True when content exceeds the view (a scrollbar is warranted). public bool HasOverflow => ContentHeight > ViewHeight; - /// True when the offset is at (or past) the bottom — used for bottom-pin. + /// True when the offset is at (or past) the bottom — used for bottom-pin. public bool AtEnd => _scrollY >= MaxScroll; /// @@ -64,7 +64,7 @@ internal sealed class UiScrollable /// Position ratio = scroll/(content-view) in [0,1] (UpdateScrollbarPosition_). public float PositionRatio => MaxScroll <= 0 ? 0f : (float)_scrollY / MaxScroll; - /// Inverse of PositionRatio — used when the user drags the thumb. + /// Inverse of PositionRatio — used when the user drags the thumb. public void SetPositionRatio(float ratio) => SetScrollY((int)MathF.Round(Math.Clamp(ratio, 0f, 1f) * MaxScroll)); diff --git a/src/AcDream.App/UI/UiScrollablePanel.cs b/src/AcDream.App/UI/UiScrollablePanel.cs index 245b99fc..ea993c13 100644 --- a/src/AcDream.App/UI/UiScrollablePanel.cs +++ b/src/AcDream.App/UI/UiScrollablePanel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Numerics; @@ -10,7 +10,7 @@ namespace AcDream.App.UI; /// pixel scroll model as chat text and item grids, and clips whole rows because /// the UI renderer does not have a scissor stack yet. /// -internal sealed class UiScrollablePanel : UiPanel +public sealed class UiScrollablePanel : UiPanel { private readonly Dictionary _baseTops = new(ReferenceEqualityComparer.Instance); diff --git a/src/AcDream.App/UI/UiScrollbar.cs b/src/AcDream.App/UI/UiScrollbar.cs index 320e31f9..25c04acc 100644 --- a/src/AcDream.App/UI/UiScrollbar.cs +++ b/src/AcDream.App/UI/UiScrollbar.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; namespace AcDream.App.UI; @@ -6,20 +6,20 @@ namespace AcDream.App.UI; /// /// Generic scrollbar. Ports retail UIElement_Scrollbar /// (RegisterElementClass(0xb) @ acclient_2013_pseudo_c.txt:124137); -/// thumb size = trackLen * ThumbRatio (min 8px); step ±1 line. +/// thumb size = trackLen * ThumbRatio (min 8px); step ±1 line. /// /// /// Dat element ids (chat LayoutDesc 0x21000006): track 0x10000012 (X=474 Y=6 W=16 H=68), /// thumb 0x1000048C. The track is instanced from base layout 0x2100003E which contains /// the full scrollbar widget with distinct up/down button children: -/// Up button element 0x10000071 — Y=0, 16×16, Normal sprite 0x06004C69. -/// Down button element 0x10000072 — Y=32, 16×16, Normal sprite 0x06004C6C. +/// Up button element 0x10000071 — Y=0, 16×16, Normal sprite 0x06004C69. +/// Down button element 0x10000072 — Y=32, 16×16, Normal sprite 0x06004C6C. /// Track body sprite: 0x06004C5F (48px tall in the base template; stretched to H=68 in chat). /// Thumb is a 3-slice: top cap 0x06004C60, middle 0x06004C63, bottom cap 0x06004C66. /// The widget reproduces referenced button children procedurally and uses their /// authored extent for drawing and hit comparison (16px in this base layout). /// -internal sealed class UiScrollbar : UiElement +public sealed class UiScrollbar : UiElement { public override bool ReceivesHoverMouseMove => true; @@ -64,8 +64,8 @@ internal sealed class UiScrollbar : UiElement public void SetScalarPosition(float position) => ScalarPosition = Math.Clamp(position, 0f, 1f); - /// RenderSurface id → (GL tex, w, h). 0 id = skip. - public Func? SpriteResolve { get; set; } + /// RenderSurface id → (GL tex, w, h). 0 id = skip. + public Func? SpriteResolve { get; set; } /// Track background sprite id (0x06004C5F from layout 0x2100003E element 0x10000455). public uint TrackSprite { get; set; } @@ -212,8 +212,8 @@ internal sealed class UiScrollbar : UiElement if (Model is not { } m) return; - // Track background — TILED vertically (retail DrawMode=Normal). The native track - // sprite (~16×32) repeats to fill the element height instead of stretch-distorting. + // Track background — TILED vertically (retail DrawMode=Normal). The native track + // sprite (~16×32) repeats to fill the element height instead of stretch-distorting. DrawTiled(ctx, resolve, TrackSprite, 0f, 0f, Width, Height); float decrementExtent = AxisExtent(DecrementButtonExtent, Height); @@ -224,7 +224,7 @@ internal sealed class UiScrollbar : UiElement DrawSprite(ctx, resolve, ActiveEndSprite, 0f, Height - incrementExtent, Width, incrementExtent); - // Thumb — only when content overflows the view. Retail 3-slice: top cap + + // Thumb — only when content overflows the view. Retail 3-slice: top cap + // tiled middle + bottom cap (base layout 0x2100003E thumb sub-elements // 0x10000364/65/66). Falls back to a single tiled middle if the caps are unset // or the thumb is too short to hold both caps. @@ -248,7 +248,7 @@ internal sealed class UiScrollbar : UiElement private void DrawHorizontalModel( UiRenderContext ctx, - Func resolve, + Func resolve, UiScrollable model) { float decrementExtent = AxisExtent(DecrementButtonExtent, Width); @@ -275,23 +275,23 @@ internal sealed class UiScrollbar : UiElement } /// Draw a sprite stretched 1:1 to the dest rect. - private void DrawSprite(UiRenderContext ctx, Func resolve, + private void DrawSprite(UiRenderContext ctx, Func resolve, uint id, float x, float y, float w, float h) { if (id == 0 || w <= 0f || h <= 0f) return; var (tex, _, _) = resolve(id); - if (!tex.IsAssigned) return; + if (tex == 0) return; ctx.DrawSprite(tex, x, y, w, h, 0f, 0f, 1f, 1f, Vector4.One); } /// Draw a sprite TILED to fill the dest rect (UV-repeat at native size on - /// both axes — the UI texture is GL_REPEAT-wrapped). A native-width axis gives 1:1. - private void DrawTiled(UiRenderContext ctx, Func resolve, + /// both axes — the UI texture is GL_REPEAT-wrapped). A native-width axis gives 1:1. + private void DrawTiled(UiRenderContext ctx, Func resolve, uint id, float x, float y, float w, float h) { if (id == 0 || w <= 0f || h <= 0f) return; var (tex, tw, th) = resolve(id); - if (!tex.IsAssigned || tw == 0 || th == 0) return; + if (tex == 0 || tw == 0 || th == 0) return; ctx.DrawSprite(tex, x, y, w, h, 0f, 0f, w / tw, h / th, Vector4.One); } @@ -301,7 +301,7 @@ internal sealed class UiScrollbar : UiElement /// private void DrawTiledClipped( UiRenderContext ctx, - Func resolve, + Func resolve, uint id, float rangeLeft, float x, @@ -310,7 +310,7 @@ internal sealed class UiScrollbar : UiElement { if (id == 0 || w <= 0f || h <= 0f) return; var (tex, tw, th) = resolve(id); - if (!tex.IsAssigned || tw == 0 || th == 0) return; + if (tex == 0 || tw == 0 || th == 0) return; float u0 = (x - rangeLeft) / tw; float u1 = u0 + w / tw; ctx.DrawSprite(tex, x, 0f, w, h, u0, 0f, u1, h / th, Vector4.One); @@ -390,13 +390,13 @@ internal sealed class UiScrollbar : UiElement if (ly >= ty && ly <= ty + th) { - // Clicked inside the thumb — begin drag with offset from thumb top. + // Clicked inside the thumb — begin drag with offset from thumb top. _draggingThumb = true; _dragOffsetY = ly - ty; } else { - // Clicked above or below thumb — page scroll (HandleButtonClick page case). + // Clicked above or below thumb — page scroll (HandleButtonClick page case). m.ScrollByPage(ly < ty ? -1 : 1); } return true; @@ -520,7 +520,7 @@ internal sealed class UiScrollbar : UiElement return false; } - private float ScalarThumbWidth(Func? resolve) + private float ScalarThumbWidth(Func? resolve) { if (resolve is not null && ThumbSprite != 0) { diff --git a/src/AcDream.App/UI/UiShortcutDigitGraphics.cs b/src/AcDream.App/UI/UiShortcutDigitGraphics.cs index 5cf3102f..0e8aafbf 100644 --- a/src/AcDream.App/UI/UiShortcutDigitGraphics.cs +++ b/src/AcDream.App/UI/UiShortcutDigitGraphics.cs @@ -1,4 +1,4 @@ -namespace AcDream.App.UI; +namespace AcDream.App.UI; /// /// Shared DAT digit overlays used by UIItem shortcut cells. Both gmToolbarUI and @@ -6,7 +6,7 @@ /// share these number arrays. The underlying ItemSlot_Empty surface remains a /// per-ItemList asset selected by that list's cell prototype. /// -internal sealed record UiShortcutDigitGraphics( +public sealed record UiShortcutDigitGraphics( uint[]? RegularDigits, uint[]? GhostedDigits, uint[]? EmptyDigits); diff --git a/src/AcDream.App/UI/UiText.cs b/src/AcDream.App/UI/UiText.cs index 54ba161a..73d57334 100644 --- a/src/AcDream.App/UI/UiText.cs +++ b/src/AcDream.App/UI/UiText.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using System.Text; @@ -20,7 +20,7 @@ namespace AcDream.App.UI; /// Display-only text remains click-through and cannot steal focus or window drag. /// /// -internal sealed class UiText : UiElement, IUiDatStateful +public sealed class UiText : UiElement, IUiDatStateful { /// Optional base-element click notice used by authored text tabs. public Action? OnClick { get; set; } @@ -30,11 +30,11 @@ internal sealed class UiText : UiElement, IUiDatStateful public uint ElementId { get; set; } /// One display line: pre-formatted text + its colour. - internal readonly record struct Line(string Text, Vector4 Color); + public readonly record struct Line(string Text, Vector4 Color); /// A caret position: a line index into the cached line list plus a /// character index (0..line.Text.Length, i.e. a caret slot between glyphs). - internal readonly record struct Pos(int Line, int Col); + public readonly record struct Pos(int Line, int Col); /// Provider of the lines to show, oldest-first. Polled each frame. public Func> LinesProvider { get; set; } = static () => Array.Empty(); @@ -58,7 +58,7 @@ internal sealed class UiText : UiElement, IUiDatStateful /// otherwise white (). /// /// Controllers that supply a per-line color via - /// (e.g. new UiText.Line(text, explicitColor)) are unaffected — they always + /// (e.g. new UiText.Line(text, explicitColor)) are unaffected — they always /// win over this default. This property is only a convenience starting point for /// controllers that want to read the dat color rather than hard-code it. /// @@ -84,7 +84,7 @@ internal sealed class UiText : UiElement, IUiDatStateful /// Resolves a dat RenderSurface id to (GL tex handle, pixel width, pixel height). /// Required when is non-zero. - public Func? SpriteResolve { get; set; } + public Func? SpriteResolve { get; set; } /// Highlight colour painted behind a selected character span. public Vector4 SelectionColor { get; set; } = new(0.25f, 0.45f, 0.85f, 0.5f); @@ -138,7 +138,7 @@ internal sealed class UiText : UiElement, IUiDatStateful /// Static right-aligned single-line mode: draws the FIRST line right-justified /// within the element rect, vertically centered, with NO scroll/selection machinery. /// Used for value labels in attribute/skill rows where the number must hug the right edge. - /// Mutually exclusive with — if both are true, Centered takes + /// Mutually exclusive with — if both are true, Centered takes /// precedence. Pair with ClickThrough = true for non-interactive labels. public bool RightAligned { get; set; } @@ -146,20 +146,20 @@ internal sealed class UiText : UiElement, IUiDatStateful /// Vertical position of the text within the element rect in single-line mode /// ( or ). /// - /// Center (default) — vertically centered, matching the original + /// Center (default) — vertically centered, matching the original /// behavior of the centered/right-aligned paths. - /// Top — text is placed at y = Padding (top of the content + /// Top — text is placed at y = Padding (top of the content /// area), so the text sits at the top of the element rather than centering in it. /// Used for footer title elements whose dat box is the full footer height (55 px) but /// the text should render near the top. - /// Bottom — text is placed at y = Height - lineHeight - Padding. + /// Bottom — text is placed at y = Height - lineHeight - Padding. /// /// Only meaningful when or is true. /// Has no effect on the scrollable multi-line path. /// public VJustify VerticalJustify { get; set; } = VJustify.Center; - /// The scroll model — also read by the linked UiScrollbar. + /// The scroll model — also read by the linked UiScrollbar. public UiScrollable Scroll { get; } = new(); /// @@ -179,7 +179,7 @@ internal sealed class UiText : UiElement, IUiDatStateful private const float WheelLines = 1f; // lines advanced per wheel notch (retail = 1 line per notch) - // ── Cached layout from the last OnDraw, so OnEvent hit-tests the SAME geometry ── + // ── Cached layout from the last OnDraw, so OnEvent hit-tests the SAME geometry ── private IReadOnlyList _lastLines = Array.Empty(); private BitmapFont? _lastFont; private UiDatFont? _lastDatFont; @@ -193,7 +193,7 @@ internal sealed class UiText : UiElement, IUiDatStateful private bool _drawTextAfterChildren; private bool _honorDatVerticalJustification; - // ── Selection state ────────────────────────────────────────────────── + // ── Selection state ────────────────────────────────────────────────── private Pos? _selAnchor; // where the drag started private Pos? _selCaret; // where the drag currently is private bool _selecting; @@ -325,7 +325,7 @@ internal sealed class UiText : UiElement, IUiDatStateful /// /// Clamp a scroll offset to [0, max] where max = content-height - view-height - /// (never negative — when everything fits, scroll is pinned to 0). Exposed for tests. + /// (never negative — when everything fits, scroll is pinned to 0). Exposed for tests. /// public static float ClampScroll(float scroll, float contentHeight, float viewHeight) { @@ -340,14 +340,14 @@ internal sealed class UiText : UiElement, IUiDatStateful if (BackgroundSprite != 0 && SpriteResolve is { } sr) { var (tex, tw, th) = sr(BackgroundSprite); - if (tex.IsAssigned && tw != 0 && th != 0) + if (tex != 0 && tw != 0 && th != 0) ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Vector4.One); } // Background must draw UNDER the transcript text. DrawStringDat emits into the // sprite bucket which flushes BEFORE rects, so a DrawRect background would wash // over the text. DrawFill routes the background through the sprite bucket too, - // submitted first → text on top. + // submitted first → text on top. ctx.DrawFill(0, 0, Width, Height, BackgroundColor); if (!_drawTextAfterChildren) @@ -520,7 +520,7 @@ internal sealed class UiText : UiElement, IUiDatStateful hx = lineX + bitmapFont!.MeasureWidth(text.Substring(0, c0)); hw = bitmapFont.MeasureWidth(text.Substring(c0, c1 - c0)); } - // Highlight sits BEHIND the line's text → sprite bucket, submitted + // Highlight sits BEHIND the line's text → sprite bucket, submitted // before this line's DrawStringDat. ctx.DrawFill(hx, y, hw, lh, SelectionColor); } @@ -572,7 +572,7 @@ internal sealed class UiText : UiElement, IUiDatStateful if (!Selectable && !WheelScrollEnabled) return false; // Silk wheel +Y = scroll up = reveal older = toward the TOP = decrease ScrollY. // ScrollByLines sign: +down/newer, -up/older. - // e.Data0 > 0 → wheel up → want older → ScrollByLines with negative lines. + // e.Data0 > 0 → wheel up → want older → ScrollByLines with negative lines. Scroll.ScrollByLines((int)(-e.Data0 * WheelLines)); return true; } @@ -616,7 +616,7 @@ internal sealed class UiText : UiElement, IUiDatStateful || Keyboard.IsKeyPressed(Silk.NET.Input.Key.ControlRight)); if (ctrl && key == Silk.NET.Input.Key.C) { - // Only touch the clipboard when there's a selection — an empty + // Only touch the clipboard when there's a selection — an empty // copy must NOT clobber what the user previously copied. if (Keyboard is not null) { @@ -636,7 +636,7 @@ internal sealed class UiText : UiElement, IUiDatStateful return false; } - // ── Selection helpers ──────────────────────────────────────────────── + // ── Selection helpers ──────────────────────────────────────────────── /// Select the entire cached transcript (Ctrl+A). private void SelectAll() @@ -670,7 +670,7 @@ internal sealed class UiText : UiElement, IUiDatStateful return SelectedText(_lastLines, start, end); } - // ── Pure, testable logic (no GL / no font texture) ─────────────────── + // ── Pure, testable logic (no GL / no font texture) ─────────────────── /// /// Compute the Y offset (local space) for a single line in the Centered/RightAligned @@ -731,7 +731,7 @@ internal sealed class UiText : UiElement, IUiDatStateful /// (inclusive of start.Col, exclusive of end.Col) from /// . Multi-line selections are joined with "\n": /// the first line from start.Col to its end, whole middle lines, and the last - /// line up to end.Col. Pure — unit-testable without GL. + /// line up to end.Col. Pure — unit-testable without GL. /// public static string SelectedText(IReadOnlyList lines, Pos start, Pos end) { @@ -870,8 +870,8 @@ internal sealed class UiText : UiElement, IUiDatStateful /// The caret column for a horizontal position (already /// adjusted for the left padding, so x=0 is the start of the text). Walks the /// string accumulating each glyph's advance and snaps the caret to whichever - /// side of the glyph midpoint falls on — natural - /// Windows-like caret placement. Pure — unit-testable with a synthetic advance. + /// side of the glyph midpoint falls on — natural + /// Windows-like caret placement. Pure — unit-testable with a synthetic advance. /// /// The line text. /// Per-character advance (pixels) lookup. @@ -888,6 +888,6 @@ internal sealed class UiText : UiElement, IUiDatStateful if (x < mid) return i; // caret sits before this glyph cursor += adv; } - return text.Length; // past the last glyph → end caret + return text.Length; // past the last glyph → end caret } } diff --git a/src/AcDream.App/UI/UiTextureElement.cs b/src/AcDream.App/UI/UiTextureElement.cs index 19ac4921..e061ed87 100644 --- a/src/AcDream.App/UI/UiTextureElement.cs +++ b/src/AcDream.App/UI/UiTextureElement.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.App.Rendering; namespace AcDream.App.UI; @@ -8,16 +8,16 @@ namespace AcDream.App.UI; /// still owns the region's geometry and chrome; controllers supply only the live /// texture handle (for example a composed item icon). /// -internal sealed class UiTextureElement : UiElement +public sealed class UiTextureElement : UiElement { public UiTextureElement() => ClickThrough = true; - public GpuTextureSlot Texture { get; set; } = GpuTextureSlot.Unassigned; + public uint Texture { get; set; } public Vector4 Tint { get; set; } = Vector4.One; protected override void OnDraw(UiRenderContext ctx) { - if (Texture.IsAssigned) + if (Texture != 0u) ctx.DrawSprite(Texture, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Tint); } } diff --git a/src/AcDream.App/UI/UiViewport.cs b/src/AcDream.App/UI/UiViewport.cs index c7f77669..f5a50323 100644 --- a/src/AcDream.App/UI/UiViewport.cs +++ b/src/AcDream.App/UI/UiViewport.cs @@ -1,11 +1,11 @@ -using System.Numerics; +using System.Numerics; namespace AcDream.App.UI; /// Leaf widget for dat Type 0xD (UIElement_Viewport). Blits the texture produced by its /// IUiViewportRenderer (run in the pre-UI hook) as a single sprite at its own rect. The 3-D render /// does NOT happen here (OnDraw only has a 2-D context). -internal sealed class UiViewport : UiElement +public sealed class UiViewport : UiElement { public override bool ConsumesDatChildren => true; @@ -34,12 +34,12 @@ internal sealed class UiViewport : UiElement /// Renderer that produces the off-screen texture. Set by GameWindow wiring (later task). public IUiViewportRenderer? Renderer { get; set; } - /// Last texture-table slot produced by the pre-UI hook. Unassigned = nothing to blit. - public GpuTextureSlot TextureHandle { get; set; } = GpuTextureSlot.Unassigned; + /// Last GL color-texture handle produced by the pre-UI hook. 0 = nothing to blit. + public uint TextureHandle { get; set; } protected override void OnDraw(UiRenderContext ctx) { - if (!Visible || !TextureHandle.IsAssigned) return; + if (!Visible || TextureHandle == 0) return; // Local origin is already at this widget's Left/Top (PushTransform applied by DrawSelfAndChildren). // V is FLIPPED (v0=1, v1=0): TextureHandle is an off-screen FBO color texture, whose origin is // bottom-left (GL), while the UI sprite convention is top-left. Without the flip the doll renders diff --git a/src/AcDream.App/UI/WindowNames.cs b/src/AcDream.App/UI/WindowNames.cs index 7bfcf748..9be64cc8 100644 --- a/src/AcDream.App/UI/WindowNames.cs +++ b/src/AcDream.App/UI/WindowNames.cs @@ -1,8 +1,8 @@ -namespace AcDream.App.UI; +namespace AcDream.App.UI; /// Canonical registry names for top-level retail-UI windows, so the /// mount, the window registry, and the toggle keybind all agree on one literal. -internal static class WindowNames +public static class WindowNames { public const string Vitals = "vitals"; public const string Toolbar = "toolbar"; diff --git a/src/AcDream.App/UI/WorldDropDispatch.cs b/src/AcDream.App/UI/WorldDropDispatch.cs index f97477b7..598c18f6 100644 --- a/src/AcDream.App/UI/WorldDropDispatch.cs +++ b/src/AcDream.App/UI/WorldDropDispatch.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Items; +using AcDream.Core.Items; namespace AcDream.App.UI; @@ -6,6 +6,6 @@ namespace AcDream.App.UI; /// One successfully dispatched inventory-to-world request together with the /// quantity selected by the retained UI. /// -internal readonly record struct WorldDropDispatch( +public readonly record struct WorldDropDispatch( PendingInventoryRequest Request, uint Amount); diff --git a/src/AcDream.App/World/ExternalContainerLifecycleController.cs b/src/AcDream.App/World/ExternalContainerLifecycleController.cs index 3545fab9..fdb06675 100644 --- a/src/AcDream.App/World/ExternalContainerLifecycleController.cs +++ b/src/AcDream.App/World/ExternalContainerLifecycleController.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Items; +using AcDream.Core.Items; namespace AcDream.App.World; @@ -7,7 +7,7 @@ namespace AcDream.App.World; /// Presentation controllers observe the same state but do not own session /// lifecycle messages. /// -internal sealed class ExternalContainerLifecycleController : IDisposable +public sealed class ExternalContainerLifecycleController : IDisposable { private readonly ExternalContainerState _state; private readonly ClientObjectTable _objects; diff --git a/src/AcDream.App/World/ILiveEntityRadarSource.cs b/src/AcDream.App/World/ILiveEntityRadarSource.cs index 8aa9d84b..bb43a239 100644 --- a/src/AcDream.App/World/ILiveEntityRadarSource.cs +++ b/src/AcDream.App/World/ILiveEntityRadarSource.cs @@ -1,4 +1,4 @@ -using AcDream.Core.World; +using AcDream.Core.World; namespace AcDream.App.World; @@ -7,7 +7,7 @@ namespace AcDream.App.World; /// expose current presentation state without publishing a second GUID identity /// map. /// -internal interface ILiveEntityRadarSource +public interface ILiveEntityRadarSource { bool TryGetMaterialized(uint serverGuid, out WorldEntity entity); bool TryGetVisible(uint serverGuid, out WorldEntity entity); diff --git a/src/AcDream.App/World/ILiveEntitySpatialQuery.cs b/src/AcDream.App/World/ILiveEntitySpatialQuery.cs index 6fd83a68..1b587267 100644 --- a/src/AcDream.App/World/ILiveEntitySpatialQuery.cs +++ b/src/AcDream.App/World/ILiveEntitySpatialQuery.cs @@ -1,4 +1,4 @@ -using AcDream.Core.World; +using AcDream.Core.World; namespace AcDream.App.World; @@ -7,7 +7,7 @@ namespace AcDream.App.World; /// Consumers resolve this owner late because GameWindow replaces its /// empty bootstrap world state after retained UI construction. /// -internal interface ILiveEntitySpatialQuery +public interface ILiveEntitySpatialQuery { void CopyLiveEntitiesNearLandblock( uint centerCellOrLandblockId, diff --git a/src/AcDream.App/World/LiveEntityPartArrayEnterWorldPort.cs b/src/AcDream.App/World/LiveEntityPartArrayEnterWorldPort.cs index c05a6886..b7ede09a 100644 --- a/src/AcDream.App/World/LiveEntityPartArrayEnterWorldPort.cs +++ b/src/AcDream.App/World/LiveEntityPartArrayEnterWorldPort.cs @@ -1,4 +1,4 @@ -namespace AcDream.App.World; +namespace AcDream.App.World; /// /// Strongly typed retail PartArray world-entry boundary. @@ -9,7 +9,7 @@ /// This dedicated port prevents that mandatory lifecycle callback from being /// confused with neighboring presentation callbacks. /// -internal sealed class LiveEntityPartArrayEnterWorldPort +public sealed class LiveEntityPartArrayEnterWorldPort { private readonly Action _handleEnterWorld; diff --git a/src/AcDream.App/World/LiveEntityPresentationController.cs b/src/AcDream.App/World/LiveEntityPresentationController.cs index ef1eba75..79653261 100644 --- a/src/AcDream.App/World/LiveEntityPresentationController.cs +++ b/src/AcDream.App/World/LiveEntityPresentationController.cs @@ -1,4 +1,4 @@ -using AcDream.App.Physics; +using AcDream.App.Physics; using AcDream.Core.Physics; using AcDream.Runtime.Entities; @@ -16,7 +16,7 @@ namespace AcDream.App.World; /// (0x00514C60). Typed script ids are retail /// PS_UnHide=0x75 and PS_Hidden=0x76 from acclient.h. /// -internal sealed class LiveEntityPresentationController : IDisposable +public sealed class LiveEntityPresentationController : IDisposable { public const uint UnHideScriptType = 0x75u; public const uint HiddenScriptType = 0x76u; diff --git a/src/AcDream.App/World/LiveEntityProjectionWithdrawalController.cs b/src/AcDream.App/World/LiveEntityProjectionWithdrawalController.cs index bd1fefeb..14d13aa4 100644 --- a/src/AcDream.App/World/LiveEntityProjectionWithdrawalController.cs +++ b/src/AcDream.App/World/LiveEntityProjectionWithdrawalController.cs @@ -1,4 +1,4 @@ -using AcDream.App.Physics; +using AcDream.App.Physics; using AcDream.App.Rendering.Vfx; using AcDream.Core.Physics; using AcDream.Core.Plugins; @@ -163,13 +163,13 @@ internal sealed class LiveEntityProjectionWithdrawalController } } -internal enum ExactProjectionWithdrawalDisposition +public enum ExactProjectionWithdrawalDisposition { Completed, Pending, Superseded, } -internal readonly record struct ExactProjectionWithdrawalOutcome( +public readonly record struct ExactProjectionWithdrawalOutcome( ExactProjectionWithdrawalDisposition Disposition, Exception? Failure); diff --git a/src/AcDream.App/World/LiveEntityRuntime.cs b/src/AcDream.App/World/LiveEntityRuntime.cs index 07ab3b80..378164ef 100644 --- a/src/AcDream.App/World/LiveEntityRuntime.cs +++ b/src/AcDream.App/World/LiveEntityRuntime.cs @@ -1,4 +1,4 @@ -using AcDream.App.Streaming; +using AcDream.App.Streaming; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; @@ -14,16 +14,16 @@ namespace AcDream.App.World; /// Animation state owned by a live object. The concrete App animation runtime /// implements this seam so identity storage does not depend on a render backend. /// -internal interface ILiveEntityAnimationRuntime +public interface ILiveEntityAnimationRuntime { WorldEntity Entity { get; } uint CurrentMotion { get; } } /// Marker for the DAT-driven effect profile added by the effect phase. -internal interface ILiveEntityEffectProfile { } +public interface ILiveEntityEffectProfile { } -internal enum LiveEntityProjectionKind +public enum LiveEntityProjectionKind { World, Attached, @@ -34,7 +34,7 @@ internal enum LiveEntityProjectionKind /// Spatial bucketing is deliberately absent: registering or removing meshes, /// scripts, and effects is a logical-lifetime operation, not a landblock move. /// -internal interface ILiveEntityResourceLifecycle +public interface ILiveEntityResourceLifecycle { void Register(WorldEntity entity); void Unregister(WorldEntity entity); @@ -127,7 +127,7 @@ internal sealed class DeferredLiveEntityRuntimeComponentLifecycle : } /// Delegate adapter used by the App composition root. -internal sealed class DelegateLiveEntityResourceLifecycle : ILiveEntityResourceLifecycle +public sealed class DelegateLiveEntityResourceLifecycle : ILiveEntityResourceLifecycle { private readonly Action _register; private readonly Action _unregister; @@ -150,7 +150,7 @@ internal sealed class DelegateLiveEntityResourceLifecycle : ILiveEntityResourceL /// temporary loss of its render bucket; this sidecar retains only graphical /// components, visibility, hydration, and teardown progress. /// -internal sealed class LiveEntityRecord +public sealed class LiveEntityRecord { private readonly RuntimeEntityDirectory _directory; @@ -339,7 +339,7 @@ internal sealed class LiveEntityRecord _directory.RefreshSnapshot(Canonical, Snapshot, refreshPosition); } -internal readonly record struct LiveEntityRegistrationResult( +public readonly record struct LiveEntityRegistrationResult( InboundCreateResult Inbound, RuntimeEntityRecord? Canonical, LiveEntityRecord? Projection, @@ -366,7 +366,7 @@ internal readonly record struct LiveEntityRegistrationResult( /// CPhysicsObj::exit_world (0x00514E60). /// /// -internal sealed class LiveEntityRuntime : ILiveEntityRadarSource +public sealed class LiveEntityRuntime : ILiveEntityRadarSource { public const uint FirstLiveEntityId = RuntimeEntityDirectory.FirstLocalEntityId; public const uint LastLiveEntityId = RuntimeEntityDirectory.LastLocalEntityId; diff --git a/src/AcDream.App/World/LiveEntityRuntimeViews.cs b/src/AcDream.App/World/LiveEntityRuntimeViews.cs index c6c824cc..79b9bd70 100644 --- a/src/AcDream.App/World/LiveEntityRuntimeViews.cs +++ b/src/AcDream.App/World/LiveEntityRuntimeViews.cs @@ -1,11 +1,11 @@ -namespace AcDream.App.World; +namespace AcDream.App.World; /// /// Strongly typed view over animation components owned by a /// . It owns only reusable traversal scratch; /// canonical component identity remains in the runtime. /// -internal sealed class LiveEntityAnimationRuntimeView +public sealed class LiveEntityAnimationRuntimeView : IEnumerable> where TAnimation : class, ILiveEntityAnimationRuntime { @@ -90,7 +90,7 @@ internal sealed class LiveEntityAnimationRuntimeView /// invalidating this traversal. Each entry is identity-checked immediately /// before it is yielded, so GUID reuse cannot advance the displaced owner. /// - internal struct Enumerator : IEnumerator> + public struct Enumerator : IEnumerator> { private readonly LiveEntityRuntime? _runtime; private readonly List> _snapshot; diff --git a/tests/AcDream.App.Tests/Composition/HostInputCameraCompositionTests.cs b/tests/AcDream.App.Tests/Composition/HostInputCameraCompositionTests.cs index 63727c77..7b081a55 100644 --- a/tests/AcDream.App.Tests/Composition/HostInputCameraCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/HostInputCameraCompositionTests.cs @@ -159,7 +159,6 @@ public sealed class HostInputCameraCompositionTests { public GpuFrameFlightController? GpuFrames { get; private set; } public IGpuDevice? GpuDevice { get; private set; } - public GpuDeviceFrameLifetime? GpuFrameLifetime { get; private set; } public SilkKeyboardSource? Keyboard { get; private set; } public SilkMouseSource? Mouse { get; private set; } public IMouseLookCursor? Cursor { get; private set; } @@ -173,9 +172,6 @@ public sealed class HostInputCameraCompositionTests public void PublishGpuDevice(IGpuDevice value) => GpuDevice = PublishOnce(GpuDevice, value); - public void PublishGpuFrameLifetime(GpuDeviceFrameLifetime value) => - GpuFrameLifetime = PublishOnce(GpuFrameLifetime, value); - public void PublishKeyboardSource(SilkKeyboardSource value) => Keyboard = PublishOnce(Keyboard, value); diff --git a/tests/AcDream.App.Tests/Composition/InteractionRetainedUiCompositionTests.cs b/tests/AcDream.App.Tests/Composition/InteractionRetainedUiCompositionTests.cs index cd65d48a..9ea7893e 100644 --- a/tests/AcDream.App.Tests/Composition/InteractionRetainedUiCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/InteractionRetainedUiCompositionTests.cs @@ -189,10 +189,9 @@ public sealed class InteractionRetainedUiCompositionTests Dependencies = new InteractionRetainedUiDependencies( Options: options, Gl: null!, - GpuDevice: null!, - CurrentGpuFrame: null!, Window: null!, Input: null!, + ShadersDirectory: "shaders", Dats: null!, DatLock: new object(), TextureCache: null!, diff --git a/tests/AcDream.App.Tests/Composition/SettingsDevToolsCompositionTests.cs b/tests/AcDream.App.Tests/Composition/SettingsDevToolsCompositionTests.cs index a4bfedc1..d9d3db6c 100644 --- a/tests/AcDream.App.Tests/Composition/SettingsDevToolsCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/SettingsDevToolsCompositionTests.cs @@ -218,7 +218,6 @@ public sealed class SettingsDevToolsCompositionTests null!, null!, null!, - null!, null, null, null, diff --git a/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs b/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs index a7b522ee..4a1aab29 100644 --- a/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs @@ -2,10 +2,8 @@ using System.Collections.Concurrent; using System.Runtime.CompilerServices; using AcDream.App.Composition; using AcDream.App.Rendering; -using AcDream.App.Rendering.Gpu; using AcDream.App.Rendering.Wb; using AcDream.App.Rendering.Residency; -using AcDream.App.Tests.Rendering.Gpu; using AcDream.App.World; using AcDream.Content; using AcDream.Core.Physics; @@ -161,7 +159,6 @@ public sealed class WorldRenderCompositionTests Factory = new Factory(hasFont); Publication = new Publication(publicationFailure); Lifetime = new RenderLifetime(Factory.Atlas); - GpuDevice = new RecordingGpuDevice(); var content = (ContentEffectsAudioResult) RuntimeHelpers.GetUninitializedObject(typeof(ContentEffectsAudioResult)); Content = content; @@ -170,7 +167,6 @@ public sealed class WorldRenderCompositionTests public Factory Factory { get; } public Publication Publication { get; } public RenderLifetime Lifetime { get; } - public RecordingGpuDevice GpuDevice { get; } public List Points { get; } = []; public ContentEffectsAudioResult Content { get; } @@ -180,7 +176,6 @@ public sealed class WorldRenderCompositionTests new WorldEnvironmentController(), Lifetime, ImmediateGpuResourceRetirementQueue.Instance, - GpuDevice, _budgets, 0xA9B4FFFFu, Path.Combine(Path.GetTempPath(), "acdream-tests"), @@ -253,15 +248,15 @@ public sealed class WorldRenderCompositionTests public SceneLightingUboBinding CreateSceneLighting(GL gl) => Resource("scene lighting"); - public DebugLineRenderer CreateDebugLines(IGpuDevice device) => + public DebugLineRenderer CreateDebugLines(GL gl, string shadersDirectory) => Resource("debug lines"); public byte[]? TryLoadDebugFont() => hasFont ? [1] : null; - public BitmapFont CreateDebugFont(IGpuDevice device, byte[] bytes) => + public BitmapFont CreateDebugFont(GL gl, byte[] bytes) => Resource("debug font"); - public TextRenderer CreateTextRenderer(IGpuDevice device) => + public TextRenderer CreateTextRenderer(GL gl, string shadersDirectory) => Resource("text renderer"); public TerrainModernRenderer CreateTerrain( @@ -300,7 +295,6 @@ public sealed class WorldRenderCompositionTests public TextureCache CreateTextureCache( GL gl, - IGpuDevice device, IDatReaderWriter dats, BindlessSupport bindless, IGpuResourceRetirementQueue retirement, diff --git a/tests/AcDream.App.Tests/GlobalUsings.cs b/tests/AcDream.App.Tests/GlobalUsings.cs index 9b259dd4..5213b6cc 100644 --- a/tests/AcDream.App.Tests/GlobalUsings.cs +++ b/tests/AcDream.App.Tests/GlobalUsings.cs @@ -2,4 +2,3 @@ global using AcDream.Runtime.Gameplay; global using AcDream.Runtime.Physics; global using ILocalPlayerMotionSource = AcDream.Runtime.Gameplay.IRuntimeLocalPlayerMotionSource; -global using AcDream.App.Rendering.Gpu; diff --git a/tests/AcDream.App.Tests/Rendering/GlTextureOwnershipTests.cs b/tests/AcDream.App.Tests/Rendering/GlTextureOwnershipTests.cs index 6b0cfdae..2edc666f 100644 --- a/tests/AcDream.App.Tests/Rendering/GlTextureOwnershipTests.cs +++ b/tests/AcDream.App.Tests/Rendering/GlTextureOwnershipTests.cs @@ -104,13 +104,8 @@ public sealed class GlTextureOwnershipTests root, "src", "AcDream.App", "Rendering", "ShaderProgramConstruction.cs")); string terrain = File.ReadAllText(Path.Combine( root, "src", "AcDream.App", "Rendering", "TerrainAtlas.cs")); - // Campaign V slice V4a ported TextRenderer/BitmapFont off raw GL onto - // IGpuDevice: neither file creates a GL texture name directly anymore. - // The checked-commit-boundary invariant this test enforces now lives in - // the RHI's GlGpuTexture, which every ported (and legacy) texture path - // shares. - string gpuTexture = File.ReadAllText(Path.Combine( - root, "src", "AcDream.App", "Rendering", "Gpu", "Gl", "GlGpuTexture.cs")); + string text = File.ReadAllText(Path.Combine( + root, "src", "AcDream.App", "Rendering", "TextRenderer.cs")); string bindless = File.ReadAllText(Path.Combine( root, "src", "AcDream.App", "Rendering", "Wb", "BindlessSupport.cs")); @@ -121,8 +116,8 @@ public sealed class GlTextureOwnershipTests Assert.Contains("GlResourceCommand.DeleteProgram", shaderPrograms, StringComparison.Ordinal); Assert.Contains("_anisotropyBindingMutation.Execute", terrain, StringComparison.Ordinal); Assert.Contains("GlResourceCommand.DeleteTexture", terrain, StringComparison.Ordinal); - Assert.Contains("GlResourceCommand.CreateName", gpuTexture, StringComparison.Ordinal); - Assert.Contains("GLHelpers.ThrowOnResourceError", gpuTexture, StringComparison.Ordinal); + Assert.Contains("GlResourceCommand.CreateTexture", text, StringComparison.Ordinal); + Assert.Contains("GlResourceCommand.Execute", text, StringComparison.Ordinal); Assert.Contains("make bindless handle", bindless, StringComparison.Ordinal); Assert.Contains("GlResourceCommand.Execute", bindless, StringComparison.Ordinal); } diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlRenderStateCacheTests.cs b/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlRenderStateCacheTests.cs index 078ebc0d..d4f32c6d 100644 --- a/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlRenderStateCacheTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlRenderStateCacheTests.cs @@ -14,8 +14,7 @@ public sealed class GlRenderStateCacheTests GpuCullMode.Back, GpuFrontFace.CounterClockwise, AlphaToCoverage: false, - ColorWrite: true, - Multisample: false); + ColorWrite: true); [Fact] public void FirstApplyReportsEveryDimensionChanged() diff --git a/tests/AcDream.App.Tests/Rendering/RenderFrameResourceControllerTests.cs b/tests/AcDream.App.Tests/Rendering/RenderFrameResourceControllerTests.cs index 6383c778..4ac71907 100644 --- a/tests/AcDream.App.Tests/Rendering/RenderFrameResourceControllerTests.cs +++ b/tests/AcDream.App.Tests/Rendering/RenderFrameResourceControllerTests.cs @@ -49,12 +49,6 @@ public sealed class RenderFrameResourceControllerTests { string source = ResourceSource(); - // TextRenderer (_worldText/_uiText) moved onto the IGpuDevice/IGpuFrame - // RHI in Campaign V slice V4a: it no longer has a per-slot BeginFrame - // step, so those two needles are retired from this ordering check. - // The remaining GL-only renderers (WbDrawDispatcher, EnvCellRenderer, - // terrain, sky/lighting) are untouched by V4a and keep their slot-based - // BeginFrame(gpuSlot) order. AssertAppearsInOrder( source, "_textures?.BeginCompositeTextureFrame();", @@ -62,6 +56,8 @@ public sealed class RenderFrameResourceControllerTests "_dispatcher?.BeginFrame(gpuSlot);", "_environmentCells?.BeginFrame(gpuSlot);", "_portalDepth?.BeginFrame(gpuSlot);", + "_worldText?.BeginFrame(gpuSlot);", + "_uiText?.BeginFrame(gpuSlot);", "_clip?.BeginFrame(gpuSlot);", "_terrain?.BeginFrame(gpuSlot);", "_lighting?.BeginFrame(gpuSlot);"); diff --git a/tests/AcDream.App.Tests/Rendering/ResourceCleanupGroupTests.cs b/tests/AcDream.App.Tests/Rendering/ResourceCleanupGroupTests.cs index d7199d01..0efce3e4 100644 --- a/tests/AcDream.App.Tests/Rendering/ResourceCleanupGroupTests.cs +++ b/tests/AcDream.App.Tests/Rendering/ResourceCleanupGroupTests.cs @@ -130,13 +130,40 @@ public sealed class ResourceCleanupGroupTests StringComparison.Ordinal); } - // TextRendererPublishesEveryConstructorResourceBeforeLaterGlWork was retired - // by Campaign V slice V4a: TextRenderer no longer owns a Shader, per-flight - // FrameBufferSet array, white-texture fallback, or tracked VAO/VBO pair — - // it now borrows a shared IGpuDevice and owns exactly one IGpuPipeline - // created through it, so there is no multi-resource construction order left - // to assert. The equivalent GL-only invariant now lives in GlTextureOwnershipTests - // (GlGpuTexture's checked-commit texture creation). + [Fact] + public void TextRendererPublishesEveryConstructorResourceBeforeLaterGlWork() + { + string source = File.ReadAllText(Path.Combine( + FindRepoRoot(), + "src", + "AcDream.App", + "Rendering", + "TextRenderer.cs")); + + AssertAppearsInOrder( + source, + "shader = new Shader(gl,", + "resources.Add(\"text shader\", shader.Dispose);", + "frameBuffers[i] = CreateFrameBufferSet(resources);", + "whiteTexture = GlResourceCommand.CreateTexture(", + "resources.Add(", + "\"white texture\"", + "GlResourceCommand.Execute(", + "initialize TextRenderer white texture", + "resources.RetryCleanup();", + "_resources = resources;"); + AssertAppearsInOrder( + source, + "private FrameBufferSet CreateFrameBufferSet(ResourceCleanupGroup resources)", + "TrackedGlResource.CreateVertexArray(", + "resources.Add(\"frame VAO\", vaoRelease.Run);", + "TrackedGlResource.CreateBuffer(", + "resources.Add(", + "\"frame VBO\""); + Assert.Contains("_resources.RetryCleanup();", source, StringComparison.Ordinal); + Assert.DoesNotContain("private FrameBufferSet CreateFrameBufferSet()", source, + StringComparison.Ordinal); + } [Fact] public void ReturnedNameIsRetainedWhenPostconditionAndFirstRollbackFail() diff --git a/tests/AcDream.App.Tests/Rendering/TextRendererFailureSafetyTests.cs b/tests/AcDream.App.Tests/Rendering/TextRendererFailureSafetyTests.cs new file mode 100644 index 00000000..77154717 --- /dev/null +++ b/tests/AcDream.App.Tests/Rendering/TextRendererFailureSafetyTests.cs @@ -0,0 +1,217 @@ +using System.Reflection; +using AcDream.App.Rendering; +using Silk.NET.OpenGL; + +namespace AcDream.App.Tests.Rendering; + +public sealed class TextRendererFailureSafetyTests +{ + [Fact] + public void Flush_CompilesCompleteGlStateScopeAsFinallyAroundBothDrawLayers() + { + MethodInfo flush = typeof(TextRenderer).GetMethod(nameof(TextRenderer.Flush))!; + MethodBody body = flush.GetMethodBody()!; + string source = File.ReadAllText(Path.Combine( + FindRepoRoot(), + "src", + "AcDream.App", + "Rendering", + "TextRenderer.cs")); + + Assert.Contains( + body.ExceptionHandlingClauses, + clause => clause.Flags == ExceptionHandlingClauseOptions.Finally); + AssertAppearsInOrder( + source, + "using var stateScope = new TextRenderGlStateScope(_glState);", + "_gl.Disable(EnableCap.Multisample);", + "DrawLayer(_spriteSegs,", + "DrawLayer(_overlaySpriteSegs,"); + } + + [Fact] + public void FailedDraw_RestoresEveryGlValueMutatedByTheTextPass() + { + var gl = new RecordingGlState + { + DepthWrite = false, + BlendSourceRgb = BlendingFactor.DstAlpha, + BlendDestinationRgb = BlendingFactor.OneMinusDstAlpha, + BlendSourceAlpha = BlendingFactor.One, + BlendDestinationAlpha = BlendingFactor.Zero, + Program = 17, + VertexArray = 23, + ArrayBuffer = 31, + ActiveUnit = TextureUnit.Texture2, + }; + gl.SetCapability(EnableCap.DepthTest, enabled: true); + gl.SetCapability(EnableCap.Blend, enabled: false); + gl.SetCapability(EnableCap.CullFace, enabled: true); + gl.SetCapability(EnableCap.SampleAlphaToCoverage, enabled: true); + gl.SetCapability(EnableCap.Multisample, enabled: false); + gl.TextureBindings[TextureUnit.Texture0] = 41; + gl.TextureBindings[TextureUnit.Texture2] = 43; + StateSnapshot expected = gl.Capture(); + + Action failedDraw = () => + { + using var stateScope = new TextRenderGlStateScope(gl); + gl.SetCapability(EnableCap.DepthTest, enabled: false); + gl.SetCapability(EnableCap.Blend, enabled: true); + gl.SetCapability(EnableCap.CullFace, enabled: false); + gl.SetCapability(EnableCap.SampleAlphaToCoverage, enabled: false); + gl.SetCapability(EnableCap.Multisample, enabled: true); + gl.DepthMask(true); + gl.BlendFuncSeparate( + BlendingFactor.SrcAlpha, + BlendingFactor.OneMinusSrcAlpha, + BlendingFactor.SrcAlpha, + BlendingFactor.OneMinusSrcAlpha); + gl.UseProgram(101); + gl.BindVertexArray(103); + gl.BindBuffer(BufferTargetARB.ArrayBuffer, 107); + gl.ActiveTexture(TextureUnit.Texture0); + gl.BindTexture(TextureTarget.Texture2D, 109); + throw new InvalidOperationException("draw upload"); + }; + + Assert.Throws(failedDraw); + + Assert.Equal(expected, gl.Capture()); + } + + private readonly record struct StateSnapshot( + bool DepthTest, + bool Blend, + bool Cull, + bool AlphaToCoverage, + bool Multisample, + bool DepthWrite, + BlendingFactor BlendSourceRgb, + BlendingFactor BlendDestinationRgb, + BlendingFactor BlendSourceAlpha, + BlendingFactor BlendDestinationAlpha, + uint Program, + uint VertexArray, + uint ArrayBuffer, + TextureUnit ActiveUnit, + uint Texture0, + uint Texture2); + + private sealed class RecordingGlState : ITextRenderGlStateApi + { + private readonly Dictionary _capabilities = []; + + public bool DepthWrite { get; set; } + public BlendingFactor BlendSourceRgb { get; set; } + public BlendingFactor BlendDestinationRgb { get; set; } + public BlendingFactor BlendSourceAlpha { get; set; } + public BlendingFactor BlendDestinationAlpha { get; set; } + public uint Program { get; set; } + public uint VertexArray { get; set; } + public uint ArrayBuffer { get; set; } + public TextureUnit ActiveUnit { get; set; } + public Dictionary TextureBindings { get; } = []; + + public StateSnapshot Capture() => new( + IsEnabled(EnableCap.DepthTest), + IsEnabled(EnableCap.Blend), + IsEnabled(EnableCap.CullFace), + IsEnabled(EnableCap.SampleAlphaToCoverage), + IsEnabled(EnableCap.Multisample), + DepthWrite, + BlendSourceRgb, + BlendDestinationRgb, + BlendSourceAlpha, + BlendDestinationAlpha, + Program, + VertexArray, + ArrayBuffer, + ActiveUnit, + TextureBindings.GetValueOrDefault(TextureUnit.Texture0), + TextureBindings.GetValueOrDefault(TextureUnit.Texture2)); + + public bool IsEnabled(EnableCap capability) => + _capabilities.GetValueOrDefault(capability); + + public int GetInteger(GetPName parameter) => parameter switch + { + GetPName.BlendSrcRgb => (int)BlendSourceRgb, + GetPName.BlendDstRgb => (int)BlendDestinationRgb, + GetPName.BlendSrcAlpha => (int)BlendSourceAlpha, + GetPName.BlendDstAlpha => (int)BlendDestinationAlpha, + GetPName.CurrentProgram => (int)Program, + GetPName.VertexArrayBinding => (int)VertexArray, + GetPName.ArrayBufferBinding => (int)ArrayBuffer, + GetPName.ActiveTexture => (int)ActiveUnit, + GetPName.TextureBinding2D => + (int)TextureBindings.GetValueOrDefault(ActiveUnit), + _ => throw new ArgumentOutOfRangeException(nameof(parameter)), + }; + + public bool GetBoolean(GetPName parameter) => + parameter == GetPName.DepthWritemask + ? DepthWrite + : throw new ArgumentOutOfRangeException(nameof(parameter)); + + public void SetCapability(EnableCap capability, bool enabled) => + _capabilities[capability] = enabled; + + public void DepthMask(bool enabled) => DepthWrite = enabled; + + public void BlendFuncSeparate( + BlendingFactor sourceRgb, + BlendingFactor destinationRgb, + BlendingFactor sourceAlpha, + BlendingFactor destinationAlpha) + { + BlendSourceRgb = sourceRgb; + BlendDestinationRgb = destinationRgb; + BlendSourceAlpha = sourceAlpha; + BlendDestinationAlpha = destinationAlpha; + } + + public void UseProgram(uint program) => Program = program; + + public void BindVertexArray(uint vertexArray) => VertexArray = vertexArray; + + public void BindBuffer(BufferTargetARB target, uint buffer) + { + Assert.Equal(BufferTargetARB.ArrayBuffer, target); + ArrayBuffer = buffer; + } + + public void ActiveTexture(TextureUnit unit) => ActiveUnit = unit; + + public void BindTexture(TextureTarget target, uint texture) + { + Assert.Equal(TextureTarget.Texture2D, target); + TextureBindings[ActiveUnit] = texture; + } + } + + private static void AssertAppearsInOrder(string source, params string[] needles) + { + int cursor = -1; + foreach (string needle in needles) + { + int next = source.IndexOf(needle, cursor + 1, StringComparison.Ordinal); + Assert.True(next >= 0, $"Missing expected source fragment: {needle}"); + Assert.True(next > cursor, $"Out-of-order source fragment: {needle}"); + cursor = next; + } + } + + private static string FindRepoRoot() + { + DirectoryInfo? directory = new(AppContext.BaseDirectory); + while (directory is not null) + { + if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx"))) + return directory.FullName; + directory = directory.Parent; + } + + throw new DirectoryNotFoundException("Could not find AcDream.slnx."); + } +} diff --git a/tests/AcDream.App.Tests/Studio/DumpLayoutTests.cs b/tests/AcDream.App.Tests/Studio/DumpLayoutTests.cs index b13533ad..2b635778 100644 --- a/tests/AcDream.App.Tests/Studio/DumpLayoutTests.cs +++ b/tests/AcDream.App.Tests/Studio/DumpLayoutTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.Studio; using AcDream.App.UI; @@ -32,7 +31,7 @@ public class DumpLayoutTests "docs", "research", "2026-06-25-retail-ui-layout-dump.json"); } - private static (GpuTextureSlot, int, int) NoTex(uint _) => (new GpuTextureSlot(1), 1, 1); + private static (uint, int, int) NoTex(uint _) => (1u, 1, 1); // ── Helpers ────────────────────────────────────────────────────────── diff --git a/tests/AcDream.App.Tests/Studio/LayoutSourceTests.cs b/tests/AcDream.App.Tests/Studio/LayoutSourceTests.cs index 30ac80f9..5d478223 100644 --- a/tests/AcDream.App.Tests/Studio/LayoutSourceTests.cs +++ b/tests/AcDream.App.Tests/Studio/LayoutSourceTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.Studio; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -13,7 +12,7 @@ namespace AcDream.App.Tests.Studio; /// public class LayoutSourceTests { - private static (GpuTextureSlot handle, int width, int height) NoTex(uint _) => (new GpuTextureSlot(1u), 1, 1); + private static (uint handle, int width, int height) NoTex(uint _) => (1u, 1, 1); /// Resolve the client dat directory, or null if unavailable (skip the test). private static string? ResolveDatDir() diff --git a/tests/AcDream.App.Tests/UI/CursorFeedbackControllerTests.cs b/tests/AcDream.App.Tests/UI/CursorFeedbackControllerTests.cs index 70bf0623..6aee28dc 100644 --- a/tests/AcDream.App.Tests/UI/CursorFeedbackControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/CursorFeedbackControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.Core.Combat; using AcDream.Core.Items; @@ -21,11 +20,11 @@ public sealed class CursorFeedbackControllerTests var accept = c.Resolve(new CursorFeedbackSnapshot( DragPayload: new object(), - DragAccept: DragAcceptState.Accept, + DragAccept: UiItemSlot.DragAcceptState.Accept, HoverResizeEdges: ResizeEdges.Right)); var reject = c.Resolve(new CursorFeedbackSnapshot( DragPayload: new object(), - DragAccept: DragAcceptState.Reject, + DragAccept: UiItemSlot.DragAcceptState.Reject, HoverResizeEdges: ResizeEdges.Right)); Assert.Equal(CursorFeedbackKind.DragAccept, accept.Kind); @@ -212,7 +211,7 @@ public sealed class CursorFeedbackControllerTests { ["Drag_rollover_accept"] = acceptCursor, }); - slot.SetItem(Target, iconTexture: new GpuTextureSlot(1u)); + slot.SetItem(Target, iconTexture: 1u); root.AddChild(slot); root.OnMouseMove(20, 20); var c = new CursorFeedbackController(interaction); diff --git a/tests/AcDream.App.Tests/UI/DatWidgetFactoryZOrderTests.cs b/tests/AcDream.App.Tests/UI/DatWidgetFactoryZOrderTests.cs index d3b8547b..52ba5332 100644 --- a/tests/AcDream.App.Tests/UI/DatWidgetFactoryZOrderTests.cs +++ b/tests/AcDream.App.Tests/UI/DatWidgetFactoryZOrderTests.cs @@ -1,11 +1,10 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI.Layout; namespace AcDream.App.Tests.UI; public class DatWidgetFactoryZOrderTests { - private static (GpuTextureSlot, int, int) NoChrome(uint id) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoChrome(uint id) => (0u, 0, 0); [Fact] public void ZOrder_HigherZLevel_DrawsBehind() diff --git a/tests/AcDream.App.Tests/UI/DragDropSpineTests.cs b/tests/AcDream.App.Tests/UI/DragDropSpineTests.cs index 232248e5..d5452b3d 100644 --- a/tests/AcDream.App.Tests/UI/DragDropSpineTests.cs +++ b/tests/AcDream.App.Tests/UI/DragDropSpineTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using Xunit; @@ -41,7 +40,7 @@ public class DragDropSpineTests [Fact] public void UiItemList_registerDragHandler_roundtrips() { - var list = new UiItemList(_ => (GpuTextureSlot.Unassigned, 0, 0)); + var list = new UiItemList(_ => (0u, 0, 0)); Assert.Null(list.DragHandler); var h = new SpyHandler(); list.RegisterDragHandler(h); @@ -57,7 +56,7 @@ public class DragDropSpineTests public void GetDragPayload_boundCell_snapshotsFields() { var cell = new UiItemSlot { SlotIndex = 4, SourceKind = ItemDragSource.ShortcutBar }; - cell.SetItem(0x5001u, new GpuTextureSlot(0x99u)); + cell.SetItem(0x5001u, 0x99u); var p = Assert.IsType(cell.GetDragPayload()); Assert.Equal(0x5001u, p.ObjId); Assert.Equal(ItemDragSource.ShortcutBar, p.SourceKind); @@ -73,10 +72,10 @@ public class DragDropSpineTests public void GetDragGhost_boundCell_returnsIconTuple() { var cell = new UiItemSlot { Width = 32, Height = 32 }; - cell.SetItem(0x5001u, new GpuTextureSlot(0x99u)); + cell.SetItem(0x5001u, 0x99u); var g = cell.GetDragGhost(); Assert.NotNull(g); - Assert.Equal(new GpuTextureSlot(0x99u), g!.Value.tex); + Assert.Equal(0x99u, g!.Value.tex); Assert.Equal(32, g.Value.w); Assert.Equal(32, g.Value.h); } @@ -85,16 +84,16 @@ public class DragDropSpineTests public void GetDragGhost_prefersDedicatedUnderlayFreeTexture() { var cell = new UiItemSlot { Width = 36, Height = 36 }; // retail bag cell is larger than its icon - cell.SetItem(0x5001u, new GpuTextureSlot(0x99u), dragIconTexture: new GpuTextureSlot(0x77u)); + cell.SetItem(0x5001u, 0x99u, dragIconTexture: 0x77u); - Assert.Equal((new GpuTextureSlot(0x77u), 32, 32), cell.GetDragGhost()); - Assert.Equal(new GpuTextureSlot(0x99u), cell.IconTexture); // source cell keeps the full m_pIcon + Assert.Equal((0x77u, 32, 32), cell.GetDragGhost()); + Assert.Equal(0x99u, cell.IconTexture); // source cell keeps the full m_pIcon } // ── cell drop-target: DragEnter overlay + DropReleased dispatch ────────── private static (UiItemList list, UiItemSlot cell, SpyHandler h) ListWithHandler() { - var list = new UiItemList(_ => (new GpuTextureSlot(1u), 1, 1)); // non-zero resolve so overlay draw is harmless + var list = new UiItemList(_ => (1u, 1, 1)); // non-zero resolve so overlay draw is harmless var h = new SpyHandler(); list.RegisterDragHandler(h); return (list, list.Cell, h); @@ -109,7 +108,7 @@ public class DragDropSpineTests var (_, cell, h) = ListWithHandler(); h.Acceptance = ItemDragAcceptance.Accept; cell.OnEvent(new UiEvent(0u, cell, UiEventType.DragEnter, Payload: SomePayload())); - Assert.Equal(DragAcceptState.Accept, cell.DragAcceptVisual); + Assert.Equal(UiItemSlot.DragAcceptState.Accept, cell.DragAcceptVisual); } [Fact] @@ -118,7 +117,7 @@ public class DragDropSpineTests var (_, cell, h) = ListWithHandler(); h.Acceptance = ItemDragAcceptance.Reject; cell.OnEvent(new UiEvent(0u, cell, UiEventType.DragEnter, Payload: SomePayload())); - Assert.Equal(DragAcceptState.Reject, cell.DragAcceptVisual); + Assert.Equal(UiItemSlot.DragAcceptState.Reject, cell.DragAcceptVisual); } [Fact] @@ -127,7 +126,7 @@ public class DragDropSpineTests var (_, cell, h) = ListWithHandler(); h.Acceptance = ItemDragAcceptance.None; cell.OnEvent(new UiEvent(0u, cell, UiEventType.DragEnter, Payload: SomePayload())); - Assert.Equal(DragAcceptState.None, cell.DragAcceptVisual); + Assert.Equal(UiItemSlot.DragAcceptState.None, cell.DragAcceptVisual); } [Fact] @@ -136,7 +135,7 @@ public class DragDropSpineTests var (_, cell, h) = ListWithHandler(); cell.OnEvent(new UiEvent(0u, cell, UiEventType.DragEnter, Payload: SomePayload())); cell.OnEvent(new UiEvent(0u, cell, UiEventType.DragOver, Payload: SomePayload())); - Assert.Equal(DragAcceptState.None, cell.DragAcceptVisual); + Assert.Equal(UiItemSlot.DragAcceptState.None, cell.DragAcceptVisual); } [Fact] @@ -180,16 +179,16 @@ public class DragDropSpineTests root.OnMouseDown(UiMouseButton.Left, 10, 10); root.OnMouseMove(20, 10); // BeginDrag → snapshot ghost cell.Clear(); // simulate the lift emptying the source - Assert.Equal((new GpuTextureSlot(0x99u), 32, 32), root.DragGhostForTest); + Assert.Equal((0x99u, 32, 32), root.DragGhostForTest); } [Fact] public void FinishDrag_overNothing_deliversNoDrop_butLiftStands() { var root = new UiRoot { Width = 800, Height = 600 }; - var list = new UiItemList(_ => (new GpuTextureSlot(1u), 1, 1)) { Left = 0, Top = 0, Width = 32, Height = 32 }; + var list = new UiItemList(_ => (1u, 1, 1)) { Left = 0, Top = 0, Width = 32, Height = 32 }; list.Cell.Width = 32; list.Cell.Height = 32; - list.Cell.SetItem(0x5001u, new GpuTextureSlot(0x99u)); + list.Cell.SetItem(0x5001u, 0x99u); var h = new SpyHandler(); list.RegisterDragHandler(h); root.AddChild(list); @@ -238,10 +237,10 @@ public class DragDropSpineTests private static (UiRoot root, UiItemList list, UiItemSlot cell) RootWithBoundSlot(uint itemId) { var root = new UiRoot { Width = 800, Height = 600 }; - var list = new UiItemList(_ => (new GpuTextureSlot(1u), 1, 1)) { Left = 0, Top = 0, Width = 32, Height = 32 }; + var list = new UiItemList(_ => (1u, 1, 1)) { Left = 0, Top = 0, Width = 32, Height = 32 }; // Tests don't run OnDraw (which sizes the cell), so size the cell explicitly. list.Cell.Width = 32; list.Cell.Height = 32; - if (itemId != 0) list.Cell.SetItem(itemId, new GpuTextureSlot(0x99u)); + if (itemId != 0) list.Cell.SetItem(itemId, 0x99u); root.AddChild(list); return (root, list, list.Cell); } @@ -250,7 +249,7 @@ public class DragDropSpineTests RootWithCatalogSlot(uint entryId) { var root = new UiRoot { Width = 800, Height = 600 }; - var list = new UiItemList(_ => (new GpuTextureSlot(1u), 1, 1)) + var list = new UiItemList(_ => (1u, 1, 1)) { Left = 0, Top = 0, @@ -263,7 +262,7 @@ public class DragDropSpineTests EntryId = entryId, Width = 32, Height = 32, - SpriteResolve = _ => (new GpuTextureSlot(1u), 1, 1), + SpriteResolve = _ => (1u, 1, 1), }; list.AddItem(cell); root.AddChild(list); @@ -428,15 +427,15 @@ public class DragDropSpineTests { var cell = new UiItemSlot(); // no parent list → FindList() null cell.OnEvent(new UiEvent(0u, cell, UiEventType.DragEnter, Payload: SomePayload())); - Assert.Equal(DragAcceptState.Reject, cell.DragAcceptVisual); + Assert.Equal(UiItemSlot.DragAcceptState.Reject, cell.DragAcceptVisual); } [Fact] public void DragEnter_listWithoutHandler_defaultsToReject() { - var list = new UiItemList(_ => (new GpuTextureSlot(1u), 1, 1)); // no RegisterDragHandler + var list = new UiItemList(_ => (1u, 1, 1)); // no RegisterDragHandler list.Cell.OnEvent(new UiEvent(0u, list.Cell, UiEventType.DragEnter, Payload: SomePayload())); - Assert.Equal(DragAcceptState.Reject, list.Cell.DragAcceptVisual); + Assert.Equal(UiItemSlot.DragAcceptState.Reject, list.Cell.DragAcceptVisual); } // ── item drag inside a Draggable window (the LIVE toolbar topology) ────── @@ -450,9 +449,9 @@ public class DragDropSpineTests { var root = new UiRoot { Width = 800, Height = 600 }; var frame = new UiPanel { Left = 10, Top = 300, Width = 200, Height = 60, Draggable = true }; - var list = new UiItemList(_ => (new GpuTextureSlot(1u), 1, 1)) { Left = 5, Top = 5, Width = 32, Height = 32 }; + var list = new UiItemList(_ => (1u, 1, 1)) { Left = 5, Top = 5, Width = 32, Height = 32 }; list.Cell.Width = 32; list.Cell.Height = 32; - if (itemId != 0) list.Cell.SetItem(itemId, new GpuTextureSlot(0x99u)); + if (itemId != 0) list.Cell.SetItem(itemId, 0x99u); frame.AddChild(list); root.AddChild(frame); return (root, frame, list); diff --git a/tests/AcDream.App.Tests/UI/ItemInteractionControllerTests.cs b/tests/AcDream.App.Tests/UI/ItemInteractionControllerTests.cs index 21ca2c89..11a9740a 100644 --- a/tests/AcDream.App.Tests/UI/ItemInteractionControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/ItemInteractionControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.Core.Combat; using AcDream.Core.Items; @@ -252,7 +251,7 @@ public sealed class ItemInteractionControllerTests }); h.GroundObject = chest; var cell = new UiItemSlot(); - cell.SetItem(itemId, GpuTextureSlot.Unassigned); + cell.SetItem(itemId, 0u); var payload = new ItemDragPayload(itemId, ItemDragSource.Inventory, 0, cell); Assert.True(h.Controller.PlaceIn3D(payload, chest)); diff --git a/tests/AcDream.App.Tests/UI/Layout/AppraisalUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/AppraisalUiControllerTests.cs index 1d34ad6b..e4a55791 100644 --- a/tests/AcDream.App.Tests/UI/Layout/AppraisalUiControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/AppraisalUiControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System.Numerics; using AcDream.App.Spells; using AcDream.Content; @@ -15,7 +14,7 @@ namespace AcDream.App.Tests.UI.Layout; public sealed class AppraisalUiControllerTests { private const uint ObjectId = 0x50000001u; - private static (GpuTextureSlot, int, int) NoTexture(uint _) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoTexture(uint _) => (0u, 0, 0); [Fact] public void ItemResponse_UsesAuthoredItemSubviewTitleAndScrollbars() @@ -742,11 +741,11 @@ public sealed class AppraisalUiControllerTests () => { }, selection: selection, spellbook: spellbook, - resolveSpellIcon: id => new GpuTextureSlot(id + 1_000u), + resolveSpellIcon: id => id + 1_000u, resolveComponentIcon: did => { resolvedComponentIcons.Add(did); - return new GpuTextureSlot(did + 2_000u); + return did + 2_000u; }, spellComponents: _ => components, magicSkill: _ => 200u, @@ -802,18 +801,14 @@ public sealed class AppraisalUiControllerTests AppraisalUiController.SpellIconId)!; UiTextureElement icon = Assert.Single( iconHost.Children.OfType()); - Assert.Equal(new GpuTextureSlot(metadata.SpellId + 1_000u), icon.Texture); + Assert.Equal(metadata.SpellId + 1_000u, icon.Texture); Assert.Equal( new uint[] { 0x06000010u, 0x06000011u }, resolvedComponentIcons); UiElement formula = layout.FindElement( AppraisalUiController.SpellFormulaListId)!; Assert.Equal( - new[] - { - new GpuTextureSlot(0x06000010u + 2_000u), - new GpuTextureSlot(0x06000011u + 2_000u), - }, + new uint[] { 0x06000010u + 2_000u, 0x06000011u + 2_000u }, formula.Children .OfType() .Select(cell => cell.RuntimeImageTexture!.Value)); @@ -833,8 +828,8 @@ public sealed class AppraisalUiControllerTests SelectionState? selection = null, RetailAppraisalNameResolver? itemNames = null, Spellbook? spellbook = null, - Func? resolveSpellIcon = null, - Func? resolveComponentIcon = null, + Func? resolveSpellIcon = null, + Func? resolveComponentIcon = null, Func>? spellComponents = null, Func? magicSkill = null, SpellExamineComponentTemplateFactory? spellComponentTemplates = null) diff --git a/tests/AcDream.App.Tests/UI/Layout/CharacterLayoutImportProbe.cs b/tests/AcDream.App.Tests/UI/Layout/CharacterLayoutImportProbe.cs index 36a6d64e..d2b334fb 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CharacterLayoutImportProbe.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CharacterLayoutImportProbe.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System; using System.Collections.Generic; using System.IO; @@ -31,10 +30,10 @@ public sealed class CharacterLayoutImportProbe if (datDir is null) return; using var dats = new DatCollection(datDir, DatAccessType.Read); - var layout = LayoutImporter.Import(dats, CharacterLayout, _ => (new GpuTextureSlot(1u), 30, 26), null); + var layout = LayoutImporter.Import(dats, CharacterLayout, _ => (1u, 30, 26), null); Assert.NotNull(layout); - CharacterStatController.Bind(layout!, SampleData.SampleCharacter, spriteResolve: _ => (new GpuTextureSlot(1u), 30, 26)); + CharacterStatController.Bind(layout!, SampleData.SampleCharacter, spriteResolve: _ => (1u, 30, 26)); var rows = new List(); CollectRows(layout!.Root, rows); @@ -62,7 +61,7 @@ public sealed class CharacterLayoutImportProbe if (datDir is null) return; using var dats = new DatCollection(datDir, DatAccessType.Read); - var layout = LayoutImporter.Import(dats, CharacterLayout, _ => (new GpuTextureSlot(1u), 30, 26), null); + var layout = LayoutImporter.Import(dats, CharacterLayout, _ => (1u, 30, 26), null); Assert.NotNull(layout); var close = layout!.FindElement(WindowChromeController.CharacterCloseButtonId); diff --git a/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs index f1f3ec80..5d2a39f0 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.Studio; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -282,7 +281,7 @@ public class CharacterStatControllerTests var layout = Fake((CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16)); + spriteResolve: id => (id, 16, 16)); var rows = list.Children.OfType().ToList(); Assert.Equal(9, rows.Count); @@ -548,7 +547,7 @@ public class CharacterStatControllerTests var layout = Fake((CharacterStatController.ListBoxId, list)); // Minimal sprite resolver — returns a fake non-zero handle so UiPanel draws it. - static (GpuTextureSlot, int, int) FakeResolve(uint id) => (new GpuTextureSlot(1), 32, 8); + static (uint, int, int) FakeResolve(uint id) => (1u, 32, 8); CharacterStatController.Bind(layout, SampleData.SampleCharacter, spriteResolve: FakeResolve); @@ -567,7 +566,7 @@ public class CharacterStatControllerTests { var list = new UiPanel(); var layout = Fake((CharacterStatController.ListBoxId, list)); - static (GpuTextureSlot, int, int) FakeResolve(uint id) => (new GpuTextureSlot(1), 32, 8); + static (uint, int, int) FakeResolve(uint id) => (1u, 32, 8); CharacterStatController.Bind(layout, SampleData.SampleCharacter, spriteResolve: FakeResolve); @@ -792,7 +791,7 @@ public class CharacterStatControllerTests Assert.Equal(3, titles.Children.Count); CharacterStatController.Bind(layout, SampleData.SampleCharacter, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16)); + spriteResolve: id => (id, 16, 16)); Assert.Equal(rootChildCount, layout.Root.Children.Count); Assert.Equal(RetailUiStateIds.Open, attributes.ActiveRetailStateId); @@ -815,7 +814,7 @@ public class CharacterStatControllerTests var skills = Assert.IsType(layout.FindElement(CharacterStatController.TabSkillsId)); CharacterStatController.Bind(layout, SampleData.SampleCharacter, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16)); + spriteResolve: id => (id, 16, 16)); skills.OnClick!(); Assert.Equal(RetailUiStateIds.Closed, attributes.ActiveRetailStateId); @@ -837,7 +836,7 @@ public class CharacterStatControllerTests var layout = Fake((CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16)); + spriteResolve: id => (id, 16, 16)); ClickTab(layout, left: 92f); @@ -892,7 +891,7 @@ public class CharacterStatControllerTests var layout = Fake((CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16)); + spriteResolve: id => (id, 16, 16)); ClickTab(layout, left: 92f); Assert.Equal(12, SkillRows(list).Count); @@ -930,7 +929,7 @@ public class CharacterStatControllerTests }); CharacterStatController.Bind(layout, SampleData.SampleCharacter, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16)); + spriteResolve: id => (id, 16, 16)); Assert.Equal("Strength", FirstRowName(visibleList)); Assert.Equal("", FirstRowName(hiddenDuplicateList)); @@ -964,7 +963,7 @@ public class CharacterStatControllerTests (CharacterStatController.FooterLine2Value, l2Value)); CharacterStatController.Bind(layout, SampleData.SampleCharacter, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16)); + spriteResolve: id => (id, 16, 16)); ClickTab(layout, left: 92f); var rows = SkillRows(list); @@ -994,7 +993,7 @@ public class CharacterStatControllerTests var requests = new List(); CharacterStatController.Bind(layout, SampleData.SampleCharacter, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16), + spriteResolve: id => (id, 16, 16), onRaiseRequest: (request, completed) => { requests.Add(request); completed(); }); ClickTab(layout, left: 92f); @@ -1028,7 +1027,7 @@ public class CharacterStatControllerTests (CharacterStatController.RaiseOneId, btn1)); CharacterStatController.Bind(layout, SampleData.SampleCharacter, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16)); + spriteResolve: id => (id, 16, 16)); ClickTab(layout, left: 92f); SkillRows(list)[5].OnClick!(); @@ -1053,7 +1052,7 @@ public class CharacterStatControllerTests var requests = new List(); CharacterStatController.Bind(layout, SampleData.SampleCharacter, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16), + spriteResolve: id => (id, 16, 16), onRaiseRequest: (request, completed) => { requests.Add(request); completed(); }); ClickTab(layout, left: 92f); @@ -1097,7 +1096,7 @@ public class CharacterStatControllerTests var requests = new List(); CharacterStatController.Bind(layout, () => sheet, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16), + spriteResolve: id => (id, 16, 16), onRaiseRequest: (request, completed) => { requests.Add(request); @@ -1150,7 +1149,7 @@ public class CharacterStatControllerTests Action? completeRaise = null; CharacterStatController.Bind(layout, () => sheet, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16), + spriteResolve: id => (id, 16, 16), onRaiseRequest: (_, completed) => completeRaise = completed); ClickTab(layout, left: 92f); @@ -1214,7 +1213,7 @@ public class CharacterStatControllerTests }); CharacterStatController.Bind(layout, SampleData.SampleCharacter, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16)); + spriteResolve: id => (id, 16, 16)); ClickTab(layout, left: 92f); @@ -1538,7 +1537,7 @@ public class CharacterStatControllerTests CharacterStatController.Bind( layout, SampleData.SampleCharacter, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16)); + spriteResolve: id => (id, 16, 16)); ApplyLayoutPass(layout.Root); @@ -1636,7 +1635,7 @@ public class CharacterStatControllerTests Width = width, Height = height, }; - return new UiDatElement(info, static _ => (GpuTextureSlot.Unassigned, 0, 0)) + return new UiDatElement(info, static _ => (0u, 0, 0)) { Top = top, Width = width, @@ -1650,7 +1649,7 @@ public class CharacterStatControllerTests var info = new ElementInfo { Id = id, Type = 1 }; info.StateMedia["Normal"] = (1u, 1); info.StateMedia["Ghosted"] = (2u, 1); - return new UiButton(info, static _ => (GpuTextureSlot.Unassigned, 0, 0)); + return new UiButton(info, static _ => (0u, 0, 0)); } private static UiText MakeTab(uint id, float left) @@ -1677,7 +1676,7 @@ public class CharacterStatControllerTests Name = "Open", PassToChildren = true, }; - return Assert.IsType(DatWidgetFactory.Create(info, static _ => (GpuTextureSlot.Unassigned, 0, 0), null)); + return Assert.IsType(DatWidgetFactory.Create(info, static _ => (0u, 0, 0), null)); } private static ImportedLayout Fake(params (uint id, UiElement e)[] items) diff --git a/tests/AcDream.App.Tests/UI/Layout/ChatLayoutConformanceTests.cs b/tests/AcDream.App.Tests/UI/Layout/ChatLayoutConformanceTests.cs index 76236c62..628247f0 100644 --- a/tests/AcDream.App.Tests/UI/Layout/ChatLayoutConformanceTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/ChatLayoutConformanceTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Core.Chat; @@ -14,7 +13,7 @@ namespace AcDream.App.Tests.UI.Layout; /// public class ChatLayoutConformanceTests { - private static (GpuTextureSlot, int, int) NoTex(uint _) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoTex(uint _) => (0, 0, 0); private static ElementInfo? Find(ElementInfo n, uint id) { diff --git a/tests/AcDream.App.Tests/UI/Layout/ChatWindowControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/ChatWindowControllerTests.cs index 9240f18a..31030d0d 100644 --- a/tests/AcDream.App.Tests/UI/Layout/ChatWindowControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/ChatWindowControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System.Collections.Generic; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -22,7 +21,7 @@ namespace AcDream.App.Tests.UI.Layout; public class ChatWindowControllerTests { // ── Null-resolve helper (no GL needed) ───────────────────────────────── - private static (GpuTextureSlot, int, int) NoTex(uint _) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoTex(uint _) => (0u, 0, 0); // ── Capture bus — records every Publish call ──────────────────────────── private sealed class CaptureBus : ICommandBus diff --git a/tests/AcDream.App.Tests/UI/Layout/CombatLayoutConformanceTests.cs b/tests/AcDream.App.Tests/UI/Layout/CombatLayoutConformanceTests.cs index 27c05acc..d8c42076 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CombatLayoutConformanceTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CombatLayoutConformanceTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -19,7 +18,7 @@ public sealed class CombatLayoutConformanceTests ElementInfo info = FixtureLoader.LoadCombatInfos(); ImportedLayout layout = LayoutImporter.Build( info, - _ => (GpuTextureSlot.Unassigned, 0, 0), + _ => (0u, 0, 0), datFont: null, stringResolve: _ => "localized"); ApplyAnchors(layout.Root); diff --git a/tests/AcDream.App.Tests/UI/Layout/CombatUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/CombatUiControllerTests.cs index 4e47bfd1..5334ea81 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CombatUiControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CombatUiControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.Combat; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -10,7 +9,7 @@ namespace AcDream.App.Tests.UI.Layout; public sealed class CombatUiControllerTests { - private static (GpuTextureSlot, int, int) NoTex(uint _) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoTex(uint _) => (0u, 0, 0); [Fact] public void CombatMode_ShowsPhysicalAndMagicPages_AndSelectsMediumByDefault() diff --git a/tests/AcDream.App.Tests/UI/Layout/CreatureAppraisalRowsTests.cs b/tests/AcDream.App.Tests/UI/Layout/CreatureAppraisalRowsTests.cs index 084e829b..926573b7 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CreatureAppraisalRowsTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CreatureAppraisalRowsTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Core.Items; @@ -131,7 +130,7 @@ public sealed class CreatureAppraisalRowsTests { var templates = new CreatureAppraisalRowTemplateFactory( FixtureLoader.LoadExaminationRowTemplateInfos(), - _ => (GpuTextureSlot.Unassigned, 0, 0), + _ => (0u, 0, 0), defaultFont: null); var row = new CreatureAppraisalRow( "Strength", diff --git a/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryFontResolveTests.cs b/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryFontResolveTests.cs index 904d2c17..8b148736 100644 --- a/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryFontResolveTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryFontResolveTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -21,7 +20,7 @@ namespace AcDream.App.Tests.UI.Layout; /// public class DatWidgetFactoryFontResolveTests { - private static (GpuTextureSlot, int, int) NoTex(uint _) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoTex(uint _) => (0, 0, 0); // ── Test 1: null fontResolve → DatFont == datFont (backward-compat) ───── diff --git a/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs b/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs index 48bac99d..f4c3b449 100644 --- a/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System.Numerics; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -6,7 +5,7 @@ namespace AcDream.App.Tests.UI.Layout; public class DatWidgetFactoryTests { - private static (GpuTextureSlot, int, int) NoTex(uint _) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoTex(uint _) => (0, 0, 0); // ── Test 1: Type 7 → UiMeter ───────────────────────────────────────────── @@ -335,7 +334,7 @@ public class DatWidgetFactoryTests public void Create_buildsUiItemList_forItemListClassId() { var info = new AcDream.App.UI.Layout.ElementInfo { Id = 0x100001A7u, Type = 0x10000031u, Width = 32, Height = 32 }; - var w = AcDream.App.UI.Layout.DatWidgetFactory.Create(info, _ => (GpuTextureSlot.Unassigned, 0, 0), null); + var w = AcDream.App.UI.Layout.DatWidgetFactory.Create(info, _ => (0u, 0, 0), null); Assert.IsType(w); } diff --git a/tests/AcDream.App.Tests/UI/Layout/EffectsUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/EffectsUiControllerTests.cs index 12ee12d7..86c0122f 100644 --- a/tests/AcDream.App.Tests/UI/Layout/EffectsUiControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/EffectsUiControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Core.Spells; @@ -7,7 +6,7 @@ namespace AcDream.App.Tests.UI.Layout; public sealed class EffectsUiControllerTests { - private static (GpuTextureSlot, int, int) NoTex(uint _) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoTex(uint _) => (0u, 0, 0); private static EffectRowTemplateFactory Templates() => new(FixtureLoader.LoadEffectRowTemplateInfos(), NoTex, defaultFont: null); @@ -48,7 +47,7 @@ public sealed class EffectsUiControllerTests [EffectsUiController.InfoTextId] = info, }); using EffectsUiController controller = EffectsUiController.Bind( - layout, spellbook, positive: true, () => 0d, NoTex, id => new GpuTextureSlot(id), + layout, spellbook, positive: true, () => 0d, NoTex, id => id, Templates(), "SELECT A SPELL")!; Assert.False(info.PreserveEndOnLayout); @@ -87,7 +86,7 @@ public sealed class EffectsUiControllerTests [EffectsUiController.ListId] = list, }); using EffectsUiController controller = EffectsUiController.Bind( - layout, spellbook, positive: true, () => 0d, NoTex, id => new GpuTextureSlot(id), + layout, spellbook, positive: true, () => 0d, NoTex, id => id, Templates(), "SELECT A SPELL")!; spellbook.OnEnchantmentAdded(new ActiveEnchantmentRecord( 42u, 1u, 60f, 1u, Bucket: 1u, SpellCategory: 100u)); @@ -130,7 +129,7 @@ public sealed class EffectsUiControllerTests info.Width = 96f; using EffectsUiController controller = EffectsUiController.Bind( - layout, spellbook, positive, () => 0d, NoTex, id => new GpuTextureSlot(id), + layout, spellbook, positive, () => 0d, NoTex, id => id, Templates(), "SELECT A SPELL")!; spellbook.OnEnchantmentAdded(new ActiveEnchantmentRecord( 42u, 1u, 60f, 1u, Bucket: 1u, SpellCategory: 42u)); @@ -174,7 +173,7 @@ public sealed class EffectsUiControllerTests [EffectsUiController.ListId] = list, }); using EffectsUiController controller = EffectsUiController.Bind( - layout, spellbook, positive: true, () => 0d, NoTex, id => new GpuTextureSlot(id), + layout, spellbook, positive: true, () => 0d, NoTex, id => id, Templates(), "SELECT A SPELL")!; spellbook.OnEnchantmentAdded(new ActiveEnchantmentRecord( @@ -226,7 +225,7 @@ public sealed class EffectsUiControllerTests int closes = 0; using EffectsUiController? controller = EffectsUiController.Bind( - layout, spellbook, positive, () => 0d, NoTex, id => new GpuTextureSlot(id), + layout, spellbook, positive, () => 0d, NoTex, id => id, Templates(), "SELECT A SPELL", () => closes++); Assert.NotNull(controller); diff --git a/tests/AcDream.App.Tests/UI/Layout/ExternalContainerControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/ExternalContainerControllerTests.cs index e4c6a8fd..bbcf5f93 100644 --- a/tests/AcDream.App.Tests/UI/Layout/ExternalContainerControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/ExternalContainerControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.App.World; @@ -32,7 +31,7 @@ public sealed class ExternalContainerControllerTests RetailWindowHandle handle = RetailWindowFrame.Mount( screen, root, - static _ => (GpuTextureSlot.Unassigned, 0, 0), + static _ => (0u, 0, 0), options); var frame = Assert.IsType(handle.OuterFrame); @@ -76,7 +75,7 @@ public sealed class ExternalContainerControllerTests var root = new TestElement { Width = 800f, Height = 110f }; var close = new UiButton( new ElementInfo { Id = ExternalContainerController.CloseButtonId, Type = 1 }, - static _ => (GpuTextureSlot.Unassigned, 0, 0)); + static _ => (0u, 0, 0)); var scrollbar = new UiScrollbar { Width = 784f, Height = 16f }; root.AddChild(Top); root.AddChild(Containers); @@ -95,7 +94,7 @@ public sealed class ExternalContainerControllerTests Window = RetailWindowFrame.Mount( Screen, root, - static _ => (GpuTextureSlot.Unassigned, 0, 0), + static _ => (0u, 0, 0), new RetailWindowFrame.Options { WindowName = WindowNames.ExternalContainer, @@ -130,8 +129,8 @@ public sealed class ExternalContainerControllerTests Selection, Interaction, Split, - static (_, _, _, _, _) => GpuTextureSlot.Unassigned, - static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + static (_, _, _, _, _) => 0u, + static (_, _, _, _, _) => 0u, Uses.Add, (item, container, placement) => Puts.Add((item, container, placement)), (item, container, placement, amount) => @@ -286,7 +285,7 @@ public sealed class ExternalContainerControllerTests h.Selection.Select(Item, SelectionChangeSource.Inventory); h.Split.Reset(5u, 2u); var source = new UiItemSlot { SourceKind = ItemDragSource.Inventory }; - source.SetItem(Item, GpuTextureSlot.Unassigned); + source.SetItem(Item, 0u); var payload = new ItemDragPayload(Item, ItemDragSource.Inventory, 0, source); UiItemSlot target = h.Contents.GetItem(0)!; @@ -305,7 +304,7 @@ public sealed class ExternalContainerControllerTests h.Objects.AddOrUpdate(new ClientObject { ObjectId = Item, Type = ItemType.Misc }); h.Objects.MoveItem(Item, Player, 0); var source = new UiItemSlot { SourceKind = ItemDragSource.Inventory }; - source.SetItem(Item, GpuTextureSlot.Unassigned); + source.SetItem(Item, 0u); Assert.True(h.Interaction.PlaceWorldItemInBackpack(0x70000A01u)); h.Controller.HandleDropRelease( @@ -334,7 +333,7 @@ public sealed class ExternalContainerControllerTests h.Selection.Select(Item, SelectionChangeSource.Inventory); h.Split.Reset(5u, 2u); var source = new UiItemSlot { SourceKind = ItemDragSource.Inventory }; - source.SetItem(Item, GpuTextureSlot.Unassigned); + source.SetItem(Item, 0u); Assert.True(h.Interaction.PlaceWorldItemInBackpack(0x70000A02u)); h.Controller.HandleDropRelease( @@ -356,7 +355,7 @@ public sealed class ExternalContainerControllerTests h.Objects.AddOrUpdate(new ClientObject { ObjectId = Item, Type = ItemType.Misc }); h.Objects.MoveItem(Item, Player, 0); var source = new UiItemSlot { SourceKind = ItemDragSource.Inventory }; - source.SetItem(Item, GpuTextureSlot.Unassigned); + source.SetItem(Item, 0u); h.Controller.HandleDropRelease( h.Contents, diff --git a/tests/AcDream.App.Tests/UI/Layout/FixtureLoader.cs b/tests/AcDream.App.Tests/UI/Layout/FixtureLoader.cs index d284fe46..c3897b13 100644 --- a/tests/AcDream.App.Tests/UI/Layout/FixtureLoader.cs +++ b/tests/AcDream.App.Tests/UI/Layout/FixtureLoader.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System.IO; using System.Text.Json; using AcDream.App.UI.Layout; @@ -10,7 +9,7 @@ namespace AcDream.App.Tests.UI.Layout; /// no dats required. Fixtures were generated from the real portal.dat and /// serialized with . /// -internal static class FixtureLoader +public static class FixtureLoader { private static readonly JsonSerializerOptions _opts = new() { @@ -27,7 +26,7 @@ internal static class FixtureLoader public static ImportedLayout LoadVitals() { var root = LoadVitalsInfos(); - return LayoutImporter.Build(root, _ => (GpuTextureSlot.Unassigned, 0, 0), null); + return LayoutImporter.Build(root, _ => (0u, 0, 0), null); } /// @@ -47,7 +46,7 @@ internal static class FixtureLoader /// conformance checks on tree structure and resolved types. /// public static ImportedLayout LoadChat() - => LayoutImporter.Build(LoadChatInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadChatInfos(), _ => (0u, 0, 0), null); /// /// Deserializes the committed chat_21000006.json fixture into a raw @@ -60,44 +59,44 @@ internal static class FixtureLoader /// Builds the committed retail radar LayoutDesc 0x21000074 fixture. public static ImportedLayout LoadRadar() - => LayoutImporter.Build(LoadRadarInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadRadarInfos(), _ => (0u, 0, 0), null); /// Returns the resolved ElementInfo tree for retail radar LayoutDesc 0x21000074. public static AcDream.App.UI.Layout.ElementInfo LoadRadarInfos() => LoadInfos("radar_21000074.json"); public static ImportedLayout LoadToolbar() - => LayoutImporter.Build(LoadToolbarInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadToolbarInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadToolbarInfos() => LoadInfos("toolbar_21000016.json"); public static ImportedLayout LoadInventory() - => LayoutImporter.Build(LoadInventoryInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadInventoryInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadInventoryInfos() => LoadInfos("inventory_21000023.json"); public static ImportedLayout LoadPaperdoll() - => LayoutImporter.Build(LoadPaperdollInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadPaperdollInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadPaperdollInfos() => LoadInfos("paperdoll_21000024.json"); public static ImportedLayout LoadCharacter() - => LayoutImporter.Build(LoadCharacterInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadCharacterInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadCharacterInfos() => LoadInfos("character_2100002E.json"); public static ImportedLayout LoadCombat() - => LayoutImporter.Build(LoadCombatInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadCombatInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadCombatInfos() => LoadInfos("combat_21000073.json"); public static ImportedLayout LoadSpellbook() - => LayoutImporter.Build(LoadSpellbookInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadSpellbookInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadSpellbookInfos() => LoadInfos("spellbook_21000034.json"); @@ -109,7 +108,7 @@ internal static class FixtureLoader => LoadInfos("component_row_21000033_10000467.json"); public static ImportedLayout LoadExamination() - => LayoutImporter.Build(LoadExaminationInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadExaminationInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadExaminationInfos() => LoadInfos("examine_2100006B_100005F2.json"); @@ -121,31 +120,31 @@ internal static class FixtureLoader => LoadInfos("examine_component_2100006B_1000032E.json"); public static ImportedLayout LoadPowerbar() - => LayoutImporter.Build(LoadPowerbarInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadPowerbarInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadPowerbarInfos() => LoadInfos("powerbar_21000072.json"); public static ImportedLayout LoadConfirmationDialog() - => LayoutImporter.Build(LoadConfirmationDialogInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadConfirmationDialogInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadConfirmationDialogInfos() => LoadInfos("dialogs_2100003C.json"); public static ImportedLayout LoadFpsDisplay() - => LayoutImporter.Build(LoadFpsDisplayInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadFpsDisplayInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadFpsDisplayInfos() => LoadInfos("smartbox_fps_2100000F.json"); public static ImportedLayout LoadPositiveEffects() - => LayoutImporter.Build(LoadPositiveEffectsInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadPositiveEffectsInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadPositiveEffectsInfos() => LoadInfos("effects_positive_2100001B.json"); public static ImportedLayout LoadNegativeEffects() - => LayoutImporter.Build(LoadNegativeEffectsInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadNegativeEffectsInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadNegativeEffectsInfos() => LoadInfos("effects_negative_2100001B.json"); @@ -155,31 +154,31 @@ internal static class FixtureLoader public static ImportedLayout LoadCharacterInformation() => LayoutImporter.Build( - LoadCharacterInformationInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + LoadCharacterInformationInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadCharacterInformationInfos() => LoadInfos("character_info_2100006E_10000183.json"); public static ImportedLayout LoadIndicators() - => LayoutImporter.Build(LoadIndicatorsInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadIndicatorsInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadIndicatorsInfos() => LoadInfos("indicators_21000071.json"); public static ImportedLayout LoadLinkStatus() - => LayoutImporter.Build(LoadLinkStatusInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadLinkStatusInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadLinkStatusInfos() => LoadInfos("link_status_2100001D.json"); public static ImportedLayout LoadVitae() - => LayoutImporter.Build(LoadVitaeInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadVitaeInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadVitaeInfos() => LoadInfos("vitae_21000020.json"); public static ImportedLayout LoadMiniGame() - => LayoutImporter.Build(LoadMiniGameInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadMiniGameInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadMiniGameInfos() => LoadInfos("mini_game_2100001E.json"); diff --git a/tests/AcDream.App.Tests/UI/Layout/IndicatorBarControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/IndicatorBarControllerTests.cs index 28bd062b..8e5c01f9 100644 --- a/tests/AcDream.App.Tests/UI/Layout/IndicatorBarControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/IndicatorBarControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Core.Items; @@ -10,7 +9,7 @@ namespace AcDream.App.Tests.UI.Layout; public sealed class IndicatorBarControllerTests { private const uint Player = 0x50000001u; - private static (GpuTextureSlot, int, int) NoTex(uint _) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoTex(uint _) => (0u, 0, 0); [Fact] public void AuthoredFixture_BuildsAllSevenRetailIndicatorButtons() diff --git a/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs index 695af4ab..9e7dcc3f 100644 --- a/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System.Collections.Generic; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -66,7 +65,7 @@ public class InventoryControllerTests StackSplitQuantityState? stackSplitQuantity = null, ItemInteractionController? itemInteraction = null) => InventoryController.Bind(layout, objects, () => Player, - iconIds: (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: (_, _, _, _, _) => 0u, strength: () => strength, datFont: null, ownerName: ownerName is null ? null : () => ownerName, sendUse: uses is null ? null : g => uses.Add(g), @@ -84,7 +83,7 @@ public class InventoryControllerTests private static UiButton MakeButton(uint id) { var info = new ElementInfo { Id = id, Type = 1 }; - return new UiButton(info, static _ => (GpuTextureSlot.Unassigned, 0, 0)) { Width = 16, Height = 16 }; + return new UiButton(info, static _ => (0u, 0, 0)) { Width = 16, Height = 16 }; } // Seed a side bag (a container) in the player's pack, plus optionally its own contents. @@ -380,7 +379,7 @@ public class InventoryControllerTests { var (layout, grid, containers, top, _, _, _, _) = BuildLayout(); InventoryController.Bind(layout, new ClientObjectTable(), () => Player, - iconIds: (_, _, _, _, _) => GpuTextureSlot.Unassigned, strength: () => 100, + iconIds: (_, _, _, _, _) => 0u, strength: () => 100, selection: new SelectionState(), datFont: null, contentsEmptySprite: 0x06004D20u, sideBagEmptySprite: 0x06005D9Cu, mainPackEmptySprite: 0x06005D9Cu); @@ -557,7 +556,7 @@ public class InventoryControllerTests nowMs: () => 1_000); InventoryController.Bind(layout, objects, () => Player, - iconIds: (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: (_, _, _, _, _) => 0u, strength: () => 100, selection: new SelectionState(), datFont: null, @@ -590,7 +589,7 @@ public class InventoryControllerTests objects.AddOrUpdate(new ClientObject { ObjectId = Player, IconId = 0x06001234u }); (ItemType type, uint icon)? mainPackCall = null; InventoryController.Bind(layout, objects, () => Player, - iconIds: (t, icon, _, _, _) => { if (icon == 0x0600127Eu) mainPackCall = (t, icon); return GpuTextureSlot.Unassigned; }, + iconIds: (t, icon, _, _, _) => { if (icon == 0x0600127Eu) mainPackCall = (t, icon); return 0u; }, strength: () => 100, selection: new SelectionState(), datFont: null); // Retail draws a constant backpack over the Container type-underlay (IconData::RenderIcons @@ -683,7 +682,7 @@ public class InventoryControllerTests var puts = new List<(uint item, uint container, int placement)>(); using var ctrl = Bind(layout, objects, puts: puts, selection: selection); var source = new UiItemSlot { SourceKind = ItemDragSource.Ground }; - source.SetItem(loot, GpuTextureSlot.Unassigned); + source.SetItem(loot, 0u); var payload = new ItemDragPayload(loot, ItemDragSource.Ground, 0, source); ctrl.HandleDropRelease(grid, grid.GetItem(1)!, payload); @@ -733,7 +732,7 @@ public class InventoryControllerTests layout, objects, () => Player, - iconIds: static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: static (_, _, _, _, _) => 0u, strength: () => 100, selection: new SelectionState(), datFont: null, @@ -782,7 +781,7 @@ public class InventoryControllerTests layout, objects, () => Player, - iconIds: static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: static (_, _, _, _, _) => 0u, strength: () => 100, selection: new SelectionState(), datFont: null, @@ -835,7 +834,7 @@ public class InventoryControllerTests layout, objects, () => Player, - iconIds: static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: static (_, _, _, _, _) => 0u, strength: () => 100, selection: new SelectionState(), datFont: null, @@ -843,7 +842,7 @@ public class InventoryControllerTests puts.Add((item, container, placement)), itemInteraction: interaction); var source = new UiItemSlot { SourceKind = ItemDragSource.Ground }; - source.SetItem(draggedLoot, GpuTextureSlot.Unassigned); + source.SetItem(draggedLoot, 0u); inventory.HandleDropRelease( grid, @@ -895,7 +894,7 @@ public class InventoryControllerTests layout, objects, () => Player, - iconIds: static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: static (_, _, _, _, _) => 0u, strength: () => 100, selection: new SelectionState(), datFont: null, @@ -903,7 +902,7 @@ public class InventoryControllerTests puts.Add((item, container, placement)), itemInteraction: interaction); var source = new UiItemSlot { SourceKind = ItemDragSource.Ground }; - source.SetItem(draggedLoot, GpuTextureSlot.Unassigned); + source.SetItem(draggedLoot, 0u); Assert.True(interaction.PlaceWorldItemInBackpack(directLoot)); Assert.Equal( @@ -960,7 +959,7 @@ public class InventoryControllerTests layout, objects, () => Player, - iconIds: static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: static (_, _, _, _, _) => 0u, strength: () => 100, selection: new SelectionState(), datFont: null, @@ -1023,7 +1022,7 @@ public class InventoryControllerTests layout, objects, () => Player, - iconIds: static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: static (_, _, _, _, _) => 0u, strength: () => 100, selection: new SelectionState(), datFont: null, @@ -1031,7 +1030,7 @@ public class InventoryControllerTests merges.Add((source, target, amount)), itemInteraction: interaction); var sourceCell = new UiItemSlot { SourceKind = ItemDragSource.Ground }; - sourceCell.SetItem(sourceStack, GpuTextureSlot.Unassigned); + sourceCell.SetItem(sourceStack, 0u); Assert.True(interaction.PlaceWorldItemInBackpack(pendingLoot)); UiItemSlot targetCell = Enumerable.Range(0, grid.GetNumUIItems()) @@ -1084,7 +1083,7 @@ public class InventoryControllerTests layout, objects, () => Player, - iconIds: static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: static (_, _, _, _, _) => 0u, strength: () => 100, selection: selection, datFont: null, @@ -1093,7 +1092,7 @@ public class InventoryControllerTests itemInteraction: interaction, stackSplitQuantity: splitQuantity); var sourceCell = new UiItemSlot { SourceKind = ItemDragSource.Ground }; - sourceCell.SetItem(sourceStack, GpuTextureSlot.Unassigned); + sourceCell.SetItem(sourceStack, 0u); Assert.True(interaction.PlaceWorldItemInBackpack(pendingLoot)); inventory.HandleDropRelease( @@ -1132,7 +1131,7 @@ public class InventoryControllerTests layout, objects, () => Player, - iconIds: static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: static (_, _, _, _, _) => 0u, strength: () => 100, selection: new SelectionState(), datFont: null, @@ -1173,7 +1172,7 @@ public class InventoryControllerTests layout, objects, () => Player, - iconIds: static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: static (_, _, _, _, _) => 0u, strength: () => 100, selection: new SelectionState(), datFont: null, @@ -1213,7 +1212,7 @@ public class InventoryControllerTests layout, objects, () => Player, - iconIds: static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: static (_, _, _, _, _) => 0u, strength: () => 100, selection: new SelectionState(), datFont: null, @@ -1240,7 +1239,7 @@ public class InventoryControllerTests var puts = new List<(uint item, uint container, int placement)>(); using var ctrl = Bind(layout, objects, puts: puts); var source = new UiItemSlot { SourceKind = ItemDragSource.Ground }; - source.SetItem(loot, GpuTextureSlot.Unassigned); + source.SetItem(loot, 0u); var payload = new ItemDragPayload(loot, ItemDragSource.Ground, 0, source); ctrl.HandleDropRelease(grid, grid.GetItem(1)!, payload); diff --git a/tests/AcDream.App.Tests/UI/Layout/InventoryFrameImportProbe.cs b/tests/AcDream.App.Tests/UI/Layout/InventoryFrameImportProbe.cs index 582e5643..32e074fe 100644 --- a/tests/AcDream.App.Tests/UI/Layout/InventoryFrameImportProbe.cs +++ b/tests/AcDream.App.Tests/UI/Layout/InventoryFrameImportProbe.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System; using System.IO; using AcDream.App.UI; @@ -44,7 +43,7 @@ public class InventoryFrameImportProbe if (datDir is null) return; // CI: no live dat — skip using var dats = new DatCollection(datDir, DatAccessType.Read); - var layout = LayoutImporter.Import(dats, Frame, _ => (GpuTextureSlot.Unassigned, 0, 0), null); + var layout = LayoutImporter.Import(dats, Frame, _ => (0u, 0, 0), null); Assert.NotNull(layout); // A representative spread across the slot grid (head, shield, the weapon composite, cloak, @@ -64,7 +63,7 @@ public class InventoryFrameImportProbe if (datDir is null) return; // CI: no live dat — skip (this is a smoke test) using var dats = new DatCollection(datDir, DatAccessType.Read); - var layout = LayoutImporter.Import(dats, Frame, _ => (GpuTextureSlot.Unassigned, 0, 0), null); + var layout = LayoutImporter.Import(dats, Frame, _ => (0u, 0, 0), null); Assert.NotNull(layout); var backdrop = layout!.FindElement(Backdrop); @@ -96,7 +95,7 @@ public class InventoryFrameImportProbe if (datDir is null) return; // CI: no live dat - skip using var dats = new DatCollection(datDir, DatAccessType.Read); - var layout = LayoutImporter.Import(dats, Frame, _ => (GpuTextureSlot.Unassigned, 0, 0), null); + var layout = LayoutImporter.Import(dats, Frame, _ => (0u, 0, 0), null); Assert.NotNull(layout); var close = layout!.FindElement(WindowChromeController.InventoryCloseButtonId); @@ -109,7 +108,7 @@ public class InventoryFrameImportProbe layout, new ClientObjectTable(), playerGuid: static () => 0u, - iconIds: static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: static (_, _, _, _, _) => 0u, strength: static () => 100, selection: new AcDream.Core.Selection.SelectionState(), datFont: null, diff --git a/tests/AcDream.App.Tests/UI/Layout/ItemCooldownUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/ItemCooldownUiControllerTests.cs index ea74c23e..6a5d2a81 100644 --- a/tests/AcDream.App.Tests/UI/Layout/ItemCooldownUiControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/ItemCooldownUiControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Core.Items; @@ -31,7 +30,7 @@ public sealed class ItemCooldownUiControllerTests double now = 112.5d; var root = new UiPanel(); var list = new UiItemList(); - list.Cell.SetItem(0x5001u, new GpuTextureSlot(99u)); + list.Cell.SetItem(0x5001u, 99u); root.AddChild(list); uint[] sprites = Enumerable.Range(1, 10) .Select(index => 0x06000000u + (uint)index) @@ -47,7 +46,7 @@ public sealed class ItemCooldownUiControllerTests Assert.Equal(sprites[5], list.Cell.ActiveCooldownSprite()); var future = new UiItemSlot(); - future.SetItem(0x5001u, new GpuTextureSlot(100u)); + future.SetItem(0x5001u, 100u); list.AddItem(future); Assert.Equal(sprites[5], future.ActiveCooldownSprite()); @@ -82,7 +81,7 @@ public sealed class ItemCooldownUiControllerTests var root = new UiPanel(); var window = new UiPanel(); var list = new UiItemList(); - list.Cell.SetItem(0x5001u, new GpuTextureSlot(99u)); + list.Cell.SetItem(0x5001u, 99u); root.AddChild(window); window.AddChild(list); uint[] sprites = Enumerable.Range(1, 10) @@ -118,7 +117,7 @@ public sealed class ItemCooldownUiControllerTests var spellbook = new Spellbook(); var root = new UiPanel(); var list = new UiItemList(); - list.Cell.SetItem(0x5001u, new GpuTextureSlot(99u)); + list.Cell.SetItem(0x5001u, 99u); root.AddChild(list); int clockReads = 0; ItemCooldownUiController controller = ItemCooldownUiController.Bind( diff --git a/tests/AcDream.App.Tests/UI/Layout/JumpPowerbarLayoutConformanceTests.cs b/tests/AcDream.App.Tests/UI/Layout/JumpPowerbarLayoutConformanceTests.cs index 131024d3..641fa9f9 100644 --- a/tests/AcDream.App.Tests/UI/Layout/JumpPowerbarLayoutConformanceTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/JumpPowerbarLayoutConformanceTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -10,7 +9,7 @@ public sealed class JumpPowerbarLayoutConformanceTests public void RetailFixture_UsesExactFloatyGeometryTrackAndJumpFill() { ElementInfo info = FixtureLoader.LoadPowerbarInfos(); - ImportedLayout layout = LayoutImporter.Build(info, _ => (GpuTextureSlot.Unassigned, 0, 0), null); + ImportedLayout layout = LayoutImporter.Build(info, _ => (0u, 0, 0), null); var meter = Assert.IsType( layout.FindElement(JumpPowerbarController.MeterId)); diff --git a/tests/AcDream.App.Tests/UI/Layout/LayoutImporterTests.cs b/tests/AcDream.App.Tests/UI/Layout/LayoutImporterTests.cs index 74ec3cc4..7bce7247 100644 --- a/tests/AcDream.App.Tests/UI/Layout/LayoutImporterTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/LayoutImporterTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -10,7 +9,7 @@ namespace AcDream.App.Tests.UI.Layout; /// public class LayoutImporterTests { - private static (GpuTextureSlot, int, int) NoTex(uint _) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoTex(uint _) => (0, 0, 0); // ── Test 1: Health meter element → UiMeter with correct rect ───────────── diff --git a/tests/AcDream.App.Tests/UI/Layout/PaperdollClickMapTests.cs b/tests/AcDream.App.Tests/UI/Layout/PaperdollClickMapTests.cs index e559da32..44fc107c 100644 --- a/tests/AcDream.App.Tests/UI/Layout/PaperdollClickMapTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/PaperdollClickMapTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Core.Items; @@ -60,7 +59,7 @@ public sealed class PaperdollClickMapTests ImportedLayout layout = Assert.IsType(LayoutImporter.Import( dats, 0x21000023u, - static _ => (GpuTextureSlot.Unassigned, 0, 0), + static _ => (0u, 0, 0), null)); UiElement dragMask = Assert.IsAssignableFrom( layout.FindElement(PaperdollController.DollDragMaskId)); diff --git a/tests/AcDream.App.Tests/UI/Layout/PaperdollControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/PaperdollControllerTests.cs index 49dfd1e3..ff74b09f 100644 --- a/tests/AcDream.App.Tests/UI/Layout/PaperdollControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/PaperdollControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System.Collections.Generic; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -67,7 +66,7 @@ public class PaperdollControllerTests systemMessage: systemMessages is null ? null : systemMessages.Add); configureInteraction?.Invoke(itemInteraction); return PaperdollController.Bind(layout, objects, () => Player, - iconIds: (_, _, _, _, _) => new GpuTextureSlot(0x1234u), + iconIds: (_, _, _, _, _) => 0x1234u, itemInteraction: itemInteraction, emptySlotSprite: emptySlot, selection: selection ?? new SelectionState(), diff --git a/tests/AcDream.App.Tests/UI/Layout/RadarControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/RadarControllerTests.cs index 0a10482e..2d18244d 100644 --- a/tests/AcDream.App.Tests/UI/Layout/RadarControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/RadarControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System.Numerics; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -133,7 +132,7 @@ public sealed class RadarControllerTests root.Children.Add(lockButton); root.Children.Add(Sprite(RadarController.DragButtonId, 87, 6, 27, 27, 0x060074C9u, type: 2)); - return LayoutImporter.Build(root, _ => (GpuTextureSlot.Unassigned, 0, 0), null); + return LayoutImporter.Build(root, _ => (0u, 0, 0), null); } private static ElementInfo Sprite( diff --git a/tests/AcDream.App.Tests/UI/Layout/RetailPanelUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/RetailPanelUiControllerTests.cs index c0c8c2ac..1ea87958 100644 --- a/tests/AcDream.App.Tests/UI/Layout/RetailPanelUiControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/RetailPanelUiControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -181,7 +180,7 @@ public sealed class RetailPanelUiControllerTests return RetailWindowFrame.Mount( root, content, - _ => (GpuTextureSlot.Unassigned, 0, 0), + _ => (0u, 0, 0), new RetailWindowFrame.Options { WindowName = name, diff --git a/tests/AcDream.App.Tests/UI/Layout/RetailWindowFrameTests.cs b/tests/AcDream.App.Tests/UI/Layout/RetailWindowFrameTests.cs index a7c90025..3d670338 100644 --- a/tests/AcDream.App.Tests/UI/Layout/RetailWindowFrameTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/RetailWindowFrameTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -6,7 +5,7 @@ namespace AcDream.App.Tests.UI.Layout; public sealed class RetailWindowFrameTests { - private static (GpuTextureSlot, int, int) NoTex(uint _) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoTex(uint _) => (0u, 0, 0); [Theory] [InlineData(9, 459f, 288f)] diff --git a/tests/AcDream.App.Tests/UI/Layout/SelectedObjectControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/SelectedObjectControllerTests.cs index 3563b1fd..57b3bca3 100644 --- a/tests/AcDream.App.Tests/UI/Layout/SelectedObjectControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/SelectedObjectControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System; using System.Collections.Generic; using System.Linq; @@ -53,7 +52,7 @@ public class SelectedObjectControllerTests ["StackedItemSelected"] = (0x06004CF4u, 3), }, }; - var overlayEl = new UiDatElement(overlayInfo, _ => (GpuTextureSlot.Unassigned, 0, 0)); + var overlayEl = new UiDatElement(overlayInfo, _ => (0u, 0, 0)); dict[SelectedObjectController.OverlayId] = overlayEl; root.AddChild(overlayEl); diff --git a/tests/AcDream.App.Tests/UI/Layout/SpellExamineComponentTemplateFactoryTests.cs b/tests/AcDream.App.Tests/UI/Layout/SpellExamineComponentTemplateFactoryTests.cs index efb57f75..bccff893 100644 --- a/tests/AcDream.App.Tests/UI/Layout/SpellExamineComponentTemplateFactoryTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/SpellExamineComponentTemplateFactoryTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -17,16 +16,16 @@ public sealed class SpellExamineComponentTemplateFactoryTests FixtureLoader.LoadExaminationComponentTemplateInfos(); var factory = new SpellExamineComponentTemplateFactory( template, - did => (new GpuTextureSlot(did + 1_000u), 32, 32), + did => (did + 1_000u, 32, 32), defaultFont: null); UiDatElement root = Assert.IsType( - factory.Create(new GpuTextureSlot(0xABCDu), owned)); + factory.Create(0xABCDu, owned)); UiDatElement missing = Assert.IsType( Assert.Single(root.Children)); Assert.Equal(SpellExamineComponentTemplateFactory.TemplateId, root.ElementId); - Assert.Equal(new GpuTextureSlot(0xABCDu), root.RuntimeImageTexture); + Assert.Equal(0xABCDu, root.RuntimeImageTexture); Assert.Empty(root.Children.OfType()); Assert.Equal( SpellExamineComponentTemplateFactory.MissingOverlayId, diff --git a/tests/AcDream.App.Tests/UI/Layout/SpellbookWindowControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/SpellbookWindowControllerTests.cs index 9b6f0b21..0c431349 100644 --- a/tests/AcDream.App.Tests/UI/Layout/SpellbookWindowControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/SpellbookWindowControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.Spells; using AcDream.Content; using AcDream.App.UI; @@ -31,7 +30,7 @@ public sealed class SpellbookWindowControllerTests { ImportedLayout layout = LayoutImporter.Build( FixtureLoader.LoadSpellbookInfos(), - _ => (GpuTextureSlot.Unassigned, 0, 0), + _ => (0u, 0, 0), datFont: null, stringResolve: value => value.StringId switch { @@ -82,7 +81,7 @@ public sealed class SpellbookWindowControllerTests { ImportedLayout layout = LayoutImporter.Build( FixtureLoader.LoadSpellbookInfos(), - _ => (GpuTextureSlot.Unassigned, 0, 0), + _ => (0u, 0, 0), datFont: null, stringResolve: value => value.StringId == 164868556u ? "Delete" : "Filter"); @@ -303,7 +302,7 @@ public sealed class SpellbookWindowControllerTests selection.Select(objectId, SelectionChangeSource.Inventory); }, setDesiredComponent: (component, amount) => desired.Add((component, amount)), - resolveComponentIcon: icon => new GpuTextureSlot(icon + 0x1000u))!; + resolveComponentIcon: icon => icon + 0x1000u)!; controller.ShowPage(SpellbookWindowPage.Components); UiItemList list = Assert.IsType( @@ -340,7 +339,7 @@ public sealed class SpellbookWindowControllerTests UiElement iconHost = Assert.IsAssignableFrom(copper.Content.FindElement( ComponentBookTemplateFactory.IconId)); Assert.Equal( - new GpuTextureSlot(0x06001100u), + 0x06001100u, Assert.IsType(Assert.Single(iconHost.Children)).Texture); copper.OnEvent(new UiEvent(0u, copper, UiEventType.Click)); @@ -402,7 +401,7 @@ public sealed class SpellbookWindowControllerTests SelectionState? selection = null, Action? selectObject = null, Action? setDesiredComponent = null, - Func? resolveComponentIcon = null) + Func? resolveComponentIcon = null) => SpellbookWindowController.Bind( layout, spellbook, @@ -410,8 +409,8 @@ public sealed class SpellbookWindowControllerTests () => 1u, components ?? new Dictionary(), selection ?? new SelectionState(), - spellId => new GpuTextureSlot(spellId + 1000u), - resolveComponentIcon ?? (iconId => new GpuTextureSlot(iconId)), + spellId => spellId + 1000u, + resolveComponentIcon ?? (iconId => iconId), spellId => spellId == 103u ? 8 : 1, selectObject ?? (_ => { }), addFavorite ?? (_ => { }), @@ -431,7 +430,7 @@ public sealed class SpellbookWindowControllerTests new ComponentBookTemplateFactory( FixtureLoader.LoadComponentCategoryTemplateInfos(), FixtureLoader.LoadComponentRowTemplateInfos(), - _ => (GpuTextureSlot.Unassigned, 0, 0), + _ => (0u, 0, 0), defaultFont: null), RowStyle, rowFont: null); diff --git a/tests/AcDream.App.Tests/UI/Layout/SpellcastingUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/SpellcastingUiControllerTests.cs index f31136b8..69464a70 100644 --- a/tests/AcDream.App.Tests/UI/Layout/SpellcastingUiControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/SpellcastingUiControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.Spells; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -11,7 +10,7 @@ namespace AcDream.App.Tests.UI.Layout; public sealed class SpellcastingUiControllerTests { - private static (GpuTextureSlot, int, int) NoTex(uint _) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoTex(uint _) => (0u, 0, 0); [Fact] public void ImportedFixture_UsesRetailEmptyCells_ShortcutDigits_AndCompleteCastButton() @@ -161,8 +160,8 @@ public sealed class SpellcastingUiControllerTests Assert.True(host.Visible); UiCatalogSlot slot = Assert.IsType(host.Children[^1]); Assert.Equal(2u, slot.EntryId); - Assert.Equal(new GpuTextureSlot(2670u), slot.CatalogIconTexture); - Assert.Equal(new GpuTextureSlot(2u), slot.CatalogOverlayTexture); + Assert.Equal(2670u, slot.CatalogIconTexture); + Assert.Equal(2u, slot.CatalogOverlayTexture); slot.OnEvent(new UiEvent(0, slot, UiEventType.Click)); var cast = Assert.IsType( @@ -339,8 +338,8 @@ public sealed class SpellcastingUiControllerTests new NoopSpellCastOperations()); return SpellcastingUiController.Bind( layout, spellbook, casting, objects, () => 1u, - spellId => new GpuTextureSlot(spellId), - item => new GpuTextureSlot(item.ObjectId), + spellId => spellId, + item => item.ObjectId, useItem, selectionState, (tab, position, spellId) => diff --git a/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs index e4da3eef..31fb6e31 100644 --- a/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System; using System.Collections.Generic; using AcDream.App.UI; @@ -67,7 +66,7 @@ public class ToolbarControllerTests void AddSlot(uint id) { - var list = new UiItemList(_ => (GpuTextureSlot.Unassigned, 0, 0)) { Width = 32, Height = 32 }; + var list = new UiItemList(_ => (0u, 0, 0)) { Width = 32, Height = 32 }; dict[id] = list; slots[id] = list; root.AddChild(list); } @@ -102,7 +101,7 @@ public class ToolbarControllerTests }, }; } - var button = new UiButton(info, _ => (GpuTextureSlot.Unassigned, 0, 0)) { Width = 32, Height = 32 }; + var button = new UiButton(info, _ => (0u, 0, 0)) { Width = 32, Height = 32 }; dict[id] = button; root.AddChild(button); } @@ -118,10 +117,10 @@ public class ToolbarControllerTests { new(Index: 0, ObjectId: 0x5001u, SpellId: 0) }; ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_,_,_,_,_) => new GpuTextureSlot(0x77u), useItem: _ => { }); + iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { }); Assert.Equal(0x5001u, slots[Row1[0]].Cell.ItemId); - Assert.Equal(new GpuTextureSlot(0x77u), slots[Row1[0]].Cell.IconTexture); + Assert.Equal(0x77u, slots[Row1[0]].Cell.IconTexture); Assert.Equal(0u, slots[Row1[1]].Cell.ItemId); // others empty } @@ -144,7 +143,7 @@ public class ToolbarControllerTests layout, repo, Store(shortcuts), - iconIds: (_, _, _, _, _) => new GpuTextureSlot(0x77u), + iconIds: (_, _, _, _, _) => 0x77u, useItem: _ => { }); Assert.Equal(0x5001u, slots[Row1[0]].Cell.ItemId); @@ -162,7 +161,7 @@ public class ToolbarControllerTests { new(Index: 2, ObjectId: 0x5002u, SpellId: 0) }; ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_,_,_,_,_) => new GpuTextureSlot(0x88u), useItem: _ => { }); + iconIds: (_,_,_,_,_) => 0x88u, useItem: _ => { }); Assert.Equal(0u, slots[Row1[2]].Cell.ItemId); // not bound yet repo.AddOrUpdate(new ClientObject { ObjectId = 0x5002u, WeenieClassId = 1u, IconId = 0x06005678u }); @@ -178,7 +177,7 @@ public class ToolbarControllerTests var shortcuts = new List { new(Index: 2, ObjectId: 0x5002u, SpellId: 0) }; var controller = ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_,_,_,_,_) => new GpuTextureSlot(0x88u), useItem: _ => { }); + iconIds: (_,_,_,_,_) => 0x88u, useItem: _ => { }); controller.Dispose(); controller.Dispose(); @@ -204,7 +203,7 @@ public class ToolbarControllerTests var selection = new SelectionState(); ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_,_,_,_,_) => new GpuTextureSlot(0x77u), + iconIds: (_,_,_,_,_) => 0x77u, useItem: g => used = g, selection: selection); UiItemSlot cell = slots[Row1[0]].Cell; @@ -227,7 +226,7 @@ public class ToolbarControllerTests var ctrl = ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }); + iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }); ctrl.BindPanelButtons( panelId => panelId is RetailPanelCatalog.Inventory or RetailPanelCatalog.Character, toggles.Add); @@ -278,7 +277,7 @@ public class ToolbarControllerTests var ctrl = ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, + iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }, itemInteraction: interaction); ctrl.BindPanelButtons( @@ -305,7 +304,7 @@ public class ToolbarControllerTests var puts = new List<(uint Item, uint Container, int Placement)>(); ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: (_, _, _, _, _) => 0u, useItem: _ => { }, playerGuid: () => player, sendPutItemInContainer: (i, c, p) => puts.Add((i, c, p))); @@ -345,7 +344,7 @@ public class ToolbarControllerTests layout, repo, new ShortcutStore(), - iconIds: static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: static (_, _, _, _, _) => 0u, useItem: static _ => { }, itemInteraction: interaction, playerGuid: () => player, @@ -395,7 +394,7 @@ public class ToolbarControllerTests layout, repo, new ShortcutStore(), - iconIds: static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: static (_, _, _, _, _) => 0u, useItem: static _ => { }, itemInteraction: interaction, playerGuid: () => player, @@ -427,7 +426,7 @@ public class ToolbarControllerTests var puts = new List<(uint Item, uint Container, int Placement)>(); ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: (_, _, _, _, _) => 0u, useItem: _ => { }, playerGuid: () => player, sendPutItemInContainer: (i, c, p) => puts.Add((i, c, p))); @@ -448,7 +447,7 @@ public class ToolbarControllerTests var repo = new ClientObjectTable(); var ctrl = ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }); + iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }); var inventoryButton = (UiButton)layout.FindElement(InventoryButtonId)!; var characterButton = (UiButton)layout.FindElement(CharacterButtonId)!; @@ -525,7 +524,7 @@ public class ToolbarControllerTests layout, repo, new ShortcutStore(), - iconIds: (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: (_, _, _, _, _) => 0u, useItem: _ => { }, itemInteraction: interaction, selectedObjectId: () => selected); @@ -583,7 +582,7 @@ public class ToolbarControllerTests layout, repo, new ShortcutStore(), - iconIds: (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: (_, _, _, _, _) => 0u, useItem: _ => { }, itemInteraction: interaction, selectedObjectId: () => selection.SelectedObjectId ?? 0u, @@ -649,7 +648,7 @@ public class ToolbarControllerTests layout, repo, new ShortcutStore(), - iconIds: (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: (_, _, _, _, _) => 0u, useItem: _ => { }, itemInteraction: interaction, selectedObjectId: () => selection.SelectedObjectId ?? 0u, @@ -692,7 +691,7 @@ public class ToolbarControllerTests layout, repo, new ShortcutStore(), - iconIds: (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: (_, _, _, _, _) => 0u, useItem: _ => { }, playerGuid: () => player); var ammoButton = (UiButton)layout.FindElement(0x10000194u)!; @@ -731,7 +730,7 @@ public class ToolbarControllerTests layout, repo, new ShortcutStore(), - iconIds: (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: (_, _, _, _, _) => 0u, useItem: _ => { }, playerGuid: () => player); var ammoButton = (UiButton)layout.FindElement(0x10000194u)!; @@ -753,7 +752,7 @@ public class ToolbarControllerTests layout, repo, new ShortcutStore(), - iconIds: (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: (_, _, _, _, _) => 0u, useItem: _ => { }, toggleCombat: () => toggles++); @@ -780,7 +779,7 @@ public class ToolbarControllerTests layout, repo, Store(shortcuts), - iconIds: (_, _, _, _, _) => new GpuTextureSlot(1u), + iconIds: (_, _, _, _, _) => 1u, useItem: id => used = id, selectItem: id => selected = id); @@ -817,7 +816,7 @@ public class ToolbarControllerTests layout, repo, Store(shortcuts), - iconIds: (_, _, _, _, _) => new GpuTextureSlot(1u), + iconIds: (_, _, _, _, _) => 1u, useItem: _ => { }, itemInteraction: interaction, selectItem: id => selection.Select(id, SelectionChangeSource.Toolbar), @@ -871,7 +870,7 @@ public class ToolbarControllerTests layout, repo, Store(shortcuts), - iconIds: (_, _, _, _, _) => new GpuTextureSlot(1u), + iconIds: (_, _, _, _, _) => 1u, useItem: _ => { }, itemInteraction: interaction); @@ -910,7 +909,7 @@ public class ToolbarControllerTests layout, repo, new ShortcutStore(), - iconIds: (_, _, _, _, _) => new GpuTextureSlot(1u), + iconIds: (_, _, _, _, _) => 1u, useItem: _ => { }, itemInteraction: interaction, sendAddShortcut: entry => sends.Add(((uint)entry.Index, entry.ObjectId))); @@ -936,7 +935,7 @@ public class ToolbarControllerTests ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }); + iconIds: (_,_,_,_,_) =>0u, useItem: _ => { }); // Only peace indicator (index 0 = 0x10000192) is visible. Assert.True (indicators[0x10000192u].Visible, "peace indicator should be visible after bind"); @@ -956,7 +955,7 @@ public class ToolbarControllerTests var ctrl = ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }); + iconIds: (_,_,_,_,_) =>0u, useItem: _ => { }); ctrl.SetCombatMode(CombatMode.Melee); @@ -978,7 +977,7 @@ public class ToolbarControllerTests ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }, + iconIds: (_,_,_,_,_) =>0u, useItem: _ => { }, combatState: combat); // Initially NonCombat after bind. @@ -1017,7 +1016,7 @@ public class ToolbarControllerTests ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }, + iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }, regularDigits: FakeRegular, ghostedDigits: FakeGhosted); // Top row: ShortcutNum == slot index, ghosted == false. @@ -1046,7 +1045,7 @@ public class ToolbarControllerTests var repo = new ClientObjectTable(); var ctrl = ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }, + iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }, regularDigits: FakeRegular, ghostedDigits: FakeGhosted); ctrl.SetCombatMode(mode); @@ -1056,7 +1055,7 @@ public class ToolbarControllerTests var cell = slots[Row1[i]].Cell; Assert.Equal(i, cell.ShortcutNum); Assert.False(cell.ShortcutGhosted, $"top-row slot {i} should be regular in {mode}"); - cell.SetItem((uint)(0x5000 + i), new GpuTextureSlot(0x99u)); + cell.SetItem((uint)(0x5000 + i), 0x99u); Assert.Same(FakeRegular, cell.ActiveDigitArray()); } // Bottom row still has no number. @@ -1074,13 +1073,13 @@ public class ToolbarControllerTests var repo = new ClientObjectTable(); var ctrl = ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }, + iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }, regularDigits: FakeRegular, ghostedDigits: FakeGhosted); ctrl.SetCombatMode(CombatMode.Magic); foreach (var id in Row1) { - slots[id].Cell.SetItem(id, new GpuTextureSlot(0x99u)); + slots[id].Cell.SetItem(id, 0x99u); Assert.True(slots[id].Cell.ShortcutGhosted); Assert.Same(FakeGhosted, slots[id].Cell.ActiveDigitArray()); } @@ -1106,7 +1105,7 @@ public class ToolbarControllerTests ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }, + iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }, regularDigits: FakeRegular, ghostedDigits: FakeGhosted); foreach (var id in Row1) @@ -1128,7 +1127,7 @@ public class ToolbarControllerTests ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }, + iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }, regularDigits: FakeRegular, ghostedDigits: FakeGhosted, emptyDigits: FakeEmpty); foreach (var id in Row1) @@ -1149,7 +1148,7 @@ public class ToolbarControllerTests ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }, + iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }, regularDigits: FakeRegular, ghostedDigits: FakeGhosted, emptyDigits: null); foreach (var id in Row1) @@ -1175,7 +1174,7 @@ public class ToolbarControllerTests int iconCallCount = 0; ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_,_,_,_,_) => { iconCallCount++; return new GpuTextureSlot(0x77u); }, useItem: _ => { }); + iconIds: (_,_,_,_,_) => { iconCallCount++; return 0x77u; }, useItem: _ => { }); int callsAfterBind = iconCallCount; // 1 call from initial Populate @@ -1200,7 +1199,7 @@ public class ToolbarControllerTests int iconCallCount = 0; ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_,_,_,_,_) => { iconCallCount++; return new GpuTextureSlot(0x99u); }, useItem: _ => { }); + iconIds: (_,_,_,_,_) => { iconCallCount++; return 0x99u; }, useItem: _ => { }); Assert.Equal(0, iconCallCount); // not called — item absent during initial Populate Assert.Equal(0u, slots[Row1[1]].Cell.ItemId); @@ -1226,7 +1225,7 @@ public class ToolbarControllerTests { new(Index: 3, ObjectId: 0x5004u, SpellId: 0) }; ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_,_,_,_,_) => new GpuTextureSlot(0xAAu), useItem: _ => { }); + iconIds: (_,_,_,_,_) => 0xAAu, useItem: _ => { }); Assert.Equal(0x5004u, slots[Row1[3]].Cell.ItemId); // bound @@ -1253,7 +1252,7 @@ public class ToolbarControllerTests int iconCallCount = 0; ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_,_,_,_,_) => { iconCallCount++; return new GpuTextureSlot(0xBBu); }, useItem: _ => { }); + iconIds: (_,_,_,_,_) => { iconCallCount++; return 0xBBu; }, useItem: _ => { }); int callsAfterBind = iconCallCount; // 1 call for the shortcut item @@ -1275,7 +1274,7 @@ public class ToolbarControllerTests var ctrl = ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }); + iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }); for (int i = 0; i < Row1.Length; i++) { @@ -1300,7 +1299,7 @@ public class ToolbarControllerTests var (layout, slots, _) = FakeToolbar(); var ctrl = ToolbarController.Bind(layout, new ClientObjectTable(), new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }); + iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }); var list = slots[Row1[0]]; var payload = new ItemDragPayload(0x5001u, ItemDragSource.Inventory, 0, new UiItemSlot()); @@ -1315,7 +1314,7 @@ public class ToolbarControllerTests var (layout, slots, _) = FakeToolbar(); var ctrl = ToolbarController.Bind(layout, new ClientObjectTable(), new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }); + iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }); var list = slots[Row1[0]]; var payload = new ItemDragPayload(0u, ItemDragSource.Inventory, 0, new UiItemSlot()); Assert.Equal(ItemDragAcceptance.None, ctrl.OnDragOver(list, list.Cell, payload)); @@ -1344,7 +1343,7 @@ public class ToolbarControllerTests uint selected = 0; var ctrl = ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_,_,_,_,_) => new GpuTextureSlot(0x77u), useItem: _ => { }, + iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { }, sendAddShortcut: add, sendRemoveShortcut: rem, selectItem: item => selected = item, selectedObjectId: () => selected); Assert.Equal(0x5001u, slots[Row1[3]].Cell.ItemId); @@ -1369,7 +1368,7 @@ public class ToolbarControllerTests new(Index: 5, ObjectId: 0x5002u, SpellId: 0) }; var (adds, removes) = NewSpies(out var add, out var rem); var ctrl = ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_,_,_,_,_) => new GpuTextureSlot(0x77u), useItem: _ => { }, + iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { }, sendAddShortcut: add, sendRemoveShortcut: rem); var payload = new ItemDragPayload(0x5001u, ItemDragSource.ShortcutBar, 3, slots[Row1[3]].Cell); @@ -1398,7 +1397,7 @@ public class ToolbarControllerTests var (adds, _) = NewSpies(out var add, out var remove); var controller = ToolbarController.Bind( layout, repo, Store(shortcuts), - iconIds: (_, _, _, _, _) => new GpuTextureSlot(1u), + iconIds: (_, _, _, _, _) => 1u, useItem: _ => { }, sendAddShortcut: add, sendRemoveShortcut: remove); @@ -1421,7 +1420,7 @@ public class ToolbarControllerTests { new(Index: 3, ObjectId: 0x5001u, SpellId: 0) }; var (adds, removes) = NewSpies(out var add, out var rem); var ctrl = ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_,_,_,_,_) => new GpuTextureSlot(0x77u), useItem: _ => { }, + iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { }, sendAddShortcut: add, sendRemoveShortcut: rem); var payload = new ItemDragPayload(0x5001u, ItemDragSource.ShortcutBar, 3, slots[Row1[3]].Cell); @@ -1447,7 +1446,7 @@ public class ToolbarControllerTests }; var wire = new List(); var ctrl = ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_, _, _, _, _) => new GpuTextureSlot(1u), + iconIds: (_, _, _, _, _) => 1u, useItem: _ => { }, sendAddShortcut: entry => wire.Add($"add:{entry.Index}:{entry.ObjectId:X8}:{entry.SpellId:X8}"), sendRemoveShortcut: slot => wire.Add($"remove:{slot}")); @@ -1480,7 +1479,7 @@ public class ToolbarControllerTests }; var wire = new List(); var ctrl = ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_, _, _, _, _) => new GpuTextureSlot(1u), + iconIds: (_, _, _, _, _) => 1u, useItem: _ => { }, sendAddShortcut: entry => wire.Add($"add:{entry.Index}:{entry.ObjectId:X8}:{entry.SpellId:X8}"), sendRemoveShortcut: slot => wire.Add($"remove:{slot}")); @@ -1505,7 +1504,7 @@ public class ToolbarControllerTests { new(Index: 3, ObjectId: 0x5001u, SpellId: 0) }; var (adds, removes) = NewSpies(out var add, out var rem); var ctrl = ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_,_,_,_,_) => new GpuTextureSlot(0x77u), useItem: _ => { }, + iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { }, sendAddShortcut: add, sendRemoveShortcut: rem); var payload = new ItemDragPayload(0x5001u, ItemDragSource.ShortcutBar, 3, slots[Row1[3]].Cell); @@ -1524,7 +1523,7 @@ public class ToolbarControllerTests var (layout, slots, _) = FakeToolbar(); ToolbarController.Bind(layout, new ClientObjectTable(), new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }); + iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }); Assert.Equal(0x060011FAu, slots[Row1[0]].Cell.DragAcceptSprite); // green cross, not the ring F9 } } diff --git a/tests/AcDream.App.Tests/UI/Layout/UiDatElementTests.cs b/tests/AcDream.App.Tests/UI/Layout/UiDatElementTests.cs index dc20149b..5bf05158 100644 --- a/tests/AcDream.App.Tests/UI/Layout/UiDatElementTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/UiDatElementTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; namespace AcDream.App.Tests.UI.Layout; @@ -11,7 +10,7 @@ public class UiDatElementTests var info = new ElementInfo(); info.StateMedia[""] = (0x06000001, 1); // DirectState (DrawMode Normal=1) info.StateMedia["ShowDetail"] = (0x06000002, 3); // named (Alphablend=3) - var e = new UiDatElement(info, _ => (GpuTextureSlot.Unassigned, 0, 0)) { ActiveState = "ShowDetail" }; + var e = new UiDatElement(info, _ => (0, 0, 0)) { ActiveState = "ShowDetail" }; Assert.Equal(0x06000002u, e.ActiveMedia().File); Assert.Equal(3, e.ActiveMedia().DrawMode); e.ActiveState = ""; @@ -22,7 +21,7 @@ public class UiDatElementTests [Fact] public void ActiveMedia_NoMedia_ReturnsZero() { - var e = new UiDatElement(new ElementInfo(), _ => (GpuTextureSlot.Unassigned, 0, 0)); + var e = new UiDatElement(new ElementInfo(), _ => (0, 0, 0)); Assert.Equal(0u, e.ActiveMedia().File); Assert.Equal(0, e.ActiveMedia().DrawMode); } @@ -32,7 +31,7 @@ public class UiDatElementTests { var info = new ElementInfo(); info.StateMedia[""] = (0x06000005, 1); - var e = new UiDatElement(info, _ => (GpuTextureSlot.Unassigned, 0, 0)) { ActiveState = "NoSuchState" }; + var e = new UiDatElement(info, _ => (0, 0, 0)) { ActiveState = "NoSuchState" }; Assert.Equal(0x06000005u, e.ActiveMedia().File); } @@ -50,7 +49,7 @@ public class UiDatElementTests info.StateMedia["Normal"] = (0x0000AAAAu, 1); info.StateMedia["Hover"] = (0x0000BBBBu, 1); - var e = new UiDatElement(info, _ => (GpuTextureSlot.Unassigned, 0, 0)); + var e = new UiDatElement(info, _ => (0, 0, 0)); // Should have defaulted to "Normal" state. Assert.Equal(0x0000AAAAu, e.ActiveMedia().File); @@ -68,7 +67,7 @@ public class UiDatElementTests info.StateMedia["Maximized"] = (0x0000CCCCu, 1); info.StateMedia["Normal"] = (0x0000DDDDu, 1); - var e = new UiDatElement(info, _ => (GpuTextureSlot.Unassigned, 0, 0)); + var e = new UiDatElement(info, _ => (0, 0, 0)); // DefaultStateName "Minimized" wins over "Normal" implicit default. Assert.Equal(0x0000BBBBu, e.ActiveMedia().File); @@ -84,7 +83,7 @@ public class UiDatElementTests var info = new ElementInfo(); info.StateMedia[""] = (0x06007777u, 1); // DirectState only (e.g. vitals chrome corner) - var e = new UiDatElement(info, _ => (GpuTextureSlot.Unassigned, 0, 0)); + var e = new UiDatElement(info, _ => (0, 0, 0)); // No DefaultStateName, no "Normal" state → ActiveState stays "" (DirectState). Assert.Equal(0x06007777u, e.ActiveMedia().File); @@ -101,7 +100,7 @@ public class UiDatElementTests Image = new UiImageMedia(0x06000009u, 3), }; info.StateMedia["ShowDetail"] = (0x06000009u, 3); - var element = new UiDatElement(info, _ => (GpuTextureSlot.Unassigned, 0, 0)) + var element = new UiDatElement(info, _ => (0u, 0, 0)) { Left = info.X, Top = info.Y, @@ -122,7 +121,7 @@ public class UiDatElementTests { var info = new ElementInfo { DefaultStateName = "Normal" }; info.StateMedia["Normal"] = (0x06000001u, 1); - var element = new UiDatElement(info, _ => (GpuTextureSlot.Unassigned, 0, 0)); + var element = new UiDatElement(info, _ => (0u, 0, 0)); Assert.False(element.TrySetRetailState(RetailUiStateIds.ShowDetail)); Assert.Equal("Normal", element.ActiveState); diff --git a/tests/AcDream.App.Tests/UI/Layout/UiViewportFactoryTests.cs b/tests/AcDream.App.Tests/UI/Layout/UiViewportFactoryTests.cs index dbcc3d32..03a4873c 100644 --- a/tests/AcDream.App.Tests/UI/Layout/UiViewportFactoryTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/UiViewportFactoryTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -6,7 +5,7 @@ namespace AcDream.App.Tests.UI.Layout; public class UiViewportFactoryTests { - private static (GpuTextureSlot, int, int) NoTex(uint _) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoTex(uint _) => (0, 0, 0); [Fact] public void Factory_builds_UiViewport_for_dat_type_0xD() diff --git a/tests/AcDream.App.Tests/UI/MarkupDocumentTests.cs b/tests/AcDream.App.Tests/UI/MarkupDocumentTests.cs index 815aabec..d45aa374 100644 --- a/tests/AcDream.App.Tests/UI/MarkupDocumentTests.cs +++ b/tests/AcDream.App.Tests/UI/MarkupDocumentTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; namespace AcDream.App.Tests.UI; @@ -23,7 +22,7 @@ public class MarkupDocumentTests " " + ""; - var panel = MarkupDocument.Build(xml, new FakeBinding(), _ => (new GpuTextureSlot(1), 32, 32)); + var panel = MarkupDocument.Build(xml, new FakeBinding(), _ => ((uint)1, 32, 32)); Assert.IsType(panel); Assert.Equal(10f, panel.Left); @@ -43,7 +42,7 @@ public class MarkupDocumentTests "" + " " + ""; - var panel = MarkupDocument.Build(xml, new FakeBinding(), _ => (new GpuTextureSlot(1), 32, 32)); + var panel = MarkupDocument.Build(xml, new FakeBinding(), _ => ((uint)1, 32, 32)); var meter = Assert.IsType(panel.Children[1]); Assert.Null(meter.Fill()); Assert.Null(meter.Label()); @@ -53,7 +52,7 @@ public class MarkupDocumentTests public void Build_ResizeAttrX_SetsHorizontalOnly() { const string xml = ""; - var panel = MarkupDocument.Build(xml, new object(), _ => (new GpuTextureSlot(1), 32, 32)); + var panel = MarkupDocument.Build(xml, new object(), _ => ((uint)1, 32, 32)); Assert.True(panel.ResizeX); Assert.False(panel.ResizeY); } @@ -66,7 +65,7 @@ public class MarkupDocumentTests "backleft=\"0x06001141\" backtile=\"0x06001140\" backright=\"0x0600113F\" " + "frontleft=\"0x06001131\" fronttile=\"0x06001132\" frontright=\"0x06001133\"/>" + ""; - var panel = MarkupDocument.Build(xml, new FakeBinding(), _ => (new GpuTextureSlot(7), 32, 32)); + var panel = MarkupDocument.Build(xml, new FakeBinding(), _ => ((uint)7, 32, 32)); var meter = Assert.IsType(panel.Children[1]); Assert.Equal(0x06001141u, meter.BackLeft); Assert.Equal(0x06001140u, meter.BackTile); diff --git a/tests/AcDream.App.Tests/UI/RetailUiAutomationProbeTests.cs b/tests/AcDream.App.Tests/UI/RetailUiAutomationProbeTests.cs index 899726a7..e1b0bc00 100644 --- a/tests/AcDream.App.Tests/UI/RetailUiAutomationProbeTests.cs +++ b/tests/AcDream.App.Tests/UI/RetailUiAutomationProbeTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System.Collections.Generic; using System.IO; using AcDream.App.UI; @@ -97,7 +96,7 @@ public sealed class RetailUiAutomationProbeTests var root = new UiRoot { Width = 240, Height = 120 }; var objects = new ClientObjectTable(); - var source = new UiItemList(_ => (new GpuTextureSlot(1u), 1, 1)) + var source = new UiItemList(_ => (1u, 1, 1)) { DatElementId = 0x10000010u, Left = 10, @@ -107,9 +106,9 @@ public sealed class RetailUiAutomationProbeTests }; source.Cell.SlotIndex = 0; source.Cell.SourceKind = ItemDragSource.Inventory; - source.Cell.SetItem(0x5001u, new GpuTextureSlot(0x99u)); + source.Cell.SetItem(0x5001u, 0x99u); - var target = new UiItemList(_ => (new GpuTextureSlot(1u), 1, 1)) + var target = new UiItemList(_ => (1u, 1, 1)) { DatElementId = 0x10000020u, Left = 70, diff --git a/tests/AcDream.App.Tests/UI/RetailUiInteractionFlowTests.cs b/tests/AcDream.App.Tests/UI/RetailUiInteractionFlowTests.cs index 62de7e12..44b0144c 100644 --- a/tests/AcDream.App.Tests/UI/RetailUiInteractionFlowTests.cs +++ b/tests/AcDream.App.Tests/UI/RetailUiInteractionFlowTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System.Collections.Generic; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -69,7 +68,7 @@ public sealed class RetailUiInteractionFlowTests height: 32); var slotsButton = new UiButton( new ElementInfo { Id = SlotsButtonId, Type = 1 }, - static _ => (GpuTextureSlot.Unassigned, 0, 0)) + static _ => (0u, 0, 0)) { DatElementId = SlotsButtonId, Left = 270, @@ -87,7 +86,7 @@ public sealed class RetailUiInteractionFlowTests }; var dollDragMask = new UiButton( new ElementInfo { Id = PaperdollController.DollDragMaskId, Type = 1 }, - static _ => (GpuTextureSlot.Unassigned, 0, 0)) + static _ => (0u, 0, 0)) { DatElementId = PaperdollController.DollDragMaskId, Left = 270, @@ -179,7 +178,7 @@ public sealed class RetailUiInteractionFlowTests Layout, Objects, playerGuid: () => Player, - iconIds: static (_, _, _, _, _) => new GpuTextureSlot(0x1234u), + iconIds: static (_, _, _, _, _) => 0x1234u, strength: () => 100, selection: Selection, datFont: null, @@ -213,7 +212,7 @@ public sealed class RetailUiInteractionFlowTests Layout, Objects, playerGuid: () => Player, - iconIds: static (_, _, _, _, _) => new GpuTextureSlot(0x1234u), + iconIds: static (_, _, _, _, _) => 0x1234u, selection: Selection, itemInteraction: itemInteraction, emptySlotSprite: 0x06004D20u, @@ -266,7 +265,7 @@ public sealed class RetailUiInteractionFlowTests } private static UiItemList ItemList(uint id, float left, float top, float width, float height) - => new(static _ => (new GpuTextureSlot(1u), 32, 32)) + => new(static _ => (1u, 32, 32)) { DatElementId = id, Left = left, diff --git a/tests/AcDream.App.Tests/UI/UiButtonTests.cs b/tests/AcDream.App.Tests/UI/UiButtonTests.cs index c1238c16..b653e39e 100644 --- a/tests/AcDream.App.Tests/UI/UiButtonTests.cs +++ b/tests/AcDream.App.Tests/UI/UiButtonTests.cs @@ -1,11 +1,10 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; namespace AcDream.App.Tests.UI; public class UiButtonTests { - private static (GpuTextureSlot, int, int) NoTex(uint _) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoTex(uint _) => (0, 0, 0); private bool _clicked; [Fact] diff --git a/tests/AcDream.App.Tests/UI/UiCollapsibleFrameTests.cs b/tests/AcDream.App.Tests/UI/UiCollapsibleFrameTests.cs index 2127c8f9..bc7abf5b 100644 --- a/tests/AcDream.App.Tests/UI/UiCollapsibleFrameTests.cs +++ b/tests/AcDream.App.Tests/UI/UiCollapsibleFrameTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using Xunit; @@ -8,7 +7,7 @@ public class UiCollapsibleFrameTests { private static UiCollapsibleFrame MakeFrame(out UiPanel row2a, out UiPanel row2b) { - var f = new UiCollapsibleFrame(_ => (new GpuTextureSlot(1u), 1, 1)) + var f = new UiCollapsibleFrame(_ => (1u, 1, 1)) { CollapsedHeight = 96f, ExpandedHeight = 128f, @@ -45,7 +44,7 @@ public class UiCollapsibleFrameTests [Fact] public void Tick_notConfigured_isNoOp() { - var f = new UiCollapsibleFrame(_ => (new GpuTextureSlot(1u), 1, 1)); // Collapsed==Expanded==0 + var f = new UiCollapsibleFrame(_ => (1u, 1, 1)); // Collapsed==Expanded==0 f.Height = 50f; f.TickForTest(0.016); Assert.Equal(50f, f.Height); // unchanged, no divide/no forced height diff --git a/tests/AcDream.App.Tests/UI/UiDatFontTests.cs b/tests/AcDream.App.Tests/UI/UiDatFontTests.cs index 3700440c..923af959 100644 --- a/tests/AcDream.App.Tests/UI/UiDatFontTests.cs +++ b/tests/AcDream.App.Tests/UI/UiDatFontTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System.Collections.Generic; using AcDream.App.UI; using DatReaderWriter.Types; @@ -92,10 +91,10 @@ public class UiDatFontTests ['B'] = Glyph('B', width: 7, before: 1, after: 1), }; var font = new UiDatFont( - fgTex: GpuTextureSlot.Unassigned, + fgTex: 0, fgW: 0, fgH: 0, - bgTex: GpuTextureSlot.Unassigned, + bgTex: 0, bgW: 0, bgH: 0, lineHeight: 16f, diff --git a/tests/AcDream.App.Tests/UI/UiItemSlotTests.cs b/tests/AcDream.App.Tests/UI/UiItemSlotTests.cs index e75965d4..de5b6946 100644 --- a/tests/AcDream.App.Tests/UI/UiItemSlotTests.cs +++ b/tests/AcDream.App.Tests/UI/UiItemSlotTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; namespace AcDream.App.Tests.UI; @@ -22,26 +21,26 @@ public class UiItemSlotTests { var s = new UiItemSlot(); Assert.Equal(0u, s.ItemId); - Assert.Equal(GpuTextureSlot.Unassigned, s.IconTexture); + Assert.Equal(0u, s.IconTexture); } [Fact] public void SetItem_setsIdAndTexture() { var s = new UiItemSlot(); - s.SetItem(0x5001u, new GpuTextureSlot(0x99u)); + s.SetItem(0x5001u, 0x99u); Assert.Equal(0x5001u, s.ItemId); - Assert.Equal(new GpuTextureSlot(0x99u), s.IconTexture); + Assert.Equal(0x99u, s.IconTexture); } [Fact] public void Clear_afterSetItem_resetsToEmpty() { var s = new UiItemSlot(); - s.SetItem(0x5001u, new GpuTextureSlot(0x99u)); + s.SetItem(0x5001u, 0x99u); s.Clear(); Assert.Equal(0u, s.ItemId); - Assert.Equal(GpuTextureSlot.Unassigned, s.IconTexture); + Assert.Equal(0u, s.IconTexture); } [Fact] @@ -59,7 +58,7 @@ public class UiItemSlotTests [Fact] public void CatalogSlot_keeps_catalog_identity_separate_from_object_guid() { - var slot = new UiCatalogSlot { EntryId = 1234u, CatalogIconTexture = new GpuTextureSlot(99u) }; + var slot = new UiCatalogSlot { EntryId = 1234u, CatalogIconTexture = 99u }; Assert.Equal(1234u, slot.EntryId); Assert.Equal(0u, slot.ItemId); @@ -148,7 +147,7 @@ public class UiItemSlotTests public void ActiveDigitArray_occupiedSlot_regular_returnsRegularDigits() { var s = new UiItemSlot { RegularDigits = Regular, GhostedDigits = Ghosted, EmptyDigits = Empty }; - s.SetItem(0x5001u, new GpuTextureSlot(0x99u)); + s.SetItem(0x5001u, 0x99u); s.SetShortcutNum(0, ghosted: false); Assert.Same(Regular, s.ActiveDigitArray()); } @@ -157,7 +156,7 @@ public class UiItemSlotTests public void ActiveDigitArray_occupiedSlot_ghosted_returnsGhostedDigits() { var s = new UiItemSlot { RegularDigits = Regular, GhostedDigits = Ghosted, EmptyDigits = Empty }; - s.SetItem(0x5001u, new GpuTextureSlot(0x99u)); + s.SetItem(0x5001u, 0x99u); s.SetShortcutNum(0, ghosted: true); Assert.Same(Ghosted, s.ActiveDigitArray()); } @@ -209,7 +208,7 @@ public class UiItemSlotTests CooldownSprites = sprites, CooldownStepProvider = id => id == 0x5001u ? 6 : 0, }; - slot.SetItem(0x5001u, new GpuTextureSlot(99u)); + slot.SetItem(0x5001u, 99u); Assert.Equal(0x06000006u, slot.ActiveCooldownSprite()); } @@ -224,7 +223,7 @@ public class UiItemSlotTests }; Assert.Equal(0u, slot.ActiveCooldownSprite()); - slot.SetItem(0x5001u, new GpuTextureSlot(99u)); + slot.SetItem(0x5001u, 99u); Assert.Equal(0u, slot.ActiveCooldownSprite()); } } diff --git a/tests/AcDream.App.Tests/UI/UiRootInputTests.cs b/tests/AcDream.App.Tests/UI/UiRootInputTests.cs index df51877f..ceca3432 100644 --- a/tests/AcDream.App.Tests/UI/UiRootInputTests.cs +++ b/tests/AcDream.App.Tests/UI/UiRootInputTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System.Numerics; using AcDream.App.UI; @@ -11,7 +10,7 @@ public class UiRootInputTests { // Regression: the per-frame anchor pass must NOT reset a window's rect, // or move/resize get undone every frame. Windows are user-positioned. - var panel = new UiNineSlicePanel(_ => (new GpuTextureSlot(1), 32, 32)); + var panel = new UiNineSlicePanel(_ => ((uint)1, 32, 32)); Assert.Equal(AnchorEdges.None, panel.Anchors); } @@ -120,7 +119,7 @@ public class UiRootInputTests { var root = new UiRoot { Width = 800, Height = 600 }; var cell = new UiItemSlot { Left = 0, Top = 0, Width = 32, Height = 32 }; - cell.SetItem(0x50000A01u, new GpuTextureSlot(0x99u)); + cell.SetItem(0x50000A01u, 0x99u); root.AddChild(cell); object? payload = null; int releaseX = 0;