Add admin-controlled self epinephrine for unconscious players

This commit is contained in:
2026-10-15 12:39:19 +02:00
parent 3b920e0715
commit 0a7102cd51
19 changed files with 346 additions and 22 deletions
@@ -90,6 +90,11 @@ ActionManager : "{795184CF9AD764DB}Configs/System/chimeraInputCommon.conf" {
} }
ActionContext ERR_ObserverContext : "{C538807EEE48BED8}Configs/System/ActionContext/ManualCameraContext.conf" { ActionContext ERR_ObserverContext : "{C538807EEE48BED8}Configs/System/ActionContext/ManualCameraContext.conf" {
Actions { Actions {
Action ERR_SelfEpinephrine {
InputSource InputSourceValue "{A7B19F416E541020}" {
Input "keyboard:KC_SPACE"
}
}
Action ERR_ObserverPrevious { Action ERR_ObserverPrevious {
InputSource InputSourceValue "{A7B19F416E541001}" { InputSource InputSourceValue "{A7B19F416E541001}" {
Input "keyboard:KC_LEFT" Input "keyboard:KC_LEFT"
@@ -131,7 +131,7 @@ class ERR_AdminMenu : ChimeraMenuBase
m_Controller = SCR_PlayerController.Cast(GetGame().GetPlayerController()); m_Controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
m_Mode = ERR_GameMode.Cast(GetGame().GetGameMode()); m_Mode = ERR_GameMode.Cast(GetGame().GetGameMode());
if (!m_Controller) { Close(); return; } if (!m_Controller) { Close(); return; }
array<string> names = {"PlaneOn", "PlaneOff", "Close", "OpenSettings", "OpenWorld", "OpenFactions", "OpenHQ", "OpenJournal", "PauseJournal", "JournalDetails", "LatestJournal", "KeepWeaponOn", "KeepWeaponOff", "IntelSummary", "RerollIntel", "OpenMods", "OpenIntel", "OpenStatus", "OpenTerrains", "OpenItems", "OpenTTS"}; array<string> names = {"PlaneOn", "PlaneOff", "Close", "OpenSettings", "OpenWorld", "OpenFactions", "OpenHQ", "OpenJournal", "PauseJournal", "JournalDetails", "LatestJournal", "KeepWeaponOn", "KeepWeaponOff", "SelfEpiOn", "SelfEpiOff", "IntelSummary", "RerollIntel", "OpenMods", "OpenIntel", "OpenStatus", "OpenTerrains", "OpenItems", "OpenTTS"};
names.Insert("RandomDay_startTimeMinutes"); names.Insert("RandomDay_startTimeMinutes");
names.Insert("Default_viewDistanceMetres"); names.Insert("Default_viewDistanceMetres");
names.Insert("OpenForces"); names.Insert("OpenForces");
@@ -257,9 +257,11 @@ class ERR_AdminMenu : ChimeraMenuBase
if (page == 2) m_LogPoll = 1; if (page == 2) m_LogPoll = 1;
} }
protected int m_KeepWeaponState = -1; protected int m_KeepWeaponState = -1;
protected int m_SelfEpiState = -1;
protected int m_RestartEmptyState = -1; protected int m_RestartEmptyState = -1;
protected void UpdateActions() protected void UpdateActions()
{ {
ERR_SelectionStyle.Pair(GetRootWidget(), "SelfEpiOn", "SelfEpiOff", m_SelfEpiState);
ERR_SelectionStyle.Pair(GetRootWidget(), "KeepWeaponOn", "KeepWeaponOff", m_KeepWeaponState); ERR_SelectionStyle.Pair(GetRootWidget(), "KeepWeaponOn", "KeepWeaponOff", m_KeepWeaponState);
ERR_SelectionStyle.Pair(GetRootWidget(), "RestartEmptyOn", "RestartEmptyOff", m_RestartEmptyState); ERR_SelectionStyle.Pair(GetRootWidget(), "RestartEmptyOn", "RestartEmptyOff", m_RestartEmptyState);
foreach (SCR_ButtonBaseComponent button : m_Buttons) foreach (SCR_ButtonBaseComponent button : m_Buttons)
@@ -278,6 +280,8 @@ class ERR_AdminMenu : ChimeraMenuBase
else if (name == "LatestJournal") button.SetEnabled(m_Ready && m_HQ); else if (name == "LatestJournal") button.SetEnabled(m_Ready && m_HQ);
else if (name == "RestartEmptyOn") button.SetEnabled(m_Ready && m_RestartEmptyState == 0); else if (name == "RestartEmptyOn") button.SetEnabled(m_Ready && m_RestartEmptyState == 0);
else if (name == "RestartEmptyOff") button.SetEnabled(m_Ready && m_RestartEmptyState == 1); else if (name == "RestartEmptyOff") button.SetEnabled(m_Ready && m_RestartEmptyState == 1);
else if (name == "SelfEpiOn") button.SetEnabled(m_Ready && m_SelfEpiState == 0);
else if (name == "SelfEpiOff") button.SetEnabled(m_Ready && m_SelfEpiState == 1);
else if (name == "KeepWeaponOn") button.SetEnabled(m_Ready && m_KeepWeaponState == 0); else if (name == "KeepWeaponOn") button.SetEnabled(m_Ready && m_KeepWeaponState == 0);
else if (name == "KeepWeaponOff") button.SetEnabled(m_Ready && m_KeepWeaponState == 1); else if (name == "KeepWeaponOff") button.SetEnabled(m_Ready && m_KeepWeaponState == 1);
} }
@@ -286,7 +290,7 @@ class ERR_AdminMenu : ChimeraMenuBase
{ {
if (!authorized) { Close(); return; } if (!authorized) { Close(); return; }
if (!values) return; if (!values) return;
m_HQ = false; m_Intel = false; m_AvailableFields.Clear(); m_KeepWeaponState = -1; m_RestartEmptyState = -1; m_HQ = false; m_Intel = false; m_AvailableFields.Clear(); m_KeepWeaponState = -1; m_SelfEpiState = -1; m_RestartEmptyState = -1;
foreach (string entry : values) foreach (string entry : values)
{ {
if (entry == "supports=hq") { m_HQ = true; continue; } if (entry == "supports=hq") { m_HQ = true; continue; }
@@ -330,6 +334,11 @@ class ERR_AdminMenu : ChimeraMenuBase
SetText("Saved_restartOnEmpty", "Saved: " + restartState); SetText("Saved_restartOnEmpty", "Saved: " + restartState);
continue; continue;
} }
if (key == "selfEpinephrine")
{
m_SelfEpiState = entry.Substring(divider + 1, entry.Length() - divider - 1).ToInt();
continue;
}
if (key == "keepWeaponOnUnconscious") if (key == "keepWeaponOnUnconscious")
{ {
m_KeepWeaponState = entry.Substring(divider + 1, entry.Length() - divider - 1).ToInt(); m_KeepWeaponState = entry.Substring(divider + 1, entry.Length() - divider - 1).ToInt();
@@ -485,6 +494,13 @@ class ERR_AdminMenu : ChimeraMenuBase
m_ClickDelay = 0.3; SetText("Feedback", "Saving restart after last disconnect..."); m_ClickDelay = 0.3; SetText("Feedback", "Saving restart after last disconnect...");
m_Controller.ERR_AdminSubmit("mission restartempty set " + restartEnabled, true); return; m_Controller.ERR_AdminSubmit("mission restartempty set " + restartEnabled, true); return;
} }
if (name == "SelfEpiOn" || name == "SelfEpiOff")
{
if (m_SelfEpiState < 0) return;
m_ClickDelay = 0.3; SetText("Feedback", "Saving self epinephrine permission...");
string selfEpi = "0"; if (name == "SelfEpiOn") selfEpi = "1";
m_Controller.ERR_AdminSubmit("mission selfepi set " + selfEpi, true); return;
}
if (name == "KeepWeaponOn" || name == "KeepWeaponOff") if (name == "KeepWeaponOn" || name == "KeepWeaponOff")
{ {
string retain = "0"; if (name == "KeepWeaponOn") retain = "1"; string retain = "0"; if (name == "KeepWeaponOn") retain = "1";
@@ -22,6 +22,7 @@ class ERR_CommandArguments
if (section == "mission" && option == "roadblocks") return "roadblockCount"; if (section == "mission" && option == "roadblocks") return "roadblockCount";
if (section == "mission" && option == "traffic") return "civilianTrafficCount"; if (section == "mission" && option == "traffic") return "civilianTrafficCount";
if (section == "mission" && option == "start") return "startMode"; if (section == "mission" && option == "start") return "startMode";
if (section == "mission" && option == "selfepi") return "selfEpinephrine";
if (section == "mission" && option == "keepweapon") return "keepWeaponOnUnconscious"; if (section == "mission" && option == "keepweapon") return "keepWeaponOnUnconscious";
if (section == "mission" && option == "restartempty") return "restartOnEmpty"; if (section == "mission" && option == "restartempty") return "restartOnEmpty";
if (section == "mission" && option == "emptyhours") return "emptyRestartHours"; if (section == "mission" && option == "emptyhours") return "emptyRestartHours";
@@ -184,6 +185,7 @@ class ERR_AdminService
if (key == "restartOnEmpty") return "On (1) restarts after the last disconnect; Off (0) preserves the round for reconnects. Default On. Applies immediately; normal round endings still apply."; if (key == "restartOnEmpty") return "On (1) restarts after the last disconnect; Off (0) preserves the round for reconnects. Default On. Applies immediately; normal round endings still apply.";
if (key == "emptyRestartHours") return "0 disables idle restarts; 1..168 real hours continuously empty. Default 5. Applies immediately, measured from mission start or the last disconnect. Connected players, including spectators, prevent idle restarts."; if (key == "emptyRestartHours") return "0 disables idle restarts; 1..168 real hours continuously empty. Default 5. Applies immediately, measured from mission start or the last disconnect. Connected players, including spectators, prevent idle restarts.";
if (key == "civilianTrafficCount") return "Set 0..6 civilian cars (default 3). Zero disables new traffic. Unclaimed cars retire only far from all players and out of sight; player-entered cars are preserved."; if (key == "civilianTrafficCount") return "Set 0..6 civilian cars (default 3). Zero disables new traffic. Unclaimed cars retire only far from all players and out of sight; player-entered cars are preserved.";
if (key == "selfEpinephrine") return "0 disabled (default), 1 enabled. Unconscious players can press Space to use a carried epinephrine injector. Applies immediately; consumes the item and keeps its normal medical effect.";
if (key == "keepWeaponOnUnconscious") return "Set 1 to retain the held firearm during unconsciousness and equip it after recovery; 0 keeps normal dropping. Applies to the next knockout."; if (key == "keepWeaponOnUnconscious") return "Set 1 to retain the held firearm during unconsciousness and equip it after recovery; 0 keeps normal dropping. Applies to the next knockout.";
if (key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent") if (key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent")
return "Set 0..100 percent or Random. Applies to newly equipped enemy soldiers; existing loadouts are not rerolled or replenished."; return "Set 0..100 percent or Random. Applies to newly equipped enemy soldiers; existing loadouts are not rerolled or replenished.";
@@ -224,6 +226,7 @@ class ERR_AdminService
if (topic == "time" || topic == "mission") if (topic == "time" || topic == "mission")
{ {
replies.Insert("/esc mission radio show|set N|random | mission abandon"); replies.Insert("/esc mission radio show|set N|random | mission abandon");
replies.Insert("/esc mission selfepi show|set 0|1; allow unconscious self-injection with Space. Disabled by default.");
replies.Insert("/esc mission restartempty show|set on|off | mission emptyhours show|set 0..168; applies now, zero hours disables idle restarts."); replies.Insert("/esc mission restartempty show|set on|off | mission emptyhours show|set 0..168; applies now, zero hours disables idle restarts.");
replies.Insert("/esc mission doctrine show|set current|coordinated | mission aishadow show|set off|on; next round only."); replies.Insert("/esc mission doctrine show|set current|coordinated | mission aishadow show|set off|on; next round only.");
replies.Insert("/esc mission start show|set prison|crash: save the next round start; current round unchanged."); replies.Insert("/esc mission start show|set prison|crash: save the next round start; current round unchanged.");
@@ -55,7 +55,7 @@ modded class ERR_GameMode
string enabled; string enabled;
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return; if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return;
string nativeEndCase; string nativeEndCase;
array<string> independentFixtures = {"coordinated-ai", "optional-site-skip", "infantry-combat", "medical-combat", "hunter-faction", "unconscious-camera", "objective-map", "town-garrisons", "site-garrisons", "radio-interaction", "enemy-loadout", "aggression", "native-end-active", "native-end-preparing", "generated-sites", "roadblocks", "mortar-station", "opening-tools", "airfield-markers", "air-logistics", "air-assault", "air-recon"}; array<string> independentFixtures = {"coordinated-ai", "optional-site-skip", "infantry-combat", "medical-combat", "hunter-faction", "unconscious-camera", "self-epinephrine", "objective-map", "town-garrisons", "site-garrisons", "radio-interaction", "enemy-loadout", "aggression", "native-end-active", "native-end-preparing", "generated-sites", "roadblocks", "mortar-station", "opening-tools", "airfield-markers", "air-logistics", "air-assault", "air-recon"};
if (System.GetCLIParam("errSmokeOutcome", nativeEndCase) && independentFixtures.Contains(nativeEndCase)) return; if (System.GetCLIParam("errSmokeOutcome", nativeEndCase) && independentFixtures.Contains(nativeEndCase)) return;
ERR_NativeObjectiveProbe.BeginRound(); ERR_NativeObjectiveProbe.BeginRound();
ERR_NativeProgressionIdentityProbe.BeginRound(); ERR_NativeProgressionIdentityProbe.BeginRound();
@@ -127,11 +127,12 @@ class ERR_FollowSurvivorCameraComponent : SCR_BaseManualCameraComponent
protected ref ERR_ObserverScopeDisplay m_ScopeDisplay = new ERR_ObserverScopeDisplay(); protected ref ERR_ObserverScopeDisplay m_ScopeDisplay = new ERR_ObserverScopeDisplay();
protected ref ERR_ObserverPIPDisplay m_PIPDisplay = new ERR_ObserverPIPDisplay(); protected ref ERR_ObserverPIPDisplay m_PIPDisplay = new ERR_ObserverPIPDisplay();
protected bool m_InputContextActive; protected bool m_InputContextActive;
protected bool m_SpaceDown;
bool InputContextActive() { return m_InputContextActive; } bool InputContextActive() { return m_InputContextActive; }
bool IsPlayerView() { return m_PlayerView; } bool IsPlayerView() { return m_PlayerView; }
// Process one edge per press; menus still consume the state so holding a key // Process one edge per press; menus still consume the state so holding a key
// while closing a menu cannot unexpectedly change the followed teammate. // while closing a menu cannot unexpectedly change the followed teammate.
void Input(bool enabled, bool left, bool right, bool enter) void Input(bool enabled, bool left, bool right, bool enter, bool space = false)
{ {
// An inactive context reports zero even while a physical key is held. Let // An inactive context reports zero even while a physical key is held. Let
// the reactivated context settle, then require release before accepting keys. // the reactivated context settle, then require release before accepting keys.
@@ -139,16 +140,20 @@ class ERR_FollowSurvivorCameraComponent : SCR_BaseManualCameraComponent
else if (m_InputResumeFrames > 0) else if (m_InputResumeFrames > 0)
{ {
enabled = false; enabled = false;
if (m_InputResumeFrames > 1 || (!left && !right && !enter)) m_InputResumeFrames--; if (m_InputResumeFrames > 1 || (!left && !right && !enter && !space)) m_InputResumeFrames--;
} }
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController()); SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
int cycle = ERR_ObserverRules.Cycle(enabled, left, right, m_LeftDown, m_RightDown); int cycle = ERR_ObserverRules.Cycle(enabled, left, right, m_LeftDown, m_RightDown);
if (controller && cycle != 0) controller.ERR_CycleObserver(cycle); if (controller && cycle != 0) controller.ERR_CycleObserver(cycle);
if (ERR_ObserverRules.Toggle(enabled, enter, m_EnterDown)) m_PlayerView = !m_PlayerView; if (ERR_ObserverRules.Toggle(enabled, enter, m_EnterDown)) m_PlayerView = !m_PlayerView;
if (controller && ERR_ObserverRules.Toggle(enabled, space, m_SpaceDown)) controller.ERR_UseSelfEpinephrine();
m_SpaceDown = space;
m_LeftDown = left; m_RightDown = right; m_EnterDown = enter; m_LeftDown = left; m_RightDown = right; m_EnterDown = enter;
} }
override bool EOnCameraInit() override bool EOnCameraInit()
{ {
// Release a held jump key after knockout before accepting self-injection.
m_InputResumeFrames = 2;
m_OrbitFov = GetCameraEntity().GetVerticalFOV(); m_OrbitFov = GetCameraEntity().GetVerticalFOV();
m_OrbitNear = GetCameraEntity().GetNearPlane(); m_OrbitNear = GetCameraEntity().GetNearPlane();
return true; return true;
@@ -159,7 +164,7 @@ class ERR_FollowSurvivorCameraComponent : SCR_BaseManualCameraComponent
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController()); SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
if (!controller || GetGame().GetCameraManager().CurrentCamera() != GetCameraEntity()) { m_PIPDisplay.Close(); m_ScopeDisplay.Close(); m_ViewHead.Restore(); return; } if (!controller || GetGame().GetCameraManager().CurrentCamera() != GetCameraEntity()) { m_PIPDisplay.Close(); m_ScopeDisplay.Close(); m_ViewHead.Restore(); return; }
m_InputContextActive = GetInputManager().IsContextActive("ERR_ObserverContext"); m_InputContextActive = GetInputManager().IsContextActive("ERR_ObserverContext");
Input(param.isManualInputEnabled, GetInputManager().GetActionValue("ERR_ObserverPrevious") > 0.5, GetInputManager().GetActionValue("ERR_ObserverNext") > 0.5, GetInputManager().GetActionValue("ERR_ObserverView") > 0.5); Input(param.isManualInputEnabled, GetInputManager().GetActionValue("ERR_ObserverPrevious") > 0.5, GetInputManager().GetActionValue("ERR_ObserverNext") > 0.5, GetInputManager().GetActionValue("ERR_ObserverView") > 0.5, GetInputManager().GetActionValue("ERR_SelfEpinephrine") > 0.5);
IEntity target = controller.ERR_ObservedEntity(); IEntity target = controller.ERR_ObservedEntity();
float fov, nearPlane; float fov, nearPlane;
if (m_PlayerView && controller.ERR_ReadPlayerView(target, param.timeSlice, param.transform, fov, nearPlane)) if (m_PlayerView && controller.ERR_ReadPlayerView(target, param.timeSlice, param.transform, fov, nearPlane))
@@ -283,6 +288,9 @@ modded class SCR_PlayerController
protected IEntity m_ERRRecoveryBody; protected IEntity m_ERRRecoveryBody;
protected float m_ERRRecoveryDeadline = -1; protected float m_ERRRecoveryDeadline = -1;
// Optional casualty action is implemented by the self-epinephrine extension.
string ERR_SelfEpinephrineHint() { return ""; }
void ERR_UseSelfEpinephrine() {}
ERR_ObserverReason ERR_ObserverState() { return m_ERRObserverReason; } ERR_ObserverReason ERR_ObserverState() { return m_ERRObserverReason; }
vector ERR_ObserverAnchor() { return m_ERRObserverAnchor; } vector ERR_ObserverAnchor() { return m_ERRObserverAnchor; }
IEntity ERR_ObservedEntity() IEntity ERR_ObservedEntity()
@@ -458,6 +466,8 @@ modded class SCR_PlayerController
// whose local entity is still streaming. Never imply no survivors from delay. // whose local entity is still streaming. Never imply no survivors from delay.
bool survivorAvailable = m_ERRObservedId != RplId.Invalid(); bool survivorAvailable = m_ERRObservedId != RplId.Invalid();
string recovery = mode.RecoveryStatus(); string recovery = mode.RecoveryStatus();
string selfEpiHint = ERR_SelfEpinephrineHint();
if (!selfEpiHint.IsEmpty()) recovery += "\n" + selfEpiHint;
if (ERR_ObserverGuidance.Refresh(m_ERRObserverReady, m_ERRShownObserverReason, m_ERRObserverReason, m_ERRShownObserverAvailable, survivorAvailable) || m_ERRShownRecoveryStatus != recovery) if (ERR_ObserverGuidance.Refresh(m_ERRObserverReady, m_ERRShownObserverReason, m_ERRObserverReason, m_ERRShownObserverAvailable, survivorAvailable) || m_ERRShownRecoveryStatus != recovery)
{ {
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance(); SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
@@ -64,7 +64,7 @@ class ERR_ScenarioComponent : ScriptComponent
#ifdef WORKBENCH #ifdef WORKBENCH
string historicalFixture; string historicalFixture;
if (!m_TerrainIndependent && System.GetCLIParam("errSmokeOutcome", historicalFixture) && historicalFixture != "normal-escape-route" && historicalFixture != "crash-start" && historicalFixture != "mounted-ammo" && historicalFixture != "alarm-hearing" && historicalFixture != "optional-site-skip" && historicalFixture != "infantry-combat" && historicalFixture != "site-garrisons" && historicalFixture != "generated-sites" && historicalFixture != "roadblocks" && historicalFixture != "opening-tools" && historicalFixture != "airfield-markers" && historicalFixture != "air-recon" && historicalFixture != "air-logistics" && historicalFixture != "air-assault") if (!m_TerrainIndependent && System.GetCLIParam("errSmokeOutcome", historicalFixture) && historicalFixture != "normal-escape-route" && historicalFixture != "crash-start" && historicalFixture != "mounted-ammo" && historicalFixture != "alarm-hearing" && historicalFixture != "optional-site-skip" && historicalFixture != "infantry-combat" && historicalFixture != "site-garrisons" && historicalFixture != "generated-sites" && historicalFixture != "roadblocks" && historicalFixture != "opening-tools" && historicalFixture != "airfield-markers" && historicalFixture != "air-recon" && historicalFixture != "air-logistics" && historicalFixture != "air-assault")
m_Definition = "{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf"; if (historicalFixture != "self-epinephrine") m_Definition = "{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf";
#endif #endif
m_RuntimeDefinition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_Definition)); m_RuntimeDefinition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_Definition));
if (!m_RuntimeDefinition || !m_RuntimeDefinition.Valid()) { m_Failed = true; return ERR_PrepareResult.FAILED; } if (!m_RuntimeDefinition || !m_RuntimeDefinition.Valid()) { m_Failed = true; return ERR_PrepareResult.FAILED; }
@@ -72,7 +72,7 @@ class ERR_ScenarioComponent : ScriptComponent
// Existing deterministic probes intentionally use authored coordinates. // Existing deterministic probes intentionally use authored coordinates.
string fixture; string fixture;
if (!m_TerrainIndependent && System.GetCLIParam("errSmokeOutcome", fixture) && fixture != "normal-escape-route" && fixture != "crash-start" && fixture != "mounted-ammo" && fixture != "alarm-hearing" && fixture != "optional-site-skip" && fixture != "infantry-combat" && fixture != "site-garrisons" && fixture != "generated-sites" && fixture != "roadblocks" && fixture != "airfield-markers" && fixture != "air-recon" && fixture != "air-logistics" && fixture != "air-assault") if (!m_TerrainIndependent && System.GetCLIParam("errSmokeOutcome", fixture) && fixture != "normal-escape-route" && fixture != "crash-start" && fixture != "mounted-ammo" && fixture != "alarm-hearing" && fixture != "optional-site-skip" && fixture != "infantry-combat" && fixture != "site-garrisons" && fixture != "generated-sites" && fixture != "roadblocks" && fixture != "airfield-markers" && fixture != "air-recon" && fixture != "air-logistics" && fixture != "air-assault")
foreach (ERR_SiteDefinition fixedSite : m_RuntimeDefinition.Sites) fixedSite.RandomClear = false; if (fixture != "self-epinephrine") foreach (ERR_SiteDefinition fixedSite : m_RuntimeDefinition.Sites) fixedSite.RandomClear = false;
if (!ERR_ConfigurePatrolHQFixture(fixture)) { m_Failed = true; return ERR_PrepareResult.FAILED; } if (!ERR_ConfigurePatrolHQFixture(fixture)) { m_Failed = true; return ERR_PrepareResult.FAILED; }
#endif #endif
if (m_Progressive && !m_RuntimeDefinition.ConfigurePopulation(m_Definition, mode.Settings().Get("fieldCampCount"), mode.Settings().Get("fieldHospitalCount"))) if (m_Progressive && !m_RuntimeDefinition.ConfigurePopulation(m_Definition, mode.Settings().Get("fieldCampCount"), mode.Settings().Get("fieldHospitalCount")))
@@ -0,0 +1,85 @@
// Optional casualty assistance. The authoritative controller supplies the body;
// clients cannot nominate another patient, an item, or an enabled setting.
class ERR_SelfEpinephrineRules
{
static bool Allowed(bool active, bool enabled, bool currentBody, bool alive, bool unconscious)
{
return active && enabled && currentBody && alive && unconscious;
}
}
modded class SCR_PlayerController
{
[RplProp()] protected bool m_ERRSelfEpinephrineEnabled;
protected float m_ERRLastSelfEpinephrine = -10000;
override void ERR_UpdateObserverServer(int direction = 0)
{
super.ERR_UpdateObserverServer(direction);
if (!Replication.IsServer()) return;
ERR_GameMode mode = ERR_GameMode.Get();
bool enabled = mode && mode.Active() && mode.Settings().Get("selfEpinephrine") == 1;
if (enabled == m_ERRSelfEpinephrineEnabled) return;
m_ERRSelfEpinephrineEnabled = enabled; Replication.BumpMe();
}
override string ERR_SelfEpinephrineHint()
{
if (!m_ERRSelfEpinephrineEnabled || ERR_ObserverState() != ERR_ObserverReason.UNCONSCIOUS) return "";
return "Press Space to use an epinephrine injector from your inventory.";
}
override void ERR_UseSelfEpinephrine()
{
if (this != GetGame().GetPlayerController() || !m_ERRSelfEpinephrineEnabled || ERR_ObserverState() != ERR_ObserverReason.UNCONSCIOUS) return;
Rpc(ERR_RequestSelfEpinephrine);
}
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
protected void ERR_RequestSelfEpinephrine()
{
if (!Replication.IsServer()) return;
float now = GetGame().GetWorld().GetWorldTime();
if (now >= m_ERRLastSelfEpinephrine && now - m_ERRLastSelfEpinephrine < 1000) return;
m_ERRLastSelfEpinephrine = now;
string result = ERR_TrySelfEpinephrine();
if (!result.IsEmpty()) Rpc(ERR_SelfEpinephrineResponse, result);
}
protected string ERR_TrySelfEpinephrine()
{
if (!Replication.IsServer()) return "";
ERR_GameMode mode = ERR_GameMode.Get();
IEntity actor = GetControlledEntity();
CharacterControllerComponent controller;
if (actor) controller = CharacterControllerComponent.Cast(actor.FindComponent(CharacterControllerComponent));
ERR_PlayerLife life;
if (mode && mode.Players()) life = mode.Players().Get(GetPlayerId());
if (!mode || !ERR_SelfEpinephrineRules.Allowed(mode.Active(), mode.Settings().Get("selfEpinephrine") == 1,
life && life.Actor == actor && mode.Players().CurrentActor(actor) == life, ERR_Actors.Alive(actor), controller && controller.IsUnconscious())) return "";
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));
array<IEntity> items = {};
if (inventory) inventory.GetItems(items);
if (items.Count() > 512) return "Unable to inspect your medical supplies.";
// Prefer the standard injector; preserve each item's ordinary medical effect.
for (int kind = 0; kind < 2; kind++)
foreach (IEntity injector : items)
{
if (!injector || !inventory.Contains(injector)) continue;
SCR_ConsumableItemComponent item = SCR_ConsumableItemComponent.Cast(injector.FindComponent(SCR_ConsumableItemComponent));
if (!item) continue;
SCR_ConsumableMorphine effect = SCR_ConsumableMorphine.Cast(item.GetConsumableEffect());
if (!effect) continue;
if (kind == 0 && !ERR_ConsumableEpinephrine.Cast(effect)) continue;
if (kind == 1 && !ERR_ConsumableCombatEpinephrine.Cast(effect)) continue;
SCR_EConsumableFailReason reason;
if (!effect.CanApplyEffect(actor, actor, reason)) continue;
// An unconscious player cannot perform the standing injection animation.
// Use the native effect/consumption path on this exact carried item once.
item.ApplyItemEffect(actor, actor, effect.GetAnimationParameters(injector, actor), injector, effect.GetDeleteOnUse());
return "Epinephrine administered. Its normal medical effect is active.";
}
return "No usable epinephrine injector in your inventory, or treatment is already active.";
}
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
protected void ERR_SelfEpinephrineResponse(string message)
{
if (this != GetGame().GetPlayerController()) return;
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
if (hints) hints.ShowCustom(message, "Self epinephrine", 6);
}
}
@@ -9,7 +9,7 @@ class ERR_Settings
static array<string> Keys() static array<string> Keys()
{ {
return {"aiDoctrine", "aiShadow", "motorbikeCount", "planeAltitudeMetres", "planeReplacementSeconds", "planeSectorSeconds", "planeReconEnabled", "fieldCampCount", "fieldHospitalCount", "startWeaponCount", "startMagazineCount", "objectGuardCount", "objectGuardEvery", "objectGuardHQMode", "hunterSpawnDistanceMetres", "roadblockCount", "civilianTrafficCount", "startMode", "keepWeaponOnUnconscious", "restartOnEmpty", "emptyRestartHours", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "radioDefenceSeconds", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "antiAirChancePercent", "antiPersonnelMineChancePercent", "automaticEquipmentChancePercent", "automaticConvoyEnabled", "convoyCount", "heliTimerMinutes"}; return {"aiDoctrine", "aiShadow", "motorbikeCount", "planeAltitudeMetres", "planeReplacementSeconds", "planeSectorSeconds", "planeReconEnabled", "fieldCampCount", "fieldHospitalCount", "startWeaponCount", "startMagazineCount", "objectGuardCount", "objectGuardEvery", "objectGuardHQMode", "hunterSpawnDistanceMetres", "roadblockCount", "civilianTrafficCount", "startMode", "keepWeaponOnUnconscious", "selfEpinephrine", "restartOnEmpty", "emptyRestartHours", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "radioDefenceSeconds", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "antiAirChancePercent", "antiPersonnelMineChancePercent", "automaticEquipmentChancePercent", "automaticConvoyEnabled", "convoyCount", "heliTimerMinutes"};
} }
static bool Valid(string key, int value) static bool Valid(string key, int value)
@@ -31,7 +31,7 @@ class ERR_Settings
if (key == "roadblockCount") return value >= 0 && value <= 30; if (key == "roadblockCount") return value >= 0 && value <= 30;
if (key == "civilianTrafficCount") return value >= 0 && value <= 6; if (key == "civilianTrafficCount") return value >= 0 && value <= 6;
if (key == "startMode") return value == 0 || value == 1; if (key == "startMode") return value == 0 || value == 1;
if (key == "keepWeaponOnUnconscious" || key == "restartOnEmpty") return value == 0 || value == 1; if (key == "selfEpinephrine" || key == "keepWeaponOnUnconscious" || key == "restartOnEmpty") return value == 0 || value == 1;
if (key == "emptyRestartHours") return value >= 0 && value <= 168; if (key == "emptyRestartHours") return value >= 0 && value <= 168;
if (key == "heliTimerMinutes") return value >= 1 && value <= 1440; if (key == "heliTimerMinutes") return value >= 1 && value <= 1440;
if (key == "convoyCount") return value >= 0 && value <= 20; if (key == "convoyCount") return value >= 0 && value <= 20;
@@ -132,7 +132,7 @@ class ERR_Settings
if (key == "hunterSpawnDistanceMetres") return 1000; if (key == "hunterSpawnDistanceMetres") return 1000;
if (key == "roadblockCount") return 4; if (key == "roadblockCount") return 4;
if (key == "civilianTrafficCount") return 3; if (key == "civilianTrafficCount") return 3;
if (key == "startMode") return 0; if (key == "startMode" || key == "selfEpinephrine") return 0;
if (key == "keepWeaponOnUnconscious" || key == "restartOnEmpty") return 1; if (key == "keepWeaponOnUnconscious" || key == "restartOnEmpty") return 1;
if (key == "emptyRestartHours") return 5; if (key == "emptyRestartHours") return 5;
if (key == "heliTimerMinutes") return 15; if (key == "heliTimerMinutes") return 15;
@@ -169,6 +169,7 @@ class ERR_Settings
m_Values.Set("civilianTrafficCount", AddedDefault("civilianTrafficCount")); m_Values.Set("civilianTrafficCount", AddedDefault("civilianTrafficCount"));
m_Values.Set("startMode", AddedDefault("startMode")); m_Values.Set("startMode", AddedDefault("startMode"));
m_Values.Set("keepWeaponOnUnconscious", AddedDefault("keepWeaponOnUnconscious")); m_Values.Set("keepWeaponOnUnconscious", AddedDefault("keepWeaponOnUnconscious"));
m_Values.Set("selfEpinephrine", AddedDefault("selfEpinephrine"));
m_Values.Set("restartOnEmpty", AddedDefault("restartOnEmpty")); m_Values.Set("restartOnEmpty", AddedDefault("restartOnEmpty"));
m_Values.Set("emptyRestartHours", AddedDefault("emptyRestartHours")); m_Values.Set("emptyRestartHours", AddedDefault("emptyRestartHours"));
m_Values.Set("reportIntervalSeconds", 10); m_Values.Set("reportIntervalSeconds", 10);
@@ -0,0 +1,139 @@
#ifdef WORKBENCH
modded class SCR_PlayerController
{
string ERR_ProbeSelfEpiAttempt() { return ERR_TrySelfEpinephrine(); }
}
modded class ERR_AdminMenu
{
bool ERR_EpiProbeReady(int state) { return m_Ready && m_SelfEpiState == state; }
bool ERR_EpiProbeEnable()
{
SCR_ButtonTextComponent button = SCR_ButtonTextComponent.GetButtonText("SelfEpiOn", GetRootWidget());
if (!button || !button.IsEnabled()) return false;
OnButton(button); return true;
}
}
modded class ERR_GameMode
{
protected int m_ERRSelfTestStage, m_ERRSelfTestDeadline;
protected float m_ERRSelfTestWait;
protected IEntity m_ERRSelfTestBody, m_ERRSelfEpi, m_ERRSelfEpiSpare, m_ERRSelfCombatEpi;
protected SCR_CharacterDamageManagerComponent m_ERRSelfTestDamage;
protected override void EOnInit(IEntity owner)
{
super.EOnInit(owner);
string outcome;
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "self-epinephrine") return;
m_ERRSelfTestDeadline = System.GetTickCount() + 360000;
GetGame().GetCallqueue().CallLater(ERR_SelfTestProbeTick, 500, true);
OnStop().Insert(ERR_StopSelfEpiProbe);
}
protected void ERR_StopSelfEpiProbe() { GetGame().GetCallqueue().Remove(ERR_SelfTestProbeTick); }
protected bool ERR_SelfTestCheck(bool ok, string detail)
{
if (ok) { Print("ERR SMOKE PASS: self epinephrine " + detail); return true; }
Print("ERR SMOKE FAIL: self epinephrine " + detail, LogLevel.ERROR); ERR_StopSelfEpiProbe(); return false;
}
protected void ERR_SelfTestKnockout()
{
m_ERRSelfTestDamage.ForceUnconsciousness();
array<ref SCR_PersistentDamageEffect> effects = m_ERRSelfTestDamage.GetAllPersistentEffectsOfType(SCR_PassiveHitZoneRegenDamageEffect);
foreach (SCR_PersistentDamageEffect effect : effects) m_ERRSelfTestDamage.TerminateDamageEffect(effect);
m_ERRSelfTestDamage.SetGroupIsBeingHealed(ECharacterHitZoneGroup.RIGHTARM, true);
}
protected void ERR_SelfTestProbeTick()
{
if (System.GetTickCount() > m_ERRSelfTestDeadline || !PreparationError().IsEmpty()) { ERR_SelfTestCheck(false, "deadline or preparation error"); return; }
if (!Active()) return;
SCR_PlayerController pc = SCR_PlayerController.Cast(GetGame().GetPlayerController());
if (!pc || !pc.GetControlledEntity() || !Players().CurrentActor(pc.GetControlledEntity())) return;
float now = GetWorld().GetWorldTime();
if (now < m_ERRSelfTestWait) return;
if (m_ERRSelfTestStage == 0)
{
m_ERRSelfTestBody = pc.GetControlledEntity();
m_ERRSelfTestDamage = SCR_CharacterDamageManagerComponent.Cast(m_ERRSelfTestBody.FindComponent(SCR_CharacterDamageManagerComponent));
if (!ERR_SelfTestCheck(m_ERRSelfTestDamage && m_ERRSelfTestDamage.ERR_IsRevivablePlayer(), "admitted player has treatable body")) return;
ERR_SelfTestKnockout(); m_ERRSelfTestStage = 5; m_ERRSelfTestWait = now + 2000; return;
}
InventoryStorageManagerComponent playerInventory = InventoryStorageManagerComponent.Cast(m_ERRSelfTestBody.FindComponent(InventoryStorageManagerComponent));
if (m_ERRSelfTestStage == 5)
{
if (!ERR_SelfTestCheck(pc.ERR_ProbeCameraActive(), "knockout opens observer camera")) return;
if (!ERR_SelfTestCheck(Settings().Get("selfEpinephrine") == 0 && pc.ERR_SelfEpinephrineHint().IsEmpty(), "self epinephrine defaults off without a Space hint")) return;
m_ERRSelfEpi = ERR_Actors.Spawn(ERR_EnemyMedicalCatalog.EPI, m_ERRSelfTestBody.GetOrigin());
m_ERRSelfEpiSpare = ERR_Actors.Spawn(ERR_EnemyMedicalCatalog.EPI, m_ERRSelfTestBody.GetOrigin());
if (!ERR_SelfTestCheck(m_ERRSelfEpi && m_ERRSelfEpiSpare && playerInventory.TryInsertItem(m_ERRSelfEpi) && playerInventory.TryInsertItem(m_ERRSelfEpiSpare), "two finite standard injectors carried by casualty")) return;
if (!ERR_SelfTestCheck(pc.ERR_ProbeSelfEpiAttempt().IsEmpty() && m_ERRSelfEpi && m_ERRSelfEpiSpare, "authority refuses direct request while admin option disabled")) return;
ERR_AdminMenu.Open();
m_ERRSelfTestStage = 51; m_ERRSelfTestWait = now + 1500; return;
}
if (m_ERRSelfTestStage == 51)
{
ERR_AdminMenu menu = ERR_AdminMenu.GetOpenMenu();
if (!menu || !menu.ERR_EpiProbeReady(0)) return;
System.MakeScreenshot("$logs:err-self-epi-menu-disabled");
if (!ERR_SelfTestCheck(menu.ERR_EpiProbeEnable(), "real admin menu button submits setting request")) return;
m_ERRSelfTestStage = 52; m_ERRSelfTestWait = now + 1500; return;
}
if (m_ERRSelfTestStage == 52)
{
ERR_AdminMenu menu = ERR_AdminMenu.GetOpenMenu();
if (!menu || !menu.ERR_EpiProbeReady(1)) return;
if (!ERR_SelfTestCheck(Settings().Get("selfEpinephrine") == 1, "admin menu owner RPC saved enabled setting")) return;
System.MakeScreenshot("$logs:err-self-epi-menu-enabled"); menu.Close();
m_ERRSelfTestStage = 6; m_ERRSelfTestWait = now + 1500; return;
}
if (m_ERRSelfTestStage == 6)
{
if (!ERR_SelfTestCheck(!pc.ERR_SelfEpinephrineHint().IsEmpty() && GetGame().GetInputManager().IsActionActive("ERR_SelfEpinephrine"), "enabled hint and Space action active on existing casualty")) return;
ERR_FollowSurvivorCameraComponent follow = pc.ERR_ProbeFollowComponent();
follow.Input(false, false, false, false, true);
follow.Input(true, false, false, false, true);
follow.Input(true, false, false, false, true);
if (!ERR_SelfTestCheck(m_ERRSelfEpi && m_ERRSelfEpiSpare, "disabled/menu-held Space cannot inject on resuming input")) return;
follow.Input(true, false, false, false, false);
follow.Input(true, false, false, false, true);
follow.Input(true, false, false, false, true);
m_ERRSelfTestStage = 7; m_ERRSelfTestWait = now + 1500; return;
}
if (m_ERRSelfTestStage == 7)
{
if (!ERR_SelfTestCheck((m_ERRSelfEpi != null) != (m_ERRSelfEpiSpare != null), "Space owner request consumes exactly one native standard injector")) return;
if (!ERR_SelfTestCheck(!m_ERRSelfTestDamage.GetAllPersistentEffectsOfType(ERR_EpinephrineEffect).IsEmpty(), "actual standard epinephrine healing effect active on own casualty")) return;
pc.ERR_ProbeSelfEpiAttempt();
if (!ERR_SelfTestCheck((m_ERRSelfEpi != null) != (m_ERRSelfEpiSpare != null), "repeated request cannot consume spare while treatment already active")) return;
m_ERRSelfTestDamage.SetGroupIsBeingHealed(ECharacterHitZoneGroup.RIGHTARM, false);
m_ERRSelfTestStage = 8; m_ERRSelfTestWait = now + 30000; return;
}
if (m_ERRSelfTestStage == 8)
{
PrintFormat("ERR SELF EPI RECOVERY: blood=%1 resilience=%2 canFight=%3 observer=%4", m_ERRSelfTestDamage.GetBloodHitZone().GetHealthScaled(), m_ERRSelfTestDamage.GetResilienceHitZone().GetHealthScaled(), ERR_Actors.CanFight(m_ERRSelfTestBody), pc.ERR_HasObserverCamera());
if (!ERR_SelfTestCheck(ERR_Actors.CanFight(m_ERRSelfTestBody) && !pc.ERR_HasObserverCamera(), "native self injection wakes original body and restores its camera")) return;
if (!ERR_SelfTestCheck(pc.ERR_ProbeSelfEpiAttempt().IsEmpty(), "conscious player cannot use unconscious shortcut")) return;
if (m_ERRSelfEpi) ERR_Actors.DeleteSpawned(m_ERRSelfEpi);
if (m_ERRSelfEpiSpare) ERR_Actors.DeleteSpawned(m_ERRSelfEpiSpare);
m_ERRSelfTestDamage.FullHeal(); ERR_SelfTestKnockout();
m_ERRSelfCombatEpi = ERR_Actors.Spawn(ERR_EnemyMedicalCatalog.COMBAT_EPI, m_ERRSelfTestBody.GetOrigin());
if (!ERR_SelfTestCheck(m_ERRSelfCombatEpi && playerInventory.TryInsertItem(m_ERRSelfCombatEpi), "one carried combat injector supplied for second self-treatment")) return;
m_ERRSelfTestStage = 9; m_ERRSelfTestWait = now + 1500; return;
}
if (m_ERRSelfTestStage == 9)
{
if (!ERR_SelfTestCheck(Settings().Set("selfEpinephrine", 0) && pc.ERR_ProbeSelfEpiAttempt().IsEmpty() && m_ERRSelfCombatEpi, "live admin disable overrides stale client permission")) return;
if (!ERR_SelfTestCheck(Settings().Set("selfEpinephrine", 1), "self injection reenabled")) return;
string combatResult = pc.ERR_ProbeSelfEpiAttempt();
PrintFormat("ERR SELF EPI COMBAT REQUEST: result=%1 item=%2 canFight=%3 resilience=%4", combatResult, m_ERRSelfCombatEpi, ERR_Actors.CanFight(m_ERRSelfTestBody), m_ERRSelfTestDamage.GetResilienceHitZone().GetHealthScaled());
// Native unconscious/get-up transitions can outlast the instant medical effect.
m_ERRSelfTestStage = 10; m_ERRSelfTestWait = now + 15000; return;
}
PrintFormat("ERR SELF EPI COMBAT RESULT: item=%1 blood=%2 resilience=%3 canFight=%4", m_ERRSelfCombatEpi, m_ERRSelfTestDamage.GetBloodHitZone().GetHealthScaled(), m_ERRSelfTestDamage.GetResilienceHitZone().GetHealthScaled(), ERR_Actors.CanFight(m_ERRSelfTestBody));
if (!ERR_SelfTestCheck(!m_ERRSelfCombatEpi && ERR_Actors.CanFight(m_ERRSelfTestBody), "combat self injection consumed and restored consciousness through native effect")) return;
Settings().Set("selfEpinephrine", 0);
m_ERRSelfTestDamage.SetGroupIsBeingHealed(ECharacterHitZoneGroup.RIGHTARM, false);
m_ERRSelfTestDamage.FullHeal();
ERR_StopSelfEpiProbe(); Transition(ERR_Phase.DEFEAT);
Print("ERR SMOKE COMPLETE: self epinephrine menu, owner RPC, Space handler, finite native consumption and recovery; scripted local fixture, not physical key or multiplayer proof");
}
}
#endif
+3 -1
View File
@@ -46,7 +46,9 @@ TextWidgetClass { Name "KeepWeaponLabel" Slot FrameWidgetSlot { Anchor 0 0 0 0 P
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "KeepWeaponOn" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 268 OffsetTop 268 SizeX 140 OffsetRight -168 SizeY 32 OffsetBottom -300 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Enabled" } } } ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "KeepWeaponOn" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 268 OffsetTop 268 SizeX 140 OffsetRight -168 SizeY 32 OffsetBottom -300 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Enabled" } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "KeepWeaponOff" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 180 OffsetLeft 180 PositionY 268 OffsetTop 268 SizeX 152 OffsetRight -332 SizeY 32 OffsetBottom -300 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Disabled" } } } ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "KeepWeaponOff" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 180 OffsetLeft 180 PositionY 268 OffsetTop 268 SizeX 152 OffsetRight -332 SizeY 32 OffsetBottom -300 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Disabled" } } }
TextWidgetClass { Name "Saved_keepWeaponOnUnconscious" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 304 OffsetTop 304 SizeX 456 OffsetRight -484 SizeY 28 OffsetBottom -332 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } TextWidgetClass { Name "Saved_keepWeaponOnUnconscious" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 304 OffsetTop 304 SizeX 456 OffsetRight -484 SizeY 28 OffsetBottom -332 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "MissionHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 332 OffsetTop 332 SizeX 456 OffsetRight -484 SizeY 28 OffsetBottom -360 } Color 0.9 0.92 0.94 1 Text "Applies next knockout; existing casualties keep their rule." "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } TextWidgetClass { Name "SelfEpiLabel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 332 OffsetTop 332 SizeX 240 OffsetRight -268 SizeY 28 OffsetBottom -360 } Color 0.9 0.92 0.94 1 Text "Self epinephrine (Space)" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "SelfEpiOn" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 280 OffsetLeft 280 PositionY 332 OffsetTop 332 SizeX 90 OffsetRight -370 SizeY 28 OffsetBottom -360 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Enabled" } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "SelfEpiOff" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 382 OffsetLeft 382 PositionY 332 OffsetTop 332 SizeX 90 OffsetRight -472 SizeY 28 OffsetBottom -360 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Disabled" } } }
TextWidgetClass { Name "Label_civilianTrafficCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 244 OffsetTop 244 SizeX 456 OffsetRight -964 SizeY 22 OffsetBottom -266 } Color 0.9 0.92 0.94 1 Text "Civilian traffic (0-6 cars; 0 off)" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } TextWidgetClass { Name "Label_civilianTrafficCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 244 OffsetTop 244 SizeX 456 OffsetRight -964 SizeY 22 OffsetBottom -266 } Color 0.9 0.92 0.94 1 Text "Civilian traffic (0-6 cars; 0 off)" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "civilianTrafficCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 268 OffsetTop 268 SizeX 180 OffsetRight -688 SizeY 32 OffsetBottom -300 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } } ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "civilianTrafficCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 268 OffsetTop 268 SizeX 180 OffsetRight -688 SizeY 32 OffsetBottom -300 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_civilianTrafficCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 700 OffsetLeft 700 PositionY 268 OffsetTop 268 SizeX 100 OffsetRight -800 SizeY 32 OffsetBottom -300 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } } ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_civilianTrafficCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 700 OffsetLeft 700 PositionY 268 OffsetTop 268 SizeX 100 OffsetRight -800 SizeY 32 OffsetBottom -300 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } }
+6
View File
@@ -33,6 +33,12 @@ Native reconnect reservation and handoff remain separate from new-character admi
Enabled retains the actual held firearm, including its magazine and attachments, and selects it again after revival. It never creates a replacement or takes a gun acquired by another inventory. Disabled allows the normal ground drop. Grenades, gadgets and mounted weapons are excluded. The native test verifies inventory retention and the same rifle in hand after waking; it does not qualify the unconscious ragdoll's hand pose. Revival inside a vehicle leaves the gun in inventory rather than forcing an equip animation. Enabled retains the actual held firearm, including its magazine and attachments, and selects it again after revival. It never creates a replacement or takes a gun acquired by another inventory. Disabled allows the normal ground drop. Grenades, gadgets and mounted weapons are excluded. The native test verifies inventory retention and the same rifle in hand after waking; it does not qualify the unconscious ragdoll's hand pose. Revival inside a vehicle leaves the gun in inventory rather than forcing an equip animation.
## Optional self epinephrine
**Esc → Co10 Escape Reintroduced → Mission → Self epinephrine (Space)** lets administrators enable self-injection. It defaults to Disabled, including when loading older settings files. `/esc mission selfepi set 1` or `0` changes the same saved preference immediately, including for existing casualties.
When enabled, an unconscious prisoner can press and release **Space** to consume one epinephrine injector from their own inventory. Standard epinephrine is preferred when usable; combat epinephrine is also supported. Each retains its normal medical effect: standard epinephrine restores resilience over time and cannot replace lost blood or stop bleeding. No injector is supplied automatically, and a genuinely dead body cannot use this action. The casualty hint explains the shortcut while enabled. Keys held during knockout, chat or menus must be released before a fresh press can inject.
## Placement ## Placement
Prison placement searches a 4×4 lattice with axis coordinates -4.5, -1.5, 1.5 and 4.5m relative to the authored center. The four inner positions are checked first, then eight edge positions, then four corners. Neighboring centers are 3m apart and remain 1.5m inside walls positioned at ±6m. The inner and edge positions are within 5m of the prison center. Terrain height and slope, physical scene/character occupancy, and pending reservations can reject a slot. Connection IDs 1 and 13 receive no special or shared position. Disconnected physical bodies still obstruct the native query. Prison placement searches a 4×4 lattice with axis coordinates -4.5, -1.5, 1.5 and 4.5m relative to the authored center. The four inner positions are checked first, then eight edge positions, then four corners. Neighboring centers are 3m apart and remain 1.5m inside walls positioned at ±6m. The inner and edge positions are within 5m of the prison center. Terrain height and slope, physical scene/character occupancy, and pending reservations can reject a slot. Connection IDs 1 and 13 receive no special or shared position. Disconnected physical bodies still obstruct the native query.
+1 -1
View File
@@ -10,7 +10,7 @@ source = BASE / 'addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenu.c'
layout = BASE / 'addons/core/UI/layouts/ERR/AdminMenu.layout' layout = BASE / 'addons/core/UI/layouts/ERR/AdminMenu.layout'
keys = re.findall(r'"([A-Za-z]+)"', body(source, 'static array<string> Keys(')) keys = re.findall(r'"([A-Za-z]+)"', body(source, 'static array<string> Keys('))
settings = re.findall(r'"([A-Za-z]+)"', body(BASE / 'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c', 'static array<string> Keys(')) settings = re.findall(r'"([A-Za-z]+)"', body(BASE / 'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c', 'static array<string> Keys('))
assert len(keys) == len(set(keys)) and set(settings) - set(keys) == {"automaticConvoyEnabled", "keepWeaponOnUnconscious", "planeReconEnabled", "restartOnEmpty"} assert len(keys) == len(set(keys)) and set(settings) - set(keys) == {"automaticConvoyEnabled", "keepWeaponOnUnconscious", "selfEpinephrine", "planeReconEnabled", "restartOnEmpty"}
assert set(keys) <= set(settings) # Every menu field must have a server setting. assert set(keys) <= set(settings) # Every menu field must have a server setting.
# Native layouts place child widget declarations inside an anonymous Children # Native layouts place child widget declarations inside an anonymous Children
# block. A missing brace can preserve most widget names yet prevent menu load. # block. A missing brace can preserve most widget names yet prevent menu load.
+1
View File
@@ -56,6 +56,7 @@ struct Game{void*local=nullptr;int spawns=0;void*GetPlayerController(){return lo
SCR_ManualCamera*SpawnEntityPrefabLocal(Resource*,World*,EntitySpawnParams){spawns++;auto*c=new SCR_ManualCamera();c->previous=manager.current;manager.SetCamera(c);return c;}} game; SCR_ManualCamera*SpawnEntityPrefabLocal(Resource*,World*,EntitySpawnParams){spawns++;auto*c=new SCR_ManualCamera();c->previous=manager.current;manager.SetCamera(c);return c;}} game;
bool gameAvailable=true;Game*GetGame(){return gameAvailable?&game:nullptr;} bool gameAvailable=true;Game*GetGame(){return gameAvailable?&game:nullptr;}
struct Controller{ struct Controller{
string ERR_SelfEpinephrineHint(){return "";}
IEntity actor=nullptr,m_ERRRecoveryBody=nullptr;float m_ERRRecoveryDeadline=-1; IEntity actor=nullptr,m_ERRRecoveryBody=nullptr;float m_ERRRecoveryDeadline=-1;
bool m_ERRObserverMonitor=false,m_ERRObserverReady=false,m_ERRShownObserverAvailable=false; bool m_ERRObserverMonitor=false,m_ERRObserverReady=false,m_ERRShownObserverAvailable=false;
string m_ERRShownRecoveryStatus; string m_ERRShownRecoveryStatus;
+46
View File
@@ -0,0 +1,46 @@
"""Execute self-injection eligibility and authoritative request throttling."""
from policy_harness import ROOT, body, run
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_SelfEpinephrine.c'
rule=body(p,'static bool Allowed(')
request=body(p,'protected void ERR_RequestSelfEpinephrine(').replace('protected ','').replace('Replication.IsServer()', 'server').replace('GetGame().GetWorld().GetWorldTime()', 'this->now').replace('Rpc(ERR_SelfEpinephrineResponse, result)', 'Reply(result)')
run(r'''
#include <cassert>
#include <string>
struct string:std::string{using std::string::string;bool IsEmpty(){return empty();}};
'''+rule+r'''
struct Controller{bool server=true;float now=0,m_ERRLastSelfEpinephrine=-10000;int calls=0,replies=0;bool eligible=true;
string ERR_TrySelfEpinephrine(){calls++;return eligible?"used":"";}void Reply(string){replies++;}
'''+request+r'''
};
int main(){
for(int bits=0;bits<32;bits++)assert(Allowed(bits&1,bits&2,bits&4,bits&8,bits&16)==(bits==31));
Controller c;c.server=false;c.ERR_RequestSelfEpinephrine();assert(c.calls==0);
c.server=true;c.ERR_RequestSelfEpinephrine();assert(c.calls==1&&c.replies==1);
for(int i=0;i<100;i++){c.ERR_RequestSelfEpinephrine();}assert(c.calls==1);
c.now=999;c.ERR_RequestSelfEpinephrine();assert(c.calls==1);
c.now=1000;c.eligible=false;c.ERR_RequestSelfEpinephrine();assert(c.calls==2&&c.replies==1);
c.eligible=true;c.now=1999;c.ERR_RequestSelfEpinephrine();assert(c.calls==2);
c.now=2000;c.ERR_RequestSelfEpinephrine();assert(c.calls==3&&c.replies==2);
c.now=0;c.ERR_RequestSelfEpinephrine();assert(c.calls==4&&c.replies==3);
}
''')
source=body(p,'protected string ERR_TrySelfEpinephrine(')
assert 'if (!Replication.IsServer()) return "";' in source
assert 'GetControlledEntity()' in source and 'life.Actor == actor' in source and 'mode.Players().CurrentActor(actor) == life' in source
assert 'mode.Settings().Get("selfEpinephrine") == 1' in source
assert 'inventory.Contains(injector)' in source and 'items.Count() > 512' in source
assert source.index('effect.CanApplyEffect(') < source.index('item.ApplyItemEffect(')
assert 'effect.GetDeleteOnUse()' in source and 'Spawn(' not in source and 'SetHealth' not in source
settings=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c'
assert '"selfEpinephrine"' in body(settings,'static array<string> Keys(')
assert 'key == "selfEpinephrine"' in body(settings,'static bool Valid(')
assert 'if (key == "startMode" || key == "selfEpinephrine") return 0;' in body(settings,'static int AddedDefault(')
assert 'selfEpinephrine' not in body(settings,'static bool Randomizable(')
menu=(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenu.c').read_text()
assert 'ERR_AdminSubmit("mission selfepi set " + selfEpi, true)' in menu
assert 'Pair(GetRootWidget(), "SelfEpiOn", "SelfEpiOff", m_SelfEpiState)' in menu
assert 'button.SetEnabled(m_Ready && m_SelfEpiState == 0)' in menu
assert 'button.SetEnabled(m_Ready && m_SelfEpiState == 1)' in menu
config=(ROOT/'addons/core/Configs/ERR/ObserverInput.conf').read_text()
assert 'keyboard:KC_SPACE' in body(ROOT/'addons/core/Configs/ERR/ObserverInput.conf','Action ERR_SelfEpinephrine')
print('PASS: actual self-injection eligibility matrix and authority/rate limit; default-off persistence, authenticated menu route, owned finite injector and native effect/consumption boundaries')
+1 -1
View File
@@ -5,7 +5,7 @@ from policy_harness import ROOT,body,run
spec=importlib.util.spec_from_file_location('import_settings',BASE/'tools/import_settings.py') spec=importlib.util.spec_from_file_location('import_settings',BASE/'tools/import_settings.py')
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m) m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
result,deferred=m.merge({'configVersion':1,'reportIntervalSeconds':20.0,'supportSquadLimit':2,'responseRadiusMetres':1500,'convoyCount':3},{'aiIntelChancePercent':0,'intelRevealChancePercent':100}) result,deferred=m.merge({'configVersion':1,'reportIntervalSeconds':20.0,'supportSquadLimit':2,'responseRadiusMetres':1500,'convoyCount':3},{'aiIntelChancePercent':0,'intelRevealChancePercent':100})
assert result==dict(aiDoctrine=0,aiShadow=0,planeAltitudeMetres=250,planeReplacementSeconds=60,planeSectorSeconds=120,planeReconEnabled=1,roadblockCount=4,civilianTrafficCount=3,startMode=0,keepWeaponOnUnconscious=1,restartOnEmpty=1,emptyRestartHours=5,reportIntervalSeconds=20,supportSquadLimit=2,responseRadiusMetres=1500,aiIntelChancePercent=0,intelRevealChancePercent=100,radioDefenceSeconds=180,startTimeMinutes=840,timeMultiplier=1,startDateCode=0,weatherProfile=0,viewDistanceMetres=0,automaticEquipmentChancePercent=25,antiAirChancePercent=7,antiPersonnelMineChancePercent=60,automaticConvoyEnabled=1,convoyCount=3,heliTimerMinutes=15) assert result==dict(aiDoctrine=0,aiShadow=0,planeAltitudeMetres=250,planeReplacementSeconds=60,planeSectorSeconds=120,planeReconEnabled=1,roadblockCount=4,civilianTrafficCount=3,startMode=0,keepWeaponOnUnconscious=1,selfEpinephrine=0,restartOnEmpty=1,emptyRestartHours=5,reportIntervalSeconds=20,supportSquadLimit=2,responseRadiusMetres=1500,aiIntelChancePercent=0,intelRevealChancePercent=100,radioDefenceSeconds=180,startTimeMinutes=840,timeMultiplier=1,startDateCode=0,weatherProfile=0,viewDistanceMetres=0,automaticEquipmentChancePercent=25,antiAirChancePercent=7,antiPersonnelMineChancePercent=60,automaticConvoyEnabled=1,convoyCount=3,heliTimerMinutes=15)
assert deferred==[] assert deferred==[]
for count in (0,1,3,20): for count in (0,1,3,20):
values,notes=m.merge(ai={'convoyCount':count}) values,notes=m.merge(ai={'convoyCount':count})
+13 -4
View File
@@ -53,16 +53,25 @@ int main(){
c.ERR_UpdateRecoveryCamera(&mode);assert(!c.m_ERRRecoveryBody&&manager.sets==before); c.ERR_UpdateRecoveryCamera(&mode);assert(!c.m_ERRRecoveryBody&&manager.sets==before);
} }
''') ''')
assert 'm_InputResumeFrames = 2;' in body(p,'override bool EOnCameraInit(')
input_body=body(p,'void Input(').replace('SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());','SCR_PlayerController* controller = &pc;').replace('controller.','controller->').replace('ERR_ObserverRules.','ERR_ObserverRules::') input_body=body(p,'void Input(').replace('SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());','SCR_PlayerController* controller = &pc;').replace('controller.','controller->').replace('ERR_ObserverRules.','ERR_ObserverRules::')
run(r'''#include <cassert> run(r'''#include <cassert>
struct ERR_ObserverRules { struct ERR_ObserverRules {
'''+body(p,'static int Cycle(')+body(p,'static bool Toggle(')+r''' '''+body(p,'static int Cycle(')+body(p,'static bool Toggle(')+r'''
}; };
struct SCR_PlayerController {int calls=0,direction=0;void ERR_CycleObserver(int d){calls++;direction=d;}} pc; struct SCR_PlayerController {int calls=0,direction=0,epi=0;void ERR_UseSelfEpinephrine(){epi++;}void ERR_CycleObserver(int d){calls++;direction=d;}} pc;
struct Follow {bool m_LeftDown=false,m_RightDown=false,m_EnterDown=false,m_PlayerView=false;int m_InputResumeFrames=0; struct Follow {bool m_LeftDown=false,m_RightDown=false,m_EnterDown=false,m_PlayerView=false,m_SpaceDown=false;int m_InputResumeFrames=0;
'''+input_body+r''' '''+input_body+r'''
}; };
int main(){ int main(){
Follow initial;initial.m_InputResumeFrames=2;
initial.Input(true,false,false,false,true);initial.Input(true,false,false,false,true);assert(pc.epi==0);
initial.Input(true,false,false,false,false);initial.Input(true,false,false,false,true);assert(pc.epi==1);pc.epi=0;
Follow epi;
epi.Input(false,false,false,false,true); epi.Input(true,false,false,false,true); epi.Input(true,false,false,false,true);assert(pc.epi==0);
epi.Input(true,false,false,false,false);epi.Input(true,false,false,false,true);assert(pc.epi==1);
epi.Input(true,false,false,false,true);assert(pc.epi==1);
epi.Input(true,false,false,false,false);epi.Input(true,false,false,false,true);assert(pc.epi==2);
Follow f;f.Input(true,true,false,false);assert(pc.calls==1&&pc.direction==-1); Follow f;f.Input(true,true,false,false);assert(pc.calls==1&&pc.direction==-1);
f.Input(true,true,false,false);assert(pc.calls==1); f.Input(true,true,false,false);assert(pc.calls==1);
f.Input(true,false,false,false);f.Input(true,false,true,false);assert(pc.calls==2&&pc.direction==1); f.Input(true,false,false,false);f.Input(true,false,true,false);assert(pc.calls==2&&pc.direction==1);
@@ -179,8 +188,8 @@ ui_follow=r'''
struct ERR_ObserverRules { struct ERR_ObserverRules {
'''+body(p,'static int Cycle(')+body(p,'static bool Toggle(')+r''' '''+body(p,'static int Cycle(')+body(p,'static bool Toggle(')+r'''
}; };
struct SCR_PlayerController {int calls=0,direction=0;void ERR_CycleObserver(int d){calls++;direction=d;}} pc; struct SCR_PlayerController {int calls=0,direction=0,epi=0;void ERR_UseSelfEpinephrine(){epi++;}void ERR_CycleObserver(int d){calls++;direction=d;}} pc;
struct Follow {bool m_LeftDown=false,m_RightDown=false,m_EnterDown=false,m_PlayerView=false;int m_InputResumeFrames=0; struct Follow {bool m_LeftDown=false,m_RightDown=false,m_EnterDown=false,m_PlayerView=false,m_SpaceDown=false;int m_InputResumeFrames=0;
'''+input_body+r''' '''+input_body+r'''
}; };
''' '''
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -14,7 +14,7 @@ p.add_argument('--steam', type=Path, default=Path.home()/'.local/share/Steam')
p.add_argument('--external-addons', type=Path, help='Installed RHS/WCS/BMP source root; expose only audited closure in a separate Chat profile') p.add_argument('--external-addons', type=Path, help='Installed RHS/WCS/BMP source root; expose only audited closure in a separate Chat profile')
p.add_argument('--compat', type=Path, required=True, help='Disposable cloned Proton compatdata directory outside Steam') p.add_argument('--compat', type=Path, required=True, help='Disposable cloned Proton compatdata directory outside Steam')
p.add_argument('--profile', type=Path, required=True, help='Separate disposable successor Workbench profile') p.add_argument('--profile', type=Path, required=True, help='Separate disposable successor Workbench profile')
p.add_argument('--smoke', choices=['coordinated-ai', 'crash-start', 'crash-geometry', 'normal-escape-route', 'escape-route', 'optional-site-skip', 'infantry-combat', 'medical-combat', 'hunter-faction', 'unconscious-camera', 'objective-map', 'town-garrisons', 'site-garrisons', 'radio-interaction', 'enemy-loadout', 'aggression', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'roadblocks', 'hq-local-handoff', 'infantry-transfer', 'hq-body', 'convoy', 'supply-convoys', 'supply-travel', 'mounted-ammo', 'alarm-hearing', 'hq-memory', 'hq-awareness', 'aircraft-field', 'aircraft-route', 'native-end-active', 'native-end-preparing', 'aircraft-control', 'aircraft-cancel', 'aircraft-translate', 'map-ownership', 'mortar-refresh', 'mortar-salvo', 'mortar-courier', 'station-teardown', 'inventory-hitzone', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'mortar-expiry', 'mortar-transfer', 'mortar-fire', 'patrol-hq', 'victory', 'lost-helicopter', 'player-death', 'radio-network', 'hq-response', 'mortar-inventory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'], default='victory') p.add_argument('--smoke', choices=['coordinated-ai', 'crash-start', 'crash-geometry', 'normal-escape-route', 'escape-route', 'optional-site-skip', 'infantry-combat', 'medical-combat', 'hunter-faction', 'unconscious-camera', 'self-epinephrine', 'objective-map', 'town-garrisons', 'site-garrisons', 'radio-interaction', 'enemy-loadout', 'aggression', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'roadblocks', 'hq-local-handoff', 'infantry-transfer', 'hq-body', 'convoy', 'supply-convoys', 'supply-travel', 'mounted-ammo', 'alarm-hearing', 'hq-memory', 'hq-awareness', 'aircraft-field', 'aircraft-route', 'native-end-active', 'native-end-preparing', 'aircraft-control', 'aircraft-cancel', 'aircraft-translate', 'map-ownership', 'mortar-refresh', 'mortar-salvo', 'mortar-courier', 'station-teardown', 'inventory-hitzone', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'mortar-expiry', 'mortar-transfer', 'mortar-fire', 'patrol-hq', 'victory', 'lost-helicopter', 'player-death', 'radio-network', 'hq-response', 'mortar-inventory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'], default='victory')
p.add_argument('--trace-random-radius', action='store_true', help='Workbench-only read-only zero-radius caller traces; one normal Chat round, capped at sixteen records') p.add_argument('--trace-random-radius', action='store_true', help='Workbench-only read-only zero-radius caller traces; one normal Chat round, capped at sixteen records')
p.add_argument('--maintenance-refuel', action='store_true', help='Opt-in normal-route check: original held Southern fuel can transfers finite fuel through its native UAZ action; one arranged tank depletion, no stock creation') p.add_argument('--maintenance-refuel', action='store_true', help='Opt-in normal-route check: original held Southern fuel can transfers finite fuel through its native UAZ action; one arranged tank depletion, no stock creation')
p.add_argument('--mounted-swap-eligibility', action='store_true', help='Read-only mounted-ammo variant: one exact finite cargo/mounted magazine CanSwap call; no transfer or radio proof') p.add_argument('--mounted-swap-eligibility', action='store_true', help='Read-only mounted-ammo variant: one exact finite cargo/mounted magazine CanSwap call; no transfer or radio proof')
@@ -44,7 +44,7 @@ if a.smoke == 'crash-start' and (a.project != 'chat' or a.rounds != 1 or a.timeo
if a.smoke == 'crash-geometry' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 180): p.error('crash-geometry requires Chat, one round and timeout at least180 seconds') if a.smoke == 'crash-geometry' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 180): p.error('crash-geometry requires Chat, one round and timeout at least180 seconds')
if a.trace_random_radius and (a.smoke != 'normal-escape-route' or a.project != 'chat' or a.rounds != 1 or a.timeout < 420): p.error('--trace-random-radius requires Chat normal-escape-route, one round and timeout at least 420 seconds') if a.trace_random_radius and (a.smoke != 'normal-escape-route' or a.project != 'chat' or a.rounds != 1 or a.timeout < 420): p.error('--trace-random-radius requires Chat normal-escape-route, one round and timeout at least 420 seconds')
if a.smoke == 'objective-map' and (a.project != 'core' or a.rounds != 1): p.error('objective-map requires --project core --rounds 1') if a.smoke == 'objective-map' and (a.project != 'core' or a.rounds != 1): p.error('objective-map requires --project core --rounds 1')
if a.smoke == 'unconscious-camera' and a.project != 'core': p.error('unconscious-camera requires --project core to keep the general smoke fixture inactive') if a.smoke in {'unconscious-camera', 'self-epinephrine'} and a.project != 'core': p.error('unconscious-camera/self-epinephrine requires --project core to keep the general smoke fixture inactive')
if a.mine_stock: if a.mine_stock:
if a.smoke != 'mounted-ammo' or a.project != 'chat' or a.rounds != 1 or a.timeout < 360: if a.smoke != 'mounted-ammo' or a.project != 'chat' or a.rounds != 1 or a.timeout < 360:
p.error('--mine-stock requires --smoke mounted-ammo --project chat --rounds 1 and timeout at least 360 seconds') p.error('--mine-stock requires --smoke mounted-ammo --project chat --rounds 1 and timeout at least 360 seconds')
@@ -158,7 +158,7 @@ if a.recon_takeover_only: command += ['-errReconTakeoverOnly', '1']
if a.body_recovery_only: command += ['-errBodyRecoveryOnly', '1'] if a.body_recovery_only: command += ['-errBodyRecoveryOnly', '1']
if a.coordinated_blocked_only: command += ['-errCoordinatedBlockedOnly', '1'] if a.coordinated_blocked_only: command += ['-errCoordinatedBlockedOnly', '1']
if a.project == 'core': if a.project == 'core':
if a.smoke not in {'coordinated-ai', 'optional-site-skip', 'infantry-combat', 'medical-combat', 'hunter-faction', 'unconscious-camera', 'objective-map', 'town-garrisons', 'site-garrisons', 'radio-interaction', 'enemy-loadout', 'aggression', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'roadblocks', 'native-end-active', 'native-end-preparing', 'station-teardown', 'inventory-hitzone', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'victory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'}: p.error('Unsupported Core-only fixture') if a.smoke not in {'coordinated-ai', 'optional-site-skip', 'infantry-combat', 'medical-combat', 'hunter-faction', 'unconscious-camera', 'self-epinephrine', 'objective-map', 'town-garrisons', 'site-garrisons', 'radio-interaction', 'enemy-loadout', 'aggression', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'roadblocks', 'native-end-active', 'native-end-preparing', 'station-teardown', 'inventory-hitzone', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'victory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'}: p.error('Unsupported Core-only fixture')
command[command.index('-plugin=ERR_ValidateWorldPlugin')] = '-plugin=ERR_ValidateBaseWorldPlugin' command[command.index('-plugin=ERR_ValidateWorldPlugin')] = '-plugin=ERR_ValidateBaseWorldPlugin'
command[command.index('-errSmoke')] = '-errBaseSmoke' command[command.index('-errSmoke')] = '-errBaseSmoke'
if a.smoke in {'station-teardown', 'inventory-hitzone', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'} and a.project != 'core': p.error('This fixture requires --project core') if a.smoke in {'station-teardown', 'inventory-hitzone', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'} and a.project != 'core': p.error('This fixture requires --project core')
+1
View File
@@ -13,6 +13,7 @@ SCHEMA={
'civilianTrafficCount':(3,0,6), 'civilianTrafficCount':(3,0,6),
'startMode':(0,0,1), 'startMode':(0,0,1),
'keepWeaponOnUnconscious':(1,0,1), 'keepWeaponOnUnconscious':(1,0,1),
'selfEpinephrine':(0,0,1),
'restartOnEmpty':(1,0,1), 'restartOnEmpty':(1,0,1),
'emptyRestartHours':(5,0,168), 'emptyRestartHours':(5,0,168),
'reportIntervalSeconds':(10,5,60), 'reportIntervalSeconds':(10,5,60),