DW Opcode Fixer

Author: DavidW

Version: beta 1
Languages: English
Platforms: Windows, Linux, macOS

Overview

This is a function that carries out a fairly systematic and wide-ranging fix to various issues in BGEE,BG2EE and IWDEE with opcodes.

This started as an attempt to improve on the relatively-crude code I wrote for the EE fixpack to use 324 blocks to implement immunity to various effects (confusion, charm etc.) Doing that consistently and automatically is actually a fairly awkward engineering problem because some spells/items create multiple effects and immunity should only apply to some of them, but the 324 method blocks everything after the actual 324 block. The goal of this code was to systematically and automatically break spells up into subspells where necessary to handle immunity. I wanted to do this automatically because (a) I think that's a more reliable way to implement a systematic policy; (b) more importantly I wanted to be able to run it in my own mods (which normally install quite late in the order) and not just in the EE fixpack.

All this required the function to be told which cosmetic effects are associated with which opcodes (this varies between BG/BG2 and IWD, of course). Doing a deep dive into that showed that there is a pretty consistent philosophy but also that there are many errors in applying that philosophy, so I ended up fixing those errors.

The other (original) goal for this code was to do all the EE fixpack COPY_EXISTING_REGEXP runs in one go, and to hardcode them to be as fast as possible. So the code also removes redundant blocks, and removes icons and strings that (in EE) are automatically delivered by the main opcode.

In doing all this it became natural to incorporate the other fixes to opcodes I'd been doing in the Fixpack: 13/55, 109/175/185, etc.

Finally, I ended up rolling out a different implementation for the removal of secondary effects (Cam does this by hardcoding 321 removal of every relevant spell/item, but I do it using sectypes).

By the time I'd finished this I'd slightly lost track/interest of exactly which bits of this ought to be in a fixpack. I thought the simplest thing was just to document it all carefully and make it available. Happy to work with people to remove any bits that don't seem FP-relevant (and keep those features in the version I want to roll out myself in my own mods.)

Compatibility

This is EE-only. It should work on any of BGEE (with or without SoD), BG2EE, and IWDEE. It is idempotent, i.e. can be installed on top of itself without issues. It ought to work at any point in the install order, though there are possible (somewhat obscure/weird) third-party spells that might confuse it (hopefully it should just skip those).

Installation

