Jump to content

guyudennis’ random questions


Recommended Posts

So, what are you looking for? The tra reference at a variable number? Doable. A line from my work:

SAY NameOffset ( AT TraNum )

That "AT variable" construct evaluates the variable and takes the tra reference at that number. Here, I'm using it in a loop as part of my component that puts area names on the world map; use a lookup table (external 2DA) to associate areas with tra reference numbers for their new string names, then loop through the map's areas to use them.

Or maybe you want to stitch strings together out of pieces in your tra file? That's doable too. In most circumstances, just using an @ reference evaluates it as a string, which you can then perform operations on. From my joke kit mod:

	SPRINT desc1 @1501
	SPRINT desc2 @1510
	SPRINT descstr ~%desc1%~^~%desc2%~

Or maybe it's something I didn't think of - but there's a good chance you can make it work anyway.

Link to comment

Thanks for the quick reply.

I'm tweaking druid class, and have a bunch of ini options to enable/disable certain class features.

So in the druid tweak tpa file, which is run fairly early since it's modifying a base class,I have this kind of code:

LAF GET_KIT_STRREF_EX STR_VAR kit_name = ~DRUID~ RET kit_strref END // custom function to return druid class description
OUTER_SPRINT help_druid @15 // my custom druid description as the basis
OUTER_PATCH_SAVE help_druid ~%help_druid%~ BEGIN // becasue we want later components to be able to inherit an already-changed description
  // do some textual replacements here based on different ini options
END
STRING_SET_EVALUATE %kit_strref% ~%help_druid%~ // set it here already in case only this particular component is installed

Then in another component, say an item tweak component, which is run towards the tail-end of the installation process to ensure all items are patched, I have this kind of code:

ACTION_IF (druid_short_bow = 1) BEGIN
  LAF GET_KIT_STRREF_EX STR_VAR kit_name = ~DRUID~ RET kit_strref END
  ACTION_IF (NOT (VARIABLE_IS_SET ~help_druid~)) BEGIN // in case the druid class tweak component from earlier was not installed
    ACTION_GET_STRREF %kit_strref% help_druid // so we pull the default one instead
  END
  OUTER_PATCH_SAVE help_druid ~%help_druid%~ BEGIN
    REPLACE_TEXTUALLY ~\(May only use the following weapons: scimitar, dagger, club, spear, quarterstaff, dart, sling\)~ ~\1, shortbow~
  END
  STRING_SET_EVALUATE %kit_strref% ~%help_druid%~
END

This runs okey, since the item tweak code can basically detect any previous changes to the druid class description made by any earlier components and have the default one as fallback, and dynamically modify the description further, and still saves the description as an variable just in case any other component might want to modify the same description later again.

However, as you can see, even though it's utilizing a tra, the code in the item tweak section is not really translation-friendly. I know I could set up the two strings in the REPLACE_TEXTUALLY portion as two further references in the tra file, but then I need to SPRINT another two variables against these two references just to satisfy syntax compatibility for REPLACE_TEXTUALLY. So I'm wondering if there's a more elegant way of doing it.

PS. I am aware I could simply break down the original class description (@15) in my tra into several pieces, then I can have all scenarios covered and perhaps avoid the need to do any textual modifications at all, but it involves even more (in my opinion needlessly consumed) variables here:

LAF GET_KIT_STRREF_EX STR_VAR kit_name = ~DRUID~ RET kit_strref END
OUTER_SPRINT help_druid1 @15
OUTER_SPRINT help_druid2 @16
OUTER_SPRINT help_druid3 @17
ACTION_IF (druid_3e_alignments = 1) BEGIN
  OUTER_SPRINT help_druid4 @19
END ELSE BEGIN
  OUTER_SPRINT help_druid4 @18
END
OUTER_SPRINT help_druid5 @20
OUTER_SPRINT help_druid ~%help_druid1%%WNL%%help_druid2%%WNL%%help_druid3%%WNL%%help_druid4%%WNL%%help_druid5%~
STRING_SET_EVALUATE %kit_strref% ~%help_druid%~

 

Link to comment

Honestly, there's nothing wrong with naming a bunch of things and then stitching them together. That code in your last example is very clear and easy to understand. Which is a good thing. Maybe we could make it a little shorter with the right tricks, but would that even really be better?

