[DLL/LUA] Durational Custom Missions

whoward69

DLL Minion
Joined
May 30, 2011
Messages
8,725
Location
Near Portsmouth, UK
NOTE: Custom missions require a modded DLL

Missions are either instantaneous (eg, culture bomb, build an academy, create a great work, etc) or durational (eg build an improvement).

How to create an instantaneous custom mission using the event hooks provided by my DLL or the CP DLL are covered in post #1104 here, and this post assume you have read that one and have successfully created an instantaneous custom mission.

This post describes how to use the same set of events to create a custom mission that takes a number or turns to complete, eg a druid sanctifying a grove, or a prospector fossicking.

We're going to add a custom mission to the Scout unit that permits them to fossick for a set number of turns on Gems, Gold, Silver or Copper and add to the player's treasury.

Firstly, we need to create the custom mission database entry, which is the same as a new instantaneous custom mission, so see the description of each field/column in post #1104
Code:
<GameData>
  <Missions>
    <Row>
      <Type>MISSION_FOSSICK</Type>
      <Description>TXT_KEY_MISSION_FOSSICK</Description>
      <Help>TXT_KEY_MISSION_FOSSICK_HELP</Help>
      <DisabledHelp>TXT_KEY_MISSION_FOSSICK_HELP_DISABLED</DisabledHelp>
      <IconAtlas>UNIT_ACTION_ATLAS</IconAtlas>
      <IconIndex>22</IconIndex>
      <OrderPriority>201</OrderPriority>
      <Visible>1</Visible>
      <EntityEventType>ENTITY_EVENT_GREAT_EVENT</EntityEventType>
      <Time>25</Time>
    </Row>
  </Missions>

  <Language_en_US>
    <Row Tag="TXT_KEY_MISSION_FOSSICK">
      <Text>Fossick</Text>
    </Row>
    <Row Tag="TXT_KEY_MISSION_FOSSICK_HELP">
      <Text>Search for easily collectable resources</Text>
    </Row>
    <Row Tag="TXT_KEY_MISSION_FOSSICK_HELP_DISABLED">
      <Text>The Scout must be on a resource to fossick</Text>
    </Row>
  </Language_en_US>
</GameData>

And we need to enable the events within the DLL
Code:
UPDATE CustomModOptions SET Value=1 WHERE Name='EVENTS_CUSTOM_MISSIONS';

Finally we need to hook three of the new custom mission events - CustomMissionPossible, CustomMissionStart and CustomMissionDoStep.

The solution presented below is slightly more complex than it needs to be as it was written to be a flexible template for others to use as the basis of their own custom missions.
Using the template, we only need to worry about four "needs to know" functions and three "do this" functions.

The template needs to know
1) What is the custom mission
2) How long is the custom mission
3) Can a unit ever perform the custom mission
4) Can a unit perform the custom mission at this plot

and the template calls "do this" functions
1) When the mission starts
2) Every turn the mission is active
3) When the mission ends

So some setup statements that will be used by the "needs to know" functions
Code:
--
-- Data that defines the parameters of the custom mission
--
local iMissionType = GameInfoTypes.MISSION_FOSSICK
local iMissionUnit = GameInfoTypes.UNIT_SCOUT
local iMissionDuration = 3

local fossickResources = {
  [GameInfoTypes.RESOURCE_GEMS] = 5,
  [GameInfoTypes.RESOURCE_GOLD] = 3,
  [GameInfoTypes.RESOURCE_SILVER] = 2,
  [GameInfoTypes.RESOURCE_COPPER] = 1
}

and the four "needs to know" functions themselves
Code:
--
-- What is the custom mission we are interested in?
--
function GetMissionType()
  -- While it would be possible for one framework instance to support many (related) missions, that's advanced stuff
  -- so we "keep it simple, stupid", and just support a single custom mission per framework instance
  return iMissionType
end

--
-- How long is the mission for this unit?
--
function GetMissionDuration(pUnit, iCurrentTurn, iStartTurn)
  -- Duration could be a function of the units type (eg a neutral wizard may take longer to summon undead than an evil one)
  -- or their xp/level (eg a high wizard may be able to summon undead faster than a novice wizard)
  -- but we'll keep it simple and just go for a fixed duration
  return iMissionDuration
