jmerry Posted September 30, 2023 Share Posted September 30, 2023 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. Quote Link to comment
guyudennis Posted September 30, 2023 Author Share Posted September 30, 2023 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%~ Quote Link to comment
jmerry Posted September 30, 2023 Share Posted September 30, 2023 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. Quote Link to comment
guyudennis Posted September 30, 2023 Author Share Posted September 30, 2023 (edited) Maybe you're right...that it's not worth the extra effort to further complicate things, and there's more than one way to judge if a piece of code is clean. Thanks for the discussion. Edited September 30, 2023 by guyudennis Quote Link to comment
subtledoctor Posted September 30, 2023 Share Posted September 30, 2023 (edited) 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: 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. 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. 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 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 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 September 30, 2023 by subtledoctor Quote Link to comment
guyudennis Posted September 30, 2023 Author Share Posted September 30, 2023 Thanks. And yes, REPLACE_TEXTUALLY is a proper bitch. Using your quoted code as an example, if your @10001 = ~I am a master of WeiDU~ it will execute fine, but if your @10001 = ~I am a master (+++) of WeiDU~ it will do nothing, NOTHING! Quote Link to comment
guyudennis Posted October 6, 2023 Author Share Posted October 6, 2023 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... Quote Link to comment
jmerry Posted October 6, 2023 Share Posted October 6, 2023 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. Quote Link to comment
guyudennis Posted October 6, 2023 Author Share Posted October 6, 2023 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: 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?) 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? Quote Link to comment
subtledoctor Posted October 6, 2023 Share Posted October 6, 2023 (edited) 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 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 October 6, 2023 by subtledoctor Quote Link to comment
guyudennis Posted October 7, 2023 Author Share Posted October 7, 2023 (edited) 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 October 7, 2023 by guyudennis Quote Link to comment
Recommended Posts
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.