Main Page   Modules   Class Hierarchy   Alphabetical List   Compound List   Compound Members   Related Pages  

Quest Programming

BBS Quest Development

In X2 there are some important new features: Stations have bulletin boards showing job offers and news articles. Quests can now use the BBS, can run multithreaded and can send messages to the player like e-mails that look much better than the X-Tension quest menus.

The Bulletin Board System

The BBS is the new frontend for quest offers and news articles. You can access it only if you dock in a station. The articles are generated by the quests and are controlled by the Questmaster. You can see one article at a time and the mission offers have at least one reply button to contact the "writer" of the offer.

IMPORTANT: The mission offers are public and not directed to the player. There may be offers which the player cannot take on. If you press a button in such an offer you are told that you are not good enough or unable to do the job.

Incoming Messages

The Incoming Message menu replaces the ugly XT quest offer/info menu. When you receive a message you are not forced to read the message immediately but the computer voice says "Incoming message" and a blinking notifier in the HUD is activated.

Messages are generated by running quests and quest offers. They look exactly like the BBS articles, even buttons are possible for interaction during a quest.

When you have read the message and press any button (an OK button is inserted automatically if there is no reply button in the message text) it is stored in the menu "All Received Messages". This menu is similar to the logbook, indeed it will completely replace the player's log! You cannot press any buttons there.

Message Design

BBS articles and incoming messages are pretty much the same and they are written in the same way. You can use several XML tags to define the text layout, colors, reply buttons and so on. Since it is difficult to use < and > in the TextDB you can use [ and ] instead. For arguments inside tags you can use ' instead of " like [tag argument='1']. All tags and argument names are written in lower case, and be aware of the XML rules.

Available outer tags:

[author]...[/author]

There should always be an author tag. Do not enter your name ;) this is the name of the sender of the message and is always visible in the messages and BBS articles. If there is no author tag the author is "Unknown".

Even news articles need an author. In this case it can be a news agency or something...

Quests can insert the author using SE_SPrintf(...) if the text starts with "[author]%s[/author]", or they can set the string variable qu_Author so that the author is automatically inserted in the super.Get...Text() functions.

There is currently no possibility to insert a speaker face or other images, but perhaps later.

[title]...[/title]

The headline of the article. It is displayed centered and underlined above the message text. This tag is optional, it is mainly important for news articles.

[text]...[/text]

The actual message. This tag is not really necessary: If you leave it out, everything except author and title is considered to be the message text. But if you use it, everything before [text] except author and title is ignored and everything beyond [/text] is cut off.

But why is it that important? Because you can supply a lot of layout arguments:

[text cols='...' colwidth='...' colspacing='...' colalign='...']
cols Number of columns (so that the message looks like a newspaper article) Default: one column, the maximum is 4 columns. (Be aware that the maximal width is 600 pixels, based on a 640x480 resolution, 4 columns do not make much sense)
colwidth The width of each column in pixels. Minimal width is 120. If colwidth is not given the maximal width is used with a spacing of 30 pixels between the columns.
colspacing Spacing between the columns in pixels. Minimum: 20, default: 30 pixels.
colalign Sets the alignment of the columns (not of the text!) If colwidth is not set colalign is ignored. Use 'left', 'right' or 'center' to set the position of the columns relative to the screen. Use 'justify' (default) to set colspacing to the maximum.

Do not use words which are longer than the column width. They are broken in the middle of the word. Also do not use two or more spaces at the end of a line, they are cut off. In both cases the following tags are not handled correctly.

In the message text the following tags can be used:

[br/]

Inserts a line break. You can use \n instead. (This is XML and not HTML. The / is necessary to show that there will be no following [/br].)

[right], [center], [justify]

Set the alignment of the text. The text is left-aligned by default. [justify] forces the text to line up between the left and right edges of the column by increasing the spacing between the words in a line. The last line of a paragraph (before \n) is not justified.

[select value='...']...[/select]

This tag is similar to the <a href="..."> link in HTML. It is a small select bar and can be used as reply button. If there are multiple buttons you can select one by pressing Up and Down. WARNING: The start and the end of the selectable text must be in the same line!

The value argument is passed to the quest which has sent this text. If there is no value argument the passed string is 0 (not "0"). That means that it is not a string. If the text is an incoming message there is an OK button with value == 0. You have to check this case in the quest. If you do not want to have this OK button (mission offers can have a button to decline the offer instead) you have to write a select button without a value argument. For example:

Are you interested?\n
\n
[select value='accept']Accept offer[/select]\n
[select]Turn down offer[/select]

