docs(research): sky/weather investigation handoff + diagnostic tools

Captures everything learned from a long worktree iteration on the
foreground-rain bug (ISSUES.md #1 / #26) plus a new star-rendering
bug observed in the same area. The code work from that worktree
(WeatherDispatcher, EmitterDescLoader.LoadFromDat, WeatherCellRenderer,
GameWindow integration) was reverted because it didn't visibly fix
the rain bug — but the research findings + diagnostic tools are
durable and should not have to be rediscovered.

What's added:
- docs/research/2026-04-26-sky-investigation-handoff.md
  Comprehensive seed prompt for the next session. Covers:
  * Bug A: foreground rain (#26) — what's open, what's confirmed,
    what's been tried
  * Bug B: stars rendering as square in corner (NEW, user-observed)
  * 40-agent decomp scan findings — retail rain is NOT camera-
    particles, NOT server-driven, NOT screen-space; the mesh IS
    a hollow octagonal tube; only 5 weather GfxObjs in Dereth
  * Things ruled out by trial (envelope, scaling, unlit, depth-
    always alone, Setup loading)
  * Things to try next (depth+zfar combined, full render-state
    audit, frame ordering, star UV bug as easier first target)
  * Acceptance criteria for "done"

- docs/research/2026-04-26-chorizite-pr-draft.md
  Upstream PR draft for Chorizite/DatReaderWriter. Five generated
  DBObj source files reference nonexistent enum values and are
  silently excluded from the NuGet build:
  ParticleEmitterInfo, Clothing, PaletteSet, DataIdMapper,
  DualDataIdMapper. Fix: delete the duplicates. Independent of
  the rain work — benefits the AC modding ecosystem broadly.

- docs/research/2026-04-26-datreaderwriter-reference.md
  Developer reference for our DatReaderWriter usage. Version,
  types we consume, known broken types, thread-safety caveats,
  upgrade procedure, NuGet-vs-vendored decision matrix.

- tools/PesChainAudit/
  Recursive PES walker — given a 0x33xxxxxx script id, walks all
  CallPES references and dumps every hook + every referenced
  ParticleEmitter's parameters. Used to prove no weather PES
  emits rain particles.

- tools/TextureDump/
  Dumps texture pixel statistics (alpha histogram, brightness,
  max) and saves as PNG for visual inspection.

- tools/WeatherEnumerator/
  Enumerates every DayGroup in a Region, lists weather SkyObjects
  (Properties & 0x04), dumps GfxObj bounding boxes.

- tools/WeatherSetupProbe/
  Loads a Setup id, dumps each part's GfxObj + frame + scale +
  surface. Used to prove weather Setups are 5cm dummy carriers.

Worktree feature/sky-fixes is being deleted in a follow-up step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-26 21:40:34 +02:00
parent a060f4fc98
commit 8db7a9ec28
11 changed files with 1085 additions and 0 deletions

View file

@ -0,0 +1,82 @@
# Upstream issue draft — Chorizite/DatReaderWriter
**Repo:** https://github.com/Chorizite/DatReaderWriter
This draft is for filing as a GitHub issue (or a 1-line-fix PR) against
the Chorizite DatReaderWriter project. The duplicate generated file
fails to build against its own enum and shadows the correct
`ParticleEmitter` registration for the `0x32xxxxxx` ID range.
---
## Title (under 80 chars)
`Duplicate ParticleEmitterInfo.generated.cs references missing DBObjType enum value`
## Summary
`DatReaderWriter/Generated/DBObjs/` contains two generated files for
the same DB type (`DB_TYPE_PARTICLE_EMITTER`, ID range
`0x320000000x3200FFFF`):
- `ParticleEmitter.generated.cs` — class `ParticleEmitter`, uses
`DBObjType.ParticleEmitter` (correct, matches the generated enum)
- `ParticleEmitterInfo.generated.cs` — class `ParticleEmitterInfo`,
uses `DBObjType.ParticleEmitterInfo` (does not exist in
`Generated/Enums/DBObjType.generated.cs`)
The second file is byte-for-byte identical to the first apart from the
class name and the enum reference. Only `DBObjType.ParticleEmitter` is
emitted by the source generator (see line 179 of
`DBObjType.generated.cs`), so the duplicate fails to compile and also
registers a second `[DBObjType(..., 0x32000000, 0x3200FFFF, ...)]`
attribute for the same ID range, which collides at runtime in
`PortalDatabase.Get<T>(uint id)`.
## Reproduction
1. Clone `Chorizite/DatReaderWriter` at master.
2. `dotnet build DatReaderWriter.sln`
3. Build fails in `Generated/DBObjs/ParticleEmitterInfo.generated.cs`
on the `DBObjType.ParticleEmitterInfo` reference (`CS0117: 'DBObjType' does not contain a definition for 'ParticleEmitterInfo'`).
If the file is hand-edited to compile (e.g. by switching the enum to
`ParticleEmitter`), two classes are then registered for the same ID
range `0x320000000x3200FFFF` and `Get<ParticleEmitterInfo>(id)` /
`Get<ParticleEmitter>(id)` ambiguously dispatch.
## Expected behavior
A single generated class for `DB_TYPE_PARTICLE_EMITTER`. The
`PortalDatabase.generated.cs` reader already exposes only
`GetParticleEmitter(uint id)` returning `ParticleEmitter?`, so
`ParticleEmitter` is the canonical name.
## Actual behavior
`ParticleEmitterInfo.generated.cs` exists and breaks the build. It
appears to be a stale artifact from an earlier pass of the source
generator when the type was renamed `ParticleEmitterInfo → ParticleEmitter`.
## Proposed fix (one line)
Delete `DatReaderWriter/Generated/DBObjs/ParticleEmitterInfo.generated.cs`.
If the source generator regenerates it, the generator's type-name map
needs the `ParticleEmitterInfo` entry removed too.
## Why this matters to downstream consumers
Downstream consumers (e.g. acdream, an open-source modern AC client
that vendors DatReaderWriter) cannot build the library as-is — we
have to manually delete the duplicate file in our vendored copy on
every update. Anyone trying to load `0x32xxxxxx` records and reaching
for `ParticleEmitterInfo` (the historical / decompiled-client name)
hits a broken type instead of being directed at the renamed
`ParticleEmitter`. A clean upstream fix removes the per-consumer
patch.
## Environment
- Commit: master @ HEAD as of 2026-04-26
- .NET SDK: 10.0
- OS: Windows 11

View file

@ -0,0 +1,158 @@
# DatReaderWriter (DRW) — developer reference
Status as of **2026-04-26**. Source for "what's safe to call, what's
broken, how to upgrade." Update this doc when DRW version bumps or when
we discover a new safety/coverage gap.
## 1. Current version
- **Package:** `Chorizite.DatReaderWriter`
- **Version:** `2.1.7` (NuGet)
- Pinned in `src/AcDream.Core/AcDream.Core.csproj` and
`src/AcDream.Cli/AcDream.Cli.csproj`. App + Core both transitively
use that one version.
- Some `tools/*` (WeatherSetupProbe, WeatherEnumerator, SkyObjectInspect,
dump-keymap, PesChainAudit) `ProjectReference` the vendored repo
directly instead of NuGet — see §7.
## 2. Source repo
- **Origin:** https://github.com/Chorizite/DatReaderWriter
- **Vendored at:** `references/DatReaderWriter/` (HEAD `63e84b6 "Bump
version"` — no git tag matching the NuGet version; the repo doesn't
tag releases, ship version is `GitVersion.yml`-driven).
- **License:** MIT, © 2024 ACClientLib (`references/DatReaderWriter/LICENSE.txt`).
- **Targets:** `net8.0`, `netstandard2.0`, `net48`. We consume from
`net10.0` projects; the `net8.0` TFM resolves cleanly via netstandard
forward-compat.
- **Project layout:** `DatReaderWriter/` (library), `…SourceGenerator/`
(Roslyn generator emits packing code), `…Tests/`, `…Benchmarks/`.
## 3. Types we use
Top-level entry points:
- `DatReaderWriter.DatCollection` — the four-database container
(`Portal`, `Cell`, `HighRes`, `Local`).
- `DatReaderWriter.Options.DatCollectionOptions` (caching mode, paths).
- `DatCollection.Get<T>(id)` / `Portal.Get<T>(id)` — generic typed read.
- `DatCollection.GetAllIdsOfType<T>()` — used by CLI enumerators.
`DatReaderWriter.DBObjs.*` consumed in `src/`:
- `LandBlock`, `LandBlockInfo`, `EnvCell`, `Region`
- `GfxObj`, `Setup`, `Surface`, `SurfaceTexture` (via SurfaceDecoder)
- `Animation`, `MotionTable`
- `ParticleEmitter`, `PhysicsScript`
- `Environment` (note: collides with `System.Environment`, must be FQN
in `AcDream.Cli/Program.cs`)
- `Wave` / sound DBObjs (via `DatSoundCache`, `SoundCookbook`)
`DatReaderWriter.Types.*` consumed:
- `AnimationFrame`, `MotionData`
- `Polygon`, `VertexArray`, `Sphere` (BSPQuery, collision)
- `SoundEntry`, `AnimationHook`, `CreateParticleHook`
- `BSPNode` / `BSPTree` / `PhysicsBSPNode` / `CellBSPNode` /
`DrawingBSPNode` (BSP traversal in `BSPQuery`, `PhysicsDataCache`)
`DatReaderWriter.Enums.*` consumed:
- `MotionCommand` (aliased `DRWMotionCommand` in MotionCommandResolver)
- `Sound` (aliased `DRWSound`)
- `StipplingType`, plus various surface/material flags
## 4. Known broken / missing types
- **`RenderMaterial`** — not implemented. Per upstream README "Known
Issues." We do not consume it.
- **`LayoutDesc`** — README marks it "supported but structure will need
cleanup." Treat fields as unstable across versions. We do not depend
on it directly.
- **`ParticleEmitterInfo`** — DRW exposes the **`ParticleEmitter`**
DBObj only. There is no separate `ParticleEmitterInfo` type. Our
`Vfx/VfxModel.cs` comment that mentions "ParticleEmitterInfo dat" is
conceptual (matches the retail header struct name); the actual lookup
in `EmitterDescLoader.cs` is `dats.Portal.Get<ParticleEmitter>(id)`
with a `using DatParticleEmitter = DatReaderWriter.DBObjs.ParticleEmitter;`
alias because we have our own `ParticleEmitter` runtime class in
`AcDream.Core.Vfx`.
- **Anything not in `DBObjs/`** — only 5 DBObj files exist
(`ActionMap`, `DBProperties`, `Iteration`, `LandBlock`,
`MasterProperty`) at the top level of that folder; the rest (GfxObj,
Setup, etc.) live elsewhere in the source tree. If a type doesn't
resolve, search the DRW source — don't assume it's missing.
## 5. Unsafe patterns
**`DatCollection` is NOT thread-safe.** `DatBinReader` holds a buffer
position field per database; concurrent `Get<T>` calls from two threads
corrupt each other and produce silently half-populated payloads
(half-empty `LandBlock.Height[]` in our case). See
`memory/feedback_phase_a1_hotfix_saga.md` rule #2 — Phase A.1 burned
~3 hotfixes mis-diagnosing this as something else.
**Rules:**
1. All `DatCollection.Get<T>()` calls run on the render thread.
2. `LandblockStreamer` runs synchronously (see header comment, lines
1732 of `src/AcDream.App/Streaming/LandblockStreamer.cs`). The
worker-thread design was reverted; the channel-based outbox is
retained so the moment a thread-safe wrapper exists we can swap
loaders back to async without touching call sites.
3. Don't claim "DRW is thread-safe per its docs." There are no such
docs. The README says nothing about concurrency.
4. The async surface area (`*Async` methods) is for I/O, not
concurrent reads — calling them from two threads is still unsafe.
**Caching:** `DatCollectionOptions` caching mode is `OnDemand`. Don't
flip to `None` without re-benchmarking — terrain mesh build hits the
same `LandBlock` repeatedly.
## 6. How to upgrade
1. Bump `Version="2.1.7"` in `src/AcDream.Core/AcDream.Core.csproj` and
`src/AcDream.Cli/AcDream.Cli.csproj` (and any `tools/*` using the
NuGet package, e.g. `TextureDump`).
2. Pull `references/DatReaderWriter/` to the matching commit so the
vendored copy stays in sync (used by tools that `ProjectReference`
it directly + as a search target for "what's in DRW today").
3. `dotnet restore && dotnet build` from repo root.
4. Run the full test suite (`dotnet test`). Watch for compile breaks
on type renames (DRW has historically renamed `DBObj` types between
minor versions).
5. Smoke-test live: launch against the local ACE server and walk
Holtburg → Foundry. Watch for half-loaded landblocks (missing
heights / null surfaces) — that's the canonical "thread-safety
regression slipped in" signature.
**Risks per area:**
- **DBObj field renames** — DRW source-generates packing; field renames
break every consumer. Grep before bumping.
- **Enum value reordering**`MotionCommand`, `Sound`,
`StipplingType` are enum-by-name in our code, so adds are safe but
removals will break.
- **BSP type changes**`BSPNode` family is consumed in `BSPQuery.cs`
(collision). Any signature change there is a high-risk diff that
needs the conformance sweep re-run.
## 7. NuGet vs vendored
- **Use NuGet** (`Chorizite.DatReaderWriter` package) for everything
shipped in `src/` and any `tools/` that doesn't need DRW internals.
This is the default. Currently `AcDream.Core`, `AcDream.Cli`, and
`tools/TextureDump`.
- **Vendor (`ProjectReference` to `references/DatReaderWriter/…`)** when:
- The tool needs a DRW type or method that isn't `public` in the
NuGet build (some helpers are `internal`).
- You're prototyping a DRW patch you intend to upstream — edit in
place, build, validate, then PR upstream.
- Diagnostic / probe tools (`WeatherSetupProbe`, `WeatherEnumerator`,
`SkyObjectInspect`, `dump-keymap`, `PesChainAudit`) where iteration
speed against DRW source matters more than ship discipline.
- **Never mix** within one assembly. A project either NuGet-references
DRW or `ProjectReference`s the vendored copy. Both produces type-
identity errors at the boundary.
- **Production code stays on NuGet.** When a vendored tool proves a
DRW change is needed, upstream it, wait for a release, and bump §6.

View file

@ -0,0 +1,164 @@
# Sky / Weather Investigation — Handoff for Next Session
**Date:** 2026-04-26
**Author:** Claude (Opus 4.7) closing out a long worktree
**Purpose:** seed prompt for a fresh session that resumes investigation of two sky-related visual bugs without re-deriving everything we learned the hard way.
---
## What you're picking up
Two open visual bugs in the sky/weather pipeline, both observed against a live ACE server in Holtburg:
### Bug A — Foreground rain missing (ISSUES.md #26)
In retail, when the active DayGroup is "Rainy", you see rain everywhere — in the air between the camera and your character, on the horizon, overhead. Volumetric appearance.
In acdream as of `main` (post-revert), you see no falling rain at all. Even the previous (rolled-back) work that rendered the rain mesh in world space produced only a "discrete cylinder of rain" around the player, not the volumetric look retail has.
### Bug B — Stars appear as a square in one corner instead of stretched across the sky
User-observed during a recent night-time launch. The star sky-object's texture appears as a small square in one corner of the geometry instead of being mapped across the full sky dome / star plane. Suspected: UV-mapping bug, or wrong texture wrap mode (CLAMP vs REPEAT), or wrong geometry for the star object.
**Both bugs may share a root cause** in how we handle sky GfxObj UVs / texture sampling. Worth investigating B first because it might be simpler AND might reveal something that explains A too.
---
## What's on `main` right now
- The original `SkyRenderer` (camera-anchored sky pass) — unchanged.
- The original hardcoded camera-attached rain emitter (`BuildRainDesc`/`BuildSnowDesc` + `UpdateWeatherParticles`) in `GameWindow.cs` — still uses the broken `StartAlpha = 0.3 → EndAlpha = 0` linear fade. This is the alpha-fade bug from the original Issue #1 (rain fades to invisible exactly at ground level).
- ISSUES.md #1 is OPEN and #26 hasn't been filed yet on main (it lived only on the deleted `feature/sky-fixes` branch).
**This handoff doc captures everything #26 contained**, so don't worry about losing it.
---
## What we already learned (40-agent decomp scan, 2026-04-26)
**Confirmed truths about retail rain:**
1. **Retail rain is NOT camera-attached particles.** Across the entire 1.4M-line decomp at `docs/research/named-retail/acclient_2013_pseudo_c.txt`, only TWO callers of `ParticleManager::CreateParticleEmitter` exist — both are animation-hook executors (`CreateParticleHook::Execute`, `CreateBlockingParticleHook::Execute`). No camera-attached rain spawner exists in retail.
2. **Retail rain is NOT server-driven.** No inbound network message triggers rain. ACE doesn't broadcast it. Weather is entirely client-side, dat-driven from `Region.SkyInfo.DayGroups[i].SkyObjects[]`. `EnvironChangeType` (fog colors + 6 thunder sound enums) is the only weather-related broadcast and it doesn't trigger visuals.
3. **Retail rain is NOT screen-space.** No fullscreen quad, no orthographic projection, no render-target swap, no post-process. Verified via grep for `D3DXMatrixOrtho`, `SetRenderTarget`, `FullscreenQuad`, etc.
4. **The mesh IS exactly a hollow octagonal tube.** Vertex dump of `0x01004C42` and `0x01004C44`:
- 24 verts (some duplicated for UV seams)
- 16 triangles forming 8 vertical wall quads
- Octagonal corners at radius 113m around center
- 815m tall (z=0.11 to z=814.90)
- **No top/bottom caps** — purely walls
- Surface 0x080000C5 = `Base1Image | Translucent | Alpha | Additive`, SurfTranslucency=0.5
5. **Only 5 unique weather GfxObjs exist in all of Dereth's 20 DayGroups:**
- `0x01004C44` — hollow octagonal tube, TexVel=(0.02, -1.7) — direct GfxObj
- `0x01004C42` — hollow octagonal tube, TexVel=(0.02, -2.0) — direct GfxObj
- `0x02000BA6`, `0x02000588`, `0x02000589` — Setups, each containing a SHARED single 5cm dummy GfxObj (`0x010001EC`, 3 verts, 1 polygon) carrying a flash PES via DefaultScript
6. **The PES referenced by weather Setups produces ONLY flash + sound effects.** Recursive walk via `tools/PesChainAudit/` confirmed: across all 5 weather PESes (`0x33000453`, `0x33000428`, `0x3300042C`, `0x33000429`, `0x3300042D`), there is exactly ONE CreateParticleHook anywhere in the chain, and it spawns `ParticleEmitter 0x320002C2` — a 10ms-lifetime, max-2-particle, zero-extent flash. The other 4 PESes are pure SoundTweaked + CallPES recursion loops (rolling thunder cadence). Zero rain particles.
7. **The rain streak texture is `0x050016A6`** — 256x256, R8G8B8 (no alpha), 99.4% black, sparse bright streaks. Engineered for additive blending. Not the texture itself broken — works as designed for an additive sparse-streak overlay.
8. **Retail uses `DEPTHTEST_ALWAYS` and `zfar*4`** for the weather pass, set at decomp `0x00507063` and `0x00507055` in `GameSky::Draw(arg2=1)`. We tried `DEPTHTEST_ALWAYS` alone — it made the visual WORSE (rain mesh now covers everything as a wall over scene geometry). Whatever combination retail uses, it's not just depth-always.
**Things we ruled out by trying:**
- Camera-attached envelope spawn (made up r12-deepdive params; appeared as small dots near camera, not retail-like)
- Scaling the rain mesh down (turns it into an obvious cylinder around player)
- Unlit additive shader (no perceptible change)
- DEPTHTEST_ALWAYS alone (made it look worse)
- Setup loading for weather Setups (Setup parts are 5cm dummy quads, not visual)
**Things we did NOT try and might be the answer:**
- **Retail's frame ordering** — maybe the weather pass renders BEFORE the scene, not after. Worth checking the main render loop in retail (probably around `LScape::draw` or `SmartBox::Draw`).
- **Per-surface render state on the additive surface** — maybe retail enables D3DRS_ALPHATESTENABLE + D3DRS_ALPHAREF for the rain Surface specifically, discarding pure-black texels.
- **Texture sampling differences** — mipmap settings, bilinear vs nearest, anisotropy. Retail's view of the cylinder wall at 113m goes through mipmap selection that might dramatically affect appearance.
- **The DEPTHTEST_ALWAYS + zfar*4 combination together** — we only tried depth alone. Maybe the zfar change is what makes it work.
- **Multiple instances of the same mesh** — retail might spawn the rain CPhysicsObj at SEVERAL anchor positions (not just the player), creating overlapping coverage. Worth re-checking `GameSky::CreateDeletePhysicsObjects` for loop bounds.
- **The actual texture pixel content displayed in retail vs ours**`tools/TextureDump/` already dumps the texture as PNG. Worth visually comparing what retail's rain looks like to verify the texture is correct.
---
## Probe tools available (already on main, in `tools/`)
These are one-shot diagnostic console apps. Each builds standalone with `Chorizite.DatReaderWriter` NuGet + an env var pointing at the dat dir.
- **`tools/PesChainAudit/`** — recursive PES walker. Given a starting `0x33xxxxxx` script id, walks all CallPES references and dumps every hook + every referenced ParticleEmitter's params. Used to prove no weather PES has rain particles.
- **`tools/TextureDump/`** — dumps texture pixel stats (alpha histogram, brightness mean/stddev, max) and saves as PNG for visual inspection.
- **`tools/WeatherEnumerator/`** — enumerates all DayGroups in a Region, lists every weather SkyObject (Properties & 0x04), dumps GfxObj bounding boxes.
- **`tools/WeatherSetupProbe/`** — loads a Setup id, dumps every part's GfxObj, frame, scale, and surface info. Used to prove weather Setups are 5cm dummy quads.
- **`tools/SetupProbe/`** — already in main (similar to WeatherSetupProbe, slightly different focus).
Run from `tools/<name>/` with `dotnet run -c Release`. Set `ACDREAM_DAT_DIR` if needed (defaults to `%USERPROFILE%\Documents\Asheron's Call`).
---
## Other useful research artifacts on main
- **`docs/research/2026-04-26-chorizite-pr-draft.md`** — upstream PR draft for the Chorizite/DatReaderWriter project. Five DBObj source files in their `Generated/` directory are stale duplicates that reference nonexistent enum values, causing them to be silently excluded from the published NuGet (`ParticleEmitterInfo`, `Clothing`, `PaletteSet`, `DataIdMapper`, `DualDataIdMapper`). The fix is to delete those files. **You can file this PR independent of the rain investigation — it benefits the whole AC modding ecosystem.**
- **`docs/research/2026-04-26-datreaderwriter-reference.md`** — DRW dev reference. Versioning, types we use, broken types catalog, upgrade procedure, NuGet vs vendored decision matrix.
- **`docs/research/named-retail/`** — the PDB-named retail decomp. Always grep here FIRST for any AC behavior question. Particularly relevant for sky/weather:
- `GameSky::Draw` at `0x00506ff0`
- `GameSky::CreateDeletePhysicsObjects` at `0x005073c0`
- `GameSky::MakeObject` at `0x00506ee0`
- `GameSky::UpdatePosition` at `0x00506dd0`
- `LScape::weather_enabled` at `0x0081cbe9`
- `SmartBox::EnableWeather` at `0x00451dd0`
- **`docs/research/deepdives/r12-weather-daynight.md`** — pre-decomp deep dive on weather mechanism. **Several claims in r12 turned out to be incorrect** (specifically the "rain is a camera-attached ParticleEmitter" claim was disproven by the 40-agent scan). Read it for context but don't trust the rain-mechanism details.
- **`docs/research/2026-04-23-lightning-real.md`** — corrected understanding of lightning (server-driven via opcode `0xF754 PlayScript`, not sky-walked). Relevant if you also work on Issue #2.
---
## Suggested next investigation paths (priority-ranked)
1. **Investigate Bug B first (the star bug)** — much more concrete (UV mapping, texture wrap, geometry — all easily inspectable). May reveal the same kind of issue that affects rain. Specifically:
- Identify which SkyObject in DayGroup[0] (Sunny / clear night) renders the stars
- Dump that GfxObj's vertices + UVs via the existing `tools/SetupProbe/` or extend `WeatherEnumerator/`
- Compare authored UV range to expected sky-dome coverage
- Check texture wrap mode in `SkyRenderer.cs` (we set REPEAT for scrolling, CLAMP_TO_EDGE otherwise — stars probably need REPEAT or full UV remapping)
2. **Re-grep retail decomp for the FULL render-state setup around the weather pass.** The agents found DEPTHTEST_ALWAYS but maybe missed alpha test, blend mode, fog state, sample state, etc. Specifically dump the full `GameSky::Draw(arg2=1)` body and verify EVERY D3D state call we should be replicating.
3. **Try DEPTHTEST_ALWAYS + zfar*4 TOGETHER.** We only tried depth alone. The zfar change may matter for how the cylinder's far wall projects.
4. **Investigate retail's frame ordering.** Where in retail's main render loop does `GameSky::Draw(arg2=1)` actually fire? Before or after scene meshes? If it's BEFORE, the depth test gives a totally different result than rendering after.
5. **File the Chorizite PR.** Independent of rain. Easy contribution.
---
## What "done" looks like
For Bug A (rain): standing in Holtburg during a Rainy DayGroup with `ACDREAM_DAY_GROUP=7`, rain visible IN the air between camera and character, not just on horizon. Looking up still matches retail. Walking forward, rain follows naturally.
For Bug B (stars): standing in Holtburg at night (`ACDREAM_DAY_GROUP=<night-group-index>`), stars visible as a full sky covering, NOT a small square in one corner.
---
## Suggested first prompt for the next session
```
Read docs/research/2026-04-26-sky-investigation-handoff.md.
Tackle Bug B (stars rendering as a square in one corner) first
since it's more concrete. Use tools/WeatherEnumerator/ as a model
for a new probe that dumps the star SkyObject's geometry and UVs.
Iterate to a fix, verify visually, commit.
Once stars work, return to Bug A (rain) using the priority-ranked
investigation paths in §"Suggested next investigation paths" of
the handoff doc. The 40-agent findings have ruled out enough that
the next likely answer is in retail's full render-state setup
around the weather pass — specifically things the agents may have
missed beyond DEPTHTEST_ALWAYS.
Use a worktree per CLAUDE.md, and don't ask "should I continue"
between sub-steps.
```