Skip to content

Commit

Permalink
Reimplement Raise Dead (and related) logic (#797)
Browse files Browse the repository at this point in the history
* Reimplement resurrection logic with aasimar/tiefling cases

* Try to address some build errors.

* More fixes

Lift local variables out of switch

Use dispatcher key type instead of query/signal

* Add some logging to observe what's going on with Raise Dead

* Use info instead of debug

* A bit more logging to try to see what's going on.

* Logging type errors.

* Another try to see any output from replaced Resurrect

* Try modifying Resurrect replacement type

I was assuming that the rebased version had a correct type, but perhaps
it wasn't.

* More logging, tweaked Resurrect result.

* Try implementing ResurrectApplyPenalties

Pulling the one from the DLL didn't seem to be working.

* Fix errors

* Trying to figure out Resurrect API

- Stripped out most logging
- Rolled back the replacement function to take the unknown argument and
  return a result. Logging the unknown argument to see what it is in
  some cases.
- Casting Raise Dead causes a double message, so one theory is that the
  unknown argument determines whether the resurrection function should
  float text.

* Type error

* Unknown argument is caster level

* Remove logging

* Add native outsiders and use them in resurrection check

Removed Darley check in anticipation of just tagging her as native.

* Add mc_subtype_native to Darley proto overrides

I think this won't parse yet, but we'll see.

* Add logging to make sure correct path is being followed.

* Use correct hit dice numbers in resurrection

* C++ brackets

Removed informative logging.

* Parse mc_subtype_native in protos

* Add mc_subtype_native to Tiefling and Aasimar

* There are two protos for each race

* Remove Raise Dead script workaround

* Eliminate XP setting from Raise Dead script

The original temple dll set the XP, but it seems like the value it set
was incorrect. I've made the reimplementation set the right XP in the
case where it isn't adding a negative level (which already resets the
target's XP). This should also have the effect of making the set XP
value correct w/r/t level drain, level adjustment, and racial hit dice,
which the script wasn't (because I don't think it has any easy way to
get that number right now).

* Capitalization error

* Tweak raise penalty XP

Using hit dice is not correct. The penalty XP should be for 1 below the
creature's effective level.

* Make the object script call in raise dead a bit more obvious

* Renamek 'unknown' variable in python resurrect()

* Fix perm level drain XP penalty to account for ECL

* Add filtering to GetLevel query

* Attempt to fix some errors

* More error fixes

* Fix level drain filtering expressions

* Try an alternate namespacing directive

* Give up on enum class `using`

* Add a function for effective drained level, and uses.

* Fix a bad variable reference

* Set target level for perm levels based on ECL.

This controls when XP gain removes the drained level. Setting it based
on just the hit dice num means that level adjusted characters get back
their levels too early.

* Get rid of float message in resurrection function

Messages are already floated in the spell conditions, so only other
scripts, like Cuthbert/Iuz resurrection would be needing these, and they
could test the result and float their own lines if they wanted. Probably
not necessary, though.

* Replace Jaroo permanent spell implementation

The existing one used his Reincarnate spell to clear the character's
status by killing them first. Now that will add a negative level on
strict rules. That could be worked around, but instead I just changed
the script to fake him casting the spell, and used the Cuthbert
resurrection mode which has no negative effects and is completely
unrestricted. This makes it insensitive to any changes to Reincarnate,
as well.

* Give Reincarnate script the same treatment as the others.

No XP setting and san_resurrect instead of constant 18

* Do san_resurrect in Jaroo fix, and document a bit more

* Move the san_resurrect call directly into the resurrect function

Avoids needing to figure out how to get the right constant in the Jaroo
file, and I think it might fix some other cases where the resurrect
function is called directly without also calling the object script.

* Change filtering logic for built-in negative level conditions.

The existing logic was forgetting about the various aligned equipment
conditions as sources of negative levels. They share the function that
applies the negative level, but have various names other than
`Temp Negative Level`.

* Operator precedence of new logic was wrong

* Add some features to forget spell functions

- Forgotten spells can be moved to pending instead of deleted from the
  array
- A percentage chance can be provided so not every spell is forgotten

* Fix some issues with the spell functions

* Add 50% spell forget chance to Raise Dead

* Add a function for using up a spontaneous slot

* Fix some typos

* Add spontaneous multi-deduction function

* Fix typo

* More typos

* Fix another mismatch

* Expose spontaneous spell deduction in python

