Jump to content

Spell Expiration Sound Effects Patch


Tash

Recommended Posts

Hi everyone,

 

I feel a little bit more comfortable when it comes to coding now; I'd had almost zero experience prior to my first BG modding attempts some time ago.

 

Part of what my two mods currently do, they bring more consistency to spell casting and expiration sound effects. While everything works in its simplicity, I want to improve my code if only slightly and make a separate component-patch.

 

My mod is for BG1. The component in question will patch all the valid spells to use proper spell expiration sound effects according to this scheme:

 

EFF_E01 - Illusion

EFF_E02 - Abjuration

EFF_E03 - Conjuration

EFF_E04 - Divination

EFF_E05 - Enchantment

EFF_E06 - Invocation

EFF_E07 - Necromancy

EFF_E08 - Alteration

 

The patch will first check for spell casting school. BG1 uses the same reference for schools and casting graphics. Special cases without any reference here will be handled manually if necessary. Next, it scans the available effects for expiration sound effects and modifies them according to the detected school. Once again, special expiration sound effects outside of EFF_E0[1-8] range will be either respected or changed manually.

 

Here's what it looks like on paper:

  COPY_EXISTING_REGEXP ~.*\.spl~ ~override~
   PATCH_IF (SOURCE_SIZE > 0x72) BEGIN
    READ_SHORT 0x22 school // spell casting school and graphics
    READ_LONG 0x6a fx_offset
     FOR (y = %fx_offset% ; y < %SOURCE_SIZE% ; y = y + 0x30) BEGIN // scan all effects
      READ_SHORT %y% opcode
      READ_ASCII (~%y%~ + 0x14) resref
       PATCH_IF ((%opcode% = 174) AND (~%resref%~ STRING_COMPARE_REGEXP ~EFF_E0[1-8]~ = 0)) BEGIN // only expiration sound effects
        PATCH_IF (%school% = 13) BEGIN // Illusion
         WRITE_ASCII (~%y%~ + 0x14) ~EFF_E01~ #8
        END
        PATCH_IF (%school% = 12) BEGIN // Abjuration
         WRITE_ASCII (~%y%~ + 0x14) ~EFF_E02~ #8
        END
        PATCH_IF (%school% = 14) BEGIN // Conjuration
         WRITE_ASCII (~%y%~ + 0x14) ~EFF_E03~ #8
        END
        PATCH_IF (%school% = 16) BEGIN // Divination
         WRITE_ASCII (~%y%~ + 0x14) ~EFF_E04~ #8
        END
        PATCH_IF (%school% = 11) BEGIN // Enchantment
         WRITE_ASCII (~%y%~ + 0x14) ~EFF_E05~ #8
        END
        PATCH_IF (%school% = 15) BEGIN // Invocation
         WRITE_ASCII (~%y%~ + 0x14) ~EFF_E06~ #8
        END
        PATCH_IF (%school% = 09) BEGIN // Necromancy
         WRITE_ASCII (~%y%~ + 0x14) ~EFF_E07~ #8
        END
        PATCH_IF (%school% = 10) BEGIN // Alteration
         WRITE_ASCII (~%y%~ + 0x14) ~EFF_E08~ #8
        END
       END
     END
   END
  BUT_ONLY

Would there be any issues with this? Can I optimize it somehow?

 

One possible issue would be if the given spell has more than one expiration sound effect, and each use different resources. I don't know of any spells like that in BG1, but I do remember a few from BG2. In such case, these spells will be excluded from the patch and handled separately.

 

Also, as I understand it, if I want to scan custom spells added by other mods, it's all just a matter of adding a GLOB. Is that right?

 

I know it's all a goblin cake for you guys, so I'd be grateful for your insights :).

Link to comment

I would only add a GLOB after COPY_EXISTING_REGEXP. It's a bit of a nitpick but it won't get files in the override folder otherwise ;)

 

Well, reading the docs I understand that GLOB will also patch files which aren't in the key; that means custom spells whose authors have consciously chosen specific sound effects for. But without GLOB, spells modified by other mods that are in the override, and that are in the key, will still be patched <- that's what I want. Do I understand this correctly? Well, in any case, patching mod-introduced spells would probably be a good idea for consistency.

 

