How to Script a Mission

From DSP Wiki
Jump to: navigation, search

In this page you will learn how to script a mission from scratch as well as some other useful information.


Getting Started

This guide assumes you already have a basic understanding of writing quests, if not then I highly recommend reading BlueKirby0's guide: "How to Make a Quest". If you're too lazy to read over that then don't worry, i'll be going over most things in his guide (but in a lot less detail).

onTrigger

To keep things simple, let's start off with Windurst Mission 7-1 (The Sixth Ministry) as it covers most of what we need to go through.

After accepting the Mission, we must go to Tosuka-Porika in Windurst Waters for a cutscene, so go ahead open up his script :scripts/zones/Windurst_Waters/Tosuka-Porika.lua.

I recommend using Notepad++ to open scripts as it has syntax highlighting and helps keep track of 'if' statements and their 'end's.

Scroll down to the onTrigger section and we see this:

function onTrigger(player,npc)

-- NOTE: cut out lines that are not needed for this guide to keep things simple

	if (player:getCurrentMission(COP) == THE_ROAD_FORKS and player:getVar("MEMORIES_OF_A_MAIDEN_Status")==10)then
		player:startEvent(0x036B);--COP event

-- NOTE: cut out lines that are not needed for this guide to keep things simple

	elseif(chasingStatus == QUEST_ACCEPTED and player:getVar("CHASING_TALES_TRACK_BOOK") > 0) then
		player:startEvent(0x019c);
	elseif(player:hasKeyItem(149) ==true) then
		player:startEvent(0x019c);
	elseif(chasingStatus == QUEST_ACCEPTED) then
		player:startEvent(0x0196); --  Add follow up cutscene
	else
		player:startEvent(0x0172); -- Standard Conversation
	end
	
end;

Now that we've got his script open, we need to add a check for Windurst Mission 7-1 before starting the cutscene to progress in the mission so let's add that check:

        elseif(chasingStatus == QUEST_ACCEPTED) then
		player:startEvent(0x0196); --  Add follow up cutscene
		----> Windurst Mission 7-1 <----
	elseif(player:getCurrentMission(WINDURST) == THE_SIXTH_MINISTRY and player:getVar("MissionStatus") == 0) then
		-- player:startEvent()
        else
		player:startEvent(0x0172); -- Standard Conversation


The code simplified:

elseif         <--- if the above code isn't true then if...
player:getCurrentMission(WINDURST) == THE_SIXTH_MINISTRY  <--- The player's Current Mission (in Windurst) is THE_SIXTH_MINISTRY 
and player:getVar("MissionStatus") == 0) <--- and "MissionStatus" var is equal to 0 (they haven't progressed in the mission)
then <--- then carry out the block of code til you see 'end' or another 'elseif' statement.

Notice how we not only check the player's current mission, we also check the variable "MissionStatus". This is done so we know that the player has just received the mission and has not made any progress in it which is why the variable "MissionStatus" has a value of 0. The reason we've commented out player:startEvent() is because we don't know the correct EventID yet. Okay we've got the correct logic/checks in there; now what? Now it's time to find the correct cutscene with the correct parameters (e.g. cutscenes that have items/keyitems/numbers/values returned as part of text).


Finding the correct cutscene (with parameters)

Now we must find the correct cutscene for this mission. To do this you can find the mission on ffxiclopedia and scroll down to the spoiler and click [show].

SPOILER WARNING: Details about a quest, mission or other Final Fantasy XI in-game storyline follow. [show]

You then find the Event IDs for that NPC in that particular zone (in our case it's Windurst Waters Event IDs and the NPC is Tosuka-Porika and find the cutscene matching that dialog using the "@cs" command on a GM char e.g.

Note: Cutscenes linked to each other are usually have similar IDs e.g. @cs 830 @cs 831 @cs 832 are most likely related to the same mission/questline

@cs 830

Once you find the correct cutscene you must convert it from decimal to hexadecimal before using it for "player:startEvent( )".

Okay so now we've found the correct cutscene (0x02cb) but it's showing this:

SCRIPT:

player:startEvent(0x02cb);

Script looks fine, lets have a look at how it displays client-sided/in-game.


Client-side cutscene triggered:

Tosuka-Porika: Ah, yes, I mustaru give you this. 
You should be able to enter the Animastery using this ring. 
It is the '      '. Try your bestaru not to lose it.


That doesn't look right!

This is because this cutscene requires a special parameter. In our case the parameter is the value of the keyitem Optistery Ring.

To add this we can either call the keyitems script at the top of the file like so:

require("scripts/globals/settings");
require("scripts/globals/titles");
require("scripts/globals/missions");
require("scripts/globals/quests");
require("scripts/zones/Windurst_Walls/TextIDs");
require("scripts/globals/keyitems"); --<----- require keyitems

After we require the keyitems script we can then add this: player:startEvent(0x02cb,0,OPTISTERY_RING);

Or we can go into the keyitems.lua file, find the id of OPTISTERY_RING and add the following to player:startEvent

player:startEvent(0x02cb,0,250); -- cutscene(0x02cb),idk what this parm does(0),keyitemparam(250) keyitem = OPTISTERY_RING

Alright, time to see if this works!

SCRIPT: player:startEvent(0x02cb,0,250); -- cutscene, param1, keyitemid Looks good; we got the correct cutscene and params. Lets see if it displays correctly in-game (client sided).

Client-side cutscene triggered:

Tosuka-Porika: Ah, yes, I mustaru give you this. 
You should be able to enter the Animastery using this ring. 
It is the Optistery Ring. Try your bestaru not to lose it.

Success! Alright now we've got the cutscene to trigger so we need to tell the script what to do when the cutscene is finished.


onEventFinish

This is where you tell the script what to do when that cutscene is finished and since we haven't told the script to do anything once the cutscene is triggered, we need to scroll down to onEventFinish and tell it what to do.

-----------------------------------
-- onEventFinish
-----------------------------------

function onEventFinish(player,csid,option)
--printf("CSID: %u",csid);
--printf("RESULT: %u",option);
	
	if(csid == 0x0037) then  -- Show Off Hat
		player:setVar("QuestHatInHand_var",player:getVar("QuestHatInHand_var")+32);
		player:setVar("QuestHatInHand_count",player:getVar("QuestHatInHand_count")+1);
	elseif(csid == 0x00a0) then
		player:setVar("MissionStatus",1);
	elseif(csid == 0x00a8) then
		finishMissionTimeline(player,1,csid,option);
	elseif(csid == 0x0183 and option == 0) then -- Early Bird Gets The Bookworm
		player:addQuest(WINDURST,EARLY_BIRD_CATCHES_THE_BOOKWORM);
	elseif(csid == 0x0193 and option == 0) then
	    player:addQuest(WINDURST,CHASING_TALES);
	elseif(csid ==0x036B)then
		player:setVar("MEMORIES_OF_A_MAIDEN_Status",11);
	end
	
end;

Alright so what does this mean?

--printf("CSID: %u",csid);
--printf("RESULT: %u",option);

When not commented out, (two hyphens "--" comments out whatever is after it on the same line) these "printf"s tell the script to show a message on DSGame-Server.exe window showing the CutsceneID of the cutscene that just finished and shows which option the user selected e.g. if the cutscene has a multi-choice option like:

Do you wish to teleport to Windurst Walls?
[Yes]
[No]

When triggering a cutscene with multi-choice options you need to comment out the line --printf("RESULT: %u",option); and when triggering the cutscene (from script, not @cs) it'll show a message in DSGameServer.exe like this:

[Lua Script]RESULT: 0


Ok now that the boring explanation is out of the way we need to tell it what to do once the cutscene is finished. Now since we dont have a cutscene with multi-choice options, what we need to do is add a line like so:

elseif(csid == 0x02cb) then

We then need to tell it what to do upon finishing that cutscene. Alright wiki says to give the keyitem Optistery Ring so lets add that to our code.

elseif(csid == 0x02cb) then
		player:addKeyItem(OPTISTERY_RING);
		player:messageSpecial(KEYITEM_OBTAINED,OPTISTERY_RING);

Looking good.

Time to test out the script so far; on your [GM] char use @addmission 2 17 (2 = Windurst, 17 = THE_SIXTH_MINISTRY) and then talk to Tosuka-Porika.

Client-sided (in-game)

Tosuka-Porika: Ah, yes, I mustaru give you this. 
You should be able to enter the Animastery using this ring. 
It is the Optistery Ring. Try your bestaru not to lose it.


Doesn't look like there are any issues but wait. Lets talk to him again and see what happens...

Tosuka-Porika: Ah, yes, I mustaru give you this. 
You should be able to enter the Animastery using this ring. 
It is the Optistery Ring. Try your bestaru not to lose it.

That shouldn't be happening... Ah wait, we forgot to set the variable for "MissionStatus". Lets add that to our onEventFinish code:

elseif(csid == 0x02cb) then
		player:addKeyItem(OPTISTERY_RING);
		player:messageSpecial(KEYITEM_OBTAINED,OPTISTERY_RING);
		player:setVar("MissionStatus",1);

Alright so now that we've set the variable MissionStatus to 1 using player:setVar("MissionStatus",1); we need to trigger the cutscene before the onEventFinish code is called.

This is what your code should look like so far:

onTrigger

-- NOTE: cut out lines that are not needed for this guide to keep things simple

elseif(chasingStatus == QUEST_ACCEPTED) then
		player:startEvent(0x0196); --  Add folllow up cutscene

		-- Windurst Mission 7-1 --
	elseif(player:getCurrentMission(WINDURST) == THE_SIXTH_MINISTRY and player:getVar("MissionStatus") == 0) then
		player:startEvent(0x02cb,0,OPTISTERY_RING);

	else
		player:startEvent(0x0172); -- Standard Conversation
	end

onEventFinish

-- NOTE: cut out lines that are not needed for this guide to keep things simple
elseif(csid ==0x036B)then
		player:setVar("MEMORIES_OF_A_MAIDEN_Status",11);

-- Windurst Mission 7-1 --
	elseif(csid == 0x02cb) then 
		player:addKeyItem(OPTISTERY_RING);
		player:messageSpecial(KEYITEM_OBTAINED,OPTISTERY_RING);
		player:setVar("MissionStatus",1);

Alright so we've got that working but we're missing the cutscene that reminds the player what they need to do next in the mission/questline. This is usually the next cutscene up from the 'starting cutscene' and adding this check is simple;

-- NOTE: cut out lines that are not needed for this guide to keep things simple

  elseif(chasingStatus == QUEST_ACCEPTED) then
	player:startEvent(0x0196); --  Add folllow up cutscene

		-- Windurst Mission 7-1 --
  elseif(player:getCurrentMission(WINDURST) == THE_SIXTH_MINISTRY and player:getVar("MissionStatus") == 0) then
	player:startEvent(0x02cb,0,OPTISTERY_RING);

  elseif(player:getCurrentMission(WINDURST) == THE_SIXTH_MINISTRY and player:getVar("MissionStatus") == 1) then -- reminder cs
	player:startEvent(0x02cc,0,OPTISTERY_RING);

  else
	player:startEvent(0x0172); -- Standard Conversation
end

Notice how we haven't added anything to onEventFinish. This is because we don't need the script to do anything other than trigger the cutscene to remind the player what to do.

Tracking progress through variables

You made it this far, good.

The Mission Walkthrough (on ffxiclopedia) is telling us (in short):

- Travel to Toraimarai Canal 
- go to (G-8)(Second Map), where there is a large room with 4 Hinge Oils accompanied by a few  bats
- If even one of the oils re-spawn before you click the Marble Door, it will be "locked" until all oils are defeated again.

Now lets navigate to scripts/zones/Toraimarai_Canal/mobs since the script we're modifying is a mob's script.

Save the following code as hinge_oil.lua

[Click here to go to the Hinge Oil Template]


This script is called whenever there's a change in the mob's current action (as far as i know)

Since we need to track how many Hinge Oils the player has killed; we need the variable to increase on each mob's death til it reaches the value of 4 (4 is the number of Hinge Oils in the room and they all need to be killed).

To do this we add the following code to onMobDeath:

function onMobDeath(mob, killer)

local CurrentMission = killer:getCurrentMission(WINDURST);
local WindyKills = killer:getVar("Windurst_7-1Kills");

	if(CurrentMission == THE_SIXTH_MINISTRY and player:getVar("MissionStatus") == 1) then
		if(WindyKills < 4) then
			killer:setVar("Windurst_7-1Kills",WindyKills+1);
		end
	end
end;

Breakdown of the code we just added:

local WindyKills = killer:getVar("Windurst_7-1Kills");

We declare this as a local variable.

This means that the variable is contained only in the script within this function.

This is useful for when you have a long-ass check like killer:getVar("Windurst_7-1Kills") and want to use an alias for that instead; it also helps make the code more readable.


if(CurrentMission == THE_SIXTH_MINISTRY and player:getVar("MissionStatus") == 1) then

The reason we check killers current mission first and the stage they're at is because we don't want every player to get the variable added to their character since they don't need it.


if(WindyKills < 4) then
			killer:setVar("Windurst_7-1Kills",WindyKills+1);
		end

WindyKills was declared above as killer:getVar("Windurst_7-1Kills")

This checks if WindyKills(killer:getVar("Windurst_7-1Kills")) variable is under 4 and adds 1 for each kill you get to "Windurst_7-1Kills" until the value reaches 4.


Notice how it's killer: and not player:. The function tells you whether to use player or killer like so:

-- THIS IS AN EXAMPLE, your script should NOT look like this, it's for demonstration purposes only.
function onMobDeath(mob, killer);
                         --^ killer, not player

function onTrigger(npc, player)
                      --^ player, not killer


Alright now that we've got that code added let's test it by throwing in a print to return the value like so:

if(WindyKills < 4) then
			killer:setVar("Windurst_7-1Kills",WindyKills+1);
			print("Windurst_7-1Kills value is"..WindyKills); -- WindyKills = killer:getVar("Windurst_7-1Kills);
		end

In DSGame-Server.exe console window you'll see [Lua Script]Windurst_7-1Kills value is 1 etc til the value reaches 4.

Once you're done testing to see if the code is working correctly, take out the line with the print in it.

Adding special checks

Lets head up to the Marble Door first in-game and trigger it to see what happens. The door is opening (which is normal for most doors but ours is special) so lets check DSGame-Server.exe. [ ! ](Insert MGS Alert here) DSGameServer is reporting an error!

[Error]luautils::OnTrigger: cannot open scripts/zones/Toraimarai_Canal/npcs/_4pc.lua: No such file or directory

For any other door we'd ignore this error but ours is special. So lets paste the following code as a template

[Click here to go to Marble Door Template]



Lets add a check to the NPC for the player's current mission and check to see if they've killed the 4 Hinge Oils like so:

function onTrigger(player,npc)

local CurrentMission = player:getCurrentMission(WINDURST);
local WindyKills = player:getVar("Windurst_7-1Kills");


	if(CurrentMission == THE_SIXTH_MINISTRY and WindyKills == 4) then
        end
end;

We did not need to include MissionStatus as that check has already been checked by the mobs onMobDeath before counting the kill. The other reason for this is because we don't want the door to remain locked after the player passes through and triggers the cutscene by talking to the Tome of Magic

Alright we've got the check for the Mission and the check to see if all 4 Hinge Oils have been defeated by the player but the door doesn't open. For the door to open we need to use GetNPCByID():openDoor() (but we first need the NPC's ID).

GetNPCByID

[Click here to see how to get the NPC ID]

Now that we've got the correct ID, lets add this into the script.

function onTrigger(player,npc)

local CurrentMission = player:getCurrentMission(WINDURST);
local WindyKills = player:getVar("Windurst_7-1Kills");


	if(CurrentMission == THE_SIXTH_MINISTRY and WindyKills == 4) then
           GetNPCByID(17469795):openDoor(2.5);
                  -- (NPCid)   :openDoor(2.5) seconds
        end
end;

Doesn't seem like there's any issues but wait! Remember we not only have to check to see if all the Hinge Oils have been killed by the player, but we also need to make sure all the mobs are de-spawned before the player can enter through the door.


Mob Action

Here's how we check if the mob is de-spawned. GetMobAction(mobId) [Click here to find out how to obtain the mobId] otherwise continue reading.

Add the following to _4pc.lua (Marble Door) once finding out the Mob Action:

Note: [Click here for info on obtaining Mob Action] otherwise continue reading.

function onTrigger(player,npc)
local CurrentMission = player:getCurrentMission(WINDURST);
local MissionStatus = player:getVar("MissionStatus");
local WindyKills = player:getVar("Windurst_7-1Kills");

-- NOTE: MobAction is 25(ACTION_SPAWN) when they're dead/despawned and 16(ACTION_ROAMING) when spawned. 
--		 Not really sure why but this seems to work.
--		 print("HingeOil 1 Action: "..GetMobAction(17469666));  -- etc

	if(CurrentMission == THE_SIXTH_MINISTRY and WindyKills == 4) then
		if((GetMobAction(17469666) == 25) and
			(GetMobAction(17469667) == 25) and
			(GetMobAction(17469668) == 25) and
			(GetMobAction(17469669) == 25)) then
			
			GetNPCByID(17469795):openDoor(2.5);
		else
			player:messageSpecial(3); -- It's sealed shut with incredibly strong magic
		end
	
	end

end;

This checks if the player's Current Mission is THE SIXTH MINISTRY and they've got the kills required AND the following mobs are de-spawned/dead then open the door.

Finishing off the Mission

We're almost done! Now we just need to talk to the Tome of Magic and finish off the Mission when returning to Tosuka-Porika. Upon 'talking' to the Tome(s) of Magic, we discover it doesn't have a script so go ahead and [copy the template] and save the file as Tome_of_Magic.lua.

Note: All the Tomes of Magic have got the same name in npc_list.sql... For this to work correctly you need to find the correct NPCs ID. You then add the following code and talk to that NPC.

Ok now lets open up the script for Tome_of_Magic, add the checks for the mission, check which Tome it is and what stage the player is at like so:

function onTrigger(player,npc)
local CurrentMission = player:getCurrentMission(WINDURST);
local WindyKills = player:getVar("Windurst_7-1Kills");
local MissionStatus = player:getVar("MissionStatus");
local npcId = npc:getID(); -- This is how we get the NPCs ID.

print("This Tome's ID is "..npcId);

	if(CurrentMission == THE_SIXTH_MINISTRY and WindyKills == 4 and MissionStatus == 1) then

		local cutscene = math.random(0x041,0x044); -- Selects a cutscene (from 0x041 to 0x044)

			player:startEvent(cutscene); -- plays the cutscene chosen (changes each time)

	end
	
end;

Now lets talk to the correct Tome (on the floor to the left of the Marble Door you entered through) and find it's ID. Check DSGameServer and see what this NPCs ID is.

Once you got the correct ID; add the following code to check if we're talking to the correct Tome:

function onTrigger(player,npc)

local CurrentMission = player:getCurrentMission(WINDURST);
local WindyKills = player:getVar("Windurst_7-1Kills");
local MissionStatus = player:getVar("MissionStatus");
local npcId = npc:getID(); -- Get the NPCs ID

	if(npcId == 17469825) then -- If it's this NPC then check the players mission and 
		if(CurrentMission == THE_SIXTH_MINISTRY and WindyKills == 4 and MissionStatus == 1) then
			player:startEvent(0x0045);
		end
				
	else
		local cs = math.random(0x041,0x044);
		player:startEvent(cs);
		
	end
end;

Now remember, these are the other Tomes that just give information but aren't needed for the mission. Since all the un-needed 'fake' Tomes have the same names (in-game and in npc_list) I decided to be lazy and have them play either cutscene 0x041, 0x042, 0x043 or 0x044(these cutscenes are general info cutscenes). This was so I didn't need to add extra scripts by renaming the NPCs. (I don't recommend doing this in any other case but it doesn't matter for these).


The player now has two options to get out of this room; use the Transporter or exit through the door they entered.

Since i'm feeling generous, you can have the [Transporter script].

Okay done that, now what?

After the cutscene, you can exit through the Marble Door on the South end of the room. 

This will take you back to Heavens Tower in Windurst.

Transporter added, player can exit through that as well as the Marble Door they entered through; next!

Head back to the Optistery and speak with Tosuka-Porika to finish the mission and to receive the key item Blank Book of the Gods.

Seems easy enough, lets get to it!

Final Stage and Missions.lua

Thought it'd be as simple as adding the checks and cutscenes to the NPC? Well you're in for a surprise; it's not!

This is how it's done:

Add the code that checks the player's progress (if it's sufficient to finish the mission): scripts/zones/Windurst_Waters/npc/Tosuka-Porika.lua:

function onTrigger(player,npc)

-- cut out lines not relevant to our mission

        if (player:getCurrentMission(COP) == THE_ROAD_FORKS and player:getVar("MEMORIES_OF_A_MAIDEN_Status")==10)then
		player:startEvent(0x036B);--COP event

-- cut out lines not relevant to our mission
         -- WINDURST MISSION 7-1 --
        elseif(player:getCurrentMission(WINDURST) == THE_SIXTH_MINISTRY and player:getVar("MissionStatus") == 0) then
		player:startEvent(0x02cb,0,OPTISTERY_RING);

	elseif(player:getCurrentMission(WINDURST) == THE_SIXTH_MINISTRY and player:getVar("MissionStatus") == 1) then
		player:startEvent(0x02cc,0,OPTISTERY_RING);

	elseif(player:getCurrentMission(WINDURST) == THE_SIXTH_MINISTRY and player:getVar("MissionStatus") == 2) then
		player:startEvent(0x02d4);
	else
		player:startEvent(0x0172); -- Standard Conversation
        end
end

(Remember we set MissionStatus to 2 upon triggering the correct cutscene with the Tome of Magic so we set the script above to check if that's happened)


onEventFinish:

function onEventFinish(player,csid,option)
--printf("CSID: %u",csid);
--printf("RESULT: %u",option);
	
	if(csid == 0x0037) then  -- Show Off Hat
		player:setVar("QuestHatInHand_var",player:getVar("QuestHatInHand_var")+32);
		player:setVar("QuestHatInHand_count",player:getVar("QuestHatInHand_count")+1);

-- cut out lines not relevant to our mission

	elseif(csid == 0x02cb) then
		player:addKeyItem(OPTISTERY_RING);
		player:messageSpecial(KEYITEM_OBTAINED,OPTISTERY_RING);
		player:setVar("MissionStatus",1);

	elseif(csid == 0x02d4) then
		finishMissionTimeline(player,4,csid,option); -- See.. Not as simple as you first thought!
                player:setVar("Windurst_7-1Kills",0) -- Delete this variable, we don't need it anymore.
	end
	
end;

Now what does this mean?

Read on and find out!

Missions.lua

Navigate to scripts/globals/ and open missions.lua. Scroll down to around Line 790 or Ctrl+F for finishMissionTimeline. You'll see something like this:

function finishMissionTimeline(player,guard,csid,option)

	nation = player:getNation();

	-- To prevent the cs conflict, use the 1st and 2nd for guard and 3/4 for npc
	-- missionid, {Guard1CS,option}, {Guard2CS,option}, {NPC1 CS,option}, {NPC2 CS,option}, {{function,value},...},
	--  1: player:addMission(nation,mission);

For now lets ignore this part and skip to Line 888-ish (elseif(nation == WINDURST))

You'll see something like this:

timeline = {

-- NOTE: CUT OUT THE LINES NOT RELEVANT TO OUR MISSION

	
 15,{0x00D8,0},{0,0}, {0,0}, {0,0}, {{11,6},{14,0},{9,74},{8,20000},{6},{12}},  -- MISSION 5-2 (Finish (Star Sibyl))


 16,{0,0}, {0,0}, {0x0032,0},{0,0}, {{14,0},{5,650},{0,0},{0,0},{0,0},{12}}, -- MISSION 6-1 (Finish (Zone: Full Moon Fountain))


 17,{0,0}, {0,0}, {0x0138,0},{0,0}, {{14,0},{11,7},{8,40000},{6},{0,0},{12}}	-- MISSION 6-2 (Finish (Star Sibyl))

           }

Now you may be thinking: "This is confusing as fuck." but don't worry, it's not.

Here's what it means:

          17,       {0,0},       {0,0},   {0x0138,0},   {0,0}, {{14,0},{11,7},{8,40000},{6},{0,0},{12}}
--        ^^         ^^^^        ^^^^            ^         ^   
--missionid^|guard1csid^| guard2csid^| guard3csid^|NPC1csid^ | ^        function list                ^|

That probably didn't make a lot of sense but lets just add the code to finish the mission and you'll understand how this works (hopefully)...

Scroll to the code for Windurst Mission 6-2 and add a comma after the {12}} like so:

17,{0,0},{0,0},{0x0138,0},{0,0},{{14,0},{11,7},{8,40000},{6},{0,0},{12}},	-- MISSION 6-2 (Finish (Star Sibyl))

This tells the script "We've got an addition to the array, keep looking!".

Now add a new line (keeping the previous line intact) with the following code:

18,{0,0},{0,0},{0,0},{0,0},{{0,0},{0,0},{0,0},{0},{0,0},{0}}

So far your code should look like this:

17,{0,0},{0,0},{0x0138,0},{0,0},{{14,0},{11,7},{8,40000},{6},{0,0},{12}},	-- MISSION 6-2 (Finish (Star Sibyl))
18,{0,0},{0,0},{0,0},{0,0},{{0,0},{0,0},{0,0},{0},{0,0},{0}}			-- MISSION 7-1 (Finish (Tosuka-Porika))

Notice how there's no comma after the last set of }'s for the line we just added. This is because we've now reached the end of the array so we show that by leaving the comma out.


Now here's what we do to the line we've just added. First we need to add the NPC's mission finishing csid (0x02d4 for Tosuka-Porika in our case) like so:

18,{0,0},{0,0},{0,0},{0x02d4,0},{{0,0},{0,0},{0,0},{0},{0,0},{0}}		-- MISSION 7-1 (Finish (Tosuka-Porika))

(changed the 4th pair of {}'s to the npcs csid) Notice how the NPCs csid is the 4th param after the Mission ID. This is where we get the 4 from in finishMissionTimeline(player,4,csid,option) in Tosuka-Porika's script.

Next we tell the 'function list' (pair of {}'s after npc's csid) what to do;

IMPORTANT:[Here's the full list of functions for 'function list'] e.g. how you'd setRank or addGil etc.

First we need to set the variable "MissionStatus" to 0 by changing the first pair of {}'s to {14,0} inside the function list like so:

18,{0,0},{0,0},{0,0},{0x02d4,0},{{14,0},{0,0},{0,0},{0},{0,0},{0}}		-- MISSION 7-1 (Finish (Tosuka-Porika))

Next we need to add the Rank Points reward (5,RankPointsValue) like so: Note: The Rank Points awarded is +50 from whatever the previous mission (that awarded rank points) gave out (from Mission 2 onwards)

18,{0,0},{0,0},{0,0},{0x02d4,0},{{14,0},{5,700},{0,0},{0},{0,0},{0}}		-- MISSION 7-1 (Finish (Tosuka-Porika))

After that we need to award the keyitem/item/gil reward for completing the mission; Book of Gods (KeyItem 251) like so:

18,{0,0},{0,0},{0,0},{0x02d4,0},{{14,0},{5,700},{10,251},{0},{0,0},{0}}		-- MISSION 7-1 (Finish (Tosuka-Porika))

Lastly we'll add the mission completing function like so:

18,{0,0},{0,0},{0,0},{0x02d4,0},{{14,0},{5,700},{10,251},{0},{0,0},{12}}	-- MISSION 7-1 (Finish (Tosuka-Porika))

Note: It does NOT matter what order you carry out the functions so long as they're contained within the function list. Remember to clear un-needed player variables by using player:setVar("VariableName",0) on the final NPC/stage of the mission.

Now test your mission to see if everything is working correctly and...

We're done. Congratulations on scripting your first mission! If you have any issues with adding missions or this guide please contact 'demolish' on our IRC channel.

Function List

-- To be used within the function list {{0,0},{0,0}, etc.. }
        --  1: player:addMission(nation,mission);
	--  2: player:messageSpecial(YOU_ACCEPT_THE_MISSION);
	--  3: player:setVar(variablename,value);
	--  4: player:tradeComplete();
	--  5: player:addRankPoints(number);
	--  6: player:setRankPoints(0);
	--  7: player:addPoint(player:getNation(),number); player:messageSpecial(YOUVE_EARNED_CONQUEST_POINTS);
	--  8: player:addGil(GIL_RATE*number); player:messageSpecial(GIL_OBTAINED,GIL_RATE*number);
	--  9: player:delKeyItem(number);
	-- 10: player:addKeyItem(number); player:messageSpecial(KEYITEM_OBTAINED,number);
	-- 11: player:setRank(number);
	-- 12: player:completeMission(nation,mission);
	-- 13: player:addTitle(number);
	-- 14: player:setVar("MissionStatus",value);

Other info and Templates

Marble Door Template

For a little more information, read the block of text just under this template while you're here or [Click here to return to the step you were on].

-----------------------------------
-- Area: Toraimarai Canal
-- NPC:  Marble Door
-- Involved In Windurst Mission 7-1
-- @zone 169
-- @pos 132 12 -19 169
-----------------------------------
package.loaded["scripts/zones/Toraimarai_Canal/TextIDs"] = nil;
require("scripts/zones/Toraimarai_Canal/TextIDs");
-----------------------------------

require("scripts/globals/settings");
require("scripts/globals/keyitems");
require("scripts/globals/quests");
require("scripts/globals/missions");


-----------------------------------
-- onTrade Action
-----------------------------------

function onTrade(player,npc,trade)

end; 

-----------------------------------
-- onTrigger Action
-----------------------------------

function onTrigger(player,npc)

end;

-----------------------------------
-- onEventUpdate
-----------------------------------

function onEventUpdate(player,csid,option)
--printf("CSID2: %u",csid);
--printf("RESULT2: %u",option);

end;

-----------------------------------
-- onEventFinish
-----------------------------------

function onEventFinish(player,csid,option)
--printf("CSID: %u",csid);
--printf("RESULT: %u",option);

end;

Notice how at the top of the file there is the NPCs name, what it's relevant to, @zone and @pos co-ordinates. It is very important to include the @pos as it helps test this NPC before the script gets committed and also for anyone else who needs to modify it later. You can find the @pos of a mob/npc/area by standing in the spot you want to find the co-ordinates of and using the GM command: @where. The command tells you your character's [X,Y,Z,(rotation)] pos (you dont need to include the rotation). The last number (after Z pos) is the zoneid.

[Click here to return to the step you were on]

Hinge Oil Template

[Click here to return to the step you were on]

-----------------------------------
-- Area: Toraimarai Canal
-- Mob: Hinge Oil
-----------------------------------

require("scripts/globals/titles");
require("scripts/globals/status");
require("scripts/globals/missions");

-----------------------------------
-- OnMobInitialise Action
-----------------------------------

function onMobInitialize(mob)
end;

-----------------------------------
-- onMobFight Action
-----------------------------------

function onMobFight(mob,target)
end;

-----------------------------------
-- onMobDeath
-----------------------------------

function onMobDeath(mob, killer)

end;

[Click here to return to the step you were on]

Tome of Magic Template

[Click here to return to the step you were on]

-----------------------------------
-- Area: Toraimarai Canal
-- NPC:  Tome of Magic ( Optional CS )
-- Involved In Windurst Mission 7-1
-- @zone 169
-- @pos 140 13 -22 169
-----------------------------------
package.loaded["scripts/zones/Toraimarai_Canal/TextIDs"] = nil;
require("scripts/zones/Toraimarai_Canal/TextIDs");
-----------------------------------

require("scripts/globals/settings");
require("scripts/globals/keyitems");
require("scripts/globals/quests");
require("scripts/globals/missions");

-----------------------------------
-- onTrade Action
-----------------------------------

function onTrade(player,npc,trade)

end; 

-----------------------------------
-- onTrigger Action
-----------------------------------

function onTrigger(player,npc)
	
end;

-----------------------------------
-- onEventUpdate
-----------------------------------

function onEventUpdate(player,csid,option)
--printf("CSID2: %u",csid);
--printf("RESULT2: %u",option);

end;

-----------------------------------
-- onEventFinish
-----------------------------------

function onEventFinish(player,csid,option)
--printf("CSID: %u",csid);
--printf("RESULT: %u",option);
end;

[Click here to return to the step you were on]


Transporter Script

[Click here (and scroll up a bit) to return to the step you were on]

-----------------------------------
-- Area: Toraimarai Canal
-- NPC:  Transporter
-- Involved In Windurst Mission 7-1
-- @zone 169
-- @pos 182 11 -60 169
-----------------------------------
package.loaded["scripts/zones/Toraimarai_Canal/TextIDs"] = nil;
require("scripts/zones/Toraimarai_Canal/TextIDs");
-----------------------------------

require("scripts/globals/settings");
require("scripts/globals/keyitems");
require("scripts/globals/quests");
require("scripts/globals/missions");

-----------------------------------
-- onTrade Action
-----------------------------------

function onTrade(player,npc,trade)

end; 

-----------------------------------
-- onTrigger Action
-----------------------------------

function onTrigger(player,npc)
player:startEvent(0x0047);	
end;

-----------------------------------
-- onEventUpdate
-----------------------------------

function onEventUpdate(player,csid,option)
--printf("CSID2: %u",csid);
--printf("RESULT2: %u",option);
end;

-----------------------------------
-- onEventFinish
-----------------------------------

function onEventFinish(player,csid,option)
--printf("CSID: %u",csid);
--printf("RESULT: %u",option);

	if(csid == 0x0047 and option == 1) then
		player:setPos(0,0,-22,192,242);
	end
end;

[Click here (and scroll up a bit) to return to the step you were on]

NPC IDs

To get the NPC's ID you first need to open npc_list.sql (in Notepad++) and look for the NPCs name (and zone). Luckily for us, DSGameServer console reported the NPC's name and the zone it's in so after a quick search for that NPC we find the following in npc_list.sql:

|npcId|                                                                                                    |zoneid|
(  355,'_4pc',      0,138.749,11.350,-19.995,1,40,40,9,0,0,0,3,0x0200000000000000000000000000000000000000,0,   169);

Unfortunately it isn't as simple as that. GetNPCByID requires the long NPC ID (17469795).

To get the long (or 'old style') NPC ID you can either use this tool or convert it to the old style like so:

NPC ID + 0x1000000 (that's 5 zeros) + (4096 * zoneId) e.g.

 355  + 0x1000000 + (4096 * 169) = 17469795

[Click here to return to the step you were on]


Mob IDs

To find the Mob ID, open mob_spawn_points.sql and search for the mob's name (in our case Hinge Oil):

| mobid    |    mobname |groupid|
('17469666', 'Hinge_Oil', '8829', '86.554', '24.000', '-23.048', '127');

Once you've found the mobs name, find it's groupid in mob_groups.sql (in our case groupid is 8829).

|groupid|poolId|zoneId|
(   8829,  1958,   169,1056,0,0,0,0,65,65);

If that mob's id is linked to the correct groupid and has the same zoneId as the zone the mob you're trying to add (in our case the mob is in Toraimarai-Canal zone 169 and everything is matching up) e.g. Mob ID is 17469666 and the ZoneId is 169 (found by tracing it's groupid in mob_groups) then [Click here to return to the step you were on].

Debugging using print

Print Variables

To print the value of a variable simply do this (lets just say that we want the value of MissionStatus):

print("Mission Status is ",..player:getVar("MissionStatus");

And DSGameServer will print

[LUA Script] Mission Status is 2


Print Mob Action

To find the mob's current action, you need the [Mob ID] and you need add the following code to an onTrigger of any NPC (in our case it's _4pc):

function onTrigger(player,npc)

-- can only check 1 mob per GetMobAction so add multiple lines for multiple mobs like so

		 print("HingeOil 1 Action: "..GetMobAction(17469666)); 
                 print("HingeOil 2 Action: "..GetMobAction(17469667)); 
                 print("HingeOil 3 Action: "..GetMobAction(17469668)); 
                 print("HingeOil 4 Action: "..GetMobAction(17469669)); 
end

DSGameServer will then 'print' the mob's current action. You can check the mob's Action once dead by either using @despawnmob mobId e.g. @despawnmob 17469666 or killing the mob and then triggering the print. If the mob is spawned, you'll get a message like this in DSGameServer.exe:

[LUA Script]HingeOil 1 Action: 16

If it's de-spawned, you'll get a message like this in DSGameServer.exe (you have to wait around 10 secs for it to take effect):

[LUA Script]HingeOil 1 Action: 25

[Click here to return to the step you were on]