Add admin-controlled self epinephrine for unconscious players
This commit is contained in:
@@ -90,6 +90,11 @@ ActionManager : "{795184CF9AD764DB}Configs/System/chimeraInputCommon.conf" {
|
||||
}
|
||||
ActionContext ERR_ObserverContext : "{C538807EEE48BED8}Configs/System/ActionContext/ManualCameraContext.conf" {
|
||||
Actions {
|
||||
Action ERR_SelfEpinephrine {
|
||||
InputSource InputSourceValue "{A7B19F416E541020}" {
|
||||
Input "keyboard:KC_SPACE"
|
||||
}
|
||||
}
|
||||
Action ERR_ObserverPrevious {
|
||||
InputSource InputSourceValue "{A7B19F416E541001}" {
|
||||
Input "keyboard:KC_LEFT"
|
||||
|
||||
@@ -131,7 +131,7 @@ class ERR_AdminMenu : ChimeraMenuBase
|
||||
m_Controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
m_Mode = ERR_GameMode.Cast(GetGame().GetGameMode());
|
||||
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("Default_viewDistanceMetres");
|
||||
names.Insert("OpenForces");
|
||||
@@ -257,9 +257,11 @@ class ERR_AdminMenu : ChimeraMenuBase
|
||||
if (page == 2) m_LogPoll = 1;
|
||||
}
|
||||
protected int m_KeepWeaponState = -1;
|
||||
protected int m_SelfEpiState = -1;
|
||||
protected int m_RestartEmptyState = -1;
|
||||
protected void UpdateActions()
|
||||
{
|
||||
ERR_SelectionStyle.Pair(GetRootWidget(), "SelfEpiOn", "SelfEpiOff", m_SelfEpiState);
|
||||
ERR_SelectionStyle.Pair(GetRootWidget(), "KeepWeaponOn", "KeepWeaponOff", m_KeepWeaponState);
|
||||
ERR_SelectionStyle.Pair(GetRootWidget(), "RestartEmptyOn", "RestartEmptyOff", m_RestartEmptyState);
|
||||
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 == "RestartEmptyOn") button.SetEnabled(m_Ready && m_RestartEmptyState == 0);
|
||||
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 == "KeepWeaponOff") button.SetEnabled(m_Ready && m_KeepWeaponState == 1);
|
||||
}
|
||||
@@ -286,7 +290,7 @@ class ERR_AdminMenu : ChimeraMenuBase
|
||||
{
|
||||
if (!authorized) { Close(); 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)
|
||||
{
|
||||
if (entry == "supports=hq") { m_HQ = true; continue; }
|
||||
@@ -330,6 +334,11 @@ class ERR_AdminMenu : ChimeraMenuBase
|
||||
SetText("Saved_restartOnEmpty", "Saved: " + restartState);
|
||||
continue;
|
||||
}
|
||||
if (key == "selfEpinephrine")
|
||||
{
|
||||
m_SelfEpiState = entry.Substring(divider + 1, entry.Length() - divider - 1).ToInt();
|
||||
continue;
|
||||
}
|
||||
if (key == "keepWeaponOnUnconscious")
|
||||
{
|
||||
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_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")
|
||||
{
|
||||
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 == "traffic") return "civilianTrafficCount";
|
||||
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 == "restartempty") return "restartOnEmpty";
|
||||
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 == "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 == "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 == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent")
|
||||
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")
|
||||
{
|
||||
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 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.");
|
||||
|
||||
@@ -55,7 +55,7 @@ modded class ERR_GameMode
|
||||
string enabled;
|
||||
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return;
|
||||
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;
|
||||
ERR_NativeObjectiveProbe.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_ObserverPIPDisplay m_PIPDisplay = new ERR_ObserverPIPDisplay();
|
||||
protected bool m_InputContextActive;
|
||||
protected bool m_SpaceDown;
|
||||
bool InputContextActive() { return m_InputContextActive; }
|
||||
bool IsPlayerView() { return m_PlayerView; }
|
||||
// Process one edge per press; menus still consume the state so holding a key
|
||||
// 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
|
||||
// the reactivated context settle, then require release before accepting keys.
|
||||
@@ -139,16 +140,20 @@ class ERR_FollowSurvivorCameraComponent : SCR_BaseManualCameraComponent
|
||||
else if (m_InputResumeFrames > 0)
|
||||
{
|
||||
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());
|
||||
int cycle = ERR_ObserverRules.Cycle(enabled, left, right, m_LeftDown, m_RightDown);
|
||||
if (controller && cycle != 0) controller.ERR_CycleObserver(cycle);
|
||||
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;
|
||||
}
|
||||
override bool EOnCameraInit()
|
||||
{
|
||||
// Release a held jump key after knockout before accepting self-injection.
|
||||
m_InputResumeFrames = 2;
|
||||
m_OrbitFov = GetCameraEntity().GetVerticalFOV();
|
||||
m_OrbitNear = GetCameraEntity().GetNearPlane();
|
||||
return true;
|
||||
@@ -159,7 +164,7 @@ class ERR_FollowSurvivorCameraComponent : SCR_BaseManualCameraComponent
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
if (!controller || GetGame().GetCameraManager().CurrentCamera() != GetCameraEntity()) { m_PIPDisplay.Close(); m_ScopeDisplay.Close(); m_ViewHead.Restore(); return; }
|
||||
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();
|
||||
float 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 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; }
|
||||
vector ERR_ObserverAnchor() { return m_ERRObserverAnchor; }
|
||||
IEntity ERR_ObservedEntity()
|
||||
@@ -458,6 +466,8 @@ modded class SCR_PlayerController
|
||||
// whose local entity is still streaming. Never imply no survivors from delay.
|
||||
bool survivorAvailable = m_ERRObservedId != RplId.Invalid();
|
||||
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)
|
||||
{
|
||||
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
|
||||
|
||||
@@ -64,7 +64,7 @@ class ERR_ScenarioComponent : ScriptComponent
|
||||
#ifdef WORKBENCH
|
||||
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")
|
||||
m_Definition = "{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf";
|
||||
if (historicalFixture != "self-epinephrine") m_Definition = "{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf";
|
||||
#endif
|
||||
m_RuntimeDefinition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_Definition));
|
||||
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.
|
||||
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")
|
||||
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; }
|
||||
#endif
|
||||
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()
|
||||
{
|
||||
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)
|
||||
@@ -31,7 +31,7 @@ class ERR_Settings
|
||||
if (key == "roadblockCount") return value >= 0 && value <= 30;
|
||||
if (key == "civilianTrafficCount") return value >= 0 && value <= 6;
|
||||
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 == "heliTimerMinutes") return value >= 1 && value <= 1440;
|
||||
if (key == "convoyCount") return value >= 0 && value <= 20;
|
||||
@@ -132,7 +132,7 @@ class ERR_Settings
|
||||
if (key == "hunterSpawnDistanceMetres") return 1000;
|
||||
if (key == "roadblockCount") return 4;
|
||||
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 == "emptyRestartHours") return 5;
|
||||
if (key == "heliTimerMinutes") return 15;
|
||||
@@ -169,6 +169,7 @@ class ERR_Settings
|
||||
m_Values.Set("civilianTrafficCount", AddedDefault("civilianTrafficCount"));
|
||||
m_Values.Set("startMode", AddedDefault("startMode"));
|
||||
m_Values.Set("keepWeaponOnUnconscious", AddedDefault("keepWeaponOnUnconscious"));
|
||||
m_Values.Set("selfEpinephrine", AddedDefault("selfEpinephrine"));
|
||||
m_Values.Set("restartOnEmpty", AddedDefault("restartOnEmpty"));
|
||||
m_Values.Set("emptyRestartHours", AddedDefault("emptyRestartHours"));
|
||||
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
|
||||
@@ -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 "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 "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" } }
|
||||
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" } } }
|
||||
|
||||
@@ -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.
|
||||
|
||||
## 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
|
||||
|
||||
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.
|
||||
|
||||
@@ -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'
|
||||
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('))
|
||||
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.
|
||||
# Native layouts place child widget declarations inside an anonymous Children
|
||||
# block. A missing brace can preserve most widget names yet prevent menu load.
|
||||
|
||||
@@ -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;
|
||||
bool gameAvailable=true;Game*GetGame(){return gameAvailable?&game:nullptr;}
|
||||
struct Controller{
|
||||
string ERR_SelfEpinephrineHint(){return "";}
|
||||
IEntity actor=nullptr,m_ERRRecoveryBody=nullptr;float m_ERRRecoveryDeadline=-1;
|
||||
bool m_ERRObserverMonitor=false,m_ERRObserverReady=false,m_ERRShownObserverAvailable=false;
|
||||
string m_ERRShownRecoveryStatus;
|
||||
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
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})
|
||||
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==[]
|
||||
for count in (0,1,3,20):
|
||||
values,notes=m.merge(ai={'convoyCount':count})
|
||||
|
||||
@@ -53,16 +53,25 @@ int main(){
|
||||
c.ERR_UpdateRecoveryCamera(&mode);assert(!c.m_ERRRecoveryBody&&manager.sets==before);
|
||||
}
|
||||
''')
|
||||
assert 'm_InputResumeFrames = 2;' in body(p,'override bool EOnCameraInit(')
|
||||
input_body=body(p,'void Input(').replace('SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());','SCR_PlayerController* controller = &pc;').replace('controller.','controller->').replace('ERR_ObserverRules.','ERR_ObserverRules::')
|
||||
run(r'''#include <cassert>
|
||||
struct ERR_ObserverRules {
|
||||
'''+body(p,'static int Cycle(')+body(p,'static bool Toggle(')+r'''
|
||||
};
|
||||
struct SCR_PlayerController {int calls=0,direction=0;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 SCR_PlayerController {int calls=0,direction=0,epi=0;void ERR_UseSelfEpinephrine(){epi++;}void ERR_CycleObserver(int d){calls++;direction=d;}} pc;
|
||||
struct Follow {bool m_LeftDown=false,m_RightDown=false,m_EnterDown=false,m_PlayerView=false,m_SpaceDown=false;int m_InputResumeFrames=0;
|
||||
'''+input_body+r'''
|
||||
};
|
||||
int main(){
|
||||
Follow initial;initial.m_InputResumeFrames=2;
|
||||
initial.Input(true,false,false,false,true);initial.Input(true,false,false,false,true);assert(pc.epi==0);
|
||||
initial.Input(true,false,false,false,false);initial.Input(true,false,false,false,true);assert(pc.epi==1);pc.epi=0;
|
||||
Follow epi;
|
||||
epi.Input(false,false,false,false,true); epi.Input(true,false,false,false,true); epi.Input(true,false,false,false,true);assert(pc.epi==0);
|
||||
epi.Input(true,false,false,false,false);epi.Input(true,false,false,false,true);assert(pc.epi==1);
|
||||
epi.Input(true,false,false,false,true);assert(pc.epi==1);
|
||||
epi.Input(true,false,false,false,false);epi.Input(true,false,false,false,true);assert(pc.epi==2);
|
||||
Follow f;f.Input(true,true,false,false);assert(pc.calls==1&&pc.direction==-1);
|
||||
f.Input(true,true,false,false);assert(pc.calls==1);
|
||||
f.Input(true,false,false,false);f.Input(true,false,true,false);assert(pc.calls==2&&pc.direction==1);
|
||||
@@ -179,8 +188,8 @@ ui_follow=r'''
|
||||
struct ERR_ObserverRules {
|
||||
'''+body(p,'static int Cycle(')+body(p,'static bool Toggle(')+r'''
|
||||
};
|
||||
struct SCR_PlayerController {int calls=0,direction=0;void ERR_CycleObserver(int d){calls++;direction=d;}} pc;
|
||||
struct Follow {bool m_LeftDown=false,m_RightDown=false,m_EnterDown=false,m_PlayerView=false;int m_InputResumeFrames=0;
|
||||
struct SCR_PlayerController {int calls=0,direction=0,epi=0;void ERR_UseSelfEpinephrine(){epi++;}void ERR_CycleObserver(int d){calls++;direction=d;}} pc;
|
||||
struct Follow {bool m_LeftDown=false,m_RightDown=false,m_EnterDown=false,m_PlayerView=false,m_SpaceDown=false;int m_InputResumeFrames=0;
|
||||
'''+input_body+r'''
|
||||
};
|
||||
'''
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -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('--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('--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('--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')
|
||||
@@ -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.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 == '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.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')
|
||||
@@ -158,7 +158,7 @@ if a.recon_takeover_only: command += ['-errReconTakeoverOnly', '1']
|
||||
if a.body_recovery_only: command += ['-errBodyRecoveryOnly', '1']
|
||||
if a.coordinated_blocked_only: command += ['-errCoordinatedBlockedOnly', '1']
|
||||
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('-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')
|
||||
|
||||
@@ -13,6 +13,7 @@ SCHEMA={
|
||||
'civilianTrafficCount':(3,0,6),
|
||||
'startMode':(0,0,1),
|
||||
'keepWeaponOnUnconscious':(1,0,1),
|
||||
'selfEpinephrine':(0,0,1),
|
||||
'restartOnEmpty':(1,0,1),
|
||||
'emptyRestartHours':(5,0,168),
|
||||
'reportIntervalSeconds':(10,5,60),
|
||||
|
||||
Reference in New Issue
Block a user