Jump to content
Sign in to follow this  
DavidW

Improving on INTERJECT_COPY_TRANS3 - passbacks only if necessary

Recommended Posts

A large part of NPC modding is the interjections into existing conversations, and WEIDU has a lot of technology to help with it. I recently wrote some functions to supplement that technology a bit; this series of posts is an explanation.

In this first post I'll explain current best practice for these interjections. (Much of this material is also covered in a very informative series of posts by Kaeloree at Spellhold Studios.) In the second post I'll say what I think is wrong with it; in the third post I'll suggest a way to improve on it.

Basic interjections work like this. An existing dialog block might look like:

IF ~~ THEN BEGIN 14
SAY ~Hello, O great adventurer.~
IF ~~ THEN REPLY ~Finally, someone recognises my greatness!~ GOTO 15
IF ~~ THEN REPLY ~Aw, shucks, I'm not so great as all that.~ GOTO 16
END

A simple interjection, most straightforwardly coded with INTERJECT_COPY_TRANS, changes it to something like

IF ~~ THEN BEGIN 14
SAY ~Hello, O great adventurer.~
IF ~~ THEN REPLY ~Finally, someone recognises my greatness!~ GOTO 15
IF ~~ THEN REPLY ~Aw, shucks, I'm not so great as all that.~ GOTO 16
IF ~InParty("imoen2")~ THEN EXTERN imoen2 17
END
<imoen block>
IF ~~ THEN BEGIN 17
SAY ~Hey, you'll give <CHARNAME> delusions of grandeur!~
IF ~~ THEN REPLY ~Finally, someone recognises my greatness!~ EXTERN original_npc 15
IF ~~ THEN REPLY ~Aw, shucks, I'm not so great as all that.~ EXTERN original_npc 16
END

In other words, if Imoen is present then the dialog skips to her interjection; the player then gets offered the original reply options from her dialog.

That works fine *unless* there are DO blocks in the original. Suppose, say, that the original block was:

IF ~~ THEN BEGIN 14
SAY ~Hello, O great adventurer. Have some gold as a token of your magnificence.~
IF ~~ THEN REPLY ~Finally, someone recognises my greatness!~ DO ~GivePartyGold(500)~ GOTO 15
IF ~~ THEN REPLY ~Aw, shucks, I'm not so great as all that.~ DO ~GivePartyGold(500)~ GOTO 16
END

If those blocks move to Imoen, then it will be her, not the NPC, who tries to give the gold to the party, and that doesn't work - she doesn't have the gold in her inventory. (It would be even more dramatic if there was an Enemy() command!) One solution - this is what INTERJECT_COPY_TRANS2 does - is to move the GivePartyGold command to the original Imoen reply, as in:

 

IF ~~ THEN BEGIN 14
SAY ~Hello, O great adventurer.  Have some gold as a token of your magnificence.~
IF ~~ THEN REPLY ~Finally, someone recognises my greatness!~ DO ~GivePartyGold(500)~ GOTO 15
IF ~~ THEN REPLY ~Aw, shucks, I'm not so great as all that.~ DO ~GivePartyGold(500)~ GOTO 16
IF ~InParty("imoen2")~ THEN DO ~GivePartyGold(500)~ EXTERN imoen2 17
END

But modders are generally advised against this, as it only works if all the DO actions are the same. Suppose instead that the original block looks like

IF ~~ THEN BEGIN 14
SAY ~Hello, O great adventurer. Have some gold as a token of your magnificence.~
IF ~~ THEN REPLY ~Finally, someone recognises my greatness!~ DO ~GivePartyGold(500)~ GOTO 15
IF ~~ THEN REPLY ~Aw, shucks, I'm not so great as all that. Your need is greater than mine - keep the gold.~ GOTO 16
END

Now there's no way to move 'the' DO command to Imoen's interjection, because there's no single DO command.

Because of this problem, current best practice is to include a 'passback'  - a block in the original NPC's dialog that the original replies move to. With a passback, the original dialog gets modified to:

IF ~~ THEN BEGIN 14
SAY ~Hello, O great adventurer. Have some gold as a token of your magnificence.~
IF ~~ THEN REPLY ~Finally, someone recognises my greatness!~ DO ~GivePartyGold(500)~ GOTO 15
IF ~~ THEN REPLY ~Aw, shucks, I'm not so great as all that. Your need is greater than mine - keep the gold.~ GOTO 16
IF ~~ THEN GOTO 99
END
IF ~~ THEN BEGIN 99
SAY ~Yes, you really are great.~
IF ~~ THEN REPLY ~Finally, someone recognises my greatness!~ DO ~GivePartyGold(500)~ GOTO 15
IF ~~ THEN REPLY ~Aw, shucks, I'm not so great as all that. Your need is greater than mine - keep the gold.~ GOTO 16
END

In the original block (block 14) the original replies never have a chance to fire - the dialog automatically jumps to block 99. After an extra line of dialog, the original replies are there.

Now when Imoen's interjection is added, we get

