Compare commits
2
Commits
7b13231f29
...
0a7102cd51
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a7102cd51 | ||
|
|
3b920e0715 |
@@ -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();
|
||||
|
||||
@@ -17,11 +17,79 @@ modded class ERR_Roadblocks
|
||||
m_Mode.Settings().Set("roadblockCount", 30);
|
||||
return super.Prepare(definition);
|
||||
}
|
||||
protected int m_ERRLayoutPosts;
|
||||
protected bool m_ERRLayoutFailed, m_ERRLayoutCaptured;
|
||||
protected SCR_ManualCamera m_ERRLayoutCamera;
|
||||
override protected void BuildPost()
|
||||
{
|
||||
string outcome;
|
||||
if (!System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "roadblocks") { super.BuildPost(); return; }
|
||||
// Inspect the actual spawned models before the transaction clears its refs.
|
||||
if (m_BuildHold)
|
||||
{
|
||||
bool valid = m_BuildCovers.Count() == 10 && m_BuildSpawned.Count() == 15;
|
||||
for (int i = 0; i < m_BuildCovers.Count() && valid; i++)
|
||||
{
|
||||
ERR_RoadblockProp prop = m_BuildCovers[i];
|
||||
IEntity entity = m_BuildSpawned[i + 4];
|
||||
vector low, high; entity.GetBounds(low, high);
|
||||
if (m_ERRLayoutPosts == 0) PrintFormat("ERR ROADBLOCK PROP NATIVE: prefab=%1 low=%2 high=%3", prop.Prefab, low, high);
|
||||
if (Math.Max(Math.AbsFloat(low[0]), Math.AbsFloat(high[0])) > prop.HalfWidth
|
||||
|| Math.Max(Math.AbsFloat(low[2]), Math.AbsFloat(high[2])) > prop.HalfDepth || high[1] > prop.Height) valid = false;
|
||||
// Every actual prop corner must stay outside the four-metre centre lane.
|
||||
for (int x = 0; x < 2; x++)
|
||||
for (int z = 0; z < 2; z++)
|
||||
{
|
||||
vector corner = low;
|
||||
if (x) corner[0] = high[0];
|
||||
if (z) corner[2] = high[2];
|
||||
vector delta = entity.CoordToParent(corner) - m_Centre;
|
||||
if (Math.AbsFloat(vector.Dot(delta, m_Right)) < 2) valid = false;
|
||||
}
|
||||
}
|
||||
if (!valid) { m_ERRLayoutFailed = true; Print("ERR SMOKE FAIL: roadblock prop geometry or centre lane", LogLevel.ERROR); }
|
||||
if (!m_ERRLayoutCaptured && m_Mode.Active())
|
||||
{
|
||||
m_ERRLayoutCaptured = true;
|
||||
EntitySpawnParams params = new EntitySpawnParams();
|
||||
m_ERRLayoutCamera = SCR_ManualCamera.Cast(GetGame().SpawnEntity(SCR_ManualCamera, GetGame().GetWorld(), params));
|
||||
if (m_ERRLayoutCamera)
|
||||
{
|
||||
vector camera[4];
|
||||
vector eye = m_Centre - m_Forward * 18 + m_Right * 12 + "0 13 0";
|
||||
Math3D.DirectionAndUpMatrix(vector.Direction(eye, m_Centre), "0 1 0", camera);
|
||||
camera[3] = eye; m_ERRLayoutCamera.SetWorldTransform(camera); m_ERRLayoutCamera.SetVerticalFOV(65);
|
||||
GetGame().GetCameraManager().SetCamera(m_ERRLayoutCamera);
|
||||
GetGame().GetCallqueue().CallLater(ERR_ProbeCaptureLayout, 3000, false);
|
||||
}
|
||||
}
|
||||
m_ERRLayoutPosts++;
|
||||
}
|
||||
super.BuildPost();
|
||||
}
|
||||
protected void ERR_ProbeCaptureLayout()
|
||||
{
|
||||
System.MakeScreenshot("$logs:err-roadblock-layout");
|
||||
GetGame().GetCallqueue().CallLater(ERR_ProbeCloseLayoutCamera, 1000, false);
|
||||
}
|
||||
protected void ERR_ProbeCloseLayoutCamera()
|
||||
{
|
||||
if (!m_ERRLayoutCamera) return;
|
||||
GetGame().GetCameraManager().SetCamera(GetGame().GetPlayerController().GetPlayerCamera());
|
||||
m_ERRLayoutCamera.Terminate(); delete m_ERRLayoutCamera; m_ERRLayoutCamera = null;
|
||||
}
|
||||
override void Stop()
|
||||
{
|
||||
GetGame().GetCallqueue().Remove(ERR_ProbeCaptureLayout);
|
||||
GetGame().GetCallqueue().Remove(ERR_ProbeCloseLayoutCamera);
|
||||
ERR_ProbeCloseLayoutCamera();
|
||||
super.Stop();
|
||||
}
|
||||
int ERR_ProbePostCount() { return m_Positions.Count(); }
|
||||
bool ERR_ProbeBackgroundPending() { return m_Background; }
|
||||
bool ERR_ProbeCompleteRoadblocks()
|
||||
{
|
||||
if (m_Target != 30 || m_Positions.Count() != 30 || m_Background || !m_Ready) return false;
|
||||
if (m_ERRLayoutFailed || m_ERRLayoutPosts < 30 || m_Target != 30 || m_Positions.Count() != 30 || m_Background || !m_Ready) return false;
|
||||
foreach (SCR_AIGroup group : m_Groups)
|
||||
{
|
||||
ERR_ForceRecord record = m_Mode.Forces().FindGroup(group);
|
||||
@@ -58,7 +126,7 @@ modded class ERR_GameMode
|
||||
super.EOnInit(owner);
|
||||
string outcome;
|
||||
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "roadblocks") return;
|
||||
m_ERRRoadblockDeadline = System.GetTickCount() + 600000;
|
||||
m_ERRRoadblockDeadline = System.GetTickCount() + 900000;
|
||||
GetGame().GetCallqueue().CallLater(ERR_RoadblockProbeTick, 500, true);
|
||||
OnStop().Insert(ERR_StopRoadblockProbe);
|
||||
}
|
||||
@@ -85,7 +153,7 @@ modded class ERR_GameMode
|
||||
}
|
||||
if (posts.ERR_ProbeBackgroundPending()) return;
|
||||
if (!posts.ERR_ProbeCompleteRoadblocks() || posts.ERR_ProbePostCount() <= m_ERRRoadblockInitial) { ERR_RoadblockProbeFail("30 physical four-guard posts, ledger and HQ admission"); return; }
|
||||
Print("ERR SMOKE PASS: roadblocks 30 physical posts / 120 guards; background completed after prison unlock, no extra posts on repeat tick");
|
||||
Print("ERR SMOKE PASS: roadblocks 30 physical posts / 120 guards / 300 checkpoint props; verified model footprints and centre lane; background completed after prison unlock, no extra posts on repeat tick");
|
||||
ERR_StopRoadblockProbe();
|
||||
Transition(ERR_Phase.DEFEAT);
|
||||
Print("ERR SMOKE COMPLETE: roadblocks background completion on Everon; scripted local fixture");
|
||||
|
||||
@@ -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
|
||||
@@ -22,6 +22,20 @@ class ERR_RoadblockRules
|
||||
}
|
||||
}
|
||||
|
||||
// A small, staged composition: each prop has its own heading and clearance box.
|
||||
class ERR_RoadblockProp
|
||||
{
|
||||
ResourceName Prefab;
|
||||
vector Position, Angles, Right, Forward;
|
||||
float HalfWidth, HalfDepth, Height;
|
||||
void ERR_RoadblockProp(ResourceName prefab, vector position, vector forward, float halfWidth, float halfDepth, float height)
|
||||
{
|
||||
Prefab = prefab; Position = position; Forward = forward;
|
||||
Right = Vector(forward[2], 0, -forward[0]); Angles = forward.VectorToAngles();
|
||||
HalfWidth = halfWidth; HalfDepth = halfDepth; Height = height;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_Roadblocks
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
@@ -39,11 +53,15 @@ class ERR_Roadblocks
|
||||
protected SCR_AIGroup m_BuildGroup;
|
||||
protected AIWaypoint m_BuildHold;
|
||||
protected ref array<IEntity> m_BuildSpawned = {};
|
||||
protected ref array<vector> m_BuildPositions = {}, m_BuildCovers = {};
|
||||
protected ref array<vector> m_BuildPositions = {};
|
||||
protected ref array<ref ERR_RoadblockProp> m_BuildCovers = {};
|
||||
protected int m_BuildActors, m_BuildCoverCount;
|
||||
protected ref array<ResourceName> m_Roster = {};
|
||||
protected ref array<ref Resource> m_Loaded = {};
|
||||
static const ResourceName COVER = "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et";
|
||||
static const ResourceName COVER = "{21D83950DF36F250}Prefabs/Props/Military/Sandbags/Sandbag_01_long_burlap.et";
|
||||
static const ResourceName BARRIER = "{64C9C1D1C2C1B38D}Prefabs/Props/Infrastructure/Roadblocks/RoadBlock_01.et";
|
||||
static const ResourceName OBSTACLE = "{A46D45922F8CAD6C}Prefabs/Props/Military/Fortification/CzechHedgehog_01_painted.et";
|
||||
static const ResourceName SIGN = "{C1977A326F2AA011}Prefabs/Structures/Signs/Military/SignCheckpoint_01_stop.et";
|
||||
void ERR_Roadblocks(ERR_GameMode mode) { m_Mode = mode; m_Mining = new ERR_RoadblockMining(mode); }
|
||||
void MineTick(array<ref ERR_HQUnit> units, array<IEntity> attempted, float now) { if (!m_Stopped) m_Mining.Tick(units, attempted, now); }
|
||||
bool Contains(SCR_AIGroup group) { return group && m_Groups.Contains(group); }
|
||||
@@ -72,7 +90,7 @@ class ERR_Roadblocks
|
||||
if (profile && profile.IsValid()) source = profile.GetResource().ToEntitySource();
|
||||
if (!source || !source.Get("m_aUnitPrefabSlots", m_Roster) || m_Roster.IsEmpty()) return false;
|
||||
// Preload before physical creation. A failed prefab never leaves half a post.
|
||||
array<ResourceName> resources = {COVER, "{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", ERR_Resources.DefendWaypoint()};
|
||||
array<ResourceName> resources = {COVER, BARRIER, OBSTACLE, SIGN, "{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", ERR_Resources.DefendWaypoint()};
|
||||
foreach (ResourceName actor : m_Roster) resources.Insert(actor);
|
||||
foreach (ResourceName prefab : resources)
|
||||
{
|
||||
@@ -119,8 +137,8 @@ class ERR_Roadblocks
|
||||
if (m_Forward.Length() < 0.1) return false;
|
||||
m_Forward.Normalize();
|
||||
m_Right = Vector(m_Forward[2], 0, -m_Forward[0]);
|
||||
// Keep all physical cover outside the travelled road. Even a narrow road has
|
||||
// a >=6m centre gap between these compact sandbag positions.
|
||||
// Protected guard positions sit on the shoulders; approach barriers narrow
|
||||
// wider roads while preserving a straight vehicle passage on every width.
|
||||
m_Shoulder = Math.Max(road.GetWidth() * 0.5 + 1.5, 4);
|
||||
m_Candidates++; m_CandidateStarted = System.GetTickCount();
|
||||
return true;
|
||||
@@ -135,32 +153,68 @@ class ERR_Roadblocks
|
||||
}
|
||||
return true;
|
||||
}
|
||||
protected bool CoverClear(vector point)
|
||||
protected bool CoverClear(ERR_RoadblockProp prop)
|
||||
{
|
||||
vector point = prop.Position;
|
||||
if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point)) return false;
|
||||
for (int x = -1; x <= 1; x++)
|
||||
for (int z = -1; z <= 1; z++)
|
||||
{
|
||||
vector sample = ERR_Actors.Ground(point + Vector(x * 1.5, 0, z * 1.5));
|
||||
vector sample = ERR_Actors.Ground(point + prop.Right * (x * prop.HalfWidth) + prop.Forward * (z * prop.HalfDepth));
|
||||
if (Math.AbsFloat(sample[1] - point[1]) > 0.3) return false;
|
||||
}
|
||||
// Planned props have separate local footprints. Exclude our own transaction
|
||||
// from the enclosing world-axis trace, which otherwise falsely overlaps
|
||||
// adjacent pieces at diagonal road headings. Still reject a moving guard.
|
||||
foreach (IEntity builtEntity : m_BuildSpawned)
|
||||
{
|
||||
if (!ChimeraCharacter.Cast(builtEntity)) continue;
|
||||
vector delta = builtEntity.GetOrigin() - point;
|
||||
if (Math.AbsFloat(vector.Dot(delta, prop.Right)) < prop.HalfWidth + 0.6
|
||||
&& Math.AbsFloat(vector.Dot(delta, prop.Forward)) < prop.HalfDepth + 0.6 && Math.AbsFloat(delta[1]) < 2) return false;
|
||||
}
|
||||
// Conservative world-axis enclosure of the rotated prop, including its feet.
|
||||
float extentX = Math.AbsFloat(prop.Right[0]) * prop.HalfWidth + Math.AbsFloat(prop.Forward[0]) * prop.HalfDepth;
|
||||
float extentZ = Math.AbsFloat(prop.Right[2]) * prop.HalfWidth + Math.AbsFloat(prop.Forward[2]) * prop.HalfDepth;
|
||||
TraceBox trace = new TraceBox(); trace.Start = point + "0 0.15 0"; trace.End = trace.Start + "0 0.01 0";
|
||||
trace.Mins = "-1.5 0 -1.5"; trace.Maxs = "1.5 2 1.5"; trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
|
||||
trace.Mins = Vector(-extentX, 0, -extentZ); trace.Maxs = Vector(extentX, prop.Height, extentZ); trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
|
||||
trace.ExcludeArray = m_BuildSpawned;
|
||||
return GetGame().GetWorld().TraceMove(trace, null) == 1;
|
||||
}
|
||||
protected void Layout(out array<ref ERR_RoadblockProp> covers)
|
||||
{
|
||||
covers.Clear();
|
||||
// Footprints enclose the native model bounds, including small placement margins.
|
||||
for (int side = -1; side <= 1; side += 2)
|
||||
{
|
||||
// Two long sandbag walls shelter each pair from the road and its approach.
|
||||
AddProp(covers, COVER, side * m_Shoulder, 0, m_Right, 1.9, 0.7, 1);
|
||||
AddProp(covers, COVER, side * (m_Shoulder + 2.8), side * 2.8, m_Forward, 1.9, 0.7, 1);
|
||||
// Opposing, staggered stop lines: the inner ends leave a >=4m centre lane.
|
||||
AddProp(covers, BARRIER, side * 3.3, side * 8, m_Forward, 1.2, 0.4, 1.3);
|
||||
AddProp(covers, OBSTACLE, side * (m_Shoulder + 4), side * 8, m_Forward, 1, 1, 1.6);
|
||||
AddProp(covers, SIGN, side * 3, side * 12, m_Forward * (side * 1.0), 0.45, 0.4, 0.8);
|
||||
}
|
||||
}
|
||||
protected void AddProp(array<ref ERR_RoadblockProp> covers, ResourceName prefab, float across, float along, vector forward, float halfWidth, float halfDepth, float height)
|
||||
{
|
||||
vector point = ERR_Actors.Ground(m_Centre + m_Right * across + m_Forward * along);
|
||||
covers.Insert(new ERR_RoadblockProp(prefab, point, forward, halfWidth, halfDepth, height));
|
||||
}
|
||||
protected void Rollback(array<IEntity> spawned, SCR_AIGroup group)
|
||||
{
|
||||
foreach (IEntity entity : spawned) ERR_Actors.DeleteSpawned(entity);
|
||||
ERR_Actors.DeleteSpawned(group);
|
||||
}
|
||||
// Start a transaction; physical pieces are created separately in BuildPost.
|
||||
protected bool SpawnPost(array<vector> positions, array<vector> covers)
|
||||
protected bool SpawnPost(array<vector> positions, array<ref ERR_RoadblockProp> covers)
|
||||
{
|
||||
if (!PlayersClear() || m_Mode.Forces().Count() >= 250 || m_BuildGroup || positions.Count() != 4 || covers.Count() != 2) return false;
|
||||
if (!PlayersClear() || m_Mode.Forces().Count() >= 250 || m_BuildGroup || positions.Count() != 4 || covers.Count() != 10) return false;
|
||||
m_BuildGroup = SCR_AIGroup.Cast(ERR_Actors.Spawn("{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", m_Centre));
|
||||
if (!m_BuildGroup) return false;
|
||||
m_BuildGroup.SetFaction(m_Mode.EnemyFaction()); m_BuildGroup.SetSpawnImmediately(false); m_BuildGroup.SetDeleteWhenEmpty(false); m_BuildGroup.SetPermanentLOD(0);
|
||||
m_BuildPositions.Copy(positions); m_BuildCovers.Copy(covers);
|
||||
m_BuildPositions.Copy(positions); m_BuildCovers.Clear();
|
||||
foreach (ERR_RoadblockProp plannedProp : covers) m_BuildCovers.Insert(plannedProp);
|
||||
m_BuildActors = 0; m_BuildCoverCount = 0;
|
||||
return true;
|
||||
}
|
||||
@@ -188,9 +242,9 @@ class ERR_Roadblocks
|
||||
}
|
||||
if (m_BuildCoverCount < m_BuildCovers.Count())
|
||||
{
|
||||
vector coverPosition = m_BuildCovers[m_BuildCoverCount];
|
||||
if (!CoverClear(coverPosition)) { CancelPost(); return; }
|
||||
IEntity cover = ERR_Actors.Spawn(COVER, coverPosition, m_Forward.VectorToAngles());
|
||||
ERR_RoadblockProp prop = m_BuildCovers[m_BuildCoverCount];
|
||||
if (!CoverClear(prop)) { CancelPost(); return; }
|
||||
IEntity cover = ERR_Actors.Spawn(prop.Prefab, prop.Position, prop.Angles);
|
||||
if (!cover) { CancelPost(); return; }
|
||||
m_BuildSpawned.Insert(cover); m_BuildCoverCount++;
|
||||
return;
|
||||
@@ -210,26 +264,26 @@ class ERR_Roadblocks
|
||||
m_BuildGroup.AddWaypoint(m_BuildHold);
|
||||
m_Mode.RegisterGroup(m_BuildGroup); m_BuildGroup.ActivateAI();
|
||||
m_Mining.Register(m_BuildGroup, m_BuildHold, m_Right);
|
||||
PrintFormat("Reintroduced [roadblocks]: %1 at %2 guards=4 roadPassage=open HQ=registered", id, m_Centre);
|
||||
PrintFormat("Reintroduced [roadblocks]: %1 at %2 guards=4 props=10 roadPassage=open HQ=registered", id, m_Centre);
|
||||
m_BuildSpawned.Clear(); m_BuildGroup = null; m_BuildHold = null;
|
||||
m_BuildPositions.Clear(); m_BuildCovers.Clear();
|
||||
m_NextSearch = System.GetTickCount() + 2000;
|
||||
}
|
||||
// 0: reject, 1: awaiting native tile, 2: ready.
|
||||
protected int Plan(out array<vector> positions, out array<vector> covers)
|
||||
protected int Plan(out array<vector> positions, out array<ref ERR_RoadblockProp> covers)
|
||||
{
|
||||
positions.Clear(); covers.Clear();
|
||||
NavmeshWorldComponent mesh = GetGame().GetAIWorld().GetNavmeshWorldComponent("Soldiers");
|
||||
if (!mesh || !PlayersClear()) return 0;
|
||||
Layout(covers);
|
||||
foreach (ERR_RoadblockProp prop : covers)
|
||||
if (!CoverClear(prop)) return 0;
|
||||
map<int, vector> reserved = new map<int, vector>();
|
||||
for (int side = -1; side <= 1; side += 2)
|
||||
{
|
||||
vector cover = ERR_Actors.Ground(m_Centre + m_Right * (side * m_Shoulder));
|
||||
if (!CoverClear(cover)) return 0;
|
||||
covers.Insert(cover);
|
||||
for (int row = -1; row <= 1; row += 2)
|
||||
{
|
||||
vector point = ERR_Actors.Ground(m_Centre + m_Right * (side * (m_Shoulder + 3)) + m_Forward * (row * 3));
|
||||
vector point = ERR_Actors.Ground(m_Centre + m_Right * (side * (m_Shoulder + 2.5)) + m_Forward * (side * (row - 1.5)));
|
||||
if (!mesh.IsTileLoaded(point)) { mesh.LoadTileIn(point); return 1; }
|
||||
vector reachable;
|
||||
if (!mesh.GetReachablePoint(point, 1, reachable) || vector.DistanceXZ(point, reachable) > 1 || !m_Placement.Clear(point, reserved)) return 0;
|
||||
@@ -260,7 +314,7 @@ class ERR_Roadblocks
|
||||
{
|
||||
m_Ready = true; m_Background = true; m_Definition = definition;
|
||||
m_NextReport = System.GetTickCount() + 60000;
|
||||
GetGame().GetCallqueue().CallLater(BackgroundStep, 500, true);
|
||||
GetGame().GetCallqueue().CallLater(BackgroundStep, 200, true);
|
||||
ERR_HQ.Event("ROADBLOCKS", Status() + " Initial search yielded; remaining posts will continue in background.");
|
||||
return ERR_PrepareResult.READY;
|
||||
}
|
||||
@@ -285,7 +339,8 @@ class ERR_Roadblocks
|
||||
}
|
||||
return;
|
||||
}
|
||||
array<vector> positions = {}, covers = {};
|
||||
array<vector> positions = {};
|
||||
array<ref ERR_RoadblockProp> covers = {};
|
||||
int plan = Plan(positions, covers);
|
||||
if (plan == 1 && System.GetTickCount() - m_CandidateStarted < 2000) return;
|
||||
if (plan == 2)
|
||||
@@ -318,7 +373,7 @@ class ERR_Roadblocks
|
||||
if (now >= m_NextReport)
|
||||
{
|
||||
m_NextReport = now + 60000;
|
||||
PrintFormat("Reintroduced [roadblocks background]: placed=%1/%2 attempts=%3 steps=%4 workMs=%5 maxStepMs=%6; still searching safe locations", m_Positions.Count(), m_Target, m_Attempts, m_BackgroundSteps, m_BackgroundWork, m_MaxBackgroundStep);
|
||||
PrintFormat("Reintroduced [roadblocks background]: placed=%1/%2 attempts=%3 steps=%4 workMs=%5 maxStepMs=%6 candidates=%7 plans=%8; still searching safe locations", m_Positions.Count(), m_Target, m_Attempts, m_BackgroundSteps, m_BackgroundWork, m_MaxBackgroundStep, m_Candidates, m_Planned);
|
||||
}
|
||||
}
|
||||
void Stop()
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -110,7 +110,10 @@
|
||||
"Prefabs/MP/ScriptedChatEntity.et": "F69BC912AC8236F9",
|
||||
"Prefabs/Props/EmptyStorages/USSR_AmmoBox_empty.et": "EBBC859CCD94D96D",
|
||||
"Prefabs/Props/Furniture/Table_01.et": "53506DCB398C7EBE",
|
||||
"Prefabs/Props/Infrastructure/Roadblocks/RoadBlock_01.et": "64C9C1D1C2C1B38D",
|
||||
"Prefabs/Props/Military/Antennas/Antenna_FIA_02/Antenna_FIA_02_base.et": "B3949F6D215A9F78",
|
||||
"Prefabs/Props/Military/Fortification/CzechHedgehog_01_painted.et": "A46D45922F8CAD6C",
|
||||
"Prefabs/Props/Military/Sandbags/Sandbag_01_long_burlap.et": "21D83950DF36F250",
|
||||
"Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et": "CFFEB23FBEA00322",
|
||||
"Prefabs/Props/Wrecks/UH1H_wreck/UH1H_wreck_static.et": "74DCE1AEC6832E18",
|
||||
"Prefabs/Structures/BuildingParts/Doors/DoorSet_Base.et": "87CADB5ADC989993",
|
||||
@@ -119,6 +122,7 @@
|
||||
"Prefabs/Structures/Military/Camps/TentSmallUS_01/TentSmallUS_01.et": "01AE5FD77A9A4C21",
|
||||
"Prefabs/Structures/Military/Camps/TentUSSR_01/TentUSSR_01_base.et": "BE5D5CFA90224646",
|
||||
"Prefabs/Structures/Military/Camps/TentUS_01/TentUSMedical_01.et": "F8427C3EFC52644E",
|
||||
"Prefabs/Structures/Signs/Military/SignCheckpoint_01_stop.et": "C1977A326F2AA011",
|
||||
"Prefabs/Structures/Walls/BarbedWire/BarbedTape_01/BarbedTape_01_Long_noStake.et": "46904AF5D473FB75",
|
||||
"Prefabs/Structures/Walls/BarbedWire/BarbedTape_01/BarbedTape_01_Short_noStake.et": "CAE1588B6265A2AB",
|
||||
"Prefabs/Structures/Walls/Concrete/ConcreteWall_01/ConcreteWall_01_3m.et": "60E5729BEA18F875",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -5,8 +5,8 @@ p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_Roadblocks.c'
|
||||
s='\n'.join(body(p,signature) for signature in ('protected ERR_PrepareResult Finish(', 'ERR_PrepareResult Prepare(', 'protected void SearchStep(', 'protected void BackgroundStep()', 'void Stop()'))
|
||||
s=re.sub(r'#ifdef WORKBENCH.*?#endif','',s,flags=re.S)
|
||||
s=s.replace('protected ', '').replace('ERR_ScenarioDefinition definition', 'Definition* definition')
|
||||
s=s.replace('array<vector> positions = {}, covers = {};', 'Array<int> positions, covers;')
|
||||
s=s.replace('GetGame().GetCallqueue().CallLater(BackgroundStep, 500, true)', 'queue.Schedule(500)').replace('GetGame().GetCallqueue().Remove(BackgroundStep)', 'queue.Remove()')
|
||||
s=s.replace('array<vector> positions = {};', 'Array<int> positions;').replace('array<ref ERR_RoadblockProp> covers = {};', 'Array<int> covers;')
|
||||
s=s.replace('GetGame().GetCallqueue().CallLater(BackgroundStep, 200, true)', 'queue.Schedule(200)').replace('GetGame().GetCallqueue().Remove(BackgroundStep)', 'queue.Remove()')
|
||||
s=s.replace('GetGame().GetWorldEntity().GetWorldBounds(m_Low, m_High)', 'Bounds(m_Low, m_High)').replace('GetGame().GetPlayerManager().GetPlayerCount()', 'players')
|
||||
s=s.replace('m_Mode.Settings().Get("roadblockCount")', 'm_Mode->target').replace('m_Mode.Forces().Count()', 'm_Mode->ledger')
|
||||
s=s.replace('m_Mode.', 'm_Mode->').replace('System.GetTickCount()', 'Now()').replace('Replication.IsServer()', 'server').replace('ERR_GameMode.Get()', 'current')
|
||||
@@ -27,7 +27,7 @@ int clockMs=0,players=1;bool server=true;
|
||||
int Now(){return clockMs;}bool GetGame(){return true;}void Bounds(int&,int&){}bool ValidBounds(int,int){return true;}
|
||||
template<class...T>void PrintFormat(T...){}void Event(string,string){}
|
||||
template<class T>struct Array:std::vector<T>{int Count(){return this->size();}void Clear(){this->clear();}};
|
||||
struct Queue{int scheduled=0,removed=0;bool active=false;void Schedule(int ms){assert(ms==500);scheduled++;active=true;}void Remove(){removed++;active=false;}};
|
||||
struct Queue{int scheduled=0,removed=0;bool active=false;void Schedule(int ms){assert(ms==200);scheduled++;active=true;}void Remove(){removed++;active=false;}};
|
||||
struct Mode{int target=30,ledger=0,phase=Phase::PRISON;bool active=true;int Phase(){return phase;}bool Active(){return active;}};Mode* current=nullptr;
|
||||
struct Definition{};
|
||||
struct Mining{bool stopped=false;void Stop(){stopped=true;}};
|
||||
@@ -99,9 +99,10 @@ print('PASS: actual roadblock scheduler yields initial deadline/attempt cap to b
|
||||
|
||||
# Run actual staged physical creation and rollback, with native entities doubled.
|
||||
s='\n'.join(body(p,sig) for sig in ('protected bool SpawnPost(', 'protected void CancelPost()', 'protected void BuildPost()'))
|
||||
s=s.replace('protected ', '').replace('array<vector> positions','Array<int>& positions').replace('array<vector> covers','Array<int>& covers')
|
||||
s=s.replace('protected ', '').replace('array<vector> positions','Array<int>& positions').replace('array<ref ERR_RoadblockProp> covers','Array<Prop>& covers').replace('ERR_RoadblockProp prop', 'Prop prop')
|
||||
s=s.replace('SCR_AIGroup.Cast','Node::Cast').replace('AIWaypoint.Cast','Node::Cast').replace('IEntity actor','Node* actor').replace('IEntity cover','Node* cover')
|
||||
s=s.replace('foreach (IEntity ownedEntity : m_BuildSpawned)','for (Node* ownedEntity : m_BuildSpawned)').replace('vector point','int point').replace('vector coverPosition','int coverPosition')
|
||||
s=s.replace('foreach (ERR_RoadblockProp plannedProp : covers)', 'for (Prop plannedProp : covers)')
|
||||
s=s.replace('m_Forward.VectorToAngles()', '0').replace('m_Positions.Count().ToString()', 'std::to_string(m_Positions.Count())')
|
||||
s=s.replace('GetGame().GetWorld().GetWorldTime()', 'Now()').replace('System.GetTickCount()', 'Now()').replace('null','nullptr')
|
||||
s=s.replace('m_BuildGroup.', 'm_BuildGroup->').replace('m_BuildHold.', 'm_BuildHold->')
|
||||
@@ -122,31 +123,32 @@ struct Ledger{int count=0,registered=0;bool accept=true;int Count(){return count
|
||||
struct Mode{Ledger ledger;int owned=0,groups=0;Ledger& Forces(){return ledger;}int EnemyFaction(){return 1;}void Own(Node*n){assert(n);owned++;}void RegisterGroup(Node*){groups++;}};
|
||||
struct Placement{bool clear=true;bool Clear(int,void*){return clear;}};
|
||||
struct Mining{int registered=0;void Register(Node*,Node*,int){registered++;}};
|
||||
struct Prop{string Prefab="cover";int Position=5,Angles=0;};
|
||||
struct Build{
|
||||
Mode m_Mode;Placement m_Placement;Mining m_Mining;Node* m_BuildGroup=nullptr;Node* m_BuildHold=nullptr;
|
||||
Array<int> m_BuildPositions,m_BuildCovers,m_Positions;Array<Node*>m_BuildSpawned,m_Groups;Array<string>m_Roster={"one","two"};
|
||||
Array<int> m_BuildPositions,m_Positions;Array<Prop>m_BuildCovers;Array<Node*>m_BuildSpawned,m_Groups;Array<string>m_Roster={"one","two"};
|
||||
int m_BuildActors=0,m_BuildCoverCount=0,m_Centre=10,m_Right=0,m_NextSearch=0,rolledBack=0;string COVER="cover";bool playersClear=true,coversClear=true;
|
||||
bool PlayersClear(){return playersClear;}bool CoverClear(int){return coversClear;}
|
||||
bool PlayersClear(){return playersClear;}bool CoverClear(Prop){return coversClear;}
|
||||
void Rollback(Array<Node*>&entities,Node*group){rolledBack+=entities.Count()+(group?1:0);}
|
||||
'''+s+r'''
|
||||
};
|
||||
int main(){Array<int> positions={1,2,3,4},covers={5,6};
|
||||
int main(){Array<int> positions={1,2,3,4};Array<Prop>covers(10);
|
||||
{Build b;int start=ERR_Actors::calls;assert(b.SpawnPost(positions,covers));assert(ERR_Actors::calls==start+1);
|
||||
for(int piece=0;piece<7;piece++){int calls=ERR_Actors::calls;b.BuildPost();assert(ERR_Actors::calls==calls+1&&b.m_Positions.Count()==0&&b.m_Mode.owned==0);}
|
||||
b.BuildPost();assert(!b.m_BuildGroup&&b.m_Positions.Count()==1&&b.m_Groups.Count()==1&&b.m_Mode.owned==8&&b.m_Mode.groups==1&&b.m_Mining.registered==1&&b.m_NextSearch==2100);
|
||||
for(int piece=0;piece<15;piece++){int calls=ERR_Actors::calls;b.BuildPost();assert(ERR_Actors::calls==calls+1&&b.m_Positions.Count()==0&&b.m_Mode.owned==0);}
|
||||
b.BuildPost();assert(!b.m_BuildGroup&&b.m_Positions.Count()==1&&b.m_Groups.Count()==1&&b.m_Mode.owned==16&&b.m_Mode.groups==1&&b.m_Mining.registered==1&&b.m_NextSearch==2100);
|
||||
}
|
||||
// Player approaches at any stage: no registered post and every owned piece removed.
|
||||
for(int stage=0;stage<=7;stage++){Build b;assert(b.SpawnPost(positions,covers));for(int i=0;i<stage;i++)b.BuildPost();b.playersClear=false;b.BuildPost();assert(!b.m_BuildGroup&&b.m_Positions.Count()==0&&b.m_Mode.owned==0&&b.rolledBack==stage+1);}
|
||||
for(int stage=0;stage<=15;stage++){Build b;assert(b.SpawnPost(positions,covers));for(int i=0;i<stage;i++)b.BuildPost();b.playersClear=false;b.BuildPost();assert(!b.m_BuildGroup&&b.m_Positions.Count()==0&&b.m_Mode.owned==0&&b.rolledBack==stage+1);}
|
||||
// Any native entity creation failure rolls back rather than consuming a post.
|
||||
for(int stage=0;stage<8;stage++){Build b;ERR_Actors::failAt=ERR_Actors::calls+stage+1;bool begun=b.SpawnPost(positions,covers);
|
||||
for(int stage=0;stage<16;stage++){Build b;ERR_Actors::failAt=ERR_Actors::calls+stage+1;bool begun=b.SpawnPost(positions,covers);
|
||||
if(stage==0)assert(!begun);else{for(int i=0;i<stage;i++)b.BuildPost();assert(b.rolledBack==stage);}
|
||||
assert(!b.m_BuildGroup&&b.m_Positions.Count()==0&&b.m_Mode.owned==0);ERR_Actors::failAt=0;
|
||||
}
|
||||
{Build b;b.SpawnPost(positions,covers);b.m_Placement.clear=false;b.BuildPost();assert(!b.m_BuildGroup&&b.rolledBack==1);}
|
||||
{Build b;b.SpawnPost(positions,covers);for(int i=0;i<4;i++)b.BuildPost();b.coversClear=false;b.BuildPost();assert(!b.m_BuildGroup&&b.rolledBack==5);}
|
||||
{Build b;b.SpawnPost(positions,covers);for(int i=0;i<7;i++)b.BuildPost();b.m_Mode.ledger.accept=false;b.BuildPost();assert(!b.m_BuildGroup&&b.rolledBack==8&&b.m_Positions.Count()==0);}
|
||||
{Build b;b.SpawnPost(positions,covers);for(int i=0;i<15;i++)b.BuildPost();b.m_Mode.ledger.accept=false;b.BuildPost();assert(!b.m_BuildGroup&&b.rolledBack==16&&b.m_Positions.Count()==0);}
|
||||
{Build b;b.SpawnPost(positions,covers);b.BuildPost();b.CancelPost();assert(!b.m_BuildGroup&&b.rolledBack==2&&b.m_Mode.owned==0);}
|
||||
for(auto*n:ERR_Actors::all)delete n;
|
||||
}
|
||||
''')
|
||||
print('PASS: actual staged post creation spawns at most one entity per callback, commits only all four guards/two covers/waypoint, and rolls back player approach, obstructions, spawn/ledger failure and cancellation')
|
||||
print('PASS: actual staged post creation spawns at most one entity per callback, commits only all four guards/ten props/waypoint, and rolls back player approach, obstructions, spawn/ledger failure and cancellation')
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
"""Execute the shipped checkpoint layout across road widths and rotations."""
|
||||
from policy_harness import ROOT, body, run
|
||||
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_Roadblocks.c'
|
||||
s='\n'.join(body(p,x) for x in ('protected void Layout(', 'protected void AddProp('))
|
||||
s=s.replace('protected ', '').replace('out ', '').replace('array<ref ERR_RoadblockProp> covers', 'Array<Prop*>& covers')
|
||||
s=s.replace('ResourceName ', 'string ').replace('vector ', 'Vec ').replace('ERR_Actors.Ground','Ground').replace('new ERR_RoadblockProp','new Prop')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <algorithm>
|
||||
using string=std::string;
|
||||
struct Vec{double x=0,y=0,z=0;Vec operator+(Vec b)const{return{x+b.x,y+b.y,z+b.z};}Vec operator-(Vec b)const{return{x-b.x,y-b.y,z-b.z};}Vec operator*(double k)const{return{x*k,y*k,z*k};}};
|
||||
double dot(Vec a,Vec b){return a.x*b.x+a.y*b.y+a.z*b.z;}
|
||||
Vec Ground(Vec p){p.y=50;return p;}
|
||||
template<class T>struct Array:std::vector<T>{void Clear(){for(auto*p:*this)delete p;this->clear();}void Insert(T p){this->push_back(p);}};
|
||||
struct Prop{string Prefab;Vec Position,Forward,Right;double HalfWidth,HalfDepth,Height;
|
||||
Prop(string p,Vec v,Vec f,double w,double d,double h):Prefab(p),Position(v),Forward(f),Right({f.z,0,-f.x}),HalfWidth(w),HalfDepth(d),Height(h){}
|
||||
};
|
||||
struct Checkpoint{Vec m_Centre={1000,50,2000},m_Forward,m_Right;double m_Shoulder;
|
||||
string COVER="cover",BARRIER="barrier",OBSTACLE="obstacle",SIGN="sign";
|
||||
'''+s+r'''
|
||||
};
|
||||
int main(){
|
||||
for(double width:{4.,6.,10.,16.})for(int degrees=0;degrees<360;degrees+=15){
|
||||
Checkpoint l;double angle=degrees*3.141592653589793/180;
|
||||
l.m_Forward={std::sin(angle),0,std::cos(angle)};l.m_Right={std::cos(angle),0,-std::sin(angle)};
|
||||
l.m_Shoulder=std::max(width*.5+1.5,4.);Array<Prop*>props;l.Layout(props);assert(props.size()==10);
|
||||
std::map<string,int>counts;std::vector<double>barriers;
|
||||
for(auto*p:props){counts[p->Prefab]++;
|
||||
Vec delta=p->Position-l.m_Centre;double across=dot(delta,l.m_Right),along=dot(delta,l.m_Forward);
|
||||
// Whole footprints (not just origins) preserve a four-metre straight passage.
|
||||
double reach=std::abs(dot(p->Right,l.m_Right))*p->HalfWidth+std::abs(dot(p->Forward,l.m_Right))*p->HalfDepth;
|
||||
assert(std::abs(across)-reach>=2.-1e-8);
|
||||
if(p->Prefab=="barrier")barriers.push_back(along);
|
||||
if(p->Prefab=="sign")assert(dot(p->Forward,l.m_Forward)*along>0); // both approaches face outward
|
||||
// Guard starting volumes cannot intersect any prop in the planned post.
|
||||
for(int side:{-1,1})for(int row:{-1,1}){
|
||||
Vec guard=l.m_Centre+l.m_Right*(side*(l.m_Shoulder+2.5))+l.m_Forward*(side*(row-1.5));
|
||||
Vec rel=guard-p->Position;
|
||||
assert(std::abs(dot(rel,p->Right))>p->HalfWidth+.45 || std::abs(dot(rel,p->Forward))>p->HalfDepth+.45);
|
||||
}
|
||||
}
|
||||
assert(counts["cover"]==4&&counts["barrier"]==2&&counts["obstacle"]==2&&counts["sign"]==2);
|
||||
assert(barriers.size()==2&&std::abs(barriers[0]-barriers[1])>15.9);
|
||||
// No physical prop footprint intersects another (separating axes).
|
||||
for(size_t i=0;i<props.size();i++)for(size_t j=i+1;j<props.size();j++){
|
||||
auto*a=props[i];auto*b=props[j];bool separate=false;
|
||||
for(Vec axis:{a->Right,a->Forward,b->Right,b->Forward}){
|
||||
double ar=std::abs(dot(a->Right,axis))*a->HalfWidth+std::abs(dot(a->Forward,axis))*a->HalfDepth;
|
||||
double br=std::abs(dot(b->Right,axis))*b->HalfWidth+std::abs(dot(b->Forward,axis))*b->HalfDepth;
|
||||
if(std::abs(dot(a->Position-b->Position,axis))>=ar+br)separate=true;
|
||||
}
|
||||
assert(separate);
|
||||
}
|
||||
props.Clear();
|
||||
}
|
||||
}
|
||||
''')
|
||||
print('PASS: shipped checkpoint layout preserves vehicle lane, opposing signs, staggered barriers, clear guard slots and separate props across 4–16m roads and all tested headings')
|
||||
|
||||
# Execute the actual clearance path: owned pieces are excluded only after
|
||||
# checking guards; terrain and foreign obstructions remain authoritative.
|
||||
s=body(p,'protected bool CoverClear(').replace('protected ', '').replace('ERR_RoadblockProp prop','Prop prop')
|
||||
s=s.replace('vector ', 'Vec ').replace('foreach (IEntity builtEntity : m_BuildSpawned)', 'for (Node* builtEntity : m_BuildSpawned)')
|
||||
s=s.replace('ChimeraCharacter.Cast(builtEntity)', 'builtEntity->character').replace('builtEntity.GetOrigin()', 'builtEntity->point')
|
||||
s=s.replace('ERR_Actors.Ground','Ground').replace('Vector(', 'Vec(').replace('Math.AbsFloat', 'std::abs').replace('vector.Dot','dot')
|
||||
s=s.replace('ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point)', 'water').replace('GetGame().GetWorld().TraceMove(trace, null)', 'TraceMove(trace)')
|
||||
s=s.replace('TraceBox trace = new TraceBox();','TraceBox trace;').replace('"0 0.15 0"','Vec(0,.15,0)').replace('"0 0.01 0"','Vec(0,.01,0)').replace('TraceFlags.', 'TraceFlags::')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
struct Vec {double v[3];Vec(double x=0,double y=0,double z=0):v{x,y,z}{}double& operator[](int i){return v[i];}Vec operator+(Vec b){return {v[0]+b[0],v[1]+b[1],v[2]+b[2]};}Vec operator-(Vec b){return {v[0]-b[0],v[1]-b[1],v[2]-b[2]};}Vec operator*(double n){return {v[0]*n,v[1]*n,v[2]*n};}};
|
||||
double dot(Vec a,Vec b){return a[0]*b[0]+a[1]*b[1]+a[2]*b[2];}
|
||||
struct Node{bool character;Vec point;};
|
||||
struct Prop{Vec Position,Right={1,0,0},Forward={0,0,1};double HalfWidth=1.9,HalfDepth=.7,Height=1;};
|
||||
namespace TraceFlags{int WORLD=1,ENTS=2;}
|
||||
struct TraceBox{Vec Start,End,Mins,Maxs;int Flags=0;std::vector<Node*>ExcludeArray;};
|
||||
struct Clearance{
|
||||
bool water=false,blocked=false;double slope=0;int traces=0;std::vector<Node*>m_BuildSpawned;
|
||||
Vec Ground(Vec p){p[1]=slope*p[0];return p;}
|
||||
double TraceMove(TraceBox trace){traces++;assert(trace.Flags==3&&trace.ExcludeArray==m_BuildSpawned);assert(std::abs(trace.Mins[0]+1.9)<1e-5&&std::abs(trace.Maxs[2]-.7)<1e-5);return blocked?0:1;}
|
||||
'''+s+r'''
|
||||
};
|
||||
int main(){
|
||||
Prop prop;Clearance c;assert(c.CoverClear(prop));
|
||||
c.water=true;assert(!c.CoverClear(prop));c.water=false;
|
||||
c.slope=.2;assert(!c.CoverClear(prop));c.slope=0;
|
||||
c.blocked=true;assert(!c.CoverClear(prop));c.blocked=false;
|
||||
Node ownProp{false,{0,0,0}},guard{true,{3,0,0}};c.m_BuildSpawned={&ownProp,&guard};assert(c.CoverClear(prop));
|
||||
int traces=c.traces;guard.point={2,0,0};assert(!c.CoverClear(prop)&&c.traces==traces);
|
||||
guard.point={0,0,1.4};assert(c.CoverClear(prop));guard.point={0,0,1};assert(!c.CoverClear(prop));
|
||||
}
|
||||
''')
|
||||
print('PASS: actual prop clearance excludes owned geometry while retaining moved-guard, foreign-obstruction, water and terrain rejection')
|
||||
@@ -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