--- a/addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeMaintenanceProbe.c +++ b/addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeMaintenanceProbe.c @@ -33,6 +33,9 @@ protected SCR_RefuelAtSupportStationAction m_Action; protected BaseActionsManagerComponent m_Actions; protected UserActionContext m_Context; + protected PlayerController m_Controller; + protected SCR_InteractionHandlerComponent m_Interaction; + protected bool m_ReachDiagnosed; protected SCR_FuelSupportStationComponent m_Station; protected SCR_GadgetManagerComponent m_Gadgets; protected ref ERR_NativeMaintenanceMove m_Move; @@ -105,7 +108,7 @@ if (m_Can.FindComponent(SCR_FuelManagerComponent) != m_CanFuel) return false; if (m_Spare.FindComponent(SCR_FuelManagerComponent) != m_SpareFuel) return false; if (!m_Station || m_Can.FindComponent(SCR_FuelSupportStationComponent) != m_Station) return false; - if (!ContextValid()) return false; + if (!ContextValid() || !InteractionValid()) return false; if (m_ActionStarted && m_Action.GetActiveContext() != m_Context) return false; if (!Contains(m_Source, m_Spare) || !(Math.AbsFloat(m_SpareFuel.GetTotalFuel() - m_InitialSpare) <= 0.001)) return false; CarControllerComponent controller = CarControllerComponent.Cast(m_Vehicle.FindComponent(CarControllerComponent)); @@ -133,6 +136,7 @@ protected bool ContextValid() { if (!m_Vehicle || !m_Action || m_Action.GetOwner() != m_Vehicle || !m_Actions || !m_Context) return false; + if (m_Action.WasDisabledByServer()) return false; if (m_Vehicle.FindComponent(BaseActionsManagerComponent) != m_Actions || !m_Actions.IsEnabled()) return false; array contexts = {}; m_Actions.GetContextList(contexts); if (contexts.Count() > 64 || !contexts.Contains(m_Context)) return false; @@ -141,12 +145,43 @@ float radius = m_Context.GetRadius(); return ERR_ForceRules.FiniteVector(m_Context.GetOrigin()) && ERR_ForceRules.Finite(radius) && radius > 0 && radius <= 10; } + protected bool InteractionValid() + { + if (!GetGame() || !m_Controller || GetGame().GetPlayerController() != m_Controller) return false; + if (!m_Player || m_Controller.GetControlledEntity() != m_Player || !m_Interaction) return false; + if (m_Controller.FindComponent(SCR_InteractionHandlerComponent) != m_Interaction) return false; + return m_Interaction.GetControlledEntity() == m_Player; + } + protected float ActionRange() + { + if (!ContextValid() || !InteractionValid()) return -1; + float globalRange = m_Interaction.GetVisibilityRange(); + if (!ERR_ForceRules.Finite(globalRange) || globalRange < 0) return -1; + float range = m_Context.GetVisibilityRange(globalRange); + if (!ERR_ForceRules.Finite(range) || range <= 0) return -1; + return range; + } protected bool AtAction() { - if (!m_Player || !ContextValid()) return false; - // Conservative sphere check also fits within a native capsule context. - if (vector.Distance(m_Player.GetOrigin(), m_Context.GetOrigin()) > Math.Min(3, m_Context.GetRadius())) return false; + float range = ActionRange(); + if (range <= 0 || !ERR_ForceRules.FiniteVector(m_Player.GetOrigin())) return false; + // The native interaction handler uses this configured context/global range + // for distance retention. Context radius/height instead shape ray selection. + // This opt-in method fixture still does not prove UI selection or held input. + float distance = vector.Distance(m_Player.GetOrigin(), m_Context.GetOrigin()); + if (!ERR_ForceRules.Finite(distance) || distance > range) return false; return m_Context.IsInVisibilityAngle(m_Player.GetOrigin()); + } + protected void DiagnoseReach() + { + if (m_ReachDiagnosed) return; + m_ReachDiagnosed = true; + bool context = ContextValid(); + bool interaction = InteractionValid(); + PrintFormat("ERR MAINTENANCE REACH: state=%1 context=%2 interaction=%3 range=%4 started=%5", m_State, context, interaction, ActionRange(), m_ActionStarted); + if (!context || !m_Player) return; + PrintFormat("ERR MAINTENANCE GEOMETRY: player=%1 point=%2 distance=%3 angle=%4", m_Player.GetOrigin(), m_Context.GetOrigin(), vector.Distance(m_Player.GetOrigin(), m_Context.GetOrigin()), m_Context.IsInVisibilityAngle(m_Player.GetOrigin())); + PrintFormat("ERR MAINTENANCE CONTEXT: radius=%1 height=%2 capsule=%3 name=%4", m_Context.GetRadius(), m_Context.GetHeight(), m_Context.IsCapsule(), m_Context.GetContextName()); } protected bool StartAction() { @@ -207,7 +242,11 @@ m_Inventory = InventoryStorageManagerComponent.Cast(m_Player.FindComponent(InventoryStorageManagerComponent)); m_Fuel = SCR_FuelManagerComponent.Cast(m_Vehicle.FindComponent(SCR_FuelManagerComponent)); m_Gadgets = SCR_GadgetManagerComponent.GetGadgetManager(m_Player); - if (!m_Source || !m_Inventory || !m_Fuel || !m_Gadgets) return false; + if (!m_Source || !m_Inventory || !m_Fuel || !m_Gadgets || !GetGame()) return false; + m_Controller = GetGame().GetPlayerController(); + if (!m_Controller) return false; + m_Interaction = SCR_InteractionHandlerComponent.Cast(m_Controller.FindComponent(SCR_InteractionHandlerComponent)); + if (!InteractionValid()) return false; array items = {}; ERR_Inventory.Items(m_Source, items); if (items.Count() > 128) return false; int cans; @@ -273,7 +312,7 @@ if (m_Stopped || m_State < 0) return -1; if (!Enabled() || !Replication.IsServer() || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active()) { Stop(); return -1; } if (m_Started < 0) m_Started = now; - if (!WithinDeadline(m_Started, now)) { Check(false, "fixed refuel proof deadline"); return -1; } + if (!WithinDeadline(m_Started, now)) { DiagnoseReach(); Check(false, "fixed refuel proof deadline"); return -1; } if (m_State == 0) { if (!Check(Select(), "read-only selection pins two original five-litre cans and exact original parked UAZ action/tank")) return -1; --- a/tests/test_native_maintenance_refuel.py +++ b/tests/test_native_maintenance_refuel.py @@ -48,6 +48,7 @@ '''+within+r''' struct Probe {float m_Deadline=-1,m_Started=-1;bool failed=false; void Check(bool, const char*) {failed=true;} + void DiagnoseReach() {} int Tick(float now) { '''+clock+r''' return 0; @@ -80,42 +81,56 @@ # action's cached point initially equals its root; SetActiveContext alone does not # initialize it. Our double models that distinction instead of hiding the bug. import re -methods='\n'.join(body(P,sig) for sig in ('protected bool ContextValid(', 'protected bool AtAction(', 'protected bool StartAction(', 'vector ApproachPosition(')) +methods='\n'.join(body(P,sig) for sig in ('protected bool ContextValid(', 'protected bool InteractionValid(', 'protected float ActionRange(', 'protected bool AtAction(', 'protected bool StartAction(', 'vector ApproachPosition(', 'protected void DiagnoseReach(')) methods=methods.replace('protected ','').replace('null','nullptr').replace('array<','Array<') methods=methods.replace('Array','Array').replace('Array','Array') for ns in ('ERR_ForceRules','ERR_Actors','vector','Math'):methods=methods.replace(ns+'.',ns+'::') -for obj in ('m_Vehicle','m_Action','m_Actions','m_Context','m_Player'):methods=re.sub(r'\b'+obj+r'\.',obj+'->',methods) -methods=methods.replace('FindComponent(BaseActionsManagerComponent)','FindComponent()') +for obj in ('m_Vehicle','m_Action','m_Actions','m_Context','m_Player','m_Controller','m_Interaction'):methods=re.sub(r'\b'+obj+r'\.',obj+'->',methods) +methods=methods.replace('FindComponent(BaseActionsManagerComponent)','FindComponent()').replace('FindComponent(SCR_InteractionHandlerComponent)','FindComponent()').replace('GetGame().','GetGame()->') run(r''' #include #include #include #include +#include struct vector {float x=0,y=0,z=0;static const vector Zero;float&operator[](int i){return i==0?x:(i==1?y:z);}float LengthSq(){return x*x+y*y+z*z;}void Normalize(){float d=std::sqrt(LengthSq());x/=d;y/=d;z/=d;}vector operator-(vector b){return {x-b.x,y-b.y,z-b.z};}vector operator+(vector b){return {x+b.x,y+b.y,z+b.z};}vector operator*(float b){return {x*b,y*b,z*b};}static float Distance(vector a,vector b){return std::sqrt((a-b).LengthSq());}}; const vector vector::Zero{}; namespace ERR_Actors {vector Ground(vector p){return p;}} namespace Math {float Min(float a,float b){return std::min(a,b);}} -namespace ERR_ForceRules {bool Finite(float v){return std::isfinite(v);}bool FiniteVector(vector v){return Finite(v.x);}} +namespace ERR_ForceRules {bool Finite(float v){return std::isfinite(v);}bool FiniteVector(vector v){return Finite(v.x)&&Finite(v.y)&&Finite(v.z);}} templatestruct Array:std::vector{using std::vector::vector;using std::vector::operator=;int Count(){return this->size();}bool Contains(T t){return std::find(this->begin(),this->end(),t)!=this->end();}}; struct BaseUserAction;struct UserActionContext;struct BaseActionsManagerComponent; struct Entity {vector pos;BaseActionsManagerComponent*manager=nullptr;vector GetOrigin(){return pos;}BaseActionsManagerComponent*FindComponent(){return manager;}}; -struct UserActionContext {vector pos{2};float radius=2;bool visible=true;Arrayactions;void GetActionsList(Array&out){out=actions;}vector GetOrigin(){return pos;}float GetRadius(){return radius;}bool IsInVisibilityAngle(vector){return visible;}}; +struct UserActionContext {vector pos{2};float radius=2,visibility=0,height=0;bool visible=true,capsule=false;Arrayactions;void GetActionsList(Array&out){out=actions;}vector GetOrigin(){return pos;}float GetRadius(){return radius;}float GetHeight(){return height;}bool IsCapsule(){return capsule;}std::string GetContextName(){return "fuel";}float GetVisibilityRange(float global){return std::max(global,visibility);}bool IsInVisibilityAngle(vector){return visible;}}; struct BaseActionsManagerComponent {bool enabled=true;Arraycontexts;bool IsEnabled(){return enabled;}void GetContextList(Array&out){out=contexts;}}; -struct BaseUserAction {Entity*owner=nullptr;UserActionContext*context=nullptr;bool progressing=false,cacheOnStart=true;int set=0,started=0;vector cached; - Entity*GetOwner(){return owner;}bool IsInProgress(){return progressing;}void SetActiveContext(UserActionContext*c){context=c;set++;}UserActionContext*GetActiveContext(){return context;} +struct BaseUserAction {Entity*owner=nullptr;UserActionContext*context=nullptr;bool progressing=false,cacheOnStart=true,disabled=false;int set=0,started=0;vector cached; + Entity*GetOwner(){return owner;}bool WasDisabledByServer(){return disabled;}bool IsInProgress(){return progressing;}void SetActiveContext(UserActionContext*c){context=c;set++;}UserActionContext*GetActiveContext(){return context;} void OnActionStart(Entity*){assert(context&&set==1);started++;if(cacheOnStart)cached=context->GetOrigin();}vector GetWorldPositionAction(){return cached;}}; -struct Probe{Entity*m_Vehicle=nullptr,*m_Player=nullptr;BaseUserAction*m_Action=nullptr;BaseActionsManagerComponent*m_Actions=nullptr;UserActionContext*m_Context=nullptr;bool m_ActionStarted=false,held=true,allowed=true;int eligibility=0; +struct SCR_InteractionHandlerComponent{Entity*actor=nullptr;float range=3;Entity*GetControlledEntity(){return actor;}float GetVisibilityRange(){return range;}}; +struct PlayerController{Entity*actor=nullptr;SCR_InteractionHandlerComponent*interaction=nullptr;Entity*GetControlledEntity(){return actor;}SCR_InteractionHandlerComponent*FindComponent(){return interaction;}}; +struct Game{PlayerController*controller=nullptr;PlayerController*GetPlayerController(){return controller;}}game;Game*currentGame=&game;Game*GetGame(){return currentGame;} +int prints=0;templatevoid PrintFormat(Args...){prints++;} +struct Probe{PlayerController*m_Controller=nullptr;SCR_InteractionHandlerComponent*m_Interaction=nullptr;bool m_ReachDiagnosed=false;int m_State=3;Entity*m_Vehicle=nullptr,*m_Player=nullptr;BaseUserAction*m_Action=nullptr;BaseActionsManagerComponent*m_Actions=nullptr;UserActionContext*m_Context=nullptr;bool m_ActionStarted=false,held=true,allowed=true;int eligibility=0; bool Held(){return held;}bool ActionAllowed(){eligibility++;assert(m_ActionStarted&&m_Action->started==1&&m_Action->GetActiveContext()==m_Context);assert(vector::Distance(m_Action->GetWorldPositionAction(),m_Context->GetOrigin())<=.01);return allowed;} '''+methods+r''' }; -struct Fixture{Entity vehicle,player;BaseUserAction action;BaseActionsManagerComponent manager;UserActionContext context;Probe probe; - Fixture(){vehicle.manager=&manager;player.pos={2};action.owner=&vehicle;manager.contexts={&context};context.actions={&action};probe.m_Vehicle=&vehicle;probe.m_Player=&player;probe.m_Action=&action;probe.m_Actions=&manager;probe.m_Context=&context;}}; +struct Fixture{PlayerController controller;SCR_InteractionHandlerComponent interaction;Entity vehicle,player;BaseUserAction action;BaseActionsManagerComponent manager;UserActionContext context;Probe probe; + Fixture(){currentGame=&game;game.controller=&controller;controller.actor=&player;controller.interaction=&interaction;interaction.actor=&player;probe.m_Controller=&controller;probe.m_Interaction=&interaction;vehicle.manager=&manager;player.pos={2};action.owner=&vehicle;manager.contexts={&context};context.actions={&action};probe.m_Vehicle=&vehicle;probe.m_Player=&player;probe.m_Action=&action;probe.m_Actions=&manager;probe.m_Context=&context;}}; int main(){ Fixture ok;assert(ok.action.GetWorldPositionAction().x==0&&ok.context.GetOrigin().x==2);assert(ok.probe.ContextValid()&&ok.probe.AtAction());assert(std::fabs(ok.probe.ApproachPosition().x-2.75f)<.001);assert(ok.probe.StartAction());assert(ok.action.started==1&&ok.action.set==1&&ok.probe.eligibility==1);assert(!ok.probe.StartAction()&&ok.action.started==1); - for(int fault=0;fault<12;fault++){Fixture f;BaseActionsManagerComponent foreign;Entity other;UserActionContext otherContext;BaseUserAction otherAction; - switch(fault){case 0:f.vehicle.manager=&foreign;break;case 1:f.manager.enabled=false;break;case 2:f.manager.contexts={&otherContext};break;case 3:f.context.actions={&otherAction};break;case 4:f.manager.contexts.resize(65,&f.context);break;case 5:f.context.actions.resize(65,&f.action);break;case 6:f.context.pos={NAN};break;case 7:f.context.radius=0;break;case 8:f.context.radius=11;break;case 9:f.action.owner=&other;break;case 10:f.probe.m_Context=nullptr;break;case 11:f.probe.m_Vehicle=nullptr;break;} + for(int fault=0;fault<13;fault++){Fixture f;BaseActionsManagerComponent foreign;Entity other;UserActionContext otherContext;BaseUserAction otherAction; + switch(fault){case 0:f.vehicle.manager=&foreign;break;case 1:f.manager.enabled=false;break;case 2:f.manager.contexts={&otherContext};break;case 3:f.context.actions={&otherAction};break;case 4:f.manager.contexts.resize(65,&f.context);break;case 5:f.context.actions.resize(65,&f.action);break;case 6:f.context.pos={NAN};break;case 7:f.context.radius=0;break;case 8:f.context.radius=11;break;case 9:f.action.owner=&other;break;case 10:f.probe.m_Context=nullptr;break;case 11:f.probe.m_Vehicle=nullptr;break;case 12:f.action.disabled=true;break;} assert(!f.probe.ContextValid()&&!f.probe.StartAction()&&!f.action.started&&!f.action.set);} for(int fault=0;fault<4;fault++){Fixture f;if(fault==0)f.probe.held=false;if(fault==1)f.player.pos={10};if(fault==2)f.context.visible=false;if(fault==3)f.action.progressing=true;assert(!f.probe.StartAction()&&!f.action.started&&!f.action.set);} + // Small elevated sphere/capsule is a target shape, not a feet reach limit. + for(bool capsule:{false,true}){Fixture f;f.context.radius=.2;f.context.height=2;f.context.capsule=capsule;f.context.pos={2,1.2,0};f.player.pos={2.5,0,0};assert(f.probe.AtAction()&&f.probe.StartAction());} + // Use exact configured global/context visibility, with no arbitrary3m ceiling. + Fixture configured;configured.interaction.range=2;configured.context.visibility=5;configured.player.pos={6.5,0,0};assert(configured.probe.ActionRange()==5&&configured.probe.AtAction());configured.player.pos={7.001f,0,0};assert(!configured.probe.AtAction()); + for(int fault=0;fault<9;fault++){Fixture f;Entity other;PlayerController foreign;SCR_InteractionHandlerComponent foreignHandler; + switch(fault){case 0:game.controller=&foreign;break;case 1:f.controller.actor=&other;break;case 2:f.controller.interaction=&foreignHandler;break;case 3:f.interaction.actor=&other;break;case 4:currentGame=nullptr;break;case 5:f.probe.m_Controller=nullptr;break;case 6:f.probe.m_Interaction=nullptr;break;case 7:f.interaction.range=-1;break;case 8:f.interaction.range=NAN;break;} + assert(!f.probe.AtAction()&&!f.probe.StartAction()&&!f.action.started);} + Fixture diagnostic;prints=0;diagnostic.probe.DiagnoseReach();assert(prints==3);diagnostic.probe.DiagnoseReach();assert(prints==3);diagnostic.probe.m_Context=nullptr;diagnostic.probe.DiagnoseReach();assert(prints==3); + Fixture missingDiagnostic;missingDiagnostic.probe.m_Context=nullptr;prints=0;missingDiagnostic.probe.DiagnoseReach();missingDiagnostic.probe.DiagnoseReach();assert(prints==1); Fixture staleCache;staleCache.action.cacheOnStart=false;assert(!staleCache.probe.StartAction()&&staleCache.action.started==1&&staleCache.probe.eligibility==0&&staleCache.probe.m_ActionStarted); Fixture refusal;refusal.probe.allowed=false;assert(!refusal.probe.StartAction()&&refusal.action.started==1&&refusal.probe.m_ActionStarted); }