"""Current map/UI/location snapshots: external VM_READ only, never heap scans.

All object roots follow the current singular entity or current presentation UI
instance service. No client function, script/layout VM, CLI, packet, input,
memory write, injection, privilege adjustment, or arbitrary instance selection.
"""
from datetime import datetime,timezone
import json
import math
from pathlib import Path
import struct

from .map_resources import load_rule
from .map_readonly_reader import Reader,file_sha256
from .map_character_context import active_context,_class,_fields
from .map_character_identity import CharacterMemoryReader

from .map_current_layout import read_current_layout
from .map_nullable import read_nullable_int32
from .map_quest_actions import read_builder_identity,read_current_autoplay,read_active_autoplay
from .map_dynamic_targets import read_quest_marker_scope

class MapReadPending(RuntimeError):pass

class MapMemorySession:
    def __init__(self,pid,*,cancel=None,reader_factory=Reader):
        self.reader=reader_factory(pid,cancel)
        try:
            self.base,self.module=self.reader.module_base()
            self.plan=load_rule('map-memory-layout')
            self.character_plan=load_rule('character-memory-layout')
            key_plan=load_rule('map-key-layout')
            if any(key_plan[k]!=self.plan[k] for k in ('game_assembly_sha256','metadata_sha256')):raise ValueError('지도 지역 키 규칙의 버전이 다릅니다.')
            metadata=Path(self.reader.image).parent/'MabinogiMobile_Data/il2cpp_data/Metadata/global-metadata.dat'
            for p,key in ((self.module,'game_assembly_sha256'),(metadata,'metadata_sha256')):
                if file_sha256(p)!=self.plan[key] or self.plan[key]!=self.character_plan[key]:
                    raise RuntimeError('현재 클라이언트 버전의 지도 읽기 규칙이 없습니다.')
            self._validated={};self._schemas={};self._names={}
            for row in self.plan['classes']+key_plan['classes']:
                key=(row['name'],row['namespace'])
                if key in self._schemas:raise ValueError('지도 클래스 선언이 중복됩니다.')
                self._schemas[key]=row
            identity=next(r for r in self.character_plan['classes'] if r['name']=='ControlObjectIdentityComponent')
            self.identity_class=_class(self.reader,self.base,identity)
            self.identity_shape=self.reader.layout(self.identity_class,identity['fields'])
        except BaseException:
            self.reader.close();raise
    def close(self):self.reader.close()
    def __enter__(self):return self
    def __exit__(self,*_):self.close()

    def watch(self,address,size):
        raw=self.reader.read(address,size)
        self._guards.append((address,raw))
        return raw

    def schema(self,name,namespace=None):
        rows=[v for k,v in self._schemas.items() if k[0]==name and (namespace is None or k[1]==namespace)]
        if len(rows)!=1:raise ValueError('지도 선언을 특정할 수 없습니다: '+name)
        return rows[0]
    def fields(self,klass,schema):
        key=(klass,schema['type_index'])
        if key in self._validated:return self._validated[key]
        r=self.reader
        if r.class_name(klass)!=(schema['name'],schema['namespace']) or r.u16(klass+0x120)!=schema['field_count']:
            raise ValueError('현재 지도 클래스/필드 구성이 변경됐습니다: '+schema['name'])
        table=r.u64(klass+0x80);result={}
        for f in schema['fields']:
            name,kind,owner,offset,_=struct.unpack('<QQQiI',r.read(table+32*f['index'],32))
            if r.ascii(name)!=f['name'] or owner!=klass or offset!=f['offset'] or ((r.u32(kind+8)>>16)&255)!=f['type']['type_code']:
                raise ValueError('현재 지도 필드 선언/위치 확인 실패: '+schema['name']+'.'+f['name'])
            result[f['name']]=offset
        self._validated[key]=result
        return result
    def object_fields(self,pointer,name,namespace=None):
        if not pointer:raise MapReadPending('현재 지도 객체가 아직 준비되지 않았습니다: '+name)
        return self.fields(self.reader.u64(pointer),self.schema(name,namespace))
    def ref(self,pointer,fields,name):return self.reader.u64(pointer+fields[name])
    def bool(self,address):
        value=self.reader.read(address,1)[0]
        if value not in (0,1):raise ValueError('지도 bool 태그 오류')
        return bool(value)
    def float(self,address):
        value=self.reader.unpack(address,'<f')
        if not math.isfinite(value):raise ValueError('현재 지도 실수 값 오류')
        return value
    def pair(self,address):
        result=struct.unpack('<ff',self.reader.read(address,8))
        if any(not math.isfinite(v) or abs(v)>1e9 for v in result):raise ValueError('현재 지도 좌표 값 오류')
        return result

    def array(self,pointer,*,name=None,stride=None,limit=20000):
        r=self.reader
        if not pointer:raise MapReadPending('현재 지도 배열이 아직 준비되지 않았습니다.')
        klass=r.u64(pointer);actual,ns=r.class_name(klass)
        length=r.u64(pointer+24);size=r.u32(klass+0x100)
        if not actual.endswith('[]') or (name and actual!=name) or not 0<=length<=limit or (stride and size!=stride):
            raise ValueError('지도 배열의 타입/길이/간격 확인 실패: '+actual)
        return length,size
    def refs(self,pointer,*,element,limit=4096):
        """Typed array/List/MMList. Validate the version before and after."""
        r=self.reader
        if not pointer:return []
        klass=r.u64(pointer);name,ns=r.class_name(klass)
        version=None;version_address=None
        if name.endswith('[]'):
            array=pointer;count,_=self.array(array,name=element+'[]',stride=8,limit=limit)
        else:
            shape=r.layout(klass)
            if name=='List`1' and ns=='System.Collections.Generic':
                fields={f['name'].lstrip('_'):f['offset'] for f in shape['fields']}
                array=r.u64(pointer+fields['items']);count=r.i32(pointer+fields['size'])
                version_address=pointer+fields['version']
            elif name.startswith(('MMList','MMReadOnlyList')) and ns=='Silvervine.ManualMemory':
                for _ in range(4):
                    fields={f['name']:f['offset'] for f in shape['fields']}
                    if {'array','size','version'}<=fields.keys():break
                    shape=r.layout(int(shape['parent_class'],16))
                if not {'array','size','version'}<=fields.keys():raise ValueError('지도 목록의 원본 필드를 확인하지 못했습니다.')
                array=r.u64(pointer+fields['array']);count=r.i32(pointer+fields['size']);version_address=pointer+fields['version']
            else:raise ValueError('지원하지 않는 지도 참조 목록: '+ns+'.'+name)
            if not 0<=count<=limit:raise ValueError('지도 참조 목록 길이 오류')
            if count:
                capacity,_=self.array(array,name=element+'[]',stride=8,limit=max(limit,30000))
                if capacity<count:raise ValueError('지도 목록/배열 길이 불일치')
        if version_address is not None:version=r.i32(version_address)
        if version_address is not None:self.watch(version_address,4)
        self.watch(array+24,8) if array else None
        values=list(struct.unpack('<'+'Q'*count,r.read(array+32,8*count))) if count else []
        if count:self.watch(array+32,8*count)
        if version_address is not None and version!=r.i32(version_address):raise MapReadPending('지도 목록 갱신 중')
        return values

    def dictionary_values(self,pointer,*,key_type=None,value_type=None,limit=4096):
        """Current rooted UI dictionaries only, reusing the existing decoder."""
        r=self.reader;d=r.dictionary(pointer)
        self.watch(pointer+d['version_offset'],4);self.watch(pointer+d['count_offset'],4)
        if d['count']>limit:raise ValueError('지도 UI 사전 길이 오류')
        result=[]
        if d.get('kind')=='manual_dense':
            keys,values=d['arrays']['keys'],d['arrays']['values']
            if keys['stride']!=4 or values['stride']!=8:raise ValueError('지도 UI 사전 키/값 타입 간격 오류')
            if key_type and keys['name']!=key_type+'[]':raise ValueError('지도 UI 사전 키 타입 오류')
            if value_type and values['name']!=value_type+'[]':raise ValueError('지도 UI 사전 값 타입 오류')
            for i in range(d['count']):
                result.append((r.i32(int(keys['pointer'],16)+32+i*4),r.u64(int(values['pointer'],16)+32+i*8)))
        elif d['count']:
            fields={f['name'].casefold():f['offset']-16 for f in d['entry_layout']['fields']}
            if not {'hashcode','key','value'}<=fields.keys() or d['stride'] not in (24,32):raise ValueError('지도 UI 사전 entry 타입 오류')
            for i in range(d['used']):
                at=int(d['entries'],16)+32+i*d['stride']
                if r.i32(at+fields['hashcode'])>=0:
                    result.append((r.i32(at+fields['key']),r.u64(at+fields['value'])))
            if len(result)!=d['count']:raise MapReadPending('지도 UI 사전 갱신 중')
        if (r.i32(pointer+d['version_offset']),r.i32(pointer+d['count_offset']))!=(d['version'],d.get('used',d['count'])):
            raise MapReadPending('지도 UI 사전 갱신 중')
        return result

    def root_components(self,context):
        r=self.reader;entity=context['chain'][3]
        _fields(r,r.u64(entity),self.plan['singular'])
        parent=r.u64(r.u64(entity)+0x58)
        flags_fields=self.fields(parent,self.schema('EntityBase'))
        flags=r.u64(entity+flags_fields['ComponentUpdateStates'])
        # ComponentUpdateState is a two-byte VALUE TYPE (bool hasValue, byte
        # state), not Byte[]. The original native getter offsets in this plan
        # were byte offsets from array data, not element indexes.
        count,stride=self.array(flags,name='ComponentUpdateState[]',stride=2,limit=30000)
        element=r.u64(r.u64(flags)+0x40)
        shape=self.fields(element,self.schema('ComponentUpdateState'))
        if shape!={'hasValue':16,'state':17}:
            raise ValueError('지도 구성요소 상태 구조 변경: hasValue/state 위치')
        result={}
        for name,entry in self.plan['components'].items():
            index=entry['flag_index'];pointer=r.u64(entity+entry['field']['offset'])
            if index is not None:
                if index%stride or not 0<=index<count*stride:
                    raise ValueError('지도 구성요소 존재 필드 범위 오류: '+name)
                present=self.bool(flags+32+index)
                pointer=pointer if present else 0
            result[name]=pointer
        return result

    def name_text(self,ident):
        if ident in self._names:return self._names[ident]
        r=self.reader;entry=self.schema('UniqueStringErinn')
        klass=_class(r,self.base,entry);fields=self.fields(klass,entry)
        array=r.u64(r.u64(klass+0xb8)+fields['s_permanentStrings'])
        count,_=self.array(array,name='String[]',stride=8,limit=self.plan['limits']['name_pool'])
        if not 0<=ident<count:raise ValueError('지도 UI 이름표 범위 오류')
        value=r.string(r.u64(array+32+ident*8))
        if len(self._names)<4096:self._names[ident]=value
        return value

    def map_key(self,address,name):
        # These are INLINE int32 UniqueString IDs, NOT String pointers. Verify
        # boxed value declaration; offset16 corresponds to inline offset0.
        schema=self.schema(name);klass=_class(self.reader,self.base,schema)
        fields=self.fields(klass,schema)
        if fields!={'uniqueStringInternalId':16}:raise ValueError('지도 지역 키의 원본 값 위치가 변경됐습니다.')
        self.watch(address,4)
        return self.name_text(self.reader.i32(address))

    def long_press_threshold(self):
        r=self.reader;entry=self.schema('FrameworkSingleton');klass=_class(r,self.base,entry)
        fields=self.fields(klass,entry);instance=r.u64(r.u64(klass+0xb8)+fields['instance'])
        if r.u64(instance)!=klass:raise MapReadPending('현재 UI 설정 문맥 준비 중')
        setting=r.u64(instance+fields['<StageConstantSetting>k__BackingField'])
        if not setting:raise MapReadPending('current_long_press_setting_missing')
        f=self.object_fields(setting,'StageConstantSetting')
        constants=self.ref(setting,f,'<StageConstants>k__BackingField')
        if not constants:raise MapReadPending('current_long_press_constants_missing')
        f=self.object_fields(constants,'StageConstants')
        value=self.ref(constants,f,'LongPressDurationSettings')
        f=self.object_fields(value,'LongPressDurationSettings')
        candidates=[offset for name,offset in f.items() if offset==20]
        if len(candidates)!=1:raise ValueError('지도 길게 누르기 설정 값 위치 오류')
        result=self.float(value+20)
        if not 0<result<4.5:raise ValueError('현재 길게 누르기 설정을 안전한 입력 시간으로 연결할 수 없습니다.')
        return result

    def marker(self,pointer):
        r=self.reader;f=self.object_fields(pointer,'MapMarkerControlData')
        self.watch(pointer+f['MapPosition'],8)
        self.watch(pointer+f['LocationDetailControlData'],8)
        self.watch(pointer+f['RatioMin'],8)
        detail=self.ref(pointer,f,'LocationDetailControlData')
        result=dict(key=hex(pointer),map_position=self.pair(pointer+f['MapPosition']),canvas_position=self.pair(pointer+f['CanvasPosition']),
            selected=self.bool(pointer+f['IsSelected']),ratio_min=self.float(pointer+f['RatioMin']),ratio_max=self.float(pointer+f['RatioMax']))
        if not detail:return dict(result,detail=None)
        d=self.object_fields(detail,'LocationDetailControlData')
        base=self.ref(detail,d,'<BaseData>k__BackingField')
        b=self.object_fields(base,'LocationDetailBaseData')
        for name in ('<MoveButtonEnabled>k__BackingField','<IsAutoMoveEnabled>k__BackingField'):
            self.watch(base+b[name],1)
        self.watch(base+b['<MoveButtonEvent>k__BackingField'],8)
        title=r.string(self.ref(base,b,'Title'));marker_id=r.string(self.ref(base,b,'MapMarkerId'))
        builder=self.ref(base,b,'locationDetailEventBuilder')
        self.watch(base+b['locationDetailEventBuilder'],8)
        data=dict(pointer=hex(detail),base_pointer=hex(base),title=title,marker_id=marker_id,
            location_type=r.i32(detail+d['MapLocationType']),distance=r.i32(base+b['<Distance>k__BackingField']),
            label=r.string(self.ref(base,b,'ButtonLabel')),move_enabled=self.bool(base+b['<MoveButtonEnabled>k__BackingField']),
            auto_move_enabled=self.bool(base+b['<IsAutoMoveEnabled>k__BackingField']),world_position=None,target_channel=None)
        if builder:
            e=self.object_fields(builder,'LocationDetailEventBuilder')
            # U<T> is value + change-version, NOT System.Nullable<T>. Native
            # GetLocationDetailButtonEvent loads Position@0x24 (8 bytes) and
            # targetChannel@0x1c (4 bytes) directly for GetMapMoveRequested.
            self.watch(builder+e['Position'],12);self.watch(builder+e['targetChannelSheetId'],8)
            for identity_field in ('npcSheetId','dungeonEntranceSheetId','moonGateSheetId','facilityType'):
                self.watch(builder+e[identity_field],4)
            data.update(world_position=self.pair(builder+e['Position']),target_channel=r.u32(builder+e['targetChannelSheetId']),
                npc_id=r.u32(builder+e['npcSheetId']),dungeon_id=r.u32(builder+e['dungeonEntranceSheetId']),
                moon_gate_id=r.u32(builder+e['moonGateSheetId']),facility_type_raw=r.i32(builder+e['facilityType']),
                battlefield_id=r.string(self.ref(builder,e,'battlefieldEntranceId')))
            # New optional evidence must not erase otherwise usable NPC/map
            # observations when a current value-type class is uninitialized.
            try:data.update(read_builder_identity(self,builder,e))
            except (ValueError,RuntimeError,KeyError) as exc:data['quest_identity_missing']=str(exc)
        event=self.ref(base,b,'<MoveButtonEvent>k__BackingField')
        try:data['quest_marker_scope']=read_quest_marker_scope(self,detail,d,builder_instance=data.get('quest_instance_id'))
        except (ValueError,RuntimeError,KeyError) as exc:
            data['quest_marker_scope']={'available':False,'missing':[str(exc)]}
        data['event_type']=r.class_name(r.u64(event)) if event else None
        data['move_sheet_ids']=[]
        data['move_event_approved']=False
        try:data['quest_autoplay']=read_current_autoplay(self,event,builder_identity={
            key:data[key] for key in ('quest_instance_id','quest_world_prop_id') if key in data})
        except (ValueError,RuntimeError,KeyError) as exc:
            data['quest_autoplay']={'available':False,'missing':[str(exc)],'action_scope':'quest_instance'}
        if event and data['event_type']==('BeginShortCutRequested','MM.Client.Presentation.EventDefinitions.Integrations'):
            f=self.object_fields(event,'BeginShortCutRequested')
            parameters=self.refs(self.ref(event,f,'<Parameter>k__BackingField'),element='ShortCutParameter',limit=32)
            for param in parameters:
                p=self.object_fields(param,'ShortCutParameter')
                data['move_sheet_ids'].append(r.u32(param+p['SheetId']))
            # File-derived MapMove plus UI entry-point movement, not dungeon
            # admission/gathering/dialogue-keyword/teleport/paid shortcut actions.
            allowed={346795422,1386758392,177401910,228307112}
            data['move_event_approved']=(self.bool(event+f['<NeedAutoPlay>k__BackingField'])
                and len(data['move_sheet_ids'])==1 and data['move_sheet_ids'][0] in allowed)
        result['detail']=data
        return result

    def panel_layout(self,selected):
        r=self.reader;entry=self.schema('PresentationSingleton');klass=_class(r,self.base,entry)
        fields=self.fields(klass,entry);instance=r.u64(r.u64(klass+0xb8)+fields['instance'])
        if r.u64(instance)!=klass:raise MapReadPending('현재 UI 서비스 준비 중')
        service=r.u64(instance+fields['<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')
        panels=[]
        for _,handle in self.dictionary_values(dictionary,limit=self.plan['limits']['instances']):
            if not handle:continue
            if r.class_name(r.u64(handle))!=('UIInstanceHandle',''):continue
            shape=r.layout(r.u64(handle));h={x['name']:x['offset'] for x in shape['fields']}
            if not {'<Status>k__BackingField','<Instance>k__BackingField'}<=h.keys():raise ValueError('현재 UI handle 필드 오류')
            if r.i32(handle+h['<Status>k__BackingField'])!=2:continue
            ui=r.u64(handle+h['<Instance>k__BackingField'])
            if ui and r.class_name(r.u64(ui))==('MapLocationPanelUIInstance','MM.Client.Presentation.UI.Instances.StageScene'):panels.append(ui)
        if len(panels)!=1:return None
        ui=panels[0];f=self.object_fields(ui,'MapLocationPanelUIInstance');controller=self.ref(ui,f,'controller')
        f=self.object_fields(controller,'MapLocationUIController')
        if self.ref(controller,f,'detailControlData')!=int(selected['detail']['pointer'],16):return None
        parent=r.u64(r.u64(controller)+0x58);base=self.fields(parent,self.schema('MMMLUIControllerBase'))
        handle=r.u64(controller+base['handle']);h=self.object_fields(handle,'MMMLUIHandle')
        if not self.bool(handle+h['shouldActive']):return None
        window_id=read_nullable_int32(self,handle,self.schema('MMMLUIHandle'),'WindowId')
        if window_id is None:return None
        service=self.ref(handle,h,'service')
        f=self.object_fields(service,'MMMLService');entries=self.ref(service,f,'entryMap')
        matches=[ptr for key,ptr in self.dictionary_values(entries,key_type='Int32',value_type='UnityMMMLWindow',limit=4096) if key==window_id]
        if len(matches)!=1:return None
        window=matches[0];w=self.object_fields(window,'UnityMMMLWindow')
        if r.i32(window+w['windowId'])!=window_id:return None
        parent=r.u64(r.u64(window)+0x58);b=self.fields(parent,self.schema('MMMLWindow'))
        if self.bool(window+b['closing']) or self.bool(window+b['needCheckLayoutDirty']):return None
        context=r.u64(window+b['mainLayoutContext']);c=self.object_fields(context,'LayoutVMContext')
        vm=self.ref(context,c,'VM');v=self.object_fields(vm,'LayoutVM')
        program=self.ref(vm,v,'Program');p=self.object_fields(program,'LayoutProgram')
        infos=self.ref(program,p,'ElementBasicInfos');values=self.ref(vm,v,'values')
        count,_=self.array(infos,name='ElementBasicInfo[]',stride=8,limit=self.plan['limits']['layout_elements'])
        value_count,_=self.array(values,name='Single[]',stride=4,limit=self.plan['limits']['layout_elements']*8+10000)
        if count*8>value_count:raise ValueError('지도 MMML 배치 배열의 길이가 다릅니다.')
        rows=[]
        for i in range(count):
            tag=self.name_text(r.i32(infos+32+i*8))
            if tag not in ('t-MapLocationPanelUI_ButtonCTA','MapLocationPanelUI_ButtonCTA'):continue
            raw=r.read(values+32+i*32,32);floats=struct.unpack('<8f',raw)
            left,top,width,height=[floats[self.plan['layout_values'][k]] for k in ('left','top','width','height')]
            if not all(math.isfinite(x) for x in (left,top,width,height)) or width<=0 or height<=0:continue
            rows.append(dict(rect_ui=(left,top,width,height),raw=raw.hex(),index=i))
        if len(rows)!=1:return None
        row=rows[0]
        self.watch(values+32+row['index']*32,32)
        if r.read(values+32+row['index']*32,32).hex()!=row['raw']:return None
        if self.ref(vm,v,'Program')!=program or self.ref(vm,v,'values')!=values:return None
        scale=self.float(service+f['lastReferenceResolutionScale'])
        size=self.pair(service+f['global_ScreenSize'])
        return dict(rect_ui=row['rect_ui'],scale=scale,screen_ui=size,window_id=window_id,source='current managed MMML LayoutVM outputs; VM not executed')

    def read(self,*,include_map=True,region_requests=None):
        self._guards=[]
        r=self.reader;context=active_context(r,self.base,self.character_plan)
        identity=CharacterMemoryReader._value(r,context['identity_pointer'],self.identity_class,self.identity_shape)
        roots=self.root_components(context);view=roots['CurrentViewingMapUIComponent']
        v=self.object_fields(view,'CurrentViewingMapUIComponent')
        channel=r.u32(view+v['<CurrentChannelSheetId>k__BackingField'])
        self.watch(view+v['<CurrentChannelSheetId>k__BackingField'],4)
        location=roots['IControlObjectLocationInaccurateUpTo1mComponent']
        l=self.object_fields(location,'ControlObjectLocationInaccurateUpTo1mComponent')
        position=self.pair(location+l['location']) if self.bool(location+l['hasControlObject']) else None
        result=dict(pid=r.pid,process_started_at=r.started_at.isoformat(),identity=identity,current_channel=channel,
            world_position=position,position_accuracy_world=1.,location_stamp=r.read(location+l['lastInvokeTime'],4).hex(),
            captured_at=datetime.now(timezone.utc).isoformat(),map_visible=False,map_ready=False,markers=[],selected=None,panel=None,
            memory_writes=0,game_inputs=0,client_functions_called=0)
        try:result['current_autoplay']=read_active_autoplay(self,context)
        except (ValueError,RuntimeError,KeyError) as exc:result['current_autoplay_missing']=str(exc)
        if include_map and roots['MapUIVisibleComponent']:
            visible=roots['MapUIVisibleComponent'];f=self.object_fields(visible,'MapUIVisibleComponent')
            for offset in f.values():self.watch(visible+offset,1)
            result['map_visible']=self.bool(visible+f['<IsVisible>k__BackingField'])
            result['map_transition']=any(self.bool(visible+f[n]) for n in ('<IsInOpeningTransition>k__BackingField',
                '<IsInClosingTransition>k__BackingField','<IsInMapChangeTransition>k__BackingField','<IsHiddenByUIStacking>k__BackingField'))
            if result['map_visible'] and not result['map_transition']:
                image=roots['MapImageUIComponent'];f=self.object_fields(image,'MapImageUIComponent');ui=self.ref(image,f,'ui')
                f=self.object_fields(ui,'MapImageUI');model=ui+f['mapModel']
                self.watch(model,24);self.watch(ui+f['<LayoutSize>k__BackingField'],8)
                meta=r.u64(model+16);m=self.object_fields(meta,'MapModelMeta')
                result['pose']=dict(resource=r.string(self.ref(meta,m,'ResourceName')),centre=self.pair(model),zoom=self.float(model+8),
                    layout_size_ui=self.pair(ui+f['<LayoutSize>k__BackingField']),world_size=r.i32(meta+m['WorldSize']),
                    map_type=r.i32(view+v['<MapType>k__BackingField']))
                result.update(world_data_key=self.map_key(view+v['<WorldDataKey>k__BackingField'],'WorldDataKey'),
                    geo_data_key=self.map_key(view+v['<GeoDataKey>k__BackingField'],'GeoDataKey'),
                    viewing_channel=r.u32(view+v['<ViewingChannelSheetId>k__BackingField']))
                markers=roots['MapMarkerUIComponent'];f=self.object_fields(markers,'MapMarkerUIComponent');data=self.ref(markers,f,'data')
                d=self.object_fields(data,'MapMarkerUIData')
                self.watch(markers+f['data'],8);self.watch(markers+f['IsMapChanging'],1)
                self.watch(data+d['HideByTransition'],1)
                self.watch(data+d['MapMarkerDatas'],8)
                self.watch(data+d['ZoomRatio'],4)
                result['marker_zoom_ratio']=self.float(data+d['ZoomRatio'])
                changing=self.bool(markers+f['IsMapChanging']) or self.bool(data+d['HideByTransition'])
                if not changing and not self.bool(ui+self.object_fields(ui,'MapImageUI')['<IsLoadingMap>k__BackingField']):
                    result['markers']=[self.marker(p) for p in self.refs(self.ref(data,d,'MapMarkerDatas'),element='MapMarkerControlData',limit=4096) if p]
                    sel=roots['SelectedMapMarkerComponent'];s=self.object_fields(sel,'SelectedMapMarkerComponent')
                    self.watch(sel+s['<SelectedMarker>k__BackingField'],8);self.watch(sel+s['<IsLongPress>k__BackingField'],1)
                    selected=self.ref(sel,s,'<SelectedMarker>k__BackingField')
                    if selected:
                        result['selected']=self.marker(selected)
                        result['selected']['long_press']=self.bool(sel+s['<IsLongPress>k__BackingField'])
                        if result['selected']['detail']:result['panel']=self.panel_layout(result['selected'])
                    try:result['hold_threshold']=self.long_press_threshold()
                    except MapReadPending as exc:result['hold_threshold_missing']=str(exc)
                    result['map_ready']=True
        if result['map_ready']:
            try:
                result['current_map_layout']=read_current_layout(self,_class)
            except (ValueError,MapReadPending,KeyError) as exc:
                result['current_map_layout_missing']=str(exc)
        if region_requests:
            from ..typed_region_movement import read_region_targets
            result['typed_region_targets']=read_region_targets(self,result,region_requests,context=context)
        # Reject switches/root replacement, not just the old payload's ID.
        if active_context(r,self.base,self.character_plan)!=context or self.root_components(context)!=roots:
            raise MapReadPending('읽기 중 현재 캐릭터/지도 참조가 변경되었습니다.')
        if CharacterMemoryReader._value(r,context['identity_pointer'],self.identity_class,self.identity_shape)!=identity:
            raise MapReadPending('읽기 중 캐릭터가 변경되었습니다.')
        if any(r.read(address,len(raw))!=raw for address,raw in self._guards):
            raise MapReadPending('읽기 중 지도 배치/목록/선택 상태가 변경되었습니다.')
        return result

class MapMemoryReader:
    def read(self,pid,*,cancel=None,include_map=True):
        with MapMemorySession(pid,cancel=cancel) as session:return session.read(include_map=include_map)