The OK button is not displayed here, selecting "Turn down offer" has the same effect.

Remember that buttons in incoming messages can be pressed only once, then the message is stored in the message log. If you want to offer a mission in a message but there are also buttons to provide infos about the mission, there must be buttons in all following messages to launch the mission.

[u]...[/u]

Marks the text as underlined.

[yellow], [red], [green], [blue], [b]

Set the text color. Of course there must be closing tags [/yellow] etc, too. [b] means bold but has the same effect as [yellow]. The color tags are the only ones you can also use for the author and the headline.

Introduction to BBS Quests

If you are familiar with writing X-Tension quests it can help to understand some things better. But almost everything has changed :) so I hope even beginners can understand it though writing a BBS quest is much more complicated. Of course you should at least know object-oriented programming and the KC Language!

BBS quests are offered in the BBS or in incoming messages, but these messages containing quest offers should be used by the plot missions only. The old quests still work like they worked before but probably they are not used any more in X2.

You do not derive your quest class from OBJ_QUEST but from OBJ_BBS_QUEST. The OBJ_BBS_QUEST itself is derived from OBJ_QUEST.

class QUESTOBJ(Q_ID) : OBJ_BBS_QUEST
{
    // Your variables and functions...
}

The main difference: Multiple quests can run at the same time, even the same quest can run multiple times at the same time! Quests now are not just simple static objects, a quest instance is created when the quest is offered and is destroyed when the quest is finished or aborted or the player did not react to the offer.

While a quest is running, variables and functions exist both in the static object and in the dynamic instance. You must carefully think in which object you want to use the variables because you cannot directly access variables in other objects even if the objects are of the same class. Be aware that variables in a newly created copy are uninitialized and that their values are lost when the instance is deleted. Sometimes you have to copy the value of a variable from and to the static object using a function.

For example you want to use a variable which counts how often the quest has been finished.

class QUESTOBJ(Q_ID) : OBJ_BBS_QUEST
{
    int qq_TimesFinished;
    ...

How do you want to use this variable? It exists in both objects but of course it must be used in the static object because the dynamic object cannot store information after it is destroyed.

To find out if the quest is finished you overwrite the function Finish(). But Finish() is called in the dynamic instance so you have to use a function to set the value.

    ...
    int qq_TimesFinished;

    int GetTimesFinished()
    {
        return qq_TimesFinished;
    }

    void SetTimesFinished(timesFinished)
    {
        qq_TimesFinished = timesFinished;
    }
    ...

Now call this function in the static object. Your static quest object is QUESTOBJ(Q_ID) or QUESTOBJ(qu_ID), and the object you are currently in is this.

BTW: If this != QUESTOBJ(qu_ID) you know that the function which you are in is called in the static object. But if you know what you are doing you should not need this check :)

    ...
    void Finish()
    {
        super.Finish();     // Call Finish() in the super object
        QUESTOBJ(qu_ID).SetTimesFinished(QUESTOBJ(qu_ID).GetTimesFinished() + 1);
        // NOT this.SetTimesFinished(...)
    }
    ...

Of course a function IncreaseTimesFinished() would make more sense.

The most of your variables are used in the dynamic instance which is your actual quest. Do not initialize these variables in Set() or anywhere else in the static object, they must be initialized in the instance. But if you have to initialize a variable before it is used in the instance you can copy the value:

qq_Destination = QUESTOBJ(qu_ID).GetDestination();

Having many access functions in a quest does not look nice so you should avoid it if possible.

To print debug output messages use the macro Q_DEBUG(str). It prints the quest ID, instance number and the output string to the debug output window. In the release version the macro has no effect. For example:

Q_DEBUG(SE_SPrintf("Check() player landed in station: %s in %s", ...))

Mission Texts

Think about what task the player needs to do, what reward he will get, what consequences a failure will have. Decide if it is a peaceful trading mission or a combat mission or a combination of both. What additional difficulties will the player face while doing the mission? How much time will the player have to finish the mission?

Before you start writing a mission you should write text messages that the player gets when

  1. the mission is offered (normally this text is a BBS article)
  2. the player has not completed the mission yet and gets a reminder
  3. the player completes the mission
  4. the player fails
  5. the player's request to do the job is accepted
  6. the player's request is rejected

The default case is that these messages are numbered from 1 to 6. In contrast to the X-Tension quests there are no logbook texts but incoming messages, and additional messages when the player tries to launch a quest. He gets some feedback that the mission has started or that he is not able to do the job.