end

--
-- Can pUnit EVER perform the mission?
--
function IsMissionUnit(pUnit)
  -- This should be a very general check, typically for the correct unit type/class
  -- More complex checks (health, hostile units, etc) should be handled in IsMissionPlot()
  return (pUnit:GetUnitType() == iMissionUnit)
end

--
-- Can pUnit perform the mission right NOW on this plot?
--
function IsMissionPlot(pUnit, iPlotX, iPlotY)
  -- Simply check the resource on the plot,
  -- but this could also be dependant on the unit's health, xp, level, promotions, proximity of cities, hostile units, etc
  return fossickResources[Map.GetPlot(iPlotX, iPlotY):GetResourceType()]
end

While there are three "do this" functions, typically only DoMissionComplete() will do any work. However, it is possible that some scenarios will have a use for the other two "do this" functions, so the template provides them
Code:
--
-- DoMissionStart()
--
-- Called once as the mission starts
--
-- Typically does nothing.  After this method is called, the unit will not be able to move
-- Do not perform "work" in this method, as if the mission is aborted (unit is moved, killed, whatever),
-- there is no way to undo the work.
-- Could be used to alert locals to an undesirable action (eg summoning undead) and summon "pitch fork wielding yokels" towards the unit's plot
--
function DoMissionStart(pUnit, iCurrentTurn, iStartTurn)
  print(string.format("DoMissionStart(%i)", iCurrentTurn))
end

--
-- DoMissionStep()
--
-- Called once per turn as the mission progresses
--
-- Typically does nothing
-- Could be used to keep directing the yokels towards the actioning unit's plot
--
function DoMissionStep(pUnit, iCurrentTurn, iStartTurn)
  print(string.format("DoMissionStep(%i)", iCurrentTurn))
end

--
-- DoMissionComplete()
--
-- Called once when the mission is completed
--
-- Typically does all the "work", will be called at the start of the turn after the mission completes,
-- so the unit will still be able to move after completing the work
--
function DoMissionComplete(pUnit, iCurrentTurn, iStartTurn)
  print(string.format("DoMissionComplete(%i)", iCurrentTurn))
  
  -- Give the player some gold
  local iGold = (Map.Rand(3, "Gold from fossicking") + 2) * fossickResources[pUnit:GetPlot():GetResourceType()]
  print(string.format("Gold: %i", iGold))
  Players[pUnit:GetOwner()]:ChangeGold(iGold)
end

The bulk of the remaining code is the framework that handles the three custom mission events
Code:
--
-- Nothing below here should need changing
-- (with the possible exception of the Get/SetMissionValue() functions, if persistent data is required)
--
local iCustomMission = GetMissionType()

-- Constants for the event handlers to return
local CUSTOM_MISSION_NO_ACTION = 0
local CUSTOM_MISSION_ACTION    = 1
local CUSTOM_MISSION_DONE      = 2

--
-- Handler for the CustomMissionPossible event
-- Can the specified unit perform the custom mission?
--
function OnCustomMissionPossible(iPlayer, iUnit, iMission, iData1, iData2, _, _, iPlotX, iPlotY, bTestVisible)
  -- Is this the custom mission we are supervising
  if (iMission == iCustomMission) then
    local pUnit = Players[iPlayer]:GetUnitByID(iUnit)

    -- Can the specified unit ever perform this mission    
    if (IsMissionUnit(pUnit) and pUnit:CanMove()) then
      local iCurrentTurn = Game.GetGameTurn()
      
      -- If the mission has been aborted, reset the temp data
      if (pUnit:GetActivityType() ~= ActivityTypes.ACTIVITY_MISSION) then
        SetMissionValue(iPlayer, iUnit, 'startTurn', iCurrentTurn)
      end

      -- If the mission could start this turn ...      
      if (GetMissionValue(iPlayer, iUnit, 'startTurn') == iCurrentTurn) then
        -- ... can the unit perform the mission right here, right now
        if (not IsMissionPlot(pUnit, iPlotX, iPlotY)) then
          return bTestVisible
        end

        return true
      end
    end
  end
  
  return false
end
GameEvents.CustomMissionPossible.Add(OnCustomMissionPossible)

