Files
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

178 lines
17 KiB
Plaintext

--- 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<UserActionContext> 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<IEntity> 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<UserActionContext>','Array<UserActionContext*>').replace('Array<BaseUserAction>','Array<BaseUserAction*>')
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 <cassert>
#include <vector>
#include <algorithm>
#include <cmath>
+#include <string>
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);}}
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;using std::vector<T>::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;Array<BaseUserAction*>actions;void GetActionsList(Array<BaseUserAction*>&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;Array<BaseUserAction*>actions;void GetActionsList(Array<BaseUserAction*>&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;Array<UserActionContext*>contexts;bool IsEnabled(){return enabled;}void GetContextList(Array<UserActionContext*>&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;template<class...Args>void 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);
}