The message texts are stored in your quest page of the TextDB. You can use SE_ReadText(qu_TextPage, messagenumber) or the macro Q_MAKETEXT(messagenumber) to read the text from the page.

The Questmaster gets the needed texts by calling some Get...Text() functions. You can overwrite them to insert strings and numbers using SE_SPrintf(). This function takes a printf-type format string containing %s for a string and %d for a number to substitute. All texts should contain the [author] tag. It is automatically inserted in Get...Text() if you set the string variable qu_Author. If the mission is offered multiple times you should implement alternating offer texts. For example:

    // Offer text (text #1):
    // "[text cols='2']This is %s of the %s. I am going to fly to %s with my ship but I expect trouble "
    // "from Xenon ships attacking me during the flight. I need a good fighter who can protect me. "
    // "If you want to escort me and my ship reaches %s, you will be payed %d cr.\n\n"
    // "[select value='start']Contact me![/select][/text]"

    // More offer texts with different words but the same meaning:
    // Offer text (text #7): ...

    string GetOfferText()
    {
        // qq_OfferID could be set in Evaluate()
        string str;
        if (qq_OfferID != 1) {
            str = "[author]" + qu_Author + "[/author]" + Q_MAKETEXT(qq_OfferID);
        } else {
            str = super.GetOfferText(); // qu_Author is inserted automatically
        }
        // Insert needed names and numbers
        return SE_SPrintf(str, qu_Author, GetObjectName(qq_Race),
            GetObjectName(qq_Destination), GetObjectName(qq_Destination), qq_Reward);
        // SE_FreeString(str) is not needed any more
    }

You can find similar possibilities to implement different offer texts in the BBS quest example Q190.

GetObjectName(object) gets a readable name of object, which can be a ship, station, race or even sector. To get an ID code string of an object use GetIDCode(object).

Normally the messages are sent to the player automatically. But if you want to send messages by yourself, you can use the following functions: this.ReceiveMessage(messagestring) stores the message and activates a notifier until the player opens the Incoming Message menu. this.DisplayMessage(messagestring) displays the message immediately. This is only used for Accept and Reject texts if the player has pressed a button. To store a message in the message log without notifying him use this.SilentMessage(messagestring). Do not use the display function if the player does not expect it! It can be confusing or annoying.

Logbook entries can still be made by calling OBJ_CLIENT.GetPlayer().LogString(entry) or using the macro Q_LOG(entry). .

Initializing the Quest Object

Like in X-Tension, the function Set() is called just before the game starts. There is no need to set qu_ID to Q_ID, this is done before Set() is called. The priority variables (qu_Priority, qu_MinPriority, qu_MaxPriority) are not used any more.

The following variables can be set:

qu_TextPage The text page must be set to TPAGE_ID.
qu_StoryState

The value of this variable determines if this quest is treated as a story quest. A story quest is different from a "normal" quest:

  • It is singlethreaded like the old X-Tension quests. Once this quest is offered or running, instances of this quest will not be created again.
  • There can be only one offer of this quest in one station BBS but the same quest can be offered in multiple stations. For example you can offer the same specific mission (the same instance) in all stations in a particular sector. Once the player starts the quest, all offers of this quest in all stations are deleted (it cannot be started multiple times).
  • If the quest has been finished it will not be offered any more, but if it is aborted it can be offered again.

All quests which belong to the X2 plot will need this behaviour. The default value of qu_StoryState is FALSE, thus the quest is a simple quest that can be offered twice or more at the same time (then it should be able to alternate the offer text) and can be started multiple times. Set the variable to TRUE to change the behaviour. It does not store just TRUE and FALSE, the Questmaster sets the value to QUEST_OFFERED if the quest has been offered the first time, to QUEST_ACCEPTED if it has been launched and to QUEST_FINISHED if it has been finished. But if the quest is aborted qu_StoryState is set to TRUE so that the mission can be offered again. You can set it to QUEST_FINISHED in Abort() to make the Questmaster ignore this quest after aborting.

qu_IncomingMessage

This variable is set to FALSE by default. Set it to TRUE if you do not want to offer this quest in a Bulletin board but in a message to the player. Though the quest object is still a BBS quest it has nothing to do with the BBS, it is more like the old X-Tension quests. Whenever the player lands in a station or enters a sector the Questmaster looks for quests which have set qu_IncomingMessages to TRUE.

If the quest is launched the function Offer() is called which sends the offer text to the player and returns QUEST_OFFERED. It can be overwritten to return QUEST_ACCEPTED. That will start the quest immediately without informing the player. You can do this if you need to prepare something but you are responsible for offering the mission later and aborting if the player declines the mission.