--
-- Handler for the CustomMissionStart event
-- Start the custom mission for the specified unit
--
function OnCustomMissionStart(iPlayer, iUnit, iMission, iData1, iData2, iFlags, iTurn)
  -- Is this the custom mission we are supervising
  if (iMission == iCustomMission) then
    local pUnit = Players[iPlayer]:GetUnitByID(iUnit)

    -- Can the specified unit ever perform this mission    
    if (IsMissionUnit(pUnit)) then
      -- Initialise the mission temp data
      SetMissionValue(iPlayer, iUnit, 'startTurn', iTurn)
      SetMissionValue(iPlayer, iUnit, 'stepTurn', iTurn)

      -- Call the mission start function
      DoMissionStart(pUnit, iTurn, iTurn, iData1, iData2, iFlags)
      
      local iDuration = GetMissionDuration(pUnit, iCurrentTurn, iTurn, iData1, iData2, iFlags)
      if (pUnit:HasMoved()) then
        -- Unit has used part of the turn to get here, so mission is next N turns
        SetMissionValue(iPlayer, iUnit, 'duration', iDuration)
      else
        -- Unit did not move to get here, so we count this turn as the first turn of the mission
        SetMissionValue(iPlayer, iUnit, 'duration', iDuration-1)
        DoMissionStep(pUnit, iTurn, iTurn, iData1, iData2, iFlags)
      end

      -- Just stand around doing nothing (as animating units is a real PITA)
      pUnit:FinishMoves()

      return CUSTOM_MISSION_ACTION
    end
  end

  return CUSTOM_MISSION_NO_ACTION
end
GameEvents.CustomMissionStart.Add(OnCustomMissionStart)

--
-- Handler for the CustomMissionDoStep event
-- Update the custom mission for the specified unit
--
function OnCustomMissionDoStep(iPlayer, iUnit, iMission, iData1, iData2, iFlags, iTurn)
  -- Is this the custom mission we are supervising
  if (iMission == iCustomMission) then
    local pUnit = Players[iPlayer]:GetUnitByID(iUnit)

    -- Can the specified unit ever perform this mission    
    if (IsMissionUnit(pUnit)) then
      local iCurrentTurn = Game.GetGameTurn()
      
      -- Is this the first time this turn for this event?
      if (not (GetMissionValue(iPlayer, iUnit, 'stepTurn') == iCurrentTurn)) then
          -- Update the mission temp data
        SetMissionValue(iPlayer, iUnit, 'stepTurn', iCurrentTurn)

        -- Get the remaining turns for this mission
        local iTurnsLeft = GetMissionValue(iPlayer, iUnit, 'duration')
        if (iTurnsLeft == nil) then
          -- This happens if the game was saved while a mission was in progress.
          -- We only store the remaining turns to benefit a unit that didn't move before starting the mission
          -- So we can recalculate the missing value, which will "short-change" a unit that didn't move, 
          -- but it's preferable to the complexity of persisting (in a generic manner) the data
          -- If this is a problem, rewrite Get/SetMissionValue() to use a persistent data store
          iTurnsLeft = GetMissionDuration(pUnit, iCurrentTurn, iTurn, iData1, iData2, iFlags) - ((iCurrentTurn-1) - iTurn)
        end
        
        -- Did the mission complete this turn ...
        if (iTurnsLeft > 0) then
          -- ... no, so call the mission step function
          DoMissionStep(pUnit, iCurrentTurn, iTurn, iData1, iData2, iFlags)
          SetMissionValue(iPlayer, iUnit, 'duration', iTurnsLeft-1)
          return CUSTOM_MISSION_ACTION
        else
          -- ... yes, so call the mission complete function
          DoMissionComplete(pUnit, iCurrentTurn, iTurn, iData1, iData2, iFlags)
          return CUSTOM_MISSION_DONE
        end
      end
    end
  end

  return CUSTOM_MISSION_NO_ACTION
end
GameEvents.CustomMissionDoStep.Add(OnCustomMissionDoStep)

The framework needs to store some data about in progress custom missions.