IF ~~ THEN BEGIN 14
SAY ~Hello, O great adventurer. Have some gold as a token of your magnificence.~
IF ~~ THEN REPLY ~Finally, someone recognises my greatness!~ DO ~GivePartyGold(500)~ GOTO 15
IF ~~ THEN REPLY ~Aw, shucks, I'm not so great as all that. Your need is greater than mine - keep the gold.~ GOTO 16
IF ~~ THEN GOTO 99
IF ~InParty("imoen2")~ THEN EXTERN imoen2 17
END
	IF ~~ THEN BEGIN 99
SAY ~Yes, you really are great.~
IF ~~ THEN REPLY ~Finally, someone recognises my greatness!~ DO ~GivePartyGold(500)~ GOTO 15
IF ~~ THEN REPLY ~Aw, shucks, I'm not so great as all that. Your need is greater than mine - keep the gold.~ GOTO 16
END
<imoen block>
IF ~~ THEN BEGIN 17
SAY ~Hey, you'll give <CHARNAME> delusions of grandeur!~
IF ~~ THEN REPLY ~Finally, someone recognises my greatness!~ DO ~GivePartyGold(500)~ EXTERN original_npc 15
IF ~~ THEN REPLY ~Aw, shucks, I'm not so great as all that. Your need is greater than mine - keep the gold.~ EXTERN original_npc 16
IF ~~ THEN GOTO 99
END


The transitions are copied to Imoen's block, and again the original replies never have a chance to fire. Instead we're routed back to the passback. The original replies are then offered to the player, and because they're being offered by the original NPC, there's no problem with actions being associated to the wrong creature.

All this normally gets implemented via the INTERJECT_COPY_TRANS3 command in WEIDU. Here's an example from BG1NPC (bg1npc/phase2/dlg/x#ict3.d):

/* Davaeorn */
/* passback supplied */
I_C_T3 ~tutu_varDAVAEO~ 0 X#DAVAEO0
== ~YESLIJ~ IF ~InParty("yeslick") InMyArea("yeslick") !StateCheck("yeslick",CD_STATE_NOTVALID)~ THEN @206
== ~DAVAEO~ IF ~InParty("yeslick") InMyArea("yeslick") !StateCheck("yeslick",CD_STATE_NOTVALID)~ THEN @207
== ~DYNAHJ~ IF ~InParty("dynaheir") InMyArea("dynaheir") !StateCheck("dynaheir",CD_STATE_NOTVALID)~ THEN @208
== ~IMOENJ~ IF ~InParty("imoen") InMyArea("imoen") !StateCheck("imoen",CD_STATE_NOTVALID) !Class("imoen",MAGE_ALL)~ THEN @209
== ~CORANJ~ IF ~InParty("coran") InMyArea("coran") !StateCheck("coran",CD_STATE_NOTVALID)~ THEN @210
== ~SHARTJ~ IF ~InParty("sharteel") InMyArea("sharteel") !StateCheck("sharteel",CD_STATE_NOTVALID)~ THEN @211
== ~FALDOJ~ IF ~InParty("faldorn") InMyArea("faldorn") !StateCheck("faldorn",CD_STATE_NOTVALID)~ THEN @212
== ~DAVAEO~ IF ~InParty("faldorn") InMyArea("faldorn") !StateCheck("faldorn",CD_STATE_NOTVALID)~ THEN @213
== ~KIVANJ~ IF ~InParty("kivan") InMyArea("kivan") !StateCheck("kivan",CD_STATE_NOTVALID)~ THEN @214
== ~VICONJ~ IF ~InParty("viconia") InMyArea("viconia") !StateCheck("viconia",CD_STATE_NOTVALID)~ THEN @215
== ~DAVAEO~ @216
END

