"""Typed delivery/reward observations and proposals; import reads bundled source pins only.

Only the explicitly enabled session opens the existing read-only transport.
Unknown model identity, ownership or occlusion never becomes an input target.
"""
from mini_masukja.native_type_reference import type_definition_index
from mini_masukja import native_canvas_rules as _canvas_rules
from copy import deepcopy
from datetime import datetime, timezone
import math
import struct

from .ui_layout_observer import CurrentUILayoutSession, XForestView, delivery_ownership
from .vendor.map_memory import MapReadPending
from .vendor.map_character_context import _class
from .arbeit_targets import ui_map, build_target
from .quest_item_delivery import (SOURCE_HASH as DELIVERY_POLICY_HASH, read_delivery_policy,
    selection_instances, choose_default_item, expected_selection, verify_current_policy)
from .quest_item_delivery_group import read_delivery_group, delivery_group_npc
from .quest_item_delivery_request import delivery_group_request, group_request_ownership
from pathlib import Path
import hashlib
import re

SOURCE_REF='data/arbeit-quest-ui-r3/source-pins.json'
# Bundled, file-pinned declarations and exact native bodies. Analysis is not a runtime dependency.
import json as _json
from .paths import root as _data_root
_SOURCE_RAW=(_data_root()/SOURCE_REF).read_bytes()
if hashlib.sha256(_SOURCE_RAW).hexdigest()!='75fa7072947df55b285a7b4e935879b6e431f36d206ba558b3ae22ad0fbd45e4':
    raise ValueError('Bundled quest UI source pins changed')
_SOURCE_PINS=_json.loads(_SOURCE_RAW)
SOURCE_VERSION=_SOURCE_PINS['source_version']
SOURCE_SHAPES=_SOURCE_PINS['source_shapes']
NATIVE_SOURCE=_SOURCE_PINS['native_source']
POINTER_SOURCE=_SOURCE_PINS['pointer_source']
del _SOURCE_RAW, _SOURCE_PINS


def _stamp(value):
    result=datetime.fromisoformat(value.replace('Z','+00:00'))
    if result.tzinfo is None:raise ValueError('timezone required')
    return result


def _fresh(value,frame):
    try:
        return -.5<=(_stamp(frame['captured_at'])-_stamp(value['observed_at'])).total_seconds()<=5
    except (KeyError,TypeError,ValueError,AttributeError):return False


def _read_near_capture(snapshot,frame):
    # The typed read follows RAM capture. Preserve both original times rather
    # than backdating a later read to the pixel capture.
    try:return abs((_stamp(frame['captured_at'])-_stamp(snapshot['captured_at'])).total_seconds())<=5
    except (KeyError,TypeError,ValueError,AttributeError):return False


def _normal(value):return ''.join(str(value).split())


