Jump to content

Checking for mod changed triggers


jastey

Recommended Posts

There are several points in the game where the BioWare paladin (Keldorn) or the paladin-equivalent (Mazzy) interject into the PC's decision. If the PC tells Sir Sarles a lie about the quality of the blended material, for example, Keldorn will reveal the truth. If the PC does not free lady Elgea (the captured lady in the bridge disctrict), Mazzy will free her instead.

Writing on a paladin NPC for BGII, I want him to interject at these situations, too. But I don't want him to do all the talking, I only want him to jump in if neither Keldorn nor Mazzy do the interjecting. In the original game, the interjections are triggered by "IsValidForPartyDialog("keldorn")" (or "IsValidForPartyDialog("mazzy")", resp.), so I made an EXTEND_BOTTOM that was triggered by "!IsValidForPartyDialog("keldorn")".

 

This only works as long as the "IsValidForPartyDialog()" triggers aren't replaced by some "InParty() !Dead() !StateCheck(,STATE_SLEEPING)" or whatever. As far as I recall, BGII Tweak pack (?) gives the possibility to change the interjection triggers that way.

 

How would I code a mod NPC interjection, so that the existing trigger would be used?

 

Would I have to

A. Look up which mod changes the triggers and check for this component installed, providing two different .d for this purpose

 

B. use some nifty WeiDU code with READ_SOMETHING that can check for the trigger and put it into a .d befor compilation

?

Link to comment

Not that I am a heavy hitter, but having been fighting with this idea for months my best advice is for independence. Just add the code in your tp2 to add in CD_STATE_NOTVALID instead:

 

/* STATE.IDS patching - thanks, Cam, if you read it */
/* adds custom IsValidForPartyDialogue state */
APPEND ~STATE.IDS~ ~0x80101FEF CD_STATE_NOTVALID~
		 UNLESS ~CD_STATE_NOTVALID~

I am still going back over BG1 NPC stuff, and finding working dialogue that has both old and new versions (IVFPD("dv") vs !SC("dv",CD_S_NV) ), so mix and match is fine. Since you are adding the interjection, you can just strap on the replacement as well :)

 

Then manually knock out the interjection if the other folks are in the party using conditions

~!InParty("keldorn") !InParty("mazzy")~ ~response only if neither are present~

~OR(2) !InParty("keldorn") !InParty("mazzy")~ ~response only if one of the two are not present~

 

This means that no matter what others do to the triggers/conditions,. you get what you want (since no one has come up with a patch to replace CD_S_NV)

Link to comment

It's just me, but I think

 

!InParty(Keldorn) InParty(MyNPC)

 

is good enough for your purposes, and

 

OR(2) !InParty(Keldorn) !InMyArea(Keldorn) InParty(MyNPC) InMyArea(MyNPC)

 

is perfect. InParty() automatically checks he isn't dead.

 

Unfortunately, just InParty() is buggy - both here and in BG2 Tweaks - because it doesn't check whether the NPC is in the same building - if he's not, the dialogue breaks and the player has to initiate it all over again with the NPC present, which is annoying. So I'd use InMyArea() as well whenever possible.

Link to comment

My problem is that "IsValidForPartyDialog" only fires, well, I don't know exactly when, but it is not always Keldorn that does the talking, even if he is in the party. If he is too far away, for example, he would not say anything.

So, if "IsValidForPartyDialog" is the trigger for one and "InParty() !Dead() etc" for the other, it could happen that the PC goes around lying to people with two silent paladins in the party. :) Therefore I have to make it equal.