The last line is the passback. All the other lines are the various interjections by party members. (For readability I've removed the various bits of BG1NPC code that localise this to BGT/BG1TUTU/BGEE.)

Share this post


Link to post

The passback solution is elegant and reliable: so what's wrong with it? 

One problem is that the code it generates is very cluttered. You can see this in the Imoen example in the last post: there are extra REPLYs in Imoen's block and in block 14 that are guaranteed never to fire. In realistic code it gets much, much worse than that - if you actually implement that Davaeorn interject (e.g. by installing BG1NPC) and then do 'weidu davaeo.dlg --transitive' you'll see what I mean. There are many copies of each block, and long strings of reply options that are just along for the ride, and if you try to make sense of the dialog block, it's almost impossible to understand the logic. But this doesn't matter that much: you can't see that tangle in-game, it doesn't slow things down, and trying to modify existing interjections in another mod is an edge case at best.

The real problem is that the passback blocks upset the flow of the dialog. Look at Davaeorn again: in the unmodded game, his line to you is the marvellously dismissive


Why have you come? Is it to steal my riches or perhaps you seek to righteously punish me for my affront to your morality? It matters little, for you will do neither. Before I dispose of you in some horribly gruesome manner, perhaps I should introduce myself. I am known as Davaeorn. I would ask you for your names, but I care little to become acquainted with the dead.


after which he goes hostile and attacks without any further comment.

In BG1NPC, if you encounter Davaeorn without any NPCs present, this becomes


Why have you come? Is it to steal my riches or perhaps you seek to righteously punish me for my affront to your morality? It matters little, for you will do neither. Before I dispose of you in some horribly gruesome manner, perhaps I should introduce myself. I am known as Davaeorn. I would ask you for your names, but I care little to become acquainted with the dead.

Now that we have been introduced, it is time for you to die!


The second line is the passback, which you also get following any interjections from party NPCs.

To my ear, that reads much less well. And in general, I think it's hard to write passbacks that don't just clutter and slow the flow of dialog (especially since you're having to add them in to dialogs which weren't originally written to facilitate them.)

What's the alternative? Well, as it happens Davaeorn doesn't need a passback at all: there's only one DO command in his original replies - ~Enemy()~ and that can be moved, INTERJECT_COPY_TRANS2 style, to the NPC interjections. Even if that wasn't the case, there's no need for a passback if there are no NPCs present, and there's no need for a passback if the last interjection comes from Yeslick or Faldorn, since - as you can see from the block I pasted above - in those cases Davaeorn has a reply to that NPC already.

But of course coding that gets very cumbersome and quite vulnerable to possible changes made by other mods (say, if someone else added a 'talk Davaeorn down' mod). The solution is to automate it: I'll explain how that works in the next post.

Share this post


Link to post

The function 'compile_with_ict_handling' is intended to address these issues. It has one argument, 'dialog', which should be the full name and path of a dialog file containing I_C_T3 (and I_C_T4) blocks.

When it runs, it goes through each ICT block and modifies the dialog to implement the interjections. The passback is only included if it's needed, which is to say: if there are multiple REPLYs to the original block with different code in (or if any block uses StartStore(), which causes trouble if it's moved). And the passback is only used in interjection chains where it's needed (i.e., when the original NPC doesn't get the last word in any case). If a passback is needed but there isn't one already, a blank one is added, i.e., the NPC replies but doesn't say anything. (That's a bit untidy, but the alternative would be breaking the logic of the dialog.) As a bonus, the code created is much tidier, without extra block copies and unused replies.

Once all the I_C_Tx blocks are implemented, any remaining dialog is COMPILEd normally. (There are limitations here: if there are cross-references between I_C_Tx and non-I_C_Tx blocks, the code will fail.)

I've implemented this in a fork of BG1NPC, available here. The function is contained in "lib/lib_interject.tpa"; it also requires some functions in "lib/alter_dlg.tpa". The only change in BG1NPC's code itself (other than turning on the AUTO_EVAL_STRINGS setting) is a replacement, in bg1npc.tp2, of

COMPILE EVALUATE_BUFFER ~bg1npc/Phase2/dlg/X#ICT3.D~ 

with

WITH_TRA "bg1npc\tra\english\x#ict3.tra" "bg1npc\tra\%LANGUAGE%\x#ict3.tra" BEGIN
     LAF compile_with_ict_handling STR_VAR dialog="bg1npc/Phase2/dlg/x#ict3.d" END
END

(I have to give it the dialog file explicitly, since my code doesn't interact with AUTO_TRA.)

Thoughts and feedback very welcome.
 

Share this post


Link to post

This means the passback line would be spared if no other mod added differently triggered transactions, but would play out like it is now if the line would be needed because of the transactions? That sounds very cool. The main reason I am using I_C_T(3) with passback as a standard is because I can't be sure another mod didn't add more transactions to the dialogue state I am interjecting to. With this function, the added passback would just be a savety measure in case that's the case.

For passback line the function just takes the last line with the original NPC talking?

What exactly does this do, what is changed in the dialogue, do you mean the dlg?

17 hours ago, DavidW said:

goes through each ICT block and modifies the dialog to implement the interjections

I do not agree that an empty dialogue box wouldn't disturb the flow of the dialogue, though. For me, an empty box would more look like a bug or "mod added".

All in all this sounds like a really good thing to get rid of at least some of the passback lines. :party:

Share this post


Link to post

Yes, I mean the dlg. And passback line is the last line of the ICT3, provided it’s spoken by the original NPC with no conditions (I’m following bg1npc conventions here).

As for an empty passback: yes, I agree, it’s ugly and breaks the flow. But in the situations where I use it, the alternative is a logic error that can break the game, so I think it’s the least worst option. (It’s only going to come up if the function gets handed an ICT3 that should have had a passback but doesn’t.)

Share this post


Link to post

Ah, I didn't catch you are explicitely talking about unconditioned passback lines (because I obviously didn't look at the example thoroughly enough).

BG1NPC had the problem of combining lines for several NPCs where there is no logixal trigger to account for one of them present. Usually, in a mod it is one NPC and then the passback line bears the trigger of this NPC beging present. Since some of my NPCs like to babble a comment into CHARNAME's ear which the original game character isn't even supposed to hear, lest alone comment on it, would it be possible to include the last line spoken by the original DLG as a passback line in your function, no matter the conditions?

I can provide examples if you want later when I am back at my computer.

Share this post


Link to post

Sure, give me an example.

(The code runs for everything in x#ict3, but of course it's possible that the block it's producing isn't working ideally.)

Share this post


Link to post
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.

Sign in to follow this  

×
×
  • Create New...