The only X2 quests which will be offered in incoming messages will be the plot quests. So if you just want to write a simple quest use the Bulletin Board System only.

qu_Timeout This variable determines how long (in ingame seconds) the BBS offer remains at least visible after it is created. After this time the offer is removed and the instance is deleted. You can set qu_Timeout to 0 if you do not want the offer to be deleted. The default value is 900 seconds (15 minutes). You can overwrite the timeout value in the instance when the offer is created. Incoming offer messages are concerned, too. After the timeout you can still read the message but the quest has already been deleted and the buttons have no effect.

Conditions for Offering the Mission

The BBS menu calls the Questmaster from time to time to get the newest BBS articles. If the Questmaster decides that some quests must be created the quest function

int EvaluatePriority(GALOBJ station, int existingOffers)

is called in all quests if their variable qu_IncomingMessage is FALSE.

The parameter station is the location where the player is docked, existingOffers is the number of offers of your quest which already exist in this station. (There can be multiple quest instances at the same time!) You cannot find out how many if your quest offers there are in total in the whole universe unless you have a static counter.

You have two things to do: First, set the variable qu_MaxOffers to limit the offers that may be created now. If you do not want to have more than 3 offers of this quest in this BBS, set qu_MaxOffers to 3 - existingOffers.

Second, return the priority of this quest in this station. The priority has an other meaning than in X-Tension quests. If you do not want to offer this quest in this station, return 0. If you want to force the Questmaster to create an instance you have to return 100, else the number tells if the quest is created more often than another quest.

For example: The Questmaster wants to create 10 BBS quests and there are 3 suitable QUESTOBJ's and calls EvaluatePriority(...) in each quest. The first quest returns 100 and is created once, the second one returns 20 and the third one 40. The second quest is created 3 times, the third one 6 times on condition that qu_MaxOffers allows it. If there are only one or two quests to be created, the priority can also be a probability: The higher the priority the higher the probability to be created. The usual priority should be about 30.

To create a quest means that the Questmaster creates an instance of your quest which actually contains the evaluated information about the speaker name, destinations etc. For example, you could write a simple fight quest which is offered multiple times in the same bulletin board. There can be different reasons, different names, different targets etc. which are stored in the instances.

The variables qu_ID, qu_TextPage and qu_Timeout are copied to the new instance, qu_Location, qu_Speaker, qu_Mood are initialized (see Speakers, faces and NPC names) and Evaluate() is called. Overwrite Evaluate() to check the offer conditions and return TRUE or FALSE to indicate whether the quest may be launched or not. Initialize all your variables here because Prepare() is not used any more.

Attention: If Evaluate() returns TRUE the quest is offered in the BBS, else it is deleted. The evaluate conditions must be independent of the player's state, it can be possible that the player cannot take on the offer. It is checked later whether the player meets the conditions or not.

If possible, you should check most of the conditions already in EvaluatePriority() and return 0 if the quest may not be enumerated. If you return FALSE in Evaluate() your quest is not replaced by another one, so there will be less BBS offers than the Questmaster has expected. But remember that you should not initialize instance variables in EvaluatePriority() like destination factories, you would need to copy them from the static object.

If you have set qu_StoryState to TRUE then Evaluate() will be called only the first time. Each further time you return 100 in EvaluatePriority() this quest instance is just copied, the offer is exactly the same. So you should check all the evaluate conditions already in EvaluatePriority(). The offers parameter is always 0 but qu_StoryState has been set to QUEST_OFFERED.

If you do not use the BBS (see qu_IncomingMessage) the returned value of EvaluatePriority() is a probability in percent. The station parameter contains the current location of the player.

The base quest module offers several macros to make checking the conditions easier:

Some of these conditions can be put into the EvaluatePriority() and Evaluate() functions but the conditions which are dependent of the player should be checked later when the player tries to select the mission.

If Evaluate() returns TRUE, the offer string returned by GetOfferText() (text 1) is offered in the station BBS. If qu_IncomingMessage is TRUE, Offer() is called instead. You do not need to overwrite it, it sends the offer and returns QUEST_OFFERED. If you overwrite it and return QUEST_ACCEPTED instead, Accept() is called and the quest starts immediately.

Running a Quest

In the offer text there must be at least one reply button. If the player presses a button, the function

int Select(string value)

is called. value contains the string which is given as argument of the [select] tag.

You can return one of the following constants:

