advanced ESD tutorial: implementing a full warp system
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++.
Introduction:
The Warp menu is hardcoded in Dark Souls 1. This means you have to be creative to implement one.
In talk ESD we simply build the text menu and set flags. In EMEVD we then create events to wait for the flags.
Analyzing:
Since bonfires are just invisible players you talk to you need to use "DS Map Studio" to find them and their talk ID
then open their talk script. As mentioned in the first ESD tutorial the State 4 holds the text menu when talking.
So we look go to "Text Editor" -> "Events" and search for "Warp" and copy the ID.
Then we find the correct option in the State 4:
AddTalkListData(menu_index=7, menu_text=15000150, required_flag=710)
Flag "710" is set when the player has the lordvessel in their inventory.
These are the results from choosing the option 7:
if GetTalkListEntryResult() == 7 and GetFlagState(706) == 1: return State_16 if GetTalkListEntryResult() == 7 and GetFlagState(706) == 0: return State_52
Editing talk ESD:
The "State_16" leads to "WarpMenuInit()" and the "State_52" simply tells the player that warping isn't possible (e.g. in Seaths prison tower or in the Painted World).
The flag "706" is set to 0 when the player is not allowed to warp and is handled in event 706.
We don't touch original stuff if we don't have to. Simply edit it to lead to the next free State number:
if GetTalkListEntryResult() == 7 and GetFlagState(706) == 1: return State_59 if GetTalkListEntryResult() == 7 and GetFlagState(706) == 0: return State_52
Before we proceed we need to think about something:
IF we build our own custom menu we need to handle the full logic.
This means that we can't just enable every bonfire if we weren't even there.
So how do we make sure the bonfire is only available when we sat down?
We simply set event flags in the "State_4":
Original:
... def enter(self): ShowShopMessage(0, 0, 0) DebugEvent(message='篝火リスト') RequestSave(0) AddTalkListData(menu_index=9, menu_text=15000005, required_flag=-1) ...
with our flags:
... def enter(self): ShowShopMessage(0, 0, 0) DebugEvent(message='篝火リスト') RequestSave(0) SetFlagState(flag=10000601, state=1) SetFlagState(flag=10000611, state=1) AddTalkListData(menu_index=9, menu_text=15000005, required_flag=-1) ...
"10000601" is the flag for the region/map e.g. Firelink Shrine. We only want the region to be available if we activated atleast one bonfire.
"10000611" is the flag for the first bonfire in the region, "10000621" would be the second one and so on…
You will need to set these flags for ALL bonfires differently.
Now we simply add our warp menu events at the end of all events:
class State_54(State): """ 54: custom warp menu """ def previous_states(self): return [State_59] def enter(self): ShowShopMessage(0, 0, 0) AddTalkListData(menu_index=1, menu_text=10000600, required_flag=10000601) AddTalkListData(menu_index=2, menu_text=10000700, required_flag=10000701) def test(self): if CompareBonfireState(0) == 1 or IsPlayerDead() == 1: return State_6 if HasPlayerBeenAttacked() == 1: return State_6 if IsTalkingToSomeoneElse() or CheckSelfDeath() or IsCharacterDisabled() or IsClientPlayer() == 1 or GetRelativeAngleBetweenPlayerAndSelf() > 120 or GetDistanceToPlayer() > 8: return State_6 if GetTalkListEntryResult() == 0: return State_55 if GetTalkListEntryResult() == 1: return State_69 if GetTalkListEntryResult() == 2: return State_79 class State_55(State): """ 55: clear and go to text menu """ def previous_states(self): return [State_54] def enter(self): ClearTalkListData() def test(self): return State_4 class State_59(State): """ 59: clear and go to warp menu """ def previous_states(self): return [State_4, State_60, State_70] def enter(self): ClearTalkListData() def test(self): return State_54
"State_54" is our warp menu. We give it talk list data with custom text IDs (DS Map Studio -> "Text Editor" -> "Events")
and set our required_flags so it only shows the option if one bonfire from the map was activated.
The first one is for e.g. firelink shrine, the second one for the Depths and you can add as many as you want.
Now wait. We set "State_59" in "State_4". What exactly is all this for?
"State_59" is simply a "cleaning the text menu" state. The talk list doesn't clear correctly if we try to do it in one event.
We need to go to one event that does "ClearTalkListData()" and then returns to our actual menu "State_54".
The same happens if we want to go back to "State_4" so we use:
if GetTalkListEntryResult() == 0: return State_55
to first go to our "State_55" that cleans the text and then returns to "State_4" (the default text menu) again.
So we have now created our own map selection menu. Now we need to add menus within them to show all activated bonfires.
This is the bonfire menu for firelink shrine (again we first go to "State_69" to clean menu text and then to "State_60"):
class State_60(State): """ 60: firelink bonfires """ def previous_states(self): return [State_69] def enter(self): ShowShopMessage(0, 0, 0) AddTalkListData(menu_index=1, menu_text=10000610, required_flag=10000611) AddTalkListData(menu_index=2, menu_text=10000620, required_flag=10000621) def test(self): if CompareBonfireState(0) == 1 or IsPlayerDead() == 1: return State_6 if HasPlayerBeenAttacked() == 1: return State_6 if IsTalkingToSomeoneElse() or CheckSelfDeath() or IsCharacterDisabled() or IsClientPlayer() == 1 or GetRelativeAngleBetweenPlayerAndSelf() > 180 or GetDistanceToPlayer() > 8: return State_6 if GetTalkListEntryResult() == 0: return State_59 if GetTalkListEntryResult() == 1: return State_61 if GetTalkListEntryResult() == 2: return State_62
As you can see we again added talk list data this time with the bonfire names and their event flag IDs.
Now this is the state when you select the firelink shrine bonfire option:
class State_61(State): """ 61: firelink shrine bonfire """ def previous_states(self): return [State_60] def enter(self): ForceEndTalk(unk1=0) ClearTalkProgressData() CloseShopMessage() EndBonfireKindleAnimLoop() ClearTalkDisabledState() SetFlagState(flag=10000610, state=1) def test(self): return State_1
We clear and end everything of the talk and then set a flag. As you can see the flag simply has 0 instead of 1 at the end.
We will used this flag in EMEVD later to warp our character.
Also we return to "State_1"
Now this is the state when you select the lordvessel "bonfire" option:
class State_62(State): """ 62: lordvessel bonfire """ def previous_states(self): return [State_60] def enter(self): ForceEndTalk(unk1=0) ClearTalkProgressData() CloseShopMessage() EndBonfireKindleAnimLoop() ClearTalkDisabledState() SetFlagState(flag=10000620, state=1) def test(self): return State_1
This is simply our menu cleaner event when we go to the bonfire menu:
class State_69(State): """ 69: clear and go to warp menu """ def previous_states(self): return [State_54] def enter(self): ClearTalkListData() def test(self): return State_60
Now we do the same for a different region e.g. the Depths:
class State_70(State): """ 70: depths bonfires """ def previous_states(self): return [State_79] def enter(self): ShowShopMessage(0, 0, 0) AddTalkListData(menu_index=1, menu_text=10000710, required_flag=10000711) AddTalkListData(menu_index=2, menu_text=10000720, required_flag=10000721) def test(self): if CompareBonfireState(0) == 1 or IsPlayerDead() == 1: return State_6 if HasPlayerBeenAttacked() == 1: return State_6 if IsTalkingToSomeoneElse() or CheckSelfDeath() or IsCharacterDisabled() or IsClientPlayer() == 1 or GetRelativeAngleBetweenPlayerAndSelf() > 180 or GetDistanceToPlayer() > 8: return State_6 if GetTalkListEntryResult() == 0: return State_59 if GetTalkListEntryResult() == 1: return State_71 if GetTalkListEntryResult() == 2: return State_72 class State_71(State): """ 71: Canalisation bonfire """ def previous_states(self): return [State_70] def enter(self): ForceEndTalk(unk1=0) ClearTalkProgressData() CloseShopMessage() EndBonfireKindleAnimLoop() ClearTalkDisabledState() SetFlagState(flag=10000710, state=1) def test(self): return State_1 class State_72(State): """ 72: custom gaping dragon bonfire """ def previous_states(self): return [State_70] def enter(self): ForceEndTalk(unk1=0) ClearTalkProgressData() CloseShopMessage() EndBonfireKindleAnimLoop() ClearTalkDisabledState() SetFlagState(flag=10000720, state=1) def test(self): return State_1 class State_79(State): """ 79: clear and go to warp menu """ def previous_states(self): return [State_54] def enter(self): ClearTalkListData() def test(self): return State_70
Again "State_70" is the menu, "State_79" to clean the menu text.
"State_71" is the first bonfire in the depths.
"State_72" is a custom bonfire.
Remember: You need to edit one bonfire then overwrite all bonfire talk scripts (except lordvessel, this one is special) and set the correct event flags in "State_4".
Editing common EMEVD:
Now we open the common emevd and write our own events to warp:
Here are the "InitializeEvent" lines for our 4 bonfires:
InitializeEvent(0, 500, 10000610, 10, 2, 1020980, 1022960); InitializeEvent(1, 500, 10000620, 18, 0, 1800980, 1802960); InitializeEvent(2, 500, 10000710, 10, 0, 1000998, 1002960); InitializeEvent(3, 500, 10000720, 10, 0, 1000997, 1002962);
We need many instances so pay attention when adding new warp location that you also change the instance numbers.
This explains the first line (firelink shrine):
"500" = our event ID
"10000610" = our flag that we set in esd to warp to a bonfire
"10" = the maps main number
"2" = the maps part number
"1020980" = a "player" object ID. You need to create new player objects next to bonfires or use existing ones.
You CAN'T warp to anything else with the "WarpPlayer" function.
"1022960" = a region point that we set as the respawn point
You can look everything up in "DS Map Studio" -> "Map Editor".
On the left side the first 2 numbers of a map are the main number and the next 2 contain the part number.
Then load the map and look for the player objects (pyramid-like hexaeder with green lines)
near the bonfires as well as the region points (octahedron with yellow lines, only look like pyramids cause they are half in the ground).
Here is our actual event that warps the character:
// custom warping $Event(500, Default, function(X0_4, X4_4, X8_4, X12_4, X16_4) { SetEventFlag(X0_4, OFF); WaitFor(EventFlag(X0_4)); ForceAnimationPlayback(10000, 7697, false, false, false); WaitFixedTimeSeconds(1.6); PlaySE(10000, SoundType.sSFX, 777777774); WaitFixedTimeSeconds(0.6); SpawnOneshotSFX(TargetEntityType.Character, 10000, 245, 20147); WaitFixedTimeSeconds(0.4); SpawnOneshotSFX(TargetEntityType.Character, 10000, 245, 20147); WaitFixedTimeSeconds(0.4); SpawnOneshotSFX(TargetEntityType.Character, 10000, 245, 20147); WaitFixedTimeSeconds(0.4); SpawnOneshotSFX(TargetEntityType.Character, 10000, 245, 20147); WaitFixedTimeSeconds(0.4); WarpPlayer(X4_4, X8_4, X12_4); SetPlayerRespawnPoint(X16_4); RestartEvent(); });
We first deactivate the flag so we don't warp randomly for some reason and then wait for the flag to be true.
When the flag is set on/true/1 in the ESD talk then this event continues and warps the player, sets the respawn point,
then restarts and turns the flag off/false/0.