Integrate mission-owned chat administration and menu

This commit is contained in:
2026-09-19 23:49:54 +02:00
parent 27989b85a3
commit 5e742691ad
154 changed files with 19498 additions and 134 deletions
+6 -1
View File
@@ -390,5 +390,10 @@ class ERR_HQ : ERR_CommandService
modded class ERR_GameMode
{
protected override ERR_CommandService CreateCommandService() { return new ERR_HQ(this); }
protected override ERR_CommandService CreateCommandService()
{
ERR_HQ hq = new ERR_HQ(this);
Admin().Register(new ERR_HQAdminProvider(hq));
return hq;
}
}
@@ -0,0 +1,48 @@
// HQ administration belongs to AI; Core sees only ERR_AdminProvider.
class ERR_HQAdminProvider : ERR_AdminProvider
{
protected ERR_HQ m_HQ;
void ERR_HQAdminProvider(ERR_HQ hq) { m_HQ = hq; }
override string Id() { return "hq"; }
override void Capabilities(array<string> values) { values.Insert("supports=hq"); }
override string Setting(string section, string option)
{
if (section != "hq") return "";
if (option == "interval") return "reportIntervalSeconds";
if (option == "squads") return "supportSquadLimit";
if (option == "radius") return "responseRadiusMetres";
return "";
}
override int JournalSequence()
{
if (!m_HQ || !m_HQ.Journal()) return -1;
return m_HQ.Journal().Sequence();
}
override bool ReadJournal(int after, array<string> lines, out int cursor, int limit = 8)
{
if (!m_HQ || !m_HQ.Journal()) return false;
m_HQ.Journal().Read(after, lines, cursor, limit);
return true;
}
override bool Execute(ERR_GameMode mode, string arguments, array<string> replies)
{
if (arguments != "status" && arguments != "hq show" && arguments != "hq log") return false;
if (!m_HQ) { replies.Insert("HQ unavailable."); return true; }
if (arguments == "hq show" || arguments == "status") replies.Insert(m_HQ.Status());
else
{
int cursor;
ReadJournal(Math.Max(0, JournalSequence() - 8), replies, cursor);
if (replies.IsEmpty()) replies.Insert("HQ journal is empty.");
}
return true;
}
override bool Help(string topic, array<string> replies)
{
if (topic != "hq") return false;
replies.Insert("/esc hq show | hq log show|next|set on|off");
replies.Insert("/esc hq interval|squads|radius show|set N");
replies.Insert("HQ assigns available physical squads through delivered reports. These commands do not create replacement forces.");
return true;
}
}
@@ -1,51 +1,2 @@
// Companion-specific commands extend Core's authenticated transport.
modded class ERR_AdminService
{
protected override string Setting(string section, string option)
{
if (section == "hq")
{
if (option == "interval") return "reportIntervalSeconds";
if (option == "squads") return "supportSquadLimit";
if (option == "radius") return "responseRadiusMetres";
}
if (section == "intel")
{
if (option == "carrying") return "aiIntelChancePercent";
}
return super.Setting(section, option);
}
protected override bool Extension(ERR_GameMode mode, string arguments, array<string> replies)
{
if (arguments == "intel show")
{
replies.Insert("aiIntelChancePercent=" + mode.Settings().Get("aiIntelChancePercent").ToString());
replies.Insert("intelRevealChancePercent=" + mode.Settings().Get("intelRevealChancePercent").ToString());
return true;
}
if (arguments == "intel reroll" || arguments == "intel reroll show")
{
ERR_IntelService intel = mode.Intel();
if (!intel) { replies.Insert("Intel service unavailable."); return true; }
if (arguments == "intel reroll" && !intel.BeginReroll())
replies.Insert("Intel reroll could not start. It requires an active round and no running reroll.");
replies.Insert(intel.RerollStatus());
return true;
}
if (arguments != "status" && arguments != "hq show" && arguments != "hq log") return super.Extension(mode, arguments, replies);
ERR_HQ hq = ERR_HQ.Cast(mode.Command());
if (!hq) { replies.Insert("HQ unavailable"); return true; }
if (arguments == "hq show" || arguments == "status") replies.Insert(hq.Status());
else
{
int cursor;
hq.Journal().Read(Math.Max(0, hq.Journal().Sequence() - 8), replies, cursor);
if (replies.IsEmpty()) replies.Insert("HQ journal is empty.");
}
return true;
}
protected override string Help()
{
return super.Help() + " | hq show|log | hq interval|squads|radius show|set N | intel show | intel carrying show|set N | intel reroll [show]";
}
}
// Chat commands are mission services in Core. Optional AI and Intel command
// providers register from their owning addons, including without ERR_Chat loaded.
@@ -566,6 +566,9 @@ modded class ERR_GameMode
}
else if (m_ERRSmokeStep == 3)
{
int adminMenu = ERR_ProbeAdminMenu(now);
if (adminMenu < 0) { ERR_Check(false, "integrated admin menu"); return; }
if (adminMenu == 0) return;
if (!ERR_CheckIntelReroll(now)) return;
if (!ERR_Check(m_ERRSmokeRadio.Knowledge() == 2, "carried intel identifies radio through intel service")) return;
if (!ERR_Check(!TuneRadio(m_ERRSmokePlayer, m_ERRSmokeRadio), "radio rejects distant player")) return;
+9
View File
@@ -0,0 +1,9 @@
MenuManager {
MenuPresets {
MenuPreset ERR_AdminPanel {
Layout "{AE761C0439D25FB8}UI/layouts/ERR/AdminMenu.layout"
ActionContext "MenuContext"
Class "ERR_AdminMenu"
}
}
}
@@ -0,0 +1,11 @@
MetaFileClass {
Name "{B48C7FA12692DE03}Configs/ERR/AdminMenus.conf"
Configurations {
CONFResourceClass PC { }
CONFResourceClass HEADLESS : PC { }
CONFResourceClass XBOX_ONE : PC { }
CONFResourceClass XBOX_SERIES : PC { }
CONFResourceClass PS4 : PC { }
CONFResourceClass PS5 : PC { }
}
}
@@ -0,0 +1,337 @@
modded enum ChimeraMenuPreset
{
ERR_AdminPanel
}
// Presentation rules only: all mutations still use the authenticated command service.
class ERR_AdminMenuFields
{
static array<string> Keys()
{
return {"radioDefenceSeconds", "aiIntelChancePercent", "intelRevealChancePercent", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres"};
}
static bool Available(string key, bool hq, bool intel)
{
if (key == "aiIntelChancePercent") return intel;
if (key == "reportIntervalSeconds" || key == "supportSquadLimit" || key == "responseRadiusMetres") return hq;
return Keys().Contains(key);
}
// A short journal has no older off-screen lines, even if its slider is zero.
static bool FollowJournal(bool empty, float slider, float height, float viewHeight, bool latest)
{
return latest || empty || slider >= 0.98 || height <= viewHeight;
}
// TextWidget interprets hash-prefixed tokens as localization IDs. Keep the
// server audit and retained entries intact; render their numeric IDs literally.
static string JournalText(string line)
{
line.Replace("#", "No. ");
return line;
}
static string Display(string key, string value)
{
if (key == "startTimeMinutes") return ERR_TimeService.Clock(value.ToInt());
if (key == "startDateCode") return ERR_EnvironmentService.Date(value.ToInt());
if (key == "weatherProfile") return ERR_EnvironmentService.ProfileName(value.ToInt());
return value;
}
static string Command(string key, string value)
{
string prefix;
if (key == "radioDefenceSeconds") prefix = "mission radio";
else if (key == "aiIntelChancePercent") prefix = "intel carrying";
else if (key == "intelRevealChancePercent") prefix = "intel reveal";
else if (key == "startTimeMinutes") prefix = "mission time";
else if (key == "timeMultiplier") prefix = "mission speed";
else if (key == "startDateCode") prefix = "mission date";
else if (key == "weatherProfile") prefix = "mission weather";
else if (key == "reportIntervalSeconds") prefix = "hq interval";
else if (key == "supportSquadLimit") prefix = "hq squads";
else if (key == "responseRadiusMetres") prefix = "hq radius";
value = value.Trim();
if (prefix.IsEmpty() || value.IsEmpty() || value.Length() > 32) return "";
return prefix + " set " + value;
}
}
// Own mission dialog. ChimeraMenuBase owns focus, cursor and input-context lifetime.
class ERR_AdminMenu : ChimeraMenuBase
{
protected SCR_PlayerController m_Controller;
protected ERR_GameMode m_Mode;
protected ref array<SCR_ButtonBaseComponent> m_Buttons = {};
protected ref array<string> m_InitializedFields = {};
protected ref array<string> m_AvailableFields = {};
protected ref array<string> m_LogLines = {};
protected bool m_Bound;
protected bool m_Ready;
protected bool m_HQ;
protected bool m_Intel;
protected int m_Page;
protected float m_Age;
protected float m_Poll;
protected float m_ClickDelay;
protected float m_LogPoll;
protected bool m_LogPending;
protected bool m_LogPaused;
protected bool m_LogLatest;
protected bool m_LogReplacePending;
protected bool m_ScrollToEnd;
protected int m_LogCursor = -1;
static ERR_AdminMenu GetOpenMenu()
{
if (!GetGame() || !GetGame().GetMenuManager()) return null;
return ERR_AdminMenu.Cast(GetGame().GetMenuManager().FindMenuByPreset(ChimeraMenuPreset.ERR_AdminPanel));
}
static void Open()
{
if (!GetGame() || !GetGame().GetMenuManager() || !ERR_GameMode.Cast(GetGame().GetGameMode()) || GetOpenMenu()) return;
if (!GetGame().GetMenuManager().OpenDialog(ChimeraMenuPreset.ERR_AdminPanel, DialogPriority.INFORMATIVE, 0, true))
SCR_ChatComponent.RadioProtocolMessage("Mission administration could not open. Check that the Core addon is current.");
}
override void OnMenuOpen()
{
super.OnMenuOpen();
m_Controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
m_Mode = ERR_GameMode.Cast(GetGame().GetGameMode());
if (!m_Controller) { Close(); return; }
array<string> names = {"Close", "OpenHQ", "BackSettings", "OpenJournal", "BackHQ", "PauseJournal", "LatestJournal", "IntelSummary", "RerollIntel"};
foreach (string key : ERR_AdminMenuFields.Keys()) names.Insert("Apply_" + key);
foreach (string name : names)
{
Widget widget = GetRootWidget().FindAnyWidget(name);
SCR_ButtonBaseComponent button;
if (widget) button = SCR_ButtonBaseComponent.Cast(widget.FindHandler(SCR_ButtonBaseComponent));
if (!button)
{
SCR_ChatComponent.RadioProtocolMessage("Mission administration layout is incomplete. Update the Core addon.");
Close(); return;
}
button.m_OnClicked.Insert(OnButton);
m_Buttons.Insert(button);
}
m_Ready = false; m_Age = 0; m_Poll = 0;
GetGame().GetInputManager().AddActionListener(UIConstants.MENU_ACTION_BACK, EActionTrigger.PRESSED, OnBack);
m_Bound = true;
ShowPage(0);
UpdateActions();
RequestSnapshot();
}
override void OnMenuClose()
{
if (m_Bound && GetGame() && GetGame().GetInputManager())
GetGame().GetInputManager().RemoveActionListener(UIConstants.MENU_ACTION_BACK, EActionTrigger.PRESSED, OnBack);
m_Bound = false;
foreach (SCR_ButtonBaseComponent button : m_Buttons)
if (button) button.m_OnClicked.Remove(OnButton);
m_Buttons.Clear(); m_Controller = null; m_Mode = null; m_Ready = false;
super.OnMenuClose();
}
override void OnMenuUpdate(float tDelta)
{
super.OnMenuUpdate(tDelta);
if (!m_Controller || m_Controller != GetGame().GetPlayerController() || !m_Mode || m_Mode != GetGame().GetGameMode() || ERR_MissionRules.Terminal(m_Mode.Phase())) { Close(); return; }
m_Age += tDelta; m_Poll += tDelta;
m_ClickDelay = Math.Max(0, m_ClickDelay - tDelta);
if (m_Age > 6)
{
m_Ready = false;
SetText("Connection", "Waiting for the server. Changes are temporarily unavailable.");
UpdateActions();
}
if (IsFocused() && m_Poll >= 2) RequestSnapshot();
if (m_Page != 2) return;
m_LogPoll += tDelta;
if (m_LogPending && m_LogPoll > 5) m_LogPending = false;
if (IsFocused() && m_Ready && m_HQ && !m_LogPaused && !m_LogPending && m_LogPoll >= 1)
{
m_LogPending = true; m_LogPoll = 0;
m_LogReplacePending = m_LogLatest;
m_Controller.ERR_AdminRequestLog(m_LogCursor, m_LogLatest);
m_LogLatest = false;
}
if (m_ScrollToEnd)
{
ScrollLayoutWidget scroll = ScrollLayoutWidget.Cast(GetRootWidget().FindAnyWidget("JournalScroll"));
if (scroll)
{
// Resolve the new wrapped text extent before asking the native scrollbar
// for its end; an unresolved hidden-page layout still has zero overflow.
GetRootWidget().Update();
scroll.SetSliderPos(0, 1);
}
m_ScrollToEnd = false;
}
}
protected void RequestSnapshot()
{
m_Poll = 0;
if (m_Controller) m_Controller.ERR_AdminRequestSnapshot();
}
protected void OnBack()
{
if (!IsFocused()) return;
foreach (string key : ERR_AdminMenuFields.Keys())
{
SCR_EditBoxComponent edit = SCR_EditBoxComponent.GetEditBoxComponent(key, GetRootWidget());
if (edit && edit.IsInWriteMode()) return;
}
if (m_Page > 0) ShowPage(m_Page - 1);
else Close();
}
protected void SetText(string name, string value)
{
TextWidget widget = TextWidget.Cast(GetRootWidget().FindAnyWidget(name));
if (widget) widget.SetText(value);
}
protected void ShowPage(int page)
{
m_Page = page;
GetRootWidget().FindAnyWidget("SettingsPage").SetVisible(page == 0);
GetRootWidget().FindAnyWidget("HQPage").SetVisible(page == 1);
GetRootWidget().FindAnyWidget("JournalPage").SetVisible(page == 2);
if (page == 0) SetText("Title", "Mission settings");
if (page == 1) SetText("Title", "HQ and mission status");
if (page == 2) { SetText("Title", "HQ activity journal"); m_LogPoll = 1; }
}
protected void UpdateActions()
{
foreach (SCR_ButtonBaseComponent button : m_Buttons)
{
string name = button.GetRootWidget().GetName();
if (name.IndexOf("Apply_") == 0)
{
string key = name.Substring(6, name.Length() - 6);
button.SetEnabled(m_Ready && m_AvailableFields.Contains(key) && ERR_AdminMenuFields.Available(key, m_HQ, m_Intel));
}
else if (name == "RerollIntel" || name == "IntelSummary") button.SetEnabled(m_Ready && m_Intel);
else if (name == "OpenJournal" || name == "LatestJournal") button.SetEnabled(m_Ready && m_HQ);
}
}
void ReceiveSnapshot(bool authorized, array<string> values, array<string> status)
{
if (!authorized) { Close(); return; }
if (!values) return;
m_HQ = false; m_Intel = false; m_AvailableFields.Clear();
foreach (string entry : values)
{
if (entry == "supports=hq") { m_HQ = true; continue; }
if (entry == "supports=intel") { m_Intel = true; continue; }
int divider = entry.IndexOf("=");
if (divider <= 0) continue;
string key = entry.Substring(0, divider);
if (!ERR_AdminMenuFields.Keys().Contains(key)) continue;
string value = ERR_AdminMenuFields.Display(key, entry.Substring(divider + 1, entry.Length() - divider - 1));
m_AvailableFields.Insert(key);
SetText("Saved_" + key, "Saved: " + value);
// Existing fields retain all draft text, including a temporarily empty edit.
if (!m_InitializedFields.Contains(key))
{
SCR_EditBoxComponent edit = SCR_EditBoxComponent.GetEditBoxComponent(key, GetRootWidget());
if (edit) edit.SetValue(value);
m_InitializedFields.Insert(key);
}
}
string summary;
if (status)
foreach (string line : status)
{
if (!summary.IsEmpty()) summary += "\n";
summary += line;
}
if (summary.IsEmpty()) summary = "No mission status is available yet.";
SetText("Status", summary);
string connection = "Connected. Saved values update live; your drafts are kept until this menu closes.";
if (!m_HQ || !m_Intel) connection = "Connected. Controls for companion systems absent from this mission are disabled.";
SetText("Connection", connection);
m_Ready = true; m_Age = 0; UpdateActions();
}
void ReceiveReply(string text)
{
if (text.Length() > 600) text = text.Substring(0, 597) + "...";
SetText("Feedback", text);
m_Poll = 2;
}
void ReceiveLog(array<string> lines, int cursor)
{
m_LogPending = false;
if (m_LogPaused) return;
bool requestedLatest = m_LogReplacePending;
if (m_LogReplacePending || cursor < m_LogCursor) { m_LogLines.Clear(); m_LogCursor = -1; }
m_LogReplacePending = false;
if (cursor > m_LogCursor && lines)
{
ScrollLayoutWidget scroll = ScrollLayoutWidget.Cast(GetRootWidget().FindAnyWidget("JournalScroll"));
float x, y, width, height, viewWidth, viewHeight;
if (scroll) { scroll.GetSliderPos(x, y); scroll.GetScreenSize(viewWidth, viewHeight); }
GetRootWidget().FindAnyWidget("JournalContent").GetScreenSize(width, height);
bool follow = ERR_AdminMenuFields.FollowJournal(m_LogLines.IsEmpty(), y, height, viewHeight, requestedLatest);
foreach (string line : lines)
{
if (m_LogLines.Count() >= 300) m_LogLines.Remove(0);
m_LogLines.Insert(line);
}
m_LogCursor = cursor;
string content;
foreach (string entry : m_LogLines)
{
if (!content.IsEmpty()) content += "\n";
content += ERR_AdminMenuFields.JournalText(entry);
}
if (content.IsEmpty()) content = "Waiting for the first HQ event...";
SetText("JournalText", content);
m_ScrollToEnd = follow;
}
SetText("JournalState", "Live. Up to 300 received entries are retained here. Pause to read older entries.");
}
protected void OnButton(SCR_ButtonBaseComponent button)
{
if (!button || !IsFocused()) return;
string name = button.GetRootWidget().GetName();
if (name == "Close") { Close(); return; }
if (name == "OpenHQ" || name == "BackHQ") { ShowPage(1); return; }
if (name == "BackSettings") { ShowPage(0); return; }
if (name == "OpenJournal") { if (m_Ready && m_HQ) ShowPage(2); return; }
if (name == "PauseJournal")
{
m_LogPaused = !m_LogPaused;
SCR_ButtonTextComponent label = SCR_ButtonTextComponent.Cast(button);
if (m_LogPaused)
{
if (label) label.SetText("Resume live");
SetText("JournalState", "Paused. The server continues recording; resume to catch up.");
}
else
{
if (label) label.SetText("Pause live");
SetText("JournalState", "Live - catching up..."); m_LogPoll = 1;
}
return;
}
if (!m_Controller || !m_Ready || m_Age > 6 || m_ClickDelay > 0) return;
if (name == "LatestJournal")
{
if (!m_HQ || m_LogPending) return;
m_LogPaused = false; m_LogLatest = true; m_LogPoll = 1;
Widget pauseWidget = GetRootWidget().FindAnyWidget("PauseJournal");
SCR_ButtonTextComponent pause = SCR_ButtonTextComponent.Cast(pauseWidget.FindHandler(SCR_ButtonTextComponent));
if (pause) pause.SetText("Pause live");
return;
}
string command;
if (name == "IntelSummary" && m_Intel) command = "intel show";
if (name == "RerollIntel" && m_Intel) command = "intel reroll";
if (name.IndexOf("Apply_") == 0)
{
string key = name.Substring(6, name.Length() - 6);
if (!m_AvailableFields.Contains(key) || !ERR_AdminMenuFields.Available(key, m_HQ, m_Intel)) return;
SCR_EditBoxComponent edit = SCR_EditBoxComponent.GetEditBoxComponent(key, GetRootWidget());
if (edit) command = ERR_AdminMenuFields.Command(key, edit.GetValue());
}
if (command.IsEmpty()) { ReceiveReply("Enter a value before applying this setting."); return; }
m_ClickDelay = 0.3;
SetText("Feedback", "Request sent. Waiting for the server result...");
m_Controller.ERR_AdminSubmit(command, true);
}
}
@@ -0,0 +1,181 @@
// Current-body administrator utilities adapted from the owner's Chat commands.
class ERR_AdminItemCallback : ScriptedInventoryOperationCallback
{
bool Finished;
bool Succeeded;
protected void Finish(bool success) { if (!Finished) { Finished = true; Succeeded = success; } }
protected override void OnComplete() { Finish(true); }
protected override void OnFailed() { Finish(false); }
protected override void OnAllComplete() { Finish(true); }
protected override void OnAllFailed() { Finish(false); }
}
// Controller-owned grant state survives authorization-session replacement.
// Only a different physical body may discard unresolved native insertion state.
class ERR_AdminGrantState
{
IEntity Actor;
ref ERR_AdminItemCallback Operation;
string Item;
float Deadline;
bool TimedOut;
void Bind(IEntity actor)
{
if (Actor == actor) return;
Actor = actor;
Operation = null;
Item = "";
Deadline = 0;
TimedOut = false;
}
}
class ERR_AdminPlayerTools
{
protected ERR_GameMode m_Mode;
protected SCR_PlayerController m_Controller;
protected IEntity m_Actor;
protected DamageManagerComponent m_Damage;
protected bool m_PreviousDamage;
protected bool m_God;
protected bool m_Stopped;
protected ref ERR_AdminGrantState m_Grants = new ERR_AdminGrantState();
void ERR_AdminPlayerTools(ERR_AdminGrantState grants = null)
{
if (grants) m_Grants = grants;
}
protected IEntity CurrentBody(ERR_GameMode mode, SCR_PlayerController controller)
{
if (!Replication.IsServer() || !GetGame() || !mode || mode != ERR_GameMode.Get() || !mode.Active() || !controller || !mode.Players()) return null;
PlayerManager manager = GetGame().GetPlayerManager();
int id = controller.GetPlayerId();
if (!manager || id <= 0 || manager.GetPlayerController(id) != controller || !SCR_Global.IsAdmin(id)) return null;
IEntity actor = manager.GetPlayerControlledEntity(id);
if (!actor || controller.GetControlledEntity() != actor || !ERR_Actors.Alive(actor) || !mode.Players().CurrentActor(actor)) return null;
return actor;
}
protected ResourceName ItemPrefab(string item)
{
if (item == "map") return "{13772C903CB5E4F7}Prefabs/Items/Equipment/Maps/Map_Paper_01/PaperMap_01_folded.et";
if (item == "helmet") return "{8A2B2A544F3ED831}Prefabs/Characters/HeadGear/Helmet_PASGT_01/Helmet_PASGT_01_base.et";
if (item == "uniform") return "{3CCA7A9BB4FD3197}Prefabs/Characters/Uniforms/Jacket_US_BDU_rolledup.et";
return "";
}
protected void RestoreDamage()
{
if (Replication.IsServer() && m_God && m_Actor && m_Damage && m_Actor.FindComponent(DamageManagerComponent) == m_Damage)
m_Damage.EnableDamageHandling(m_PreviousDamage);
m_God = false;
m_Damage = null;
}
bool Execute(ERR_GameMode mode, SCR_PlayerController controller, string args, array<string> replies)
{
args = args.Trim(); args.ToLower();
bool godCommand = args == "help god" || args == "god" || args == "ungod" || args.StartsWith("god ");
bool itemCommand = args == "help cheat" || args == "cheat" || args.StartsWith("cheat ");
if (!godCommand && !itemCommand) return false;
IEntity actor = CurrentBody(mode, controller);
if (!actor)
{
Stop();
replies.Insert("Administrator access and your current living prisoner are required.");
return true;
}
if (m_Stopped || (m_Actor && (m_Actor != actor || m_Controller != controller || m_Mode != mode)))
{
Stop();
replies.Insert("This player-tool session has ended; use a new session for your current character.");
return true;
}
m_Mode = mode; m_Controller = controller; m_Actor = actor;
m_Grants.Bind(actor);
if (godCommand)
{
if (args == "god show")
{
string status = "off";
if (m_God) status = "on";
replies.Insert("God mode: " + status + " for your current character.");
return true;
}
if (args == "ungod" || args == "god set off")
{
RestoreDamage();
replies.Insert("God mode off; the previous damage-handling state is restored.");
return true;
}
if (args != "god" && args != "god set on")
{
replies.Insert("Use /esc god show | /esc god set on|off. Aliases: /esc god | /esc ungod.");
return true;
}
if (!m_God)
{
m_Damage = DamageManagerComponent.Cast(actor.FindComponent(DamageManagerComponent));
if (!m_Damage) { replies.Insert("Your character damage manager is not ready."); return true; }
m_PreviousDamage = m_Damage.IsDamageHandlingEnabled();
m_Damage.EnableDamageHandling(false);
m_God = true;
}
replies.Insert("God mode on for your current character.");
return true;
}
string item;
if (args.Length() > 6) item = args.Substring(6, args.Length() - 6);
ResourceName prefab = ItemPrefab(item);
if (prefab.IsEmpty()) { replies.Insert("Use /esc cheat map | /esc cheat helmet | /esc cheat uniform (vanilla paper map, PASGT helmet or BDU jacket)."); return true; }
if (m_Grants.TimedOut) { replies.Insert("An inventory insertion is unresolved; further item grants are blocked for this character."); return true; }
if (m_Grants.Operation) { replies.Insert("An item grant is already pending; wait for its result."); return true; }
SCR_InventoryStorageManagerComponent inventory = SCR_InventoryStorageManagerComponent.Cast(actor.FindComponent(SCR_InventoryStorageManagerComponent));
if (!inventory) { replies.Insert("Your character inventory is not ready."); return true; }
m_Grants.Operation = new ERR_AdminItemCallback();
m_Grants.Item = "paper map";
if (item == "helmet") m_Grants.Item = "PASGT helmet";
if (item == "uniform") m_Grants.Item = "BDU jacket";
m_Grants.Deadline = GetGame().GetWorld().GetWorldTime() + 30000;
// Deposit into storage: do not replace worn gear or use a hidden attachment slot.
if (!inventory.TrySpawnPrefabToStorage(prefab, purpose: EStoragePurpose.PURPOSE_DEPOSIT, cb: m_Grants.Operation))
{
m_Grants.Operation = null;
replies.Insert("Item grant was not accepted; check inventory space.");
return true;
}
replies.Insert("Adding vanilla " + m_Grants.Item + " to your inventory.");
return true;
}
void Tick(array<string> replies)
{
if (m_Stopped || !m_Actor) return;
if (CurrentBody(m_Mode, m_Controller) != m_Actor) { Stop(); return; }
if (!m_Grants.Operation || m_Grants.TimedOut) return;
if (m_Grants.Operation.Finished)
{
if (m_Grants.Operation.Succeeded) replies.Insert("Vanilla " + m_Grants.Item + " was added to your inventory.");
else replies.Insert("Vanilla " + m_Grants.Item + " could not be added to your inventory.");
m_Grants.Operation = null;
return;
}
if (GetGame().GetWorld().GetWorldTime() >= m_Grants.Deadline)
{
m_Grants.TimedOut = true;
replies.Insert("Item grant has not confirmed completion; further grants are blocked for this character to avoid duplicates.");
}
}
void Stop()
{
RestoreDamage();
m_Stopped = true;
// Keep controller-owned insertion state across revoke/regrant. Native callbacks
// only record completion; stopped tools can never message or grant another item.
m_Actor = null;
m_Controller = null;
m_Mode = null;
}
}
@@ -0,0 +1,12 @@
// Optional systems register their own commands with the mission. Core never
// imports companion classes; transport and authorization stay in ERR_AdminService.
class ERR_AdminProvider
{
string Id() { return ""; }
string Setting(string section, string option) { return ""; }
bool Execute(ERR_GameMode mode, string arguments, array<string> replies) { return false; }
bool Help(string topic, array<string> replies) { return false; }
void Capabilities(array<string> values) {}
int JournalSequence() { return -1; }
bool ReadJournal(int after, array<string> lines, out int cursor, int limit = 8) { cursor = after; return false; }
}
+110 -5
View File
@@ -19,11 +19,58 @@ class ERR_CommandArguments
}
class ERR_AdminService
{
protected ref array<ref ERR_AdminProvider> m_Providers = {};
void Register(ERR_AdminProvider provider)
{
if (!provider || provider.Id().IsEmpty()) return;
for (int i = 0; i < m_Providers.Count(); i++)
{
if (m_Providers[i].Id() != provider.Id()) continue;
m_Providers[i] = provider;
return;
}
m_Providers.Insert(provider);
}
void Capabilities(array<string> values)
{
foreach (ERR_AdminProvider provider : m_Providers) provider.Capabilities(values);
}
int JournalSequence()
{
foreach (ERR_AdminProvider provider : m_Providers)
if (provider.JournalSequence() >= 0) return provider.JournalSequence();
return -1;
}
bool ReadJournal(int after, array<string> lines, out int cursor, int limit = 8)
{
lines.Clear();
cursor = after;
foreach (ERR_AdminProvider provider : m_Providers)
if (provider.ReadJournal(after, lines, cursor, limit)) return true;
return false;
}
protected string Setting(string section, string option)
{
return ERR_CommandArguments.CoreSetting(section, option);
string key = ERR_CommandArguments.CoreSetting(section, option);
if (!key.IsEmpty()) return key;
foreach (ERR_AdminProvider provider : m_Providers)
{
key = provider.Setting(section, option);
if (!key.IsEmpty()) return key;
}
return "";
}
protected bool Extension(ERR_GameMode mode, string arguments, array<string> replies)
{
bool handled;
foreach (ERR_AdminProvider provider : m_Providers)
{
if (!provider.Execute(mode, arguments, replies)) continue;
handled = true;
if (arguments != "status") return true;
}
return handled;
}
protected bool Extension(ERR_GameMode mode, string arguments, array<string> replies) { return false; }
protected bool TimeCommand(ERR_GameMode mode, array<string> parts, array<string> replies)
{
if (parts.Count() < 2 || parts[0] != "mission" || (parts[1] != "time" && parts[1] != "speed")) return false;
@@ -68,9 +115,48 @@ class ERR_AdminService
replies.Insert(failure);
}
}
protected string Help()
protected void Help(string topic, array<string> replies)
{
return "/esc status | settings | mission abandon | mission radio show|set N | mission date show|set YYYY-MM-DD|authored | mission weather show|set authored|clear|overcast|rain | mission time show|set HH:MM | mission speed show|set N | intel reveal show|set N|help | spectate next|previous";
foreach (ERR_AdminProvider provider : m_Providers)
if (provider.Help(topic, replies)) return;
if (topic == "time" || topic == "mission")
{
replies.Insert("/esc mission radio show|set N | mission abandon");
replies.Insert("/esc time show|set HH:MM | mission speed show|set N; saved changes apply next round.");
replies.Insert("/esc mission date show|set YYYY-MM-DD|authored | mission weather show|set authored|clear|overcast|rain");
return;
}
if (topic == "intel")
{
replies.Insert("/esc intel reveal show|set N|help (0-100); affects newly recovered documents immediately.");
replies.Insert("AI carrying and reroll commands require the Intel addon.");
return;
}
if (!topic.IsEmpty() && topic != "help")
{
replies.Insert("This command topic is unavailable in the loaded mission: " + topic + ".");
return;
}
replies.Insert("/esc menu | status | settings | help mission|time|intel|hq|cheat|god");
replies.Insert("/esc mission abandon | mission radio show|set N | time show|set HH:MM");
replies.Insert("/esc mission date|weather|speed show|set VALUE | intel reveal show|set N|help");
replies.Insert("/esc spectate next|previous; optional systems add their own command topics.");
}
protected bool Unavailable(array<string> parts, array<string> replies)
{
if (parts.IsEmpty()) return false;
string topic = parts[0];
if (topic == "convoys" || topic == "heli" || topic == "helitimer" || topic == "helitest" || topic == "ai")
{
replies.Insert("Capability unavailable: successor " + topic + " commands have not been implemented; no setting or gameplay was changed.");
return true;
}
if (topic == "hq" || (topic == "intel" && parts.Count() > 1 && (parts[1] == "carrying" || parts[1] == "chance" || parts[1] == "reroll" || parts[1] == "show")))
{
replies.Insert("Command unavailable or invalid for this mission. Use /esc help " + topic + ".");
return true;
}
return false;
}
void Execute(ERR_GameMode mode, bool authorized, string arguments, array<string> replies)
{
@@ -79,6 +165,7 @@ class ERR_AdminService
if (!authorized) { replies.Insert("Administrator access required."); return; }
if (arguments.Length() > 160) { replies.Insert("Command is too long."); return; }
arguments = arguments.Trim(); arguments.ToLower();
if (arguments == "time" || arguments.IndexOf("time ") == 0) arguments = "mission " + arguments;
if (arguments == "status") { replies.Insert(mode.Status()); Extension(mode, arguments, replies); return; }
if (arguments == "mission abandon")
{
@@ -95,6 +182,23 @@ class ERR_AdminService
if (Extension(mode, arguments, replies)) return;
array<string> parts = {};
arguments.Split(" ", parts, true);
if (parts.IsEmpty() || parts[0] == "help")
{
string topic;
if (parts.Count() > 1) topic = parts[1];
Help(topic, replies);
return;
}
if (parts.Count() == 1 || (parts.Count() == 2 && parts[1] == "help"))
{
Help(parts[0], replies);
return;
}
if (parts.Count() == 2 && parts[0] == "mission" && parts[1] == "time")
{
Help("time", replies);
return;
}
if (ERR_EnvironmentCommands.Execute(mode.Settings(), mode.Environment(), parts, replies)) return;
if (TimeCommand(mode, parts, replies)) return;
if (parts.Count() == 3 || parts.Count() == 4)
@@ -106,6 +210,7 @@ class ERR_AdminService
return;
}
}
replies.Insert(Help());
if (Unavailable(parts, replies)) return;
Help("", replies);
}
}
@@ -0,0 +1,142 @@
// Server-owned state for one authenticated controller and one mission instance.
// No command business logic or per-player state is kept in the RPC bridge.
class ERR_AdminSession
{
protected ERR_GameMode m_Mode;
protected SCR_PlayerController m_Controller;
protected IEntity m_Body;
protected int m_Token;
protected bool m_Stopped;
protected bool m_Live;
protected int m_Cursor;
protected float m_LastRequest = -1000;
protected float m_LastSnapshot = -1000;
protected float m_LastLog = -1000;
protected float m_NextFeed;
protected ref ERR_AdminGrantState m_Grants;
protected ref ERR_AdminPlayerTools m_Tools;
void ERR_AdminSession(ERR_GameMode mode, SCR_PlayerController controller, int token, ERR_AdminGrantState grants)
{
m_Mode = mode; m_Controller = controller; m_Token = token;
m_Grants = grants;
m_Tools = new ERR_AdminPlayerTools(m_Grants);
m_Body = controller.GetControlledEntity();
mode.OnStop().Insert(Stop);
}
int Token() { return m_Token; }
bool Valid()
{
if (m_Stopped || !Replication.IsServer() || !GetGame() || !m_Controller || !m_Mode || m_Mode != ERR_GameMode.Get()) return false;
PlayerManager players = GetGame().GetPlayerManager();
return players && players.GetPlayerController(m_Controller.GetPlayerId()) == m_Controller && SCR_Global.IsAdmin(m_Controller.GetPlayerId());
}
protected bool Authorize()
{
if (m_Stopped) return false;
if (Valid()) return true;
if (m_Controller)
{
array<string> empty = {};
m_Controller.ERR_AdminDeliverSnapshot(m_Token, false, empty, empty);
}
Stop();
return false;
}
protected void CheckBody()
{
IEntity body = m_Controller.GetControlledEntity();
if (body == m_Body) return;
m_Tools.Stop();
m_Tools = new ERR_AdminPlayerTools(m_Grants);
m_Body = body;
}
void Command(string arguments, bool menu)
{
if (!Authorize()) return;
float now = GetGame().GetWorld().GetWorldTime();
if (arguments.Length() > 160 || now - m_LastRequest < 250) return;
m_LastRequest = now;
CheckBody();
arguments = arguments.Trim(); arguments.ToLower();
if (arguments == "menu")
{
m_Controller.ERR_AdminDeliverOpen(m_Token);
Snapshot(true);
return;
}
array<string> replies = {};
if (!JournalCommand(arguments, replies) && !m_Tools.Execute(m_Mode, m_Controller, arguments, replies))
m_Mode.Admin().Execute(m_Mode, true, arguments, replies);
foreach (string reply : replies) m_Controller.ERR_AdminDeliverReply(m_Token, reply, menu);
if (menu) Snapshot(true);
}
protected bool JournalCommand(string arguments, array<string> replies)
{
if (arguments != "hq log" && arguments != "hq log show" && arguments != "hq log next" && arguments != "hq log set on" && arguments != "hq log set off") return false;
if (arguments == "hq log set off") { m_Live = false; replies.Insert("HQ live log stopped."); return true; }
if (m_Mode.Admin().JournalSequence() < 0) { m_Live = false; replies.Insert("HQ journal is unavailable in this mission."); return true; }
if (arguments == "hq log" || arguments == "hq log show" || arguments == "hq log set on")
m_Cursor = Math.Max(0, m_Mode.Admin().JournalSequence() - 8);
m_Mode.Admin().ReadJournal(m_Cursor, replies, m_Cursor, 8);
if (arguments == "hq log set on") { m_Live = true; m_NextFeed = GetGame().GetWorld().GetWorldTime() + 2000; }
if (replies.IsEmpty()) replies.Insert("HQ journal has no new entries.");
return true;
}
void Snapshot(bool force = false)
{
if (!Authorize()) return;
float now = GetGame().GetWorld().GetWorldTime();
if (!force && now - m_LastSnapshot < 500) return;
m_LastSnapshot = now;
array<string> values = {};
array<string> status = {};
if (!m_Mode.Settings()) { status.Insert("Mission settings are not ready."); m_Controller.ERR_AdminDeliverSnapshot(m_Token, true, values, status); return; }
foreach (string key : ERR_Settings.Keys()) values.Insert(key + "=" + m_Mode.Settings().Get(key).ToString());
m_Mode.Admin().Capabilities(values);
m_Mode.Admin().Execute(m_Mode, true, "status", status);
m_Controller.ERR_AdminDeliverSnapshot(m_Token, true, values, status);
}
void Log(int cursor, bool latest)
{
if (!Authorize()) return;
float now = GetGame().GetWorld().GetWorldTime();
if (now - m_LastLog < 500) return;
m_LastLog = now;
if (m_Mode.Admin().JournalSequence() < 0)
{
array<string> unavailable = {"HQ journal is unavailable in this mission."};
m_Controller.ERR_AdminDeliverLog(m_Token, unavailable, 0);
return;
}
int after = Math.Clamp(cursor, 0, m_Mode.Admin().JournalSequence());
if (latest || cursor < 0) after = Math.Max(0, m_Mode.Admin().JournalSequence() - 40);
array<string> lines = {};
int next;
m_Mode.Admin().ReadJournal(after, lines, next, 40);
m_Controller.ERR_AdminDeliverLog(m_Token, lines, next);
}
bool Tick()
{
if (!Authorize()) return false;
CheckBody();
array<string> replies = {};
m_Tools.Tick(replies);
foreach (string reply : replies) m_Controller.ERR_AdminDeliverReply(m_Token, reply, false);
float now = GetGame().GetWorld().GetWorldTime();
if (m_Live && now >= m_NextFeed)
{
m_NextFeed = now + 2000;
m_Mode.Admin().ReadJournal(m_Cursor, replies, m_Cursor, 8);
foreach (string line : replies) m_Controller.ERR_AdminDeliverReply(m_Token, line, false);
}
return true;
}
void Stop()
{
if (m_Stopped) return;
m_Stopped = true; m_Live = false;
m_Tools.Stop();
if (m_Mode) m_Mode.OnStop().Remove(Stop);
}
}
@@ -0,0 +1,54 @@
// Local chat registration and mission scope. RPC transport stays on the controller.
class ERR_ChatClient
{
protected SCR_PlayerController m_Controller;
protected ERR_GameMode m_Mode;
protected ChatCommandInvoker m_Command;
protected int m_Attempts;
protected int m_Token = 1;
protected bool m_Stopped;
void ERR_ChatClient(SCR_PlayerController controller)
{
m_Controller = controller;
m_Mode = ERR_GameMode.Get();
Register();
GetGame().GetCallqueue().CallLater(Poll, 1000, true);
}
protected void Register()
{
if (m_Stopped || m_Command || m_Attempts++ >= 40 || !GetGame() || m_Controller != GetGame().GetPlayerController()) return;
SCR_ChatPanelManager manager = SCR_ChatPanelManager.GetInstance();
if (manager) m_Command = manager.GetCommandInvoker("esc");
if (m_Command) m_Command.Insert(OnCommand);
else GetGame().GetCallqueue().CallLater(Register, 250, false);
}
protected void Poll()
{
if (!GetGame() || m_Controller != GetGame().GetPlayerController()) { Stop(); return; }
Scope();
}
int Scope()
{
ERR_GameMode mode = ERR_GameMode.Get();
if (mode != m_Mode)
{
m_Mode = mode;
m_Token++;
ERR_AdminMenu menu = ERR_AdminMenu.GetOpenMenu();
if (menu) menu.Close();
}
return m_Token;
}
bool Accept(int token) { return !m_Stopped && GetGame() && m_Controller == GetGame().GetPlayerController() && ERR_GameMode.Get() && token == Scope(); }
protected void OnCommand(SCR_ChatPanel panel, string arguments) { m_Controller.ERR_AdminSubmit(arguments); }
void Stop()
{
if (m_Stopped) return;
m_Stopped = true;
if (GetGame()) { GetGame().GetCallqueue().Remove(Register); GetGame().GetCallqueue().Remove(Poll); }
if (m_Command) m_Command.Remove(OnCommand);
m_Command = null;
ERR_AdminMenu menu = ERR_AdminMenu.GetOpenMenu();
if (menu) menu.Close();
}
}
@@ -0,0 +1,158 @@
#ifdef WORKBENCH
// Explicit local fixture: real dialog, widgets and owner RPCs; no mouse/remote claim.
modded class ERR_AdminMenu
{
bool ERR_ProbeReady() { return m_Ready && m_AvailableFields.Count() == 10; }
void ERR_ProbePage(int page) { ShowPage(page); }
string ERR_ProbeDraft() { return SCR_EditBoxComponent.GetEditBoxComponent("radioDefenceSeconds", GetRootWidget()).GetValue(); }
void ERR_ProbeEdit(string text) { SCR_EditBoxComponent.GetEditBoxComponent("radioDefenceSeconds", GetRootWidget()).SetValue(text); }
void ERR_ProbeApply()
{
Widget widget = GetRootWidget().FindAnyWidget("Apply_radioDefenceSeconds");
OnButton(SCR_ButtonBaseComponent.Cast(widget.FindHandler(SCR_ButtonBaseComponent)));
}
bool ERR_ProbeHQ() { return m_HQ; }
bool ERR_ProbeIntel() { return m_Intel; }
int ERR_ProbeLogCursor() { return m_LogCursor; }
bool ERR_ProbeJournalVisible()
{
TextWidget text = TextWidget.Cast(GetRootWidget().FindAnyWidget("JournalText"));
ScrollLayoutWidget scroll = ScrollLayoutWidget.Cast(GetRootWidget().FindAnyWidget("JournalScroll"));
if (!text || !scroll || m_LogLines.IsEmpty()) return false;
float width, height, viewWidth, viewHeight, x, y, viewX, viewY;
text.GetScreenSize(width, height); scroll.GetScreenSize(viewWidth, viewHeight);
text.GetScreenPos(x, y); scroll.GetScreenPos(viewX, viewY);
Print(string.Format("ERR ADMIN JOURNAL TEXT: lines=%1 chars=%2 bounds=%3,%4,%5,%6", m_LogLines.Count(), text.GetText().Length(), x, y, width, height));
Print(string.Format("ERR ADMIN JOURNAL VIEWPORT: bounds=%1,%2,%3,%4", viewX, viewY, viewWidth, viewHeight));
float sliderX, sliderY;
scroll.GetSliderPos(sliderX, sliderY);
Print(string.Format("ERR ADMIN JOURNAL FOLLOW: slider=%1,%2 overflow=%3", sliderX, sliderY, height - viewHeight));
if (height > viewHeight && sliderY < 0.98) return false;
return m_Page == 2 && text.GetText().Contains("No. ") && width > viewWidth * 0.5 && height > 2 && x < viewX + viewWidth && x + width > viewX && y < viewY + viewHeight && y + height > viewY;
}
}
class ERR_NativeAdminMenuProbe
{
protected ERR_GameMode m_Mode;
protected SCR_PlayerController m_Controller;
protected DamageManagerComponent m_Damage;
protected bool m_PreviousDamage;
protected int m_Stage;
protected float m_Deadline;
protected float m_Wait;
protected int m_RadioSeconds;
protected bool m_Failed;
void ERR_NativeAdminMenuProbe(ERR_GameMode mode) { m_Mode = mode; }
protected int Fail(string reason)
{
Print("ERR SMOKE FAIL: admin integration " + reason, LogLevel.ERROR);
m_Failed = true;
ERR_AdminMenu menu = ERR_AdminMenu.GetOpenMenu();
if (menu) menu.Close();
return -1;
}
protected void Pass(string reason) { Print("ERR SMOKE PASS: admin integration " + reason); }
int Tick(float now)
{
if (m_Failed) return -1;
if (m_Stage == 10) return 1;
if (m_Stage > 0 && now > m_Deadline) return Fail("timed out at stage " + m_Stage.ToString());
if (now < m_Wait) return 0;
ERR_AdminMenu menu = ERR_AdminMenu.GetOpenMenu();
if (m_Stage == 0)
{
m_Controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
if (!m_Controller || !SCR_Global.IsAdmin(m_Controller.GetPlayerId())) return Fail("local controller is not an authenticated administrator");
m_Damage = DamageManagerComponent.Cast(m_Controller.GetControlledEntity().FindComponent(DamageManagerComponent));
m_PreviousDamage = m_Damage.IsDamageHandlingEnabled();
m_RadioSeconds = m_Mode.Settings().Get("radioDefenceSeconds");
m_Deadline = now + 45000;
m_Controller.ERR_AdminSubmit("menu");
m_Stage = 1; return 0;
}
if (m_Stage < 6 && !menu) return Fail("dialog unexpectedly closed");
if (m_Stage == 1)
{
if (!menu.ERR_ProbeReady()) return 0;
array<string> capabilities = {}; m_Mode.Admin().Capabilities(capabilities);
if (menu.ERR_ProbeHQ() != capabilities.Contains("supports=hq") || menu.ERR_ProbeIntel() != capabilities.Contains("supports=intel")) return Fail("capability snapshot mismatch");
Pass("real owner RPC opens menu and returns current settings/capabilities");
menu.ERR_ProbeEdit("47");
m_Controller.ERR_AdminRequestSnapshot();
m_Wait = now + 3000; m_Stage = 2; return 0;
}
if (m_Stage == 2)
{
if (menu.ERR_ProbeDraft() != "47") return Fail("background snapshot overwrote draft");
Pass("background polling preserves editable draft");
menu.ERR_ProbeApply();
m_Wait = now + 1500; m_Stage = 3; return 0;
}
if (m_Stage == 3)
{
if (m_Mode.Settings().Get("radioDefenceSeconds") != 47) return Fail("Apply did not persist through authenticated transport");
Pass("native Apply control routes and saves only requested setting");
System.MakeScreenshot("$logs:err-admin-settings");
m_Controller.ERR_AdminSubmit("mission radio set " + m_RadioSeconds.ToString(), true);
menu.ERR_ProbePage(1);
m_Wait = now + 2000; m_Stage = 4; return 0;
}
if (m_Stage == 4)
{
if (m_Mode.Settings().Get("radioDefenceSeconds") != m_RadioSeconds) return Fail("test setting restoration failed");
Pass("test setting restored without resetting menu drafts");
System.MakeScreenshot("$logs:err-admin-hq");
if (menu.ERR_ProbeHQ()) menu.ERR_ProbePage(2);
m_Wait = now + 3000; m_Stage = 5; return 0;
}
if (m_Stage == 5)
{
if (menu.ERR_ProbeHQ() && menu.ERR_ProbeLogCursor() < 0) return Fail("journal owner RPC did not return");
if (menu.ERR_ProbeHQ() && !menu.ERR_ProbeJournalVisible()) return Fail("received journal text is empty, collapsed or outside its viewport");
System.MakeScreenshot("$logs:err-admin-journal");
Pass("HQ capability and journal page remain within current mission services");
menu.Close();
m_Controller.ERR_AdminSubmit("god set on");
m_Wait = now + 1500; m_Stage = 6; return 0;
}
if (m_Stage == 6)
{
if (ERR_AdminMenu.GetOpenMenu() || m_Damage.IsDamageHandlingEnabled()) return Fail("dialog close or god command failed");
Pass("dialog closes and authenticated god command affects current body");
m_Controller.ERR_AdminSubmit("god set off");
m_Wait = now + 1500; m_Stage = 7; return 0;
}
if (m_Stage == 7)
{
if (m_Damage.IsDamageHandlingEnabled() != m_PreviousDamage) return Fail("god command did not restore prior damage state");
Pass("god off restores exact prior damage state");
m_Controller.ERR_AdminSubmit("menu");
m_Wait = now + 1500; m_Stage = 8; return 0;
}
if (m_Stage == 8)
{
if (!menu || !menu.ERR_ProbeReady()) return Fail("second menu open did not become ready");
array<string> empty = {};
menu.ReceiveSnapshot(false, empty, empty);
m_Stage = 9; m_Wait = now + 500; return 0;
}
if (m_Stage == 9)
{
if (ERR_AdminMenu.GetOpenMenu()) return Fail("denied authorization snapshot did not close menu");
Pass("repeated open works and denied snapshot closes dialog");
m_Stage = 10; return 1;
}
return 0;
}
}
modded class ERR_GameMode
{
protected ref ERR_NativeAdminMenuProbe m_ERRAdminMenuProbe;
int ERR_ProbeAdminMenu(float now)
{
if (!m_ERRAdminMenuProbe) m_ERRAdminMenuProbe = new ERR_NativeAdminMenuProbe(this);
return m_ERRAdminMenuProbe.Tick(now);
}
}
#endif
@@ -659,6 +659,12 @@ modded class ERR_GameMode
m_ERRBaseStep = 901;
return;
}
if (m_ERRExtractionCase == "victory")
{
int adminMenu = ERR_ProbeAdminMenu(now);
if (adminMenu < 0) { ERR_BaseCheck(false, "Core admin menu integration"); return; }
if (adminMenu == 0) return;
}
if (!ERR_NativeAdminSettingsProbe.Run(this) || !ERR_NativeDefinitionProbe.Run() || !ERR_NativeContractsProbe.Run(this, m_ERRBasePlayer) || !ERR_BaseCover() || !ERR_NativeTimeProbe.Run(this)) return;
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
if (!ERR_BaseCheck(Objectives() && Objectives().Current() && Objectives().Current().GetTaskName() == "Escape the prison", "native prison objective exists")) return;
@@ -1,61 +1,137 @@
// Enfusion requires player-owned RPCs on the controller. Everything else is
// delegated to mission-owned chat, session, command-provider and menu classes.
modded class SCR_PlayerController
{
protected ChatCommandInvoker m_ERRCommand;
protected float m_ERRLastRequest = -1000;
protected int m_ERRRegisterAttempts;
override void OnControlledEntityChanged(IEntity from, IEntity to)
{
super.OnControlledEntityChanged(from, to);
if (this == GetGame().GetPlayerController() && ERR_GameMode.Get()) ERR_Register();
}
protected void ERR_Register()
{
if (m_ERRCommand || m_ERRRegisterAttempts++ > 40) return;
SCR_ChatPanelManager manager = SCR_ChatPanelManager.GetInstance();
if (!manager) { GetGame().GetCallqueue().CallLater(ERR_Register, 250, false); return; }
m_ERRCommand = manager.GetCommandInvoker("esc");
if (m_ERRCommand) m_ERRCommand.Insert(ERR_OnCommand);
}
protected override void ERR_OnObserverMonitorStarted()
{
super.ERR_OnObserverMonitorStarted();
ERR_Register();
}
protected void ERR_OnCommand(SCR_ChatPanel panel, string arguments)
{
string localCommand = arguments.Trim(); localCommand.ToLower();
if (localCommand == "spectate next" || localCommand == "spectate previous")
{
int direction = 1;
if (localCommand == "spectate previous") direction = -1;
ERR_CycleObserver(direction);
return;
}
Rpc(ERR_Request, arguments);
}
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
protected void ERR_Request(string arguments)
{
ERR_GameMode mode = ERR_GameMode.Get();
if (!Replication.IsServer() || !mode) return;
float now = GetGame().GetWorld().GetWorldTime();
if (arguments.Length() > 160 || now - m_ERRLastRequest < 250) return;
m_ERRLastRequest = now;
arguments = arguments.Trim(); arguments.ToLower();
array<string> replies = {};
mode.Admin().Execute(mode, SCR_Global.IsAdmin(GetPlayerId()), arguments, replies);
foreach (string reply : replies) ERR_Reply(reply);
}
protected void ERR_Reply(string text) { Rpc(ERR_Response, text); }
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
protected void ERR_Response(string text) { SCR_ChatComponent.RadioProtocolMessage(text); }
void ~SCR_PlayerController()
{
if (GetGame()) GetGame().GetCallqueue().Remove(ERR_Register);
if (m_ERRCommand) m_ERRCommand.Remove(ERR_OnCommand);
}
protected ref ERR_ChatClient m_ERRChatClient;
protected ref ERR_AdminSession m_ERRAdminSession;
protected ref ERR_AdminGrantState m_ERRAdminGrants = new ERR_AdminGrantState();
protected bool m_ERRAdminTicking;
protected ERR_GameMode m_ERRServerChatMode;
protected int m_ERRServerChatToken;
override void OnControlledEntityChanged(IEntity from, IEntity to)
{
super.OnControlledEntityChanged(from, to);
ERR_StartChatClient();
}
protected override void ERR_OnObserverMonitorStarted()
{
super.ERR_OnObserverMonitorStarted();
ERR_StartChatClient();
}
protected void ERR_StartChatClient()
{
if (!m_ERRChatClient && GetGame() && this == GetGame().GetPlayerController() && ERR_GameMode.Get()) m_ERRChatClient = new ERR_ChatClient(this);
}
void ERR_AdminSubmit(string arguments, bool menu = false)
{
ERR_StartChatClient();
if (!m_ERRChatClient || this != GetGame().GetPlayerController()) return;
string localCommand = arguments.Trim(); localCommand.ToLower();
if (localCommand == "spectate next" || localCommand == "spectate previous")
{
int direction = 1;
if (localCommand == "spectate previous") direction = -1;
ERR_CycleObserver(direction);
return;
}
Rpc(ERR_AdminRequest, m_ERRChatClient.Scope(), arguments, menu);
}
void ERR_AdminRequestSnapshot()
{
ERR_StartChatClient();
if (m_ERRChatClient && this == GetGame().GetPlayerController()) Rpc(ERR_AdminSnapshotRequest, m_ERRChatClient.Scope());
}
void ERR_AdminRequestLog(int cursor, bool latest = false)
{
ERR_StartChatClient();
if (m_ERRChatClient && this == GetGame().GetPlayerController()) Rpc(ERR_AdminLogRequest, m_ERRChatClient.Scope(), cursor, latest);
}
protected ERR_AdminSession ERR_ServerSession(int token)
{
ERR_GameMode mode = ERR_GameMode.Get();
if (!Replication.IsServer() || !mode || token <= 0 || ERR_MissionRules.Terminal(mode.Phase())) return null;
// A delayed command from the previous round must not bind itself to this one.
if (m_ERRServerChatToken > 0)
{
if (mode == m_ERRServerChatMode && token != m_ERRServerChatToken) return null;
if (mode != m_ERRServerChatMode && token <= m_ERRServerChatToken) return null;
}
m_ERRServerChatMode = mode;
m_ERRServerChatToken = token;
if (m_ERRAdminSession && token < m_ERRAdminSession.Token()) return null;
if (!m_ERRAdminSession || token != m_ERRAdminSession.Token() || !m_ERRAdminSession.Valid())
{
if (m_ERRAdminSession) m_ERRAdminSession.Stop();
m_ERRAdminSession = new ERR_AdminSession(mode, this, token, m_ERRAdminGrants);
}
if (!m_ERRAdminTicking)
{
m_ERRAdminTicking = true;
GetGame().GetCallqueue().CallLater(ERR_AdminTick, 1000, true);
}
return m_ERRAdminSession;
}
protected void ERR_AdminTick()
{
if (m_ERRAdminSession && m_ERRAdminSession.Tick()) return;
m_ERRAdminSession = null;
m_ERRAdminTicking = false;
if (GetGame()) GetGame().GetCallqueue().Remove(ERR_AdminTick);
}
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
protected void ERR_AdminRequest(int token, string arguments, bool menu)
{
ERR_AdminSession session = ERR_ServerSession(token);
if (session) session.Command(arguments, menu);
}
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
protected void ERR_AdminSnapshotRequest(int token)
{
ERR_AdminSession session = ERR_ServerSession(token);
if (session) session.Snapshot();
}
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
protected void ERR_AdminLogRequest(int token, int cursor, bool latest)
{
ERR_AdminSession session = ERR_ServerSession(token);
if (session) session.Log(cursor, latest);
}
void ERR_AdminDeliverOpen(int token) { Rpc(ERR_AdminOpenResponse, token); }
void ERR_AdminDeliverReply(int token, string text, bool menu) { Rpc(ERR_AdminReplyResponse, token, text, menu); }
void ERR_AdminDeliverSnapshot(int token, bool authorized, array<string> values, array<string> status) { Rpc(ERR_AdminSnapshotResponse, token, authorized, values, status); }
void ERR_AdminDeliverLog(int token, array<string> lines, int cursor) { Rpc(ERR_AdminLogResponse, token, lines, cursor); }
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
protected void ERR_AdminOpenResponse(int token)
{
if (m_ERRChatClient && m_ERRChatClient.Accept(token)) ERR_AdminMenu.Open();
}
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
protected void ERR_AdminReplyResponse(int token, string text, bool menu)
{
if (!m_ERRChatClient || !m_ERRChatClient.Accept(token)) return;
if (!menu) SCR_ChatComponent.RadioProtocolMessage(text);
ERR_AdminMenu panel = ERR_AdminMenu.GetOpenMenu();
if (panel) panel.ReceiveReply(text);
}
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
protected void ERR_AdminSnapshotResponse(int token, bool authorized, array<string> values, array<string> status)
{
if (!m_ERRChatClient || !m_ERRChatClient.Accept(token)) return;
ERR_AdminMenu panel = ERR_AdminMenu.GetOpenMenu();
if (panel) panel.ReceiveSnapshot(authorized, values, status);
else if (!authorized) SCR_ChatComponent.RadioProtocolMessage("Administrator access required.");
}
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
protected void ERR_AdminLogResponse(int token, array<string> lines, int cursor)
{
if (!m_ERRChatClient || !m_ERRChatClient.Accept(token)) return;
ERR_AdminMenu panel = ERR_AdminMenu.GetOpenMenu();
if (panel) panel.ReceiveLog(lines, cursor);
}
void ~SCR_PlayerController()
{
if (m_ERRChatClient) m_ERRChatClient.Stop();
if (m_ERRAdminSession) m_ERRAdminSession.Stop();
if (GetGame()) GetGame().GetCallqueue().Remove(ERR_AdminTick);
}
}
+587
View File
@@ -0,0 +1,587 @@
FrameWidgetClass {
Name "ERR_AdminRoot" Slot FrameWidgetSlot {
Anchor 0 0 1 1 OffsetLeft 0 OffsetTop 0 OffsetRight 0 OffsetBottom 0
}
{
ImageWidgetClass {
Name "DimBackground" Slot FrameWidgetSlot {
Anchor 0 0 1 1 OffsetLeft 0 OffsetTop 0 OffsetRight 0 OffsetBottom 0
}
Color 0 0 0 0.65
}
FrameWidgetClass {
Name "Panel" Slot FrameWidgetSlot {
Anchor 0.5 0.5 0.5 0.5 PositionX 0 OffsetLeft 0 PositionY 0 OffsetTop 0 SizeX 1000 OffsetRight -1000 SizeY 760 OffsetBottom -760 Alignment 0.5 0.5
}
{
ImageWidgetClass {
Name "PanelBackground" Slot FrameWidgetSlot {
Anchor 0 0 1 1 OffsetLeft 0 OffsetTop 0 OffsetRight 0 OffsetBottom 0
}
Color 0.035 0.05 0.065 0.98
}
TextWidgetClass {
Name "Title" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 24 OffsetTop 24 SizeX 790 OffsetRight -818 SizeY 44 OffsetBottom -68
}
Color 0.9 0.92 0.94 1 Text "Mission settings" "Font Size" 34 "Min Font Size" 34 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Close" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 844 OffsetLeft 844 PositionY 24 OffsetTop 24 SizeX 120 OffsetRight -964 SizeY 42 OffsetBottom -66
}
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Close"
}
}
}
TextWidgetClass {
Name "Connection" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 78 OffsetTop 78 SizeX 936 OffsetRight -964 SizeY 28 OffsetBottom -106
}
Color 0.9 0.92 0.94 1 Text "Requesting administrator access..." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
FrameWidgetClass {
Name "SettingsPage" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 0 OffsetTop 0 SizeX 1000 OffsetRight -1000 SizeY 760 OffsetBottom -760
}
{
TextWidgetClass {
Name "SettingsHelp" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 110 OffsetTop 110 SizeX 936 OffsetRight -964 SizeY 28 OffsetBottom -138
}
Color 0.9 0.92 0.94 1 Text "Changes are saved on the server. Clock, date and weather apply next round." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Label_radioDefenceSeconds" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 152 OffsetTop 152 SizeX 350 OffsetRight -378 SizeY 27 OffsetBottom -179
}
Color 0.9 0.92 0.94 1 Text "Radio defence (30-1800 seconds)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Saved_radioDefenceSeconds" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 180 OffsetTop 180 SizeX 350 OffsetRight -378 SizeY 22 OffsetBottom -202
}
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
ButtonWidgetClass : "{0022F0B45ADBC5AC}UI/layouts/WidgetLibrary/EditBox/WLib_EditBox.layout" {
Name "radioDefenceSeconds" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 386 OffsetLeft 386 PositionY 152 OffsetTop 152 SizeX 260 OffsetRight -646 SizeY 42 OffsetBottom -194
}
components {
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
m_sLabel ""
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Apply_radioDefenceSeconds" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 664 OffsetLeft 664 PositionY 152 OffsetTop 152 SizeX 150 OffsetRight -814 SizeY 42 OffsetBottom -194
}
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Apply"
}
}
}
TextWidgetClass {
Name "Label_aiIntelChancePercent" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 210 OffsetTop 210 SizeX 350 OffsetRight -378 SizeY 27 OffsetBottom -237
}
Color 0.9 0.92 0.94 1 Text "AI carrying intel (0-100%)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Saved_aiIntelChancePercent" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 238 OffsetTop 238 SizeX 350 OffsetRight -378 SizeY 22 OffsetBottom -260
}
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
ButtonWidgetClass : "{0022F0B45ADBC5AC}UI/layouts/WidgetLibrary/EditBox/WLib_EditBox.layout" {
Name "aiIntelChancePercent" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 386 OffsetLeft 386 PositionY 210 OffsetTop 210 SizeX 260 OffsetRight -646 SizeY 42 OffsetBottom -252
}
components {
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
m_sLabel ""
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Apply_aiIntelChancePercent" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 664 OffsetLeft 664 PositionY 210 OffsetTop 210 SizeX 150 OffsetRight -814 SizeY 42 OffsetBottom -252
}
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Apply"
}
}
}
TextWidgetClass {
Name "Label_intelRevealChancePercent" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 268 OffsetTop 268 SizeX 350 OffsetRight -378 SizeY 27 OffsetBottom -295
}
Color 0.9 0.92 0.94 1 Text "First-document exact reveal (0-100%)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Saved_intelRevealChancePercent" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 296 OffsetTop 296 SizeX 350 OffsetRight -378 SizeY 22 OffsetBottom -318
}
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
ButtonWidgetClass : "{0022F0B45ADBC5AC}UI/layouts/WidgetLibrary/EditBox/WLib_EditBox.layout" {
Name "intelRevealChancePercent" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 386 OffsetLeft 386 PositionY 268 OffsetTop 268 SizeX 260 OffsetRight -646 SizeY 42 OffsetBottom -310
}
components {
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
m_sLabel ""
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Apply_intelRevealChancePercent" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 664 OffsetLeft 664 PositionY 268 OffsetTop 268 SizeX 150 OffsetRight -814 SizeY 42 OffsetBottom -310
}
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Apply"
}
}
}
TextWidgetClass {
Name "Label_startTimeMinutes" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 326 OffsetTop 326 SizeX 350 OffsetRight -378 SizeY 27 OffsetBottom -353
}
Color 0.9 0.92 0.94 1 Text "Next-round start time (HH:MM)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Saved_startTimeMinutes" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 354 OffsetTop 354 SizeX 350 OffsetRight -378 SizeY 22 OffsetBottom -376
}
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
ButtonWidgetClass : "{0022F0B45ADBC5AC}UI/layouts/WidgetLibrary/EditBox/WLib_EditBox.layout" {
Name "startTimeMinutes" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 386 OffsetLeft 386 PositionY 326 OffsetTop 326 SizeX 260 OffsetRight -646 SizeY 42 OffsetBottom -368
}
components {
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
m_sLabel ""
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Apply_startTimeMinutes" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 664 OffsetLeft 664 PositionY 326 OffsetTop 326 SizeX 150 OffsetRight -814 SizeY 42 OffsetBottom -368
}
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Apply"
}
}
}
TextWidgetClass {
Name "Label_timeMultiplier" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 384 OffsetTop 384 SizeX 350 OffsetRight -378 SizeY 27 OffsetBottom -411
}
Color 0.9 0.92 0.94 1 Text "Next-round time speed (1-24x)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Saved_timeMultiplier" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 412 OffsetTop 412 SizeX 350 OffsetRight -378 SizeY 22 OffsetBottom -434
}
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
ButtonWidgetClass : "{0022F0B45ADBC5AC}UI/layouts/WidgetLibrary/EditBox/WLib_EditBox.layout" {
Name "timeMultiplier" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 386 OffsetLeft 386 PositionY 384 OffsetTop 384 SizeX 260 OffsetRight -646 SizeY 42 OffsetBottom -426
}
components {
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
m_sLabel ""
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Apply_timeMultiplier" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 664 OffsetLeft 664 PositionY 384 OffsetTop 384 SizeX 150 OffsetRight -814 SizeY 42 OffsetBottom -426
}
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Apply"
}
}
}
TextWidgetClass {
Name "Label_startDateCode" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 442 OffsetTop 442 SizeX 350 OffsetRight -378 SizeY 27 OffsetBottom -469
}
Color 0.9 0.92 0.94 1 Text "Start date (YYYY-MM-DD or authored)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Saved_startDateCode" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 470 OffsetTop 470 SizeX 350 OffsetRight -378 SizeY 22 OffsetBottom -492
}
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
ButtonWidgetClass : "{0022F0B45ADBC5AC}UI/layouts/WidgetLibrary/EditBox/WLib_EditBox.layout" {
Name "startDateCode" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 386 OffsetLeft 386 PositionY 442 OffsetTop 442 SizeX 260 OffsetRight -646 SizeY 42 OffsetBottom -484
}
components {
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
m_sLabel ""
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Apply_startDateCode" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 664 OffsetLeft 664 PositionY 442 OffsetTop 442 SizeX 150 OffsetRight -814 SizeY 42 OffsetBottom -484
}
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Apply"
}
}
}
TextWidgetClass {
Name "Label_weatherProfile" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 500 OffsetTop 500 SizeX 350 OffsetRight -378 SizeY 27 OffsetBottom -527
}
Color 0.9 0.92 0.94 1 Text "Weather (authored / clear / overcast / rain)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Saved_weatherProfile" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 528 OffsetTop 528 SizeX 350 OffsetRight -378 SizeY 22 OffsetBottom -550
}
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
ButtonWidgetClass : "{0022F0B45ADBC5AC}UI/layouts/WidgetLibrary/EditBox/WLib_EditBox.layout" {
Name "weatherProfile" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 386 OffsetLeft 386 PositionY 500 OffsetTop 500 SizeX 260 OffsetRight -646 SizeY 42 OffsetBottom -542
}
components {
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
m_sLabel ""
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Apply_weatherProfile" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 664 OffsetLeft 664 PositionY 500 OffsetTop 500 SizeX 150 OffsetRight -814 SizeY 42 OffsetBottom -542
}
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Apply"
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "IntelSummary" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 575 OffsetTop 575 SizeX 200 OffsetRight -228 SizeY 42 OffsetBottom -617
}
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Intel summary"
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "RerollIntel" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 250 OffsetLeft 250 PositionY 575 OffsetTop 575 SizeX 200 OffsetRight -450 SizeY 42 OffsetBottom -617
}
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Reroll AI intel"
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "OpenHQ" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 714 OffsetLeft 714 PositionY 575 OffsetTop 575 SizeX 250 OffsetRight -964 SizeY 42 OffsetBottom -617
}
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "HQ / status"
}
}
}
TextWidgetClass {
Name "RerollHelp" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 625 OffsetTop 625 SizeX 936 OffsetRight -964 SizeY 26 OffsetBottom -651
}
Color 0.9 0.92 0.94 1 Text "Reroll updates existing AI through the Intel service; recovered intel is preserved." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
}
}
FrameWidgetClass {
Name "HQPage" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 0 OffsetTop 0 SizeX 1000 OffsetRight -1000 SizeY 760 OffsetBottom -760
}
{
TextWidgetClass {
Name "Label_reportIntervalSeconds" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 140 OffsetTop 140 SizeX 350 OffsetRight -378 SizeY 27 OffsetBottom -167
}
Color 0.9 0.92 0.94 1 Text "Report interval (5-60 seconds)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Saved_reportIntervalSeconds" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 168 OffsetTop 168 SizeX 350 OffsetRight -378 SizeY 22 OffsetBottom -190
}
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
ButtonWidgetClass : "{0022F0B45ADBC5AC}UI/layouts/WidgetLibrary/EditBox/WLib_EditBox.layout" {
Name "reportIntervalSeconds" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 386 OffsetLeft 386 PositionY 140 OffsetTop 140 SizeX 260 OffsetRight -646 SizeY 42 OffsetBottom -182
}
components {
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
m_sLabel ""
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Apply_reportIntervalSeconds" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 664 OffsetLeft 664 PositionY 140 OffsetTop 140 SizeX 150 OffsetRight -814 SizeY 42 OffsetBottom -182
}
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Apply"
}
}
}
TextWidgetClass {
Name "Label_supportSquadLimit" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 207 OffsetTop 207 SizeX 350 OffsetRight -378 SizeY 27 OffsetBottom -234
}
Color 0.9 0.92 0.94 1 Text "Support squad limit (0-8)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Saved_supportSquadLimit" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 235 OffsetTop 235 SizeX 350 OffsetRight -378 SizeY 22 OffsetBottom -257
}
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
ButtonWidgetClass : "{0022F0B45ADBC5AC}UI/layouts/WidgetLibrary/EditBox/WLib_EditBox.layout" {
Name "supportSquadLimit" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 386 OffsetLeft 386 PositionY 207 OffsetTop 207 SizeX 260 OffsetRight -646 SizeY 42 OffsetBottom -249
}
components {
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
m_sLabel ""
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Apply_supportSquadLimit" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 664 OffsetLeft 664 PositionY 207 OffsetTop 207 SizeX 150 OffsetRight -814 SizeY 42 OffsetBottom -249
}
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Apply"
}
}
}
TextWidgetClass {
Name "Label_responseRadiusMetres" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 274 OffsetTop 274 SizeX 350 OffsetRight -378 SizeY 27 OffsetBottom -301
}
Color 0.9 0.92 0.94 1 Text "Response radius (500-5000 metres)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Saved_responseRadiusMetres" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 302 OffsetTop 302 SizeX 350 OffsetRight -378 SizeY 22 OffsetBottom -324
}
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
ButtonWidgetClass : "{0022F0B45ADBC5AC}UI/layouts/WidgetLibrary/EditBox/WLib_EditBox.layout" {
Name "responseRadiusMetres" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 386 OffsetLeft 386 PositionY 274 OffsetTop 274 SizeX 260 OffsetRight -646 SizeY 42 OffsetBottom -316
}
components {
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
m_sLabel ""
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Apply_responseRadiusMetres" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 664 OffsetLeft 664 PositionY 274 OffsetTop 274 SizeX 150 OffsetRight -814 SizeY 42 OffsetBottom -316
}
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Apply"
}
}
}
TextWidgetClass {
Name "Status" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 350 OffsetTop 350 SizeX 936 OffsetRight -964 SizeY 210 OffsetBottom -560
}
Color 0.9 0.92 0.94 1 Text "Waiting for mission status..." "Font Size" 19 "Min Font Size" 19 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "BackSettings" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 595 OffsetTop 595 SizeX 250 OffsetRight -278 SizeY 42 OffsetBottom -637
}
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Back to settings"
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "OpenJournal" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 714 OffsetLeft 714 PositionY 595 OffsetTop 595 SizeX 250 OffsetRight -964 SizeY 42 OffsetBottom -637
}
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Open live journal"
}
}
}
}
}
FrameWidgetClass {
Name "JournalPage" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 0 OffsetTop 0 SizeX 1000 OffsetRight -1000 SizeY 760 OffsetBottom -760
}
{
TextWidgetClass {
Name "JournalHelp" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 110 OffsetTop 110 SizeX 936 OffsetRight -964 SizeY 30 OffsetBottom -140
}
Color 0.9 0.92 0.94 1 Text "Radio reports, orders and physical support activity from the mission HQ." "Font Size" 19 "Min Font Size" 19 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
ScrollLayoutWidgetClass {
Name "JournalScroll" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 150 OffsetTop 150 SizeX 936 OffsetRight -964 SizeY 400 OffsetBottom -550
}
components {
SCR_GamepadScrollComponent "{5707100AC31E657E}" {
}
}
{
VerticalLayoutWidgetClass {
Name "JournalContent" Slot AlignableSlot {
HorizontalAlign 3 VerticalAlign 3 Padding 8 4 14 4
}
{
TextWidgetClass {
Name "JournalText" Slot LayoutSlot {
HorizontalAlign 3 Padding 0 0 0 8 SizeMode Fill
}
Clipping Ancestor Color 0.9 0.92 0.94 1 Text "Waiting for the first HQ event..." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
}
}
}
}
TextWidgetClass {
Name "JournalState" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 558 OffsetTop 558 SizeX 936 OffsetRight -964 SizeY 34 OffsetBottom -592
}
Color 0.9 0.92 0.94 1 Text "Waiting for the server..." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "BackHQ" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 605 OffsetTop 605 SizeX 250 OffsetRight -278 SizeY 42 OffsetBottom -647
}
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Back to HQ"
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "PauseJournal" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 365 OffsetLeft 365 PositionY 605 OffsetTop 605 SizeX 250 OffsetRight -615 SizeY 42 OffsetBottom -647
}
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Pause live"
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "LatestJournal" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 714 OffsetLeft 714 PositionY 605 OffsetTop 605 SizeX 250 OffsetRight -964 SizeY 42 OffsetBottom -647
}
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Jump to latest"
}
}
}
}
}
TextWidgetClass {
Name "Feedback" Slot FrameWidgetSlot {
Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 668 OffsetTop 668 SizeX 936 OffsetRight -964 SizeY 78 OffsetBottom -746
}
Color 0.9 0.92 0.94 1 Text "" "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
}
}
}
}
@@ -0,0 +1,11 @@
MetaFileClass {
Name "{AE761C0439D25FB8}UI/layouts/ERR/AdminMenu.layout"
Configurations {
LayoutResourceClass PC { }
LayoutResourceClass HEADLESS : PC { }
LayoutResourceClass XBOX_ONE : PC { }
LayoutResourceClass XBOX_SERIES : PC { }
LayoutResourceClass PS4 : PC { }
LayoutResourceClass PS5 : PC { }
}
}
+10
View File
@@ -5,4 +5,14 @@ GameProject {
Dependencies {
"58D0FB3206B6F859"
}
Configurations {
GameProjectConfig PC {
MenuManagerSettings MenuManagerSettings "{50C890C996B0A6FC}" {
MenuConfigs {
"{C747AFB6B750CE9A}Configs/System/chimeraMenus.conf"
"{B48C7FA12692DE03}Configs/ERR/AdminMenus.conf"
}
}
}
}
}
@@ -0,0 +1,38 @@
// AI document settings and rerolls belong to Intel, independently of Chat.
class ERR_IntelAdminProvider : ERR_AdminProvider
{
override string Id() { return "intel"; }
override void Capabilities(array<string> values) { values.Insert("supports=intel"); }
override string Setting(string section, string option)
{
if (section == "intel" && (option == "carrying" || option == "chance")) return "aiIntelChancePercent";
return "";
}
override bool Execute(ERR_GameMode mode, string arguments, array<string> replies)
{
if (arguments == "intel show")
{
replies.Insert("aiIntelChancePercent=" + mode.Settings().Get("aiIntelChancePercent").ToString());
replies.Insert("intelRevealChancePercent=" + mode.Settings().Get("intelRevealChancePercent").ToString());
return true;
}
if (arguments == "intel reroll" || arguments == "intel reroll show")
{
ERR_IntelService intel = mode.Intel();
if (!intel) { replies.Insert("Intel service unavailable."); return true; }
if (arguments == "intel reroll" && !intel.BeginReroll())
replies.Insert("Intel reroll could not start. It requires an active round and no running reroll.");
replies.Insert(intel.RerollStatus());
return true;
}
return false;
}
override bool Help(string topic, array<string> replies)
{
if (topic != "intel") return false;
replies.Insert("/esc intel show | intel carrying show|set N (0-100; alias: intel chance)");
replies.Insert("Carrying changes affect newly registered AI. /esc intel reroll applies to eligible living AI; intel reroll show reports progress.");
replies.Insert("/esc intel reveal show|set N|help (0-100); newly recovered documents change, known locations and spent documents remain unchanged.");
return true;
}
}
@@ -159,6 +159,7 @@ modded class ERR_GameMode
super.EOnInit(owner);
if (SCR_Global.IsEditMode() || !Replication.IsServer()) return;
m_Intel = new ERR_IntelService(this);
Admin().Register(new ERR_IntelAdminProvider());
OnGroup().Insert(m_Intel.RegisterGroup);
OnTick().Insert(m_Intel.Tick);
OnStop().Insert(m_Intel.Stop);