-- Lifecycle: install once, forward previous update/shutdown, fail closed. return function(sr,N,Reader,Render,Z,config,ART,RenderMech,Arms,encode,Spatial,BridgeProbe) local key='__ARENA_MECH_PART_HUD' if rawget(_G,key) then return {installed=true,duplicate=true} end local loader=rawget(_G,'CowboyBingusModLoader');local previous=rawget(_G,'update') if type(sr)~='table' or type(loader)~='table' or loader.api~=1 or type(previous)~='function' then return {installed=false,reason='Bingus API1 required'} end local App,Net,GS=sr.Application,sr.Network,sr.GameSession local function call(f,...) if type(f)~='function' then return nil end local ok,a,b=pcall(f,...);if ok then return a,b end end local dir=call(os.getenv,'APPDATA');dir=dir and (dir..'/Arrowhead/Helldivers2/') or nil local C=config(dir);local D=RenderMech(sr,C,ART,Render(sr,C)) local S={diagnostic_count=0,installed=true,clock=0,next_poll=0,status='START',errors=0,log_lines=0,rows=nil,title=nil,stale_since=nil} rawset(_G,key,S) local function log(s) if not C.debug or not dir or S.log_lines>=400 then return end pcall(function() local f=io.open(dir..'mech_hud.log','a');if not f then return end if (f:seek('end') or 0)>262144 then f:close();return end f:write(os.date('!%Y-%m-%dT%H:%M:%SZ')..' '..s..'\n');f:close();S.log_lines=S.log_lines+1 end) end local function status(s) if S.status~=s then S.status=s;log(s) end end log('MECH_PART_HUD 1.11.1 START read_only=1 contract='..tostring(N.version)) local function integer(v,hi) return type(v)=='number' and v==math.floor(v) and v>=0 and v<=hi end local function local_avatar(session,peer) local ids=call(GS.objects_owned_by,session,peer);if type(ids)~='table' then return nil end if S.player and ids and call(GS.game_object_is_type,session,S.player,'un6y1d')==true then local owned=false;for _,id in pairs(ids) do if id==S.player then owned=true;break end end if not owned then S.player=nil end else S.player=nil end if S.player==nil then local n=0 for _,id in pairs(ids) do n=n+1;if n>1024 then return nil end if integer(id,32766) and call(GS.game_object_is_type,session,id,'un6y1d')==true then if S.player~=nil and S.player~=id then S.player=nil;return nil end;S.player=id end end end if S.player==nil then return nil end local av=call(GS.game_object_field,session,S.player,'baegche') if not integer(av,32766) or av==0 or call(GS.game_object_exists,session,av)~=true then return nil end return av end local function build(v) local rows={{label='HULL',hp=v.hull_valid and v.hull or nil,max=v.hull_max,major=true}} local by={};for _,z in ipairs(v.zones) do by[z.info.id]=z end for _,id in ipairs(Z.order) do local z=by[id] if z then rows[#rows+1]={label=z.info.label,hp=z.valid and z.hp or nil,max=z.max,minor=z.info.minor} end end local zones={};for id,z in pairs(by) do zones[id]={hp=z.valid and z.hp or nil,max=z.max} end return rows,zones end -- Arm cache belongs to the complete descriptor AND the current session/world. local AR={list=nil,next=0,expires=0,vehicle=nil,dumped=false,fails=0} local function reset_arms() if BridgeProbe then BridgeProbe.reset()end S.bridge_meta=nil AR={list=nil,next=0,expires=0,vehicle=nil,dumped=false,fails=0};S.arms=nil;S.arm_status=nil;S.arm_association=nil;S.arm_pair_sig=nil end local function hide() S.rows=nil;reset_arms();D.clear() end -- Diagnostics never supply displayed arm values or guess an ownership edge. local function api_inventory() local out={} for _,name in ipairs({'Unit','World','GameSession','EntityManager','UnitSynchronizer','Vector3'})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<=96 and type(v)=='function'then names[#names+1]=k;if #names>=256 then break end end end end table.sort(names);out[name]=names;out[name..'_type']=type(t) end return out end local function probe_candidates(meta) local reads={};if not meta then return reads end for i,d in ipairs(meta.candidate_descriptors or {})do if i>8 then break end N.win.begin_sample();local g=N.graph(N.win.read,N.base) local ok,a=pcall(function()local v=Arms.read(g,d);g:validate();return v end) if ok then reads[#reads+1]={descriptor=d,hp=a.hp,main=a.main,max=a.max,source=a.source,zones=a.zones,side=a.side,skip=a.skip} else reads[#reads+1]={descriptor=d,error=tostring(a)}end end return reads end local function arm_dump(v,list,link,meta,reads) if AR.dumped or not C.debug or not dir or not encode or S.diagnostic_count>=8 then return end;AR.dumped=true pcall(function() local out={format='mech-spatial-diagnostic-v3',hud='1.11.1',utc=os.date('!%Y-%m-%dT%H:%M:%SZ'),mech=v.vehicle,link=link,meta=meta,arms=reads, display_linked=#list>0,candidate_reads=#list==0 and probe_candidates(meta)or nil,api_functions=api_inventory(), note='Spatial matching is a guarded heuristic, NOT native attachment proof; unsupported bindings and close/duplicate candidates are refused.'} local path=dir..'mech_hud_spatial_'..os.date('!%Y%m%d_%H%M%S')..string.format('_%02d.json',S.diagnostic_count+1) local e=io.open(path,'rb');if e then e:close();return end local f=io.open(path,'wb');if f then f:write(encode(out));f:close();S.diagnostic_count=S.diagnostic_count+1;log('ARM_DIAGNOSTIC '..path:match('[^/]+$')) end end) end local function arms_update(v,avatar,session,peer) local model=v.model if not model.arms or not Arms then S.arms=nil;return end if not N.same(AR.vehicle,v.vehicle)then reset_arms();AR.vehicle={entity=v.vehicle.entity,unit=v.vehicle.unit,goid=v.vehicle.goid,resource=v.vehicle.resource} end -- Geometric matches are never reused across polls: discover a fresh roster every time. if AR.spatial then AR.list=nil;AR.next=0;S.arms=nil;S.arm_association=nil end if AR.list and S.clock>=AR.expires then AR.list=nil;S.arms=nil;S.arm_association=nil end if not AR.list then if S.clock=3 then AR.list=nil;AR.fails=0;log('ARM_RELINK '..tostring(res)) end return end AR.fails=0 if not AR.dumped then local reads={};for i,a in ipairs(res) do reads[i]={descriptor=a.descriptor,hp=a.hp,main=a.main,source=a.source,max=a.max,zones=a.zones,side=a.side,guessed=a.guessed} end arm_dump(v,AR.list,AR.link,AR.meta,reads) end -- Sides: resource/zone-verified; otherwise numbered (no guessed left/right). -- A guessed (uncatalogued) arm takes the side opposite the one known arm. local sides={};for _,a in ipairs(res) do if a.side then sides[a.side]=true end end for _,a in ipairs(res) do if not a.side and a.guessed then if sides.L and not sides.R then a.side='R' elseif sides.R and not sides.L then a.side='L' end end end local arms={};local known=true for _,a in ipairs(res) do if not a.side then known=false end end for i,a in ipairs(res) do arms[#arms+1]={hp=a.valid and math.max(0,a.hp) or nil,max=a.max,side=known and a.side or nil,index=i} end if AR.spatial then AR.warm_count=math.min(3,(AR.warm_count or 0)+1) if AR.warm_count<3 then S.arms=nil;S.arm_association=nil;return end end S.arms=arms;S.arm_association=AR.spatial and 'SPATIAL_HEURISTIC'or nil local sig={};for _,a in ipairs(res) do sig[#sig+1]=tostring(a.side)..'='..tostring(a.hp)..'/'..tostring(a.max)..'('..tostring(a.source)..')' end sig=table.concat(sig,' ');if sig~=AR.sig then AR.sig=sig;log('ARMS '..sig) end end local function probe_update(v,av,session,peer) if not BridgeProbe or not C.debug or C.spatial==false or not S.bridge_meta then return end local report=BridgeProbe.step({avatar=av,session=session,peer=peer,world=S.world},v.vehicle,S.bridge_meta,S.clock) if not report or not dir or not encode then return end local written,err=pcall(function() report.utc=os.date('!%Y-%m-%dT%H:%M:%SZ') local name='mech_hud_bridge_'..os.date('!%Y%m%d_%H%M%S')..string.format('_%02d.json',BridgeProbe.runs) local old=io.open(dir..name,'rb');if old then old:close();return 'EXISTS'end local f=io.open(dir..name,'wb');if not f then return 'OPEN_FAILED'end f:write(encode(report));f:close();log('BRIDGE_EVIDENCE '..report.status..' scanned='..tostring(report.scanned)..'/'..tostring(report.enumerated)..' '..name) return 'WRITTEN' end) if not written or err~='WRITTEN'then log('BRIDGE_REPORT_ERROR '..tostring(err))end end local function poll(session,peer) local av=local_avatar(session,peer) if not av then status('WAIT_AVATAR');hide();return end local v,why,kind=Reader.sample(av,S.clock) if N.ready and N.reason and S.native_contract~=N.reason then S.native_contract=N.reason;log('NATIVE_COMPATIBLE '..tostring(N.reason)) end if v then S.rows,S.zones=build(v) local arms_ok,arms_error=pcall(arms_update,v,av,session,peer) if not arms_ok then reset_arms();AR.next=S.clock+5;AR.vehicle={entity=v.vehicle.entity,unit=v.vehicle.unit,goid=v.vehicle.goid,resource=v.vehicle.resource} if S.arm_error~=tostring(arms_error)then S.arm_error=tostring(arms_error);log('ARM_UPDATE_ERROR '..S.arm_error)end else S.arm_error=nil end local pok,perr=pcall(probe_update,v,av,session,peer) if not pok then if BridgeProbe then BridgeProbe.reset()end;S.bridge_meta=nil;log('BRIDGE_QUERY_ERROR '..tostring(perr))end S.title=v.model.name;S.hull,S.hull_max=v.hull_valid and v.hull or nil,v.hull_max;S.code=v.model.code;S.cn=v.model.cn;S.arm_labels=v.model.arm_labels;S.stale_since=nil local show='SHOW '..v.model.name..' goid='..v.vehicle.goid..' entity='..v.vehicle.entity if S.status~=show then status(show) end return end if AR.spatial then S.arms=nil;S.arm_association=nil;AR.warm_count=0 end if kind=='TRANSIENT' and S.rows then -- Keep the last value up to 1 s on a proven short read only. S.stale_since=S.stale_since or S.clock if S.clock-S.stale_since<1 then return end end status(tostring(kind)..' '..tostring(why));hide() end local function update(dt) local step=type(dt)=='number' and dt==dt and math.max(0,math.min(0.25,dt)) or 0.016667 S.clock=S.clock+step local session=call(Net.game_session);local peer=call(Net.peer_id) local worlds=call(App.worlds) or {};local world=call(App.main_world) if session~=S.session or peer~=S.peer or world~=S.world then D.reset(worlds);S.rows=nil;reset_arms();S.player=nil;S.session,S.peer,S.world=session,peer,world end if not session or not peer or call(GS.in_session,session)~=true then status('WAIT_SESSION');hide();return end if S.clock>=S.next_poll then S.next_poll=S.clock+C.poll;poll(session,peer) end if not S.rows then return end if sr.Window and call(sr.Window.show_cursor)==true then D.clear();return end if not D.ensure(worlds,world) then return end local w,h=call(sr.Gui.resolution) if type(w)~='number' or type(h)~='number' or w<=0 or h<=0 then return end local before=D.mode D.draw(w,h,{title={code=S.code or 'EXO',cn=S.cn or ''},arm_labels=S.arm_labels,rows=S.rows,zones=S.zones,arms=S.arms,arm_association=S.arm_association,hull=S.hull,hull_max=S.hull_max,clock=S.clock}) if D.mode~=before then log('RENDER_MODE '..tostring(D.mode)) end end local retry=0 rawset(_G,'update',function(...) if not S.disabled and S.clock>=retry then local ok,err=pcall(update,...) if not ok then S.errors=S.errors+1;pcall(D.clear);S.rows=nil;retry=S.clock+1 log('RECOVERABLE_ERROR '..tostring(err)) if S.errors>=20 then S.disabled=true;log('DISABLED_AFTER_20_ERRORS') end end else local dt=select(1,...);S.clock=S.clock+(type(dt)=='number' and dt==dt and math.max(0,math.min(0.25,dt)) or 0.016667) end return previous(...) end) local stop=rawget(_G,'shutdown') rawset(_G,'shutdown',function(...) pcall(D.reset,call(App.worlds) or {}) if type(stop)=='function' then return stop(...) end end) return S end