This is to overcome a short-coming in the existing event parameters (I didn't want to change the C++ code in the DLL as that would impact the CP DLL)
and also to handle the special case when the unit is already on the plot and doesn't need to move onto it

Code:
--
-- For a mission that takes 3 turns to complete, you will see (for example)
--   Turn 2 - player moves unit onto plot and clicks the mission button - DoMissionStart(2)
--   Turn 3 - (1st full turn of mission) unit is not cycled to, after End Turn is clicked - DoMissionStep(3)
--   Turn 4 - (2nd turn of mission) unit is not cycled to, after End Turn is clicked - DoMissionStep(4)
--   Turn 5 - (3rd turn of mission) unit is not cycled to, after End Turn is clicked - DoMissionStep(5)
--   Turn 6 - DoMissionComplete(6) - unit is cycled to and may move this turn
-- OR
--   Turn 2 - unit starts turn on plot and player clicks the mission button - DoMissionStart(2) and DoMissionStep(2)
--   Turn 3 - (2nd turn of mission) unit is not cycled to, after End Turn is clicked - DoMissionStep(3)
--   Turn 4 - (3rd turn of mission) unit is not cycled to, after End Turn is clicked - DoMissionStep(4)
--   Turn 5 - DoMissionComplete(5) - unit is cycled to and may move this turn
--

The following code does this in a non-persistent way.
There is one situation, see comment above in the CustomMissionDoStep() handler, where this may not be acceptable. In which case
you'll need to rewrite the Get/SetMissionValue() functions to persist the data into the modding database.
Code:
--
-- Temporary store for mission data
--
-- See comment in OnCustomMissionDoStep, you may want to make this persistent
--
local missionData = {}

function GetMissionData(iPlayer, iUnit)
  if (iPlayer) then
    if (missionData[iPlayer] == nil) then
      missionData[iPlayer] = {}
    end

    if (iUnit) then    
      if (missionData[iPlayer][iUnit] == nil) then
        missionData[iPlayer][iUnit] = {}
      end
      
      return missionData[iPlayer][iUnit]
    else
      return missionData[iPlayer]
    end
  else
    return missionData
  end
end

function GetMissionValue(iPlayer, iUnit, sKey)
  return GetMissionData(iPlayer, iUnit)[sKey]
end

function SetMissionValue(iPlayer, iUnit, sKey, value)
  GetMissionData(iPlayer, iUnit)[sKey] = value
end

Complete Lua code
Spoiler :
Code:
--
-- For a mission that takes 3 turns to complete, you will see (for example)
--   Turn 2 - player moves unit onto plot and clicks the mission button - DoMissionStart(2)
--   Turn 3 - (1st full turn of mission) unit is not cycled, after End Turn is clicked - DoMissionStep(3)
--   Turn 4 - (2nd turn of mission) unit is not cycled, after End Turn is clicked - DoMissionStep(4)
--   Turn 5 - (3rd turn of mission) unit is not cycled, after End Turn is clicked - DoMissionStep(5)
--   Turn 6 - DoMissionComplete(6) - unit is cycled to and may move this turn
-- OR
--   Turn 2 - unit starts turn on plot and player clicks the mission button - DoMissionStart(2) and DoMissionStep(2)
--   Turn 3 - (2nd turn of mission) unit is not cycled, after End Turn is clicked - DoMissionStep(3)
--   Turn 4 - (3rd turn of mission) unit is not cycled, after End Turn is clicked - DoMissionStep(4)
--   Turn 5 - DoMissionComplete(5) - unit is cycled to and may move this turn
--

--
-- Data that defines the parameters of the custom mission
--
local iMissionType = GameInfoTypes.MISSION_FOSSICK
local iMissionUnit = GameInfoTypes.UNIT_SCOUT
local iMissionDuration = 3

local fossickResources = {
  [GameInfoTypes.RESOURCE_GEMS] = 5,
  [GameInfoTypes.RESOURCE_GOLD] = 3,
  [GameInfoTypes.RESOURCE_SILVER] = 2,
  [GameInfoTypes.RESOURCE_COPPER] = 1
}

--
-- What is the custom mission we are interested in?
--
function GetMissionType()
  return iMissionType
end

--
-- How long is the mission for this unit?
--
function GetMissionDuration(pUnit, iCurrentTurn, iStartTurn)
  return iMissionDuration
end

--
-- Can pUnit EVER perform the mission?
--
function IsMissionUnit(pUnit)
  -- This should be a very general check, typically for the correct unit type/class
  -- More complex checks (health, hostile units, etc) should be handled in IsMissionPlot()
  return (pUnit:GetUnitType() == iMissionUnit)
end

--
-- Can pUnit perform the mission right NOW on this plot?
--
function IsMissionPlot(pUnit, iPlotX, iPlotY)
  -- Simply check the resource on the plot,
  -- but this could also be dependant on the unit's health, xp, level, promotions, proximity of cities, hostile units, etc
  return fossickResources[Map.GetPlot(iPlotX, iPlotY):GetResourceType()]
end

--
-- DoMissionStart()
--
-- Called once as the mission starts
--
-- Typically does nothing.  After this method is called, the unit will not be able to move
-- Do not perform "work" in this method, as if the mission is aborted (unit is moved, killed, whatever),
-- there is no way to undo the work
-- Could be used to alert locals to an undesirable action (eg summoning undead) and summon "pitch fork wielding yokels" towards the unit's plot
--
function DoMissionStart(pUnit, iCurrentTurn, iStartTurn)
  print(string.format("DoMissionStart(%i)", iCurrentTurn))
end

--
-- DoMissionStep()
--
-- Called once per turn as the mission progresses
--
-- Typically does nothing
-- Could be used to keep directing the yokels towards the actioning unit's plot
--
function DoMissionStep(pUnit, iCurrentTurn, iStartTurn)
  print(string.format("DoMissionStep(%i)", iCurrentTurn))
end

--
-- DoMissionComplete()
--
-- Called once when the mission is completed
--
-- Typically does all the "work", will be called at the start of the turn the mission completes in,
-- so the unit will still be able to move after completing the work
--
function DoMissionComplete(pUnit, iCurrentTurn, iStartTurn)
  print(string.format("DoMissionComplete(%i)", iCurrentTurn))
  
  -- Give the player some gold
  local iGold = (Map.Rand(3, "Gold from fossicking") + 2) * fossickResources[pUnit:GetPlot():GetResourceType()]
  print(string.format("Gold: %i", iGold))
  Players[pUnit:GetOwner()]:ChangeGold(iGold)
end


--
-- Nothing below here should need changing
-- (with the possible exception of the Get/SetMissionValue() functions, if persistent data is required)
--
local iCustomMission = GetMissionType()

-- Constants for the event handlers to return
local CUSTOM_MISSION_NO_ACTION = 0
local CUSTOM_MISSION_ACTION    = 1
local CUSTOM_MISSION_DONE      = 2

--
-- Handler for the CustomMissionPossible event
--
function OnCustomMissionPossible(iPlayer, iUnit, iMission, iData1, iData2, _, _, iPlotX, iPlotY, bTestVisible)
  -- Is this the custom mission we are supervising
  if (iMission == iCustomMission) then
    local pUnit = Players[iPlayer]:GetUnitByID(iUnit)

    -- Can the specified unit ever perform this mission    
    if (IsMissionUnit(pUnit) and pUnit:CanMove()) then
      local iCurrentTurn = Game.GetGameTurn()
      
      -- If the mission has been aborted, reset the temp data
      if (pUnit:GetActivityType() ~= ActivityTypes.ACTIVITY_MISSION) then
        SetMissionValue(iPlayer, iUnit, 'startTurn', iCurrentTurn)
      end

      -- If the mission could start this turn ...      
      if (GetMissionValue(iPlayer, iUnit, 'startTurn') == iCurrentTurn) then
        -- ... can the unit perform the mission right here, right now
        if (not IsMissionPlot(pUnit, iPlotX, iPlotY)) then
          return bTestVisible
        end

        return true
      end
    end
  end
  
  return false
end
GameEvents.CustomMissionPossible.Add(OnCustomMissionPossible)

--
-- Handler for the CustomMissionStart event
--
function OnCustomMissionStart(iPlayer, iUnit, iMission, iData1, iData2, iFlags, iTurn)
  -- Is this the custom mission we are supervising
  if (iMission == iCustomMission) then
    local pUnit = Players[iPlayer]:GetUnitByID(iUnit)

    -- Can the specified unit ever perform this mission    
    if (IsMissionUnit(pUnit)) then
      -- Initialise the mission temp data
      SetMissionValue(iPlayer, iUnit, 'startTurn', iTurn)
      SetMissionValue(iPlayer, iUnit, 'stepTurn', iTurn)

      -- Call the mission start function
      DoMissionStart(pUnit, iTurn, iTurn, iData1, iData2, iFlags)
      
      local iDuration = GetMissionDuration(pUnit, iCurrentTurn, iTurn, iData1, iData2, iFlags)
      if (pUnit:HasMoved()) then
        -- Unit has used part of the turn to get here, so mission is next N turns
        SetMissionValue(iPlayer, iUnit, 'duration', iDuration)
      else
        -- Unit did not move to get here, so we count this turn as the first turn of the mission
        SetMissionValue(iPlayer, iUnit, 'duration', iDuration-1)
        DoMissionStep(pUnit, iTurn, iTurn, iData1, iData2, iFlags)
      end

      -- Just stand around doing nothing (as animating units is a real PITA)
      pUnit:FinishMoves()

      return CUSTOM_MISSION_ACTION
    end
  end

  return CUSTOM_MISSION_NO_ACTION
end
GameEvents.CustomMissionStart.Add(OnCustomMissionStart)

--
-- Handler for the CustomMissionDoStep event
--
function OnCustomMissionDoStep(iPlayer, iUnit, iMission, iData1, iData2, iFlags, iTurn)
  -- Is this the custom mission we are supervising
  if (iMission == iCustomMission) then
    local pUnit = Players[iPlayer]:GetUnitByID(iUnit)

    -- Can the specified unit ever perform this mission    
    if (IsMissionUnit(pUnit)) then
      local iCurrentTurn = Game.GetGameTurn()
      
      -- Is this the first time this turn for this event?
      if (not (GetMissionValue(iPlayer, iUnit, 'stepTurn') == iCurrentTurn)) then
          -- Update the mission temp data
        SetMissionValue(iPlayer, iUnit, 'stepTurn', iCurrentTurn)

        -- Get the remaining turns for this mission
        local iTurnsLeft = GetMissionValue(iPlayer, iUnit, 'duration')
        if (iTurnsLeft == nil) then
          -- This happens if the game was saved while a mission was in progress.
          -- We only store the remaining turns to benefit a unit that didn't move before starting the mission
          -- So we can recalculate the missing value, which will "short-change" a unit that didn't move, 
          -- but it's preferable to the complexity of persisting (in a generic manner) the data
          -- If this is a problem, rewrite Get/SetMissionValue() to use a persistent data store
          iTurnsLeft = GetMissionDuration(pUnit, iCurrentTurn, iTurn, iData1, iData2, iFlags) - ((iCurrentTurn-1) - iTurn)
        end
        
        -- Did the mission complete this turn ...
        if (iTurnsLeft > 0) then
          -- ... no, so call the mission step function
          DoMissionStep(pUnit, iCurrentTurn, iTurn, iData1, iData2, iFlags)
          SetMissionValue(iPlayer, iUnit, 'duration', iTurnsLeft-1)
          return CUSTOM_MISSION_ACTION
        else
          -- ... yes, so call the mission complete function
          DoMissionComplete(pUnit, iCurrentTurn, iTurn, iData1, iData2, iFlags)
          return CUSTOM_MISSION_DONE
        end
      end
    end
  end

  return CUSTOM_MISSION_NO_ACTION
end
GameEvents.CustomMissionDoStep.Add(OnCustomMissionDoStep)


--
-- Temporary store for mission data
--
-- See comment in OnCustomMissionDoStep, you may want to make this persistent
--
local missionData = {}

function GetMissionData(iPlayer, iUnit)
  if (iPlayer) then
    if (missionData[iPlayer] == nil) then
      missionData[iPlayer] = {}
    end

    if (iUnit) then    
      if (missionData[iPlayer][iUnit] == nil) then
        missionData[iPlayer][iUnit] = {}
      end
      
      return missionData[iPlayer][iUnit]
    else
      return missionData[iPlayer]
    end
  else
    return missionData
  end
end

function GetMissionValue(iPlayer, iUnit, sKey)
  return GetMissionData(iPlayer, iUnit)[sKey]
end

function SetMissionValue(iPlayer, iUnit, sKey, value)
  GetMissionData(iPlayer, iUnit)[sKey] = value
end
 
Back
Top Bottom