It's been more than a year since I made that joke kit I pulled my earlier "stitching things together" example from. Would I do some things different if I tried that project now? Of course. But the bit I showed you isn't one of the pieces I would change. It does its job, and it does it cleanly.

Link to comment

Patching text and dealing with translations is hard. But in some cases maybe worth it? I do it to an extent in Scales of Balance. What I do:

  1. Find a word or phrase to match against, and look it up in the dialog.tlk of every game language. In my armor mod I grab the phrase "armor class" but for a class description you might use the phrase "CLASS FEATURES." This should be pretty easy to find by looking at strref 9560 (the druid class description) in every language's .tlk file.
  2. Put each of those translations into its own .tra reference, in variants of a special .tra file. See my different versions of armor.tra in these subdirectories in Scales of Balance.
  3. Adapt code to figure out which version of your phrase matches the version in the language used by the player
    Spoiler
    //USE GAME INSTALL LANGUAGE FOR WORD MATCHING_______________________________________
    //
    ACTION_IF NOT (VARIABLE_IS_SET %EE_LANGUAGE%) BEGIN
      COPY_EXISTING ~leat01.itm~ ~override~
        READ_STRREF 0x08 ac_string
      BUT_ONLY
      ACTION_FOR_EACH lang IN ~en_US~ ~pl_PL~ ~de_DE~ ~es_ES~ ~fr_FR~ ~cs_CZ~ ~ru_RU~ ~ko_KR~ BEGIN
        WITH_TRA ~scales_of_balance/language/%lang%/armor.tra~ BEGIN
          OUTER_SPRINT ac_lang @10001
        END	  
        ACTION_IF NOT (~%ac_string%~ STRING_CONTAINS_REGEXP ~%ac_lang%~) BEGIN
          OUTER_SPRINT yaras_lang ~%lang%~
        END
      END
    END
    
    ACTION_IF (VARIABLE_IS_SET %EE_LANGUAGE%) BEGIN
      OUTER_SPRINT ee_lang ~%EE_LANGUAGE%~
      ACTION_IF (FILE_EXISTS ~scales_of_balance/language/%ee_lang%/armor.tra~) BEGIN
        OUTER_SPRINT yaras_lang ~%EE_LANGUAGE%~
      END
    END
    
    ACTION_IF NOT (VARIABLE_IS_SET %yaras_lang%) BEGIN
      OUTER_SPRINT yaras_lang ~en_US~
    END
  4. Now that you know which .tra files to use and have it as a variable (in the above example, %yaras_lang%), SPRINT those versions of your text replacements into variables:
    Spoiler
    WITH_TRA ~scales_of_balance/language/%yaras_lang%/armor.tra~ BEGIN
      OUTER_SPRINT catch_ac @10001
      OUTER_SPRINT new_ac @10002
    END
  5. Then make your replacement, and if all goes well it should work in different languages even with the finicky "REPLACE_TEXTUALLY" -
    Spoiler
    	READ_LONG 0x54 valid
    	PATCH_IF (%valid% >= 0) BEGIN // verify name is valid
    	  READ_STRREF 0x54 "desc"
    	  INNER_PATCH_SAVE new_desc ~%desc%~ BEGIN
    		REPLACE_TEXTUALLY ~%catch_ac%~  ~%new_ac%~
    	  END
    	  SAY_EVALUATED 0x54 ~%new_desc%~
    	END

You can concatenate different additions to the class description and keep making the same replacement, as long as you keep the original replacement text your additions will just stack up and still be readable.

Edited by subtledoctor
Link to comment
On 2/6/2023 at 12:25 AM, subtledoctor said:

Only if it uses op148. No need for this with op146. 

EDIT - and the difference is not necessarily [AoE vs. non-AoE]. You use op148 specifically when the original spell has a value of 4 in the header target field. (Some AoE spells use different targeting. e.g. Hold Person. To do this with Hold Person you would use op146.)

EDIT 2 - here’s some code that does this in Weidu. Lines 2179-2210. 

Coming back to this as I finally got around to implement this; one question regarding your example code: for op146, why do you use target = 2 (instead of 1) and timing = 9 (instead of 1)? If I look at IESDP spell file format, it says the target does not matter here because it's cast as a subspell. The timing bit I guess comes down to my lack of understanding of the differences between 1 and 9...

Link to comment

You're talking about the target of the 148 effect itself, which does matter.

Target = 2 means "cast the subspell on the ability's target".