Also, I think this should speed up the process:

  COPY_EXISTING_REGEXP GLOB ~^.+\.spl$~ ~override~
   PATCH_IF (SOURCE_SIZE > 0x72) BEGIN // only if there is more than a header
    READ_SHORT 0x22 school
     PATCH_IF (%school% != 00) BEGIN // only if a school is actually assigned
      READ_LONG 0x6a fx_offset
       FOR (y = %fx_offset% ; y < %SOURCE_SIZE% ; y = y + 0x30) BEGIN
       (...)
Link to comment

I added an associative array and loops that might be safer on files with non-standard orders (effects before abilities).

 

ACTION_DEFINE_ASSOCIATIVE_ARRAY expiresounds BEGIN
  13 => EFF_E01 // Illusion
  12 => EFF_E02 // Abjuration
  14 => EFF_E03 // Conjuration
  16 => EFF_E04 // Divination
  11 => EFF_E05 // Enchantment
  15 => EFF_E06 // Invocation
  9  => EFF_E07 // Necromancy
  10 => EFF_E08 // Alteration
END

DEFINE_PATCH_FUNCTION ~UPDATE_EXPIRY_SOUND~
  INT_VAR offset = 0
  STR_VAR sound = ~~
BEGIN
  READ_SHORT (offset) opcode
  READ_ASCII (offset + 0x14) resref
  PATCH_IF (opcode == 174 && (~%resref%~ STRING_COMPARE_REGEXP ~EFF_E0[1-8]~ == 0)) BEGIN
    WRITE_ASCIIE (offset + 0x14) ~%sound%~ #8
  END
END

COPY_EXISTING_REGEXP GLOB ~^.+\.spl$~ ~override~
  PATCH_IF (SOURCE_SIZE > 0x72) BEGIN
    READ_SHORT 0x22 school // spell casting school and graphics
    PATCH_IF (VARIABLE_IS_SET $expiresounds(~%school%~)) BEGIN
      TEXT_SPRINT sound $expiresounds(~%school%~)
      READ_LONG  0x64 abilities_off
      READ_SHORT 0x68 num_abilities
      READ_LONG  0x6a effects_off
      READ_SHORT 0x6e effects_ind
      READ_SHORT 0x70 num_effects
      FOR (i = 0; i < num_effects; i += 1) BEGIN
        LPF ~UPDATE_EXPIRY_SOUND~ INT_VAR offset = (effects_off + 0x30*(effects_ind + i)) STR_VAR sound END
      END
      FOR (i = 0; i < num_abilities; i += 1) BEGIN
        READ_SHORT (abilities_off + 0x28*i + 0x1e) num_features
        READ_SHORT (abilities_off + 0x28*i + 0x20) features_ind
        FOR (j = 0; j < num_features; j += 1) BEGIN
          LPF ~UPDATE_EXPIRY_SOUND~ INT_VAR offset = (effects_off + 0x30*(features_ind + j)) STR_VAR sound END
        END
      END
    END
  END
  BUT_ONLY
Regarding use of GLOB, it would be much weirder if your component behaved differently on vanilla files versus mod-added ones, so I would always use it.

 

Also, I would think mod-added spells would be even more likely to have unintentional mismatches between schools and expiry sounds. I can't imagine what the rationale would be for an intentional mismatch.

Link to comment

All right! Thanks, CreevsDaak, Imp and Mike1072! :)

 

Mike, big thanks for the revised code. I'll have to take a look and understand new functions, but it indeed seems like these non-standard spell files could potentially be a problem with my simple code.

 

I'll also use GLOB as you guys suggest.

Link to comment

A few questions about arrays, which seem very useful.

 

From Mike's code:

(...)
PATCH_IF (VARIABLE_IS_SET $expiresounds(~%school%~)) BEGIN
 TEXT_SPRINT sound $expiresounds(~%school%~)
(...)

Do I understand this correctly that an array can be called twice in the code; the first time it's called it returns the key, the second time it returns the associated value/string?

 

