Files
EscapistsReintroduced/tests/test_spectator_recovery.py
T
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

194 lines
13 KiB
Python

"""Execute production input dispatch and recovery with native camera/editor doubles."""
from policy_harness import ROOT,body,run
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Observer.c'
s=body(p,'protected void ERR_UpdateRecoveryCamera(').replace('protected ','').replace('ERR_GameMode mode','ERR_GameMode* mode').replace('null','nullptr')
for name in ('ERR_Actors','SCR_EditorManagerEntity','LogLevel'):
s=s.replace(name+'.',name+'::')
for name in ('SCR_EditorManagerEntity','CameraManager','CameraBase'):
s=s.replace(name+' ',name+'* ')
for name in ('mode','editor','manager'):
s=s.replace(name+'.',name+'->')
s=s.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->')
run(r'''#include <cassert>
struct Actor {bool alive=true,conscious=true;};using IEntity=Actor*;
struct ERR_Actors {static bool Alive(IEntity a){return a&&a->alive;}static bool CanFight(IEntity a){return Alive(a)&&a->conscious;}};
struct ERR_GameMode {bool active=true;bool Active(){return active;}};
struct SCR_EditorManagerEntity {bool opened=false,canClose=true;int closes=0;static SCR_EditorManagerEntity* current;static SCR_EditorManagerEntity* GetInstance(){return current;}bool IsOpened(){return opened;}void Close(bool){closes++;if(canClose)opened=false;}} editor;
SCR_EditorManagerEntity* SCR_EditorManagerEntity::current=&editor;
struct CameraBase{} player,other;
struct CameraManager {CameraBase* current=&other;int sets=0;void SetCamera(CameraBase* c){current=c;sets++;}CameraBase* CurrentCamera(){return current;}} manager;
struct World {float now=0;float GetWorldTime(){return now;}} world;
struct Game {World* GetWorld(){return &world;}CameraManager* GetCameraManager(){return &manager;}} game;
Game* GetGame(){return &game;}namespace LogLevel {enum{WARNING};}int warnings=0;void Print(const char*,int){warnings++;}
struct Controller {
IEntity controlled=nullptr,m_ERRRecoveryBody=nullptr;float m_ERRRecoveryDeadline=-1;CameraBase* camera=&player;
IEntity GetControlledEntity(){return controlled;}CameraBase* GetPlayerCamera(){return camera;}
'''+s+r'''
};
int main(){
ERR_GameMode mode;Actor body,replacement;Controller c;c.controlled=&body;
// Healthy admins retain their camera: restoration belongs only to recovery.
c.ERR_UpdateRecoveryCamera(&mode);assert(manager.sets==0&&editor.closes==0);
body.conscious=false;c.ERR_UpdateRecoveryCamera(&mode);assert(c.m_ERRRecoveryBody==&body&&manager.sets==0);
// Recovery works with NO observer-camera instance and a different active camera.
editor.opened=true;body.conscious=true;c.ERR_UpdateRecoveryCamera(&mode);
assert(editor.closes==1&&!editor.opened&&manager.current==&player&&!c.m_ERRRecoveryBody);
manager.current=&other;c.ERR_UpdateRecoveryCamera(&mode);assert(manager.current==&other); // no permanent camera lock
// An asynchronously closing editor retains the pending recovery; next tick completes.
body.conscious=false;c.ERR_UpdateRecoveryCamera(&mode);body.conscious=true;editor.opened=true;editor.canClose=false;
c.ERR_UpdateRecoveryCamera(&mode);assert(c.m_ERRRecoveryBody==&body);
editor.canClose=true;c.ERR_UpdateRecoveryCamera(&mode);assert(!c.m_ERRRecoveryBody&&manager.current==&player);
// Wait for late camera creation, then return once.
body.conscious=false;c.ERR_UpdateRecoveryCamera(&mode);body.conscious=true;c.camera=nullptr;
c.ERR_UpdateRecoveryCamera(&mode);assert(c.m_ERRRecoveryBody==&body);
c.camera=&player;c.ERR_UpdateRecoveryCamera(&mode);assert(!c.m_ERRRecoveryBody);
// Refused editor close is bounded, warning instead of permanently stealing input.
body.conscious=false;c.ERR_UpdateRecoveryCamera(&mode);body.conscious=true;editor.opened=true;editor.canClose=false;
c.ERR_UpdateRecoveryCamera(&mode);world.now=10001;c.ERR_UpdateRecoveryCamera(&mode);assert(!c.m_ERRRecoveryBody&&warnings==1);
editor.opened=false;editor.canClose=true;
// Changed possession / stopped round must not return a stale casualty's camera.
body.conscious=false;c.ERR_UpdateRecoveryCamera(&mode);c.controlled=&replacement;int before=manager.sets;
c.ERR_UpdateRecoveryCamera(&mode);assert(!c.m_ERRRecoveryBody&&manager.sets==before);
c.controlled=&body;c.ERR_UpdateRecoveryCamera(&mode);mode.active=false;body.conscious=true;
c.ERR_UpdateRecoveryCamera(&mode);assert(!c.m_ERRRecoveryBody&&manager.sets==before);
}
''')
input_body=body(p,'void Input(').replace('SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());','SCR_PlayerController* controller = &pc;').replace('controller.','controller->').replace('ERR_ObserverRules.','ERR_ObserverRules::')
run(r'''#include <cassert>
struct ERR_ObserverRules {
'''+body(p,'static int Cycle(')+body(p,'static bool Toggle(')+r'''
};
struct SCR_PlayerController {int calls=0,direction=0;void ERR_CycleObserver(int d){calls++;direction=d;}} pc;
struct Follow {bool m_LeftDown=false,m_RightDown=false,m_EnterDown=false,m_PlayerView=false;int m_InputResumeFrames=0;
'''+input_body+r'''
};
int main(){
Follow f;f.Input(true,true,false,false);assert(pc.calls==1&&pc.direction==-1);
f.Input(true,true,false,false);assert(pc.calls==1);
f.Input(true,false,false,false);f.Input(true,false,true,false);assert(pc.calls==2&&pc.direction==1);
f.Input(true,true,true,false);assert(pc.calls==2);
f.Input(false,false,false,true);f.Input(true,false,false,true);assert(!f.m_PlayerView); // menu-held Enter
f.Input(true,false,false,false);f.Input(true,false,false,true);assert(f.m_PlayerView);
f.Input(true,false,false,true);assert(f.m_PlayerView);
f.Input(true,false,false,false);f.Input(true,false,false,true);assert(!f.m_PlayerView);
// Inactive chat context masks held keys as zero; submission must not become
// a new camera press when the context resumes (including its first zero frame).
f.Input(false,false,false,false);
f.Input(true,false,false,false);
f.Input(true,false,false,true);assert(!f.m_PlayerView);
f.Input(true,false,false,true);assert(!f.m_PlayerView);
f.Input(true,false,false,false);
f.Input(true,false,false,true);assert(f.m_PlayerView);
int before=pc.calls;
f.Input(false,false,false,false);
f.Input(true,false,false,false);
f.Input(true,true,false,false);f.Input(true,true,false,false);assert(pc.calls==before);
f.Input(true,false,false,false);f.Input(true,true,false,false);assert(pc.calls==before+1);
}
''')
# Execute the actual chat-manager state, capture helper, and pre-activation gate.
# Chat contexts expire independently of widget focus; open panels are authoritative.
def camera_ui_cpp(source):
import re
source=source.replace('override protected ', '').replace('super.', 'Base::').replace('null','nullptr')
# Enforce initializes local booleans to false; C++ otherwise leaves them undefined.
source=re.sub(r'\bbool (\w+);',r'bool \1=false;',source)
source=source.replace('GetGame().', 'GetGame()->').replace('GetInputManager().', 'GetInputManager()->').replace('GetWorkspace().', 'GetWorkspace()->')
for typ in ('SCR_ChatPanelManager','SCR_ChatPanel','EditBoxWidget','ERR_ObserverChatInput'):
source=source.replace(typ+'.',typ+'::')
source=source.replace('foreach (SCR_ChatPanel panel : m_aChatPanels)', 'for (SCR_ChatPanel panel : m_aChatPanels)')
pointer_names=[]
for typ in ('SCR_ChatPanelManager','SCR_ChatPanel','Widget','InputManager'):
pointer_names+=re.findall(r'\b'+typ+r' (\w+)',source)
source=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',source)
for name in pointer_names:
source=re.sub(r'\b'+name+r'\.',name+'->',source)
source=source.replace('FindHandler(SCR_ChatPanel)', 'FindHandler()')
return source
manager_method=camera_ui_cpp(body(p,'bool ERR_HasOpenChatPanel('))
capture=camera_ui_cpp(body(p,'static bool Capture('))
gate=camera_ui_cpp(body(p,'override protected bool IsDisabledByMenu('))
ui_prelude=r'''#include <cassert>
#include <string>
#include <vector>
struct SCR_ChatPanel {bool open=false;bool IsOpen(){return open;}static SCR_ChatPanel*Cast(SCR_ChatPanel*p){return p;}};
struct Widget {Widget*parent=nullptr;bool edit=false;SCR_ChatPanel*handler=nullptr;Widget*GetParent(){return parent;}SCR_ChatPanel*FindHandler(){return handler;}};
struct EditBoxWidget {static Widget*Cast(Widget*w){return w&&w->edit?w:nullptr;}};
struct InputManager {bool chat=false;int renewals=0;
bool IsContextActive(const char*name){assert(std::string(name)=="ChatContext");return chat;}
void ActivateContext(const char*name,int durationMs){assert(std::string(name)=="ChatContext"&&durationMs==1);chat=true;renewals++;}
void Expire(){chat=false;}
} inputs;
struct Workspace {Widget*focus=nullptr;Widget*GetFocusedWidget(){return focus;}} workspace;
struct Game {InputManager*GetInputManager(){return &inputs;}Workspace*GetWorkspace(){return &workspace;}} game;
Game*GetGame(){return &game;}
struct SCR_ChatPanelManager {static SCR_ChatPanelManager*current;std::vector<SCR_ChatPanel*>m_aChatPanels;
static SCR_ChatPanelManager*GetInstance(){return current;}
'''+manager_method+r'''
} chatManager;
SCR_ChatPanelManager*SCR_ChatPanelManager::current=&chatManager;
struct ERR_ObserverChatInput {
'''+capture+r'''
};
struct Base {bool menu=false;bool IsDisabledByMenu(){return menu;}};
'''
ui_tests=r'''
int main(){
Camera c;SCR_ChatPanel registered,unregistered;
chatManager.m_aChatPanels={nullptr,&registered};
assert(!chatManager.ERR_HasOpenChatPanel());assert(!c.IsDisabledByMenu());assert(inputs.renewals==0);
// A dead observer can retain open chat after the native HUD stops renewing it.
// No focus or previously active context is required for registered open chat.
registered.open=true;assert(chatManager.ERR_HasOpenChatPanel());
assert(c.IsDisabledByMenu());assert(inputs.chat&&inputs.renewals==1);
inputs.Expire();assert(c.IsDisabledByMenu());assert(inputs.chat&&inputs.renewals==2);
// Native menus retain priority; a camera must not activate input behind them.
inputs.Expire();c.menu=true;assert(c.IsDisabledByMenu());assert(!inputs.chat&&inputs.renewals==2);c.menu=false;
registered.open=false;assert(!chatManager.ERR_HasOpenChatPanel());assert(!c.IsDisabledByMenu());
// An unregistered still-open panel is discoverable through focus ancestry.
Widget panelWidget,child,edit,ordinary;panelWidget.handler=&unregistered;child.parent=&panelWidget;
unregistered.open=true;workspace.focus=&child;
assert(c.IsDisabledByMenu());assert(inputs.chat&&inputs.renewals==3);
inputs.Expire();SCR_ChatPanelManager::current=nullptr;
assert(c.IsDisabledByMenu());assert(inputs.chat&&inputs.renewals==4);
// A closed panel cannot renew chat, even if it still owns focused widgets.
unregistered.open=false;inputs.Expire();assert(!c.IsDisabledByMenu());assert(inputs.renewals==4);
edit.edit=true;edit.parent=&panelWidget;workspace.focus=&edit;
assert(c.IsDisabledByMenu());assert(!inputs.chat&&inputs.renewals==4);
// Ordinary password/edit dialogs pause the camera without acquiring ChatContext.
edit.parent=nullptr;child.parent=&edit;workspace.focus=&child;
assert(c.IsDisabledByMenu());assert(!inputs.chat&&inputs.renewals==4);
workspace.focus=&ordinary;assert(!c.IsDisabledByMenu());workspace.focus=nullptr;
assert(!c.IsDisabledByMenu());SCR_ChatPanelManager::current=&chatManager;
// A residual native chat context still blocks the camera until it expires.
inputs.chat=true;assert(c.IsDisabledByMenu());assert(inputs.renewals==4);
inputs.Expire();assert(!c.IsDisabledByMenu());
// Integrate Capture with the production input edge/release state machine.
Follow f;registered.open=true;f.Input(!c.IsDisabledByMenu(),false,false,false);
registered.open=false;inputs.Expire();f.Input(!c.IsDisabledByMenu(),false,false,false);
f.Input(!c.IsDisabledByMenu(),false,false,true);f.Input(!c.IsDisabledByMenu(),false,false,true);
assert(!f.m_PlayerView); // submitting/holding Enter cannot become a camera toggle
f.Input(!c.IsDisabledByMenu(),false,false,false);f.Input(!c.IsDisabledByMenu(),false,false,true);assert(f.m_PlayerView);
registered.open=true;f.Input(!c.IsDisabledByMenu(),false,false,false);
registered.open=false;inputs.Expire();f.Input(!c.IsDisabledByMenu(),false,false,false);
f.Input(!c.IsDisabledByMenu(),true,false,false);assert(pc.calls==0);
f.Input(!c.IsDisabledByMenu(),false,false,false);f.Input(!c.IsDisabledByMenu(),true,false,false);assert(pc.calls==1);
}
'''
ui_follow=r'''
struct ERR_ObserverRules {
'''+body(p,'static int Cycle(')+body(p,'static bool Toggle(')+r'''
};
struct SCR_PlayerController {int calls=0,direction=0;void ERR_CycleObserver(int d){calls++;direction=d;}} pc;
struct Follow {bool m_LeftDown=false,m_RightDown=false,m_EnterDown=false,m_PlayerView=false;int m_InputResumeFrames=0;
'''+input_body+r'''
};
'''
run(ui_prelude+'struct Camera:Base {\n'+gate+'\n};\n'+ui_follow+ui_tests)
assert (ROOT/'addons/core/Prefabs/ERR/SpectatorCamera.et').read_text().startswith('ERR_ObserverCamera {')
config=(ROOT/'addons/core/Configs/ERR/ObserverInput.conf').read_text()
for key in ('KC_LEFT','KC_RIGHT','KC_RETURN','KC_NUMPADENTER'): assert f'keyboard:{key}' in config
assert 'chimeraInputCommon.conf' in config and 'ActionContext/ManualCameraContext.conf' in config
assert 'Debug.KeyState' not in p.read_text()
print('PASS: actual open-chat capture/context renewal and camera/menu priority, text-entry isolation, held-key release; actual revival camera handoff with editor close/retry, missing camera, replacement body and stopped round')