Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
194 lines
13 KiB
Python
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,®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;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')
|