Tools:
- soulstruct / esdtool / zeditor
- DS Map Studio
- DarkScript3
This tutorial will use soulstruct talk structure. You can write directly into the editor but I recommend using Notepad++.
Esdtool converts the talk files to text files. After you made your changes you need to convert them back.
Zeditor shows the talk as states (which they actually are).
It has a clear advantage to show which states the current state can transfer to.
On the other hand is copying, pasting and editing states in text format more comfortable.
Introduction
All talk scripts are assigned to a character. Talk scripts can't be assigned to objects.
This means that every bonfire and covenant place without a leader has an invisible character standing there.
You can use DS Map Studio to search a map for the invisible character (hemisphere shape and red mesh structure).
Important to note is that talk scripts are active as soon as you step into a certain radius (about 10m).
This means it's already checking for distance and shows the text e.g. "Talk" as soon as you are close enough.
Find the talk script ID, open soulstructs "talk", click on the ID and copy the text (Ctrl+A then Ctrl+C) to Notepad++.
Example: changing covenant rewards
We are using the sunlight altar as an example to add a reward at a certain rank. The talk script always starts with event 0.
The structure of a state class needs the
""" 0: No description. """
line with the correct number of the state. Otherwise it won't work.
class State_0(State): """ 0: No description. """ def test(self): return State_3
It simply shows that it goes to "State_3" so we keep looking:
class State_3(State): """ 3: No description. """ def previous_states(self): return [State_0, State_1, State_2, State_46] def enter(self): DebugEvent(message='unknow') def test(self): if GetOneLineHelpStatus() == 0 and HasDisableTalkPeriodElapsed() == 1 and IsTalkingToSomeoneElse() == 0 and CheckSelfDeath() == 0 and IsCharacterDisabled() == 0 and IsClientPlayer() == 0 and GetRelativeAngleBetweenPlayerAndSelf() <= 45 and GetDistanceToPlayer() <= 3 and (GetFlagState(853) == 1 or GetStatus(6) + GetStatus(13) + GetStatus(13) + GetStatus(13) + GetStatus(13) + GetStatus(13) > 25): return State_2 if GetOneLineHelpStatus() == 1 and (IsTalkingToSomeoneElse() or CheckSelfDeath() or IsCharacterDisabled() or IsClientPlayer() == 1 or GetRelativeAngleBetweenPlayerAndSelf() > 45 or GetDistanceToPlayer() > 3): return State_1 if GetOneLineHelpStatus() == 1 and IsPlayerTalkingToMe() == 1 and GetRelativeAngleBetweenPlayerAndSelf() <= 45 and GetDistanceToPlayer() <= 3: return State_10
It simply checks if the player is still close enough. If the player is then it goes to State_2 to show the talk text pop-up:
class State_2(State): """ 2: No description. """ def previous_states(self): return [State_3] def enter(self): DisplayOneLineHelp(text_id=10010220) def test(self): return State_3
The function "DisplayOneLineHelp(text_id=10010220)" does exactly as it sounds.
The text ID can be found in text editor -> event and says "Pray at the Altar of Sunlight".
Then it goes back to "State_3" to check again. If the player is not close enough anymore it goes to "State_1" and sets the DisplayOneLineHelp to "-1" which disables the pop-up.
Now the important one is "State_10" where the player actually pressed "A" to talk to the character:
class State_10(State): """ 10: No description. """ def previous_states(self): return [State_3] def enter(self): ForceCloseMenu() SetTalkTime(2.5) ClearTalkActionState() SetFlagState(flag=11015030, state=1) DisplayOneLineHelp(text_id=-1) def test(self): return State_4
Here the scripts cleans everything that might still be running (cause other states might also refer to this state to return to the actual talk menu which is "State_4" (default state for the talk menu):
class State_4(State): """ 4: No description. """ def previous_states(self): return [State_7, State_10] def enter(self): AddTalkListData(menu_index=1, menu_text=15000360, required_flag=715) AddTalkListData(menu_index=3, menu_text=15000260, required_flag=853) AddTalkListData(menu_index=2, menu_text=15000200, required_flag=-1) AddTalkListData(menu_index=5, menu_text=15000350, required_flag=286) ShowShopMessage(0, 0, 0) AddTalkListData(menu_index=4, menu_text=15000005, required_flag=-1) SetFlagState(flag=71100095, state=1) def test(self): if GetTalkListEntryResult() == 0: return State_11 if GetTalkListEntryResult() == 4: return State_11 if IsTalkingToSomeoneElse() or CheckSelfDeath() or IsCharacterDisabled() or IsClientPlayer() == 1 or GetRelativeAngleBetweenPlayerAndSelf() > 120 or GetDistanceToPlayer() > 5: return State_9 if IsAttackedBySomeone() == 1 or CheckSelfDeath() == 1: return State_9 if GetTalkListEntryResult() == 2: return State_21 if GetTalkListEntryResult() == 3: return State_29 if GetTalkListEntryResult() == 1: return State_35 if GetTalkListEntryResult() == 5: return State_36 def exit(self): ClearTalkListData()
This is the talk menu where we see a list of talk options. As you might have noticed the states contain "def previous_states(self):" which are simply the previous states.
Then "def enter(self):" is everything that gets executed when this state is entered. "def test(self):" checks for the player inputs so it can go to the next state.
"def exit(self):" is everything that gets executed when this state is left.
Now the function "AddTalkListData" is pretty obvious. The "menu_index" is the talk list result the player chooses that gets checked. The text gets written into the list in the order of the "AddTalkListData" functions.
The "menu_text" can be found in the text editor -> event. The "required_flag" can only be a true/on flag. It is not possible to check if a flag is off.
If you need to implements this then you need to use the emevd common event script, create a new event that sets a custom flag on when a specific flag is off.
Then you can check use your custom flag there.
"GetTalkListEntryResult() == 0:" always equals the "B" button press.
"GetTalkListEntryResult() == 4:" always equals the "Leave" option.
"GetTalkListEntryResult() == 1:" has text "Offer soul of Great Lord" which is the option to trade Gwyns Soul for the Sunlight Spear.
"GetTalkListEntryResult() == 2:" has text "Enter Covenant".
"GetTalkListEntryResult() == 5:" has text "Learn gesture"
"GetTalkListEntryResult() == 3:" has text "Offer <?gdsparam@375?>" which is the placeholder for the goods parameter ID 375 which is the "Sunlight Medal".
The rank-up gets probably granted after we offer the item so we follow result 3 which leads to "State_29":
class State_29(State): """ 29: No description. """ def previous_states(self): return [State_4] def test(self): if ComparePlayerStatus(17, 0, 100) == 1: return State_32 if IsEquipmentIDObtained(3, 375) == 0: return State_26 else: return State_25
This event checks if the player is already level 100 in the covenant with the "ComparePlayerStatus(17, 0, 100) == 1:" function (so any offering gets denied).
"IsEquipmentIDObtained(3, 375) == 0:" checks if the category "3" which is the goods category and the ID "375" which is the "Sunlight Medal" is not in the players inventory.
In every other case it goes to "State_25":
class State_25(State): """ 25: No description. """ def previous_states(self): return [State_29] def enter(self): OpenGenericDialog(unk1=8, text_id=10020200, unk2=3, unk3=4, display_distance=2) def test(self): if IsTalkingToSomeoneElse() or CheckSelfDeath() or IsCharacterDisabled() or IsClientPlayer() == 1 or GetRelativeAngleBetweenPlayerAndSelf() > 120 or GetDistanceToPlayer() > 5: return State_22 if GetGenericDialogButtonResult() == 0 and IsGenericDialogOpen() == 0: return State_24 if GetGenericDialogButtonResult() == 2 and IsGenericDialogOpen() == 0: return State_24 if GetGenericDialogButtonResult() == 1 and IsGenericDialogOpen() == 0: return State_23
It asks the player if he wants to offer the item. "GetGenericDialogButtonResult() == 1" is the confirmation so we look at "State_23":
class State_23(State): """ 23: No description. """ def previous_states(self): return [State_25] def enter(self): DebugEvent(message='ๆงใใ') ChangePlayerStats(unk1=15, unk2=0, unk3=1) PlayerEquipmentQuantityChange(3, 375, -1) SetFlagState(flag=844, state=1) def test(self): if ComparePlayerStatus(17, 0, 80) == 1 and GetFlagState(844) == 0: return State_43 if ComparePlayerStatus(17, 0, 30) == 1 and GetFlagState(844) == 0: return State_43 if ComparePlayerStatus(17, 0, 10) == 1 and GetFlagState(11010595) == 0 and GetFlagState(844) == 0: return State_31 if ComparePlayerStatus(17, 0, 10) == 1 and GetFlagState(844) == 0: return State_43 if GetFlagState(844) == 0: return State_42 if IsTalkingToSomeoneElse() or CheckSelfDeath() or IsCharacterDisabled() or IsClientPlayer() == 1 or GetRelativeAngleBetweenPlayerAndSelf() > 120 or GetDistanceToPlayer() > 5: return State_22
THIS is the important event. It changes the players level for the covenant, takes the offered item out of the players inventory and sets the flag so the player gets the join reward "Lightning Spear".
Now it checks how many levels the player has reached in the "def test(self):" section. As you hopefully know there is only one covenant reward at rank 1 (level 10).
Now the other ranks all lead to "State_43" which simply gives the player a line of text which says he gained a rank.
To implement our own reward we first look up the max state number in the talk which is 47. Now we change these lines in the "State_23":
if ComparePlayerStatus(17, 0, 30) == 1 and GetFlagState(844) == 0: return State_43
to the next free state number (if we never got the reward it does to our state, if not we simply get the rank up message):
if ComparePlayerStatus(17, 0, 30) == 1 and GetFlagState(11010542) == 0 and GetFlagState(844) == 0: return State_48 if ComparePlayerStatus(17, 0, 30) == 1 and GetFlagState(844) == 0: return State_43
and write our own State class at the end of the file which could look like this:
class State_48(State): """ 48: custom covenant rank """ def previous_states(self): return [State_23] def enter(self): SetFlagState(flag=11010542, state=1) def test(self): if GetDistanceToPlayer() >= 5: return State_11 if IsMenuOpen(63) == 0: return State_43
We simply set a custom flag that we will use in the emevd common event script to give the item to the player. This is done with all rewards.
The state tests if the player has moved too far away and returns to the talk menu or
checks if the item reward pop-up was confirmed by the player already and then continues to "State_43" as normally, gives the player the rank gained message and returns to the talk menu.
Give the player the item reward
Use DarkScript3 and open the "events\common.emevd(.dcx)" file.
To find the events responsible for rewarding the player with a covenant item we could simply search the "State_31" which is the rank 1 reward and we find SetFlagState(flag=11010595, state=1).
Now we search for the ID "11010595" and find these 2 lines that hold this flag:
InitializeEvent(14, 911, 11010595, 1130, 1);
Explanation:
"14" = an ongoing instance number of the event, set it to the next free available number from the same event
"911" = the event
"11010595" = the ID of the flag
"1130" = the param "item lots" entry ID that refers to the item (that you get when reaching the covenant rank)
"1" = is used in the event to set the flag to true
This event rewards the player with the item if the flag isn't already on to avoid double reward within the same game cycle.
InitializeEvent(1, 8200, 3, 5510, 50000130, 11010595);
Explanation:
"1" = an ongoing instance number of the event, set it to the next free available number from the same event
"8200" = the event
"3" = defines the kind of item. "0" = "weapons", "1" = "armor", "2" = "Rings", "3" = "Goods".
"5510" = the spell ID, in this case the lightning spear
"50000130" = the "item flag" of the param "item lots" entry
"11010595" = the ID of the flag
This event blocks the player from getting the reward in a new game cycle if he already owns the item somewhere.
You need to create a new item lot parameter. Now we can write our own 2 events with a custom event flag from above e.g. "11010542" (search if it's not taken in any event script):
InitializeEvent(51, 911, 11010542, XXXX, 1); InitializeEvent(28, 8200, 3, 5510, YYYYYYYYY, 11010542);
XXXX = enter your item lot param ID that you created
YYYYYYYYY = the item lots item flag ID
and enter them into the script after the their last event instance.