* Attempt to fix problem with python wrapper

* Set bit in class code when deducting spells

For some reason the class code for spells needs to have the 7th
bit set, it seems.

* Missed a renaming

* Deduct spontaneous spells when raising
  • Loading branch information
dolio authored Dec 17, 2024
1 parent 1489821 commit b3e726d
Show file tree
Hide file tree
Showing 21 changed files with 373 additions and 194 deletions.
160 changes: 155 additions & 5 deletions TemplePlus/critter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "tig/tig_startup.h"
#include "util/fixes.h"
#include "gamesystems/particlesystems.h"
#include "animgoals/anim.h"
#include <graphics/mdfmaterials.h>
#include <infrastructure/meshes.h>
#include <infrastructure/vfs.h>
Expand All @@ -32,6 +33,7 @@
#include "rng.h"
#include "weapon.h"
#include "d20_race.h"
#include "d20_level.h"
#include "location.h"


Expand All @@ -57,8 +59,6 @@ static struct CritterAddresses : temple::AddressTable {
void (__cdecl *BalorDeath)(objHndl critter);
void (__cdecl *SetConcealed)(objHndl critter, int concealed);

uint32_t(__cdecl *Resurrect)(objHndl critter, ResurrectType type, int unk);

uint32_t(__cdecl *IsDeadOrUnconscious)(objHndl critter);

objHndl (__cdecl *GiveItem)(objHndl critter, int protoId);
Expand Down Expand Up @@ -89,7 +89,6 @@ static struct CritterAddresses : temple::AddressTable {
rebase(SetSubdualDamage, 0x1001DB10);
rebase(BalorDeath, 0x100F66F0);
rebase(SetConcealed, 0x10080670);
rebase(Resurrect, 0x100809C0);
rebase(IsDeadOrUnconscious, 0x100803E0);
rebase(GiveItem, 0x1006CC30);

Expand Down Expand Up @@ -178,6 +177,10 @@ class CritterReplacements : public TempleFix
replaceFunction<int(__cdecl)(objHndl,objHndl,objHndl,int)>(0x10020B60, [](objHndl wielder, objHndl prim, objHndl scnd, int animId) {
return (int)critterSys.GetWeaponAnim(wielder, prim, scnd, (gfx::WeaponAnim)animId);
});

replaceFunction<uint32_t(__cdecl)(objHndl,ResurrectType,int)>(0x100809C0, [](objHndl critter, ResurrectType type, int clvl) {
return critterSys.Resurrect(critter, type, clvl);
});
}

private:
Expand Down Expand Up @@ -914,8 +917,132 @@ void LegacyCritterSystem::SetConcealedWithFollowers(objHndl obj, int newState){
}
}