Target = 1 means "cast the subspell on the original caster, no matter what the ability's target is".

For this application of replacing spell X with a wrapper spell Y that casts X as a subspell, we definitely want the former. For example, if the spell we're converting is Magic Missile, using target = 2 would mean that we click on an enemy, cast the original Magic Missile at them, and the missiles go and do damage. Using target = 1 would mean that we click on an enemy, cast the original Magic Missile on ourselves, and ouch.

As for timing mode 9 versus timing mode 1 ... there's no difference with opcode 146 or 148, as they're inherently one-shot effects. Timing modes 9 and 1 are only different for lingering effects; the difference is particularly stark with opcodes that modify stats. In that case, timing mode 9 puts a permanent effect on the character, while timing mode 9 modifies the base stat in the CRE file. For example, a Dragon Disciple's CON bonus is timing mode 9, while a manual of health is timing mode 1. Both increment CON by 1, but the different timing modes make for very different effects.

Link to comment

Thanks.

But here I think you meant to say 146 instead of 148 (which according to IESDP should just have target = 1) ?

5 hours ago, jmerry said:

You're talking about the target of the 148 effect itself, which does matter.

Target = 2 means "cast the subspell on the ability's target".

Target = 1 means "cast the subspell on the original caster, no matter what the ability's target is".

For this application of replacing spell X with a wrapper spell Y that casts X as a subspell, we definitely want the former. For example, if the spell we're converting is Magic Missile, using target = 2 would mean that we click on an enemy, cast the original Magic Missile at them, and the missiles go and do damage. Using target = 1 would mean that we click on an enemy, cast the original Magic Missile on ourselves, and ouch.

Other than that, two more observations/questions regarding @subtledoctor's example code:

  1. The LPF DELETE_EFFECT INT_VAR match_probability2 = 0 END bit does not clean out all existing spell effects as intended, as there's spells (ie conjure XXX elemental) whose spell effects have non-zero probability2 settings. (Probably a reading of effect# offset coupled with a loop to delete them all would be a more robust solution?)
  2. In case the payload spell has multiple ability headers for different levels, should the wrapper spell inherit all these headers (with each header just a 146/148)? I guess it doesn't really matter in practice, but in principle we only need one ability header for the wrapper spell, right?
Link to comment
19 minutes ago, guyudennis said:

The LPF DELETE_EFFECT INT_VAR match_probability2 = 0 END bit does not clean out all existing spell effects as intended

Indeed, I wrote that a long long time ago and was concerned about using DELETE_EFFECT without any matching parameters, but I have since become comfortable using more simply: "LPF DELETE_EFFECT END"

19 minutes ago, guyudennis said:

In case the payload spell has multiple ability headers for different levels, should the wrapper spell inherit all these headers (with each header just a 146/148)? I guess it doesn't really matter in practice, but in principle we only need one ability header for the wrapper spell, right?

Correct, it would be cleaner if the wrapper spell only had a single header. But, having multiple identical headers is harmless, and going to the trouble of removing all but one header would be more work, more code, and more opportunities for bugs. With a low ratio of reward-to-effort and lots of other things to spend time on, I figured :undecided:

EDIT - this is a super useful function and it would be great if more mods did this instead of cloning spells. So if you are interested in cleaning up my code/fixing errors and turning this into a nice portable function, I would support that 100%. Would be a nice addition over here.

Edited by subtledoctor
Link to comment

Final check before I post it in the other thread? @subtledoctor @jmerry

/////////////////////////////////////////////////////////////////////////////////////////////////////
// Borrowed and adapted from 5E Spellcasting: create wrapper spell from any existing payload spell //
/////////////////////////////////////////////////////////////////////////////////////////////////////

DEFINE_ACTION_FUNCTION PAYLOAD_SPELL_TO_WRAPPER_SPELL
  INT_VAR
    wrapper_type = 4 // default to innate (0 - special, 1 - wizard, 2 - priest, 3 - psionic, 4 - innate, 5 - bard song)
    wrapper_location = 4 // default to ability (0 - none, 1 - weapon, 2 - spell, 3 - item, 4 - ability)
  STR_VAR
    payload_spell = ~~ // default to empty
    wrapper_spell = ~~ // default to empty
    wrapper_name = ~~ // default to empty (payload spell name)
    wrapper_desc = ~~ // default to empty (payload spell description)
  RET
    debug_msg
