Jump to content

[Discussion] Immunities, aka 101 vs. 318/324


CamDawg

Recommended Posts

We've had a pretty robust discussion about the use of immunities using either opcode 101, in which you can make a target immune to specific opcodes, or opcodes 318/324, in which you can have applied effects get blocked based characteristics of the target such as race or a stat.

I thought I'd lay out pros and cons of both approaches (as I see them at least) with the goal of setting guidelines as to when we use 101s or 318/324.

Pros using opcode 101

  1. We can more broadly use internal items (e.g. ring95 for undead or ringdemn for demons) to provide generalized immunities
  2. Is generally the method used already, so there's less for EEFP to adjust (and, by extension, potentially break)
  3. More flexible when you need to make exceptions (ringdemn's poison immunity vs. the Chromatic Demon)

Cons of op 101

  1. Needs to be supplemented with a small army of opcodes that block strings, animations, and portrait icons associated with the main opcode being blocked; see the Immunity Effect Batches of BG2FP.
  2. While there are ways to block specific animations, there's no way to block specific sounds. So despite the above con, there's an additional drawback that sounds (unless they're tied to a VEF/VVC that can be blocked) will still play. This includes expiry sounds.
  3. Depending on the circumstance, can either do partial immunities poorly or not at all, e.g. the (half-)elven immunity to sleep and charm

Pros of ops 318/324

  1. Exceptionally clean. One well-placed opcode blocks everything (sound, visuals, &c.) without the need for a small fleet of companion effects. This includes the sound problem with 101s.
  2. Is really the only good way to do partial immunities. Since it's attached to every source of an effect that we want to block, dice will be rolled every time the effect is invoked.

Cons of ops 318/324

  1. Handles mixed spells poorly, e.g. a spell that poisons and causes sleep can't have a clean 318/324 implementation unless we push those into subspells or EFFs
  2. Has to be placed on every item or spell that causes the effects you wish to block
  3. Inflexible when used for racial or kit immunities, though this is not the case for stat-targeted 318/324s.

There's a simple case study in the Chromatic Demon of Watcher's Keep. The story of the level is that its ice form is weak to poison: except that it's equipped with ringdemn, which makes it immune to poison. If we're using strict 318/324 racial checks on items and spells that poison, we now have to carve out an exception one way or the other: either make the chromatic demon a special demon race that can be poisoned, or some other ad-hoc solution were we have a manual exclusion for it on all of those poison spells and items. Since BG2FP doesn't have the 318/324 option, we simply cloned ringdemn, removed the poison 101s, and give that to the Chromatic Demon instead.

Poison immunity is not the best example, as it's actually a good candidate to be swapped to 318/324 as it can be means-tested via stat checks. Cloudkill will slay (55) targets under a specified HD cutoff; using the 101 approach we have to supplement the immunity to poison by explicitly blocking Cloudkill spells via opcode 206. If we instead use a 318/324 vs. poison resistance on the Cloudkill itself, we avoid this outright.

Another good candidate to move from 101s to 318/324 is the Song of Kaudies in IWDEE. It provides 50% immunity to sound-based attacks (think Wailing of Virgins, Demilich Howls, or (Great) Shout). However, since there's no stat for 'audio resistance' that we can means-test, we have to weigh the better functionality of the Song providing immunities is worth burning a spell state.

Link to comment

I think there are two factors here.

(1) What feature of a creature actually codes its immunity to some effect?
(2) Given an answer to (1), how do we implement that immunity?

I'll use charm immunity for undead as an example. In BG2, what makes an undead creature immune to Charm is that it has ring95. In IWD, what makes it immune is that it has GENERAL=UNDEAD. In other words, in IWD it's immune simply because it's undead; in BG2 the fact that it's undead only matters through the developer's decision to give the creature ring95. Internal to the logic of BG2, undead aren't charm-proofed; only ring95-holders are.

As I said in the earlier mail thread, these are two fundamentally different choices of immunity architecture, embedded *very* deeply into the respective games. There are defensible arguments for either, but I think it's a really bad idea to try rewriting the architecture of any of the games to this degree. I suspect Cam's compelling example of the Chromatic Demon would just be the tip of the iceberg.

But even holding the architecture fixed, there are still substantive questions about how it is implemented. In the IWd architecture, there's not much choice: there is no way to attach effects to a RACE or GENERAL setting, so the only available implementation is to tell each appropriate spell and item to ignore creatures of that RACE or GENERAL, using 318/324. This is basically what IWDEE already does.

It's more complicated on the BG2 architecture. There are 3 options:

(i) Use 101 on the immunity item (ring95 in our example) to block the core effect, and then add a bunch of string/animation immunities to block associated strings/animations.
(ii) For each spell/effect that implements the blocked effect (e.g. Charm), add that spell to the immunity item via 206. (Or 318; I don't think it matters.)
(iii) Get the immunity item to set a spellstate (say, BLOCKS_CHARM), and then edit all spells/items that inflict the blocked event so as to add a 318/324 block keyed to that spellstate.

Importantly, these are not mutually exclusive.

I'm going to argue that we should always *at least* use (i) when it's viable. The absolutely-most-important feature of an immunity-to-charm option is that it blocks the charm opcodes. Secondarily, it needs to block the clearly-visible signatures of charm: the displayed 'Charmed'/'Dominated' strings, the icons, and the SPNWCHRM animation. Blocking other effects (e.g. sounds) is a very distant third. It is way too dangerous to rely on (ii) and (iii) to block the charm opcodes: even in the unmodded game I'd be nervous, but mods add vast numbers of new items and spells and there is no way to enforce that those mod items/spells add 206 to the immunity items *or* 318/324 to themselves. Blocking the relevant opcodes direct is the only safe option.

BUT there is a perfectly good case for ALSO blocking the core spells directly via either (ii) or (iii): it addresses the sound-effect issue and any other stray effects, and it's harmless to double up. In principle I think (iii) (new spellstate set by the immunity item, spells detect the spellstate and self-block) is better: it's more elegant, clutters up the spell less, and is resilient against mods that clone the spell. However, the cost in spellstates is way too high: we'd need 20 or more. So (reluctantly!) I think in practice using 206 on the immunity item is better.

However, for immunity *spells* there is an spellstate available already: the spellstate set by DS. There is no reason why, e.g., Charm Person couldn't just self-block if it detects the CHAOTIC_COMMANDS spellstate. (We can't, unfortunately, add that spellstate to ring95, because both SoD and mods like SCS assume that CHAOTIC_COMMANDS marks the specific - and dispellable/breachable - spell, not just generic immunity.) I think that move is more elegant for spells than adding a bunch of 206s to the spell (though I'm not super-sold on this.)

Cam raises two special cases - Cloudkill and Song of Kaudies - but I think they fit in this general logical framework. The underlying issue is that the game contains no resource to designate a spell as Poison / Sound. So the relevant effect needs to sit on the spell itself: that's much cleaner logic than just calling out the spells to each relevant item. Doing so for Cloudkill, as Cam says, is easy: just do a 318/324 keyed to poison resistance. I agree that Sound needs a new spellstate, IMMUNE_TO_SOUND; then what marks something as a Sound effect is just that it does a 318/324 self-block if it runs into IMMUNE_TO_SOUND.  I think this is a satisfactory reason to use up one spellstate slot; in general I think using up the odd one is fine, I just would want to resist a systematic policy that uses up dozens. 

Summary:

- For IWDEE creature immunities, I don't think we should do anything
- For BG(2)EE creature immunities, we should maintain the existing 101 blocks but should also add a 206 block to probably any vanilla-game resource; certainly any vanilla-game resource with sound effects.
- For spells that grant immunities, we should use their DS spellstates/stats to directly block any vanilla-game resource (or, again, any vanilla-game resource with a sound effect). That means that we should after all include basic DS in the fixpack. 
- We should follow Cam's suggestions for Cloudkill and Song of Kaudies.

Link to comment

Doing a quick automated audit in BG2, I think there would indeed need to be about 20 spellstates to implement that method of doing effect blocking:

charm (6)
haste(16)
fear (23,24,106)
unconscious (39,217)
slow (40,210)
stun (45)
death (55,206)
blindness (74)
feeblemind (76)
disease (78)
deafness (80)
fatigue (93)
hold (109,175,185)
confusion (128)
petrification (134)
polymorph (135)
imprisonment (211)
maze (213)
energy drain (216)
disintegrate (238)

I am actually tempted - there are significant AI advantages - but it does rather depend on how much mods are using up splstate.ids - cf my other thread.

Link to comment

I've more or less approached this from the (pretty conservative) mentality of BG2FP but you've really brought up a number of great points.

First and foremost: I cannot imagine trying to radically alter the (as you put it) deeply embedded immunity architecture. This is not something I've been able to really express well, so I really appreciate that you have, and I agree.

1 hour ago, DavidW said:

(i) Use 101 on the immunity item (ring95 in our example) to block the core effect, and then add a bunch of string/animation immunities to block associated strings/animations.
(ii) For each spell/effect that implements the blocked effect (e.g. Charm), add that spell to the immunity item via 206. (Or 318; I don't think it matters.)
(iii) Get the immunity item to set a spellstate (say, BLOCKS_CHARM), and then edit all spells/items that inflict the blocked event so as to add a 318/324 block keyed to that spellstate.

Now this gets to the heart of it. BG2FP is, for a number of reasons, exceptionally conservative. Option 3 isn't available for BG2FP but, even so, we barely used option 2 except when 1 was insufficient, e.g. immunity to slow explicitly blocks slow spells since they contain a lot of other stuff (AC and THAC0 penalties) that you can't just block. This may sound a little silly, but extending 206s as a matter of course is not really something I had considered, and is a nice way to partially mitigate the downside of a 101 approach. DS is an interesting option to facilitate this that I also hadn't considered

33 minutes ago, DavidW said:

unconscious (39,217)

One of the nice benefits of, say, using spell states is that we could finally draw a more distinct line between sleep and unconsciousness despite them both using op39 on the main. aVENGER made a concerted effort to distinguish the two during IWDEE development, e.g. elves had resistance to the Sleep spell, but not Smashing Wave despite them both using op 39 internally. This didn't require a spellstate since these could be keyed off race, but any item or spell that protects from sleep could now use this distinction if we went the spell state route.

33 minutes ago, DavidW said:

hold (109,175,185)

@Luke had pointed out (based in turn from a post that @kjeron made) that we really should draw a distinction between paralyzation (109) vs. hold (175/185). So we probably would want two spellstates here instead of one.

Link to comment
On 3/17/2022 at 10:59 PM, DavidW said:

(iii) Get the immunity item to set a spellstate (say, BLOCKS_CHARM), and then edit all spells/items that inflict the blocked event so as to add a 318/324 block keyed to that spellstate.

Importantly, these are not mutually exclusive.

...

BUT there is a perfectly good case for ALSO blocking the core spells directly via either (ii) or (iii): it addresses the sound-effect issue and any other stray effects, and it's harmless to double up. In principle I think (iii) (new spellstate set by the immunity item, spells detect the spellstate and self-block) is better: it's more elegant, clutters up the spell less, and is resilient against mods that clone the spell. However, the cost in spellstates is way too high: we'd need 20 or more. So (reluctantly!) I think in practice using 206 on the immunity item is better.

Just to cross over from the other thread: this is probably a good, concrete example of a circumstance where using proficiencies would be a favorable substitute for using spellstates:

  • Have ring95.itm apply op233 to increase long sword proficiency by, say, (4<<16).
  • Patch charm spells with op318 effects to block themselves if the target has a value bit-equal to (4<<16) in stat 90.
  • Make up a .2DA table showing that the 19th bit of stat 90 means BLOCK_CHARM so the effect is identifiable to someone post-install.

Bob's your uncle. 400 more spellstates. I know it's black magic but... it works.

Spoiler

 

APPEND ~splprot.2da~ ~STAT_90_BIT_EQUAL%TAB%90%TAB%-1%TAB%8~ UNLESS ~STAT_90_BIT_EQUAL~

COPY_EXISTING ~splprot.2da~ ~override~
  COUNT_2DA_COLS cols
  READ_2DA_ENTRIES_NOW rows cols
  FOR (row = 1; row < rows; ++row) BEGIN
    READ_2DA_ENTRY_FORMER rows row 0 ~stat~
    PATCH_IF ~%stat%~ STRING_EQUAL_CASE ~STAT_90_BIT_EQUAL~ BEGIN
      SET stat_90_bequal = %row%
    END
  END
BUT_ONLY

COPY_EXISTING ~ring95.itm~ ~override~
  LPF ADD_ITEM_EQEFFECT INT_VAR target = 1 opcode = 233 parameter1 = (4 << 16) parameter2 = (90 + (0x10000 * 1)) timing = 2 END
BUT_ONLY

COPY_EXISTING ~spwi104.spl~ ~override~
  LPF ADD_SPELL_EFFECT INT_VAR insert_point = 0 target = 2 opcode = 318 parameter1 = (4 << 16) parameter2 = %stat_90_bequal% timing = 0 duration = 1 STR_VAR resource = ~spwi104~ END
BUT_ONLY

 

Only thing is, it is not detectable by scripts. Script triggers can only detect numerical values  in stats, or bit-equality values in the first three bits via the Proficiency() trigger.

Edited by subtledoctor
Link to comment

Introducing proficiencies as a counterpart of spellstates in the fixpack would lead to futureproof issues though - that is way too hacky to incorporate to a potential official patch, enforce mods to add EE fixpack-specific detection and the combination forces an update on any mod relying on this when the patch with the counterpart official solution gets rolled out to the proficiencyfixed solution. Using proficiencies as this is cool and bright, but that should really remain in external mods.

Link to comment

I mean, on the one hand it is precisely as hacky as spellstates themselves - just a free bit of information being attached as an effect to a .CRE file to represent a certain condition. The only difference is where you find those free bits, and whether you expect a script to include a ‘CheckStatGT’ trigger for that stat (why the “ExtraProficiency” stats cannot be used for such purpose). If it is a backup method, and spellstates are not being considered anyway, then the concern about an official solution doesn’t really hold. The only way mods should become dependent on something like this would be if they can replicate it independent of the FixPack (which should be easy enough).

On the other hand, yeah, I sort of get it. 

Edited by subtledoctor
Link to comment
8 hours ago, DavidW said:

energy drain (216)

this one can actually be avoided since we have the LEVEL_DRAIN_IMMUNITY stat (can be set via op282...)

Related: we should also take into account all those items/spells that "drain" attributes via something like op44 – Strength bonus (f.i. "shadowwp.itm")...

8 hours ago, DavidW said:

I am actually tempted - there are significant AI advantages

This is certainly another good reason for expanding "splstate.ids".

So basically instead of checking for GENERAL/RACE/CLASS/etc... you would just use

!CheckSpellState(scstarget,IMMUNITY_TO_HOLD) // So as to avoid special cases such as the Chromatic Demon

And similarly, things like CLERIC_HOLD_PERSON would become

Spoiler
  • op324 (GENERAL != HUMANOID, timing=0, duration=0, resource="sppr208")
  • op324 (SPLSTATE = IMMUNITY_TO_HOLD, timing=0, duration=0, resource="sppr208")
  • op175 // automatically displays string `STRREF_EFFECT_HOLD` and the portrait icon "Held". It is imperative this opcode is here (before cosmetic effects) for later compatibility with IWDification (in particular "7eyes.2da")
  • op215 (resource="spmindat.vvc")
  • op174 (resource="eff_e05.wav" – ending sound)
8 hours ago, DavidW said:

but it does rather depend on how much mods are using up splstate.ids

On an unmodded game, there should be 122 free slots (123 on BGEE since it lacks `118 HARDINESS`...)

9 hours ago, DavidW said:

In principle I think (iii) (new spellstate set by the immunity item, spells detect the spellstate and self-block) is better: it's more elegant, clutters up the spell less, and is resilient against mods that clone the spell.

Of course mods that make resources from scratch should adapt to the new system, otherwise the so called stray effects would still fire and there would not be any feedback string (the so called STRREF_FEEDBACK_IMMUNE_RESOURCE from op324...)

9 hours ago, DavidW said:

add that spell to the immunity item via 206. (Or 318; I don't think it matters.)

206 can block SPL and EFF V2 files (provided that `Parent Resource Type` @ 0x90 is set to `1|Spell`; moreover, the `Resource` field of opcode #206 and the `Parent Resource` field @ 0x94 of the EFF file must be the same string). It will also fire a string in the combat log upon triggering (however, the string is not firing as of v2.6, regression @Galactygon @Bubb ...?)

318 can block SPL/ITM/EFF V2 files (regardless of the value of `Parent Resource Type` @ 0x90; moreover, the `Resource` field of opcode #318 and the `Parent Resource` field @ 0x94 of the EFF file must be the same string), However, unlike op206, it won't display a string upon triggering (op324 is functionally identical to op318 except that will display STRREF_FEEDBACK_IMMUNE_RESOURCE upon triggering).

So to sum up: in case of ITM files, you need either 318 or 324; in case of SPL files, it doesn't really matter (unless you want to display a feedback string...)

Edited by Luke
Link to comment
12 hours ago, CamDawg said:

One of the nice benefits of, say, using spell states is that we could finally draw a more distinct line between sleep and unconsciousness despite them both using op39 on the main

Exactly.

Spellstates should also solve the issue with CLERIC_CHAOTIC_COMMANDS protecting against CLERIC_EARTHQUAKE and the like... There is no need for alternate Sleep opcode (at least in principle...)

I've already tried fixing this issue without using spellstates... As you can see, it's quite a mess and can't be fully automated... On top of that, there are some limitations too...

12 hours ago, CamDawg said:

@Luke had pointed out (based in turn from a post that @kjeron made) that we really should draw a distinction between paralyzation (109) vs. hold (175/185). So we probably would want two spellstates here instead of one.

Indeed.

The fact that there is not a real distinction between paralyzation (109) and hold (175) (let's not consider op185 which is a special case) is probably because op162 (used by spells such as CLERIC_REMOVE_PARALYSIS and CLERIC_FREE_ACTION) removes both of them...

Having said that, we really should draw a distinction between the two... I mean, why having two separate opcodes then...?

Edited by Luke
Link to comment
On 3/16/2022 at 9:23 PM, CamDawg said:

While there are ways to block specific animations, there's no way to block specific sounds. So despite the above con, there's an additional drawback that sounds (unless they're tied to a VEF/VVC that can be blocked) will still play. This includes expiry sounds.

I think it is worth mentioning that this issue is also relevant for removal spells.

Suppose a Bonebat ("bdbonbat.cre") paralyzes (via "bdbonbat.itm") your character and you decide to cast CLERIC_REMOVE_PARALYSIS.

The spell will certainly remove the main effect (op109) along with the Held portrait icon... but the expiry sound will still play (even if nothing has actually ended!).

However, this issue cannot be solved with spellstates: in this case we just need to make sure all these resources are properly removed via op321 effects...

Edited by Luke
Link to comment
5 hours ago, Luke said:

we should also take into account all those items/spells that "drain" attributes via something like op44 – Strength bonus (f.i. "shadowwp.itm")...

I was being selective given that splstate slots are an at-least-somewhat-scarce resource.(I did an automated trawl through all the 101 effects in BG2EE.) In this case, only one spell in BG2 gives immunity to op44, and its existing implementation looks fine without a spellstate.

13 hours ago, DavidW said:

I am actually tempted - there are significant AI advantages

On reflection, they're not as good as I thought. SCS needs to distinguish between protections that are immediately obvious (e.g. don't charm undead) and protections that you can only discover through trial and error (e.g. don't charm people wearing Helms of Charm Protection). So I take back the 'AI advantages' point, although I'm still attracted to the overall elegance of the scheme.

 

12 hours ago, subtledoctor said:

400 more spellstates. I know it's black magic but... it works.

That's incredibly clever. I think I agree that it's too hacky to use in a FP, but the very fact that it can be done takes the pressure of splstate.2da and makes me somewhat more relaxed about grabbing a bunch of spellstates for FP.

Link to comment
5 hours ago, Luke said:

206 can block SPL and EFF V2 files (provided that `Parent Resource Type` @ 0x90 is set to `1|Spell`; moreover, the `Resource` field of opcode #206 and the `Parent Resource` field @ 0x94 of the EFF file must be the same string). It will also fire a string in the combat log upon triggering (however, the string is not firing as of v2.6, regression @Galactygon @Bubb ...?)

Yes, there's been a regression. op206's string is now bound by:

if (!pEffect->m_sourceRes.IsValid() || (pImmunitySpell->m_error != 0xf00074 && pImmunitySpell->m_error != 0xf00080))
{
    uint nNewFeedback = 0xffffffff;
    if (pImmunitySpell->m_error == 0xf00074) {
        nNewFeedback = 0xf00073;
    }
    pImmunitySpell->m_error = nNewFeedback;
}

Basically, the strref has to be 0xF00074 or 0xF00080 for the engine to display it.

Link to comment
On 3/18/2022 at 6:48 PM, Bubb said:

Yes, there's been a regression. op206's string is now bound by:

if (!pEffect->m_sourceRes.IsValid() || (pImmunitySpell->m_error != 0xf00074 && pImmunitySpell->m_error != 0xf00080))
{
    uint nNewFeedback = 0xffffffff;
    if (pImmunitySpell->m_error == 0xf00074) {
        nNewFeedback = 0xf00073;
    }
    pImmunitySpell->m_error = nNewFeedback;
}

Basically, the strref has to be 0xF00074 or 0xF00080 for the engine to display it.

Thanks for confirming it, I'll add it to the IESDP...

Link to comment
On 3/18/2022 at 6:48 PM, Bubb said:

Yes, there's been a regression. op206's string is now bound by:

if (!pEffect->m_sourceRes.IsValid() || (pImmunitySpell->m_error != 0xf00074 && pImmunitySpell->m_error != 0xf00080))
{
    uint nNewFeedback = 0xffffffff;
    if (pImmunitySpell->m_error == 0xf00074) {
        nNewFeedback = 0xf00073;
    }
    pImmunitySpell->m_error = nNewFeedback;
}

Basically, the strref has to be 0xF00074 or 0xF00080 for the engine to display it.

Are you sure about this one...?

I tried with both 0xF00074 => row 116 => STRREF_FEEDBACK_EVADE_RESOURCE and 0xF00080 => row 128 => STRREF_FEEDBACK_IMMUNE_RESOURCE, still nothing 😕...

Edited by Luke
Link to comment
7 hours ago, Luke said:

Are you sure about this one...?

Yes, how are you testing it? Take Wizard Eye (SPWI425.SPL), remove all spell abilities except the first one, and test op206 => String = 45909, 0xF00074, 0xF00080:

Spoiler

45909, (No message):

R3qzqv7.png

0xF00074, (Evades effects from Wizard Eye):

Hj7B94u.png

0xF00080, (Unaffected by effects from Wizard Eye):

INO7HYu.png

 

Link to comment

Join the conversation

You are posting as a guest. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...