Embed RHS 1.9 compatibility; record Workshop 0.0.141
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
check: /tmp/err-policy-qg43b_h1/check.cpp:81: int main(): Assertion `!f.Allowed()' failed.
|
||||
PASS: authority-scoped slow recovery, bleeding pauses resilience, medicine unaffected, player-only lethal damage cap without healing
|
||||
PASS: bleeding severity survives low health, retains armour, causes no knockout and restores the actual damage/blood-loss cap
|
||||
PASS: viable passive recovery survives native delay; bleeding, disabled/full effects and low-blood resilience do not block defeat
|
||||
Traceback (most recent call last):
|
||||
File "/tmp/erri-heartbeat-20261105-1134/supply-proposal/proposal/tests/test_player_medical.py", line 153, in <module>
|
||||
run(r'''
|
||||
~~~^^^^^
|
||||
#include <cassert>
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
...<53 lines>...
|
||||
}
|
||||
^
|
||||
''')
|
||||
^^^^
|
||||
File "/home/jakub/devel/escapistsrereintroduced/tests/policy_harness.py", line 14, in run
|
||||
subprocess.run([str(p/'check')],check=True)
|
||||
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/usr/lib/python3.14/subprocess.py", line 578, in run
|
||||
raise CalledProcessError(retcode, process.args,
|
||||
output=stdout, stderr=stderr)
|
||||
subprocess.CalledProcessError: Command '['/tmp/err-policy-qg43b_h1/check']' died with <Signals.SIGABRT: 6>.
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
// Native injection animation/consumption; only resilience is restored over time.
|
||||
// This is not a blood replacement, bandage, full heal, or resurrection.
|
||||
[BaseContainerProps()]
|
||||
class ERR_ConsumableEpinephrine : SCR_ConsumableMorphine
|
||||
{
|
||||
override bool CanApplyEffect(notnull IEntity target, notnull IEntity user, out SCR_EConsumableFailReason failReason)
|
||||
{
|
||||
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(target.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||
CharacterControllerComponent controller = CharacterControllerComponent.Cast(target.FindComponent(CharacterControllerComponent));
|
||||
if (!damage || damage.GetState() == EDamageState.DESTROYED || !controller || !controller.IsUnconscious())
|
||||
{ failReason = SCR_EConsumableFailReason.UNDAMAGED; return false; }
|
||||
array<ref SCR_PersistentDamageEffect> effects = damage.GetAllPersistentEffectsOfType(ERR_EpinephrineEffect);
|
||||
if (!effects.IsEmpty()) { failReason = SCR_EConsumableFailReason.ALREADY_APPLIED; return false; }
|
||||
return true;
|
||||
}
|
||||
}
|
||||
class ERR_EpinephrineEffect : SCR_DotDamageEffect
|
||||
{
|
||||
override bool HijackDamageEffect(SCR_ExtendedDamageManagerComponent dmgManager)
|
||||
{
|
||||
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(dmgManager);
|
||||
if (damage) SetAffectedHitZone(damage.GetResilienceHitZone());
|
||||
return false;
|
||||
}
|
||||
override EDamageType GetDefaultDamageType() { return EDamageType.HEALING; }
|
||||
}
|
||||
// Separate stronger field injector. Standard epinephrine above keeps its
|
||||
// resilience-over-time behavior; neither item restores a dead character.
|
||||
class ERR_CombatEpinephrineRules
|
||||
{
|
||||
static float Heal(float current, bool leg)
|
||||
{
|
||||
float amount = 0.15;
|
||||
if (leg) amount = 0.5;
|
||||
return Math.Min(current + amount, 1.0);
|
||||
}
|
||||
static float Blood(float current, float threshold) { return Math.Min(1, current + threshold + 0.01); }
|
||||
}
|
||||
[BaseContainerProps()]
|
||||
class ERR_ConsumableCombatEpinephrine : SCR_ConsumableMorphine
|
||||
{
|
||||
override bool CanApplyEffect(notnull IEntity target, notnull IEntity user, out SCR_EConsumableFailReason failReason)
|
||||
{
|
||||
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(target.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||
if (!damage || damage.GetState() == EDamageState.DESTROYED) { failReason = SCR_EConsumableFailReason.UNDAMAGED; return false; }
|
||||
if (damage.IsBleeding()) return true;
|
||||
HitZone blood = damage.GetBloodHitZone(), resilience = damage.GetResilienceHitZone();
|
||||
if ((blood && blood.GetHealthScaled() < 1) || (resilience && resilience.GetHealthScaled() < 1)) return true;
|
||||
array<HitZone> zones = {}; damage.GetPhysicalHitZones(zones);
|
||||
foreach (HitZone zone : zones) if (zone && zone.GetHealthScaled() < 1) return true;
|
||||
failReason = SCR_EConsumableFailReason.UNDAMAGED; return false;
|
||||
}
|
||||
}
|
||||
class ERR_CombatEpinephrineEffect : SCR_InstantDamageEffect
|
||||
{
|
||||
override EDamageType GetDefaultDamageType() { return EDamageType.HEALING; }
|
||||
override void OnEffectAdded(SCR_ExtendedDamageManagerComponent dmgManager)
|
||||
{
|
||||
super.OnEffectAdded(dmgManager);
|
||||
if (!Replication.IsServer()) return;
|
||||
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(dmgManager);
|
||||
if (!damage || damage.GetState() == EDamageState.DESTROYED) return;
|
||||
foreach (ECharacterHitZoneGroup group : {ECharacterHitZoneGroup.HEAD, ECharacterHitZoneGroup.UPPERTORSO, ECharacterHitZoneGroup.LOWERTORSO, ECharacterHitZoneGroup.LEFTARM, ECharacterHitZoneGroup.RIGHTARM, ECharacterHitZoneGroup.LEFTLEG, ECharacterHitZoneGroup.RIGHTLEG}) damage.RemoveGroupBleeding(group);
|
||||
SCR_CharacterBloodHitZone blood = damage.GetBloodHitZone();
|
||||
if (blood && blood.GetHealthScaled() <= blood.GetDamageStateThreshold(ECharacterBloodState.FAINTING))
|
||||
blood.SetHealthScaled(ERR_CombatEpinephrineRules.Blood(blood.GetHealthScaled(), blood.GetDamageStateThreshold(ECharacterBloodState.UNCONSCIOUS)));
|
||||
SCR_CharacterResilienceHitZone resilience = damage.GetResilienceHitZone();
|
||||
if (resilience) resilience.SetHealthScaled(1);
|
||||
array<HitZone> zones = {}; damage.GetPhysicalHitZones(zones);
|
||||
foreach (HitZone zone : zones)
|
||||
{
|
||||
SCR_CharacterHitZone physical = SCR_CharacterHitZone.Cast(zone);
|
||||
if (!physical || zone.GetHealthScaled() >= 1) continue;
|
||||
int group = physical.GetHitZoneGroup();
|
||||
zone.SetHealthScaled(ERR_CombatEpinephrineRules.Heal(zone.GetHealthScaled(), group == ECharacterHitZoneGroup.LEFTLEG || group == ECharacterHitZoneGroup.RIGHTLEG));
|
||||
}
|
||||
CharacterControllerComponent controller = CharacterControllerComponent.Cast(damage.GetOwner().FindComponent(CharacterControllerComponent));
|
||||
if (controller && controller.IsUnconscious()) controller.SetUnconscious(false);
|
||||
}
|
||||
}
|
||||
// The held injector uses the existing native injection interaction and animation.
|
||||
// Only its displayed name changes; ordinary morphine keeps its native behavior.
|
||||
modded class SCR_MorphineUserAction
|
||||
{
|
||||
protected bool m_ERRHoldingEpinephrine;
|
||||
protected bool m_ERRHoldingCombatEpinephrine;
|
||||
override bool CanBeShownScript(IEntity user)
|
||||
{
|
||||
m_ERRHoldingEpinephrine = false; m_ERRHoldingCombatEpinephrine = false;
|
||||
bool shown = super.CanBeShownScript(user);
|
||||
ChimeraCharacter character = ChimeraCharacter.Cast(user);
|
||||
if (shown && character)
|
||||
{
|
||||
SCR_ConsumableItemComponent item = GetConsumableComponent(character);
|
||||
m_ERRHoldingEpinephrine = item && ERR_ConsumableEpinephrine.Cast(item.GetConsumableEffect());
|
||||
m_ERRHoldingCombatEpinephrine = item && ERR_ConsumableCombatEpinephrine.Cast(item.GetConsumableEffect());
|
||||
}
|
||||
return shown;
|
||||
}
|
||||
override bool GetActionNameScript(out string outName)
|
||||
{
|
||||
if (m_ERRHoldingCombatEpinephrine) { outName = "Administer combat epinephrine"; return true; }
|
||||
if (m_ERRHoldingEpinephrine) { outName = "Administer epinephrine"; return true; }
|
||||
return super.GetActionNameScript(outName);
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
"""Execute player policy methods; native damage/regen boundaries are modeled."""
|
||||
from policy_harness import ROOT, body, run
|
||||
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerMedical.c'
|
||||
source=p.read_text()
|
||||
methods='\n'.join(body(p,s) for s in ['void ERR_EnableCasualtyProtection(', 'override protected bool IsIndefinitelyUnconscious(', 'override float GetResilienceRegenScale('])
|
||||
methods=methods.replace('override ','').replace('protected ','').replace('Replication.IsServer()','server').replace('super.','Native::')
|
||||
run('''#include <cassert>
|
||||
bool server=true;
|
||||
struct Native {bool unconscious=false,changed=false;float regen=1;int writes=0;bool bleeding=false;
|
||||
void SetPermitUnconsciousness(bool value,bool c){unconscious=value;changed=c;writes++;}
|
||||
void SetRegenScale(float value,bool){regen=value;writes++;}
|
||||
bool IsIndefinitelyUnconscious(bool){return true;}bool IsBleeding(){return bleeding;}float GetResilienceRegenScale(){return .8f;}
|
||||
};
|
||||
struct Policy:Native{bool m_ERRRevivablePlayer=false;
|
||||
'''+methods+'''
|
||||
};
|
||||
int main(){Policy p;server=false;p.ERR_EnableCasualtyProtection();assert(!p.m_ERRRevivablePlayer&&p.writes==0);
|
||||
server=true;p.ERR_EnableCasualtyProtection();assert(p.unconscious&&p.changed&&p.regen==.25f);
|
||||
assert(!p.IsIndefinitelyUnconscious());p.bleeding=true;assert(p.GetResilienceRegenScale()==0);
|
||||
p.bleeding=false;assert(p.GetResilienceRegenScale()==.8f);Policy ai;assert(ai.IsIndefinitelyUnconscious());ai.bleeding=true;assert(ai.GetResilienceRegenScale()==.8f);
|
||||
}
|
||||
''')
|
||||
# Execute the real damage cap across wound/blood scales; healing is untouched.
|
||||
cap=body(p, 'static float AllowedDamage(').replace('Math.Clamp','std::clamp').replace('Math.Min','std::min').replace('Math.Max','std::max').replace('0.01','0.01f').replace(', 1)',', 1.0f)').replace('(0,','(0.0f,')
|
||||
run('''#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
'''+cap+'''
|
||||
int main(){
|
||||
for(float maximum:{100.0f,5000.0f}) for(float threshold:{0.0f,.05f,.2f}) {
|
||||
float floor=maximum*(threshold+.01f);
|
||||
assert(AllowedDamage(-10,maximum,maximum,threshold)==-10);
|
||||
assert(AllowedDamage(100000, floor,maximum,threshold)==0);
|
||||
assert(AllowedDamage(100000, floor/2,maximum,threshold)==0);
|
||||
float health=maximum;
|
||||
for(int i=0;i<1000;i++) health-=AllowedDamage(13,health,maximum,threshold);
|
||||
assert(std::abs(health-floor)<.001f);
|
||||
assert(AllowedDamage(1,maximum,maximum,threshold)==1);
|
||||
}
|
||||
}
|
||||
''')
|
||||
# Passive resilience has its own native rate; treatment DPS bypasses this getter.
|
||||
rate=body(p,'override float CalculatePassiveRegenDPS(').replace('override ','').replace('super.','Native::').replace('SCR_CharacterDamageManagerComponent manager = SCR_CharacterDamageManagerComponent.Cast(GetHitZoneContainer());','Manager* manager = target;').replace('manager.','manager->')
|
||||
run('''#include <cassert>
|
||||
struct Manager { bool player; bool ERR_IsRevivablePlayer(){return player;} };
|
||||
struct Native {float CalculatePassiveRegenDPS(bool){return 4;}};
|
||||
struct Policy:Native {Manager* target=nullptr;
|
||||
'''+rate+'''
|
||||
};
|
||||
int main(){Policy p;assert(p.CalculatePassiveRegenDPS()==4);Manager ai{false};p.target=&ai;assert(p.CalculatePassiveRegenDPS()==4);ai.player=true;assert(p.CalculatePassiveRegenDPS()==1);assert(p.CalculatePassiveRegenDPS(false)==4);}
|
||||
''')
|
||||
assert 'SCR_CharacterResilienceHitZone.Cast(this)) return damage' in source
|
||||
assert 'damage <= 0' in source and '!manager.ERR_IsRevivablePlayer()' in source
|
||||
assert 'override void Kill(' in source
|
||||
assert 'FullHeal(' not in source and 'SetHealth(' not in source
|
||||
print('PASS: authority-scoped slow recovery, bleeding pauses resilience, medicine unaffected, player-only lethal damage cap without healing')
|
||||
# The native bleeding evaluator recomputes severity after the hit has already
|
||||
# lowered health. Exercise production query/cap methods with native armour at
|
||||
# the virtual-call boundary: low remaining health must not erase wound severity.
|
||||
query=body(p, 'float ERR_ComputeWoundDamage(').replace('notnull BaseDamageContext', 'Context')
|
||||
effective=body(p, 'override float ComputeEffectiveDamage(')
|
||||
effective=effective.replace('override ', 'virtual ').replace('notnull BaseDamageContext', 'Context').replace('super.', 'Native::')
|
||||
effective=effective.replace('Replication.IsServer()', 'server').replace('SCR_CharacterResilienceHitZone.Cast(this)', 'resilience')
|
||||
effective=effective.replace('SCR_CharacterDamageManagerComponent manager = SCR_CharacterDamageManagerComponent.Cast(GetHitZoneContainer());', 'Manager* manager = target;').replace('manager.', 'manager->')
|
||||
effective=effective.replace('ERR_PlayerMedicalRules.', '').replace('EDamageState.DESTROYED', '0')
|
||||
run('''#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
bool server=true;
|
||||
struct Context {float value;};
|
||||
struct Manager {bool player=true;int knockouts=0;bool ERR_IsRevivablePlayer(){return player;}void ERR_RequestKnockout(){knockouts++;}};
|
||||
struct Native {virtual float ComputeEffectiveDamage(Context c,bool){return c.value;}};
|
||||
'''+cap+'''
|
||||
struct Policy:Native {
|
||||
bool m_ERRWoundQuery=false,resilience=false;Manager* target=nullptr;float health=1;
|
||||
float GetHealth(){return health;}float GetMaxHealth(){return 100;}float GetDamageStateThreshold(int){return 0;}
|
||||
'''+query+effective+'''
|
||||
};
|
||||
struct Armoured:Policy {float armour=1;float ComputeEffectiveDamage(Context c,bool dot) override {if(!dot)c.value*=armour;return Policy::ComputeEffectiveDamage(c,dot);}};
|
||||
int main(){Manager player;Armoured zone;zone.target=&player;
|
||||
assert(zone.ComputeEffectiveDamage({60},false)==0 && player.knockouts==1);
|
||||
assert(zone.ERR_ComputeWoundDamage({60})==60 && player.knockouts==1);
|
||||
assert(!zone.m_ERRWoundQuery && zone.ComputeEffectiveDamage({60},true)==0);
|
||||
assert(zone.ERR_ComputeWoundDamage({2})==2);
|
||||
zone.armour=.2f;assert(std::abs(zone.ERR_ComputeWoundDamage({60})-12)<.001f);
|
||||
assert(zone.ComputeEffectiveDamage({60},false)==0);
|
||||
zone.health=100;assert(std::abs(zone.ComputeEffectiveDamage({60},false)-12)<.001f);
|
||||
zone.health=1;player.player=false;assert(std::abs(zone.ComputeEffectiveDamage({60},false)-12)<.001f);
|
||||
player.player=true;zone.resilience=true;assert(std::abs(zone.ComputeEffectiveDamage({60},false)-12)<.001f);
|
||||
zone.resilience=false;server=false;assert(std::abs(zone.ComputeEffectiveDamage({60},false)-12)<.001f);
|
||||
}
|
||||
''')
|
||||
print('PASS: bleeding severity survives low health, retains armour, causes no knockout and restores the actual damage/blood-loss cap')
|
||||
# Run the production recovery prediction over real effect states modeled at the
|
||||
# native boundary, including a pending regeneration delay and exhausted effects.
|
||||
future=body(p,'bool ERR_CanRecoverNaturally(')
|
||||
future=future.replace('array<ref SCR_PersistentDamageEffect> effects','auto effects').replace('GetAllPersistentEffectsOfType(SCR_PassiveHitZoneRegenDamageEffect, true)','GetAllPersistentEffectsOfType()')
|
||||
future=future.replace('foreach (SCR_PersistentDamageEffect effect : effects)','for (Effect* effect : effects)').replace('DotDamageEffect dot = DotDamageEffect.Cast(effect);','Effect* dot = effect;').replace('HitZone zone','Zone* zone').replace('dot.','dot->').replace('zone.','zone->').replace('EDamageType.REGENERATION','1')
|
||||
run('''#include <cassert>
|
||||
#include <vector>
|
||||
struct Zone {float value=0;float GetHealthScaled(){return value;}};
|
||||
struct Effect {Zone* zone;float dps;int type=1;int GetDamageType(){return type;}float GetDPS(){return dps;}Zone* GetAffectedHitZone(){return zone;}};
|
||||
struct Policy {bool m_ERRRevivablePlayer=true,bleeding=false;float scale=1;Zone blood,resilience;std::vector<Effect*> effects;
|
||||
bool IsBleeding(){return bleeding;}auto GetAllPersistentEffectsOfType(){return effects;}
|
||||
Zone* GetBloodHitZone(){return &blood;}Zone* GetResilienceHitZone(){return &resilience;}float GetResilienceRegenScale(){return scale;}
|
||||
'''+future+'''
|
||||
};
|
||||
int main(){Policy p;assert(!p.ERR_CanRecoverNaturally());Effect e{&p.blood,-.1f};p.effects={&e};
|
||||
assert(p.ERR_CanRecoverNaturally());p.bleeding=true;assert(!p.ERR_CanRecoverNaturally());p.bleeding=false;
|
||||
p.blood.value=1;assert(!p.ERR_CanRecoverNaturally());p.blood.value=.2;
|
||||
e.dps=0;assert(!p.ERR_CanRecoverNaturally());e.dps=-1;e.type=2;assert(!p.ERR_CanRecoverNaturally());e.type=1;
|
||||
e.zone=&p.resilience;p.scale=0;assert(!p.ERR_CanRecoverNaturally());p.scale=.1;assert(p.ERR_CanRecoverNaturally());
|
||||
p.m_ERRRevivablePlayer=false;assert(!p.ERR_CanRecoverNaturally());
|
||||
}
|
||||
''')
|
||||
print('PASS: viable passive recovery survives native delay; bleeding, disabled/full effects and low-blood resilience do not block defeat')
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
check: /tmp/err-policy-gy4hr1ou/check.cpp:86: int main(): Assertion `f.Allowed()' failed.
|
||||
PASS: authority-scoped slow recovery, bleeding pauses resilience, medicine unaffected, player-only lethal damage cap without healing
|
||||
PASS: bleeding severity survives low health, retains armour, causes no knockout and restores the actual damage/blood-loss cap
|
||||
PASS: viable passive recovery survives native delay; bleeding, disabled/full effects and low-blood resilience do not block defeat
|
||||
Traceback (most recent call last):
|
||||
File "/tmp/erri-heartbeat-20261105-1134/supply-proposal/proposal/tests/test_player_medical.py", line 153, in <module>
|
||||
run(r'''
|
||||
~~~^^^^^
|
||||
#include <cassert>
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
...<53 lines>...
|
||||
}
|
||||
^
|
||||
''')
|
||||
^^^^
|
||||
File "/home/jakub/devel/escapistsrereintroduced/tests/policy_harness.py", line 14, in run
|
||||
subprocess.run([str(p/'check')],check=True)
|
||||
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/usr/lib/python3.14/subprocess.py", line 578, in run
|
||||
raise CalledProcessError(retcode, process.args,
|
||||
output=stdout, stderr=stderr)
|
||||
subprocess.CalledProcessError: Command '['/tmp/err-policy-gy4hr1ou/check']' died with <Signals.SIGABRT: 6>.
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"status": "demonstrated in actual-method policy harness",
|
||||
"trigger": "Living conscious nonbleeding patient; blood above native FAINTING threshold but below full; resilience and physical zones full.",
|
||||
"before": "CanApplyEffect admits blood<1; actual authored OnEffectAdded leaves this patient unchanged. Native ordinary consumable application deletes on use, so eligibility can offer a finite useless dose.",
|
||||
"after": "Blood-only admission uses the same <=FAINTING threshold as the unchanged effect. Explicit native unconsciousness is independently useful and admitted after missing/dead guard; missing controller does not block other benefits.",
|
||||
"scope": "One eligibility method and appended controls in existing registered test_player_medical.py; no new effect, refill, stock, source actor or controller flow.",
|
||||
"unchanged_method_hashes": {
|
||||
"class ERR_ConsumableEpinephrine": "f9e1d20314a2fe27f91b54fd129406b5aa3ad07f88f07094c2677652a5ea9597",
|
||||
"class ERR_EpinephrineEffect": "fbce1bd66fecbcc5e36772aaea16291e380a1b3eb5c77ae93c34f7aac5f240e1",
|
||||
"class ERR_CombatEpinephrineRules": "b9d8faa84f90a8ab7d159136c6273c957ab363337d83ba28d29c2ab613e10fd9",
|
||||
"class ERR_CombatEpinephrineEffect": "21b78b827258b76e0edd51dc6b9f39f7d354a2b1876fe15c6e7dd37c45c917d7",
|
||||
"modded class SCR_MorphineUserAction": "633db427c1b287075f922d97b2fe5ef27f4bae795b0dacb3221562ca7b0b5059"
|
||||
},
|
||||
"validation": [
|
||||
{
|
||||
"case": "baseline-red",
|
||||
"exit_code": 1,
|
||||
"log": "baseline-red.log",
|
||||
"expected": "runtime assertion failure",
|
||||
"result": "compiled, assertion !f.Allowed() fails for mild blood-only patient"
|
||||
},
|
||||
{
|
||||
"case": "green",
|
||||
"exit_code": 0,
|
||||
"log": "green.log",
|
||||
"expected": 0,
|
||||
"result": "all existing and appended controls pass"
|
||||
},
|
||||
{
|
||||
"case": "consciousness-mutation-red",
|
||||
"exit_code": 1,
|
||||
"log": "consciousness-mutation-red.log",
|
||||
"expected": "runtime assertion failure",
|
||||
"result": "compiled, assertion f.Allowed() fails for unconscious patient"
|
||||
}
|
||||
],
|
||||
"limitations": [
|
||||
"Native superclass/effect propagation and inventory/animation/remote consumption are opaque doubles; no new native test or multiplayer qualification.",
|
||||
"Inspected vanilla consumable implementation is retained local script-diff-v1.8.0.10 source, not a newly verified live SDK version.",
|
||||
"Prior compile-only mutant rejected due unused variable is preserved separately and is not runtime proof."
|
||||
],
|
||||
"reference_metadata": {
|
||||
"/home/jakub/devel/escapistsrereintroduced/addons/core/Prefabs/ERR/CombatEpinephrine.et": "aa57d78bae861dc4a0fc509f7916190d2f2f0b36a1c3867f9bf9a071717cd7f5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
--- a/addons/core/Scripts/Game/ERR/00_Core/ERR_Epinephrine.c
|
||||
+++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_Epinephrine.c
|
||||
@@ -43,9 +43,11 @@
|
||||
{
|
||||
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(target.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||
if (!damage || damage.GetState() == EDamageState.DESTROYED) { failReason = SCR_EConsumableFailReason.UNDAMAGED; return false; }
|
||||
+ CharacterControllerComponent controller = CharacterControllerComponent.Cast(target.FindComponent(CharacterControllerComponent));
|
||||
+ if (controller && controller.IsUnconscious()) return true;
|
||||
if (damage.IsBleeding()) return true;
|
||||
HitZone blood = damage.GetBloodHitZone(), resilience = damage.GetResilienceHitZone();
|
||||
- if ((blood && blood.GetHealthScaled() < 1) || (resilience && resilience.GetHealthScaled() < 1)) return true;
|
||||
+ if ((blood && blood.GetHealthScaled() <= blood.GetDamageStateThreshold(ECharacterBloodState.FAINTING)) || (resilience && resilience.GetHealthScaled() < 1)) return true;
|
||||
array<HitZone> zones = {}; damage.GetPhysicalHitZones(zones);
|
||||
foreach (HitZone zone : zones) if (zone && zone.GetHealthScaled() < 1) return true;
|
||||
failReason = SCR_EConsumableFailReason.UNDAMAGED; return false;
|
||||
--- a/tests/test_player_medical.py
|
||||
+++ b/tests/test_player_medical.py
|
||||
@@ -114,3 +114,100 @@
|
||||
}
|
||||
''')
|
||||
print('PASS: viable passive recovery survives native delay; bleeding, disabled/full effects and low-blood resilience do not block defeat')
|
||||
+
|
||||
+
|
||||
+"""Actual combat injector admission/effect; native component/effect boundaries doubled.
|
||||
+No animation, inventory consumption, replication or native base-effect qualification.
|
||||
+"""
|
||||
+from pathlib import Path
|
||||
+import os, re
|
||||
+from policy_harness import ROOT, body, run
|
||||
+p=Path(os.environ.get('ERR_EPINEPHRINE_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Epinephrine.c'))
|
||||
+s=p.read_text()
|
||||
+def method(class_name, signature):
|
||||
+ start=s.index('class '+class_name)
|
||||
+ a=s.index(signature,start); b=s.index('{',a); end=b+1; depth=1
|
||||
+ while depth:
|
||||
+ depth+=(s[end]=='{')-(s[end]=='}'); end+=1
|
||||
+ return s[a:end]
|
||||
+def cpp(t):
|
||||
+ t=t.replace('override ', '').replace('notnull ', '').replace('out SCR_EConsumableFailReason failReason','SCR_EConsumableFailReason& failReason')
|
||||
+ t=t.replace('SCR_CharacterDamageManagerComponent.Cast(target.FindComponent(SCR_CharacterDamageManagerComponent))','target->damage')
|
||||
+ t=t.replace('SCR_CharacterDamageManagerComponent.Cast(dmgManager)','dmgManager')
|
||||
+ t=t.replace('CharacterControllerComponent.Cast(target.FindComponent(CharacterControllerComponent))','target->controller')
|
||||
+ t=t.replace('CharacterControllerComponent.Cast(damage.GetOwner().FindComponent(CharacterControllerComponent))','damage->owner->controller')
|
||||
+ t=t.replace('SCR_CharacterHitZone.Cast(zone)','dynamic_cast<SCR_CharacterHitZone*>(zone)')
|
||||
+ t=t.replace('array<HitZone>', 'Array<HitZone*>').replace('foreach (', 'for (').replace('null','nullptr')
|
||||
+ for typ in ('SCR_CharacterDamageManagerComponent','SCR_ExtendedDamageManagerComponent','HitZone','SCR_CharacterBloodHitZone','SCR_CharacterResilienceHitZone','SCR_CharacterHitZone','CharacterControllerComponent'):
|
||||
+ t=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',t)
|
||||
+ t=t.replace('HitZone* blood = damage.GetBloodHitZone(), resilience = damage.GetResilienceHitZone();','HitZone* blood = damage.GetBloodHitZone(); HitZone* resilience = damage.GetResilienceHitZone();')
|
||||
+ for obj in ('damage','blood','resilience','zone','physical','controller'):
|
||||
+ t=t.replace(obj+'.',obj+'->')
|
||||
+ for obj in ('EDamageState','SCR_EConsumableFailReason','ECharacterBloodState','ECharacterHitZoneGroup','ERR_CombatEpinephrineRules','Replication','Math'):
|
||||
+ t=t.replace(obj+'.',obj+'::')
|
||||
+ t=t.replace('super.', 'Native::')
|
||||
+ return t
|
||||
+rules=cpp(body(p,'class ERR_CombatEpinephrineRules')).replace('class ERR_CombatEpinephrineRules','struct ERR_CombatEpinephrineRules')+';'
|
||||
+admit=cpp(method('ERR_ConsumableCombatEpinephrine','override bool CanApplyEffect('))
|
||||
+effect=cpp(method('ERR_CombatEpinephrineEffect','override void OnEffectAdded('))
|
||||
+run(r'''
|
||||
+#include <cassert>
|
||||
+#include <vector>
|
||||
+#include <algorithm>
|
||||
+#include <cmath>
|
||||
+namespace EDamageState{enum{UNDAMAGED,DESTROYED};}
|
||||
+enum class SCR_EConsumableFailReason{NONE,UNDAMAGED};
|
||||
+namespace ECharacterBloodState{enum{FAINTING,UNCONSCIOUS};}
|
||||
+enum class ECharacterHitZoneGroup{HEAD,UPPERTORSO,LOWERTORSO,LEFTARM,RIGHTARM,LEFTLEG,RIGHTLEG};
|
||||
+namespace Replication{bool server=true;bool IsServer(){return server;}}
|
||||
+namespace Math{float Min(float a,float b){return std::min(a,b);}}
|
||||
+template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;};
|
||||
+struct HitZone{float health=1;virtual ~HitZone()=default;float GetHealthScaled(){return health;}void SetHealthScaled(float n){health=n;}virtual float GetDamageStateThreshold(int){return .7f;}};
|
||||
+struct SCR_CharacterBloodHitZone:HitZone{float faint=.7f,unconscious=.5f;float GetDamageStateThreshold(int state)override{return state==ECharacterBloodState::FAINTING?faint:unconscious;}};
|
||||
+struct SCR_CharacterResilienceHitZone:HitZone{};
|
||||
+struct SCR_CharacterHitZone:HitZone{ECharacterHitZoneGroup group=ECharacterHitZoneGroup::LEFTARM;int GetHitZoneGroup(){return int(group);}};
|
||||
+// The source compares enum to an int; Enforce permits this conversion.
|
||||
+bool operator==(int a,ECharacterHitZoneGroup b){return a==int(b);}
|
||||
+struct CharacterControllerComponent{bool unconscious=false;bool IsUnconscious(){return unconscious;}void SetUnconscious(bool n){unconscious=n;}};
|
||||
+struct SCR_CharacterDamageManagerComponent;
|
||||
+struct Entity{SCR_CharacterDamageManagerComponent* damage=nullptr;CharacterControllerComponent*controller=nullptr;};using IEntity=Entity*;
|
||||
+struct SCR_CharacterDamageManagerComponent{Entity*owner=nullptr;int state=EDamageState::UNDAMAGED;bool bleeding=false;SCR_CharacterBloodHitZone*blood=nullptr;SCR_CharacterResilienceHitZone*resilience=nullptr;Array<HitZone*>zones;
|
||||
+ int GetState(){return state;}bool IsBleeding(){return bleeding;}SCR_CharacterBloodHitZone*GetBloodHitZone(){return blood;}SCR_CharacterResilienceHitZone*GetResilienceHitZone(){return resilience;}void GetPhysicalHitZones(Array<HitZone*>&out){out=zones;}void RemoveGroupBleeding(ECharacterHitZoneGroup){bleeding=false;}
|
||||
+};
|
||||
+using SCR_ExtendedDamageManagerComponent=SCR_CharacterDamageManagerComponent;
|
||||
+// Native inherited effect handling is opaque. No extra health restoration is
|
||||
+// fabricated: the authored effect is instant and supplies no generic damage.
|
||||
+struct Native{int calls=0;void OnEffectAdded(SCR_ExtendedDamageManagerComponent*){calls++;}};
|
||||
+'''+rules+'\nstruct Admission{'+admit+'};\nstruct Effect:Native{'+effect+r'''};
|
||||
+struct Fixture{Entity actor;CharacterControllerComponent controller;SCR_CharacterDamageManagerComponent damage;SCR_CharacterBloodHitZone blood;SCR_CharacterResilienceHitZone resilience;SCR_CharacterHitZone arm,leg;Admission admission;Effect effect;
|
||||
+ Fixture(){actor.damage=&damage;actor.controller=&controller;damage.owner=&actor;damage.blood=&blood;damage.resilience=&resilience;leg.group=ECharacterHitZoneGroup::LEFTLEG;damage.zones={&arm,&leg};Replication::server=true;}
|
||||
+ bool Allowed(){SCR_EConsumableFailReason reason=SCR_EConsumableFailReason::NONE;return admission.CanApplyEffect(&actor,&actor,reason);}
|
||||
+ void Apply(){effect.OnEffectAdded(&damage);}
|
||||
+};
|
||||
+int main(){
|
||||
+ // Concrete baseline mismatch: mild blood loss is the only injury. Applying the
|
||||
+ // actual effect changes no health/bleeding/consciousness, so admission must refuse.
|
||||
+ for(float value:{.7001f,.85f,.999f}){Fixture f;f.blood.health=value;f.Apply();assert(f.effect.calls==1&&f.blood.health==value&&f.resilience.health==1&&f.arm.health==1&&f.leg.health==1&&!f.damage.bleeding&&!f.controller.unconscious);assert(!f.Allowed());}
|
||||
+ // Native threshold is data-driven, including exact equality; no guessed cutoff.
|
||||
+ for(float cutoff:{.6f,.7f,.8f}){Fixture f;f.blood.faint=cutoff;f.blood.health=cutoff;assert(f.Allowed());f.Apply();assert(f.blood.health>cutoff);}
|
||||
+ {Fixture f;f.blood.health=.3f;assert(f.Allowed());f.Apply();assert(f.blood.health>.3f);}
|
||||
+ // Waking is an independent implemented benefit, including full health.
|
||||
+ for(float blood:{.9f,1.f}){Fixture f;f.blood.health=blood;f.controller.unconscious=true;assert(f.Allowed());f.Apply();assert(!f.controller.unconscious&&f.blood.health==blood);}
|
||||
+ {Fixture f;f.controller.unconscious=true;f.damage.state=EDamageState::DESTROYED;assert(!f.Allowed());f.Apply();assert(f.controller.unconscious);}
|
||||
+ // Preserve all other useful existing benefits even with mildly depleted blood.
|
||||
+ {Fixture f;f.blood.health=.9f;f.damage.bleeding=true;assert(f.Allowed());f.Apply();assert(!f.damage.bleeding&&f.blood.health==.9f);}
|
||||
+ {Fixture f;f.blood.health=.9f;f.resilience.health=.2f;f.controller.unconscious=true;assert(f.Allowed());f.Apply();assert(f.resilience.health==1&&!f.controller.unconscious&&f.blood.health==.9f);}
|
||||
+ {Fixture f;f.blood.health=.9f;f.arm.health=.5f;assert(f.Allowed());f.Apply();assert(std::abs(f.arm.health-.65f)<.00001f);}
|
||||
+ {Fixture f;f.leg.health=.2f;assert(f.Allowed());f.Apply();assert(std::abs(f.leg.health-.7f)<.00001f);}
|
||||
+ // A missing controller must not suppress non-consciousness benefits.
|
||||
+ for(int benefit=0;benefit<4;benefit++){Fixture f;f.actor.controller=nullptr;if(benefit==0)f.damage.bleeding=true;if(benefit==1)f.blood.health=.3f;if(benefit==2)f.resilience.health=.3f;if(benefit==3)f.arm.health=.3f;assert(f.Allowed());f.Apply();}
|
||||
+ {Fixture f;assert(!f.Allowed());f.actor.damage=nullptr;assert(!f.Allowed());}
|
||||
+ {Fixture f;f.damage.state=EDamageState::DESTROYED;f.damage.bleeding=true;f.blood.health=.1f;assert(!f.Allowed());f.Apply();assert(f.blood.health==.1f&&f.damage.bleeding);}
|
||||
+ {Fixture f;f.damage.blood=nullptr;f.damage.resilience=nullptr;assert(!f.Allowed());f.arm.health=.5f;assert(f.Allowed());}
|
||||
+ {Fixture f;f.blood.health=.1f;Replication::server=false;f.Apply();assert(f.blood.health==.1f);}
|
||||
+}
|
||||
+''')
|
||||
+prefab=(ROOT/'addons/core/Prefabs/ERR/CombatEpinephrine.et').read_text()
|
||||
+assert 'm_fItemRegenerationSpeed 0' in prefab and 'm_fItemRegenerationDuration 0' in prefab
|
||||
+assert 'ERR_CombatEpinephrineEffect' in prefab
|
||||
+print('PASS: actual combat injector admission/effect alignment; mild blood-only refusal, native threshold equality and all existing useful treatments preserved; engine consumption untested')
|
||||
@@ -0,0 +1,4 @@
|
||||
PASS: authority-scoped slow recovery, bleeding pauses resilience, medicine unaffected, player-only lethal damage cap without healing
|
||||
PASS: bleeding severity survives low health, retains armour, causes no knockout and restores the actual damage/blood-loss cap
|
||||
PASS: viable passive recovery survives native delay; bleeding, disabled/full effects and low-blood resilience do not block defeat
|
||||
PASS: actual combat injector admission/effect alignment; mild blood-only refusal, native threshold equality and all existing useful treatments preserved; engine consumption untested
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/tmp/err-policy-_hmozrxx/check.cpp: In member function ‘bool Admission::CanApplyEffect(IEntity, IEntity, SCR_EConsumableFailReason&)’:
|
||||
/tmp/err-policy-_hmozrxx/check.cpp:43:33: error: unused variable ‘controller’ [-Werror=unused-variable]
|
||||
43 | CharacterControllerComponent* controller = target->controller;
|
||||
| ^~~~~~~~~~
|
||||
cc1plus: all warnings being treated as errors
|
||||
PASS: authority-scoped slow recovery, bleeding pauses resilience, medicine unaffected, player-only lethal damage cap without healing
|
||||
PASS: bleeding severity survives low health, retains armour, causes no knockout and restores the actual damage/blood-loss cap
|
||||
PASS: viable passive recovery survives native delay; bleeding, disabled/full effects and low-blood resilience do not block defeat
|
||||
Traceback (most recent call last):
|
||||
File "/tmp/erri-heartbeat-20261105-1134/supply-proposal/proposal/tests/test_player_medical.py", line 153, in <module>
|
||||
run(r'''
|
||||
~~~^^^^^
|
||||
#include <cassert>
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
...<53 lines>...
|
||||
}
|
||||
^
|
||||
''')
|
||||
^^^^
|
||||
File "/home/jakub/devel/escapistsrereintroduced/tests/policy_harness.py", line 13, in run
|
||||
subprocess.run(['g++','-std=c++17','-Wall','-Werror',str(p/'check.cpp'),'-o',str(p/'check')],check=True)
|
||||
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/usr/lib/python3.14/subprocess.py", line 578, in run
|
||||
raise CalledProcessError(retcode, process.args,
|
||||
output=stdout, stderr=stderr)
|
||||
subprocess.CalledProcessError: Command '['g++', '-std=c++17', '-Wall', '-Werror', '/tmp/err-policy-_hmozrxx/check.cpp', '-o', '/tmp/err-policy-_hmozrxx/check']' returned non-zero exit status 1.
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
// Native injection animation/consumption; only resilience is restored over time.
|
||||
// This is not a blood replacement, bandage, full heal, or resurrection.
|
||||
[BaseContainerProps()]
|
||||
class ERR_ConsumableEpinephrine : SCR_ConsumableMorphine
|
||||
{
|
||||
override bool CanApplyEffect(notnull IEntity target, notnull IEntity user, out SCR_EConsumableFailReason failReason)
|
||||
{
|
||||
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(target.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||
CharacterControllerComponent controller = CharacterControllerComponent.Cast(target.FindComponent(CharacterControllerComponent));
|
||||
if (!damage || damage.GetState() == EDamageState.DESTROYED || !controller || !controller.IsUnconscious())
|
||||
{ failReason = SCR_EConsumableFailReason.UNDAMAGED; return false; }
|
||||
array<ref SCR_PersistentDamageEffect> effects = damage.GetAllPersistentEffectsOfType(ERR_EpinephrineEffect);
|
||||
if (!effects.IsEmpty()) { failReason = SCR_EConsumableFailReason.ALREADY_APPLIED; return false; }
|
||||
return true;
|
||||
}
|
||||
}
|
||||
class ERR_EpinephrineEffect : SCR_DotDamageEffect
|
||||
{
|
||||
override bool HijackDamageEffect(SCR_ExtendedDamageManagerComponent dmgManager)
|
||||
{
|
||||
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(dmgManager);
|
||||
if (damage) SetAffectedHitZone(damage.GetResilienceHitZone());
|
||||
return false;
|
||||
}
|
||||
override EDamageType GetDefaultDamageType() { return EDamageType.HEALING; }
|
||||
}
|
||||
// Separate stronger field injector. Standard epinephrine above keeps its
|
||||
// resilience-over-time behavior; neither item restores a dead character.
|
||||
class ERR_CombatEpinephrineRules
|
||||
{
|
||||
static float Heal(float current, bool leg)
|
||||
{
|
||||
float amount = 0.15;
|
||||
if (leg) amount = 0.5;
|
||||
return Math.Min(current + amount, 1.0);
|
||||
}
|
||||
static float Blood(float current, float threshold) { return Math.Min(1, current + threshold + 0.01); }
|
||||
}
|
||||
[BaseContainerProps()]
|
||||
class ERR_ConsumableCombatEpinephrine : SCR_ConsumableMorphine
|
||||
{
|
||||
override bool CanApplyEffect(notnull IEntity target, notnull IEntity user, out SCR_EConsumableFailReason failReason)
|
||||
{
|
||||
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(target.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||
if (!damage || damage.GetState() == EDamageState.DESTROYED) { failReason = SCR_EConsumableFailReason.UNDAMAGED; return false; }
|
||||
CharacterControllerComponent controller = CharacterControllerComponent.Cast(target.FindComponent(CharacterControllerComponent));
|
||||
if (controller && controller.IsUnconscious()) return true;
|
||||
if (damage.IsBleeding()) return true;
|
||||
HitZone blood = damage.GetBloodHitZone(), resilience = damage.GetResilienceHitZone();
|
||||
if ((blood && blood.GetHealthScaled() <= blood.GetDamageStateThreshold(ECharacterBloodState.FAINTING)) || (resilience && resilience.GetHealthScaled() < 1)) return true;
|
||||
array<HitZone> zones = {}; damage.GetPhysicalHitZones(zones);
|
||||
foreach (HitZone zone : zones) if (zone && zone.GetHealthScaled() < 1) return true;
|
||||
failReason = SCR_EConsumableFailReason.UNDAMAGED; return false;
|
||||
}
|
||||
}
|
||||
class ERR_CombatEpinephrineEffect : SCR_InstantDamageEffect
|
||||
{
|
||||
override EDamageType GetDefaultDamageType() { return EDamageType.HEALING; }
|
||||
override void OnEffectAdded(SCR_ExtendedDamageManagerComponent dmgManager)
|
||||
{
|
||||
super.OnEffectAdded(dmgManager);
|
||||
if (!Replication.IsServer()) return;
|
||||
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(dmgManager);
|
||||
if (!damage || damage.GetState() == EDamageState.DESTROYED) return;
|
||||
foreach (ECharacterHitZoneGroup group : {ECharacterHitZoneGroup.HEAD, ECharacterHitZoneGroup.UPPERTORSO, ECharacterHitZoneGroup.LOWERTORSO, ECharacterHitZoneGroup.LEFTARM, ECharacterHitZoneGroup.RIGHTARM, ECharacterHitZoneGroup.LEFTLEG, ECharacterHitZoneGroup.RIGHTLEG}) damage.RemoveGroupBleeding(group);
|
||||
SCR_CharacterBloodHitZone blood = damage.GetBloodHitZone();
|
||||
if (blood && blood.GetHealthScaled() <= blood.GetDamageStateThreshold(ECharacterBloodState.FAINTING))
|
||||
blood.SetHealthScaled(ERR_CombatEpinephrineRules.Blood(blood.GetHealthScaled(), blood.GetDamageStateThreshold(ECharacterBloodState.UNCONSCIOUS)));
|
||||
SCR_CharacterResilienceHitZone resilience = damage.GetResilienceHitZone();
|
||||
if (resilience) resilience.SetHealthScaled(1);
|
||||
array<HitZone> zones = {}; damage.GetPhysicalHitZones(zones);
|
||||
foreach (HitZone zone : zones)
|
||||
{
|
||||
SCR_CharacterHitZone physical = SCR_CharacterHitZone.Cast(zone);
|
||||
if (!physical || zone.GetHealthScaled() >= 1) continue;
|
||||
int group = physical.GetHitZoneGroup();
|
||||
zone.SetHealthScaled(ERR_CombatEpinephrineRules.Heal(zone.GetHealthScaled(), group == ECharacterHitZoneGroup.LEFTLEG || group == ECharacterHitZoneGroup.RIGHTLEG));
|
||||
}
|
||||
CharacterControllerComponent controller = CharacterControllerComponent.Cast(damage.GetOwner().FindComponent(CharacterControllerComponent));
|
||||
if (controller && controller.IsUnconscious()) controller.SetUnconscious(false);
|
||||
}
|
||||
}
|
||||
// The held injector uses the existing native injection interaction and animation.
|
||||
// Only its displayed name changes; ordinary morphine keeps its native behavior.
|
||||
modded class SCR_MorphineUserAction
|
||||
{
|
||||
protected bool m_ERRHoldingEpinephrine;
|
||||
protected bool m_ERRHoldingCombatEpinephrine;
|
||||
override bool CanBeShownScript(IEntity user)
|
||||
{
|
||||
m_ERRHoldingEpinephrine = false; m_ERRHoldingCombatEpinephrine = false;
|
||||
bool shown = super.CanBeShownScript(user);
|
||||
ChimeraCharacter character = ChimeraCharacter.Cast(user);
|
||||
if (shown && character)
|
||||
{
|
||||
SCR_ConsumableItemComponent item = GetConsumableComponent(character);
|
||||
m_ERRHoldingEpinephrine = item && ERR_ConsumableEpinephrine.Cast(item.GetConsumableEffect());
|
||||
m_ERRHoldingCombatEpinephrine = item && ERR_ConsumableCombatEpinephrine.Cast(item.GetConsumableEffect());
|
||||
}
|
||||
return shown;
|
||||
}
|
||||
override bool GetActionNameScript(out string outName)
|
||||
{
|
||||
if (m_ERRHoldingCombatEpinephrine) { outName = "Administer combat epinephrine"; return true; }
|
||||
if (m_ERRHoldingEpinephrine) { outName = "Administer epinephrine"; return true; }
|
||||
return super.GetActionNameScript(outName);
|
||||
}
|
||||
}
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
"""Execute player policy methods; native damage/regen boundaries are modeled."""
|
||||
from policy_harness import ROOT, body, run
|
||||
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerMedical.c'
|
||||
source=p.read_text()
|
||||
methods='\n'.join(body(p,s) for s in ['void ERR_EnableCasualtyProtection(', 'override protected bool IsIndefinitelyUnconscious(', 'override float GetResilienceRegenScale('])
|
||||
methods=methods.replace('override ','').replace('protected ','').replace('Replication.IsServer()','server').replace('super.','Native::')
|
||||
run('''#include <cassert>
|
||||
bool server=true;
|
||||
struct Native {bool unconscious=false,changed=false;float regen=1;int writes=0;bool bleeding=false;
|
||||
void SetPermitUnconsciousness(bool value,bool c){unconscious=value;changed=c;writes++;}
|
||||
void SetRegenScale(float value,bool){regen=value;writes++;}
|
||||
bool IsIndefinitelyUnconscious(bool){return true;}bool IsBleeding(){return bleeding;}float GetResilienceRegenScale(){return .8f;}
|
||||
};
|
||||
struct Policy:Native{bool m_ERRRevivablePlayer=false;
|
||||
'''+methods+'''
|
||||
};
|
||||
int main(){Policy p;server=false;p.ERR_EnableCasualtyProtection();assert(!p.m_ERRRevivablePlayer&&p.writes==0);
|
||||
server=true;p.ERR_EnableCasualtyProtection();assert(p.unconscious&&p.changed&&p.regen==.25f);
|
||||
assert(!p.IsIndefinitelyUnconscious());p.bleeding=true;assert(p.GetResilienceRegenScale()==0);
|
||||
p.bleeding=false;assert(p.GetResilienceRegenScale()==.8f);Policy ai;assert(ai.IsIndefinitelyUnconscious());ai.bleeding=true;assert(ai.GetResilienceRegenScale()==.8f);
|
||||
}
|
||||
''')
|
||||
# Execute the real damage cap across wound/blood scales; healing is untouched.
|
||||
cap=body(p, 'static float AllowedDamage(').replace('Math.Clamp','std::clamp').replace('Math.Min','std::min').replace('Math.Max','std::max').replace('0.01','0.01f').replace(', 1)',', 1.0f)').replace('(0,','(0.0f,')
|
||||
run('''#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
'''+cap+'''
|
||||
int main(){
|
||||
for(float maximum:{100.0f,5000.0f}) for(float threshold:{0.0f,.05f,.2f}) {
|
||||
float floor=maximum*(threshold+.01f);
|
||||
assert(AllowedDamage(-10,maximum,maximum,threshold)==-10);
|
||||
assert(AllowedDamage(100000, floor,maximum,threshold)==0);
|
||||
assert(AllowedDamage(100000, floor/2,maximum,threshold)==0);
|
||||
float health=maximum;
|
||||
for(int i=0;i<1000;i++) health-=AllowedDamage(13,health,maximum,threshold);
|
||||
assert(std::abs(health-floor)<.001f);
|
||||
assert(AllowedDamage(1,maximum,maximum,threshold)==1);
|
||||
}
|
||||
}
|
||||
''')
|
||||
# Passive resilience has its own native rate; treatment DPS bypasses this getter.
|
||||
rate=body(p,'override float CalculatePassiveRegenDPS(').replace('override ','').replace('super.','Native::').replace('SCR_CharacterDamageManagerComponent manager = SCR_CharacterDamageManagerComponent.Cast(GetHitZoneContainer());','Manager* manager = target;').replace('manager.','manager->')
|
||||
run('''#include <cassert>
|
||||
struct Manager { bool player; bool ERR_IsRevivablePlayer(){return player;} };
|
||||
struct Native {float CalculatePassiveRegenDPS(bool){return 4;}};
|
||||
struct Policy:Native {Manager* target=nullptr;
|
||||
'''+rate+'''
|
||||
};
|
||||
int main(){Policy p;assert(p.CalculatePassiveRegenDPS()==4);Manager ai{false};p.target=&ai;assert(p.CalculatePassiveRegenDPS()==4);ai.player=true;assert(p.CalculatePassiveRegenDPS()==1);assert(p.CalculatePassiveRegenDPS(false)==4);}
|
||||
''')
|
||||
assert 'SCR_CharacterResilienceHitZone.Cast(this)) return damage' in source
|
||||
assert 'damage <= 0' in source and '!manager.ERR_IsRevivablePlayer()' in source
|
||||
assert 'override void Kill(' in source
|
||||
assert 'FullHeal(' not in source and 'SetHealth(' not in source
|
||||
print('PASS: authority-scoped slow recovery, bleeding pauses resilience, medicine unaffected, player-only lethal damage cap without healing')
|
||||
# The native bleeding evaluator recomputes severity after the hit has already
|
||||
# lowered health. Exercise production query/cap methods with native armour at
|
||||
# the virtual-call boundary: low remaining health must not erase wound severity.
|
||||
query=body(p, 'float ERR_ComputeWoundDamage(').replace('notnull BaseDamageContext', 'Context')
|
||||
effective=body(p, 'override float ComputeEffectiveDamage(')
|
||||
effective=effective.replace('override ', 'virtual ').replace('notnull BaseDamageContext', 'Context').replace('super.', 'Native::')
|
||||
effective=effective.replace('Replication.IsServer()', 'server').replace('SCR_CharacterResilienceHitZone.Cast(this)', 'resilience')
|
||||
effective=effective.replace('SCR_CharacterDamageManagerComponent manager = SCR_CharacterDamageManagerComponent.Cast(GetHitZoneContainer());', 'Manager* manager = target;').replace('manager.', 'manager->')
|
||||
effective=effective.replace('ERR_PlayerMedicalRules.', '').replace('EDamageState.DESTROYED', '0')
|
||||
run('''#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
bool server=true;
|
||||
struct Context {float value;};
|
||||
struct Manager {bool player=true;int knockouts=0;bool ERR_IsRevivablePlayer(){return player;}void ERR_RequestKnockout(){knockouts++;}};
|
||||
struct Native {virtual float ComputeEffectiveDamage(Context c,bool){return c.value;}};
|
||||
'''+cap+'''
|
||||
struct Policy:Native {
|
||||
bool m_ERRWoundQuery=false,resilience=false;Manager* target=nullptr;float health=1;
|
||||
float GetHealth(){return health;}float GetMaxHealth(){return 100;}float GetDamageStateThreshold(int){return 0;}
|
||||
'''+query+effective+'''
|
||||
};
|
||||
struct Armoured:Policy {float armour=1;float ComputeEffectiveDamage(Context c,bool dot) override {if(!dot)c.value*=armour;return Policy::ComputeEffectiveDamage(c,dot);}};
|
||||
int main(){Manager player;Armoured zone;zone.target=&player;
|
||||
assert(zone.ComputeEffectiveDamage({60},false)==0 && player.knockouts==1);
|
||||
assert(zone.ERR_ComputeWoundDamage({60})==60 && player.knockouts==1);
|
||||
assert(!zone.m_ERRWoundQuery && zone.ComputeEffectiveDamage({60},true)==0);
|
||||
assert(zone.ERR_ComputeWoundDamage({2})==2);
|
||||
zone.armour=.2f;assert(std::abs(zone.ERR_ComputeWoundDamage({60})-12)<.001f);
|
||||
assert(zone.ComputeEffectiveDamage({60},false)==0);
|
||||
zone.health=100;assert(std::abs(zone.ComputeEffectiveDamage({60},false)-12)<.001f);
|
||||
zone.health=1;player.player=false;assert(std::abs(zone.ComputeEffectiveDamage({60},false)-12)<.001f);
|
||||
player.player=true;zone.resilience=true;assert(std::abs(zone.ComputeEffectiveDamage({60},false)-12)<.001f);
|
||||
zone.resilience=false;server=false;assert(std::abs(zone.ComputeEffectiveDamage({60},false)-12)<.001f);
|
||||
}
|
||||
''')
|
||||
print('PASS: bleeding severity survives low health, retains armour, causes no knockout and restores the actual damage/blood-loss cap')
|
||||
# Run the production recovery prediction over real effect states modeled at the
|
||||
# native boundary, including a pending regeneration delay and exhausted effects.
|
||||
future=body(p,'bool ERR_CanRecoverNaturally(')
|
||||
future=future.replace('array<ref SCR_PersistentDamageEffect> effects','auto effects').replace('GetAllPersistentEffectsOfType(SCR_PassiveHitZoneRegenDamageEffect, true)','GetAllPersistentEffectsOfType()')
|
||||
future=future.replace('foreach (SCR_PersistentDamageEffect effect : effects)','for (Effect* effect : effects)').replace('DotDamageEffect dot = DotDamageEffect.Cast(effect);','Effect* dot = effect;').replace('HitZone zone','Zone* zone').replace('dot.','dot->').replace('zone.','zone->').replace('EDamageType.REGENERATION','1')
|
||||
run('''#include <cassert>
|
||||
#include <vector>
|
||||
struct Zone {float value=0;float GetHealthScaled(){return value;}};
|
||||
struct Effect {Zone* zone;float dps;int type=1;int GetDamageType(){return type;}float GetDPS(){return dps;}Zone* GetAffectedHitZone(){return zone;}};
|
||||
struct Policy {bool m_ERRRevivablePlayer=true,bleeding=false;float scale=1;Zone blood,resilience;std::vector<Effect*> effects;
|
||||
bool IsBleeding(){return bleeding;}auto GetAllPersistentEffectsOfType(){return effects;}
|
||||
Zone* GetBloodHitZone(){return &blood;}Zone* GetResilienceHitZone(){return &resilience;}float GetResilienceRegenScale(){return scale;}
|
||||
'''+future+'''
|
||||
};
|
||||
int main(){Policy p;assert(!p.ERR_CanRecoverNaturally());Effect e{&p.blood,-.1f};p.effects={&e};
|
||||
assert(p.ERR_CanRecoverNaturally());p.bleeding=true;assert(!p.ERR_CanRecoverNaturally());p.bleeding=false;
|
||||
p.blood.value=1;assert(!p.ERR_CanRecoverNaturally());p.blood.value=.2;
|
||||
e.dps=0;assert(!p.ERR_CanRecoverNaturally());e.dps=-1;e.type=2;assert(!p.ERR_CanRecoverNaturally());e.type=1;
|
||||
e.zone=&p.resilience;p.scale=0;assert(!p.ERR_CanRecoverNaturally());p.scale=.1;assert(p.ERR_CanRecoverNaturally());
|
||||
p.m_ERRRevivablePlayer=false;assert(!p.ERR_CanRecoverNaturally());
|
||||
}
|
||||
''')
|
||||
print('PASS: viable passive recovery survives native delay; bleeding, disabled/full effects and low-blood resilience do not block defeat')
|
||||
|
||||
|
||||
"""Actual combat injector admission/effect; native component/effect boundaries doubled.
|
||||
No animation, inventory consumption, replication or native base-effect qualification.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import os, re
|
||||
from policy_harness import ROOT, body, run
|
||||
p=Path(os.environ.get('ERR_EPINEPHRINE_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Epinephrine.c'))
|
||||
s=p.read_text()
|
||||
def method(class_name, signature):
|
||||
start=s.index('class '+class_name)
|
||||
a=s.index(signature,start); b=s.index('{',a); end=b+1; depth=1
|
||||
while depth:
|
||||
depth+=(s[end]=='{')-(s[end]=='}'); end+=1
|
||||
return s[a:end]
|
||||
def cpp(t):
|
||||
t=t.replace('override ', '').replace('notnull ', '').replace('out SCR_EConsumableFailReason failReason','SCR_EConsumableFailReason& failReason')
|
||||
t=t.replace('SCR_CharacterDamageManagerComponent.Cast(target.FindComponent(SCR_CharacterDamageManagerComponent))','target->damage')
|
||||
t=t.replace('SCR_CharacterDamageManagerComponent.Cast(dmgManager)','dmgManager')
|
||||
t=t.replace('CharacterControllerComponent.Cast(target.FindComponent(CharacterControllerComponent))','target->controller')
|
||||
t=t.replace('CharacterControllerComponent.Cast(damage.GetOwner().FindComponent(CharacterControllerComponent))','damage->owner->controller')
|
||||
t=t.replace('SCR_CharacterHitZone.Cast(zone)','dynamic_cast<SCR_CharacterHitZone*>(zone)')
|
||||
t=t.replace('array<HitZone>', 'Array<HitZone*>').replace('foreach (', 'for (').replace('null','nullptr')
|
||||
for typ in ('SCR_CharacterDamageManagerComponent','SCR_ExtendedDamageManagerComponent','HitZone','SCR_CharacterBloodHitZone','SCR_CharacterResilienceHitZone','SCR_CharacterHitZone','CharacterControllerComponent'):
|
||||
t=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',t)
|
||||
t=t.replace('HitZone* blood = damage.GetBloodHitZone(), resilience = damage.GetResilienceHitZone();','HitZone* blood = damage.GetBloodHitZone(); HitZone* resilience = damage.GetResilienceHitZone();')
|
||||
for obj in ('damage','blood','resilience','zone','physical','controller'):
|
||||
t=t.replace(obj+'.',obj+'->')
|
||||
for obj in ('EDamageState','SCR_EConsumableFailReason','ECharacterBloodState','ECharacterHitZoneGroup','ERR_CombatEpinephrineRules','Replication','Math'):
|
||||
t=t.replace(obj+'.',obj+'::')
|
||||
t=t.replace('super.', 'Native::')
|
||||
return t
|
||||
rules=cpp(body(p,'class ERR_CombatEpinephrineRules')).replace('class ERR_CombatEpinephrineRules','struct ERR_CombatEpinephrineRules')+';'
|
||||
admit=cpp(method('ERR_ConsumableCombatEpinephrine','override bool CanApplyEffect('))
|
||||
effect=cpp(method('ERR_CombatEpinephrineEffect','override void OnEffectAdded('))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
namespace EDamageState{enum{UNDAMAGED,DESTROYED};}
|
||||
enum class SCR_EConsumableFailReason{NONE,UNDAMAGED};
|
||||
namespace ECharacterBloodState{enum{FAINTING,UNCONSCIOUS};}
|
||||
enum class ECharacterHitZoneGroup{HEAD,UPPERTORSO,LOWERTORSO,LEFTARM,RIGHTARM,LEFTLEG,RIGHTLEG};
|
||||
namespace Replication{bool server=true;bool IsServer(){return server;}}
|
||||
namespace Math{float Min(float a,float b){return std::min(a,b);}}
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;};
|
||||
struct HitZone{float health=1;virtual ~HitZone()=default;float GetHealthScaled(){return health;}void SetHealthScaled(float n){health=n;}virtual float GetDamageStateThreshold(int){return .7f;}};
|
||||
struct SCR_CharacterBloodHitZone:HitZone{float faint=.7f,unconscious=.5f;float GetDamageStateThreshold(int state)override{return state==ECharacterBloodState::FAINTING?faint:unconscious;}};
|
||||
struct SCR_CharacterResilienceHitZone:HitZone{};
|
||||
struct SCR_CharacterHitZone:HitZone{ECharacterHitZoneGroup group=ECharacterHitZoneGroup::LEFTARM;int GetHitZoneGroup(){return int(group);}};
|
||||
// The source compares enum to an int; Enforce permits this conversion.
|
||||
bool operator==(int a,ECharacterHitZoneGroup b){return a==int(b);}
|
||||
struct CharacterControllerComponent{bool unconscious=false;bool IsUnconscious(){return unconscious;}void SetUnconscious(bool n){unconscious=n;}};
|
||||
struct SCR_CharacterDamageManagerComponent;
|
||||
struct Entity{SCR_CharacterDamageManagerComponent* damage=nullptr;CharacterControllerComponent*controller=nullptr;};using IEntity=Entity*;
|
||||
struct SCR_CharacterDamageManagerComponent{Entity*owner=nullptr;int state=EDamageState::UNDAMAGED;bool bleeding=false;SCR_CharacterBloodHitZone*blood=nullptr;SCR_CharacterResilienceHitZone*resilience=nullptr;Array<HitZone*>zones;
|
||||
int GetState(){return state;}bool IsBleeding(){return bleeding;}SCR_CharacterBloodHitZone*GetBloodHitZone(){return blood;}SCR_CharacterResilienceHitZone*GetResilienceHitZone(){return resilience;}void GetPhysicalHitZones(Array<HitZone*>&out){out=zones;}void RemoveGroupBleeding(ECharacterHitZoneGroup){bleeding=false;}
|
||||
};
|
||||
using SCR_ExtendedDamageManagerComponent=SCR_CharacterDamageManagerComponent;
|
||||
// Native inherited effect handling is opaque. No extra health restoration is
|
||||
// fabricated: the authored effect is instant and supplies no generic damage.
|
||||
struct Native{int calls=0;void OnEffectAdded(SCR_ExtendedDamageManagerComponent*){calls++;}};
|
||||
'''+rules+'\nstruct Admission{'+admit+'};\nstruct Effect:Native{'+effect+r'''};
|
||||
struct Fixture{Entity actor;CharacterControllerComponent controller;SCR_CharacterDamageManagerComponent damage;SCR_CharacterBloodHitZone blood;SCR_CharacterResilienceHitZone resilience;SCR_CharacterHitZone arm,leg;Admission admission;Effect effect;
|
||||
Fixture(){actor.damage=&damage;actor.controller=&controller;damage.owner=&actor;damage.blood=&blood;damage.resilience=&resilience;leg.group=ECharacterHitZoneGroup::LEFTLEG;damage.zones={&arm,&leg};Replication::server=true;}
|
||||
bool Allowed(){SCR_EConsumableFailReason reason=SCR_EConsumableFailReason::NONE;return admission.CanApplyEffect(&actor,&actor,reason);}
|
||||
void Apply(){effect.OnEffectAdded(&damage);}
|
||||
};
|
||||
int main(){
|
||||
// Concrete baseline mismatch: mild blood loss is the only injury. Applying the
|
||||
// actual effect changes no health/bleeding/consciousness, so admission must refuse.
|
||||
for(float value:{.7001f,.85f,.999f}){Fixture f;f.blood.health=value;f.Apply();assert(f.effect.calls==1&&f.blood.health==value&&f.resilience.health==1&&f.arm.health==1&&f.leg.health==1&&!f.damage.bleeding&&!f.controller.unconscious);assert(!f.Allowed());}
|
||||
// Native threshold is data-driven, including exact equality; no guessed cutoff.
|
||||
for(float cutoff:{.6f,.7f,.8f}){Fixture f;f.blood.faint=cutoff;f.blood.health=cutoff;assert(f.Allowed());f.Apply();assert(f.blood.health>cutoff);}
|
||||
{Fixture f;f.blood.health=.3f;assert(f.Allowed());f.Apply();assert(f.blood.health>.3f);}
|
||||
// Waking is an independent implemented benefit, including full health.
|
||||
for(float blood:{.9f,1.f}){Fixture f;f.blood.health=blood;f.controller.unconscious=true;assert(f.Allowed());f.Apply();assert(!f.controller.unconscious&&f.blood.health==blood);}
|
||||
{Fixture f;f.controller.unconscious=true;f.damage.state=EDamageState::DESTROYED;assert(!f.Allowed());f.Apply();assert(f.controller.unconscious);}
|
||||
// Preserve all other useful existing benefits even with mildly depleted blood.
|
||||
{Fixture f;f.blood.health=.9f;f.damage.bleeding=true;assert(f.Allowed());f.Apply();assert(!f.damage.bleeding&&f.blood.health==.9f);}
|
||||
{Fixture f;f.blood.health=.9f;f.resilience.health=.2f;f.controller.unconscious=true;assert(f.Allowed());f.Apply();assert(f.resilience.health==1&&!f.controller.unconscious&&f.blood.health==.9f);}
|
||||
{Fixture f;f.blood.health=.9f;f.arm.health=.5f;assert(f.Allowed());f.Apply();assert(std::abs(f.arm.health-.65f)<.00001f);}
|
||||
{Fixture f;f.leg.health=.2f;assert(f.Allowed());f.Apply();assert(std::abs(f.leg.health-.7f)<.00001f);}
|
||||
// A missing controller must not suppress non-consciousness benefits.
|
||||
for(int benefit=0;benefit<4;benefit++){Fixture f;f.actor.controller=nullptr;if(benefit==0)f.damage.bleeding=true;if(benefit==1)f.blood.health=.3f;if(benefit==2)f.resilience.health=.3f;if(benefit==3)f.arm.health=.3f;assert(f.Allowed());f.Apply();}
|
||||
{Fixture f;assert(!f.Allowed());f.actor.damage=nullptr;assert(!f.Allowed());}
|
||||
{Fixture f;f.damage.state=EDamageState::DESTROYED;f.damage.bleeding=true;f.blood.health=.1f;assert(!f.Allowed());f.Apply();assert(f.blood.health==.1f&&f.damage.bleeding);}
|
||||
{Fixture f;f.damage.blood=nullptr;f.damage.resilience=nullptr;assert(!f.Allowed());f.arm.health=.5f;assert(f.Allowed());}
|
||||
{Fixture f;f.blood.health=.1f;Replication::server=false;f.Apply();assert(f.blood.health==.1f);}
|
||||
}
|
||||
''')
|
||||
prefab=(ROOT/'addons/core/Prefabs/ERR/CombatEpinephrine.et').read_text()
|
||||
assert 'm_fItemRegenerationSpeed 0' in prefab and 'm_fItemRegenerationDuration 0' in prefab
|
||||
assert 'ERR_CombatEpinephrineEffect' in prefab
|
||||
print('PASS: actual combat injector admission/effect alignment; mild blood-only refusal, native threshold equality and all existing useful treatments preserved; engine consumption untested')
|
||||
@@ -0,0 +1,23 @@
|
||||
[
|
||||
{
|
||||
"case": "baseline-red",
|
||||
"exit_code": 1,
|
||||
"log": "baseline-red.log",
|
||||
"expected": "runtime assertion failure",
|
||||
"result": "compiled, assertion !f.Allowed() fails for mild blood-only patient"
|
||||
},
|
||||
{
|
||||
"case": "green",
|
||||
"exit_code": 0,
|
||||
"log": "green.log",
|
||||
"expected": 0,
|
||||
"result": "all existing and appended controls pass"
|
||||
},
|
||||
{
|
||||
"case": "consciousness-mutation-red",
|
||||
"exit_code": 1,
|
||||
"log": "consciousness-mutation-red.log",
|
||||
"expected": "runtime assertion failure",
|
||||
"result": "compiled, assertion f.Allowed() fails for unconscious patient"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"head": "9418b6950a12610361d96a99d058e9e10de03713",
|
||||
"files": {
|
||||
"addons/core/Scripts/Game/ERR/00_Core/ERR_Epinephrine.c": {
|
||||
"baseline_sha256": "5fb23182223e7b951a63171959c8701ac439ec290755d7445b2768ccdde489c2",
|
||||
"proposal_sha256": "14a75e040f510773313c7e70b57f338821bc1ded650bc65133968dcad6e49c7c",
|
||||
"index_sha256": "5fb23182223e7b951a63171959c8701ac439ec290755d7445b2768ccdde489c2"
|
||||
},
|
||||
"tests/test_player_medical.py": {
|
||||
"baseline_sha256": "e4b3140a3afc0161db53f261e5480e51f1ca09f85657e25b0efb7dbba3336e3f",
|
||||
"proposal_sha256": "2cd8792af614fe077611c8f7fcd9f5fac2cc0258cdca5444a713cd8262ed7030",
|
||||
"index_sha256": "e4b3140a3afc0161db53f261e5480e51f1ca09f85657e25b0efb7dbba3336e3f"
|
||||
}
|
||||
},
|
||||
"patch_sha256": "276641f08ce7eaac621b146364dee292c7be05eb5ad3546d3513750b2efcb937",
|
||||
"artifacts": {
|
||||
"finding.json": "ff9df65aec786981022aead10b1abebbdd7c1f82a3c6735e1530a13aee5b4496",
|
||||
"consciousness-mutation-red.log": "04a6739ae37c3fa99e8608b798e4066bba114e7c3504b6334acdebd43b1867fb",
|
||||
"mutation-unused-local-compile-failure.log": "d40e852a12dd1cf5f4fa5919c3d68c62d04ac962fcc4e13a60973d86180f7fc2",
|
||||
"results.json": "b89a5ebf49d9e980bdcf457bce5fedea95596560b18eb371c3848118855a8194",
|
||||
"baseline-red.log": "92e380e7077199ec186bd9bcd33a5d43132fd7176b8d3648b9857b8131fe94a0",
|
||||
"without-consciousness-control.c": "566c793adffd3ac34fd765d759fc0c53efe721a59e60bca3a631197408e8c012",
|
||||
"green.log": "015695a0ee8782f7cce3a6527821a00818af2e18c0565b75511d7620b7845095"
|
||||
},
|
||||
"apply_check": true,
|
||||
"working_baselines_exact": true
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
"""Actual combat injector admission/effect; native component/effect boundaries doubled.
|
||||
No animation, inventory consumption, replication or native base-effect qualification.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import os, re
|
||||
from policy_harness import ROOT, body, run
|
||||
p=Path(os.environ.get('ERR_EPINEPHRINE_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Epinephrine.c'))
|
||||
s=p.read_text()
|
||||
def method(class_name, signature):
|
||||
start=s.index('class '+class_name)
|
||||
a=s.index(signature,start); b=s.index('{',a); end=b+1; depth=1
|
||||
while depth:
|
||||
depth+=(s[end]=='{')-(s[end]=='}'); end+=1
|
||||
return s[a:end]
|
||||
def cpp(t):
|
||||
t=t.replace('override ', '').replace('notnull ', '').replace('out SCR_EConsumableFailReason failReason','SCR_EConsumableFailReason& failReason')
|
||||
t=t.replace('SCR_CharacterDamageManagerComponent.Cast(target.FindComponent(SCR_CharacterDamageManagerComponent))','target->damage')
|
||||
t=t.replace('SCR_CharacterDamageManagerComponent.Cast(dmgManager)','dmgManager')
|
||||
t=t.replace('CharacterControllerComponent.Cast(target.FindComponent(CharacterControllerComponent))','target->controller')
|
||||
t=t.replace('CharacterControllerComponent.Cast(damage.GetOwner().FindComponent(CharacterControllerComponent))','damage->owner->controller')
|
||||
t=t.replace('SCR_CharacterHitZone.Cast(zone)','dynamic_cast<SCR_CharacterHitZone*>(zone)')
|
||||
t=t.replace('array<HitZone>', 'Array<HitZone*>').replace('foreach (', 'for (').replace('null','nullptr')
|
||||
for typ in ('SCR_CharacterDamageManagerComponent','SCR_ExtendedDamageManagerComponent','HitZone','SCR_CharacterBloodHitZone','SCR_CharacterResilienceHitZone','SCR_CharacterHitZone','CharacterControllerComponent'):
|
||||
t=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',t)
|
||||
t=t.replace('HitZone* blood = damage.GetBloodHitZone(), resilience = damage.GetResilienceHitZone();','HitZone* blood = damage.GetBloodHitZone(); HitZone* resilience = damage.GetResilienceHitZone();')
|
||||
for obj in ('damage','blood','resilience','zone','physical','controller'):
|
||||
t=t.replace(obj+'.',obj+'->')
|
||||
for obj in ('EDamageState','SCR_EConsumableFailReason','ECharacterBloodState','ECharacterHitZoneGroup','ERR_CombatEpinephrineRules','Replication','Math'):
|
||||
t=t.replace(obj+'.',obj+'::')
|
||||
t=t.replace('super.', 'Native::')
|
||||
return t
|
||||
rules=cpp(body(p,'class ERR_CombatEpinephrineRules')).replace('class ERR_CombatEpinephrineRules','struct ERR_CombatEpinephrineRules')+';'
|
||||
admit=cpp(method('ERR_ConsumableCombatEpinephrine','override bool CanApplyEffect('))
|
||||
effect=cpp(method('ERR_CombatEpinephrineEffect','override void OnEffectAdded('))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
namespace EDamageState{enum{UNDAMAGED,DESTROYED};}
|
||||
enum class SCR_EConsumableFailReason{NONE,UNDAMAGED};
|
||||
namespace ECharacterBloodState{enum{FAINTING,UNCONSCIOUS};}
|
||||
enum class ECharacterHitZoneGroup{HEAD,UPPERTORSO,LOWERTORSO,LEFTARM,RIGHTARM,LEFTLEG,RIGHTLEG};
|
||||
namespace Replication{bool server=true;bool IsServer(){return server;}}
|
||||
namespace Math{float Min(float a,float b){return std::min(a,b);}}
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;};
|
||||
struct HitZone{float health=1;virtual ~HitZone()=default;float GetHealthScaled(){return health;}void SetHealthScaled(float n){health=n;}virtual float GetDamageStateThreshold(int){return .7f;}};
|
||||
struct SCR_CharacterBloodHitZone:HitZone{float faint=.7f,unconscious=.5f;float GetDamageStateThreshold(int state)override{return state==ECharacterBloodState::FAINTING?faint:unconscious;}};
|
||||
struct SCR_CharacterResilienceHitZone:HitZone{};
|
||||
struct SCR_CharacterHitZone:HitZone{ECharacterHitZoneGroup group=ECharacterHitZoneGroup::LEFTARM;int GetHitZoneGroup(){return int(group);}};
|
||||
// The source compares enum to an int; Enforce permits this conversion.
|
||||
bool operator==(int a,ECharacterHitZoneGroup b){return a==int(b);}
|
||||
struct CharacterControllerComponent{bool unconscious=false;bool IsUnconscious(){return unconscious;}void SetUnconscious(bool n){unconscious=n;}};
|
||||
struct SCR_CharacterDamageManagerComponent;
|
||||
struct Entity{SCR_CharacterDamageManagerComponent* damage=nullptr;CharacterControllerComponent*controller=nullptr;};using IEntity=Entity*;
|
||||
struct SCR_CharacterDamageManagerComponent{Entity*owner=nullptr;int state=EDamageState::UNDAMAGED;bool bleeding=false;SCR_CharacterBloodHitZone*blood=nullptr;SCR_CharacterResilienceHitZone*resilience=nullptr;Array<HitZone*>zones;
|
||||
int GetState(){return state;}bool IsBleeding(){return bleeding;}SCR_CharacterBloodHitZone*GetBloodHitZone(){return blood;}SCR_CharacterResilienceHitZone*GetResilienceHitZone(){return resilience;}void GetPhysicalHitZones(Array<HitZone*>&out){out=zones;}void RemoveGroupBleeding(ECharacterHitZoneGroup){bleeding=false;}
|
||||
};
|
||||
using SCR_ExtendedDamageManagerComponent=SCR_CharacterDamageManagerComponent;
|
||||
// Native inherited effect handling is opaque. No extra health restoration is
|
||||
// fabricated: the authored effect is instant and supplies no generic damage.
|
||||
struct Native{int calls=0;void OnEffectAdded(SCR_ExtendedDamageManagerComponent*){calls++;}};
|
||||
'''+rules+'\nstruct Admission{'+admit+'};\nstruct Effect:Native{'+effect+r'''};
|
||||
struct Fixture{Entity actor;CharacterControllerComponent controller;SCR_CharacterDamageManagerComponent damage;SCR_CharacterBloodHitZone blood;SCR_CharacterResilienceHitZone resilience;SCR_CharacterHitZone arm,leg;Admission admission;Effect effect;
|
||||
Fixture(){actor.damage=&damage;actor.controller=&controller;damage.owner=&actor;damage.blood=&blood;damage.resilience=&resilience;leg.group=ECharacterHitZoneGroup::LEFTLEG;damage.zones={&arm,&leg};Replication::server=true;}
|
||||
bool Allowed(){SCR_EConsumableFailReason reason=SCR_EConsumableFailReason::NONE;return admission.CanApplyEffect(&actor,&actor,reason);}
|
||||
void Apply(){effect.OnEffectAdded(&damage);}
|
||||
};
|
||||
int main(){
|
||||
// Concrete baseline mismatch: mild blood loss is the only injury. Applying the
|
||||
// actual effect changes no health/bleeding/consciousness, so admission must refuse.
|
||||
for(float value:{.7001f,.85f,.999f}){Fixture f;f.blood.health=value;f.Apply();assert(f.effect.calls==1&&f.blood.health==value&&f.resilience.health==1&&f.arm.health==1&&f.leg.health==1&&!f.damage.bleeding&&!f.controller.unconscious);assert(!f.Allowed());}
|
||||
// Native threshold is data-driven, including exact equality; no guessed cutoff.
|
||||
for(float cutoff:{.6f,.7f,.8f}){Fixture f;f.blood.faint=cutoff;f.blood.health=cutoff;assert(f.Allowed());f.Apply();assert(f.blood.health>cutoff);}
|
||||
{Fixture f;f.blood.health=.3f;assert(f.Allowed());f.Apply();assert(f.blood.health>.3f);}
|
||||
// Waking is an independent implemented benefit, including full health.
|
||||
for(float blood:{.9f,1.f}){Fixture f;f.blood.health=blood;f.controller.unconscious=true;assert(f.Allowed());f.Apply();assert(!f.controller.unconscious&&f.blood.health==blood);}
|
||||
{Fixture f;f.controller.unconscious=true;f.damage.state=EDamageState::DESTROYED;assert(!f.Allowed());f.Apply();assert(f.controller.unconscious);}
|
||||
// Preserve all other useful existing benefits even with mildly depleted blood.
|
||||
{Fixture f;f.blood.health=.9f;f.damage.bleeding=true;assert(f.Allowed());f.Apply();assert(!f.damage.bleeding&&f.blood.health==.9f);}
|
||||
{Fixture f;f.blood.health=.9f;f.resilience.health=.2f;f.controller.unconscious=true;assert(f.Allowed());f.Apply();assert(f.resilience.health==1&&!f.controller.unconscious&&f.blood.health==.9f);}
|
||||
{Fixture f;f.blood.health=.9f;f.arm.health=.5f;assert(f.Allowed());f.Apply();assert(std::abs(f.arm.health-.65f)<.00001f);}
|
||||
{Fixture f;f.leg.health=.2f;assert(f.Allowed());f.Apply();assert(std::abs(f.leg.health-.7f)<.00001f);}
|
||||
// A missing controller must not suppress non-consciousness benefits.
|
||||
for(int benefit=0;benefit<4;benefit++){Fixture f;f.actor.controller=nullptr;if(benefit==0)f.damage.bleeding=true;if(benefit==1)f.blood.health=.3f;if(benefit==2)f.resilience.health=.3f;if(benefit==3)f.arm.health=.3f;assert(f.Allowed());f.Apply();}
|
||||
{Fixture f;assert(!f.Allowed());f.actor.damage=nullptr;assert(!f.Allowed());}
|
||||
{Fixture f;f.damage.state=EDamageState::DESTROYED;f.damage.bleeding=true;f.blood.health=.1f;assert(!f.Allowed());f.Apply();assert(f.blood.health==.1f&&f.damage.bleeding);}
|
||||
{Fixture f;f.damage.blood=nullptr;f.damage.resilience=nullptr;assert(!f.Allowed());f.arm.health=.5f;assert(f.Allowed());}
|
||||
{Fixture f;f.blood.health=.1f;Replication::server=false;f.Apply();assert(f.blood.health==.1f);}
|
||||
}
|
||||
''')
|
||||
prefab=(ROOT/'addons/core/Prefabs/ERR/CombatEpinephrine.et').read_text()
|
||||
assert 'm_fItemRegenerationSpeed 0' in prefab and 'm_fItemRegenerationDuration 0' in prefab
|
||||
assert 'ERR_CombatEpinephrineEffect' in prefab
|
||||
print('PASS: actual combat injector admission/effect alignment; mild blood-only refusal, native threshold equality and all existing useful treatments preserved; engine consumption untested')
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
// Native injection animation/consumption; only resilience is restored over time.
|
||||
// This is not a blood replacement, bandage, full heal, or resurrection.
|
||||
[BaseContainerProps()]
|
||||
class ERR_ConsumableEpinephrine : SCR_ConsumableMorphine
|
||||
{
|
||||
override bool CanApplyEffect(notnull IEntity target, notnull IEntity user, out SCR_EConsumableFailReason failReason)
|
||||
{
|
||||
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(target.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||
CharacterControllerComponent controller = CharacterControllerComponent.Cast(target.FindComponent(CharacterControllerComponent));
|
||||
if (!damage || damage.GetState() == EDamageState.DESTROYED || !controller || !controller.IsUnconscious())
|
||||
{ failReason = SCR_EConsumableFailReason.UNDAMAGED; return false; }
|
||||
array<ref SCR_PersistentDamageEffect> effects = damage.GetAllPersistentEffectsOfType(ERR_EpinephrineEffect);
|
||||
if (!effects.IsEmpty()) { failReason = SCR_EConsumableFailReason.ALREADY_APPLIED; return false; }
|
||||
return true;
|
||||
}
|
||||
}
|
||||
class ERR_EpinephrineEffect : SCR_DotDamageEffect
|
||||
{
|
||||
override bool HijackDamageEffect(SCR_ExtendedDamageManagerComponent dmgManager)
|
||||
{
|
||||
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(dmgManager);
|
||||
if (damage) SetAffectedHitZone(damage.GetResilienceHitZone());
|
||||
return false;
|
||||
}
|
||||
override EDamageType GetDefaultDamageType() { return EDamageType.HEALING; }
|
||||
}
|
||||
// Separate stronger field injector. Standard epinephrine above keeps its
|
||||
// resilience-over-time behavior; neither item restores a dead character.
|
||||
class ERR_CombatEpinephrineRules
|
||||
{
|
||||
static float Heal(float current, bool leg)
|
||||
{
|
||||
float amount = 0.15;
|
||||
if (leg) amount = 0.5;
|
||||
return Math.Min(current + amount, 1.0);
|
||||
}
|
||||
static float Blood(float current, float threshold) { return Math.Min(1, current + threshold + 0.01); }
|
||||
}
|
||||
[BaseContainerProps()]
|
||||
class ERR_ConsumableCombatEpinephrine : SCR_ConsumableMorphine
|
||||
{
|
||||
override bool CanApplyEffect(notnull IEntity target, notnull IEntity user, out SCR_EConsumableFailReason failReason)
|
||||
{
|
||||
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(target.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||
if (!damage || damage.GetState() == EDamageState.DESTROYED) { failReason = SCR_EConsumableFailReason.UNDAMAGED; return false; }
|
||||
if (damage.IsBleeding()) return true;
|
||||
HitZone blood = damage.GetBloodHitZone(), resilience = damage.GetResilienceHitZone();
|
||||
if ((blood && blood.GetHealthScaled() <= blood.GetDamageStateThreshold(ECharacterBloodState.FAINTING)) || (resilience && resilience.GetHealthScaled() < 1)) return true;
|
||||
array<HitZone> zones = {}; damage.GetPhysicalHitZones(zones);
|
||||
foreach (HitZone zone : zones) if (zone && zone.GetHealthScaled() < 1) return true;
|
||||
failReason = SCR_EConsumableFailReason.UNDAMAGED; return false;
|
||||
}
|
||||
}
|
||||
class ERR_CombatEpinephrineEffect : SCR_InstantDamageEffect
|
||||
{
|
||||
override EDamageType GetDefaultDamageType() { return EDamageType.HEALING; }
|
||||
override void OnEffectAdded(SCR_ExtendedDamageManagerComponent dmgManager)
|
||||
{
|
||||
super.OnEffectAdded(dmgManager);
|
||||
if (!Replication.IsServer()) return;
|
||||
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(dmgManager);
|
||||
if (!damage || damage.GetState() == EDamageState.DESTROYED) return;
|
||||
foreach (ECharacterHitZoneGroup group : {ECharacterHitZoneGroup.HEAD, ECharacterHitZoneGroup.UPPERTORSO, ECharacterHitZoneGroup.LOWERTORSO, ECharacterHitZoneGroup.LEFTARM, ECharacterHitZoneGroup.RIGHTARM, ECharacterHitZoneGroup.LEFTLEG, ECharacterHitZoneGroup.RIGHTLEG}) damage.RemoveGroupBleeding(group);
|
||||
SCR_CharacterBloodHitZone blood = damage.GetBloodHitZone();
|
||||
if (blood && blood.GetHealthScaled() <= blood.GetDamageStateThreshold(ECharacterBloodState.FAINTING))
|
||||
blood.SetHealthScaled(ERR_CombatEpinephrineRules.Blood(blood.GetHealthScaled(), blood.GetDamageStateThreshold(ECharacterBloodState.UNCONSCIOUS)));
|
||||
SCR_CharacterResilienceHitZone resilience = damage.GetResilienceHitZone();
|
||||
if (resilience) resilience.SetHealthScaled(1);
|
||||
array<HitZone> zones = {}; damage.GetPhysicalHitZones(zones);
|
||||
foreach (HitZone zone : zones)
|
||||
{
|
||||
SCR_CharacterHitZone physical = SCR_CharacterHitZone.Cast(zone);
|
||||
if (!physical || zone.GetHealthScaled() >= 1) continue;
|
||||
int group = physical.GetHitZoneGroup();
|
||||
zone.SetHealthScaled(ERR_CombatEpinephrineRules.Heal(zone.GetHealthScaled(), group == ECharacterHitZoneGroup.LEFTLEG || group == ECharacterHitZoneGroup.RIGHTLEG));
|
||||
}
|
||||
CharacterControllerComponent controller = CharacterControllerComponent.Cast(damage.GetOwner().FindComponent(CharacterControllerComponent));
|
||||
if (controller && controller.IsUnconscious()) controller.SetUnconscious(false);
|
||||
}
|
||||
}
|
||||
// The held injector uses the existing native injection interaction and animation.
|
||||
// Only its displayed name changes; ordinary morphine keeps its native behavior.
|
||||
modded class SCR_MorphineUserAction
|
||||
{
|
||||
protected bool m_ERRHoldingEpinephrine;
|
||||
protected bool m_ERRHoldingCombatEpinephrine;
|
||||
override bool CanBeShownScript(IEntity user)
|
||||
{
|
||||
m_ERRHoldingEpinephrine = false; m_ERRHoldingCombatEpinephrine = false;
|
||||
bool shown = super.CanBeShownScript(user);
|
||||
ChimeraCharacter character = ChimeraCharacter.Cast(user);
|
||||
if (shown && character)
|
||||
{
|
||||
SCR_ConsumableItemComponent item = GetConsumableComponent(character);
|
||||
m_ERRHoldingEpinephrine = item && ERR_ConsumableEpinephrine.Cast(item.GetConsumableEffect());
|
||||
m_ERRHoldingCombatEpinephrine = item && ERR_ConsumableCombatEpinephrine.Cast(item.GetConsumableEffect());
|
||||
}
|
||||
return shown;
|
||||
}
|
||||
override bool GetActionNameScript(out string outName)
|
||||
{
|
||||
if (m_ERRHoldingCombatEpinephrine) { outName = "Administer combat epinephrine"; return true; }
|
||||
if (m_ERRHoldingEpinephrine) { outName = "Administer epinephrine"; return true; }
|
||||
return super.GetActionNameScript(outName);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user