# 2.6 — Picking and selection vs the building-render discipline ## RETAIL Retail picking is a DEFERRED, DRAW-RESOLVED hit test: the click arms a global "selection cursor", and the actual hit testing happens INSIDE the very same render traversal that draws the world — same cell flood, same portal views, same viewcone gates. There is no separate ray-through-the-world trace. CHAIN. (1) Mouse click reaches a UIElement_SmartBoxWrapper handler (0x004e5430, pc:233355-233375): if a 3D-icon UI element is under the mouse it short-circuits via SmartBox::set_found_object; otherwise it calls SmartBox::find_object(x,y) (0x00451C60, Ghidra decompile): stores the click pixel, zeroes click_object_id/click_object_index, validates the pixel is inside the 3D viewport, then calls Render::set_selection_cursor(x-vpX, y-vpY, polys=true) (0x0054B750, Ghidra: check_selection=1, check_curr_object_polys=1, clears m_MouseSelectData.bFoundPolygon/bFoundSphere), sets CPhysicsPart::selected_object_in_view=0 and SmartBox::lookingForObject=1. The accumulator struct Render::MouseSelectData (acclient.h:46610-46620) carries {bFoundPolygon, fClosestPolygon, PolygonID, PolygonIndex, bFoundSphere, fClosestSphere, SphereID, SphereIndex}. (2) During the next world draw, SmartBox::DrawNoBlit (0x00454c20, pc:93700-93740) runs SetNormalMode → update_viewer → RenderNormalMode. Inside Render::update_viewpoint (0x0054cdd0, pc:343689-343696), if check_selection, the click pixel is converted ONCE into a world ray direction: Render::pick_ray (0x0054B610, Ghidra: pixel × xinvscale/yinvscale − tx/ty, composed through the camera Xaxis/Yaxis/Zaxis, normalized) → stored in Render::selection_ray. (3) Every object part draw goes through CPhysicsPart::Draw (0x0050d7a0, Ghidra-confirmed): it sets the load-bearing gate Render::check_curr_object = (physobj != null && physobj->id != 0) || CPhysicsPart::creature_mode != 0, sets RenderDeviceD3D::s_current_physics_part = this, and calls the device DrawMesh. CPhysicsPart::creature_mode (static, 0x00843bf4) has NO writer anywhere in the named pseudo-C (the creature_mode written by CreatureMode::Render 0x004529d0 / cleared by SmartBox::SetNormalMode 0x00453120 is a different field, SmartBox's instance field for char-gen rendering) — so in-world the gate is exactly "this part belongs to a CPhysicsObj with a non-zero server object id". (4) RenderDeviceD3D::DrawMesh (0x005a0860, pc:429180-429340) is where pick meets visibility: with no portal list it runs Render::viewconeCheck(gfxobj->drawing_sphere) and only if not OUTSIDE calls Render::GfxObjUnderSelectionRay(gfxobj) (pc:429262, 0x005a09bb) before DrawMeshInternal; with a portal list it loops the per-cell portal views (Render::set_view(view,i) 0x0054d0e0, honoring RenderDeviceD3D::building_view), runs viewconeCheck per view, and calls GfxObjUnderSelectionRay exactly once on the first view that passes (pc:429318-429325, 0x005a08f8-0x005a090c, var_8_1 guard). An object OUTSIDE in every portal view is never selection-tested at all. (5) Render::GfxObjUnderSelectionRay (0x0054c740, Ghidra-confirmed): requires check_selection && check_curr_object && s_current_physics_part != null. It transforms selection_ray + viewpoint into part-local space (Frame::globaltolocalvec, direction scaled by 1/|gfxobj_scale|, ray length = RAY_INFINITE_DISTANCE — no max distance). Coarse test: CSphere::sphere_intersects_ray(gfxobj->drawing_sphere); a sphere hit farther than the globally-closest polygon hit so far is discarded entirely. Otherwise it records the nearest sphere hit (SphereID = CPhysicsPart::get_physobj_id = physobj->id, SphereIndex = physobj_index), then — because check_curr_object_polys — scans the object's FULL FLAT polygon array gfxobj->polygons[0..num_polygons] (stride 0x30; Ghidra-confirmed loop), NOT the DrawingBSP-filtered subset, breaking on the FIRST polygon the ray hits in array order; that hit's distance is then compared against the global fClosestPolygon and recorded if nearer. CPolygon::polygon_hits_ray (0x005395E0, Ghidra: if sides_type==0 (single-sided) and dot(ray.dir, plane.N) > 0 → miss; else Plane::compute_time_of_intersection + point_in_polygon). (6) Harvest, end of the SAME DrawNoBlit frame (pc:93722-93733): Render::GetMouseSelectionObjectID (0x0054C950): bFoundPolygon → PolygonID, else bFoundSphere → SphereID, else 0 — any polygon hit categorically beats any sphere hit. The id lands in SmartBox::click_object_id and fires ECM_UI::SendNotice_SmartBoxObjectFound(id); lookingForObject=0; Render::clear_selection_cursor (0x0054B790). WHAT IS NOT TESTED. EnvCell structure polys: RenderDeviceD3D::DrawEnvCell (0x0059f170, pc:427898-427935) pushes structure->polygons straight onto Render::PolyList (planeMask=0xffffffff) or D3DPolyRender::DrawMesh for built meshes — s_current_physics_part is never set, GfxObjUnderSelectionRay is never reached: CELL GEOMETRY IS INVISIBLE TO THE PICK. Same for land cells (DrawLandCell 0x0059f120 → ACRender::landPolysDraw). Buildings DO route through CPhysicsPart::Draw (DrawBuilding 0x0059f2a0, pc:429282-429295), BUT CBuildingObj::makeBuilding (0x006b53a0, pc:701302) calls CPhysicsObj::InitObjectBegin(result, 0, 0) and InitObjectBegin (0x0050ff80, pc:278065-278073) does this->id = arg2 → buildings have id 0 → check_curr_object=0 → building shells (including their baked door/window portal quads) are NEVER pick candidates. Dat-baked statics likewise: EnvCell statics CPhysicsObj::makeObject(setupId, 0, 0) pc:309713 (0x0052c3e7), landblock statics pc:314697 (0x00530acb) — object_iid 0. ANSWERS. Q1: retail hit-tests ONLY CPhysicsObj parts whose physobj->id != 0 (server-identified weenies), per-poly with sphere fallback, at draw time. It never picks against cell geometry; walls never "swallow" a click by being hit — they are invisible to the pick; missing everything yields id 0 (deselect). A baked closed-door/window portal poly on a building shell can NOT swallow a click (id 0 gate). Q2: yes — enforced by the SAME PView, by construction: objects only reach CPhysicsPart::Draw via the flood's drawn cells, and the selection test only fires in portal views where the drawing_sphere passes viewconeCheck (DrawMesh 0x005a0860). Granularity is sphere-vs-portal-view-cone, not per-pixel — an object whose sphere pokes through a doorway view can be picked via a polygon that is pixel-wise clipped; retail accepts that slop. Q3: the door ENTITY (server weenie, id != 0) takes the click categorically; the shell's baked portal quad never competes because the building is not a candidate. The only place portal polys touch picking at all is on id!=0 objects, where the FULL flat poly array is tested whether or not the DrawingBSP drew those polys this frame. Bonus: retail also tracks "is the selected object still visible" through the same draw — SmartBox::set_selected_object_id (0x00451BA0, pc:90768) sets CPhysicsPart::viewcone_check_object_id; CPhysicsPart::Draw flips selected_object_in_view=1 when DrawMesh returns 2 (drawn) for that id (Ghidra); CPlayerSystem::OnObjectRangeExit (0x00560870, pc:365073-365090) deselects via ACCWeenieObject::SetSelectedObject(0,0) when range-exited AND not in view, else re-registers a range handler at 25 m indoors / 75 m outdoors (0x005608a1). ## ACDREAM acdream picks SYNCHRONOUSLY at input time against a server-entity dictionary, with a screen-rect sphere test plus a parallel ad-hoc ray-vs-cell-polygon occluder; it has zero coupling to the PView flood that decides what draws. CHAIN. (1) InputAction.SelectLeft / SelectDblLeft → GameWindow.PickAndStoreSelection (src/AcDream.App/Rendering/GameWindow.cs:10414-10419 → 10515). (2) Candidates = _entitiesByServerGuid.Values (GameWindow.cs:10546) — the dict is populated ONLY from server spawns (GameWindow.cs:2855, `_entitiesByServerGuid[spawn.Guid] = entity`), so building shells, dat-baked EnvCell statics (0x40xxxxxx ids), and atlas scenery are structurally absent. WorldPicker additionally skips ServerGuid==0 and the player (src/AcDream.Core/Selection/WorldPicker.cs:253-254) — note retail does NOT skip the player. (3) Test = the screen-rect overload WorldPicker.Pick (WorldPicker.cs:210-285): per candidate, the Setup-level SelectionSphere (resolved by GameWindow.TryGetEntitySelectionSphere, GameWindow.cs:11010-11045: dat Setup.SelectionSphere scaled by entity scale, rotated to world; null → skipped) is projected via ScreenProjection.TryProjectSphereToScreenRect to a screen rect, inflated 8 px to match the target-indicator brackets, hit-tested against the mouse pixel; among containing rects the nearest clip.W depth wins (WorldPicker.cs:261-282). There is NO polygon stage — Stage B (CPolygon::polygon_hits_ray port) is explicitly deferred per issue #71 (WorldPicker.cs:199-204), so retail's "any polygon hit beats any sphere hit" arbitration does not exist. (4) Occlusion (#86, closed 2026-05-19): PickAndStoreSelection snapshots every loaded EnvCell's physics (`_physicsDataCache.CellStructIds`, GameWindow.cs:10535-10540) and passes cellOccluder = CellBspRayOccluder.NearestWallT (GameWindow.cs:10565-10568). NearestWallT (src/AcDream.Core/Selection/CellBspRayOccluder.cs:42-79) Möller-Trumbore-tests the click ray against EVERY polygon in every loaded cell's `Resolved` dict — which is cellStruct.PhysicsPolygons (src/AcDream.Core/Physics/PhysicsDataCache.cs:176,208), the collision set, NOT the drawn set — two-sided, no BSP, no portal awareness; the nearest wall t becomes a clip.W depth and candidates deeper than it are skipped (WorldPicker.cs:229-248, 276). Portal-opening polys live in a separate dict (CellPhysics.PortalPolygons, PhysicsDataCache.cs:178-179, 212, 571) that the occluder does not consult. (5) Visibility coupling: NONE. RetailPViewRenderer.DrawInside produces a per-frame RetailPViewFrameResult with OrderedVisibleCells (src/AcDream.App/Rendering/RetailPViewRenderer.cs:44, 71, 153) but the picker never reads it; an entity in a cell the flood rejected is still a candidate as long as no occluder polygon happens to lie along the ray, and conversely the occluder can block entities the flood draws. (6) Selected-object tracking: _selectedGuid persists until the next pick; TargetIndicatorPanel projects the same SelectionSphere every frame (GameWindow.cs:1278-1322) with no "was it drawn this frame" gate and no range-based deselect — no analog of retail's viewcone_check_object_id / selected_object_in_view / OnObjectRangeExit machinery. ## DIVERGENCES ### [MEDIUM] pick-outside-draw-traversal (UNVERIFIED (verifier hit token limit)) — Pick is resolved outside the draw traversal — visibility enforced by a parallel mechanism, not the flood - blastRadius: The pick can select entities the flood does not draw (entity in a cell the PView rejected — e.g. a cellar NPC below the floor, or an entity around a corner the flood culled — is pickable whenever no EnvCell physics polygon happens to lie along the ray, including whenever the cell physics simply is not streamed in), and can refuse entities the flood draws (see occluder-stricter-than-retail). No single filed issue maps to it, but it is the structural risk for the holistic port: every flood fix in the #108/#109/#114 family silently leaves picking behind, because what-you-can-click and what-you-can-see are computed by two unrelated mechanisms. Contributes to the 'indoor world feels right' mandate at doorways and multi-story interiors. - retailEvidence: Pick is resolved inside the same render traversal that draws: arming via SmartBox::find_object 0x00451C60 → Render::set_selection_cursor 0x0054B750; ray built once in Render::update_viewpoint (pc:343689, 0x0054cfab); per-part test Render::GfxObjUnderSelectionRay 0x0054c740 fires only from RenderDeviceD3D::DrawMesh 0x005a0860 after viewconeCheck(drawing_sphere) passes for at least one portal view (pc:429262 / 429318-429325); objects in non-flooded cells never reach CPhysicsPart::Draw 0x0050d7a0 at all; harvest at end of SmartBox::DrawNoBlit 0x00454c20 (pc:93722-93733) via Render::GetMouseSelectionObjectID 0x0054C950. - acdreamEvidence: GameWindow.PickAndStoreSelection (GameWindow.cs:10515) runs synchronously at input time against _entitiesByServerGuid.Values (GameWindow.cs:10546) with no reference to RetailPViewRenderer's per-frame OrderedVisibleCells (RetailPViewRenderer.cs:44,71); occlusion is the separate CellBspRayOccluder ray test (GameWindow.cs:10565-10568, CellBspRayOccluder.cs:42-79). - portShape: Adopt retail's arm-then-resolve-in-draw shape: a click arms a selection ray (Render::pick_ray equivalent already exists as WorldPicker.BuildRay); during the unified DrawInside traversal, each entity that passes the same per-cell / per-portal-view gates the draw uses gets a sphere test (and later the Stage-B first-poly-hit test) into a MouseSelectData-style accumulator; harvest after the frame. CellBspRayOccluder and the per-pick cell-physics snapshot get deleted — occlusion falls out of 'not submitted by the flood → not testable', the one-gate discipline. ### [LOW] occluder-stricter-and-looser-than-retail (UNVERIFIED (verifier hit token limit)) — Cell-polygon ray occluder blocks picks retail allows and allows picks retail blocks - blastRadius: Stricter: an entity visually behind a baked column / interior wall of a cell that IS in the drawn flood is pickable in retail (cell geometry is invisible to the pick; only the sphere-vs-portal-view gate applies) but blocked in acdream by the wall polygon's smaller ray-t; outdoors, an NPC standing behind a building is retail-pickable (drawn, z-buffer notwithstanding) but acdream's ray may cross the building's loaded interior EnvCells and block. Looser: beyond the streamed cell-physics window there is no occlusion at all. Both directions are rare in practice and arguably acdream's strict side feels nicer — but it is not retail, and it hard-codes a second visibility oracle. Possible doorway-blocking variant is an open question (see openQuestions). - retailEvidence: DrawEnvCell 0x0059f170 (pc:427898-427935) submits cell polys with no selection hook; GfxObjUnderSelectionRay 0x0054c740 requires s_current_physics_part, set only in CPhysicsPart::Draw 0x0050d7a0 — cell geometry never participates; occlusion granularity is drawing_sphere vs portal view cone in DrawMesh 0x005a0860. - acdreamEvidence: CellBspRayOccluder.NearestWallT (CellBspRayOccluder.cs:42-79) tests every Resolved physics polygon of every loaded EnvCell (snapshot at GameWindow.cs:10535-10540); WorldPicker.Pick skips candidates deeper than the wall depth (WorldPicker.cs:276). - portShape: Subsumed by pick-outside-draw-traversal: once the pick rides the draw traversal, delete the occluder rather than tuning it. No standalone fix recommended. ### [LOW] no-poly-stage-no-poly-beats-sphere (UNVERIFIED (verifier hit token limit)) — No polygon stage: screen-rect sphere test replaces retail's first-poly-hit + poly-beats-sphere arbitration - blastRadius: Over-pick on entities whose visible mesh is much smaller than their SelectionSphere rect (the documented #71 deferral), and no per-part precision on multi-part setups (retail tests each CPhysicsPart's gfxobj drawing_sphere + flat poly array; acdream tests one Setup-level SelectionSphere). Deliberate Stage-A design that intentionally matches the drawn indicator rect — only worth revisiting if visual testing surfaces an over-pick, exactly as WorldPicker.cs:199-204 already states. - retailEvidence: GfxObjUnderSelectionRay 0x0054c740 (Ghidra): per-part CSphere::sphere_intersects_ray(drawing_sphere) coarse gate, then first-hit-in-array-order scan of gfxobj->polygons[0..num_polygons] via CPolygon::polygon_hits_ray 0x005395E0; GetMouseSelectionObjectID 0x0054C950 returns PolygonID whenever any polygon hit exists, else SphereID — polygon hits categorically beat sphere hits. - acdreamEvidence: WorldPicker.Pick screen-rect overload (WorldPicker.cs:210-285): Setup.SelectionSphere → screen rect + 8 px inflate → nearest clip.W; Stage B deferred per issue #71 (WorldPicker.cs:199-204); sphere source is the Setup-level SelectionSphere (GameWindow.cs:11010-11045), not per-part drawing_spheres. - portShape: When the pick moves into the draw traversal, Stage B becomes natural: per drawn part, sphere gate on the part's mesh bounding sphere then first-poly-hit against the part's flat poly list (the mesh data WbMeshAdapter already decodes), with the retail accumulator arbitration. Keep #71's trigger condition — only do it when an over-pick is actually observed. ### [LOW] no-selected-in-view-tracking (UNVERIFIED (verifier hit token limit)) — No selected_object_in_view analog — indicator persists and selection never auto-deselects on occlusion/range - blastRadius: Target indicator brackets keep drawing on a selected entity that walks behind a wall or that the flood stops drawing, and selection never expires by range; retail hides/deselects via drawn-this-frame tracking + 25 m indoor / 75 m outdoor range handlers. Pure polish; no filed issue. - retailEvidence: SmartBox::set_selected_object_id 0x00451BA0 (pc:90768) sets CPhysicsPart::viewcone_check_object_id; CPhysicsPart::Draw 0x0050d7a0 (Ghidra) sets selected_object_in_view=1 when DrawMesh returns 2 (drawn) for the matching physobj->id; CPlayerSystem::OnObjectRangeExit 0x00560870 (pc:365073-365090) deselects via ACCWeenieObject::SetSelectedObject(0,0) when range-exited and not in view, else re-registers at 25 m indoors / 75 m outdoors (0x005608a1). - acdreamEvidence: _selectedGuid persists until the next pick (GameWindow.cs:10570-10572); TargetIndicatorPanel resolver (GameWindow.cs:1278-1322) projects the SelectionSphere unconditionally — no drawn-this-frame flag, no range/visibility deselect. - portShape: One flag on the draw traversal: when the per-entity draw submission processes the currently-selected guid, mark selectedDrawnThisFrame; indicator reads it; a range-exit hook (the interaction layer already tracks distances for UseRadius) deselects when out-of-range AND not drawn. Small, fully parasitic on the holistic port's unified entity walk. ## OPEN QUESTIONS - Does Chorizite DatReaderWriter's CellStruct.PhysicsPolygons (the occluder's polygon set, PhysicsDataCache.cs:176/208) include polygons spanning OPEN doorway portals? Retail-side I confirmed CCellStruct::UnPack (Ghidra 0x00533D00) resolves the portal index list into the VISIBLE polygons array (`portals[i] = polygons + idx`), matching acdream's PhysicsDataCache.cs:178 comment — but that says nothing about whether physics_polygons ALSO contains a coplanar quad at the opening. If it does, CellBspRayOccluder blocks clicks on entities visible through an open doorway (user-visible: 'can't select the NPC in the next room'). Testable in 2 minutes in-client at the Holtburg inn vestibule, or by dumping a doorway cell via the existing CellDump apparatus and ray-testing through the portal. Becomes moot if the pick moves into the draw traversal. - CPhysicsPart::creature_mode (static, 0x00843bf4) has no writer anywhere in the 1.4M-line named pseudo-C — either dead (gate is purely physobj->id != 0) or written by raw address from the obfuscated/packed minority. Does not change in-world conclusions, but a writer hiding in packed code could widen the pick gate in some mode I haven't identified. - Retail does not exclude the local player from the pick (CPhysicsPart::Draw gates only on id != 0; the player has an id and is drawn in third person) — I did not trace whether ECM_UI::SendNotice_SmartBoxObjectFound consumers accept a self-click as a selection. acdream explicitly skips the player (WorldPicker.cs:254, skipServerGuid). Worth one retail-client test before porting the harvest path. - The consumer chain of ECM_UI::SendNotice_SmartBoxObjectFound (what the UI layer does when click_object_id is 0 or resolves to no weenie) was not traced — I assumed deselect-on-zero. Affects only the harvest port's edge cases, not the geometry/visibility conclusions. - Whether retail's sphere-hit fallback (GetMouseSelectionObjectID returns SphereID only when NO polygon hit exists anywhere in the frame) matters in practice for tiny/poly-less objects — determines whether acdream's Stage-A rect test is closer to retail's fallback than to its primary path, i.e. how urgent #71 (Stage B poly refine) really is.