Version: beta 1
Languages: English
Platforms: Windows, Linux, macOS
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.)
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).
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:
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:
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.
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.
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.
Sleep is heavily overloaded: its uses include
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.
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:
My underlying assumptions:
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.)
Here is how I am fairly sure Slow and Haste are supposed to work:
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:
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.
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' backlash problem is fixed.
A bunch of 'display string' entries have timing mode 0 rather than 1; that's probably harmless but we fix it anyway.
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.
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.)
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.)
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.
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.)
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.
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:
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.
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.
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:
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:
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.
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 beta 1 (August 2023)
Original release.