Embed RHS 1.9 compatibility; record Workshop 0.0.141
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-11-05 16:39:35 +01:00
parent 9418b6950a
commit 9caed3f12d
325 changed files with 76722 additions and 25 deletions
@@ -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);
}
}
@@ -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')