Integrate mission-owned chat administration and menu
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 { }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user