203 lines
14 KiB
Python
203 lines
14 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);
|
|
}
|
|
''')
|
|
assert 'm_InputResumeFrames = 2;' in body(p,'override bool EOnCameraInit(')
|
|
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,epi=0;void ERR_UseSelfEpinephrine(){epi++;}void ERR_CycleObserver(int d){calls++;direction=d;}} pc;
|
|
struct Follow {bool m_LeftDown=false,m_RightDown=false,m_EnterDown=false,m_PlayerView=false,m_SpaceDown=false;int m_InputResumeFrames=0;
|
|
'''+input_body+r'''
|
|
};
|
|
int main(){
|
|
Follow initial;initial.m_InputResumeFrames=2;
|
|
initial.Input(true,false,false,false,true);initial.Input(true,false,false,false,true);assert(pc.epi==0);
|
|
initial.Input(true,false,false,false,false);initial.Input(true,false,false,false,true);assert(pc.epi==1);pc.epi=0;
|
|
Follow epi;
|
|
epi.Input(false,false,false,false,true); epi.Input(true,false,false,false,true); epi.Input(true,false,false,false,true);assert(pc.epi==0);
|
|
epi.Input(true,false,false,false,false);epi.Input(true,false,false,false,true);assert(pc.epi==1);
|
|
epi.Input(true,false,false,false,true);assert(pc.epi==1);
|
|
epi.Input(true,false,false,false,false);epi.Input(true,false,false,false,true);assert(pc.epi==2);
|
|
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,®istered};
|
|
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,epi=0;void ERR_UseSelfEpinephrine(){epi++;}void ERR_CycleObserver(int d){calls++;direction=d;}} pc;
|
|
struct Follow {bool m_LeftDown=false,m_RightDown=false,m_EnterDown=false,m_PlayerView=false,m_SpaceDown=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')
|