BEGIN
  COPY_EXISTING ~%payload_spell%.spl~ ~override/%wrapper_spell%.spl~
    READ_STRREF NAME1 payload_name
    PATCH_IF (STRING_LENGTH ~%wrapper_name%~ > 0) BEGIN
      SAY NAME1 ~%wrapper_name%~ // set spell name
    END
    READ_STRREF NAME1 wrapper_name
    PATCH_IF (STRING_LENGTH ~%wrapper_desc%~ > 0) BEGIN
      SAY UNIDENTIFIED_DESC ~%wrapper_desc%~ // set spell description
    END
    WRITE_SHORT 0x1c wrapper_type // set spell type
    PATCH_IF wrapper_type = 4 BEGIN
      WRITE_BYTE 0x27 0 // no sectype for innates
    END
    LPF ALTER_SPELL_HEADER INT_VAR location = wrapper_location END // set ability location
    READ_LONG 0x64 abil_offset
    READ_SHORT 0x68 abil_number
    READ_BYTE (%abil_offset% + 0x0c) abil_target
    READ_SHORT (%abil_offset% + 0x0e) abil_range
    WHILE (%abil_number% > 1) BEGIN
      SET abil_number = (%abil_number% - 1)
      READ_SHORT (%abil_offset% + 0x10 + (0x28 * %abil_number%)) abil_minlv
      LPF DELETE_SPELL_HEADER INT_VAR header_type = "-1" min_level = abil_minlv END // delete all but the 1st ability headers
    END
    WRITE_SHORT (%abil_offset% + 0x26) 1 // set projectile to none
    LPF DELETE_EFFECT END // delete all existing spell extended effects
    PATCH_IF (%abil_target% = 4) BEGIN // if ability target is any point within range
      LPF ADD_SPELL_EFFECT INT_VAR opcode = 148 target = 1 parameter2 = 1 timing = 1 STR_VAR resource = EVAL ~%payload_spell%~ END // cast spell at point: payload spell
      PATCH_IF (%abil_range% < 35) BEGIN
        PATCH_IF (%abil_range% > 4) BEGIN
          LPF ALTER_SPELL_HEADER INT_VAR range = (%abil_range% - 3) END // workaround to engine wierdness
        END
        PATCH_IF (%abil_range% < 5) BEGIN
          LPF ALTER_SPELL_HEADER INT_VAR target = 5 END	// set ability target to self; might need to carve out exceptions, e.g. burning hands
        END
      END
    END ELSE BEGIN
      LPF ADD_SPELL_EFFECT INT_VAR opcode = 146 target = 2 parameter2 = 1 timing = 1 STR_VAR resource = EVAL ~%payload_spell%~ END // caste spell at creature: payload spell
    END
  BUT_ONLY
  OUTER_TEXT_SPRINT debug_msg ~Wrapper spell %wrapper_spell% (%wrapper_name%) of type %wrapper_type% created at location %wrapper_location% to launch payload spell %payload_spell% (%payload_name%).~
END

The function would make a "wrapper spell" that would in turn launch a "payload spell".

For the wrapper spell, one can further define spell type, spell location, spell name, and spell description. Otherwise, it would retain the payload spell's most information intact (sectype would be changed to none in case of innate type), have only one ability header without any projectile, and have only one extended effect to launch the payload spell (via 146/148 depending on the payload spell's targeting).

I toyed around the idea of also giving the option to change wrapper spell flags and exclusion flags, but could not get my head around how the default value would be set, since both none and all could be potentially what one wants to set exactly. Distinguishing BAND or BOR is another complication.

I hope I finally got the targeting of 146 and 148 correct respectively... please help me double check.

Sample usage:

ACTION_FOR_EACH filename IN ~sppr731~ ~sppr732~ BEGIN // payload spells: fire elemental transformation, earth elemental transformation
  LAF PAYLOAD_SPELL_TO_WRAPPER_SPELL
    INT_VAR wrapper_type = 4
            wrapper_location = 4
    STR_VAR payload_spell = EVAL ~%filename%~
            wrapper_spell = EVAL ~%filename%w~
            wrapper_name = EVAL ~%filename% wrapper~
            wrapper_desc = EVAL ~%filename% blah blah blah~
    RET debug_msg
  END
  PRINT ~%debug_msg%~
END
Edited by guyudennis
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...