-- d4: bounded Entity -> component -> Unit evidence. Never supplies display values. -- Only entities actually returned by World.entities (or TransformComponent.parent) -- enter engine queries. Native IDs never become entity/unit handles. return function(sr) local P={runs=0,state=nil,last=nil,next_at=0} local function fn(ns,k)local ok,f=pcall(function()return sr[ns]and sr[ns][k]end);if ok and type(f)=='function'then return f end end local function validnum(n,max)return type(n)=='number'and n==math.floor(n)and n>=0 and n<=max end local function describe(x) local t=type(x) if t=='number'then return {type=t,value=x==x and math.abs(x)<1e16 and x or 'nonfinite/out-of-range'}end if t=='string'then return {type=t,value=x:sub(1,160)}end if t=='nil'or t=='boolean'then return {type=t,value=x}end local ok,s=pcall(tostring,x);return {type=t,representation=ok and s:sub(1,160)or '',not_identity=true} end local function invoke(f,...) if not f then return false,'API_MISSING'end local ok,a,b=pcall(f,...);if not ok then return false,tostring(a):sub(1,180)end return true,a,b end local function normalize(h) if type(h)~='string'then return nil end if h:sub(1,2):lower()=='0x'then h=h:sub(3)end if #h~=16 or h:find('[^%x]')then return nil end return h:lower() end local function resource_hex(v) -- Use the documented converter on the original object, never parse tostring. if type(v)=='string'then return normalize(v),'HEX_STRING'end if v==nil then return nil,'NIL_RESOURCE'end local convert=fn('IdString64','to_hex');if not convert then return nil,'TO_HEX_API_MISSING'end local ok,h=invoke(convert,v);if not ok then return nil,'TO_HEX_ERROR:'..h end return normalize(h),normalize(h)and'IDSTRING64_TO_HEX'or'TO_HEX_FORMAT_UNSUPPORTED' end local function cp(d)return {entity=d.entity,goid=d.goid,unit=d.unit,resource=d.resource}end local function same(a,b)return a and b and a.entity==b.entity and a.goid==b.goid and a.unit==b.unit and a.resource==b.resource end local function context_same(a,b)return a and b and a.world==b.world and a.session==b.session and a.peer==b.peer end local function scope(c) local a,b,w=fn('Network','game_session'),fn('Network','peer_id'),fn('Application','main_world') return a and b and w and a()==c.session and b()==c.peer and w()==c.world end local function inventory() local out={} for _,name in ipairs({'World','EntityManager','UnitComponent','TransformComponent','Unit','IdString64','Entity'})do local t=sr[name];local names={};if type(t)=='table'then for k,v in pairs(t)do if type(k)=='string'and #k<100 and type(v)=='function'and #names<256 then names[#names+1]=k end end end -- Math/hash constructors can be functions with a namespace __index. if name=='IdString64'and fn(name,'to_hex')then out.idstring_to_hex_available=true end table.sort(names);out[name]={type=type(t),functions=names} end return out end local function finish(st,reason) local r=st.result;r.status=reason;r.scanned=st.cursor-1;r.remaining=math.max(0,(st.count or 0)-r.scanned);r.complete=reason=='COMPLETE_OBSERVATION_NOT_MAPPING' r.duration_game_seconds=st.now-st.started;r.max_slice_cpu_ms=st.max_cpu*1000 P.runs=P.runs+1;P.state=nil;P.next_at=st.now+5 -- Never repeat the same completed/failed scan automatically from index 1. P.last={context=st.context,vehicle=st.vehicle};return r end function P.reset()P.state=nil end local function components(st,e,row) local ok,inst,extra=invoke(st.instances,st.uc,e) if not ok then row.unit_status='INSTANCES_ERROR';row.error=inst;return nil,true end if inst==nil then row.unit_status='NO_UNIT_COMPONENT';return nil,false end if not validnum(inst,4294967295)or extra~=nil then row.unit_status='UNSUPPORTED_COMPONENT_INSTANCES';return nil,true end local uk,u=invoke(st.unit,st.uc,e) if not uk then row.unit_status='UNIT_GET_ERROR';row.error=u;return nil,true end if u==nil then row.unit_status='NO_UNIT';return nil,false end local alive,a=invoke(st.unit_alive,u) if not alive or a~=true then row.unit_status=alive and'DEAD_UNIT'or'UNIT_ALIVE_ERROR';return nil,not alive end local wk,w=invoke(st.unit_world,u) if not wk or w~=st.context.world then row.unit_status=wk and'WRONG_WORLD'or'WORLD_QUERY_ERROR';return nil,not wk end row.unit_status='LIVE_UNIT_IN_WORLD';row.unit=describe(u) local rk,res=invoke(st.resource,u) if not rk then row.resource_error=res;return u,true end row.resource=describe(res);row.resource_hex,row.resource_conversion=resource_hex(res) return u,false end local function inspect(st,e,i) local row={world_index=i,entity=describe(e)} local ok,a=invoke(st.alive,e) if not ok then row.status='ENTITY_ALIVE_ERROR';row.error=a;return row,true,false end if a~=true then row.status='ENTITY_GONE';return row,false,false end st.result.alive_entities=st.result.alive_entities+1 local u,err=components(st,e,row) if u then st.result.unit_entities=st.result.unit_entities+1 end local reason if row.resource_hex then local count=st.result.resource_counts[row.resource_hex] if count~=nil then st.result.resource_counts[row.resource_hex]=count+1 elseif st.resource_kinds<256 then st.result.resource_counts[row.resource_hex]=1;st.resource_kinds=st.resource_kinds+1 else st.result.resource_histogram_overflow=st.result.resource_histogram_overflow+1 end if st.resources[row.resource_hex]then reason='RESOURCE_HASH_CANDIDATE_NOT_INSTANCE_PROOF'end elseif row.resource then st.result.resource_conversion_failures=st.result.resource_conversion_failures+1 end local nk,name=invoke(st.debug_name,e) if nk then row.debug_name=describe(name) if type(name)=='string'then local lower=name:lower() if lower:find('walker',1,true)or lower:find('mech',1,true)then reason=reason or'DEBUG_NAME_HINT_NOT_PROOF'end end end if type(e)=='number'then for _,d in ipairs(st.result.native_descriptors)do if e==d.entity or e==d.unit or e==d.goid then reason=reason or'NUMERIC_EQUALITY_NOT_MAPPING' row.numeric_equalities=row.numeric_equalities or{};row.numeric_equalities[#row.numeric_equalities+1]={native=cp(d),entity=e==d.entity,unit=e==d.unit,goid=e==d.goid} end end end local sampled=i<=4 or i%st.sample_stride==0 if reason or sampled then if st.tc and st.parent and st.transform_instances then local tok,ti,extra=invoke(st.transform_instances,st.tc,e) if tok and validnum(ti,4294967295)and extra==nil then local pok,parent=invoke(st.parent,st.tc,e) row.parent_query=pok and'OK'or'ERROR' if not pok then row.parent_error=parent elseif parent~=nil then row.parent={entity=describe(parent),world_index=st.indices[parent],self_reference=parent==e,not_mech_ownership_proof=true} local live,pa=invoke(st.alive,parent) if live and pa==true then components(st,parent,row.parent)end end else row.parent_query=tok and'NO_SUPPORTED_TRANSFORM_INSTANCE'or'TRANSFORM_INSTANCES_ERROR'end else row.parent_query='PARENT_API_OR_MANAGER_UNAVAILABLE'end end row.status='OBSERVED';row.candidate_reason=reason return row,err,reason~=nil,sampled end function P.step(c,vehicle,meta,clock) if P.runs>=3 or type(clock)~='number'then return nil end local ok,r=pcall(function() if not scope(c)then P.reset();return nil end local st=P.state if st and (not context_same(st.context,c)or not same(st.vehicle,vehicle))then P.reset();st=nil end if not st then if clock=24 or seen[d.entity]then return finish(st,'NATIVE_ROSTER_BOUND_OR_DUPLICATE')end seen[d.entity]=true;st.result.native_descriptors[#st.result.native_descriptors+1]=cp(d);st.resources[d.resource]=true end end local required={{'World','entities'},{'EntityManager','alive'},{'EntityManager','unit_component'},{'UnitComponent','instances'},{'UnitComponent','unit'},{'Unit','alive'},{'Unit','world'},{'Unit','resource_name'}} for _,spec in ipairs(required)do if not fn(spec[1],spec[2])then st.result.missing_api=spec[1]..'.'..spec[2];return finish(st,'ENTITY_API_MISSING')end end st.alive=fn('EntityManager','alive');st.debug_name=fn('EntityManager','debug_name');st.instances=fn('UnitComponent','instances');st.unit=fn('UnitComponent','unit') st.unit_alive=fn('Unit','alive');st.unit_world=fn('Unit','world');st.resource=fn('Unit','resource_name') local cok,uc=invoke(fn('EntityManager','unit_component'),c.world) if not cok or uc==nil then st.result.manager_result=describe(uc);return finish(st,cok and'NO_UNIT_COMPONENT_MANAGER'or'UNIT_COMPONENT_MANAGER_ERROR')end st.uc=uc local tok,tc=invoke(fn('EntityManager','transform_component'),c.world);if tok then st.tc=tc end st.transform_instances=fn('TransformComponent','instances');st.parent=fn('TransformComponent','parent') local eok,es=invoke(fn('World','entities'),c.world) if not eok then st.result.entity_error=es;return finish(st,'ENTITY_ENUMERATION_ERROR')end if type(es)~='table'then st.result.entities_type=type(es);return finish(st,'ENTITIES_NOT_ARRAY')end local n,highest=0,0 for k in pairs(es)do n=n+1;if n>32768 or not validnum(k,32768)or k==0 then return finish(st,'ENTITY_LIST_BOUND_OR_KEY')end;highest=math.max(highest,k)end if n~=highest then return finish(st,'ENTITY_LIST_HOLES')end st.list=es;st.count=n;st.result.enumerated=n;st.indices={};st.sample_stride=math.max(1,math.floor(n/12)) for i=1,n do local e=es[i];local et=type(e) if et~='userdata'and et~='number'then st.result.entity_type=et;return finish(st,'ENTITY_HANDLE_TYPE_UNSUPPORTED')end if st.indices[e]then return finish(st,'DUPLICATE_ENTITY_HANDLE')end;st.indices[e]=i end end st.now=clock if clock-st.started>90 then return finish(st,'TIME_BUDGET_NOT_RESTARTED')end local slice=os.clock();local last=math.min(st.count,st.cursor+95) for i=st.cursor,last do local row,err,candidate,sampled=inspect(st,st.list[i],i) if err then st.result.errors=st.result.errors+1 end if candidate then if #st.result.candidates<64 then st.result.candidates[#st.result.candidates+1]=row else st.result.candidate_overflow=st.result.candidate_overflow+1 end elseif sampled and #st.result.samples<20 then st.result.samples[#st.result.samples+1]=row end st.cursor=i+1 if st.result.errors>=8 then return finish(st,'ENTITY_QUERY_ERRORS')end if os.clock()-slice>=.002 then break end end st.max_cpu=math.max(st.max_cpu,os.clock()-slice) if not scope(c)then P.reset();return nil end if st.cursor>st.count then return finish(st,'COMPLETE_OBSERVATION_NOT_MAPPING')end return nil end) if not ok then local st=P.state;if st then st.now=clock;st.result.error=tostring(r):sub(1,240);return finish(st,'ENTITY_PROBE_LUA_ERROR')end P.next_at=clock+5;return nil end return r end return P end