uint32_t LegacyCritterSystem::Resurrect(objHndl critter, ResurrectType type, int unk) {
return addresses.Resurrect(critter, type, unk);
bool LegacyCritterSystem::ShouldResurrect(objHndl critter, ResurrectType type) {
// I think this is the intended check in the original. Seems like null should
// still short circuit, though. TBD.
if (!IsDeadNullDestroyed(critter)) return false;

auto category = GetCategory(critter);
auto protoid = objSystem->GetProtoId(critter);
auto deathEffect = conds.GetByName("Killed By Death Effect");
auto hd = objects.GetHitDiceNum(critter, false);
auto con = objects.StatLevelGetBase(critter, stat_constitution);

// Note: cases here intentionally fall through to avoid duplicating tests.
switch (type)
{
case ResurrectType::CuthbertResurrect:
return true; // gods do what they want
case ResurrectType::RaiseDead:
// creatures killed by death effects can't be raised
if (d20Sys.d20QueryWithData(critter, DK_QUE_Critter_Has_Condition, deathEffect, 0) == 1)
return false;
case ResurrectType::Resurrect:
// resurrect/raise costs 1 hit dice or 2 constitution; can't do it
// if you can't pay.
if (hd < 1 || hd == 1 && con < 3) return false;

switch (category)
{
case mc_type_outsider:
// Native outsiders can be raised/resurrected. Others can't.
if (IsCategorySubtype(critter, mc_subtype_native)) break;

case mc_type_elemental:
return false;
default:
break;
}
case ResurrectType::ResurrectTrue:
// Even true resurrection doesn't work on constructs or undead.
// In principle you can true resurrect an undead creature to the
// _original_ creature, but we'd need to know what that is.
switch (category)
{
case mc_type_construct:
case mc_type_undead:
return false;
}

// all checks passed
return true;

default:
return false;
}
}

uint32_t LegacyCritterSystem::Resurrect(objHndl critter, ResurrectType type, int casterLvl) {
uint32_t result = 0;
if (ShouldResurrect(critter, type)) {
result = 1;
ResurrectApplyPenalties(critter, type);
}
d20Sys.d20SendSignal(critter, DK_SIG_Resurrection, 0, 0);
return result;
}

void LegacyCritterSystem::ResurrectApplyPenalties(objHndl critter, ResurrectType type) {
auto hd = objects.GetHitDiceNum(critter, false);
bool damaged = false;
int con = 0;
CondStruct *negLevel;
vector<int> negArgs({0, 0, 0});

// Note: intentional fallthrough for common logic.
switch (type)
{
case ResurrectType::RaiseDead:
// Raise Dead causes a 50% chance of losing each prepared spell.
//
// Note: this will also affect Reincarnate, because it just applies
// the same condition. The books don't indicate that it behaves this
// way, so possibly revisit this later (with a new enum value perhaps).
if (config.stricterRulesEnforcement) {
spellSys.ForgetMemorized(critter, true, 50);
spellSys.DeductSpontaneous(critter, static_cast<Stat>(-1), 50);
}

// Raise dead leaves the target with 1 hp/hit die
damaged = true;
case ResurrectType::Resurrect:
// Raise and Resurrect reduce level/hit dice by 1 or reduce constitution.
if (hd > 1) {
// The level loss wasn't in the original game, so test for strict rules.
if (config.stricterRulesEnforcement) {
negLevel = conds.GetByName("Perm Negative Level");
conds.AddTo(critter, negLevel, negArgs);
} else {
// The original game just took away XP.
auto effLv = GetEffectiveDrainedLevel(critter);
auto newXp = d20LevelSys.GetPenaltyXPForDrainedLevel(effLv-1);
objects.setInt32(critter, obj_f_critter_experience, newXp);
}
} else {
// The resurrection check should have already ensured this doesn't go
// negative.
con = objects.StatLevelGetBase(critter, stat_constitution);
objects.StatLevelSetBase(critter, stat_constitution, con-2);
}
case ResurrectType::CuthbertResurrect:
case ResurrectType::ResurrectTrue:
// No negative consequences
break;
}

// reset damage
if (damaged) {
auto maxHp = objects.StatLevelGet(critter, Stat::stat_hp_max);
// hit dice might have been reduced
hd = objects.GetHitDiceNum(critter, false);
SetHpDamage(critter, maxHp - hd);
} else {
SetHpDamage(critter, 0);
}
gameSystems->GetAnim().PushAnimate(critter, 67);

// Let the game know the critter has been resurrected, for e.g. plot purposes
pythonObjIntegration.ExecuteObjectScript(critter, critter, ObjScriptEvent::Resurrect);
}

uint32_t LegacyCritterSystem::Dominate(objHndl critter, objHndl caster) {
Expand Down Expand Up @@ -1021,6 +1148,10 @@ Race LegacyCritterSystem::GetRace(objHndl critter, bool getBaseRace) {
return (Race)race;
}

Subrace LegacyCritterSystem::GetSubrace(objHndl critter) {
return (Subrace)objects.StatLevelGet(critter, stat_subrace);
}

Gender LegacyCritterSystem::GetGender(objHndl critter) {
return (Gender)objects.StatLevelGet(critter, stat_gender);
}
Expand Down Expand Up @@ -1799,6 +1930,25 @@ int LegacyCritterSystem::GetEffectiveLevel(objHndl & objHnd)
return lvl;
}

int LegacyCritterSystem::GetEffectiveDrainedLevel(objHndl & critter, LevelDrainType incl)
{
if (!critter) return 0;

LevelDrainType omit = ~incl;
auto lvl = dispatch.Dispatch61GetLevel(critter, stat_level, nullptr, objHndl::null, omit);
auto lvlAdj = d20RaceSys.GetLevelAdjustment(critter);
auto racialHdCount = 0;
auto ocritter = objSystem->GetObject(critter);

if (ocritter->IsPC()) {
Dice racialHd = d20RaceSys.GetHitDice(critterSys.GetRace(critter, false));
racialHdCount = racialHd.GetCount();
} else {
racialHdCount = ocritter->GetInt32(obj_f_npc_hitdice_idx, 0);
}
return lvl + lvlAdj + racialHdCount;
}

int LegacyCritterSystem::GetCritterAttackType(objHndl obj, int attackIdx)
{
int damageIdx = GetDamageIdx(obj, attackIdx);
Expand Down
7 changes: 7 additions & 0 deletions TemplePlus/critter.h
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,8 @@ struct LegacyCritterSystem : temple::AddressTable
Third argument seems unused.
*/
uint32_t Resurrect(objHndl critter, ResurrectType type, int unk);
bool ShouldResurrect(objHndl critter, ResurrectType type);
void ResurrectApplyPenalties(objHndl critter, ResurrectType type);

/*
Dominates a critter.
Expand Down Expand Up @@ -324,11 +326,16 @@ struct LegacyCritterSystem : temple::AddressTable
int GetSize(objHndl handle);

int GetEffectiveLevel(objHndl& objHnd); // Get Effective Character Level (used for determining XP gain / requirements)

// Get Effective Character Level, including level drain effects.
// Default excludes temporary negative levels.
int GetEffectiveDrainedLevel(objHndl& critter, LevelDrainType incl = LevelDrainType::DrainedOrLostLevel);
int GetLevel(objHndl critter);

int SkillLevel(objHndl critter, SkillEnum skill);

Race GetRace(objHndl critter, bool getBaseRace = true);
Subrace GetSubrace(objHndl critter);

Gender GetGender(objHndl critter);

Expand Down
9 changes: 9 additions & 0 deletions TemplePlus/d20_level.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,15 @@ uint32_t D20LevelSystem::GetXpRequireForLevel(uint32_t level)

}


uint32_t D20LevelSystem::GetPenaltyXPForDrainedLevel(uint32_t level)
{
auto xp0 = GetXpRequireForLevel(level);
auto xp1 = GetXpRequireForLevel(level+1);

return (xp0 >=0 && xp1 > 0 && xp1 > xp0 ) ? (xp0 + xp1) / 2 : 0;
}

int D20LevelSystem::GetSurplusXp(objHndl handle){
auto lvl = objects.StatLevelGet(handle, stat_level);
if (lvl > 1){
Expand Down
1 change: 1 addition & 0 deletions TemplePlus/d20_level.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class D20LevelSystem
}

uint32_t GetXpRequireForLevel(uint32_t level);
uint32_t GetPenaltyXPForDrainedLevel(uint32_t level);
int GetSurplusXp(objHndl handle);
int GetSpellsPerLevel(const objHndl handle, Stat classCode, int spellLvl, int casterLvl);
private:
Expand Down
3 changes: 2 additions & 1 deletion TemplePlus/dispatcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -973,7 +973,7 @@ int DispatcherSystem::Dispatch60GetAttackDice(objHndl obj, DispIoAttackDice* dis

}

int DispatcherSystem::Dispatch61GetLevel(objHndl handle, Stat stat, BonusList* bonlist, objHndl someObj)
int DispatcherSystem::Dispatch61GetLevel(objHndl handle, Stat stat, BonusList* bonlist, objHndl someObj, LevelDrainType omit)
{
auto obj = objSystem->GetObject(handle);
if (!obj)
Expand All @@ -983,6 +983,7 @@ int DispatcherSystem::Dispatch61GetLevel(objHndl handle, Stat stat, BonusList* b
return 0;
DispIoObjBonus evtObj;
evtObj.obj = someObj;
evtObj.flags = static_cast<uint32_t>(omit);
if (bonlist) {
evtObj.bonOut = bonlist;
}
Expand Down
2 changes: 1 addition & 1 deletion TemplePlus/dispatcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ struct DispatcherSystem : temple::AddressTable
void DispatchSpellDamage(objHndl obj, DamagePacket* damage, objHndl target, SpellPacketBody *spellPkt);
int DispatchD20ActionCheck(D20Actn* d20Actn, TurnBasedStatus* turnBasedStatus, enum_disp_type dispType);
int Dispatch60GetAttackDice(objHndl obj, DispIoAttackDice * dispIo);
int Dispatch61GetLevel(objHndl obj, Stat stat, BonusList* bonlist = nullptr, objHndl someObj = objHndl::null); // get level after accounting for level drain effects
int Dispatch61GetLevel(objHndl obj, Stat stat, BonusList* bonlist = nullptr, objHndl someObj = objHndl::null, LevelDrainType omit = LevelDrainType::NoDrainedLevel); // get level after accounting for level drain effects

int DispatchGetBonus(objHndl critter, DispIoBonusList* bonusListOut, enum_disp_type dispType, D20DispatcherKey key);

Expand Down
31 changes: 26 additions & 5 deletions TemplePlus/general_condition_fixes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,29 @@ class GeneralConditionFixes : public TempleFix {
replaceFunction(0x100FFD20, WeaponKeenCritHitRange); // fixes Weapon Keen stacking (Keen Edge spell / Keen enchantment)
replaceFunction(0x100F8320, ImprovedCriticalGetCritThreatRange); // fixes stacking with Keen Edge spell / Keen enchantment

// Allow filtering out certain types of negative levels from the GetLevel query.
static int (*origNegLvl)(DispatcherCallbackArgs) = replaceFunction<int(DispatcherCallbackArgs)>(0x100EF8B0, [](DispatcherCallbackArgs args)->int {
GET_DISPIO(dispIoTypeObjBonus, DispIoObjBonus);
auto condName = args.subDispNode->condNode->condStruct->condName;

auto omit = static_cast<LevelDrainType>(dispIo->flags);

auto mask = LevelDrainType::NegativeLevel;

// Temp Negative Level and the various aligned equipment penalties count
// as `NegativeLevel`. The only built-in condition that is not of this
// type is Perm Negative Level.
if (!_stricmp(condName, "Perm Negative Level")) {
mask = LevelDrainType::DrainedLevel;
}

// flags indicate whether we should _skip_ a particular condition, so that the
// existing default of 0 includes all adjustments.
if ((omit & mask) == mask) return 0;

return origNegLvl(args);
});

// Amulet of Natural Armor - bonus type changed to 10 so it doesn't stack with other enhancement bonuses (Barkskin, Righteous Might)
replaceFunction<int(DispatcherCallbackArgs)>(0x10104AB0, [](DispatcherCallbackArgs args)->int {
GET_DISPIO(dispIOTypeAttackBonus, DispIoAttackBonus);
Expand Down Expand Up @@ -191,7 +214,7 @@ int GeneralConditionFixes::PermanentNegativeLevelOnAdd(DispatcherCallbackArgs ar
{
auto highestLvl = 0;
auto highestClass = 0;

auto effLv = critterSys.GetEffectiveDrainedLevel(args.objHndCaller);

critterSys.CritterHpChanged(args.objHndCaller, objHndl::null, -5);

Expand All @@ -203,9 +226,7 @@ int GeneralConditionFixes::PermanentNegativeLevelOnAdd(DispatcherCallbackArgs ar
critterSys.Kill(args.objHndCaller);
}
else {
auto xp0 = d20LevelSys.GetXpRequireForLevel(hd);
auto xp1 = d20LevelSys.GetXpRequireForLevel(hd+1);
auto newXp = (xp0 >=0 && xp1 > 0 && xp1 > xp0 ) ? (xp0 + xp1) / 2 : 0;
auto newXp = d20LevelSys.GetPenaltyXPForDrainedLevel(effLv);

// set negative XP
objects.setInt32(args.objHndCaller, obj_f_critter_experience, newXp);
Expand All @@ -214,7 +235,7 @@ int GeneralConditionFixes::PermanentNegativeLevelOnAdd(DispatcherCallbackArgs ar
histSys.CreateRollHistoryLineFromMesfile(22, args.objHndCaller, objHndl::null); // [ACTOR] ~loses a level permanently~[TAG_LEVEL_LOSS]!
combatSys.FloatCombatLine(args.objHndCaller, 126); //"Permanant Level Loss"

args.SetCondArg(1, hd+1);
args.SetCondArg(1, effLv+1);

return 0;
}
Expand Down
6 changes: 6 additions & 0 deletions TemplePlus/protos.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,12 @@ int ProtosHooks::ParseMonsterSubcategory(int colIdx, objHndl handle, char * cont
continue;
}

// native subtype won't be in the array
if (!_strcmpi(tokItem.text, "mc_subtype_native")) {
subcatFlags |= MonsterSubcategoryFlag::mc_subtype_native;
continue;
}

for (auto i=0; i < arrayLen; i++){
if (!_strcmpi(strings[i], tokItem.text)){
auto flagTmp = 1 << i;
Expand Down
Loading

0 comments on commit b3e726d

Please sign in to comment.