Difference between revisions of "Interacting with the player: BBS forms"

From PioneerWiki
Jump to: navigation, search
(Internationalization moved to its own page)
(The player's mission list: More info and buildMissionDescription.)
 
(7 intermediate revisions by 2 users not shown)
Line 1: Line 1:
{{Outdated}}
 
 
The bulletin board system is currently the only place where real dialogue between a script and the player can take place. Bulletin boards can exist within any <code>SpaceStation</code> in the current system. They are created in a station the first time that a ship is either spawned, or lands, in that station. They continue to exist until the player leaves the system or quits the game. They are not saved in saved games, although a saved game contains information about which ones did exist.
 
The bulletin board system is currently the only place where real dialogue between a script and the player can take place. Bulletin boards can exist within any <code>SpaceStation</code> in the current system. They are created in a station the first time that a ship is either spawned, or lands, in that station. They continue to exist until the player leaves the system or quits the game. They are not saved in saved games, although a saved game contains information about which ones did exist.
  
Line 20: Line 19:
 
One important thing to bear in mind is that the script cannot query a bulletin boad to find out what adverts already exist. Each script must carefully track each advert that it has created if it is to have any control over how long they remain, and to be able to re-create them after a reload.
 
One important thing to bear in mind is that the script cannot query a bulletin boad to find out what adverts already exist. Each script must carefully track each advert that it has created if it is to have any control over how long they remain, and to be able to re-create them after a reload.
  
Here is an example of adding an advert. The effect of clicking the advert is simply to send a message to the player console with the name of the station. The advert is only added if the station is in space. There is no mission here; I am simply illustrating the mechanics of adding the advert.
+
Here is an example of adding an advert. The effect of clicking the advert is simply to send a message to the player console with the name of the station and the reference number which is also the adverts place on the BBS. The advert is only added if the station is in space. There is no mission here, it is simply to illustrate the mechanics of adding an advert.
  
  local Event = import("Event")
+
  local Event = require "Event"
  local Comms = import("Comms")
+
  local Comms = require "Comms"
 +
 +
local ref  -- Variable to save the advert's reference number
 
   
 
   
 
  local onCreateBB = function (station)
 
  local onCreateBB = function (station)
Line 29: Line 30:
 
     -- This function can be in any scope that's visible when AddAdvert() is called
 
     -- This function can be in any scope that's visible when AddAdvert() is called
 
     local sendStationName = function ()
 
     local sendStationName = function ()
         Comms.ImportantMessage(station.label)
+
         Comms.ImportantMessage(station.label .. "\n" .. "Add ref number: " .. ref)
 
     end
 
     end
 
+
 
     if station.type == 'STARPORT_ORBITAL' then
 
     if station.type == 'STARPORT_ORBITAL' then
         station:AddAdvert('Need the name of this station?',sendStationName)
+
         ref = station:AddAdvert('Need the name of this station?',sendStationName)
 
     end
 
     end
 
  end
 
  end
Line 41: Line 42:
 
This code will create an advert:
 
This code will create an advert:
  
[[File:interact.bbsad.1.png]]
+
[[File:Stationname1.png]]
  
Looking at the image, you will notice that my advert has appeared at a completely arbitrary location on the bulletin board. There is no way to specify the location, and no way to determine it.
+
Looking at the image, you will notice that the advert has appeared at a completely arbitrary location on the bulletin board. There is no way to specify the location, and no way to determine it.
  
 
Clicking on the advert causes this to happen:
 
Clicking on the advert causes this to happen:
  
[[File:interact.bbsad.2.png]]
+
[[File:Stationname2.png]]
  
Even though the only thing our function did was to send a message to the console (visible at the bottom), you can see that Pioneer automatically created a form for our advert. The only control is the "Go back" button, and the name and face are defaulted to that which was displayed on the main bulletin board.
+
Even though the only thing our function did was to send a message to the console, you can see that Pioneer automatically created a form for our advert. The only control is the 'Hang up' button.
 +
 
 +
Going back to the World View you can see the message on the Comms terminal:
 +
 
 +
[[File:Stationname3.png]]
  
 
==The BBS form==
 
==The BBS form==
Line 57: Line 62:
 
The form itself is passed to the function specified in the <code>SpaceStation.AddAdvert()</code> method. In the example above, that function would be <code>sendStationName()</code>, which simply ignored any parameters sent to it. This resulted in the form being blank.
 
The form itself is passed to the function specified in the <code>SpaceStation.AddAdvert()</code> method. In the example above, that function would be <code>sendStationName()</code>, which simply ignored any parameters sent to it. This resulted in the form being blank.
  
The form object which is passed to this function has methods for adding the content. <code>SetTitle()</code> and <code>SetMessage()</code> each accept a string. <code>SetFace()</code> takes a table of information which defines the photofit face on the left. <code>AddOption()</code> adds clickable options with buttons. <code>Clear()</code> removes the Message and Options, but preserves the Title and Face, while <code>Close()</code> acts the same way as the "Go back" button.
+
The form object which is passed to this function has methods for adding the content. <code>SetTitle()</code> and <code>SetMessage()</code> each accept a string. <code>SetFace()</code> takes a table of information which defines the photofit face on the left but in the following example <code>SetFace()</code> is called without any arguments and the face will therefore be completely randomized.
  
The following example doesn't have any clickable options, but does have a customized form:
+
A minimal example without any clickable options:
  
 +
local Event = import("Event")
 +
 
  local populateForm = function (form)
 
  local populateForm = function (form)
    local facedata = {
 
        name = "Bob",
 
        female = true,
 
        title = "Lorry driver",
 
    }
 
 
 
     form:SetTitle('This appears above the face picture')
 
     form:SetTitle('This appears above the face picture')
     form:SetFace(facedata)
+
     form:SetFace()
 
     form:SetMessage([[This is the main message.
 
     form:SetMessage([[This is the main message.
 
   
 
   
Line 83: Line 84:
 
As before, an advert was created:
 
As before, an advert was created:
  
[[File:interact.bbsad.3.png]]
+
[[File:Populateform2.png]]
  
Randomly, it has appeared at the top of the list.
+
We have clicked on the ad and can see that <code>populateForm</code> was called, and it successfully filled the form with content. If you 'hang up' and press the ad again a completely new face is generated. Lets improve on this and make the face persistent for the existence of the advert. Making it reappear in a saved game would take some more work (see "[[Surviving a reload]]").
  
Clicking on the advert causes this to happen:
+
local Character = require "Character"
 
+
local Event = require "Event"
[[File:interact.bbsad.4.png]]
+
 +
local client = {}
 +
local message
 +
 +
local populateForm = function (form)
 +
    form:SetTitle('Pizza time!')
 +
    form:SetFace(client)
 +
    form:SetMessage(message)
 +
end
 +
 +
local onCreateBB = function (station)
 +
    client = Character.New()
 +
    message = "I'm " .. client.name .. " and I need some pizza. I'm thinking pepperoni and cheeze. You feelin me?"
 +
    station:AddAdvert('Special delivery needed',populateForm)
 +
end
 +
 +
Event.Register("onCreateBB", onCreateBB)
  
Now we can see that <code>populateForm</code> was called, and it successfully filled the form with content. All of the face options were optional; if they aren't provided, Pioneer will choose random values. I have left out a couple of options here; it's wise to specify them all, because after a saved game is loaded, it's the script's job to make sure that the same face appears (see "[[Surviving a reload]]").
+
We introduce a new module ''' '[https://pioneerspacesim.net/codedoc/files2/Character-lua.html Character]' ''' which basically is an rpg style character sheet with methods to work with it. It's a tool to generate and work with non-player characters (npc's). <code>client = Character.New()</code> sets client to an all new character with basic characteristics, name, title, and looks. A character can be sent directly to the SetFace method and that's what we're after here. We also used the 'clients' name.
  
 
==Adding options to the form==
 
==Adding options to the form==
  
In the example above, I created a function named <code>populateForm()</code> which was run when the advert button was clicked. That's not the only time it can be run; it is also run whenever options on its form are clicked.
+
In the example above, we created a function named <code>populateForm()</code> which was run when the advert button was clicked. That's not the only time it can be run; it is also run whenever options on its form are clicked. To make use of this, it is passed two additional parameters, both of which <code>populateForm()</code> ignored. The first parameter is the form object, the second is the advert's unique reference and the third is the number of the option that was clicked. Because it handles all chat events, by convention we instead name this function <code>onChat()</code>, which is how it shall be named from now on.
  
To make use of this, it is passed two additional parameters, both of which <code>populateForm()</code> ignored. The first parameter is the form object, the second is the advert's unique reference and the third is the number of the option that was clicked. Because it handles all chat events, by convention we instead name this function <code>onChat()</code>, which is how it shall be named from now on.
+
In the first example that called the function 'sendStationName' we caught the reference number from ''' 'AddAdvert()' ''' in a variable named  ''' 'ref' '''. As you may remember, ''' 'AddAdvert()' ''' takes two arguments; A string and a function. The function is passed three arguments. The second of these arguments is the reference number so we could have picked it up from within ''' 'sendStationName' '''. The ''' 'ref' ''' number is useful if your script adds several adverts, each of which might have slightly differently worded content.
  
The previous example did not store or use the value returned by <code>AddAdvert()</code>. It is this value which is sent as the second parameter; very useful if your script adds several adverts, each of which might have slightly differently worded content.
+
You've already learned the methods: ''' 'SetTitle()' ''', ''' 'SetMessage()' ''', and  ''' 'SetFace()' '''. Let's add the rest of the form() methods:<br>
 
+
* ''' form:AddOption() ''': adds clickable options with buttons. It takes two arguments: A string to set the text of the button and the option nr that will be sent to the form. This value is 0 when first called from the BBS by clicking the add. Every option form will have a default 'Hang up' option which returns -1.<br>
When the advert was clicked in the main bulletin board list, it was passed the value <code>0</code> as the option.
+
* ''' form:Clear() ''': removes the Message and Options, but preserves the Title and Face.<br>
 +
* ''' form:Close() ''': Closes the form.<br>
 +
* ''' form:RemoveAdvertOnClose() ''': Closes the form and removes the ad.<br>
  
 
Form options are declared like this:
 
Form options are declared like this:
  
  form:AddOption('I am option one',1)
+
form:AddOption('I am option one',1)
  form:AddOption('I am option two',2)
+
form:AddOption('I am option two',2)
  
These options will appear with the specified caption, and will call <code>onChat()</code>, which will receive the form object, the advert reference and the number that was specified after the caption in <code>AddOption()</code>.
+
These options will appear with the specified caption, and will call <code>onChat()</code>, which will receive the form object, the advert reference and the option number that was specified after the caption in <code>AddOption()</code>.
  
The codedoc has a brilliant example of a complete onChat, which I will reproduce here:
+
The onChat function below is adapted from an earlier example from the codedoc:
  
 +
local Event = require 'Event'
 +
 
  local onChat = function (form, ref, option)
 
  local onChat = function (form, ref, option)
 
     form:Clear()
 
     form:Clear()
Line 122: Line 143:
 
         form:SetMessage("What's your favourite colour?")
 
         form:SetMessage("What's your favourite colour?")
 
   
 
   
         form:AddOption("Red",       1)
+
         form:AddOption("Red",     1)
         form:AddOption("Green",     2)
+
         form:AddOption("Green",   2)
         form:AddOption("Yellow",   3)
+
         form:AddOption("Blue",     3)
        form:AddOption("Blue",      4)
 
        form:AddOption("Hang up.", -1)
 
 
   
 
   
 
         return
 
         return
Line 134: Line 153:
 
     if option == 1 then
 
     if option == 1 then
 
         form:SetMessage("Ahh red, the colour of raspberries.")
 
         form:SetMessage("Ahh red, the colour of raspberries.")
        form:AddOption("Hang up.", -1)
 
 
         return
 
         return
 
     end
 
     end
Line 141: Line 159:
 
     if option == 2 then
 
     if option == 2 then
 
         form:SetMessage("Ahh green, the colour of trees.")
 
         form:SetMessage("Ahh green, the colour of trees.")
        form:AddOption("Hang up.", -1)
 
 
         return
 
         return
 
     end
 
     end
 
   
 
   
     -- option 3 - yellow
+
     -- option 3 - blue
 
     if option == 3 then
 
     if option == 3 then
        form:SetMessage("Ahh yellow, the colour of the sun.")
 
        form:AddOption("Hang up.", -1)
 
        return
 
    end
 
 
    -- option 4 - blue
 
    if option == 4 then
 
 
         form:SetMessage("Ahh blue, the colour of the ocean.")
 
         form:SetMessage("Ahh blue, the colour of the ocean.")
        form:AddOption("Hang up.", -1)
 
 
         return
 
         return
 
     end
 
     end
Line 162: Line 171:
 
     form:Close()
 
     form:Close()
 
  end
 
  end
 +
 +
local onCreateBB = function (station)
 +
    station:AddAdvert('onChat1',onChat)
 +
end
 +
 +
Event.Register("onCreateBB", onCreateBB)
  
 
Here, every time <code>onChat()</code> is called, regardless of the specified option, the form is cleared. The option is checked, and the relevant content is added to the form. Any other functions can be called from here, and this is how the script gets input from the player.
 
Here, every time <code>onChat()</code> is called, regardless of the specified option, the form is cleared. The option is checked, and the relevant content is added to the form. Any other functions can be called from here, and this is how the script gets input from the player.
Line 167: Line 182:
 
An alternative format might be this:
 
An alternative format might be this:
  
 +
local Event = require 'Event'
 +
 
  local onChat = function (form, ref, option)
 
  local onChat = function (form, ref, option)
 
     form:Clear()
 
     form:Clear()
Line 175: Line 192:
 
             form:SetMessage("What's your favourite colour?")
 
             form:SetMessage("What's your favourite colour?")
 
   
 
   
             form:AddOption("Red",       1)
+
             form:AddOption("Red",     1)
             form:AddOption("Green",     2)
+
             form:AddOption("Green",   2)
             form:AddOption("Yellow",   3)
+
             form:AddOption("Blue",     3)
            form:AddOption("Blue",      4)
 
            form:AddOption("Hang up.", -1)
 
 
         end,
 
         end,
 
         [1] = function ()
 
         [1] = function ()
 
             form:SetMessage("Ahh red, the colour of raspberries.")
 
             form:SetMessage("Ahh red, the colour of raspberries.")
            form:AddOption("Hang up.", -1)
 
 
         end,
 
         end,
 
         [2] = function ()
 
         [2] = function ()
 
             form:SetMessage("Ahh green, the colour of trees.")
 
             form:SetMessage("Ahh green, the colour of trees.")
            form:AddOption("Hang up.", -1)
 
 
         end,
 
         end,
 
         [3] = function ()
 
         [3] = function ()
            form:SetMessage("Ahh yellow, the colour of the sun.")
 
            form:AddOption("Hang up.", -1)
 
        end,
 
        [4] = function ()
 
 
             form:SetMessage("Ahh blue, the colour of the ocean.")
 
             form:SetMessage("Ahh blue, the colour of the ocean.")
            form:AddOption("Hang up.", -1)
 
 
         end,
 
         end,
 
         [-1] = function ()
 
         [-1] = function ()
 
             form:Close()
 
             form:Close()
 
         end
 
         end
    }
+
        }
    options[option]()
+
        options[option]()
 
  end
 
  end
 +
 +
local onCreateBB = function (station)
 +
    station:AddAdvert('onChat2',onChat)
 +
end
 +
 +
Event.Register("onCreateBB", onCreateBB)
  
==The player's mission list==
+
Try this out:
  
Once the player has negotiated with your form, there might well be a mission in play. It could be a delivery, an assassination, a rush to tell somebody not to leave because so-and-so loves them... the possibilities are limited only by your creativity.
+
* To understand what ''' 'form:Clear()' ''' does, comment out the line above containing it and restart Pioneer. As the form is no longer cleared and the 'Hang up' button is included automatically, there now appears to be only one form. The color selection buttons still works though.
  
The player needs a way to keep track of all the missions that they have agreed to undertake. Pioneer provides this through the player's mission screen, which they can access at any time using the F3 button, and looking at the missions tab.
+
* Try adding 'Go back' buttons from the color options:
 +
[1] = function ()
 +
    form:SetMessage("Ahh red, the colour of raspberries.")
 +
    form:AddOption("Go back", 0)
 +
end,
  
The content of this screen is controlled by some methods on the <code>Player</code> object, which can always be found at <code>Game.player</code>, and which inherits from <code>Ship</code> and <code>Body</code>. Missions are added to the screen using the <code>AddMission()</code> method. It takes a table of info, and returns an integer reference to that mission, which should be stored so that it can be updated or removed later. So, it usually looks a little like <code>ref = Game.player:AddMission(table_of_info)</code>.
+
* Try booth suggestions above at the same time. ''' 'form:Clear()' ''' is your friend.
  
In practice, it might look more like this:
+
* Stick this code into the second onChat example above (or adopt it to the first one) to try out ''' 'form:RemoveAdvertOnClose()' ''' and to test the ''' 'ref' ''' argument:
 
+
  form:AddOption("Report post", 4)  -- In option[0]
  local mission_storage={}
 
 
   
 
   
table.insert(mission_storage,Game.player:AddMission({
+
            ...
    type = "Fetch beer",
 
    client = "Bert Beerbreath",
 
    due = Game.time + 600, -- ten minutes' time
 
    reward = 10,
 
    location = Game.player.frameBody.path, -- here, basically
 
    status = 'ACTIVE'
 
}))
 
 
   
 
   
  table.insert(mission_storage,Game.player:AddMission({
+
  [4] = function ()
    type = "Fetch curry",
+
    form:SetMessage("The ad has been reported and will not be shown on your BBS. " ..
    client = "Curt Curryface",
+
                    "Thank you for helping us to improve 'Haber Connect'!\n" ..
    due = Game.time + 900, -- fifteen minutes' time
+
                    "The ad was number " .. ref .. " from the top of the list." )
    reward = 5,
+
     form:RemoveAdvertOnClose()
    location = Game.player.frameBody.path,
+
  end,
     status = 'ACTIVE'
 
  }))
 
  
I don't recommend using <code>Game.player.frameBody.path</code> here. I'm only using it because it always returns something, whether docked or not. A real mission would probably use a space station here.
+
Please note! The '''ref''' nr and position in the '''BBS''' isn't really the same. It probably is the same when the '''BBS''' is first created and that's why it works here but if an add is removed in front of this 'test add' the '''ref''' nr will be one off. We're basically just poking the code a bit.
  
This creates visible missions on the mission screen:
+
==The player's mission list==
  
[[File:interact.misscrn.1.png]]
+
Once the player has negotiated with your form, there might well be a mission in play. It could be a delivery, an assassination, a rush to tell somebody not to leave because so-and-so loves them... the possibilities are limited only by your creativity. The player needs a way to keep track of all the missions that they have agreed to undertake. Pioneer provides this through the player's mission screen, which they can access at any time using the F3 button, and looking at the missions tab. The content of this screen is controlled by some methods on the <code>Player</code> object, which can always be found at <code>Game.player</code>, and which inherits from <code>Ship</code> and <code>Body</code>. Missions are added to the screen using the <code>Mission.New()</code> method. It takes a table of info, and returns an integer reference to that mission, which should be stored so that it can be updated or removed later. Below follows a typical use case from the [https://codedoc.pioneerspacesim.net codedoc].
  
These missions will remain exactly like that forever, unless a script explicitly makes changes. There is no automatic logic, and no automatic removal. Your script must keep track of them. In this example, I have the references in the <code>mission_storage</code> table.
+
Create a new mission and add it to the player’s mission list while retrieving the reference number:
  
The <code>UpdateMission()</code> method allows a script to alter any detail. The status can be <code>'ACTIVE'</code>,<code>'FAILED'</code> or <code>'COMPLETED'</code>. I'm going to change the status of the first mission to <code>'COMPLETED'</code>.
+
ref = Mission.New({
 +
    'type'      = 'Delivery', -- Must be a translatable token!
 +
    'client'    = Character.New(),
 +
    'due'       = Game.time + 3*24*60*60,       -- three days
 +
    'reward'   = 123.45,
 +
    'location' = SystemPath:New(0,0,0,0,16),  -- Mars High, Sol
 +
    'status'   = 'ACTIVE',
 +
})
  
Game.player:UpdateMission(mission_storage[1],{status='COMPLETED'})
+
In practice, it might look more like this:
  
The updated information can be a partial table. The unspecified members will remain unchanged:
+
local Character = require 'Character'
 +
local Event = require 'Event'
 +
local Game = require 'Game'
 +
local Mission = require 'Mission'
 +
 +
local missions = {}
 +
 +
local onShipDocked = function (ship)
 +
    if ship:IsPlayer() then
 +
 +
        table.insert(missions, Mission.New({
 +
            type = "Taxi",
 +
            client = Character.New(),
 +
            due = Game.time + 600, -- ten minutes' time
 +
            reward = 10,
 +
            location = Game.player.frameBody.path, -- here, basically
 +
            status = 'ACTIVE'
 +
        }))
 +
    end
 +
end
 +
 +
Event.Register("onShipDocked", onShipDocked)
  
[[File:interact.misscrn.2.png]]
+
I don't recommend using <code>Game.player.frameBody.path</code> here. I'm only using it because it always returns something, whether docked or not. A real mission would probably use a space station here. For this demonstration we've generated 'Taxi' missions and they will be recognized by the 'Taxi' module as such.
  
The <code>GetMission()</code> method allows a script to read data from a mission, if the script has the reference. It returns a table with that information. Here, I'm going to add ten minutes to the deadline of the second mission:
+
This creates a mission visible on the mission screen:
  
local due_date = Game.player:GetMission(mission_storage[2]).due
+
[[File:Missionlist3.png|1024px]]
Game.player:UpdateMission(mission_storage[2],{due = due_date + 600})
 
  
The result:
+
The next example is extended with a much scaled down version of the [https://github.com/pioneerspacesim/pioneer/blob/master/data/modules/DeliverPackage/DeliverPackage.lua#L418:L451 onShipDocked] function from the DeliverPackage module. Since these missions are recognized by a module in Pioneer already ('Taxi') they will 'probably' be handled at a later stage if we fail to remove them with this test mission. If you generate a new mission type you must also handle removing it or the mission will remain in the game forever. There is no automatic logic, and no automatic removal. Your script must keep track of them.
  
[[File:interact.misscrn.3.png]]
+
local Character = require 'Character'
 
+
local Comms = require 'Comms'
The <code>RemoveMission()</code> method allows a script to remove a mission. Here I remove the completed one:
+
local Event = require 'Event'
 
+
  local Game = require 'Game'
  Game.player:RemoveMission(mission_storage[1])
+
  local Mission = require 'Mission'
  table.remove(mission_storage,1)
+
local Player = require 'Player'
 
+
local Timer = require 'Timer'
And here it isn't:
+
 
+
local missions = {}
[[File:interact.misscrn.4.png]]
+
 
+
local onShipDocked = function (ship, station)
==Mission flavours==
+
    if ship:IsPlayer() then
 
+
A script can define a mission, and then place many instances of it onto many bulletin boards. A script that introduces many instances should provide some variety; it would harm immersion if all delivery missions were worded identically, for example.
+
    -- On docking, starting a new game, we create
 
+
    -- two npc's and book them on a taxi mission.
To introduce variety, we can use tables, which contain all of the lines needed for bulletin board forms, messages and so forth. It's then a simple matter to use a similar, but differently worded, table to make the same misison appear different. A flavour might look like this:
+
        table.insert(missions, Mission.New({
 
+
            type = "Taxi",
{
+
            client = Character.New(),
    title = "Shill bidder wanted for auction",
+
            due = Game.time + 10, -- ten seconds
    greeting = "Hi there. Want to earn some quick cash?",
+
            reward = 10,
    yesplease = "Sure. What do you need me to do?",
+
            location = Game.player.frameBody.path, -- here, basically
    nothanks = "No, ta - this looks a bit shady.",
+
            status = 'ACTIVE'
}
+
        }))
 
+
        table.insert(missions, Mission.New({
An alternative flavour might look like this:
+
            type = "Taxi",
 
+
            client = Character.New(),
{
+
            due = Game.time + 20, -- 20 seconds
      title = "Help me win an auction.",
+
            reward = 5000,
      greeting = "Hello. I'll pay you to place fake bids for me. Interested?",
+
            location = Game.player.frameBody.path, -- here, basically
      yesplease = "Yes, I like to live dangerously.",
+
            status = 'ACTIVE'
      nothanks = "No thanks; I'd rather not get arrested for fraud.",
+
        }))
}
+
    end
 
+
Ideally, we just need a table with as many of these as we can be bothered to write, then select one at random.
+
    -- Magically, without moving, we've arrived at the destination
 
+
    -- after 15 seconds and check in with our passengers.
There are issues with translation, though. Because flavours are often full of colloquial language, and can feature several ways of saying basically the same thing, translating them all word-for-word is not necessarily productive. To this end, the Translate system features a pair of methods which can ease the handling of flavours, especially in different languages.
+
    Timer:CallAt(Game.time+15, function () -- 15 seconds timer
 
+
        for ref,mission in pairs(missions) do
The <code>Translate:AddFlavour()</code> method allows a flavour to be added, and marked as being of a specific language. By convention, these statements would appear in the script's <code>Languages.lua</code> file so that translators could see them and be inspired to write some in other languages. The method takes three arguments. The first is the language of the flavour, as a string. The second is a name, so that the <code>Translate</code> class can give flavours back to the correct script. The third is the flavour table itself. If my script was named <code>TestModule</code>, I might define the two flavours above in my <code>Languages.lua</code> as follows:
+
            if Game.time > mission.due then
 
+
                mission.status = 'FAILED'
Translate:AddFlavour('English','TestModule',{
+
                Comms.ImportantMessage('You suck dude! You suck like REALLY MUCH!', mission.client.name)
      title = "Shill bidder wanted for auction",
+
            else
      greeting = "Hi there. Want to earn some quick cash?",
+
                Comms.ImportantMessage('Thanks for the ride!', mission.client.name)
      yesplease = "Sure. What do you need me to do?",
+
                mission.status = "COMPLETED"
      nothanks = "No, ta - this looks a bit shady.",
+
                Game.player:AddMoney(mission.reward)
  })
+
            end
 +
            mission:Remove()
 +
            missions[ref] = nil
 +
        end
 +
    end)
 +
  end
 
   
 
   
  Translate:AddFlavour('English','TestModule',{
+
  Event.Register("onShipDocked", onShipDocked)
      title = "Help me win an auction.",
 
      greeting = "Hello. I'll pay you to place fake bids for me. Interested?",
 
      yesplease = "Yes, I like to live dangerously.",
 
      nothanks = "No thanks; I'd rather not get arrested for fraud.",
 
})
 
  
As an added benefit, <code>Translate:AddFlavour()</code> checks all flavour tables for uniformity. It does this by comparing all of the table's keys with that of the first English flavour that was specified; a technical consequence of this is that an English flavour needs to come first in your <code>Languages.lua</code> file, otherwise Pioneer will give you an error.
+
Missions Created:
  
To fetch the flavours into your script, use <code>Translate.GetFlavours()</code>. It takes a single argument, which is the module name that was specified in <code>AddFlavour()</code>'s second parameter. It doesn't matter to <code>Translate</code> how many flavours there are in each language, or whether it's an equal number. If my current language is not English, and there are no flavours in that language, English flavours will be used instead. If there is only one flavour in my language, I will only see that one variety.
+
[[File:Missionlist1.png]]
  
The following example will select a random flavour from my flavour tables.
+
1 Mission failed and 1 mission completed:
  
local all_flavours = Translate:GetFlavours('TestModule')
+
[[File:Missionlist2.png]]
local flavour = all_flavours[Engine.rand:Integer(1,#all_flavours)]
 
  
<code>GetFlavours()</code> returns a table of flavours. The second line of code generates a random number between 1 and the number of flavours in that table, and returns that flavour from the table. After this, <code>flavour.title</code> will either be <code>"Shill bidder wanted for auction"</code> or <code>"Help me win an auction."</code>. Adding more flavours means more variety.
+
More info? If you look to the right on the mission list there is a button named 'More info' which will let you see more specific details of your mission. This will need to be specified in the mission script itself so if you press this button in the example above Pioneer will crash. The common name for the function you need to write in your script is '''buildMissionDescription'''. Here is what it looks like in [https://github.com/pioneerspacesim/pioneer/blob/2f277dae8b2036d2c7120ec2b4d7946a46df3473/data/modules/DeliverPackage/DeliverPackage.lua#L486-L514 DeliverPackage.lua].
  
 
==Maintaining immersion==
 
==Maintaining immersion==

Latest revision as of 15:12, 29 June 2023

The bulletin board system is currently the only place where real dialogue between a script and the player can take place. Bulletin boards can exist within any SpaceStation in the current system. They are created in a station the first time that a ship is either spawned, or lands, in that station. They continue to exist until the player leaves the system or quits the game. They are not saved in saved games, although a saved game contains information about which ones did exist.

When a bulletin board is created, the onCreateBB event is triggered, and passes the SpaceStation body in which that bulletin board was created. There is an exception to this: The event is not triggered after loading a game for those bulletin boards which, having existed at the time of saving, are re-created. The consequences of this will be covered later (see "Surviving a reload").

There are two components to any mission's entry on a bulletin board: The advert and the form. The advert is the part that is displayed on the main bulletin board list, along with all the other entries. The form is the part that appears on screen when the player clicks the advert's button.

The BBS advert

Adverts are placed onto a BBS by calling the station's AddAdvert() method, once the bulletin board has been created. Depending on the nature of your script, you might want to always place one advert on every station (as seen with the Breakdowns & Servicing script), or you might want to place an arbitrary number of adverts on a given station (as seen with deliveries, or assassinations).

The opportunities to add an advert are presented by two events. onCreateBB is the obvious one; there is also onUpdateBB, which is called for all existing bulletin boards in the current system, approximately once every hour or two. The actual interval is not particularly predictable.

The AddAdvert() method takes three arguments. The first is the text that will appear on the advert. The second is the function that will be called when the player clicks the advert. The third is optional, and is a function that can be called when the advert is deleted for any reason.

AddAdvert() returns a reference number, which can subsequently be used to remove the advert using RemoveAdvert().

It's the job of the scripter to decide how many, if any, adverts to add to a bulletin board when onCreateBB is triggered, and whether to add or remove any when onUpdateBB is triggered. Tests could include the population of the system, the type of starport or the type of planet. In the future, tests will be able to include the government type of the system.

One important thing to bear in mind is that the script cannot query a bulletin boad to find out what adverts already exist. Each script must carefully track each advert that it has created if it is to have any control over how long they remain, and to be able to re-create them after a reload.

Here is an example of adding an advert. The effect of clicking the advert is simply to send a message to the player console with the name of the station and the reference number which is also the adverts place on the BBS. The advert is only added if the station is in space. There is no mission here, it is simply to illustrate the mechanics of adding an advert.

local Event = require "Event"
local Comms = require "Comms"

local ref  -- Variable to save the advert's reference number

local onCreateBB = function (station)

    -- This function can be in any scope that's visible when AddAdvert() is called
    local sendStationName = function ()
        Comms.ImportantMessage(station.label .. "\n" .. "Add ref number: " .. ref)
    end

    if station.type == 'STARPORT_ORBITAL' then
        ref = station:AddAdvert('Need the name of this station?',sendStationName)
    end
end

Event.Register("onCreateBB", onCreateBB)

This code will create an advert:

Stationname1.png

Looking at the image, you will notice that the advert has appeared at a completely arbitrary location on the bulletin board. There is no way to specify the location, and no way to determine it.

Clicking on the advert causes this to happen:

Stationname2.png

Even though the only thing our function did was to send a message to the console, you can see that Pioneer automatically created a form for our advert. The only control is the 'Hang up' button.

Going back to the World View you can see the message on the Comms terminal:

Stationname3.png

The BBS form

Once the player has clicked on an advert, they are presented with a form. Each advert has only one form. The content of the form is added by the script, and can be modified at any time. It consists of a title, a face, a message and zero or more clickable options.

The form itself is passed to the function specified in the SpaceStation.AddAdvert() method. In the example above, that function would be sendStationName(), which simply ignored any parameters sent to it. This resulted in the form being blank.

The form object which is passed to this function has methods for adding the content. SetTitle() and SetMessage() each accept a string. SetFace() takes a table of information which defines the photofit face on the left but in the following example SetFace() is called without any arguments and the face will therefore be completely randomized.

A minimal example without any clickable options:

local Event = import("Event")

local populateForm = function (form)
    form:SetTitle('This appears above the face picture')
    form:SetFace()
    form:SetMessage([[This is the main message.

It is normally a multi-line string.]])
end

local onCreateBB = function (station)
    station:AddAdvert('This appears in the advert list',populateForm)
end

Event.Register("onCreateBB", onCreateBB)

As before, an advert was created:

Populateform2.png

We have clicked on the ad and can see that populateForm was called, and it successfully filled the form with content. If you 'hang up' and press the ad again a completely new face is generated. Lets improve on this and make the face persistent for the existence of the advert. Making it reappear in a saved game would take some more work (see "Surviving a reload").

local Character = require "Character"
local Event = require "Event"

local client = {}
local message

local populateForm = function (form)
    form:SetTitle('Pizza time!')
    form:SetFace(client)
    form:SetMessage(message)
end

local onCreateBB = function (station)
    client = Character.New()
    message = "I'm " .. client.name .. " and I need some pizza. I'm thinking pepperoni and cheeze. You feelin me?"
    station:AddAdvert('Special delivery needed',populateForm)
end

Event.Register("onCreateBB", onCreateBB)

We introduce a new module 'Character' which basically is an rpg style character sheet with methods to work with it. It's a tool to generate and work with non-player characters (npc's). client = Character.New() sets client to an all new character with basic characteristics, name, title, and looks. A character can be sent directly to the SetFace method and that's what we're after here. We also used the 'clients' name.

Adding options to the form

In the example above, we created a function named populateForm() which was run when the advert button was clicked. That's not the only time it can be run; it is also run whenever options on its form are clicked. To make use of this, it is passed two additional parameters, both of which populateForm() ignored. The first parameter is the form object, the second is the advert's unique reference and the third is the number of the option that was clicked. Because it handles all chat events, by convention we instead name this function onChat(), which is how it shall be named from now on.

In the first example that called the function 'sendStationName' we caught the reference number from 'AddAdvert()' in a variable named 'ref' . As you may remember, 'AddAdvert()' takes two arguments; A string and a function. The function is passed three arguments. The second of these arguments is the reference number so we could have picked it up from within 'sendStationName' . The 'ref' number is useful if your script adds several adverts, each of which might have slightly differently worded content.

You've already learned the methods: 'SetTitle()' , 'SetMessage()' , and 'SetFace()' . Let's add the rest of the form() methods:

  • form:AddOption() : adds clickable options with buttons. It takes two arguments: A string to set the text of the button and the option nr that will be sent to the form. This value is 0 when first called from the BBS by clicking the add. Every option form will have a default 'Hang up' option which returns -1.
  • form:Clear() : removes the Message and Options, but preserves the Title and Face.
  • form:Close() : Closes the form.
  • form:RemoveAdvertOnClose() : Closes the form and removes the ad.

Form options are declared like this:

form:AddOption('I am option one',1)
form:AddOption('I am option two',2)

These options will appear with the specified caption, and will call onChat(), which will receive the form object, the advert reference and the option number that was specified after the caption in AddOption().

The onChat function below is adapted from an earlier example from the codedoc:

local Event = require 'Event'

local onChat = function (form, ref, option)
    form:Clear()

    -- option 0 is called when the form is first activated from the
    -- bulletin board
    if option == 0 then

        form:SetTitle("Favourite colour")
        form:SetMessage("What's your favourite colour?")

        form:AddOption("Red",      1)
        form:AddOption("Green",    2)
        form:AddOption("Blue",     3)

        return
    end

    -- option 1 - red
    if option == 1 then
        form:SetMessage("Ahh red, the colour of raspberries.")
        return
    end

    -- option 2 - green
    if option == 2 then
        form:SetMessage("Ahh green, the colour of trees.")
        return
    end

    -- option 3 - blue
    if option == 3 then
        form:SetMessage("Ahh blue, the colour of the ocean.")
        return
    end

    -- only option left is -1, hang up
    form:Close()
end

local onCreateBB = function (station)
    station:AddAdvert('onChat1',onChat)
end

Event.Register("onCreateBB", onCreateBB)

Here, every time onChat() is called, regardless of the specified option, the form is cleared. The option is checked, and the relevant content is added to the form. Any other functions can be called from here, and this is how the script gets input from the player.

An alternative format might be this:

local Event = require 'Event'

local onChat = function (form, ref, option)
    form:Clear()

    local options = {
        [0] = function ()
            form:SetTitle("Favourite colour")
            form:SetMessage("What's your favourite colour?")

            form:AddOption("Red",      1)
            form:AddOption("Green",    2)
            form:AddOption("Blue",     3)
        end,
        [1] = function ()
            form:SetMessage("Ahh red, the colour of raspberries.")
        end,
        [2] = function ()
            form:SetMessage("Ahh green, the colour of trees.")
        end,
        [3] = function ()
            form:SetMessage("Ahh blue, the colour of the ocean.")
        end,
        [-1] = function ()
            form:Close()
        end
        }
        options[option]()
end

local onCreateBB = function (station)
    station:AddAdvert('onChat2',onChat)
end

Event.Register("onCreateBB", onCreateBB)

Try this out:

  • To understand what 'form:Clear()' does, comment out the line above containing it and restart Pioneer. As the form is no longer cleared and the 'Hang up' button is included automatically, there now appears to be only one form. The color selection buttons still works though.
  • Try adding 'Go back' buttons from the color options:
[1] = function ()
    form:SetMessage("Ahh red, the colour of raspberries.")
    form:AddOption("Go back", 0)
end,
  • Try booth suggestions above at the same time. 'form:Clear()' is your friend.
  • Stick this code into the second onChat example above (or adopt it to the first one) to try out 'form:RemoveAdvertOnClose()' and to test the 'ref' argument:
form:AddOption("Report post", 4)   -- In option[0]

           ...

[4] = function ()
    form:SetMessage("The ad has been reported and will not be shown on your BBS. " ..
                    "Thank you for helping us to improve 'Haber Connect'!\n" ..
                    "The ad was number " .. ref .. " from the top of the list." )
    form:RemoveAdvertOnClose()
end,

Please note! The ref nr and position in the BBS isn't really the same. It probably is the same when the BBS is first created and that's why it works here but if an add is removed in front of this 'test add' the ref nr will be one off. We're basically just poking the code a bit.

The player's mission list

Once the player has negotiated with your form, there might well be a mission in play. It could be a delivery, an assassination, a rush to tell somebody not to leave because so-and-so loves them... the possibilities are limited only by your creativity. The player needs a way to keep track of all the missions that they have agreed to undertake. Pioneer provides this through the player's mission screen, which they can access at any time using the F3 button, and looking at the missions tab. The content of this screen is controlled by some methods on the Player object, which can always be found at Game.player, and which inherits from Ship and Body. Missions are added to the screen using the Mission.New() method. It takes a table of info, and returns an integer reference to that mission, which should be stored so that it can be updated or removed later. Below follows a typical use case from the codedoc.

Create a new mission and add it to the player’s mission list while retrieving the reference number:

ref = Mission.New({
    'type'      = 'Delivery', -- Must be a translatable token!
    'client'    = Character.New(),
    'due'       = Game.time + 3*24*60*60,       -- three days
    'reward'    = 123.45,
    'location'  = SystemPath:New(0,0,0,0,16),   -- Mars High, Sol
    'status'    = 'ACTIVE',
})

In practice, it might look more like this:

local Character = require 'Character'
local Event = require 'Event'
local Game = require 'Game'
local Mission = require 'Mission'

local missions = {}

local onShipDocked = function (ship)
    if ship:IsPlayer() then

        table.insert(missions, Mission.New({
            type = "Taxi",
            client = Character.New(),
            due = Game.time + 600, -- ten minutes' time
            reward = 10,
            location = Game.player.frameBody.path, -- here, basically
            status = 'ACTIVE'
        }))
    end
end

Event.Register("onShipDocked", onShipDocked)

I don't recommend using Game.player.frameBody.path here. I'm only using it because it always returns something, whether docked or not. A real mission would probably use a space station here. For this demonstration we've generated 'Taxi' missions and they will be recognized by the 'Taxi' module as such.

This creates a mission visible on the mission screen:

Missionlist3.png

The next example is extended with a much scaled down version of the onShipDocked function from the DeliverPackage module. Since these missions are recognized by a module in Pioneer already ('Taxi') they will 'probably' be handled at a later stage if we fail to remove them with this test mission. If you generate a new mission type you must also handle removing it or the mission will remain in the game forever. There is no automatic logic, and no automatic removal. Your script must keep track of them.

local Character = require 'Character'
local Comms = require 'Comms'
local Event = require 'Event'
local Game = require 'Game'
local Mission = require 'Mission'
local Player = require 'Player'
local Timer = require 'Timer'

local missions = {}

local onShipDocked = function (ship, station)
    if ship:IsPlayer() then

    -- On docking, starting a new game, we create
    -- two npc's and book them on a taxi mission. 
        table.insert(missions, Mission.New({
            type = "Taxi",
            client = Character.New(),
            due = Game.time + 10, -- ten seconds
            reward = 10,
            location = Game.player.frameBody.path, -- here, basically
            status = 'ACTIVE'
        }))
        table.insert(missions, Mission.New({
            type = "Taxi",
            client = Character.New(),
            due = Game.time + 20, -- 20 seconds
            reward = 5000,
            location = Game.player.frameBody.path, -- here, basically
            status = 'ACTIVE'
        }))
    end

    -- Magically, without moving, we've arrived at the destination
    -- after 15 seconds and check in with our passengers.
    Timer:CallAt(Game.time+15, function ()  -- 15 seconds timer
        for ref,mission in pairs(missions) do
            if Game.time > mission.due then
                mission.status = 'FAILED'
                Comms.ImportantMessage('You suck dude! You suck like REALLY MUCH!', mission.client.name)
            else
                Comms.ImportantMessage('Thanks for the ride!', mission.client.name)
                mission.status = "COMPLETED"
                Game.player:AddMoney(mission.reward)
            end
            mission:Remove()
            missions[ref] = nil
        end
    end)
end

Event.Register("onShipDocked", onShipDocked)

Missions Created:

Missionlist1.png

1 Mission failed and 1 mission completed:

Missionlist2.png

More info? If you look to the right on the mission list there is a button named 'More info' which will let you see more specific details of your mission. This will need to be specified in the mission script itself so if you press this button in the example above Pioneer will crash. The common name for the function you need to write in your script is buildMissionDescription. Here is what it looks like in DeliverPackage.lua.

Maintaining immersion

Fictionally, of course, this bulletin board is visible to any and all ships that dock at the space station, not just the player. It is important that bulletin board missions are not all scaled to the capabilities of the player. Delivery missions with unreasonable deadlines should not be ruled out. Neither should cargo missions requiring much more cargo space than the player's ship has, or combat missions for which the player is completely unqualified.

These missions should deal with the player gracefully; either allowing them to fail, and providing consequences, or preventing them from being given the mission.

It's also important, if your script serves many instances of a mission, to periodically clear away bulletin board adverts and place new ones. Not just those with obvious time constraints, but any others; the assumption that the player should make is that perhaps some other character has taken these missions.

Where to go from here

To see simple example of BBS interaction used in the game, look in data/modules/DonateToCranks/DonateToCranks.lua, and its accompanying language file data/lang/modules-donatetocranks/en.json, and for an actual mission, look in data/modules/DeliverPackage/DeliverPackage.lua.