In the above example, "$expiresounds(~%school%~)" is the same in both lines, so how does TEXT_SPRINT know to use the string (eg. "EFF_E07") instead of its defined key ("9")? Is it because TEXT_SPRINT is internally smart, or because key has already been returned in PATCH_IF condition?

 

I'm also having a hard time understanding the last bit of this launched patch function:

FOR (i = 0 ; i < %num_effects% ; i += 1) BEGIN
 LPF %change_exp_sound_fx% INT_VAR offset = (%effects_off% + 0x30 * (%effects_ind% + i)) STR_VAR sound_fx END

While "INT_VAR offset = (...)" is clear, at the end of the line, "STR_VAR sound_fx" has no equation mark and no value/string, just plain empty variable, so I've no idea what it does then.

 

Other than that, in Mike's code some of the byte references are differant than at IESDP, so it's hard for me to look things up. For example, 0x70 is normally the number of effects and 0x6e is casting feature offset; these are now swapped around in Mike's sample. I'm a little confused, is this what you call the "non-standard" spell files? Are there really that many of them? Why would anybody want to change the position of headers...?

Link to comment

 

(...)
PATCH_IF (VARIABLE_IS_SET $expiresounds(~%school%~)) BEGIN
 TEXT_SPRINT sound $expiresounds(~%school%~)
(...)
Do I understand this correctly that an array can be called twice in the code; the first time it's called it returns the key, the second time it returns the associated value/string?

 

The important part of that if statement is the VARIABLE_IS_SET command. It takes a string parameter and returns true if a value has been assigned to a variable with that name. WeiDU's array syntax is just a fancy way of dealing with variables: $expiresounds(~15~) corresponds to the variable expiresounds_15.

 

So, you can compare the above code to this:

PATCH_IF (VARIABLE_IS_SET expiresounds_15) BEGIN
  TEXT_SPRINT sound ~%expiresounds_15%~

I'm also having a hard time understanding the last bit of this launched patch function:

FOR (i = 0 ; i < %num_effects% ; i += 1) BEGIN
 LPF %change_exp_sound_fx% INT_VAR offset = (%effects_off% + 0x30 * (%effects_ind% + i)) STR_VAR sound_fx END
While "INT_VAR offset = (...)" is clear, at the end of the line, "STR_VAR sound_fx" has no equation mark and no value/string, just plain empty variable, so I've no idea what it does then.

 

It's a shorthand for STR_VAR sound_fx = ~%sound_fx%~.

 

 

Other than that, in Mike's code some of the byte references are differant than at IESDP, so it's hard for me to look things up. For example, 0x70 is normally the number of effects and 0x6e is casting feature offset; these are now swapped around in Mike's sample.

 

There was one typo there; fixed now.

 

The naming is not consistent between IESDP and tools like NearInfinity. I settled on the names I use now after some frustration with using the others. I make a clear distinction between offsets (positions in the file) and indices (positions in a list).

 

The code I provided is actually a perfect example of looping over everything in the file. A spell consists of abilities and effects. Each ability can have multiple effects. The spell can also have global effects. So, to iterate over all of the effects, you need to iterate over the global effects and the effects associated with each ability. I refer to the effects associated with abilities as features, to distinguish them from the global effects.

 

 

I'm a little confused, is this what you call the "non-standard" spell files? Are there really that many of them? Why would anybody want to change the position of headers...?

The standard layout would be HEADER + ABILITIES + EFFECTS. However, the SPL format just as easily supports files with the layout HEADER + EFFECTS + ABILITIES. So if you iterate from effects_off to the end of the file, you aren't guaranteed that you will only encounter effects.

Link to comment

Thanks for your time, Mike! You've been very helpful :).

 

I'd like to use your code and give you credit, but first I just want to make sure I understand everything, hence my questions. No blind copy & pasting on my side. I'm no coder, but with IESDP and all the available documentation, it's not that hard. So I study every command, but once in a while things are different in practice, or in other people's codes, and then it's just, "did I miss something?"

Link to comment

Archived

This topic is now archived and is closed to further replies.

×
×
  • Create New...