You need to INCLUDE the file 'opcode_wrap.tph' (in this distribution, located in %MOD_FOLDER%/dw_opcodes/lib) and then launch the action function 'opcode_wrap'. This function takes these arguments:

  1. STR_VAR loc: The path to a folder containing the folders 'lib' (dw_opcodes' function library), 'data' (files containing the data on which cosmetics are associated with which opcodes), and 'files' (contains the (1) file that needs to be copied over). The default value for this path is '%MOD_FOLDER%/dw_opcodes'; in this distribution, all those three folders can be found there.
  2. STR_VAR tra_loc: The path to the tra folder in which the function's tra files can be found. The default value for this folder is 'lang'.
  3. STR_VAR tra: the actual name of the tra file containing the (4) strings used by the function, with tra indices 700001-700004. The default value for this name is 'dw_opcodes.tra'; in this distribution (which is English-only) the tra file is %MOD_FOLDER%/lang/english/dw_opcodes.tra. (If the function cannot find the tra file it will default to hardcoded (English-only) values.)
  4. INT_VAR telemetry (must be 0 or 1): if this is set to 1, the function will display various debug information, and will save some of it in weidu_external/data/dw_shared/dw_opcode_log.txt. If it is set to 0, this information will not be displayed. The default value is 0.

This component comes with a tp2 that runs the function.

There is also an action function 'opcode_wrap_if_needed' included in opcode_wrap.tph. This function takes the same arguments as opcode_wrap (and some additional ones described below). It will run opcode_wrap only if (a) this is an Enhanced-Edition install and (b) it has not yet been run in this mod.

The opcode_wrap_if_needed function also attempts to work out if opcode_wrap needs to be run again to affect the iwdspells implementation of IWD spells (this would be relevant for a mod like IWDification or SCS that distributes those spells). This uses these additional arguments:

  1. INT_VAR handle_iwd (must be 0 or 1): if set to 1, the function tries to handle the IWD spells. The default value is 0.
  2. INT_VARs component_number_iwd_arcane, component_number_iwd_divine, component_number_iwd_bard: should be set to the component numbers of the components (in the current mod) that install those sets of IWD spells. (It doesn't actually matter which is which, the mod checks all three.) The default values are -1.
If handle_iwd is set to 1, opcode_wrap will be run (on EE installs) if (a) any of the three lots of IWD spells (arcane/divine/bard) has been installed and (b) the current component is not itself trying to install IWD spells.

This is a little kludgy. It will only edit the IWD spells if some component (of any mod using opcode_wrap_if_needed) is run after installing the IWD spells; also, it will miss some spells if someone installs a non-IWDspells component between two IWDspells components. Hopefully in the longer term this can all be dropped entirely because we'll ship IWDspells with the 324 changes already made.

Hardcoded manual fixes

Fixing missing/inaccurate cosmetic secondary opcodes

By a 'secondary opcode' I mean any opcode which is standardly applied alongside some 'primary' opcode that has a direct game-mechanical effect (like paralysis, petrification, or charm). Most such opcodes are cosmetic: display a string, play a sound, add an icon, etc. A small number have mechanical effects of their own: for instance, Slow reduces the target's AC and THAC0.

In an ideally-logical game engine, there would be no secondary opcodes because the effects would be built in to the primary opcode, perhaps with some ability to control their details built into the opcode's parameters: this would guarantee that secondary opcodes were properly applied, blocked, and removed by any effect that applies, blocks, or removes the primary opcode.. The IE is not an ideally-logical game engine (though EE takes some baby steps here) and for the most part secondary opcodes have to be manually applied, blocked, and removed.

Fixes to cosmetic secondary opcodes are based on two observations.

  1. There is a very systematic and consistent philosophy used for secondary opcodes in the IE games, whenever the primary opcode has any effect on the character that isn't just damage or death. Specifically:
    • A string is displayed telling the player that the primary opcode has been applied. (e.g., 'charmed'.)
    • There is some kind of visual effect (often also a sound) localized on the character, showing the player that the primary opcode has been applied.
    • An icon is placed on the character's portrait and character sheet reminding the player that the primary opcode is active
    There is overwhelming evidence that this is part of the core design philosophy for the IE games.
  2. The philosophy is very inconsistently applied. The most carefully designed resources (player-learnable spells) almost invariably use it, but other resources often forget it. This is particularly common for enemy attack powers, which often seem to have been built in haste and leave out some or all of the effects.
My working assumption from these two observations is that developer intent is for all spells and items to keep to the design philosophy, so that when it is missing it is by mistake. It's extremely easy to see how that mistake would occur: because of the structure of the IE nothing automates the philosophy, so that item designers have to add all the secondary opcodes manually. Since in addition they are mostly cosmetic, so that their absence is only a low-priority bug, it's totally unsurprising to find them left off often, especially in late-made items.

All of this is implemented manually (i.e. I don't have any code that tries to enforce it automatically) as there are too many potential edge cases.

Separating out different versions of the 'sleep' opcode (opcode 39)

Sleep is heavily overloaded: its uses include

  • being put to sleep (example: Sleep, SPWI116.SPL) - should display 'sleep' icon and show 'sleep' string
  • being rendered unconscious (example: Color Spray, SPWI105.SPL) - functionally equivalent to sleep, so best to dress the same way
  • being rendered hopeless (example: Emotion:Hopelessness, SPWI411.SPL) - displays hopelessness icon, provides immunity to fear
  • - being nauseated (example: Stinking Cloud, SPWI213.SPL) - should display 'nauseated' icon
  • being momentarily knocked down by a Wing Buffet effect (example: Wing Buffet, SPIN695.SPL) - displayed no icon in oBG2, BG2EE gives the 'sleep' icon but I think that's misleading. Of the available options the best choice is 'unconscious' (130)
  • being 'knocked back and stunned' (example: the Staff of the Ram, STAF21.ITM) - despite being described as 'stun' in the text I think this is best conceptualized as 'unconscious', given the string issue.
  • falling to the ground during an earthquake (example: Earthquake, SPPR720.SPL) - awkward but I think 'unconscious' is the best fit; also needs to be externalized for later use.
  • being pinned (specific to Ixil's Spike, SPER12.ITM) - likewise.

In addition (regrettably, in my view) opcode 39 plays the 'unconscious' string automatically.

Beyond cosmetic issues, the main issue is how opcode 39 interfaces with immunity effects. My implementation, as an attempted compromise between precision and economy, is to have two immunity states, SLEEP_IMMUNITY and NAUSEA_IMMUNITY. SLEEP_IMMUNITY covers magical sleep but also magical unconsciousness and hopelessness. (It helps that virtually everything that grants sleep immunity is actually a broad-spectrum mental-attack immunity). NAUSEA_IMMUNITY covers Stinking Cloud. Neither immunity protects from knockback unconsciousness, from being knocked over by an Earthquake, or being pinned. We give immunity to Earthquake knockdown and pinning to boss monsters, via exporting the effect to subspells and granting immunity to those subspells. (Boss monsters need not to have their scripts blocked from running for any significant length of time.) We leave full-on immunity to 39 only to the fairly-few creatures who cannot be knocked around at all.

Separating out death effects (13,55) and implementing immunity to death magic

Mechanically, the difference between these opcodes is that 13 lets you choose the form of death while 55 lets you target by race, class etc.

As used in the game, 55 is specifically for death magic. 13 covers other direct-death effects that aren't death magic: important examples are deadly poison (e.g. vortex spider or green slime venom) and vorpal strikes. 55 is also often used for instant-kill effects on targeted creatures, though sometimes the game has to use 13 to bypass death-magic immunity, e.g. undead-destruction effects. An important exception that I think is an ancient issue dating back to oBG: Cloudkill uses 55.

Immunity to 13 is rare and mostly restricted to boss monsters and min-hp creatures. Immunity to 55 is common for monsters, and normally represents immunity to death magic but sometimes seems to represent immunity to Cloudkill's death effect (notably for slimes/puddings).

The designers seem *mostly* to have kept this straight but there are a fair few places where they get mixed up.

Here I want to fix some specific issues but also to disentangle the cloudkill issue. Note that in EE Cloudkill affects no creature that has poison immunity, so that it's no longer necessary for poison-immune creatures to have 55 immunity.

As additional issues, PW:Kill (opcode 209) fairly clearly needs to be associated with 55 immunity but rarely is - this is so common that I just treat 209 immunity as a secondary effect to associate with 55 immunity and roll it out in the systematic patch. And while there's no very good PnP reason to associate 238 ('disintegrate') with death effects, there's a very clear in-game lore reason: Death Ward quite explicitly calls out Disintegrate as covered by its protection from 'death magic'.

For the most part, see the code itself for specific fixes. I'll call out some possibly-controversial ones:

  • Death Ward, Avoid Death, Hindo's Doom and the Cloak of the Lich should grant immunity only to 55, not also to 13. The in-game text for each specifically refers to 'death magic', which fairly clearly does not include immunity to being beheaded or to green slime venom. Death Ward had this added in EE, and Avoid Death and Hindo's Doom are ToB items, so I think it's a fairly safe assumption that it was added in each case due to confusion between 13 and 55. (Note that none of them give immunity to the 'vorpal strike' string, which is necessary for a proper implementation of 13 immunity.)
  • 'Avoid Death' no longer gives immunity to level drain, petrification, or Imprisonment, none of which are documented or included in other implementations of death magic immunity. Again I think Ockham's Razor says this were mistakes made when someone did a hurried implementation of a HLA in ToB and didn't have a systematic account of what 'death magic' means to hand.

Separating out paralyze/hold/web effects (109/157/175/185)

My underlying assumptions:

  1. 109 represents generic paralyzation effects. 175 represents, specifically, the 'Hold' effect, restricted mostly to the relevant spells. 185 covers effects that physically prevent you moving (as well as being a cutscene-usable unblockable hold).
  2. The writers of BG1 understood this fine. But ever since TotSC they have been gradually losing track of the logic, until by the EE the distinction between 109 and 175 is lost entirely and the meaning of 185 is quite blurred. Hence, in divining developer intent we give a lot of credence to specific choices of 109 vs 175 in BG1, and successively less as we go on. (I think 'developer intent' also becomes a bit blurry here: I think the intent of the people who designed the IE doesn't quite match the intent of the level designers!)
The main practical effect is that this restores a clean difference between immunity to paralysis (109) and immunity to hold (175), managed by the separate PARALYZE_IMMUNITY and HOLD_IMMUNITY splstates, but there are two secondary problems to address.
  1. Immunity to 109 really needs immunity to the 'held' icon, but that would then erroneously hide it if the creature was targeted by a 175 or 185 effect, which is bad. Options:
    • (A) suck it up.
    • (B) clone the 'held' icon. But the problem is that a creature could then have 'held' displayed twice (in admittedly edge-case circumstances).
    • (C) clone the 'held' icon and rename it 'Paralyzed', for which a strref exists. That's much better technically but does change the game slightly more.
    I have dithered on this but my current feeling is (C) is best. ((B) is also coded, but commented out.)
  2. ) Web and Hold are crowding the same description-string namespace. 157 immunity ought to give immunity to 'held', but if it does, we get erroneous immunity to that string when hit by an actual Hold. The answer here is simpler, I think. We need two different implementations of the 'Held' string. We'll copy the standard string (14201) to a new string and remap the enginest entry.

The other design issue we clean up is that at least in EE, opcode 157 imposes not just the cosmetic web effect but also paralysis. Most in-game items don't recognize this so there is a lot of redundant application of 109 alongside 157 and a lot of pairing of immunities, all of which we try to cleanly separate.

As usual I leave the code itself as documentation of most of the changed items, but I'll call out the most obviously player-facing ones. Firstly, I remove 109 immunity from Berserker and Barbarian rage. (Their descriptions quite specifically refer to 'hold' - to 'hold spells' in one case.) Secondly, I give Undead Hunters immunity to 109 but not 175. This is actually in contradiction with the description (which mentions Hold but not Paralyze) but I think it's clear developer intent is to give immunity to undead paralysis. (We fix the description.)

Slow, Haste and their interactions

Here is how I am fairly sure Slow and Haste are supposed to work:

  • Haste just applies the opcode (all its effects are internalized to it).
  • Slow applies the opcode but also reduces THAC0 and AC by 4. This is how the canonical implementations of Slow do it and how it is defined in the only in-game text to describe Slow (the actual slow spell description). Other items that slow the target implicitly call back to that description (they refer to 'slowing' the target). This is also how it works in PnP, where slow effects (italicized) are references to the spell effect.
  • Slow negates haste and vice versa. (This is explicit in the description of the Slow spell: 'It negates Haste, but does not otherwise affect magically hasted or slowed creatures.').
  • Neither slow nor haste have cumulative effects.

This is not how it works in oBG2 or EE. Some slow spells/items, notably Slow itself but also Ardulia's Folly, do impose the THAC0/AC penalties; others do not (e.g. the Flail of the Ages). There is some effort made to prevent slow spells from being cumulative but it is partial and inconsistent (you can slow someone with Ardulia's Folly multiple times, for instance, or cast Slow on someone who's already been slowed by it, and they'll suffer -8 THAC0/AC. The Slow opcode removes the Haste opcode and vice versa but the THAC0/AC penalties are not removed (and the result of sequentially casting Haste and then Slow is that you are slowed, not that you are back to normal).

I think it is reasonably clear that this is not how the designers want it to work: rather, it works like this because the ideally-intended implementation is basically impossible in oBG2. There's no straightforward way to stop multiple slows being cumulative (leading to punishingly severe THAC0/AC penalties) and no straightforward way to implement cancellation.

Since it is possible (albeit challenging) to implement this in EE, I have standardized on this behavior. At the technical level, this involves:

  1. Dumping the payload of slow and haste spells/items into subspells, and granting those subspells the DW_PERSISTENT_16 / DW_PERSISTENT_40 sectypes.
  2. Getting the slow subspell to remove (by sectype) any existing slow effects, and then to block its own implementation (and play the string 'haste negated by slow') if the target is hasted.
  3. Vice versa for the haste subspell.
  4. After casting the subspell, getting the main slow spell/item to remove any haste effects, again by sectype, and vice versa for haste spells/item.
The result is that (i) these effects aren't cumulative; (ii) if you cast slow on a hasted target, the only effect is that the 'negated' string is displayed and the original haste effect is removed (and vice versa).

Taking advantage of EE Disease opcode

The EE disease opcode is very flexible and various secondary effects can be incorporated into it. The only concrete case I know is Dolorous Decay, which from description is clearly a disease effect but was implemented as a poison effect.

Handling (largely) cosmetic Time Stop effects

Spells with no projectile play awkwardly with Time Stop: they come in instantly but then (usually) nothing happens till the Time Stop ends. This looks a bit odd and can mess with the synchronization of animations and effects. I move some such effects (basically, attack spells and summons) to an invisible, effectively-infinitely-fast, projectile. (This code actually isn't hardcoded: it runs through all the SPWI/SPPR spells looking for cases where it applies. Theoretically other spells should be affected too, but there are awkward scripting side effects.

Fire shields

Fire shields' backlash problem is fixed.

Minor things

A bunch of 'display string' entries have timing mode 0 rather than 1; that's probably harmless but we fix it anyway.

Fixes made in the pass-through-all-resources code

Standardizing on a splstate-based immunity system for opcode immunities.

This was the original core of this code, before it suffered from scope creep. It implements the EE fixpack idea that immunity to (say) Charm should be implemented through (i) assigning a splstate 'CHARM_IMMUNITY' to immunity-granting spells/items/creatures and (ii) putting a 324 block at the beginning of any Charm spell/item to block itself when targeting any creature with the CHARM_IMMUNITY splstate. The rationale for doing this is that secondary opcodes can be automatically blocked, so that (e.g.) animations played, or sounds made at the end of the spell, get blocked if the primary opcode is blocked.

If we were designing a game from scratch, it would be natural to use entirely this system and eliminate manual immunity to effects (through opcodes 101, 296, etc). Given the mess this would make of existing mods, I've avoided this. (Though see below under 'Undead and confusion'.)

The automated implementation of this is fairly complicated due to the existence of resources that use multiple primary opcodes and the interactions with effect-removal; I describe it below, after explaining how effect-removal works.

Standardizing on a sectype-based removal system for opcode curing.

Secondary opcodes also cause problems for resources (e.g. the Remove Paralysis spell) that cure an existing opcode. The secondary opcodes associated to that opcode are not automatically removed; sometimes they can be removed manually (i.e. icons can be removed by opcode 240) but often there is no direct way to remove them.

Following his/my original conversations on this, CamDawg has an implementation of this through opcode 321 (remove spell): basically, to every cure opcode is assigned a subspell that removes every single spell/item that uses the opcode. That list has to be hardcoded into the spell, of course.

I've gone with a different implementation: persistent (i.e. duration >6 sec) effects (excluding icons, which can be removed using opcode 240) are packaged into a subspell and that subspell is marked with a secondary type keyed to the primary opcode (e.g. persistent effects associated to Charm go to DW_PERSISTENT_5). Then any effect that cures an opcode also throws a remove-secondary-type effect keyed to the appropriate secondary type. The main advantage of this approach over the 321-based approach is that if spells/items are cloned (fairly common in modern mods) then the cloned resource's secondary effects are automatically handled correctly. (The 321-based method only works if the immunity list is regenerated after any cloning happens. I think this will lead to rather fewer problems with mods. (There is a secondary advantage: this method works in oBG2 as well and could in due course be ported to the BG2 fixpack.)

Removing redundant duplicate blocks

There are many duplicated immunity/removal blocks in EE files due to overenthusiastic automated edits. Redundant copies are deleted. (This is partly for aesthetic purposes but it also makes life easier at later stages.)

Removing redundant icons

EE has allowed many opcodes to automatically generate an appropriate icon, but actual in-game resources often don't take advantage of these changes. This bit of the loop removes any redundant icons. Again, that's partly aesthetic but should also interact more safely with immunity and cure effects.

Allowing the removal of ability drain

There are no 'cure' opcodes associated with the 6 ability-drain icons. As a spinoff of the sectype-based cure system we're using, we rearrange so that any effect applying a temporary ability immunity has secondary type DW_PERSISTENT_700, and so can be cured by 221. This has no effect in the unmodded game but makes it easy to implement ability-drain removal (e.g. SCS's tweak that lets Restoration cure ability drain.)

Implementation notes for the pass-through-all-resources code

The implementation comes in two phases. The first phase is a COPY_EXISTING_REGEXP that goes through every item/spell in the game, and analyzes them one ability block at a time. For reasons of speed this block is entirely hardcoded and uses no low-level functions. The second phase is a pass through a list of items and spells generated in the first phase, to implement any 324 and sectype changes made that can't be done in the primary phase. This block edits many fewer items and makes some use of the CamDawg *_EFFECT function library.

First phase

The primary phase edits every spell/item, and applies the function opcode_pass_core to every ability block in the spell/item. opcode_pass_core loops through a header's opcodes three times, with the following effects:

  1. Loop 1 is mostly data collection. It collects a number of lists (which icons are used, which strings are displayed, which icons are blocked, which strings are blocked, which 101s are used, and in each case which header contains each use/block/whatever). But its main task is to find the block's 'primary payload' (if there is one). A primary payload is basically any opcode that can be blocked/prevented (there is a hardcoded list). We want to determine if there is a primary payload, if so what it is, and whether there is more than one. (Ability score drains count as one payload - internally coded by the virtual opcode '700' - no matter how many actually occur.)

    Loop 1 also does a bit of tidying up. It checks for any block that (a) appears more than once, where (b) having more than one appearance doesn't change anything, and removes it.

  2. Finally, loop 1 makes a record of each opcode in the block tied to an immunity splstate, and each 101 in the block associated with an immunity splstate.

  3. Loop 2 occurs only if there is a single primary payload. It is pure data-collection and determines three things. (i) is the header block 'simple'? Simple header blocks consist only of the primary payload, cosmetic effects like icons and strings, and opcodes that we know to treat as secondary opcodes associated with the payload (like Slow's THAC0 and AC penalties or Fear's morale modifiers.) (ii) Does the header block contain persistent effects? Persistent effects are anything with a timing mode of 0 or 4 and a duration of >6 seconds, with the exception of the payload itself and icons (which can be removed directly). If the payload is ability drain, the actual ability-drain opcodes are also treated as persistent. (iii) Does the header do damage (use opcode 12)? (There is an exception if the primary payload is 55 (death) and the damage is type=magic (e.g. Finger of Death) in which case we treat the damage as a secondary opcode.)
  4. After Loop 2 we pause to record some key information about the header block. Specifically we categorize it as one of:
    • 'skip' (no primary payload, no need to do any subsequent editing in the second phase)
    • 'haste_slow' (there is either an opcode 16 or an opcode 40 in the block)
    • 'simple' (there is only one primary payload, it is not 16 or 40, and loop 2 found no additional non-secondary, non-cosmetic opcodes except possibly 12)
    • 'complex' (there is only one primary payload, it is not 16 or 40, but loop 2 found additional non-secondary, non-cosmetic opcodes other than 12)
    • 'multiple' (there is more than one primary payload, none of which are 16 or 40).
    • Also at this stage we record whether the primary payload is removable and has persistent effects.
  5. Loop 3 is the main place we do edits. It does the following to each effect block, dependent on the opcode:
    • 12 (Damage): if the resource is simple, move to the front of the block (before any immunity block is applied). (Movements to the front are implemented by appending the block to a string 'front' and then deleting it.)
    • 16 (Haste): advanced versions of Haste (16) block (via setting the HASTE_IMMUNITY splstate) any other haste effect; any version removes (via 221) the DW_PERSISTENT_40 sectype
    • 39 (Sleep): as well as incorporating any icon into the main opcode, determines (via icon type) whether the effect is 'short unconsciousness', 'nausea', or 'default'.
    • 40 (Slow): removes (via 221) the DW_PERSISTENT_16 sectype
    • 78 (Disease): sets appropriate icon
    • 98 (Regeneration): sets appropriate icon
    • 101 (immunity to opcode): add the appropriate immunity splstate (taking a little care to get it right if we have 101 immunity to 39).
    • 139 (Display string): move this to the front if (a) the resource is simple and (b) the string is the potion-drinking string 'gulp'. ('gulp' needs to bypass immunities: even if you're immune to the potion's effects, we still need the string to display.)
    • 142 (display icon): Delete any instance that is (or can be) applied directly through an opcode.
    • 170 (damage animation): if the resource is simple, move to the front of the block (before any immunity block is applied). (Movements to the front are implemented by appending the block to a string 'front' and then deleting it.)
    • 206 (immunity to spell): A number of these are redundant (notably, immunity to haste/slow spells used to avoid cumulative effects) and we remove them.
    • 269 (Shake screen): if the resource is simple, move to the front of the block (before any immunity block is applied). (Movements to the front are implemented by appending the block to a string 'front' and then deleting it.)
    • 321 (remove spell): A number of these are redundant (notably, immunity to haste/slow spells used to avoid cumulative effects) and we remove them.
    • 324 (keyed immunity to spell): remove any 324 block that we are about to reimplement, based on the record made in loop 1 (this is for idempotence).
    • 328 (apply splstate): remove any 328 block that we are about to reimplement, based on the record made in loop 1 (again, this is for idempotence).
    • Any cure-opcode icon: get it also to apply a 221 to remove the associated sectype (DW_PERSISTENT_WHATEVER) and check that all icons that should be removed are being removed.
  6. After loop 3, if the block is categorized as 'simple' we prepend any 324 needed to give immunity to its primary payload. (Note that we have removed any damage opcode to the front, so that it will not be blocked.)
  7. Finally, we prepend the contents of the 'front' string, finishing the implementation of moving some blocks to the front of the header.

Once opcode_pass_core has been applied to each block in a resource, we have a list of all the payloads in the resource, as well as some data (based on the loop-2 record) as to how complicated the resource is. We categorize the resource itself based on that output, with one more category: 'inconsistent', for resources that have different types, or different payloads, in different blocks. We then note whether we need to handle the resource in the second phase:

  • Resources of type 'skip' never need handling.
  • Resources of type 'simple' need handling only if they have a removable primary payload and persistent secondary effects.
  • All other resources always need handling.

Second phase

The second phase implements any necessary changes to handle immunity to and removal of payloads. It loops through each resource flagged as needing such in phase 1 and applies the function 'second_pass_process_resource' to it, sending that function as arguments both the resource id itself and its phase 1-identified type.

second_pass_process_resource does the following:

  1. Analyzes the resource to see if it needs 'dismembering': that is, moving its payload(s) to subspells. Resources of type 'inconsistent', 'haste_slow', and 'multiple' always need dismembering. Resources of type 'simple' never need dismembering. Resources of type 'complex' need dismembering only if additional non-secondary opcodes occur in any block after the primary payload or its associated secondary opcodes. (If not, we can just add the immunity block in the middle.)
  2. For complex resources that don't need dismembering, actually add the immunity block.
  3. Also analyzes the resource to see if it has persistent effects. (We don't bother doing this for type 'simple' as we've already checked in the primary phase: if they didn't we wouldn't have listed them for phase 2.
  4. Checks to see if we can get away with directly changing the sectype of the resource. We can do this if (a) it's a spell (items don't have sectypes); (b) either the spell has no name or its sectype is currently 0. If we can, if the resource has only one payload, and if the resource has persistent effects in the first place, change the sectype so they can be removed.
  5. If the resource needs dismembering, do it. Names for subspells are autogenerated, trying to stay close to the original spell/item name. (e.g. we'll automatically use the first available SPWIxxxL-type subspell name.) There is one subspell for each primary payload and we include secondary opcodes as appropriate (using a game-specific set of rules for which strings, animations etc are associated with each primary opcode). Since these are subspells, we can automatically assign them a sectype that lets them be removed as appropriate.
  6. If the resource doesn't need dismembering, but does have persistent effects, and if those persistent effects can't be handled just by changing the sectype of the primary spell, break the persistent effects into a subspell and assign it an appropriate secondary type. (Again, subspell names are autogenerated.) Normally the payload opcode itself is not included in the subspell; exceptions are ability drains (so that we can remove them) and haste/slow (needed to handle haste/slow mutual negation).

Acknowledgements

As always, thanks to those who created the tools that made the modern Infinity Engine modding scene possible: in particular, Westley Weimer, for the original WeiDU; TheBigg and Wisp for developing it into the remarkable tool it now is; the various contributors to Near Infinity, which grows more powerful with every iteration.

The fixes in this function owe much to the various discussions of the Enhanced Edition fixpack, and especially to CamDawg.

Copyright Information

CC TK

DW Opcode Fixer is ©2022-Present, David Wallace.

Since in practice I'm obviously not going to sue anyone, I'll use this section to say what I'd like people's attitude to re-using and redistributing my mods. Basically, I don't mind what you do with the code provided you (a) give me full credit when you borrow or re-use my code in your own mod, and (b) don't actually mirror this mod (or any modified version of this mod) somewhere else.

Version History

Version beta 1 (August 2023)

Original release.