(If I use "InParty() !Dead() etc" for Ajantis it would be him doing all the interjecting, and I didn't want that.)

 

kulyok: What does "InMyArea(Keldorn)" do exactly?

 

cmorgan: I have to admit I din't get what you suggested. Would you mind explaining it again slooooowly?

Link to comment

InMyArea means exactly it - checks whether an NPC is in the same area as the talking character. For example, if I talk to Phaere and my companions are waiting by the temple of Lloth, their interjections wouldn't trigger, because InMyArea(Companion) would be false.

 

IsValidForPartyDialog is, I think, a combination of See() and !State_Sleeping.

 

I think you'll be fine with InParty(), actually. Again, if you use InParty(), you do not have to use Dead(), dead characters are not chosen automatically.

 

- If you use !InParty("Keldorn") check with BG2Tweaks, if Keldorn is in the party, he will speak, if Keldorn is not in the party, Ajantis will speak.

 

- If you use !InParty("Keldorn") check without BG2Tweaks, if Keldorn is in the party, he will speak if he is valid for party dialogue(IsValidForPartyDialogue("Keldorn")), and if Keldorn is not in the party, Ajantis will speak.

Link to comment

IsValidForParyDialogue is horribly, horribly flaky. It will return false under bizarre circumstances, such as the target being the gabber, or a character standing between the target and speaker, and all sorts of other idiocy.

 

I wish Tweaks could swap them cleanly for an InParty, InMyArea check but swapping two-for-one on triggers will break anything in an OR() trigger.

Link to comment

2 questions:

 

1. Assuming the trigger changing mod component is installed. That would give a dialogue with the following reply options (in the original code, I have a check variable that prevents the Ajantis interjection to happen twice but I left it out here):

 

SAY ~blabla~

IF ~InParty("keldorn") !StateCheck("keldorn",CD_STATE_NOTVALID) ~ THEN EXTERN ~KELDORJ~ xy

IF ~InParty("C#Ajantis") !StateCheck("C#Ajantis",CD_STATE_NOTVALID)

!IsValidForPartyDialog("Keldorn")~ THEN EXTERN ~C#AjanJ~ yz

END

 

What would happen if "IsValidForPartyDialog("Keldorn")" would not be true:

1. Ajantis would talk, because the answers are called from bottom to top.

Or

2. There would be an answer hick-up (maybe a NO VALID LINK OR REPLY) due to two valid actions (because Keldorn would also like to talk, because his trigger is true nonetheless)

?

 

If it's the former, I don't think I will think about this further but just leave it as it is.

 

2. question:

What would be the preferable trigger for an interjection? I am currently using "InParty("C#Ajantis") !StateCheck("C#Ajantis",CD_STATE_NOTVALID)" (learned just recently that !Dead() can be left out in this case *cough*). Maybe I should include "InMyArea()"?

Link to comment
SAY ~blabla~

IF ~InParty("keldorn") !StateCheck("keldorn",CD_STATE_NOTVALID) ~ THEN EXTERN ~KELDORJ~ xy

IF ~InParty("C#Ajantis") !StateCheck("C#Ajantis",CD_STATE_NOTVALID)

!IsValidForPartyDialog("Keldorn")~ THEN EXTERN ~C#AjanJ~ yz

END

 

This is already at risk of 'no valid links' if neither Ajantis or Keldorn are in the party. I'm not sure what we're trying to do here--have Keldorn interject if available, otherwise Ajantis? Use the natural trigger ordering then instead of the cross-checks. Bioware was terrible about stuff like this:

 

IF ~~ THEN BEGIN 0 SAY #0
 IF ~!IfValidForPartyDialogue("Aerie")
  !IfValidForPartyDialogue("Anomen")~ THEN GOTO 1
 IF ~!IfValidForPartyDialogue("Aerie")
  IfValidForPartyDialogue("Anomen")~ THEN EXTERN ANOMENJ 0
 IF ~IfValidForPartyDialogue("Aerie")~ THEN EXTERN AERIEJ 0
END

 

The only way the engine would evaluate transitions 0 and 1 is if Aerie isn't valid and transition 0 would only get evaluated if both Aerie and Anomen are not valid. All of the !IsValid checks here are redundant:

 

IF ~~ THEN BEGIN 0 SAY #0
 IF ~~ THEN GOTO 1
 IF ~IfValidForPartyDialogue("Anomen")~ THEN EXTERN ANOMENJ 0
 IF ~IfValidForPartyDialogue("Aerie")~ THEN EXTERN AERIEJ 0
END

 

2. question:

What would be the preferable trigger for an interjection? I am currently using "InParty("C#Ajantis") !StateCheck("C#Ajantis",CD_STATE_NOTVALID)" (learned just recently that !Dead() can be left out in this case *cough*). Maybe I should include "InMyArea()"?

I'd suggest adding See instead of InMyArea. See is a de facto InMyArea check (can't see characters in other areas) as well as a range check (can't see characters out of the 30' visual range).

Link to comment
This is already at risk of 'no valid links' if neither Ajantis or Keldorn are in the party. I'm not sure what we're trying to do here--have Keldorn interject if available, otherwise Ajantis?

Sorry, my fault. Yes, I meant this and it was a principle question, the example was not meant to look like complete code.

 

Use the natural trigger ordering then instead of the cross-checks.
I don't know what you mean, do you mean I should put the most important interjection at the bottom, and it will be executed because it gets evaluated first? (This was, more or less, my 1st question).
All of the !IsValid checks here are redundant:

 

IF ~~ THEN BEGIN 0 SAY #0
 IF ~~ THEN GOTO 1
 IF ~IfValidForPartyDialogue("Anomen")~ THEN EXTERN ANOMENJ 0
 IF ~IfValidForPartyDialogue("Aerie")~ THEN EXTERN AERIEJ 0
END

If this works the same way as the original, it means the answer to my question would be the 1st, and I will no longer think about this. Thank you!
Link to comment

Let's try this out for discussion purposes:

 