def _unity_module(reader):
    """Bounded read-only module query, same access as the existing Reader."""
    import ctypes as c
    from ctypes import wintypes as w
    api=c.WinDLL('psapi',use_last_error=True)
    api.EnumProcessModulesEx.argtypes=[w.HANDLE,c.POINTER(w.HMODULE),w.DWORD,c.POINTER(w.DWORD),w.DWORD]
    api.EnumProcessModulesEx.restype=w.BOOL
    api.GetModuleFileNameExW.argtypes=[w.HANDLE,w.HMODULE,w.LPWSTR,w.DWORD]
    api.GetModuleFileNameExW.restype=w.DWORD
    modules,needed=(w.HMODULE*4096)(),w.DWORD()
    if not api.EnumProcessModulesEx(reader.handle,modules,c.sizeof(modules),c.byref(needed),3):
        raise OSError(c.get_last_error(),'Read-only Unity UI module query failed')
    if needed.value>c.sizeof(modules):raise ValueError('Unity UI module query exceeds bound')
    matches=[]
    for index in range(needed.value//c.sizeof(w.HMODULE)):
        path=c.create_unicode_buffer(32768)
        if api.GetModuleFileNameExW(reader.handle,modules[index],path,len(path)) and Path(path.value).name.casefold()=='unityplayer.dll':
            if Path(path.value).resolve().parent!=Path(reader.image).resolve().parent:
                raise ValueError('Current Unity UI module belongs to a different installation')
            matches.append((int(modules[index]),path.value))
    if len(matches)!=1:raise ValueError('Exact current Unity UI module unavailable')
    return matches[0]


def _apply_trs(point,position,rotation,scale):
    """Source Transform position chain: scale, quaternion rotate, translate."""
    if not all(math.isfinite(x) for x in (*point,*position,*rotation,*scale)):
        raise ValueError('Current native transform contains nonfinite values')
    if abs(sum(x*x for x in rotation)-1)>1e-4 or min(scale)<=0:
        raise ValueError('Current native UI quaternion/scale unsupported')
    x,y,z=(point[i]*scale[i] for i in range(3));qx,qy,qz,qw=rotation
    tx=2*(qy*z-qz*y);ty=2*(qz*x-qx*z);tz=2*(qx*y-qy*x)
    return (x+qw*tx+qy*tz-qz*ty+position[0],
            y+qw*ty+qz*tx-qx*tz+position[1],
            z+qw*tz+qx*ty-qy*tx+position[2])


class NativeUnityUI:
    """Read known UI getter data without calling any client/native getter.

    A named GameAssembly ICall must already be resolved. Both source bodies and
    loaded module bytes are pinned before their data layout can be consumed.
    Unresolved caches/unknown getter forms remain unavailable.
    """
    def __init__(self,session,*,module_provider=_unity_module):
        self.s=session;self.r=session.reader;self.base,path=module_provider(self.r)
        self.raw=Path(path).read_bytes()
        if hashlib.sha256(self.raw).hexdigest()!=NATIVE_SOURCE['unity_player_sha256']:
            raise ValueError('Current Unity UI module source changed')
        pe=struct.unpack_from('<I',self.raw,0x3c)[0]
        if self.raw[pe:pe+4]!=b'PE\0\0':raise ValueError('Pinned Unity UI PE signature missing')
        count=struct.unpack_from('<H',self.raw,pe+6)[0];optional=struct.unpack_from('<H',self.raw,pe+20)[0]
        if not 1<=count<=96 or struct.unpack_from('<H',self.raw,pe+24)[0]!=0x20b:
            raise ValueError('Pinned Unity UI PE layout changed')
        self.sections=[]
        for index in range(count):
            entry=pe+24+optional+index*40
            size,rva,raw_size,offset=struct.unpack_from('<4I',self.raw,entry+8)
            flags=struct.unpack_from('<I',self.raw,entry+36)[0]
            self.sections.append((rva,size,offset,raw_size,flags))
        self.exceptions=struct.unpack_from('<2I',self.raw,pe+24+112+3*8)

    def file_bytes(self,rva,size,*,executable=True):
        for start,length,offset,raw_size,flags in self.sections:
            if start<=rva and rva+size<=start+min(length,raw_size) and (not executable or flags&0x20000000):
                return self.raw[offset+rva-start:offset+rva-start+size]
        raise ValueError('Named UI getter lies outside pinned executable section')

    def resolved(self,owner,method):
        rows=[w for w in NATIVE_SOURCE['wrappers'] if (w['owner'],w['method'])==(owner,method)]
        if len(rows)!=1:raise ValueError('No source-pinned named UI getter')
        row=rows[0];code=bytes.fromhex(row['raw_hex'])
        if self.r.read(self.s.base+row['wrapper_rva'],len(code))!=code:
            raise ValueError('Current named UI getter wrapper changed')
        name=(row['icall_name']+'\0').encode()
        if self.r.read(self.s.base+row['name_rva'],len(name))!=name:
            raise ValueError('Current named UI getter resolver string changed')
        pointer=struct.unpack('<Q',self.s.watch(self.s.base+row['cache_rva'],8))[0]
        if not pointer:raise MapReadPending('Named UI getter cache unresolved; resolver invocation is forbidden')
        rva=pointer-self.base
        expected=self.file_bytes(rva,16)
        if self.r.read(pointer,16)!=expected:raise ValueError('Current native UI getter entry changed')
        return rva

    def native_pointer(self,managed,expected_types):
        if not managed:raise MapReadPending('Current Unity UI object unavailable')
        klass=self.r.u64(managed)
        name,namespace=self.r.class_name(klass)
        if name not in expected_types or namespace!='UnityEngine':
            raise ValueError('Current Unity UI object type differs from named getter')
        for _ in range(8):
            if self.r.class_name(klass)==('Object','UnityEngine'):break
            klass=self.r.u64(klass+0x58)
        else:raise ValueError('Current Unity object base unavailable')
        fields=self.s.fields(klass,self.s.source_shape('Object','UnityEngine'))
        pointer=struct.unpack('<Q',self.s.watch(managed+fields['m_CachedPtr'],8))[0]
        if not pointer:raise MapReadPending('Current Unity UI object was destroyed')
        return pointer

    def pinned_entry(self,owner,method,rva,size):
        if self.resolved(owner,method)!=rva:
            raise ValueError('Named native UI getter does not match the inspected data-layout candidate')
        code=self.file_bytes(rva,size)
        if self.r.read(self.base+rva,size)!=code:raise ValueError('Current native getter body changed')

    def rect_world(self,managed_rect):
        """Exact clean local rect + the current complete TRS parent chain.

        Output is world space. It must not be consumed as screen coordinates.
        """
        self.pinned_entry('RectTransform','get_rect_Injected',0x86430,317)
        self.pinned_entry('Transform','get_position_Injected',0x8c3a0,329)
        for rva,size in ((0x5469b0,63),(0x546750,420),(0x54c680,422)):
            if self.r.read(self.base+rva,size)!=self.file_bytes(rva,size):raise ValueError('Native UI transform source body changed')
        native=self.native_pointer(managed_rect,{'RectTransform'})
        if self.s.watch(native+0xf0,2)!=b'\0\0':raise MapReadPending('Current native rect/anchor data dirty')
        rect=struct.unpack('<4f',self.s.watch(native+0xb8,16))
        if not all(math.isfinite(v) for v in rect) or min(rect[2:])<=0:raise ValueError('Current native local rect invalid')
        hierarchy,index=struct.unpack('<Qi',self.s.watch(native+0x38,12))
        if not hierarchy or self.r.u64(hierarchy)==0:raise MapReadPending('Current native transform hierarchy not initialized')
        arrays=struct.unpack('<2Q',self.s.watch(hierarchy+0x18,16))
        dirty=struct.unpack('<Q',self.s.watch(hierarchy+0x48,8))[0]
        bit=struct.unpack('<i',self.s.watch(self.base+0x1a914a0,4))[0]
        if not all(arrays) or not dirty or not 0<=bit<64:raise ValueError('Current native transform arrays/dirty bit unavailable')
        chain=[];seen=set()
        while index>=0:
            if index in seen or not 0<=index<=100000 or len(chain)>=128:
                raise ValueError('Current native transform ancestry exceeds bounded source traversal')
            seen.add(index)
            flags=struct.unpack('<Q',self.s.watch(dirty+index*8,8))[0]
            if flags&(1<<bit):raise MapReadPending('Current native rect ancestor dirty')
            values=struct.unpack('<12f',self.s.watch(arrays[0]+48*index,48))
            chain.append((values[:3],values[4:8],values[8:11]))
            index=struct.unpack('<i',self.s.watch(arrays[1]+index*4,4))[0]
        x,y,w,h=rect;corners=[(x,y,0),(x+w,y,0),(x+w,y+h,0),(x,y+h,0)]
        for position,rotation,scale in chain:
            corners=[_apply_trs(point,position,rotation,scale) for point in corners]
        self.s.assert_stable()
        return {'corners_world':[list(p) for p in corners],'coordinate_space':'world',
                'native_pointer':native,'hierarchy':hierarchy,'ancestor_count':len(chain),
                'source':'named pinned RectTransform/Transform getter data and clean current TRS hierarchy',
                'source_hashes':[NATIVE_SOURCE['unity_player_sha256'],SOURCE_VERSION['game_assembly_sha256']]}

    def active_cached(self,managed_game_object):
        native=self.native_pointer(managed_game_object,{'GameObject'})
        return self.native_active_cached(native)

    def component_game_object(self,managed):
        self.pinned_entry('Component','get_gameObject',0xe6330,365)
        klass=self.r.u64(managed);seen=set();found=False
        while klass and klass not in seen and len(seen)<16:
            seen.add(klass)
            if self.r.class_name(klass)==('Component','UnityEngine'):found=True
            if self.r.class_name(klass)==('Object','UnityEngine'):break
            klass=self.r.u64(klass+0x58)
        if not found or self.r.class_name(klass)!=('Object','UnityEngine'):
            raise ValueError('Current raycast target is not a source Unity Component')
        offsets=self.s.fields(klass,self.s.source_shape('Object','UnityEngine'))
        native=struct.unpack('<Q',self.s.watch(managed+offsets['m_CachedPtr'],8))[0]
        if not native:raise MapReadPending('Current raycast component was destroyed')
        game_object=struct.unpack('<Q',self.s.watch(native+0x30,8))[0]
        if not game_object:raise MapReadPending('Current raycast GameObject unavailable')
        return game_object

    def native_active_cached(self,native):
        self.pinned_entry('GameObject','get_activeInHierarchy',0xef540,298)
        if self.r.read(self.base+0x298070,206)!=self.file_bytes(0x298070,206):
            raise ValueError('Current native active hierarchy source body changed')
        value=self.s.watch(native+0x57,1)[0]
        if value not in (0,1):raise MapReadPending('Current hierarchy activity cache unresolved')
        return bool(value)

    def function_bytes(self,rva,limit=4096):
        table,size=self.exceptions
        rows=self.file_bytes(table,size,executable=False)
        lo,hi=0,len(rows)//12
        while lo<hi:
            mid=(lo+hi)//2;begin,end,_=struct.unpack_from('<3I',rows,mid*12)
            if begin<rva:lo=mid+1
            else:hi=mid
        if lo>=len(rows)//12:raise ValueError('Named native UI getter lacks a bounded source extent')
        begin,end,_=struct.unpack_from('<3I',rows,lo*12)
        if begin!=rva or not 0<end-begin<=limit:raise ValueError('Named native UI getter source extent unsupported')
        raw=self.file_bytes(begin,end-begin)
        if self.r.read(self.base+begin,len(raw))!=raw:raise ValueError('Current native UI getter body differs from pinned file')
        return raw

    def scalar(self,managed,owner,method):
        return _canvas_rules.read_scalar(self,managed,owner,method,pending=MapReadPending)

    def canvas_for_transform(self,transform,canvases):
        return _canvas_rules.canvas_for_transform(self,transform,canvases,pending=MapReadPending)

    def overlay_rect(self,managed_rect,canvas,viewport,*,allow_clipped=False,native_canvas=False):
        return _canvas_rules.overlay_rect(self,managed_rect,canvas,viewport,pending=MapReadPending,
            allow_clipped=allow_clipped,native_canvas=native_canvas)


class QuestUIActionSession(CurrentUILayoutSession):
    """Actual rooted reader extension. All methods use the inherited read guard."""
    def source_shape(self,name,namespace=None):
        if namespace is None:
            namespace={'ButtonControl':'MM.Client.Framework.UICommon.Buttons',
                'ButtonState':'MM.Client.Framework.UICommon.Buttons'}.get(name)
        rows=[s for s in SOURCE_SHAPES if s['name']==name and (namespace is None or s['namespace']==namespace)]
        if rows:
            if len(rows)!=1:raise ValueError('Quest UI source type is ambiguous: '+name)
            if any(self.plan.get(k)!=SOURCE_VERSION[k] for k in SOURCE_VERSION):
                raise ValueError('Quest UI source version differs from current reader plan')
            return rows[0]
        return super().source_shape(name,namespace)

    def typed_fields(self,pointer,name):
        if not pointer:raise MapReadPending('Current quest UI object missing: '+name)
        return self.fields(self.reader.u64(pointer),self.source_shape(name))

    def field(self,pointer,name,member):
        shape=self.source_shape(name);offsets=self.typed_fields(pointer,name)
        row=next(x for x in shape['fields'] if x['name']==member)
        address=pointer+offsets[member];code=row['type']['type_code']
        if code==17:
            enum=self.source_shape(row['type']['name'],row['type']['namespace'])
            underlying=next((f for f in enum['fields'] if f['name']=='value__'),None)
            if not underlying or underlying['type']['type_code']!=8:raise ValueError('Selected UI inline field is not an Int32 enum')
            type_pointer=self.reader.u64(self.reader.u64(self.reader.u64(pointer)+0x80)+32*row['index']+8)
            if type_definition_index(self, type_pointer)!=enum['type_index']:raise ValueError('Current UI enum definition changed')
            return struct.unpack('<i',self.watch(address,4))[0]
        fmt={2:'<?',8:'<i',9:'<I',10:'<q',11:'<Q',12:'<f',14:'<Q',18:'<Q',21:'<Q',28:'<Q',29:'<Q'}.get(code)
        if not fmt:raise ValueError('Selected quest UI field is not a scalar/reference: '+member)
        value=struct.unpack(fmt,self.watch(address,struct.calcsize(fmt)))[0]
        if code==2 and self.reader.read(address,1)[0] not in (0,1):raise ValueError('Invalid UI Boolean')
        return self.reader.string(value) if code==14 and value else value

    def value_array(self,pointer,element,limit=512):
        """Closed List/MMList of structs, with original version/array guards."""
        r=self.reader;klass=r.u64(pointer);name,namespace=r.class_name(klass)
        if name not in ('List`1','MMList`1','MMReadOnlyList`1'):
            raise ValueError('Unsupported current value-list class')
        current=klass
        for _ in range(4):
            fields={x['name'].lstrip('_'):x for x in r.layout(current)['fields']}
            if {'items','size','version'}<=fields.keys():array_field='items';break
            if {'array','size','version'}<=fields.keys():array_field='array';break
            current=r.u64(current+0x58)
        else:raise ValueError('Current value list fields unavailable')
        if fields['size']['type_code']!=8 or fields['version']['type_code']!=8:
            raise ValueError('Current value list count type changed')
        count=struct.unpack('<i',self.watch(pointer+fields['size']['offset'],4))[0]
        self.watch(pointer+fields['version']['offset'],4)
        if not 0<=count<=limit:raise ValueError('Current value list exceeds selected bound')
        array=struct.unpack('<Q',self.watch(pointer+fields[array_field]['offset'],8))[0]
        if not count:return []
        length,stride=self.array(array,name=element+'[]',limit=30000)
        shape=self.source_shape(element)
        self.fields(r.u64(r.u64(array)+0x40),shape)
        if count>length or stride!=shape['type_definition_size_raw'][0]-16:
            raise ValueError('Current value array stride/extent differs from source')
        self.watch(array+24,8);self.watch(array+32,count*stride)
        return [array+32+i*stride for i in range(count)]

    def inline_scalar(self,address,type_name,field):
        shape=self.source_shape(type_name);row=next(f for f in shape['fields'] if f['name']==field)
        offset=row['offset']-16;code=row['type']['type_code']
        fmt={2:'<?',8:'<i',10:'<q',12:'<f',14:'<Q',18:'<Q'}.get(code)
        # CommonItemData.Type is an inline enum, not an object reference.
        # Accept this one source-pinned enum/storage declaration only.
        if code==17 and type_name=='CommonItemData' and field=='Type':
            enum=self.source_shape('CommonItemDataType','Shared.Items')
            storage=[f for f in enum['fields'] if f['name']=='value__']
            if (row['type'].get('name')!=enum['name'] or row['type'].get('namespace')!=enum['namespace']
                    or row['type'].get('definition_index')!=enum['type_index']
                    or len(storage)!=1 or storage[0]['offset']!=16 or storage[0]['type']['type_code']!=8):
                raise ValueError('Current item enum storage source changed')
            fmt='<i'
        if not fmt or offset<0:raise ValueError('Unsupported inline UI value '+field)
        raw=self.watch(address+offset,struct.calcsize(fmt));value=struct.unpack(fmt,raw)[0]
        if code==2 and raw not in (b'\0',b'\1'):raise ValueError('Invalid inline Boolean')
        return self.reader.string(value) if code==14 and value else value

    def current_items(self,component):
        pointer=self.field(component,'DeliverUIViewModelComponent','myDeliverableItems')
        items=[]
        for index,address in enumerate(self.value_array(pointer,'CommonItemData')):
            fields={k:self.inline_scalar(address,'CommonItemData',k) for k in
                ('Type','ItemId','IdLong','SheetIdValue','Count','DisplayName','IsLocked','IsPreserved','UsedByQuest')}
            if fields['Count']<0 or not fields['ItemId']:raise ValueError('Current delivery item identity/count missing')
            enum=self.source_shape('CommonItemDataType','Shared.Items')['enum_constants']
            names=[name for name,value in enum.items() if value==fields['Type']]
            if len(names)!=1:raise ValueError('Current item type is not in pinned CommonItemDataType')
            kind=names[0];domain=_DELIVERY_ITEM_DOMAINS.get(kind)
            items.append(dict(index=index,item_type=fields['Type'],item_type_name=kind,
                table_domain=domain,domain_source=SOURCE_REF+'#CommonItemData.Type/CommonItemDataType',
                item_pointer=fields['ItemId'],item_id=str(fields['IdLong']),
                sheet_id=fields['SheetIdValue'],count=fields['Count'],name=fields['DisplayName'],
                locked=fields['IsLocked'],preserved=fields['IsPreserved'],used_by_quest=fields['UsedByQuest']))
        if len({x['item_pointer'] for x in items})!=len(items):raise ValueError('Current delivery item identities are duplicate')
        return items

    def allowed_sheets(self,context):
        pointer=self.field(context,'DeliverContext','allowedSheetIds');r=self.reader;klass=r.u64(pointer)
        if r.class_name(klass)!=('MMHashSet`1','Silvervine.ManualMemory'):
            raise ValueError('Allowed delivery sheet collection changed')
        for _ in range(4):
            name,namespace=r.class_name(klass)
            if name=='CommonHashSet`1' and namespace=='Silvervine.ManualMemory.Internal':break
            klass=r.u64(klass+0x58)
        else:raise ValueError('Allowed delivery sheet set base unavailable')
        source=self.source_shape('CommonHashSet`1');fields=r.layout(klass,[f['name'] for f in source['fields']])['fields']
        f={x['name']:x for x in fields}
        if f['count']['type_code']!=8 or f['version']['type_code']!=8:raise ValueError('Sheet set count type changed')
        count=struct.unpack('<i',self.watch(pointer+f['count']['offset'],4))[0]
        self.watch(pointer+f['version']['offset'],4)
        if not 0<=count<=128:raise ValueError('Allowed delivery sheet set exceeds bound')
        if count==0:return []
        array=struct.unpack('<Q',self.watch(pointer+f['values']['offset'],8))[0]
        length,_=self.array(array,name='Int32[]',stride=4,limit=30000)
        if count>length:raise ValueError('Allowed delivery sheet array truncated')
        values=list(struct.unpack('<'+'i'*count,self.watch(array+32,4*count)))
        if len(set(values))!=count:raise ValueError('Allowed delivery sheet set duplicate')
        return values

    def selected_items(self,context,items):
        """ItemsToDeliver -> actual BaseItemIconMetadata.Count -> same item ID."""
        pointer=self.field(context,'DeliverContext','<ItemsToDeliver>k__BackingField');r=self.reader
        info=r.dictionary(pointer)
        if info.get('kind')=='manual_dense' or not 0<=info['count']<=128:
            raise ValueError('ItemsToDeliver dictionary kind/count changed')
        self.watch(pointer+info['version_offset'],4);self.watch(pointer+info['count_offset'],4)
        if not info['count']:return []
        fields={f['name'].lower():f for f in info['entry_layout']['fields']}
        if (set(fields)!={'hashcode','next','key','value'} or fields['key']['type_code']!=18 or fields['value']['type_code']!=17):
            raise ValueError('ItemsToDeliver entry types changed')
        offsets={k:f['offset']-16 for k,f in fields.items()}
        value_size=self.source_shape('BaseItemIconMetadata')['type_definition_size_raw'][0]-16
        if min(offsets.values())<0 or offsets['value']+value_size>info['stride']:
            raise ValueError('Selected item metadata exceeds dictionary entry')
        array=int(info['entries'],16);self.watch(array+24,8)
        lookup={i['item_pointer']:i for i in items};selected=[]
        for index in range(info['used']):
            address=array+32+index*info['stride'];self.watch(address,info['stride'])
            if r.i32(address+offsets['hashcode'])<0:continue
            key=r.u64(address+offsets['key']);item=lookup.get(key)
            if item is None:raise MapReadPending('Selected item ID no longer belongs to current deliverable list')
            meta=address+offsets['value'];count=self.inline_scalar(meta,'BaseItemIconMetadata','<Count>k__BackingField')
            name=self.inline_scalar(meta,'BaseItemIconMetadata','<DisplayName>k__BackingField')
            if not 0<count<=item['count'] or _normal(name)!=_normal(item['name']):
                raise ValueError('Selected item metadata quantity/identity differs from current item')
            selected.append(dict(item_id=item['item_id'],item_pointer=key,sheet_id=item['sheet_id'],name=name,count=count,
                table_domain=item.get('table_domain'),item_type=item.get('item_type'),item_type_name=item.get('item_type_name')))
        if len(selected)!=info['count'] or len({x['item_pointer'] for x in selected})!=len(selected):
            raise MapReadPending('Selected delivery collection changed during traversal')
        return selected

    def delivery_contexts(self,component,fields):
        items=self.current_items(component);contexts=[]
        pointer=self.field(component,'DeliverUIViewModelComponent','deliverContexts')
        for index,entry in enumerate(self.refs(pointer,element='DeliverContext',limit=64)):
            amount=self.field(entry,'DeliverContext','TargetAmount')
            if amount<=0:raise ValueError('Current delivery requirement is not positive')
            selected=self.selected_items(entry,items);allowed=self.allowed_sheets(entry)
            if any(x['sheet_id'] not in allowed for x in selected):raise ValueError('Selected item outside current allowed sheet IDs')
            contexts.append(dict(index=index,target_amount=amount,
                target_display_name=self.field(entry,'DeliverContext','TargetDisplayName'),allowed_sheet_ids=allowed,
                selected_items=selected,selected_amount=sum(x['count'] for x in selected),
                allow_locked_item=self.field(entry,'DeliverContext','allowLockedItem'),
                source='current DeliverContext.ItemsToDeliver/BaseItemIconMetadata joined to current CommonItemData.ItemId'))
        self._delivery_items=items
        try:self._delivery_policy=read_delivery_policy(self,component)
        except (OSError,RuntimeError,ValueError,KeyError,TypeError) as exc:
            self._delivery_policy=dict(confirmed=False,reason=str(exc))
        try:self._delivery_group=read_delivery_group(self,component)
        except (OSError,RuntimeError,ValueError,KeyError,TypeError) as exc:
            self._delivery_group=dict(confirmed=False,reason=str(exc))
        return contexts

    def current_model(self,ui,fields):
        self._current_ui=ui
        model=super().current_model(ui,fields)
        if self.reader.class_name(self.reader.u64(ui))[0]!='DeliverUIInstance':return model
        pointer=self.declared_ref(ui,fields,'viewModel');mf=self.declared_fields(pointer,'MMMLUIViewModel`2')
        address=pointer+mf['<ViewModelRoot>k__BackingField']['offset'];self.watch(address,16)
        forest=self.reader.u64(address);ident=self.reader.i32(address+8);view=XForestView(self,forest)
        root=view.attributes(ident,{'uiData'}).get('uiData')
        if not isinstance(root,dict) or root.get('forest')!=forest:raise MapReadPending('Current delivery uiData root missing')
        desc=view.attributes(root['element_id'],{'deliverableItems','targetDeliverItems'})
        for role in ('deliverableItems','targetDeliverItems'):
            collection=desc.get(role)
            if not isinstance(collection,dict) or collection.get('forest')!=forest:raise MapReadPending('Current delivery model collection missing: '+role)
            rows=[]
            for index,child in enumerate(view.children(collection['element_id'])):
                if index>=512:raise ValueError('Current delivery model collection exceeds bound')
                values=view.attributes(child,{'uiData','itemData','onclick','onLongPress','displayName','isSelected','isEmpty'})
                descriptor=values.get('uiData')
                if isinstance(descriptor,dict) and descriptor.get('forest')==forest:
                    values=view.attributes(descriptor['element_id'],{'itemData','onclick','onLongPress','displayName','isSelected','isEmpty'})
                data=values.get('itemData');icon={}
                if isinstance(data,dict) and data.get('forest')==forest:
                    icon=view.attributes(data['element_id'],{'displayName','isLocked','disabled','countText'})
                rows.append(dict(index=index,element_id=child,values=values,item_data=icon))
            model[role]=rows
        return model

    def current_layout(self,target_id):
        if target_id=='reward.confirm':return self.current_reward()
        snapshot=super().current_layout(target_id)
        snapshot['ui_pointer']=self._current_ui
        try:self.bind_runtime_callbacks(snapshot)
        except (ValueError,RuntimeError,OSError,KeyError) as exc:
            snapshot['missing'].append('current callback binding: '+str(exc))
        if target_id.startswith('delivery.'):
            snapshot['details']['deliverable_items']=self._delivery_items
            snapshot['details']['typed_selection_complete']=True
            snapshot['details']['delivery_policy']=self._delivery_policy
            snapshot['details']['delivery_group']=self._delivery_group
        # Preserve actual observations even when they cannot certify clearance.
        for candidate in snapshot.get('candidates',[]):
            candidate['occlusion_observation']=self.read_target_occlusion(
                snapshot['window_id'],candidate['rect_ui'],snapshot.get('ui_pointer'))
        self.assert_stable()
        return snapshot

    def bind_runtime_callbacks(self,snapshot,handle=None):
        """LayoutVM exact index→ElementId→current runtime XForest callback.

        Solid-list offsets are reflected from its actual closed generic class;
        open generic zero offsets and visual row ordering are never used.
        """
        r=self.reader
        if handle is None:
            ui=self._current_ui
            name=r.class_name(r.u64(ui))[0];fields=self.declared_fields(ui,name)
            controller=self.declared_ref(ui,fields,'controller')
            parent=r.u64(r.u64(controller)+0x58)
            base=self.fields(parent,self.schema('MMMLUIControllerBase'))
            handle=struct.unpack('<Q',self.watch(controller+base['handle'],8))[0]
        h=self.object_fields(handle,'MMMLUIHandle');window_id=self.window_id(handle)
        if window_id!=snapshot['window_id']:raise MapReadPending('Current callback window changed')
        service=self.ref(handle,h,'service');sf=self.object_fields(service,'MMMLService')
        windows=[p for k,p in self.dictionary_values(self.ref(service,sf,'entryMap'),key_type='Int32',value_type='UnityMMMLWindow',limit=4096) if k==window_id]
        if len(windows)!=1:raise MapReadPending('Current callback window unavailable')
        window=windows[0];base_class=r.u64(r.u64(window)+0x58)
        b=self.fields(base_class,self.schema('MMMLWindow'))
        context=struct.unpack('<Q',self.watch(window+b['mainLayoutContext'],8))[0]
        c=self.object_fields(context,'LayoutVMContext');vm=self.ref(context,c,'VM')
        vs=self.schema('LayoutVM');v=self.fields(r.u64(vm),vs)
        field=next(f for f in vs['fields'] if f['name']=='elementIdFromElementIndex')
        type_pointer=r.u64(r.u64(r.u64(vm)+0x80)+32*field['index']+8)
        if ((r.u32(type_pointer+8)>>16)&255)!=21:raise ValueError('Current element index type changed')
        generic=r.u64(type_pointer);generic_type=r.u64(generic)
        if type_definition_index(self, generic_type)!=self.source_shape('MMSolidList`1')['type_index']:
            raise ValueError('Current element index generic definition changed')
        closed=r.u64(generic+24)
        if not closed or r.class_name(closed)!=('MMSolidList`1','Silvervine.ManualMemory'):
            raise MapReadPending('Current closed element index class unavailable')
        layout=r.layout(closed,['chunk','offset','Count']);f={x['name']:x for x in layout['fields']}
        if [f[k]['type_code'] for k in ('chunk','offset','Count')]!=[29,8,8]:raise ValueError('Current solid element index fields changed')
        address=vm+v['elementIdFromElementIndex'];offsets={k:x['offset']-16 for k,x in f.items()}
        if any(o<0 or o+({'chunk':8}.get(k,4))>16 for k,o in offsets.items()):raise ValueError('Current solid element index exceeds source 16-byte field')
        array=struct.unpack('<Q',self.watch(address+offsets['chunk'],8))[0]
        first=struct.unpack('<i',self.watch(address+offsets['offset'],4))[0]
        count=struct.unpack('<i',self.watch(address+offsets['Count'],4))[0]
        length,_=self.array(array,name='ElementId[]',stride=4,limit=30000)
        self.fields(r.u64(r.u64(array)+0x40),self.source_shape('ElementId'))
        if not 0<=first<=first+count<=length:raise ValueError('Current solid element index bounds invalid')
        forest=struct.unpack('<Q',self.watch(window+b['runtimeForest'],8))[0]
        if self.ref(vm,v,'Forest')!=forest:raise MapReadPending('Current layout/runtime forest differ')
        view=XForestView(self,forest)
        wf=self.typed_fields(window,'UnityMMMLWindow')
        elements=dict(self.dictionary_values(self.ref(window,wf,'elements'),key_type='ElementId',value_type='MMMLElementBase',limit=30000))
        for candidate in snapshot['candidates']:
            index=candidate['index']
            if not 0<=index<count:raise ValueError('Current candidate outside element index list')
            ident=struct.unpack('<i',self.watch(array+32+4*(first+index),4))[0]
            attrs=view.attributes(ident,{'onclick','disabled'})
            candidate.update(element_id=ident,runtime_attributes=attrs,
                callback_source='current LayoutVM.elementIdFromElementIndex/runtimeForest')
            event=attrs.get('onclick')
            if isinstance(event,dict) and event.get('event_present') is True:
                candidate['model_event_id']=event['event_id']
            element=elements.get(ident)
            if element:
                ef=self.base_fields(element,'MMMLElementBase')
                if self.reader.i32(element+ef['Id'])!=ident or self.reader.u64(element+ef['Window'])!=window:
                    raise MapReadPending('Current MMML element identity/window changed')
                if self.watch(element+ef['IsDisposed'],1)!=b'\0' or self.watch(element+ef['IsLayoutDirty'],1)!=b'\0':
                    continue
                candidate.update(control_pointer=element,root_object_pointer=struct.unpack('<Q',self.watch(element+ef['RootObject'],8))[0])
                try:
                    bf=self.base_fields(element,'MMMLButtonElement')
                    candidate['button_pointer']=struct.unpack('<Q',self.watch(element+bf['buttonControl'],8))[0]
                except ValueError:pass

    def base_fields(self,pointer,name,namespace=None):
        shape=self.source_shape(name,namespace);klass=self.reader.u64(pointer);seen=set()
        while klass and klass not in seen and len(seen)<24:
            seen.add(klass)
            if self.reader.class_name(klass)==(name,shape['namespace']):return self.fields(klass,shape)
            klass=self.reader.u64(klass+0x58)
        raise ValueError('Current source base class unavailable: '+name)

    def slot_raw(self,slot):
        r=self.reader;klass=r.u64(slot)
        if r.class_name(klass)!=('UISlot`1','MM.Client.Framework.UI.Object'):
            raise ValueError('Current UI slot type changed')
        names=[f['name'] for f in self.source_shape('UISlot`1')['fields']]
        fields={f['name']:f for f in r.layout(klass,names)['fields']}
        raw=fields['<Raw>k__BackingField']
        if raw['type_code'] not in (18,19) or not 16<=raw['offset']<=128:
            raise ValueError('Current closed UI slot is not a bounded reference')
        pointer=struct.unpack('<Q',self.watch(slot+raw['offset'],8))[0]
        if not pointer:raise MapReadPending('Current UI slot disposed')
        return pointer

    def root_transform(self,root):
        name,namespace=self.reader.class_name(self.reader.u64(root))
        if (name,namespace)==('UIChildTransform','MM.Client.Framework.UI.Object'):
            return self.field(root,'UIChildTransform','<Transform>k__BackingField')
        if (name,namespace)==('UISlot`1','MM.Client.Framework.UI.Object'):
            raw=self.slot_raw(root)
            if self.reader.class_name(self.reader.u64(raw))==('RectTransform','UnityEngine'):return raw
        raise MapReadPending('A incomplete: current RootObject implementation has no supported source transform')

    def root_game_object(self,root,native):
        name,namespace=self.reader.class_name(self.reader.u64(root))
        if (name,namespace)==('UIChildTransform','MM.Client.Framework.UI.Object'):
            component=self.field(root,'UIChildTransform','<Transform>k__BackingField')
        elif (name,namespace)==('UISlot`1','MM.Client.Framework.UI.Object'):
            component=self.slot_raw(root)
        else:raise MapReadPending('A incomplete: unsupported current RootObject implementation')
        # Raw can be a ButtonSlot/Graphic/RectTransform component. Its exact
        # source Unity Component ancestor is validated by the native helper.
        return native.component_game_object(component)

    def read_control_geometry(self,button,ui_pointer,frame):
        root=self.field(button,'ButtonControl','<RootObject>k__BackingField')
        slot=self.field(button,'ButtonControl','<RaycastTarget>k__BackingField')
        graphic=self.slot_raw(slot)
        return self.read_graphic_geometry(graphic,root,button,ui_pointer,frame)

    def read_graphic_geometry(self,graphic,root,control,ui_pointer,frame):
        native=getattr(self,'_native_ui',None)
        if native is None:native=self._native_ui=NativeUnityUI(self)
        fields=self.base_fields(graphic,'Graphic','UnityEngine.UI')
        read=lambda key:struct.unpack('<Q',self.watch(graphic+fields[key],8))[0]
        if self.watch(graphic+fields['m_RaycastTarget'],1)!=b'\1':raise MapReadPending('Current graphic is not raycastable')
        rect=read('m_RectTransform');canvas=read('m_Canvas');renderer=read('m_CanvasRenderer')
        if not all((rect,canvas,renderer)):raise MapReadPending('Current graphic render caches unavailable')
        if not native.native_active_cached(self.root_game_object(root,native)):
            raise MapReadPending('Current button owner hierarchy inactive')
        game_object=native.component_game_object(graphic)
        if game_object!=native.component_game_object(rect):raise ValueError('Current graphic/rect GameObject differs')
        if not native.native_active_cached(game_object):raise MapReadPending('Current target hierarchy inactive')
        if native.scalar(renderer,'CanvasRenderer','get_cull')!=0:raise MapReadPending('Current target graphic is culled')
        viewport=frame.get('viewport') or (frame.get('current_context') or {}).get('viewport')
        if isinstance(viewport,dict):viewport=(viewport['width'],viewport['height'])
        if not viewport:
            physical=frame['physical_client_rect'];viewport=(physical[2]-physical[0],physical[3]-physical[1])
        geometry=native.overlay_rect(rect,canvas,viewport)
        observed=datetime.now(timezone.utc).isoformat()
        proof=dict(confirmed=True,source=geometry['source'],source_hashes=geometry['source_hashes'],
            ui_pointer=ui_pointer,control_pointer=control,root_object_pointer=root,
            frame_id=frame['frame_id'],window_signature=frame['window_signature'],observed_at=observed,
            coordinate_space='physical-client-pixels',transform_verified=True,active_hierarchy_verified=True,
            source_revision_verified=True)
        return dict(rect_client=geometry['rect_client'],rect_source='current-native-output',native_geometry_proof=proof,
            root_object_pointer=root,control_pointer=control,graphic_pointer=graphic,target_native_game_object=game_object,
            ui_pointer=ui_pointer,frame_id=frame['frame_id'],window_signature=frame['window_signature'],observed_at=observed,
            source_hashes=geometry['source_hashes'],scale=1,profile='current-native-overlay')

    def attach_current_geometry(self,snapshot,frame):
        """Decorate exact current controls inside this same stable read session."""
        if snapshot.get('ui_type')=='QuestClearUIInstance':
            controls=[c for c in snapshot['details'].get('button_controls',[]) if c.get('shown') is True]
            if len(controls)==1:
                button=controls[0]['confirmButton']['control_pointer']
                geometry=self.read_control_geometry(button,snapshot['ui_pointer'],frame)
                geometry.update(index=0,window_id=snapshot['ui_pointer'],window_kind='native-ui-root',
                    model_event_id=self.field(button,'ButtonControl','ButtonActionCallback'),event_kind='native-button-delegate',
                    button_pointer=button)
                snapshot.update(reward_geometry=geometry,window_id=snapshot['ui_pointer'],layout_stable=True)
                candidates=[geometry]
            else:candidates=[]
        else:
            candidates=snapshot.get('candidates',[])
            for candidate in candidates:
                if candidate.get('button_pointer'):
                    geometry=self.read_control_geometry(candidate['button_pointer'],snapshot['ui_pointer'],frame)
                    # Callback ownership is the MMML element; native geometry is
                    # the actual same element's ButtonControl child.
                    candidate.update(geometry)
                    candidate.update(window_id=snapshot['window_id'],window_kind='mmml-window',event_kind='mmml-event')
        pointer=self.current_pointer_state()
        snapshot['pointer_state']=pointer
        for candidate in candidates:
            if candidate.get('target_native_game_object'):
                candidate['pointer_state']=pointer
        self.assert_stable()
        return snapshot

    def active_instances(self,name):
        r=self.reader;shape=self.source_shape('PresentationSingleton');klass=_class(r,self.base,shape)
        fields=self.fields(klass,shape);statics=r.u64(klass+0xb8)
        instance=struct.unpack('<Q',self.watch(statics+fields['instance'],8))[0]
        if not instance or r.u64(instance)!=klass:raise MapReadPending('Current UI singleton unavailable')
        service=self.field(instance,'PresentationSingleton','<UIInstanceService>k__BackingField')
        f=self.object_fields(service,'UIInstanceService');table=self.ref(service,f,'handleTable')
        f=self.object_fields(table,'UIInstanceHandleTable');dictionary=self.ref(table,f,'handleTable');out=[]
        for _,handle in self.dictionary_values(dictionary,limit=self.plan['limits']['instances']):
            if self.field(handle,'UIInstanceHandle','<Status>k__BackingField')!=2:continue
            pointer=self.field(handle,'UIInstanceHandle','<Instance>k__BackingField')
            if pointer and r.class_name(r.u64(pointer))==(name,'MM.Client.Presentation.UI.Instances.StageScene'):
                out.append(pointer)
        return out

    def current_reward(self):
        """Rooted current reward identity, button control and published source data."""
        self._guards=[];panels=self.active_instances('QuestClearUIInstance')
        if not panels:
            self.assert_stable()
            return dict(target_id='reward.confirm',ui_type='QuestClearUIInstance',
                details={'reward_present':False},source='current typed active UIInstanceHandleTable',
                source_hash=ui_map()['targets']['reward.confirm']['evidence'][0]['payload_sha256'],
                captured_at=datetime.now(timezone.utc).isoformat(),layout_stable=True,
                missing=[],memory_writes=0,game_inputs=0)
        if len(panels)!=1:raise MapReadPending('Exact current reward UI count: '+str(len(panels)))
        pointer=panels[0];component=self.field(pointer,'QuestClearUIInstance','questClearUIComponent')
        ui=self.field(pointer,'QuestClearUIInstance','ui')
        if ui!=self.field(component,'QuestClearUIComponent','ui'):
            raise MapReadPending('Current reward UI/component root differs')
        details={key:self.field(component,'QuestClearUIComponent',member) for key,member in
            [('is_active','isActive'),('is_modal_opened','isModalOpened'),('show_reserved','isShowReserved'),
             ('stackable_group','isStackableQuestGroup')]}
        details['reward_present']=True
        details['is_show_state']=self.field(ui,'TitledScrollUI','isShowState')
        details['parts_dirty']=self.field(ui,'TitledScrollUI','isPartsDirty')
        active=self.field(component,'QuestClearUIComponent','uiActiveData')
        entries=self.field(active,'QuestClearUIState','ClearedQuests');cleared=[]
        for entry in self.refs(entries,element='QuestClearEntry',limit=64):
            info=self.field(entry,'QuestClearEntry','QuestClearInfo')
            cleared.append(dict(instance_id=self.inline_value(entry,'QuestClearEntry','QuestInstanceId','QuestInstanceId','int64Value'),
                quest_id=self.inline_value(entry,'QuestClearEntry','QuestStaticDataId','QuestStaticDataId','id'),
                title=self.field(info,'QuestClearInfo','DisplayName'),
                has_selective_reward=self.field(info,'QuestClearInfo','HasSelectiveReward')))
        details['cleared_quests']=cleared
        controls=self.field(ui,'TitledScrollUI','partsControls');buttons=[]
        for control in self.refs(controls,element='ITitledScrollUIPartsControl',limit=64):
            if self.reader.class_name(self.reader.u64(control))!=('TitledScrollUICommonButtonPartsControl','MM.Client.Presentation.UI.Controls.TitledScrollUI'):
                continue
            state={'shown':self.field(control,'TitledScrollUICommonButtonPartsControl','isShowState')}
            for member in ('confirmButton','otherButton','toggleButton'):
                button=self.field(control,'TitledScrollUICommonButtonPartsControl',member)
                state[member]={key:self.field(button,'ButtonControl',key) for key in ('enabled','interactable')}
                state[member]['root_object']=self.field(button,'ButtonControl','<RootObject>k__BackingField')
                state[member]['control_pointer']=button
                state[member]['callback_pointer']=self.field(button,'ButtonControl','ButtonActionCallback')
            buttons.append(state)
        details['button_controls']=buttons
        self.assert_stable()
        return dict(target_id='reward.confirm',ui_type='QuestClearUIInstance',ui_pointer=pointer,details=details,
            source='current typed QuestClearUIInstance/QuestClearUIState/ButtonControl',
            source_hash=ui_map()['targets']['reward.confirm']['evidence'][0]['payload_sha256'],
            captured_at=datetime.now(timezone.utc).isoformat(),layout_stable=False,
            missing=['current reward parent geometry and active child visibility','current target raycast ownership'],
            memory_writes=0,game_inputs=0)

    def assert_stable(self):
        if any(self.reader.read(address,len(raw))!=raw for address,raw in self._guards):
            raise MapReadPending('Current quest UI changed during read')

    def ui_system_root(self):
        shape=self.source_shape('UIAPI');klass=_class(self.reader,self.base,shape)
        fields=self.fields(klass,shape);statics=self.reader.u64(klass+0xb8)
        pointer=struct.unpack('<Q',self.watch(statics+fields['<System>k__BackingField'],8))[0]
        self.typed_fields(pointer,'UISystem')
        return pointer

    def hashset_refs(self,pointer,limit=128):
        r=self.reader;klass=r.u64(pointer)
        if r.class_name(klass)!=('HashSet`1','System.Collections.Generic'):raise ValueError('Current raycast blocker set type changed')
        source=self.source_shape('HashSet`1');shape=r.layout(klass,[f['name'] for f in source['fields']]);f={x['name']:x for x in shape['fields']}
        for name in ('_count','_lastIndex','_version'):
            if f[name]['type_code']!=8:raise ValueError('Current blocker set counter type changed')
        count=struct.unpack('<i',self.watch(pointer+f['_count']['offset'],4))[0]
        last=struct.unpack('<i',self.watch(pointer+f['_lastIndex']['offset'],4))[0]
        self.watch(pointer+f['_version']['offset'],4)
        if not 0<=count<=last<=limit:raise ValueError('Current blocker set exceeds bound')
        if not count:return []
        array=struct.unpack('<Q',self.watch(pointer+f['_slots']['offset'],8))[0]
        length,stride=self.array(array,name='Slot[]',limit=30000)
        klass=r.u64(r.u64(array)+0x40);fields=r.layout(klass,['hashCode','next','value'])['fields']
        if [x['type_code'] for x in fields] not in ([8,8,18],[8,8,19]):raise ValueError('Current blocker set slot type changed')
        offsets=[x['offset']-16 for x in fields]
        if last>length or min(offsets)<0 or offsets[2]+8>stride:raise ValueError('Current blocker set slot bounds changed')
        values=[]
        for index in range(last):
            at=array+32+index*stride;self.watch(at,stride)
            if r.i32(at+offsets[0])>=0:values.append(r.u64(at+offsets[2]))
        if len(values)!=count or not all(values):raise MapReadPending('Current blocker set changed during read')
        return values

    def read_target_occlusion(self,window_id,rect_ui,target_ui_pointer=None):
        """Read actual input blockers; no absence-of-blocker clearance shortcut.

        Unknown native/custom blockers and pointer-cache freshness are retained.
        The method never moves a pointer or invokes Unity's RaycastAll.
        """
        result=dict(occluded=None,source='current UIAPI.System/UnityUIEventSystem/raycastBlockHandleContainer',
            window_id=window_id,rect_ui=list(rect_ui),target_ui_pointer=target_ui_pointer,
            captured_at=datetime.now(timezone.utc).isoformat(),blockers=[],game_inputs=0)
        try:
            if len(rect_ui)!=4 or any(type(x) not in (int,float) or not math.isfinite(x) for x in rect_ui) or min(rect_ui[2:])<=0:
                raise ValueError('current source target rectangle required')
            system=self.ui_system_root();events=self.field(system,'UISystem','<EventSystem>k__BackingField')
            container=self.field(events,'UnityUIEventSystem','raycastBlockHandleContainer')
            pointer=self.field(container,'RaycastBlockHandleContainer','handles')
            for handle in self.hashset_refs(pointer):
                name,namespace=self.reader.class_name(self.reader.u64(handle))
                if name!='RectRaycastBlockHandle':
                    result['blockers'].append({'type':name,'namespace':namespace,'resolved':False});continue
                # Two assemblies declare this same name/namespace with different
                # CheckBlockRaycast coordinate semantics. Resolve exact class.
                klass=self.reader.u64(handle)
                rows=[s for s in SOURCE_SHAPES if s['name']==name and s['namespace']==namespace]
                rows=[s for s in rows if _class(self.reader,self.base,s)==klass]
                if len(rows)!=1:raise ValueError('Current raycast blocker exact source definition unavailable')
                source=rows[0];fields=self.fields(klass,source)
                row=next(f for f in source['fields'] if f['name']=='<BlockRect>k__BackingField')
                if row['type'].get('name')!='Rect' or row['type']['type_code']!=17:raise ValueError('Raycast block rectangle type changed')
                rect=struct.unpack('<4f',self.watch(handle+fields[row['name']],16))
                if not all(math.isfinite(v) for v in rect):raise ValueError('Current raycast block rectangle invalid')
                result['blockers'].append(dict(type=name,namespace=namespace,source_type_index=source['type_index'],rect=list(rect),resolved=True,
                    coordinate_space='source CheckBlockRaycast screen-space; target transform must be verified'))
            raycaster=self.field(events,'UnityUIEventSystem','raycaster')
            result['raycaster_present']=bool(raycaster)
            if raycaster:
                result['last_pointer_raycast']=self.current_pointer_raycast(raycaster)
            result['reason']='current_target_raycast_clearance_not_proven; pointer cache is not a raycast at the proposed point'
            self.assert_stable()
        except (OSError,RuntimeError,ValueError,KeyError,TypeError) as exc:
            result['reason']=str(exc)
        return result

    def current_pointer_raycast(self,raycaster):
        """Actual retained results, explicitly NOT a new target-point raycast."""
        pointer=self.field(raycaster,'UnityUIRaycaster','dummyPointerEventData')
        shape=self.source_shape('PointerEventData');fields=self.typed_fields(pointer,'PointerEventData')
        row=next(f for f in shape['fields'] if f['type'].get('name')=='Vector2' and
                 f['name'] in ('<position>k__BackingField','m_Position','position'))
        position=list(struct.unpack('<2f',self.watch(pointer+fields[row['name']],8)))
        if not all(math.isfinite(x) for x in position):raise ValueError('Current raycast pointer position invalid')
        records=self.field(raycaster,'UnityUIRaycaster','raycastResults');hits=[]
        screen=next(f for f in self.source_shape('RaycastResult')['fields'] if f['name']=='screenPosition')['offset']-16
        for value in self.value_array(records,'RaycastResult',limit=512):
            pos=list(struct.unpack('<2f',self.watch(value+screen,8)))
            hits.append({'game_object':self.inline_scalar(value,'RaycastResult','m_GameObject'),
                'depth':self.inline_scalar(value,'RaycastResult','depth'),
                'sorting_layer':self.inline_scalar(value,'RaycastResult','sortingLayer'),
                'sorting_order':self.inline_scalar(value,'RaycastResult','sortingOrder'),'screen_position':pos})
        return {'position':position,'results':hits,'age_known':False,
                'clearance_at_requested_point':False,'source':'current UnityUIRaycaster retained RaycastResult array'}

    def closed_fields(self,pointer,name):
        shape=self.source_shape(name);klass=self.reader.u64(pointer)
        if self.reader.class_name(klass)!=(name,shape['namespace']):raise ValueError('Current generic UI type changed: '+name)
        fields={f['name']:f for f in self.reader.layout(klass,[f['name'] for f in shape['fields']])['fields']}
        for field in shape['fields']:
            actual=fields[field['name']]
            if actual['type_code']!=field['type']['type_code'] or not 16<=actual['offset']<=2048:
                # Literal/static generic fields do not occupy the object.
                if field['type'].get('attrs',0)&16:continue
                raise ValueError('Current generic UI field changed: '+field['name'])
        return fields

    def reference_dictionary(self,pointer,limit=64):
        d=self.reader.dictionary(pointer)
        if d.get('kind')=='manual_dense' or not 0<=d['count']<=d['used']<=limit:
            raise ValueError('Current pointer cache dictionary exceeds selected source shape')
        self.watch(pointer+d['version_offset'],4);self.watch(pointer+d['count_offset'],4)
        f={x['name'].casefold():x for x in d['entry_layout']['fields']}
        if any(f[k]['type_code'] not in (18,21,19) for k in ('key','value')):
            raise ValueError('Current pointer cache dictionary key/value are not references')
        offsets={k:f[k]['offset']-16 for k in ('hashcode','key','value')};out=[]
        if min(offsets.values())<0 or max(offsets.values())+8>d['stride']:raise ValueError('Current pointer cache entry bounds invalid')
        for i in range(d['used']):
            at=int(d['entries'],16)+32+i*d['stride'];self.watch(at,d['stride'])
            if self.reader.i32(at+offsets['hashcode'])>=0:
                out.append((self.reader.u64(at+offsets['key']),self.reader.u64(at+offsets['value'])))
        if len(out)!=d['count']:raise MapReadPending('Current pointer cache dictionary changed')
        return out

    def current_pointer_state(self):
        """Current post-processing caches, preserving source timing and all targets.

        A cache is never declared clear solely from an empty blocker collection.
        It can certify a probe only after the exact observed position changes.
        """
        system=self.ui_system_root();events=self.field(system,'UISystem','<EventSystem>k__BackingField')
        processor=self.field(events,'UnityUIEventSystem','pointerProcessor')
        fields=self.closed_fields(processor,'UIPointerProcessor`2')
        self.verify_pointer_source(fields)
        table=struct.unpack('<Q',self.watch(processor+fields['raycastCaches']['offset'],8))[0]
        caches=[]
        for handle,cache in self.reference_dictionary(table):
            cf=self.closed_fields(cache,'PointerRaycastCache')
            if any(cf[key]['offset']!=offset for key,offset in POINTER_SOURCE['cache_offsets'].items()):
                raise ValueError('Current pointer cache offsets differ from pinned update source')
            position=list(struct.unpack('<2f',self.watch(cache+cf['LastScreenPosition']['offset'],8)))
            updated=struct.unpack('<f',self.watch(cache+cf['LastUpdateTime']['offset'],4))[0]
            if not all(math.isfinite(x) for x in position+[updated]):raise ValueError('Current pointer cache numeric data invalid')
            targets=struct.unpack('<Q',self.watch(cache+cf['CachedTargets']['offset'],8))[0]
            entries=[]
            for obj in self.refs(targets,element='GameObject',limit=512):
                native=getattr(self,'_native_ui',None)
                if native is None:native=self._native_ui=NativeUnityUI(self)
                entries.append(native.native_pointer(obj,{'GameObject'}))
            caches.append(dict(handle=handle,cache_pointer=cache,position=position,update_time=updated,targets=entries))
        native=getattr(self,'_native_ui',None)
        if native is None:native=self._native_ui=NativeUnityUI(self)
        final_set=struct.unpack('<Q',self.watch(processor+fields['newTargets']['offset'],8))[0]
        final_targets=[native.native_pointer(obj,{'GameObject'}) for obj in self.hashset_refs(final_set,limit=512)]
        self.assert_stable()
        return dict(caches=caches,source='current UIPointerProcessor.newTargets plus current raw-cache position/update time',
            source_hashes=[SOURCE_VERSION['game_assembly_sha256'],SOURCE_VERSION['metadata_sha256']],
            observed_at=datetime.now(timezone.utc).isoformat(),game_inputs=0,
            target_semantics_verified=True,final_targets=final_targets,
            final_target_source='current UIPointerProcessor.newTargets after registered-event/blocker/penetration filtering')

    def verify_pointer_source(self,fields):
        if any(self.plan.get(k)!=POINTER_SOURCE[k] for k in SOURCE_VERSION):
            raise ValueError('Current pointer source version differs')
        if any(fields[key]['offset']!=offset for key,offset in POINTER_SOURCE['processor_offsets'].items()):
            raise ValueError('Current pointer processor offsets differ from selected shared source')
        for method in POINTER_SOURCE['methods']:
            raw=bytes.fromhex(method['raw_hex'])
            if hashlib.sha256(raw).hexdigest()!=method['sha256']:
                raise ValueError('Embedded pointer source body hash invalid')
            if self.reader.read(self.base+method['rva'],len(raw))!=raw:
                raise ValueError('Current pointer processing body changed: '+method['name'])


def quest_ui_session(session):
    """Share an existing read handle and guard list; never open/close a process."""
    if isinstance(session,QuestUIActionSession):return session
    proxy=QuestUIActionSession.__new__(QuestUIActionSession);proxy.__dict__=session.__dict__
    return proxy


def _digest(value):
    return hashlib.sha256(_json.dumps(value,sort_keys=True,separators=(',',':')).encode()).hexdigest()


def _producer_revision(snapshot):
    if not snapshot.get('source_hash'):raise ValueError('validated UI payload revision missing')
    return _digest(dict(producer='quest-ui-r3',ui_payload=snapshot['source_hash'],
        **SOURCE_VERSION,unity_player_sha256=NATIVE_SOURCE['unity_player_sha256'],delivery_policy=DELIVERY_POLICY_HASH,
        delivery_group_source=((snapshot.get('details') or {}).get('delivery_group') or {}).get('source_hash')))


def _probe_receipt(frame):
    row=frame.get('pointer_probe_receipt')
    if row:return row
    pending=frame.get('pending_input') or {}
    rows=(pending.get('receipts',[]) or (pending.get('port_result') or {}).get('receipts',[]) or
          (pending.get('input_receipt') or {}).get('receipts',[]))
    matches=[r for r in rows if (r.get('command') or {}).get('kind')=='pointer_probe' and r.get('status')=='sent_unconfirmed']
    return matches[-1] if matches else None


def _pointer_scene(candidate,snapshot):
    keys=('ui_pointer','control_pointer','root_object_pointer','button_pointer','handler_pointer','model_event_id',
          'window_id','window_kind','event_kind','index','target_native_game_object','rect_client')
    value={k:candidate.get(k,snapshot.get(k)) for k in keys}
    if any(type(value[k]) is not int or value[k]<=0 for k in ('ui_pointer','control_pointer','root_object_pointer','window_id','target_native_game_object')):
        raise ValueError('current exact UI target/control/callback identity missing')
    controls=[value[k] for k in ('button_pointer','handler_pointer') if value[k] is not None]
    if not controls or any(type(v) is not int or v<=0 for v in controls):
        raise ValueError('current exact native button or event handler missing')
    if type(value['model_event_id']) is not int or value['model_event_id']<0:
        raise ValueError('current exact UI callback identity missing')
    if type(value['index']) is not int or value['index']<0:raise ValueError('current source target ordinal missing')
    if not value['rect_client'] or len(value['rect_client'])!=4:raise ValueError('current source target rectangle missing')
    # A recycled control/event ID can be rebound to a different item/quest while
    # keeping its rectangle. Preserve the typed semantic model in the scene ID.
    value['semantic_state']=snapshot.get('pointer_semantics',snapshot.get('details',{}))
    value['source_revision']=_producer_revision(snapshot)
    return _digest(value)


def prepare_pointer_proposal(candidate,snapshot,frame):
    """Actual current rect -> move-only probe -> verified no-move click.

    No synthetic clearance is generated. A changed game cache at the requested
    point and source-confirmed target semantics are required after a probe.
    """
    semantics=snapshot.get('pointer_semantics') or {}
    intended=semantics.get('action_kind','pointer_click')
    if intended not in ('pointer_click','pointer_scroll'):
        raise ValueError('unsupported source pointer action')
    delta=semantics.get('delta') if intended=='pointer_scroll' else None
    if intended=='pointer_scroll' and (type(delta) is not int or delta not in (-120,120)):
        raise ValueError('current source scroll single notch required')
    geometry=candidate.get('native_geometry_proof') or {}
    if (any(geometry.get(k) is not True for k in ('confirmed','transform_verified','active_hierarchy_verified','source_revision_verified'))
            or geometry.get('frame_id')!=frame['frame_id'] or geometry.get('window_signature')!=frame['window_signature']):
        raise ValueError('current exact source geometry proof missing for pointer probe')
    owner=frame.get('input_owner') or {}
    if owner.get('continuous') is not True or not owner.get('epoch'):
        raise ValueError('current uninterrupted input owner required for pointer probe')
    if not _fresh(owner,frame):raise ValueError('current pointer input ownership is stale')
    state=candidate.get('pointer_state') or snapshot.get('pointer_state') or {}
    if not state.get('source') or not _fresh(state,frame):raise ValueError('current pointer cache observation unavailable')
    caches=state.get('caches',[])
    if not caches:raise ValueError('current game pointer cache has no initialized pointer')
    signature=_pointer_scene(candidate,snapshot);rect=candidate['rect_client']
    left,top,width,height=rect
    if any(type(x) not in (int,float) or not math.isfinite(x) for x in rect) or min(width,height)<4:
        raise ValueError('current target cannot contain a distinct bounded probe point')
    height_px=(frame.get('viewport') or {}).get('height') if isinstance(frame.get('viewport'),dict) else None
    if height_px is None:
        physical=frame['physical_client_rect'];height_px=physical[3]-physical[1]
    receipt=_probe_receipt(frame)
    if receipt:
        if len(caches)!=1:raise ValueError('current pointer cache mouse owner became ambiguous after probe')
        context=(receipt.get('command') or {}).get('probe_context') or receipt.get('target_binding')
        if not isinstance(context,dict):raise ValueError('previous pointer probe target receipt missing')
        if (context.get('scene_signature')!=signature or context.get('input_epoch')!=owner['epoch'] or
                context.get('window_signature')!=frame['window_signature']):
            raise ValueError('previous pointer probe belongs to a changed target/owner')
        if receipt.get('status')!='sent_unconfirmed' or not receipt.get('operation_id') or not receipt.get('dispatch_finished_at'):
            raise ValueError('actual sent pointer probe receipt required')
        if _stamp(state['observed_at'])<=_stamp(receipt['dispatch_finished_at']):
            raise ValueError('current game pointer observation predates probe dispatch')
        matching=[c for c in caches if c['handle']==context['pointer_handle'] and c['cache_pointer']==context['pointer_cache']]
        if len(matching)!=1:raise ValueError('current exact game pointer cache changed owner')
        cache=matching[0]
        if cache['position']!=context['point_screen'] or cache['position']==context['before_position']:
            raise ValueError('game has not observed the distinct source target probe point')
        if _digest(cache)==context['before_results_hash'] or cache['update_time']<=context['before_update_time']:
            raise ValueError('game pointer cache has not advanced after the probe')
        if context.get('pointer_action','pointer_click')!=intended or context.get('scroll_delta')!=delta:
            raise ValueError('previous pointer probe action or scroll delta changed')
        if intended=='pointer_scroll':
            proof=candidate.get('scroll_receiver_proof') or {}
            expected=dict(frame_id=frame['frame_id'],window_signature=frame['window_signature'],
                target_native_game_object=candidate['target_native_game_object'],
                handler_pointer=candidate.get('handler_pointer'),delta=delta,point_screen=context['point_screen'])
            if (any(proof.get(k) is not True for k in ('confirmed','registered','route_verified','enabled','raycast_verified'))
                    or not proof.get('source') or not proof.get('source_hashes') or not _fresh(proof,frame)
                    or any(proof.get(k)!=v for k,v in expected.items())
                    or _stamp(proof['observed_at'])<=_stamp(receipt['dispatch_finished_at'])):
                raise ValueError('current exact scroll receiver and source gates not verified')
        else:
            if state.get('target_semantics_verified') is not True:
                raise ValueError('A incomplete: current pointer cache final target semantics not source verified')
            if candidate['target_native_game_object'] not in state.get('final_targets',[]):
                raise ValueError('game final pointer targets do not contain the owned control')
        verified=dict(confirmed=True,probe_operation_id=receipt['operation_id'],input_epoch=owner['epoch'],
            probe_context=context,source_refs=state['source_hashes'],observed_at=state['observed_at'])
        occlusion=dict(occluded=False,basis='current exact target raycast result',source=state['source'],
            observed_at=state['observed_at'],frame_id=frame['frame_id'],window_signature=frame['window_signature'],
            session_id=frame['session_id'],character_id=frame['character_id'],source_revision=snapshot['source_hash'],
            window_id=snapshot.get('window_id'),target_ui_pointer=snapshot.get('ui_pointer'),
            target_rect_ui=candidate.get('rect_ui'),target_index=candidate.get('index'),pointer_verified=verified)
        if intended=='pointer_scroll':
            verified['source_refs']=sorted(set(verified['source_refs']+proof['source_hashes']))
            verified['scroll_receiver_proof']=proof
        result=dict(input_kind=intended,point_client=context['point'],probe_context=context,
            pointer_verified=verified,raycast_proof=verified,occlusion=occlusion,occluded=False)
        if intended=='pointer_scroll':result['delta']=delta
        return result
    # Choose an interior pixel that differs from every retained pointer position.
    choices=[(round(left+width/2),round(top+height/2)),(math.ceil(left)+1,math.ceil(top)+1),
             (math.floor(left+width)-2,math.floor(top+height)-2)]
    points=[p for p in choices if left<p[0]<left+width and top<p[1]<top+height and
            all([p[0],height_px-p[1]]!=c['position'] for c in caches)]
    if not points:raise ValueError('current source rectangle has no distinct safe pointer probe pixel')
    # Multiple pointer handles (e.g. gamepad + mouse) require an exact raw-input
    # owner supplier; choosing one based on order would fabricate ownership.
    if len(caches)!=1:raise ValueError('current pointer cache mouse owner is ambiguous')
    cache=caches[0];point=list(points[0])
    context=dict(target_ui_pointer=candidate['ui_pointer'],control_pointer=candidate['control_pointer'],
        root_object_pointer=candidate['root_object_pointer'],button_pointer=candidate.get('button_pointer'),
        handler_pointer=candidate.get('handler_pointer'),
        target_index=candidate['index'],model_event_id=candidate['model_event_id'],window_id=candidate['window_id'],
        window_kind=candidate.get('window_kind'),event_kind=candidate.get('event_kind'),
        target_native_game_object=candidate['target_native_game_object'],point=point,point_screen=[point[0],height_px-point[1]],
        before_position=cache['position'],before_results_hash=_digest(cache),before_update_time=cache['update_time'],
        pointer_handle=cache['handle'],pointer_cache=cache['cache_pointer'],
        input_epoch=owner['epoch'],frame_id=frame['frame_id'],window_signature=frame['window_signature'],
        source_hashes=sorted(set(geometry['source_hashes']+state['source_hashes']+[snapshot['source_hash']])),
        source_revision=_producer_revision(snapshot),scene_signature=signature,
        rect_client=rect,target_id=snapshot.get('target_id'),ui_type=snapshot.get('ui_type'))
    if intended=='pointer_scroll':context.update(pointer_action=intended,scroll_delta=delta)
    return dict(input_kind='pointer_probe',pointer_probe_ready=True,point_client=point,probe_context=context,occluded=None)


def pointer_effect_contract(pointer_result,owner,node,frame,observed_at):
    fields=dict(pointer_verified=True,probe_target_signature=pointer_result['probe_context']['scene_signature'],
        probe_point=pointer_result['point_client'])
    proof=_effect(owner,node,frame,'pointer_probe',fields,observed_at,
        source_revision=pointer_result['probe_context']['source_revision'])
    return dict(expected_effect=_expected(proof,fields=fields),
        effect_proofs=[proof] if pointer_result['input_kind'] in ('pointer_click','pointer_scroll') else [])


def make_pointer_guard(frame_provider,*,session_factory=QuestUIActionSession,scene_reader=None):
    """Factory for SourceActionPort's last-moment read-only scene guard.

    ``scene_reader(session, probe_context, frame)`` may supply an exact rooted
    custom screen as ``(candidate, typed_snapshot)``. The default reads supported
    quest UI. Constructing the callback never opens a process or captures pixels.
    The original observation and receipt timestamps are not rewritten.
    """
    def guard(binding,latest_context,phase):
        if phase not in ('pointer_probe','pointer_click','pointer_scroll'):raise ValueError('unsupported pointer phase')
        frame=deepcopy(frame_provider())
        if not isinstance(frame,dict):raise ValueError('current owned observer frame unavailable')
        window=latest_context['window'];pid=window.pid
        if (frame.get('window_signature')!=binding.get('window_signature') or
                (frame.get('input_owner') or {}).get('epoch')!=binding.get('input_epoch')):
            raise ValueError('current pointer observer owner differs from dispatch binding')
        if (frame.get('pid',(frame.get('window') or {}).get('pid'))!=pid or
                frame.get('process_started_at')!=latest_context.get('observed_process_started_at',frame.get('process_started_at'))):
            raise ValueError('current pointer process occurrence changed')
        with session_factory(pid) as session:
            if session.reader.pid!=pid or session.reader.started_at!=_stamp(frame['process_started_at']):
                raise ValueError('current pointer read belongs to a reused PID')
            if scene_reader is not None:
                candidate,snapshot=scene_reader(session,binding,frame)
            else:
                target=binding.get('target_id')
                if not target:raise ValueError('current pointer source target ID missing')
                snapshot=session.current_layout(target)
                session.attach_current_geometry(snapshot,frame)
                candidates=([snapshot['reward_geometry']] if target=='reward.confirm' else snapshot.get('candidates',[]))
                candidates=[c for c in candidates if (c.get('control_pointer'),c.get('root_object_pointer'),c.get('model_event_id'),c.get('index'))==
                    (binding['control_pointer'],binding['root_object_pointer'],binding['model_event_id'],binding['target_index'])]
                if len(candidates)!=1:raise ValueError('current pointer root/control/callback is no longer unique')
                candidate=candidates[0]
            if _pointer_scene(candidate,snapshot)!=binding['scene_signature']:
                raise ValueError('current pointer source scene changed before input')
            if phase=='pointer_probe':
                caches=(snapshot.get('pointer_state') or {}).get('caches',[])
                rows=[c for c in caches if c.get('handle')==binding['pointer_handle'] and c.get('cache_pointer')==binding['pointer_cache']]
                if len(rows)!=1 or _digest(rows[0])!=binding['before_results_hash']:
                    raise ValueError('current pointer cache changed before move dispatch; obtain a fresh proposal')
                verified=None
            else:
                actual=prepare_pointer_proposal(candidate,snapshot,frame)
                if actual['input_kind']!=phase or actual['probe_context']!=binding:
                    raise ValueError('current game hit does not verify the exact sent probe')
                verified=actual['pointer_verified']
            stable=getattr(session,'assert_stable',None)
            if stable is not None:stable()
            else:quest_ui_session(session).assert_stable()
            state=snapshot['pointer_state']
        result=dict(confirmed=True,input_epoch=binding['input_epoch'],scene_signature=binding['scene_signature'],
            source_refs=state['source_hashes'],observed_at=state['observed_at'])
        if verified is not None:result['pointer_verified']=verified
        return result
    return guard


_DELIVERY_ITEM_DOMAINS={name:name+'Sheet' for name in
    ('Consumable','Equipment','QuestFakeItem','Costume','Ridable')}


def _delivery_targets(snapshot,node,owner):
    params=node.get('params',{});typed=params.get('typed_operands')
    if 'typed_operands' not in params:
        targets={(None,int(t['table_data']['raw_id'])&0xffffffff):t['table_data']
            for t in params.get('targets',[]) if t.get('kind')=='item' and t.get('table_data',{}).get('raw_id') is not None}
        if not targets:raise ValueError('exact objective item sheet identity missing')
        return targets,False
    if not isinstance(typed,dict):raise ValueError('current typed delivery operands missing')
    binding=node.get('runtime_operation_binding') or {};scope=typed.get('scope') or {}
    current=owner or binding
    if (typed.get('ready') is not True or typed.get('operation')!='objective.deliver'
            or typed.get('source')!='current-root-typed-quest-collections'
            or not _fresh(typed,{'captured_at':snapshot['captured_at']})
            or any((typed.get('source_version') or {}).get(k)!=SOURCE_VERSION[k] for k in SOURCE_VERSION)
            or any(scope.get(k)!=node.get(k) for k in ('quest_id','step_id','objective_index'))
            or any(scope.get(k) is None or scope.get(k)!=current.get(k) for k in
                ('quest_id','instance_id','accept_time','step_id','step_index','objective_index','process_started_at'))
            or any(typed.get(k)!=current.get(k) or typed.get(k) is None for k in ('session_id','character_id'))):
        raise ValueError('current typed delivery operand occurrence/source changed')
    npc=(typed.get('operands') or {}).get('npc') or {}
    group_request=delivery_group_request(snapshot,node) if not npc else None
    if not npc and group_request is None:npc=delivery_group_npc(snapshot,node) or {}
    if group_request is None and (npc.get('table_domain')!='NPCSheet' or type(npc.get('id')) is not int
            or (npc['id']&0xffffffff)!=(snapshot['details'].get('target_npc_id',-1)&0xffffffff)):
        raise ValueError('current delivery NPC differs from exact typed operand')
    targets={};rows=(typed.get('operands') or {}).get('items')
    if not isinstance(rows,list) or not rows:raise ValueError('current typed delivery item operands missing')
    for item in rows:
        domain=item.get('table_domain');sid=item.get('id');amount=item.get('amount') or {};raw=item.get('raw') or {}
        if (domain not in _DELIVERY_ITEM_DOMAINS.values() or type(sid) is not int or not (sid&0xffffffff)
                or item.get('declared_type')!='Shared.Symbols.'+domain+'Id'
                or raw.get('interpreted') is not True or raw.get('domain_mapping_verified') is not True
                or raw.get('table_domain')!=domain or raw.get('value')!=sid
                or amount.get('declared_type')!='System.Int32' or type(amount.get('value')) is not int
                or amount['value']<=0 or (amount.get('raw') or {}).get('interpreted') is not True
                or amount['raw'].get('value')!=amount['value']):
            raise ValueError('typed delivery item domain/id/amount is not exact source data')
        key=(domain,sid&0xffffffff)
        targets.setdefault(key,dict(amount=0,source_items=[]))
        targets[key]['amount']+=amount['value'];targets[key]['source_items'].append(deepcopy(item))
    return targets,True


def delivery_reservations(snapshot,node,owner=None):
    """Join exact current goal domains/amounts to current contexts and stacks.

    The current UI supplies selection/requirements. Typed objective quantities
    must agree; neither catalog text nor an ID shared by another domain suffices.
    """
    details=snapshot.get('details',{});contexts=details.get('delivery_contexts')
    items=details.get('deliverable_items');model=details.get('model',{})
    if details.get('typed_selection_complete') is not True or not isinstance(contexts,list) or not contexts or not isinstance(items,list):
        raise ValueError('current typed delivery requirements/selection missing')
    targets,typed=_delivery_targets(snapshot,node,owner)
    required={};selected={};per_stack={};matched=[];matched_amounts={}
    for context in contexts:
        allowed={int(x)&0xffffffff for x in context.get('allowed_sheet_ids',[])}
        candidates={key for key in targets if key[1] in allowed}
        if typed:
            # AllowedSheetIds has no domain. A same-number foreign current stack
            # cannot disambiguate it, even when its caption happens to match.
            current_keys={(item.get('table_domain'),int(item['sheet_id'])&0xffffffff) for item in items
                if (int(item['sheet_id'])&0xffffffff) in allowed}
            candidates &= current_keys
            if len(candidates)==1:
                chosen=next(iter(candidates))
                if any(key[1]==chosen[1] and key[0]!=chosen[0] for key in current_keys):
                    raise ValueError('current delivery sheet ID has ambiguous item domains')
        if len(candidates)!=1:raise ValueError('current delivery requirement sheet/domain ambiguous for this objective')
        domain,sheet=next(iter(candidates));key=(domain+':'+str(sheet)) if typed else str(sheet)
        amount=context.get('target_amount')
        if type(amount) is not int or amount<=0:raise ValueError('current required amount missing')
        expected_name=context.get('target_display_name') if typed else targets[(domain,sheet)]['display_name']
        if not typed and _normal(context.get('target_display_name'))!=_normal(expected_name):
            raise ValueError('current requirement item name differs from exact current sheet')
        required[key]=required.get(key,0)+amount;selected.setdefault(key,0)
        matched_amounts[(domain,sheet)]=matched_amounts.get((domain,sheet),0)+amount
        rows=context.get('selected_items')
        if not isinstance(rows,list):raise ValueError('selected item dictionary missing')
        total=0
        for row in rows:
            count=row.get('count');sid=int(row['sheet_id'])&0xffffffff
            owned=[item for item in items if item.get('item_pointer')==row.get('item_pointer')]
            if (sid!=sheet or type(count) is not int or count<=0 or len(owned)!=1
                    or (not typed and _normal(row.get('name'))!=_normal(expected_name))
                    or (typed and row.get('table_domain')!=domain)):
                raise ValueError('selected current item does not belong to requirement')
            item=owned[0]
            if ((int(item.get('sheet_id',-1))&0xffffffff)!=sheet or _normal(item.get('name'))!=_normal(row.get('name'))
                    or (typed and item.get('table_domain')!=domain)):
                raise ValueError('selected dictionary and current item sheet/domain/name differ')
            if item.get('preserved') or (item.get('locked') and context.get('allow_locked_item') is not True):
                raise ValueError('selected current item violates original preservation/lock constraint')
            identity=row['item_pointer'];per_stack[identity]=per_stack.get(identity,0)+count
            if per_stack[identity]>item['count']:raise ValueError('selected item quantity exceeds current stack')
            total+=count
        if total!=context.get('selected_amount') or total>amount:raise ValueError('current selected amount mismatch/oversupply')
        selected[key]+=total;matched.append(dict(context_index=context['index'],sheet_id=sheet,table_domain=domain,required=amount,selected=total))
    if typed and matched_amounts!={key:target['amount'] for key,target in targets.items()}:
        raise ValueError('current delivery requirements differ from exact typed objective amounts')
    rows=[r['values'] for r in model.get('rows',[]) if 'isDeliverButtonEnabled' in r.get('values',{})]
    if len(rows)!=1:raise ValueError('current delivery button model unavailable/ambiguous')
    descriptor=rows[0]
    if type(descriptor.get('isDeliverButtonEnabled')) is not bool or not isinstance(descriptor.get('deliverClicked'),dict) or descriptor['deliverClicked'].get('event_present') is not True:
        raise ValueError('current delivery callback/enabled state missing')
    return dict(required_by_item=required,selected_by_item=selected,contexts=matched,
        typed_objective_join=typed,enabled=descriptor['isDeliverButtonEnabled'],label=descriptor.get('deliverButtonText'),
        submit_event_id=descriptor['deliverClicked'].get('event_id'),
        selected_context=details.get('selectedDeliverContextIndex'))


def _effect(owner,node,frame,phase,fields,observed_at,*,source_revision):
    if not isinstance(source_revision,str) or not source_revision:raise ValueError('validated effect producer revision required')
    return dict(kind='ui_state',phase=phase,node_id=node['id'],quest_id=node['quest_id'],
        **{k:owner[k] for k in ('instance_id','accept_time','step_id','objective_index') if k in owner},
        source='current typed quest UI fields',source_revision=source_revision,observed_at=observed_at,
        session_id=frame['session_id'],character_id=frame['character_id'],fields=deepcopy(fields))


def _expected(proof,*,fields=None,changed_fields=None):
    value={k:proof[k] for k in ('kind','phase','node_id','quest_id','instance_id','accept_time','step_id','objective_index','source_revision') if k in proof}
    value.update(fields=deepcopy(fields or {}),changed_fields=deepcopy(changed_fields or {}))
    return value


def _occlusion(candidate,snapshot,frame):
    proof=candidate.get('occlusion')
    if not isinstance(proof,dict) or type(proof.get('occluded')) is not bool:
        raise ValueError('current target raycast/modal ownership unavailable')
    if (proof.get('frame_id')!=frame['frame_id'] or proof.get('window_signature')!=frame['window_signature'] or
            proof.get('session_id')!=frame['session_id'] or proof.get('source_revision')!=snapshot['source_hash'] or
            str(proof.get('character_id'))!=str(frame['character_id']) or
            proof.get('window_id')!=snapshot.get('window_id') or
            proof.get('target_ui_pointer')!=snapshot.get('ui_pointer') or
            proof.get('target_rect_ui')!=candidate.get('rect_ui') or
            proof.get('target_index')!=candidate.get('index') or not _fresh(proof,frame)):
        raise ValueError('current target raycast evidence binding mismatch')
    if proof['occluded']:raise ValueError('current target is occluded')
    if proof.get('basis')!='current exact target raycast result':
        raise ValueError('absence of a modal is not target raycast clearance')
    return proof


def bind_quest_ui_action(snapshot,frame,node,semantic):
    """Current reader output -> source rect -> semantic proposal/effect proofs.

    Caption boxes may disambiguate source rectangles but never become coordinates.
    Missing raycast evidence preserves actual state proofs while vetoing input.
    """
    result=dict(evidence_kind='current-frame',bindings={},missing=[],effect_proofs=[],
        current_action_binding={},layout_snapshot=snapshot,game_inputs=0)
    try:
        if any(not frame.get(k) for k in ('frame_id','captured_at','character_id','session_id','window_signature')):
            raise ValueError('current owned capture identity missing')
        if not _read_near_capture(snapshot,frame):raise ValueError('current UI read/capture time mismatch')
        if node['op']=='reward.claim':return bind_reward_action(snapshot,frame,node,semantic)
        owner_node=node
        group_request=delivery_group_request(snapshot,node)
        group_npc=delivery_group_npc(snapshot,node) if group_request is None else None
        if group_npc is not None:
            owner_node=deepcopy(node)
            owner_node['params']['targets']=[{'kind':'npc','table_data':group_npc}]
        owner,missing=(group_request_ownership(snapshot,frame,node,group_request) if group_request is not None
                       else delivery_ownership(snapshot,frame,owner_node))
        if missing:raise ValueError('; '.join(missing))
        if not _fresh(frame['current_quest'],frame):raise ValueError('current accepted quest ownership is stale')
        reservations=delivery_reservations(snapshot,node,dict(frame['current_quest'],**owner))
        fields={k:reservations[k] for k in ('required_by_item','selected_by_item','selected_context')}
        details=snapshot['details']
        fields['selected_instances']=selection_instances(details['delivery_contexts'])
        fields.update(is_delivered=details.get('is_delivered'),is_delivering=details.get('is_delivering'))
        proof=_effect(owner,node,frame,'delivery',fields,snapshot['captured_at'],source_revision=_producer_revision(snapshot));result['effect_proofs']=[proof]
        if 'delivery_policy' in details:verify_current_policy(details)
        complete=reservations['required_by_item']==reservations['selected_by_item']
        target_id='delivery.submit' if complete else 'delivery.item'
        if details.get('is_delivering') is not False or details.get('is_delivered') is not False:
            raise ValueError('current delivery is already running/delivered; observe parent effect')
        if complete:
            if reservations['enabled'] is not True:raise ValueError('current delivery submit disabled')
            label=reservations['label'];fact=semantic.get('facts',{}).get('delivery_submit_caption')
            expected=_expected(proof,fields={'is_delivered':True})
            event_id=reservations['submit_event_id']
        else:
            if 'delivery_policy' in details:
                item,context=choose_default_item(details,reservations)
            else:
                # Compatibility for old explicit observation adapters. The native
                # supplier always emits policy evidence (including its failure).
                wanted=[c for c in reservations['contexts'] if c['selected']<c['required'] and c['context_index']==reservations['selected_context']]
                if len(wanted)!=1:raise ValueError('current selected delivery context does not identify one outstanding requirement')
                context=wanted[0];sheet=context['sheet_id'];domain=context['table_domain']
                items=[i for i in details['deliverable_items'] if (i['sheet_id']&0xffffffff)==sheet
                    and (domain is None or i.get('table_domain')==domain) and not i['locked'] and not i['preserved'] and i['count']>0]
                if len(items)!=1:raise ValueError('current selectable delivery stack is absent/ambiguous')
                item=items[0]
            label=item['name'];fact=semantic.get('facts',{}).get('delivery_item')
            descriptors=[d for d in details['model'].get('deliverableItems',[]) if d['index']==item['index']]
            if len(descriptors)!=1 or not descriptors[0]['values'].get('onclick',{}).get('event_present'):
                raise ValueError('current exact item model callback missing')
            if _normal(descriptors[0].get('item_data',{}).get('displayName'))!=_normal(label):
                raise ValueError('current item descriptor order/name differs from typed item list')
            event_id=descriptors[0]['values']['onclick'].get('event_id')
            expected=_expected(proof,fields={'selected_instances':expected_selection(details,item,context)},
                changed_fields={'selected_by_item':fields['selected_by_item']})
        scale=snapshot['scale'];width=semantic['frame']['width'];height=semantic['frame']['height']
        if not math.isfinite(scale) or scale<=0 or any(abs(a*scale-b)>2 for a,b in zip(snapshot['screen_ui'],(width,height))):
            raise ValueError('current UI scale/viewport mismatch')
        allowed=ui_map()['targets'][target_id]['runtime_layout_tags'];matches=[]
        for candidate in snapshot['candidates']:
            if candidate['tag'] not in allowed:continue
            if event_id is not None and candidate.get('model_event_id')==event_id:
                matches.append(candidate)
        if len(matches)!=1:raise ValueError('current exact model callback does not identify one source layout rectangle')
        if fact:
            if _normal(fact['observed_text'])!=_normal(label):raise ValueError('current source control caption differs from model')
            x,y,w,h=[v*scale for v in matches[0]['rect_ui']];l,t,r,b=fact['box']
            if not (x<=l and y<=t and r<=x+w and b<=y+h):raise ValueError('current caption is outside its exact source callback rectangle')
        candidate=deepcopy(matches[0]);pointer=None
        if candidate.get('native_geometry_proof'):
            pointer=prepare_pointer_proposal(candidate,snapshot,frame);candidate.update(pointer)
            contract=pointer_effect_contract(pointer,owner,node,frame,(candidate.get('pointer_state') or snapshot['pointer_state'])['observed_at'])
            result['effect_proofs'].extend(contract['effect_proofs'])
            if pointer['input_kind']=='pointer_probe':expected=contract['expected_effect']
        else:_occlusion(candidate,snapshot,frame)
        # Selection additionally joins the actual DOM callback to the model event.
        if not complete and candidate.get('model_event_id')!=descriptors[0]['values']['onclick'].get('event_id'):
            raise ValueError('current layout item callback differs from selected model callback')
        origin=(frame.get('physical_client_rect') or [0,0])[:2]
        safe=(frame.get('current_context') or {}).get('safe_rect',[0,0,width,height]);l,t,r,b=safe
        binding=dict(candidate,**owner,frame_id=frame['frame_id'],screen='delivery',label=label,
            source_bound=True,layout_stable=snapshot.get('layout_stable') is True,semantic_unique=True,
            enabled=True,occluded=candidate.get('occluded',False),current_identity_bound=True,ownership_verified=True,
            rect_source=candidate.get('rect_source','current-mmml-output'),layout_source=snapshot['source'],layout_revision=snapshot['source_hash'],
            required_by_item=reservations['required_by_item'],selected_by_item=reservations['selected_by_item'],
            is_deliver_button_enabled=reservations['enabled'],item_identity_verified=not complete)
        observed_at=(candidate.get('native_geometry_proof') or {}).get('observed_at',snapshot['captured_at'])
        action=dict(owner,node_id=node['id'],source='current typed delivery UI occurrence/item/layout join',
            observed_at=observed_at,expected_effect=expected,source_revision=expected['source_revision'],
            phase='pointer_probe' if pointer and pointer['input_kind']=='pointer_probe' else ('delivery.submit' if complete else 'delivery.select'))
        result.update(frame_id=frame['frame_id'],observed_at=observed_at,session_id=frame['session_id'],
            window_signature=frame['window_signature'],viewport={'width':width,'height':height,'origin':origin},
            layout_scale=scale,safe_rect=[l,t,r-l,b-t],source_hashes=[snapshot['source_hash']]+candidate.get('source_hashes',[]),
            goal={'quest_id':owner['quest_id'],'npc':owner['npc']},bindings={target_id:binding},
            selected_target_id=target_id,current_action_binding=action)
        result['proposal']=build_target(target_id,result['viewport'],result)
        if result['proposal']['status']=='ready':result['proposal'].update(current_binding=action,expected_effect=expected)
    except (KeyError,TypeError,ValueError,AttributeError) as exc:
        result['missing'].append(str(exc))
    return result


def reward_ownership(snapshot,frame,node):
    current=frame.get('current_quest') or {}
    if (current.get('confirmed') is not True or current.get('source')!='current-root-typed-quest-collections'
            or not _fresh(current,frame) or current.get('session_id')!=frame.get('session_id')
            or current.get('process_started_at')!=frame.get('process_started_at')
            or str(current.get('character_id'))!=str(frame.get('character_id'))
            or current.get('quest_id')!=node.get('quest_id')
            or current.get('instance_id') is None or not current.get('accept_time')):
        raise ValueError('current reward occurrence owner unavailable')
    details=snapshot['details'];entries=details.get('cleared_quests',[])
    if details.get('reward_present') is not False:
        if len(entries)!=1 or any(entries[0].get(k)!=current[k] for k in ('quest_id','instance_id')):
            raise ValueError('current reward contains a different or grouped quest occurrence')
    return {k:current[k] for k in ('quest_id','instance_id','accept_time','step_id','objective_index') if k in current}


def bind_reward_action(snapshot,frame,node,semantic):
    """Observe the actual clear result; never call visible UI a claimed reward."""
    result=dict(evidence_kind='current-frame',bindings={},missing=[],effect_proofs=[],
        current_action_binding={},layout_snapshot=snapshot,game_inputs=0)
    try:
        if not _read_near_capture(snapshot,frame):raise ValueError('current reward/capture time mismatch')
        owner=reward_ownership(snapshot,frame,node);details=snapshot['details']
        visible=details.get('reward_present')
        if type(visible) is not bool:raise ValueError('current reward root presence unknown')
        fields={'reward_visible':visible}
        if visible:
            fields.update(cleared_instance_ids=[str(e['instance_id']) for e in details['cleared_quests']],
                has_selective_reward=details['cleared_quests'][0]['has_selective_reward'])
        proof=_effect(owner,node,frame,'reward',fields,snapshot['captured_at'],source_revision=_producer_revision(snapshot));result['effect_proofs']=[proof]
        if not visible:return result
        if fields['has_selective_reward'] is not False:raise ValueError('reward selection requires an explicit user choice policy')
        if (details.get('is_active') is not True or details.get('is_show_state') is not True
                or details.get('parts_dirty') is not False or details.get('show_reserved') is not False):
            raise ValueError('current reward layout is inactive or changing')
        controls=[c for c in details.get('button_controls',[]) if c.get('shown') is True]
        if len(controls)!=1 or controls[0].get('confirmButton',{}).get('enabled') is not True or controls[0]['confirmButton'].get('interactable') is not True:
            raise ValueError('current reward confirm control disabled or ambiguous')
        geometry=snapshot.get('reward_geometry')
        if not isinstance(geometry,dict):
            raise ValueError('A incomplete: current native reward parent rectangle/active children supplier')
        if (geometry.get('ui_pointer')!=snapshot.get('ui_pointer') or geometry.get('frame_id')!=frame['frame_id']
                or geometry.get('window_signature')!=frame['window_signature'] or not _fresh(geometry,frame)):
            raise ValueError('current reward geometry belongs to another root/frame')
        geometry=deepcopy(geometry);pointer=None
        if geometry.get('rect_source')=='current-native-output':
            pointer=prepare_pointer_proposal(geometry,snapshot,frame);geometry.update(pointer)
            contract=pointer_effect_contract(pointer,owner,node,frame,snapshot['pointer_state']['observed_at'])
            result['effect_proofs'].extend(contract['effect_proofs'])
        elif geometry.get('active_children')!=['Button_Action']:
            raise ValueError('current reward active children differ from sole source confirm control')
        if not pointer:_occlusion(geometry,snapshot,frame)
        title=details['cleared_quests'][0]['title'];fact=semantic.get('facts',{}).get('reward_title')
        if not title:raise ValueError('current owned reward model has no title')
        if fact and _normal(fact.get('observed_text'))!=_normal(title):
            raise ValueError('current reward title does not match owned result model')
        expected=_expected(proof,fields={'reward_visible':False})
        if pointer and pointer['input_kind']=='pointer_probe':expected=contract['expected_effect']
        observed_at=(geometry.get('native_geometry_proof') or {}).get('observed_at',snapshot['captured_at'])
        action=dict(owner,node_id=node['id'],source='current typed QuestClearEntry and actual reward control',
                    observed_at=observed_at,expected_effect=expected,source_revision=expected['source_revision'],
                    phase='pointer_probe' if pointer and pointer['input_kind']=='pointer_probe' else 'reward.confirm')
        binding=dict(geometry,**owner,frame_id=frame['frame_id'],screen='quest-reward',label='확인',
            source_bound=True,layout_stable=True,semantic_unique=True,enabled=True,occluded=geometry.get('occluded',False),
            current_identity_bound=True,ownership_verified=True,reward_title_confirmed=True)
        width=semantic['frame']['width'];height=semantic['frame']['height']
        safe=(frame.get('current_context') or {}).get('safe_rect',[0,0,width,height]);l,t,r,b=safe
        result.update(frame_id=frame['frame_id'],observed_at=observed_at,session_id=frame['session_id'],
            window_signature=frame['window_signature'],viewport={'width':width,'height':height,
                'origin':frame.get('physical_client_rect',[0,0])[:2]},
            layout_scale=geometry['scale'],safe_rect=geometry.get('safe_rect',[l,t,r-l,b-t]),
            profile=geometry['profile'],source_hashes=[snapshot['source_hash']]+geometry.get('source_hashes',[]),
            goal={'quest_id':owner['quest_id']},bindings={'reward.confirm':binding},
            selected_target_id='reward.confirm',current_action_binding=action)
        result['proposal']=build_target('reward.confirm',result['viewport'],result)
        if result['proposal']['status']=='ready':result['proposal'].update(current_binding=action,expected_effect=expected)
    except (KeyError,TypeError,ValueError,AttributeError) as exc:result['missing'].append(str(exc))
    return result


class QuestUIActionReader:
    """Opt-in production entry; importing/constructing never reads the game."""
    def __init__(self,*,enabled=False,session_factory=QuestUIActionSession):
        self.enabled=enabled;self.session_factory=session_factory

    def read(self,*,frame,node,semantic):
        if not self.enabled:return {'evidence_kind':'unavailable','missing':['typed quest UI reading disabled'],'game_inputs':0}
        target={'objective.deliver':'delivery.item','reward.claim':'reward.confirm'}.get(node.get('op'))
        if not target:return {'evidence_kind':'unavailable','missing':['typed quest UI operation unsupported: '+str(node.get('op'))],'game_inputs':0}
        window=frame.get('window') or {};pid=window.get('pid',frame.get('pid'))
        if type(pid) is not int or pid<=0:return {'evidence_kind':'unavailable','missing':['bound game PID'],'game_inputs':0}
        try:
            with self.session_factory(pid) as session:
                if session.reader.pid!=pid or session.reader.started_at!=_stamp(frame['process_started_at']):
                    raise ValueError('current UI read belongs to a different process occurrence')
                snapshot=session.current_layout(target)
                if hasattr(session,'attach_current_geometry'):
                    try:session.attach_current_geometry(snapshot,frame)
                    except (OSError,RuntimeError,ValueError,KeyError,TypeError,AttributeError) as exc:
                        snapshot.setdefault('missing',[]).append(str(exc))
        except (OSError,RuntimeError,ValueError,KeyError,TypeError,AttributeError) as exc:
            return {'evidence_kind':'unavailable','missing':[str(exc)],'game_inputs':0}
        return bind_quest_ui_action(snapshot,frame,node,semantic)