QUEST_ACCEPTED Starts the quest. Accept() is called which sends GetAcceptText() (text 5) to the player to inform him. The offer is removed from the BBS and stored in the message log. If this is a story quest, qu_StoryState is set to QUEST_ACCEPTED and the quest will not be offered any more (EvaluatePriority() is not called any more).
QUEST_REJECTED The quest is not started. Reject() is called which sends GetRejectText() (text 6) to the player. The message should contain the reason why the player cannot take on the offer.
QUEST_NONE The quest is not started and nothing happens.

Select() is not only called to start a mission, it is also called every time a button is pressed in an incoming message, even the OK button. You should find out which button has been pressed. You can use the quest state for this check. It is stored in qu_State. Its value is QUEST_OFFERED as long as the BBS offer or the offer message exists. It is set to QUEST_ACCEPTED after starting the quest and QUEST_CONTINUED while running. Do NOT change qu_State directly!

If you want to delete the quest while it is offered do not use any of the functions mentioned below! Just set qu_State to QUEST_NONE. It will take effect after about 30 seconds or immediately if you set it in Select().

Like in X-Tension quests, the further communication is done from the Check() function. It is called whenever the player enters a new sector or docks at a station and determines if the quest is finished, will be aborted or requires further action. One of the following paths should be taken:

The functions Offer(), Accept(), Reject(), UpdateMessage(), Finish() or Abort() are called by the Questmaster, you do not need to call them by yourself. You can overwrite them if you want to change their behaviour.

The Questmaster functions QuestReady(), QuestAccepted(), QuestDeclined(), QuestFinished() and QuestAborted() are not used any more because the Questmaster cannot know which quest has called the functions, therefore BBSQuestFinished() and BBSQuestAborted() need the parameter this to identify the quest instance.

If you want to check if the player ship is docked at a station it is not enough to check the environment. In X2 you can fly around inside a station. Use the macro Q_PLAYER_DOCKED to find out whether the playership is inside a docking bay. The definition of Q_PLAYER_DOCKED is:

#define Q_PLAYER_DOCKED (OBJ_CLIENT.GetInStationMode() == INSTATIONMODE_DOCKED)

Check() is called just before the station trade menu appears so you can be sure that the player does not fly around in the station. Apart from Check() you can use the function Vbi() to check the mission state more often. It is called every frame so you do not need to START tasks to check the mission state. You can also finish and abort the quest using BBSQuestFinished() and BBSQuestAborted() from Vbi() or from wherever you want.

If Select() is called while the quest is running, return the same value that you would return in Check(). Normally you should return QUEST_CONTINUED.

The function Destruct() is called before the quest instance is destroyed. That does not only happen if the quest is finished or aborted but also if the offer is redrawn (if qu_Timeout seconds have passed or qu_State is set to QUEST_NONE while the quest is offered). You can do some cleanup there.

If you want that the player can ask ship pilots how to get to a specific destination sector like in the X plot and in X-Tension quests, you can set the so-called question sector by calling this.SetQuestionSector(x, y); where x and y are the sector coordinates. In X-Tension the Questmaster stored the one and only question sector, but now the player can do multiple missions, thus there can be multiple destination sectors :) But every quest can have only one question sector. To reset the question sector call this.ResetQuestionSector();

Results for the Player

Rewards for players can be credits, and/or a raise in fame. To give the player 1000 credits, use the function OBJ_CLIENT.GetPlayer.AddMoney(1000). For successful trading missions, you might increase the notoriety relation between the race and the player. For example, if the player finished a trading mission with the space station object station, use the following code to increase the notoriety of its owner race by 1:

gStation.GetOwner().AddNotoriety(OBJ_CLIENT.GetPlayer(), 1);

In case you want to set the notoriety to a certain value instead of adding a relative offset, you can call SetNotoriety(), e.g.:

ga_Races[RACE_BORON].SetNotoriety(OBJ_CLIENT.GetPlayer(), 1000)

This sets the notoriety to exactly 1000! This however can only be useful in debug code or in a Force() function, as every normal notoriety reference should be relative to the player's current state!

Speakers, Faces and NPC Names

The quest variable qu_Speaker defines which face the player should see while a text is displayed (Currently it is not possible to display a player face in incoming messages but you might need a speaker for a customized comm menu). qu_Speaker should be initialised to a meaningful ID depending on where the NPC is located. Also the name of the NPC, which is written in the [author] tag, should be queried from the location that the NPC is talking from.

Here are the two most common cases:

Example Quests

Here is a quest framework, which you can use for your own quests: BBS Quest Framework



Generated on Mon Aug 26 18:26:35 2002 for X² KC by doxygen1.2.17