IF ~~ THEN BEGIN AjantisInterjectsIfNoKeldorn
SAY ~This is the original state~
IF ~~ THEN REPLY ~This is the reply if neither Keldorn or Ajantis are around~
IF ~InParty("C#Ajantis") !StateCheck("C#Ajantis",CD_STATE_NOTVALID) See("C#Ajantis") !InParty("keldorn")~ THEN EXTERN ~C#AjanJ~ yz //plays if keldorn is not present but Ajantis is.
IF ~InParty("keldorn") !StateCheck("keldorn",CD_STATE_NOTVALID) See("keldorn")~ THEN EXTERN ~KELDORJ~ xy  //plays if keldorn present
END

 

So, two ways to make this work as an interjection, right?

#1 ICT2 into the state using ~InParty("C#Ajantis") !StateCheck("C#Ajantis",CD_STATE_NOTVALID) See("C#Ajantis") !InParty("keldorn")~ as the trigger condition

#2 false the dialogue state completely and rebuild it using the exact code for everything you want.

 

As far as I can tell, both versions would have 1. a reply without Keldorn or Ajantis around, 2. Keldorn replies if he is in the party, 3. Ajantis replies if Keldorn isn't in the party. The advantage of #1 is that other additions to the dialogue could take place from other mods. The advantage of #2 is you get to repair the flaky code by rebuilding exactly what you want.

 

(note = See() blows away hiding or invisible participants, which is fine, but needs to be evident to future readers :) )

Link to comment

I guess I'm getting confused as to where the start and end points are. If you're concerned that a mod has altered the existing triggers, you can add an extra layer of protection here by False()ing the transitions with ADD_TRANS_TRIGGER. While Tweaks redefines IVFPD as InParty, it doesn't restructure the triggers as radically as you're suggesting. If you use IVFPD, it will behave as an InParty check if Tweaks is installed, regardless of install order, and without having to require two sets of d files. It redefines IVFPD to behave as InParty in the IDS files, not through a textual replacement for just this reason.

 

I don't know what you mean, do you mean I should put the most important interjection at the bottom, and it will be executed because it gets evaluated first? (This was, more or less, my 1st question).

Yes, exactly. Transitions are evaluated in this order:

  • Transitions without replies, bottom to top. If none of these are true, then
  • Transitions with PC-selected replies are presented. If the reply trigger is false, it's not presented. If all reply options evaluated false, then
  • No valid links or replies

There used to be a nice little writeup in the WeiDU readme about transition ordering by JC, but I can't find it anymore. :)

 

So, let's say this is our example state:

 

IF ~~ THEN BEGIN 0 SAY #0
 IF ~~ THEN GOTO 1
 IF ~InParty("Keldorn") See("Keldorn") !StateCheck("Keldorn",CD_STATE_NOTVALID)~ THEN EXTERN KELDORJ 0
END

 

Now we want to add an Ajantis interjection. There are really four scenarios here, and this is more or less in increasing difficulty:

  1. We want Ajantis to have priority in this interjection and then continue the dialogue
  2. We want Ajantis to have the priority interjection, but we also want Keldorn to interject if he's available
  3. We want Keldorn to interject and then the dialogue to continue
  4. We want Keldorn to have the priority interjection, but we also want Ajantis to interject if he's available

Case 1.. This is a simple INTERJECT or EXTEND_BOTTOM:

 

INTERJECT FOO 0 CDAjantisFoo
== AJANTISJ IF ~InParty("Ajantis") See("Ajantis") !StateCheck("Ajantis",CD_STATE_NOTVALID)~ THEN @0
END FOO 1

EXTEND_BOTTOM FOO 0
 IF ~InParty("Ajantis") See("Ajantis") !StateCheck("Ajantis",CD_STATE_NOTVALID)~
EXTERN AJANTISJ 0
END

BEGIN AJANTISJ
IF ~~ THEN BEGIN 0 SAY @0
 IF ~~ THEN EXTERN FOO 1
END

 

Interject has the advantage of creating AJANTISJ 0 whereas EXTEND_BOTTOM requires that you create the AJANTISJ 0 state. However, EXTEND_BOTTOM can add more than one transition and does not create a lot of interjection variables--this would be the end state if you used the INTERJECT above on our sample dialogue:

 

[code]IF ~~ THEN BEGIN 0 SAY #0
 IF ~~ THEN GOTO 1
 IF ~InParty("Keldorn") See("Keldorn") !StateCheck("Keldorn",CD_STATE_NOTVALID)~ THEN EXTERN KELDORJ 0
 IF ~InParty("Ajantis") See("Ajantis") !StateCheck("Ajantis",CD_STATE_NOTVALID) Global("CDAjantisFoo","GLOBAL",0)~ THEN DO ~SetGlobal("CDAjantisFoo","GLOBAL",1)"~ EXTERN AJANTISJ 0
END

Note how it adds the name of the interjection, CDAjantisFoo, as a global variable.

 

Case 2. A casebook INTERJECT_COPY_TRANS, though it can also be done with EXTEND_BOTTOM:

 

INTERJECT_COPY_TRANS FOO 0 CDAjantisFoo
== AJANTISJ IF ~InParty("Ajantis") See("Ajantis") !StateCheck("Ajantis",CD_STATE_NOTVALID)~ THEN @0
END

 

Or alternatively

 

EXTEND_BOTTOM FOO 0
 IF ~InParty("Ajantis") See("Ajantis") !StateCheck("Ajantis",CD_STATE_NOTVALID)~
EXTERN AJANTISJ 0
END

BEGIN AJANTISJ
IF ~~ THEN BEGIN 0 SAY @0
 COPY_TRANS FOO 0
END

 

Case 3. Now we're getting into some fun stuff. You can use the INTERJECT/EXTEND_BOTTOM methods, above, but with additional conditions to ensure that Keldorn isn't valid:

 

INTERJECT FOO 0 CDAjantisFoo
== AJANTISJ IF ~InParty("Ajantis") See("Ajantis") !StateCheck("Ajantis",CD_STATE_NOTVALID) OR(3) !InParty("Keldorn") !See("Keldorn") StateCheck("Keldorn",CD_STATE_NOTVALID)~ THEN @0
END FOO 1

 

Another method would be to EXTEND_BOTTOM both Keldorn and Ajantis interjections in order. You'd end up with two identical Keldorn transitions, but the original one would never be used as it would have identical triggers to the later one (that gets evaluated first). EXTEND_TOP could be used, but it's generally unfriendly to other modders (it re-numbers the existing transitions) and you'd still have to make sure transition 0 didn't fire before your new one.

 

Case 4. Same as above, but you'll need to add an INTERJECT_COPY_TRANS to KELDORJ 0 for Ajantis' comment.

Link to comment
IsValidForParyDialogue is horribly, horribly flaky. It will return false under bizarre circumstances, such as the target being the gabber, or a character standing between the target and speaker, and all sorts of other idiocy.
I've never believed this. It's simply the standard block with a !IsGabber() check, equivalent to
!Name("Imoen",LastTalkedToBy())
InParty("Imoen")
See("Imoen")
!StateCheck("Imoen",1)

I don't think it's quite as fragile as everyone makes it out to be (it can suck, but I've always found it reasonable suckage).

 

The uber state checks people use aren't really necessary for interjections if you don't want them as death and sleep are the only things that should break a transition (silence, stun, et al., don't actually keep a character from interjecting).

 

The only way the engine would evaluate transitions 0 and 1 is if Aerie isn't valid and transition 0 would only get evaluated if both Aerie and Anomen are not valid. All of the !IsValid checks here are redundant:
Note that links are weighted differently than replies (links always have precedence, even when they have no associated triggers). I'd be more specific, but it's been a long time.
Link to comment

CamDawg:

While Tweaks redefines IVFPD as InParty, it doesn't restructure the triggers as radically as you're suggesting. If you use IVFPD, it will behave as an InParty check if Tweaks is installed, regardless of install order, and without having to require two sets of d files. It redefines IVFPD to behave as InParty in the IDS files, not through a textual replacement for just this reason.
I think I got what you are saying, and it seems I totally misunderstood how the IVFPD tweak is performed, I thought it actually replaces the trigger textually in the dlgs. Seems my question was a bit pointless, but I appreciate your detailed answer!

Thank you all. :)

Link to comment
The uber state checks people use aren't really necessary for interjections if you don't want them as death and sleep are the only things that should break a transition (silence, stun, et al., don't actually keep a character from interjecting).

I prefer the state checks for just this reason. It's not a matter of using them to prevent broken links, but also because I think it's nonsensical for a silenced/stunned character to jump into a conversation.

Link to comment

[OT]I definitely request this thread be mined for tutorial data/inclusion - this is the clearest step-by-step evaluation of this set of options I have read. There are some great beginner tutorials, but this is the kind of stuff I find golden for us wanabees who are just *this close* to really understanding what is possible :) [/OT]

 

Regardless of functionality/flakiness concerns, for a modder to be sure that mod-added code will work correctly is to add in and use the extra checks; while IVFPD() may work, and we could all debate about the invisible/silent/etc. interjections, I think that for Jastey's point was that she wanted to build on an option that worked *only* if another NPC did not interject.

 

If I misunderstood, please correct me, but rereading the first post indicates the basic concern as "how do I do this so that anything changing the triggers still allows Ajantis to respond only if others do not?"

 

If so, the simplest option seems to be to use CamDawg's Case 3.

 

(note to self: need another marathon runthrough on BG1 NPC - evaluating code in light of this thread. I need to take up chain-smoking...)

Link to comment

Archived

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

×
×
  • Create New...