1. We have added a Gift Upgrades feature that allows you to gift an account upgrade to another member, just in time for the holiday season. You can see the gift option when going to the Account Upgrades screen, or on any user profile screen.
    Dismiss Notice

City State Quests with XML and Lua

Discussion in 'Civ5 - SDK / LUA' started by whoward69, Apr 17, 2016.

  1. whoward69

    whoward69 DLL Minion

    Joined:
    May 30, 2011
    Messages:
    8,518
    Location:
    Near Portsmouth, UK
    (Requires v81 or later of "DLL - Various Mod Components", and probably of more interest to Total Conversion modders.)


    Background

    City State quests (clear a barbarian camp, bully another city state, discover the most techs, etc) are all hard-coded in the DLL.
    Adding one is not possible (without modding the DLL), which for a Total Conversion means you're stuck with the standard quests, which may not be relevant for the milieu.
    Custom quests, such as rescuing a maiden held captive by a dragon, finding all the "keys" to a magic box, or just sending a bard to entertain the king, are either not possible, or have to be faked with a lot of Lua using the standard quests as a framework.

    V81 of "DLL - Various Mod Components" makes all of these (and many other) quests possible - via XML/SQL and Lua GameEvents.

    Overview

    A City State quest comprises
    1. Conditions that have to be met for the quest to be a candidate for handing out
    2. Biases that determine which of the City State types (mercantile, religious, etc) and personalities (friendly, hostile, etc) are most/least likely to hand out the quest
    3. Static information about the quest (duration, friendship gained, minimum number of participants, etc)
    4. Dynamic information about the active quest (location of the camp to be cleared, other City State to be bullied, etc)
    5. Conditions for the quest (once active) to be completed (won), cancelled (lost) or revoked (eg if the CS handing out the quest is bullied by the player)
    6. Messages to display when one of the above conditions are met
    7. An icon and tooltip to display for the quest (used only by the UI)

    This information is now obtained by the quest logic in the DLL either from the Quests database table or via GameEvents.

    Spoiler Quests table and GameEvents :

    The definition of the new Quests table is
    Code:
    	<Table name="Quests">
    		<Column name="ID" type="integer" primarykey="true" autoincrement="true"/>
    		<Column name="Type" type="text" unique="true" notnull="true"/>
    
    		<!-- Set to true if quest is backed by C++ code in the DLL -->
    		<Column name="Internal" type="boolean" default="false"/>
    
    		<!-- Set to false to disable this type of event -->
    		<Column name="Enabled" type="boolean" default="true"/>
    		<!-- Game option (if any) to disable this event for, eg GAMEOPTION_NO_POLICIES, GAMEOPTION_NO_RELIGION, GAMEOPTION_NO_SCIENCE -->
    		<Column name="DisabledOnOption" type="text" default="NULL"/>
    
    		<!-- Set to true for global (one quest applies to all players), false for personal (one quest per player) quests -->
    		<Column name="Global" type="boolean" default="false"/>
    
    		<!-- Set to false to override the default behaviour of revoking the quest in the player bullies the CS -->
    		<Column name="RevokeOnBully" type="boolean" default="true"/>
    
    		<!-- Set to true if this is a contest type quest (eg most culture, science, etc) - must also hook the QuestContestValue event -->
    		<Column name="Contest" type="boolean" default="false"/>
    
    		<!-- Mininimum number of majors the CS must have met before the quest can be considered, typically used for contest quests -->
    		<Column name="MinPlayers" type="integer" default="1"/>
    
    		<!-- Duration in turns this quest runs for; will be automatically adjusted for non-standard game speeds -->
    		<Column name="Duration" type="integer" default="-1"/>
    
    		<!-- Friendship/Influence gained on completion of the quest -->
    		<Column name="Friendship" type="integer" default="0"/>
    
    		<!-- Notification text pairs, set to NULL to force a QuestSendNotification event -->
    		<Column name="StartSummary" type="text" default="NULL" reference="Language_en_US(Tag)"/>
    		<Column name="StartMessage" type="text" default="NULL" reference="Language_en_US(Tag)"/>
    		<Column name="FinishSummary" type="text" default="NULL" reference="Language_en_US(Tag)"/>
    		<Column name="FinishMessage" type="text" default="NULL" reference="Language_en_US(Tag)"/>
    		<Column name="CancelSummary" type="text" default="NULL" reference="Language_en_US(Tag)"/>
    		<Column name="CancelMessage" type="text" default="NULL" reference="Language_en_US(Tag)"/>
    
    		<!-- Personality biases, only one of these will ever apply -->
    		<Column name="BiasFriendly" type="integer" default="100"/>
    		<Column name="BiasHostile" type="integer" default="100"/>
    		<Column name="BiasNeutral" type="integer" default="100"/>
    		<Column name="BiasIrrational" type="integer" default="100"/>
    
    		<!-- Trait biases, only one of these will ever apply -->
    		<Column name="BiasMaritime" type="integer" default="100"/>
    		<Column name="BiasMercantile" type="integer" default="100"/>
    		<Column name="BiasCultured" type="integer" default="100"/>
    		<Column name="BiasMilitaristic" type="integer" default="100"/>
    		<Column name="BiasReligious" type="integer" default="100"/>
    
    		<!-- UI related data, not processed by the DLL in any way -->
    		<!-- The priority for displaying the quest,
    		     there are "holes" in the standard quest sequence to permit custom quests to be inserted if needed -->
    		<Column name="Priority" type="integer" default="100"/>
    		<!-- The icon associated with the quest, or the function name to determine the icon -->
    		<Column name="Icon" type="text" default="[ICON_TEAM_1]"/>
    		<!-- The TXT_KEY associated with the quest (can use {1_TargetName:textkey} if there is a target for the quest (bully, find, etc)),
    		     or the function name to determine the tooltip -->
    		<Column name="Tooltip" type="text" default="TXT_KEY_CITY_STATE_QUEST_GENERIC_FORMAL"/>
    	</Table>
    
    and the new GameEvents are
    Code:
        <!-- Events sent by City State quests (v81) -->
        <!--   ASSUMPTION: There is only one active quest of any given MinorCivQuestTypes per major, that is, (iPlayer, iCS, iQuest) is unique -->
        <!--   GameEvents.QuestIsAvailable.Add(function(iPlayer, iCS, iQuest, bNewQuest, iData1, iData2) return false end) -->
        <!--   GameEvents.QuestIsCompleted.Add(function(iPlayer, iCS, iQuest, bLastTurn) return false end) -->
        <!--   GameEvents.QuestIsRevoked.Add(function(iPlayer, iCS, iQuest) return false end) -->
        <!--   GameEvents.QuestIsExpired.Add(function(iPlayer, iCS, iQuest) return false end) -->
        <!--   GameEvents.QuestStart.Add(function(iPlayer, iCS, iQuest, bNewQuest, iStartTurn, iData1, iData2) end) -->
        <!--   GameEvents.QuestGetData.Add(function(iPlayer, iCS, iQuest, bData1) return 0 end) -->
        <!--   GameEvents.QuestSendNotification.Add(function(iPlayer, iCS, iQuest, iStartTurn, iEndTurn, iData1, iData2, bStarted, bFinished, sNames) end) -->
        <!--   GameEvents.QuestContestValue.Add(function(iPlayer, iCS, iQuest) return 0 end) -->
    
    Additional quest related API methods are also available
    Code:
    	LUAAPIEXTN(DoMinorCivStartQuestForPlayer, void, iMajor, iQuest);
    	LUAAPIEXTN(GetQuestTurnsDuration, int, iMajor, iQuest);
    	LUAAPIEXTN(IsEverBulliedByMajor, bool, iPlayer);
    	LUAAPIEXTN(IsRecentlyBulliedByMajor, bool, iPlayer);
    	LUAAPIEXTN(AddQuestNotification, void, iCS, sMessage, sSummary, iPlotX, iPlotY, bNewQuest);
    
    And also an API to permit control of the AIs economic and military strategy
    Code:
    	LUAAPIEXTN(GetActiveEconomicStrategies, table);
    	LUAAPIEXTN(IsActiveEconomicStrategy, bool, iStrategy);
    	LUAAPIEXTN(ActivateEconomicStrategy, void, iStrategy);
    	LUAAPIEXTN(DeactivateEconomicStrategy, void, iStrategy);
    
    	LUAAPIEXTN(GetActiveMilitaryStrategies, table);
    	LUAAPIEXTN(IsActiveMilitaryStrategy, bool, iStrategy);
    	LUAAPIEXTN(ActivateMilitaryStrategy, void, iStrategy);
    	LUAAPIEXTN(DeactivateMilitaryStrategy, void, iStrategy);
    
    We'll cover (most of) these in detail in the following three example quests.


    Types of Quests

    There are three categories of City State quests - Global (eg build a World Wonder), Global Race (eg gain the most techs) and Personal (eg bully another City State or discover a Natural Wonder).
    Not all database columns and GameEvents are relevant to each type of quest, but every custom quest will have an entry in the Quests table and implement one or more quest GameEvent handlers.

    Example 1: Global Quest - Circumnavigate the Globe

    The aim of this quest is to be the first to circumnavigate the globe.

    The Quests database table entry is
    Code:
    	<Quests>
    		<Row>
    			<Type>MINOR_CIV_QUEST_CIRCUMNAVIGATE</Type>
    			<Global>true</Global>
    			<MinPlayers>2</MinPlayers>
    			<RevokeOnBully>false</RevokeOnBully>
    			<Friendship>50</Friendship>
    
    			<BiasMaritime>300</BiasMaritime>
    			<BiasMercantile>200</BiasMercantile>
    
    			<!-- While we could easily use simple TXT_KEYs here, we'll use the Lua GameEvent (as this is an example!)
    			<StartSummary>TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_START_S</StartSummary>
    			<StartMessage>TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_START</StartMessage -->
    
    			<FinishSummary>TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_FINISH_S</FinishSummary>
    			<FinishMessage>TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_FINISH</FinishMessage>
    			<CancelSummary>TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_CANCEL_S</CancelSummary>
    			<CancelMessage>TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_CANCEL</CancelMessage>
    
    			<Priority>150</Priority>
    			<!-- To prove a point, we'll get the icon and tooltip via Lua functions - see CityStateQuests_Helpers_Circumnavigate.lua -->
    			<!--Icon>[ICON_TRADE]</Icon-->
    			<Icon>GetCircumnavigateQuestIcon</Icon>
    			<!--Tooltip>TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_FORMAL</Tooltip-->
    			<Tooltip>GetCircumnavigateQuestTooltip</Tooltip>
    		</Row>
    	</Quests>
    
    The unique Type for this quest is MINOR_CIV_QUEST_CIRCUMNAVIGATE, a unique id will be automatically assigned by the database, when you need it, use GameInfoTypes.MINOR_CIV_QUEST_CIRCUMNAVIGATE as you usually would.

    This quest is only awarded once by a City State, and if it's already active, other players can join in. This makes it a global quest, so we set Global to true. Also, we need more than one player who could complete the quest, so we set MinPlayers to 2 to indicate that we want at least two players (human or AI) to participate.

    By default, if a player bullies a City State after a quest has be handed out, that quest will be revoked. As we don't want that behaviour, we override it by setting RevokeOnBully to false.

    The player that completes the quest typically gains a friendship boost with the City State, for this quest that boost will be 50 and this is set by the Friendship value.

    As this is an exploration/trading type quest, we'll increase the liklihood that the quest will be handed out by a maritime or mercantile City State. The default biases are 100, so 300 means 3 times more likely, while 50 is half as likely. Biases cannot be negative. A bias of 0 means that the quest will never be handed out by a City State with the associated type or personality.

    When the City State hands out (starts) the quest, a notification is generated. A notification comprises two text strings - the message and the summary. If these messages are simple strings, we can place their TXT_KEY entries directly into the StartSummary and StartMessage values for the quest. However, if the strings are not simple, omitting the entries will cause a GameEvent to be generated, in which we can format the strings as desired and generate the notification from Lua.
    The start message and summary for this quest are simple, but as this is an example, we'll omit them to show how to generate the notification via Lua.

    When the quest is complete, the player has either won (they finished the quest) or lost (someone else finished it, so the quest for them has been cancelled). Again, a notification is sent, and we either put the TXT_KEYs directly into the quest entry in the database table or omit them to use the GameEvent. As we're already showing how to generate the start notification from Lua, we'll use TXT_KEYs for these four entries.

    That's it as far as the quest logic in the DLL is concerned, but the UI needs three additional pieces of information. For the UI, the quest needs an icon, a tooltip and a relative place in a list of quests to be displayed.
    The Priority entry is where in a list of quests this quest appears - see the entries for the standard City State quests in the Quests table to work out about where you want your custom quest to appear in the list - 150 is after the "Build a Road" quest.
    The Icon entry is either an [ICON_XYZ] string or the name of a Lua function to determine the icon. If the entry starts with a square bracket ([), this is a specific icon, otherwise it's the name of a function.
    Similarly, the Tooltip entry is either a TXT_KEY or the name of a Lua function to determine the tooltip. If the entry starts with the eight characters TXT_KEY_, this is a simple text string, otherwise it's the name of a function.
    For this quest we could have used a simple icon and tooltip entry, but as this is an example, we'll use Lua functions to generate the icon and tooltip.

    As we've just mentioned them, we'll look at the functions to get the icon and tooltip for the UI.

    To use these you'll need to include the modded version of the CityStateStatusHelper.lua file into your mod. If you don't override this core file, nothing bad will happen, you just won't see any custom quests in the City State popups (or other mods that use the helpers, eg the City States screen of my "UI - Trade Opportunities" mod).

    The modded CityStateStatusHelper.lua file includes all files that match the pattern "CityStateQuests_Helpers_{something}.lua", so we need to create a "CityStateQuests_Helpers_Circumnavigate.lua" file and set it to be VFS=true (do NOT add it as an InGameUIAddin).

    Each function receives five parameters that we could use to return a different icon (see the standard GetReligionQuestIcon function in CityStateStatusHelper.lua file) or format the tooltip (see the GetTourismContestQuestTooltip function below)

    Code:
    --
    -- This file MUST follow the naming pattern of "CityStateQuests_Helpers_{something}.lua"
    --
    
    function GetCircumnavigateQuestIcon(iMajor, iMinor, iQuest, iData1, iData2)
      return "[ICON_TRADE]"
    end
    
    function GetCircumnavigateQuestTooltip(iMajor, iMinor, iQuest, iData1, iData2)
      return Locale.Lookup("TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_FORMAL")
    end
    
    Our TXT_KEY is very simple

    Code:
    	<Language_en_US>
    		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_FORMAL">
    			<Text>They want the map circumnavigated.</Text>
    		</Row>
    	</Language_en_US>
    
    As we're looking at TXT_KEYs the ones for the won (finished) and lost (cancelled) notifications are

    Code:
    	<Language_en_US>
    		<!-- We can ONLY use the {1_MinorName:textkey} replacement, and {2_InfluenceReward} for _FINISH, no others are available -->
    		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_FINISH">
    			<Text>You have successfully circumnavigated the map, much to the delight of {1_MinorName:textkey}! Your [ICON_INFLUENCE] Influence over them has increased by [COLOR_POSITIVE_TEXT]{2_InfluenceReward}[ENDCOLOR].</Text>
    		</Row>
    		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_FINISH_S">
    			<Text>Map circumnavigated for {1_MinorName:textkey}!</Text>
    		</Row>
    		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_CANCEL">
    			<Text>The map has been circumnavigated by someone else!</Text>
    		</Row>
    		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_CANCEL_S">
    			<Text>Someone Else Circumnavigated For {1_MinorName:textkey}</Text>
    		</Row>
    	</Language_en_US>
    
    Note that we get the City State name passed as a parameter/substition ({1_MinorName:textkey}) for these TXT_KEYs, while the finished pair also receive the amount of friendship/influence gained ({2_InfluenceReward}).

    And now for the GameEvent handlers.

    Every custom quest must handle the QuestIsAvailable GameEvent - this event is used to ascertain if the quest can be started by a specific player for a specific City State. We are going to keep this example simple, so our only condition for starting the quest is that the globe must not have already been circumnavigated and that the player must be in the Medieval Era

    Code:
    local iThisQuest = GameInfoTypes.MINOR_CIV_QUEST_CIRCUMNAVIGATE
    local iEraMinimum = GameInfoTypes.ERA_MEDIEVAL
    
    function OnQuestIsAvailable(iPlayer, iCS, iQuest, bNewQuest, iData1, iData2)
    	if (iQuest == iThisQuest) then
    		-- If the map has already been circumnavigated, this quest is no longer available
    		if (iPlayerCircumnavigating == -1) then
    			-- Only let the player join in if they are in (or past) the appropriate era
    			-- We could do something more complex here like checking for the ability to cross oceans (but that assumes a map with at least some water)
    			-- or we could check the width of the map being greater than a certain value and WrapX being true
    			return Players[iPlayer]:GetCurrentEra() >= iEraMinimum
    		end
    	end
    	
    	return false
    end
    GameEvents.QuestIsAvailable.Add(OnQuestIsAvailable)
    
    We'll worry about how iPlayerCircumnavigating gets set later, but it records which player, if anyone, was the first to circumnavigate the globe.

    As we didn't specify values in the Quests entry for StartSummary and StartMessage, we need to handle the QuestSendNotification GameEvent to generate the notification to the player that the City State has issued the challenge

    Code:
    function OnQuestSendNotification(iPlayer, iCS, iQuest, iStartTurn, iEndTurn, iData1, iData2, bStart, bFinish, sNames)
    	if (iQuest == iThisQuest) then
    		if (bStart) then
    			-- Notify the player the quest has started
    			-- If both StartMessage and StartSummary are present in the quest definition, this will never occur
    			
    			-- While we could handle this with simple TXT_KEYs, we'll manually send the notification here as an example
    			local sMinor = Players[iCS]:GetName()
    			local sMessage, sSummary
    			if (Map.IsWrapX()) then
    				-- Map wraps, so we'll ask for circumnavigation
    				sMessage = "TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_START"
    				sSummary = "TXT_KEY_MINOR_CIV_QUEST_CIRCUMNAVIGATE_START_S"
    			else
    				-- Map is a single continent, so we'll ask the player to cross it
    				-- To implement this, needs MUCH more work
    				sMessage = "TXT_KEY_MINOR_CIV_QUEST_TRANSCONTINENT_START"
    				sSummary = "TXT_KEY_MINOR_CIV_QUEST_TRANSCONTINENT_START_S"
    			end
    
    			Players[iPlayer]:AddQuestNotification(iCS, Locale.ConvertTextKey(sMessage, sMinor), Locale.ConvertTextKey(sSummary, sMinor), -1, -1, true)
    		end
    	end
    end
    GameEvents.QuestSendNotification.Add(OnQuestSendNotification)
    
    Note that the same GameEvent is used for all three notifications (if the TXT_KEYs aren't in the Quests table), you differentiate the notification to send with the bStart and bFinish parameters (these will NEVER both be true).

    Also note the new API method on the Player object pPlayer:AddQuestNotification(iCS, sMsg, sSum, iX, iY, bNewQuest) for sending the notification. While you can use the standard pPlayer:AddNotification() method, it is NOT recommended as it will not produce a notification that behaves the same as the standard City State quests.

    This custom quest must also handle the QuestIsCompleted and QuestIsExpired GameEvents. The former returns true if the player has won, the latter returns true if the player has lost, return false means that the quest is ongoing. Our handlers just need to ascertain if the player who actually circumnavigated the globe is the current player.

    Code:
    function OnQuestIsCompleted(iPlayer, iCS, iQuest, bLastTurn)
    	if (iQuest == iThisQuest) then
    		-- Have we completed the quest by being the first player to circumnavigate the map?
    		return (iPlayer == iPlayerCircumnavigating)
    	end
    	
    	return false
    end
    GameEvents.QuestIsCompleted.Add(OnQuestIsCompleted)
    
    function OnQuestIsExpired(iPlayer, iCS, iQuest)
    	if (iQuest == iThisQuest) then
    		-- Did someone beat us to it?
    		return (iPlayerCircumnavigating ~= -1 and iPlayerCircumnavigating ~= iPlayer)
    	end
    	
    	return false
    end
    GameEvents.QuestIsExpired.Add(OnQuestIsExpired)
    
    So the City State issues the challenge and waits for someone to circumnavigate the globe. Presumably the player will build a ship and start exploring ... but what about the AI?
    The DLL sends a QuestStart GameEvent at the commencement of the quest, so we can hook this and add some logic to give the AI a nudge in the right direction. We could check its units for some ships capable of exploration and if not locate a coastal city or three and add an exploration type ship to the build queue and hope they get built and then explore. Alternatively, we can just make sure the AI strategy to recon the sea is active (via a new API method).

    Code:
    function OnQuestStart(iPlayer, iCS, iQuest, bNewQuest, iStartTurn, iData1, iData2)
    	if (iQuest == iThisQuest) then
    		if (not Players[iPlayer]:IsHuman()) then
    			-- Direct the AI to attempt to circumnavigate by activating the strategy to recon at sea
    			Players[iPlayer]:ActivateEconomicStrategy(GameInfoTypes.ECONOMICAISTRATEGY_NEED_RECON_SEA)
    		end
    	end
    end
    GameEvents.QuestStart.Add(OnQuestStart)
    
    Finally, we need to look at how the iPlayerCircumnavigating variable gets set. We use the CircumnavigatedGlobe GameEvent along with some simple data persistence

    Code:
    --
    -- House-keeping of who circumnavigated first
    --
    
    local modDB = Modding.OpenSaveData()
    local iPlayerCircumnavigating = modDB.GetValue("MINOR_CIV_QUEST_CIRCUMNAVIGATE_PLAYER") or -1
    
    function OnCircumnavigatedGlobe(iTeam)
    	-- We get the team that circumnavigated the globe, so we'll give the win to the team leader
    	-- As an example this is acceptable, a production mod may want to give the win to all team members,
    	-- or track the current player and just give it to them
    	iPlayerCircumnavigating = Players[Teams[iTeam]:GetLeaderID()]:GetID()
    	
    	modDB.SetValue("MINOR_CIV_QUEST_CIRCUMNAVIGATE_PLAYER", iPlayerCircumnavigating)
    	
    	GameEvents.CircumnavigatedGlobe.Remove(OnCircumnavigatedGlobe)
    end
    if (iPlayerCircumnavigating == -1) then
    	GameEvents.CircumnavigatedGlobe.Add(OnCircumnavigatedGlobe)
    end
    
    The complete code is in the "Quests - Circumnavigate" mod
     
  2. whoward69

    whoward69 DLL Minion

    Joined:
    May 30, 2011
    Messages:
    8,518
    Location:
    Near Portsmouth, UK
    Example 2: Global Race - Tourism

    The aim of this quest is to accumulate the most (base) Tourism over a fix number of turns - very much like the Culture, Faith and Tech races.

    The Quests database table entry is
    Code:
    	<Quests>
    		<Row>
    			<Type>MINOR_CIV_QUEST_TOURISM_CONTEST</Type>
    			<!-- If we can't generate culture, we can't defend against tourism, so consequently we don't generate any -->
    			<DisabledOnOption>GAMEOPTION_NO_POLICIES</DisabledOnOption>
    			
    			<Global>true</Global>
    			<Contest>true</Contest>
    			<Duration>30</Duration>
    			<MinPlayers>3</MinPlayers>
    			<RevokeOnBully>false</RevokeOnBully>
    			<Friendship>40</Friendship>
    
    			<FinishSummary>TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_FINISH_S</FinishSummary>
    			<FinishMessage>TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_FINISH</FinishMessage>
    			<CancelSummary>TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_CANCEL_S</CancelSummary>
    			<CancelMessage>TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_CANCEL</CancelMessage>
    
    			<BiasFriendly>200</BiasFriendly>
    			<BiasHostile>50</BiasHostile>
    			<BiasCultured>300</BiasCultured>
    			<BiasMercantile>200</BiasMercantile>
    			<BiasMaritime>150</BiasMaritime>
    			<BiasMilitaristic>75</BiasMilitaristic>
    
    			<Priority>4</Priority>
    			<Icon>[ICON_TOURISM]</Icon>
    			<Tooltip>GetTourismContestQuestTooltip</Tooltip>
    		</Row>
    	</Quests>
    
    The DisabledOnOption entry refers to a Game Option, which if set to true, disables the quest. In this case, if the player has disabled culture (policies), it also disables tourism, so no one will be generating any, so the race will be a dead heat with everyone in it finishing on zero!

    This is a global race type quest, so we need to set both Global and Contest to true and give the Duration - 30 turns. Note that the duration should be set for Standard game speed, the DLL will automatically adjust it for the actual game speed.
    MinPlayers, RevokeOnBully and Friendship are the same as for the Circumnavigate quest.

    Notifications for win (finish) and lose (cancel) can be handled by simple TXT_KEYs, but we need to include duration into the start notification, so we'll handle that via the QuestSendNotification GameEvent.

    Biases favour friendly cultured City States and avoid hostile militaristic ones.

    The UI values place the icon just after the other three global races, but as the tooltip needs to include if we are winning or losing, we need a function to generate it.

    The associated TXT_KEYs are
    Code:
    	<Language_en_US>
    		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_FINISH">
    			<Text>You have impressed {1_MinorCivName:textkey} with your tourism!  They turn a blind eye to the tourism of other civilizations, and your [ICON_INFLUENCE] Influence over them has increased by [COLOR_POSITIVE_TEXT]{2_InfluenceReward}[ENDCOLOR].  Civilizations that succeeded (ties are allowed):[NEWLINE]</Text>
    		</Row>
    		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_FINISH_S">
    			<Text>{1_MinorCivName:textkey} is in awe of you!</Text>
    		</Row>
    		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_CANCEL">
    			<Text>Another civilization has impressed {1_MinorCivName:textkey} with its tourism.  Your tourism growth was not enough, and your [ICON_INFLUENCE] Influence remains the same as before.  Civilizations that succeeded (ties are allowed):[NEWLINE]</Text>
    		</Row>
    		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_CANCEL_S">
    			<Text>{1_MinorCivName:textkey} looks elsewhere</Text>
    		</Row>
    	</Language_en_US>
    
    Note that because this is a contest type quest, the DLL will automatically add the list of winners to the end of the notification messages.

    Our tooltip function is

    Code:
    function GetTourismContestQuestTooltip(iMajor, iMinor, iQuest, iData1, iData2)
      local pMinor = Players[iMinor]
    
      -- We need this value for both winning and losing tooltips
      local iMajorScore = pMinor:GetMinorCivContestValueForPlayer(iMajor, iQuest)
    
      -- Is the player (one of) the contest leader(s)
      if (pMinor:IsMinorCivContestLeader(iMajor, iQuest)) then
        -- Yes, so tell them they are winning
        return Locale.Lookup("TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_WINNING_FORMAL", iMajorScore)
      else
        -- No, so get the leaders score ...
        local iLeaderScore = pMinor:GetMinorCivContestValueForLeader(iQuest)
    	
    	-- ... and tell them how much they are losing by
        return Locale.Lookup("TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_LOSING_FORMAL", iLeaderScore, iMajorScore)
      end
    end
    
    which is pretty much a copy of the standard tooltip for a culture race, and the associated TXT_KEYs are

    Code:
    	<Language_en_US>
    		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_FORMAL">
    			<Text>They will reward the player with the largest Tourism growth.</Text>
    		</Row>
    		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_WINNING_FORMAL">
    			<Text>{TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_FORMAL} So far, you have the lead with [ICON_TOURISM][COLOR_POSITIVE_TEXT]{1_PlayerScore}[ENDCOLOR].</Text>
    		</Row>
    		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_LOSING_FORMAL">
    			<Text>{TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_FORMAL} So far, the leader has [ICON_TOURISM]{1_LeaderScore} and you have [ICON_TOURISM][COLOR_POSITIVE_TEXT]{2_PlayerScore}[ENDCOLOR].  </Text>
    		</Row>
    	</Language_en_US>
    
    In the same way that we had to track which player, if any, had circumnavigated the globe, we also need to track how much tourism a player has generated during the race. But not just on a per player basis. Multiple City States can start a tourism race on different turns with different players particiapting in the races, and these races can (and will) overlap. A single City State will never start another tourism race until the first has finished. So we need to record tourism generated per player per City State. The code to do this is as follows,

    Code:
    --
    -- House-keeping of how much tourism has been generated by each major for each quest
    --
    
    local modDB = Modding.OpenSaveData()
    local cachedTourism = {}
    
    function GetKey(iPlayer, iCS)
    	return string.format("MINOR_CIV_QUEST_TOURISM_%i_%i", iPlayer, iCS)
    end
    
    function GetTotalTourism(iPlayer, iCS)
    	if (cachedTourism[iPlayer] == nil) then
    		cachedTourism[iPlayer] = {}
    	end
    	
    	if (cachedTourism[iPlayer][iCS] == nil) then
    		cachedTourism[iPlayer][iCS] = (modDB.GetValue(GetKey(iPlayer, iCS)) or 0)
    	end
    	
    	return cachedTourism[iPlayer][iCS]
    end
    
    function SetTotalTourism(iPlayer, iCS, iValue)
    	modDB.SetValue(GetKey(iPlayer, iCS), iValue)
    
    	if (cachedTourism[iPlayer] == nil) then
    		cachedTourism[iPlayer] = {}
    	end
    
    	cachedTourism[iPlayer][iCS] = iValue
    end
    
    function ChangeTotalTourism(iPlayer, iCS, iValue)
    	SetTotalTourism(iPlayer, iCS, GetTotalTourism(iPlayer, iCS) + iValue)
    end
    
    function OnPlayerDoTurn(iPlayer)
    	if (iPlayer > 0 and iPlayer <= GameDefines.MAX_MAJOR_CIVS) then
    		local pPlayer = Players[iPlayer-1]
    		if (pPlayer:IsAlive() and pPlayer:GetCurrentEra() >= iEraMinimum) then
    			local iTourism = pPlayer:GetTourism()
    			
    			if (iTourism > 0) then
    				for iCS = GameDefines.MAX_MAJOR_CIVS, GameDefines.MAX_CIV_PLAYERS-1, 1 do
    					if (Players[iCS]:IsAlive()) then
    						ChangeTotalTourism(iPlayer-1, iCS, iTourism)
    					end
    				end
    			end
    		end
    	end
    end
    GameEvents.PlayerDoTurn.Add(OnPlayerDoTurn)
    
    We can now use SetTotalTourism() and GetTotalTourism() to track the races.

    Every custom quest must handle the QuestIsAvailable event, we'll keep our conditions simple and just require the player to have started the Industrial Era.

    Code:
    local iThisQuest = GameInfoTypes.MINOR_CIV_QUEST_TOURISM_CONTEST
    local iEraMinimum = GameInfoTypes.ERA_INDUSTRIAL
    
    function OnQuestIsAvailable(iPlayer, iCS, iQuest, bNewQuest, iData1, iData2)
    	if (iQuest == iThisQuest) then
    		-- Only let the player join in if they are in (or past) the appropriate era
    		return Players[iPlayer]:GetCurrentEra() >= iEraMinimum
    	end
    	
    	return false
    end
    GameEvents.QuestIsAvailable.Add(OnQuestIsAvailable)
    
    When the quest starts, we need to reset the tourism generated by this player for the City State handing out the quest ...

    Code:
    function OnQuestStart(iPlayer, iCS, iQuest, bNewQuest, iStartTurn, iData1, iData2)
    	if (iQuest == iThisQuest) then
    		SetTotalTourism(iPlayer, iCS, 0)
    	end
    end
    GameEvents.QuestStart.Add(OnQuestStart)
    
    ... and fire the starting gun!

    Code:
    function OnQuestSendNotification(iPlayer, iCS, iQuest, iStartTurn, iEndTurn, iData1, iData2, bStart, bFinish, sNames)
    	if (iQuest == iThisQuest) then
    		if (bStart) then
    			local pMinor = Players[iCS]
    
    			local iTurnsRemaining = iEndTurn - Game.GetGameTurn()
    			local iTurnsDuration = iEndTurn - iStartTurn  -- Don't be tempted to do a database lookup here, as that doesn't allow for game speed scaling
    			local sMinor = pMinor:GetName()
    			
    			local sMessage = Locale.ConvertTextKey("TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_START", iTurnsDuration, iTurnsRemaining, sMinor)
    			local sSummary = Locale.ConvertTextKey("TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_START_S", sMinor)
    
    			Players[iPlayer]:AddQuestNotification(iCS, sMessage, sSummary, -1, -1, true)
    		end
    	end
    end
    GameEvents.QuestSendNotification.Add(OnQuestSendNotification)
    
    And the associated TXT_KEYs

    Code:
    	<Language_en_US>
    		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_START">
    			<Text>The desire to travel is flourishing in {3_MinorCivName:textkey}, and they seek tourism guidance.  Whoever can produce the most [ICON_TOURISM] Tourism in a period of {2_TurnsDuration} turns will gain [ICON_INFLUENCE] Influence with them.  [COLOR_POSITIVE_TEXT]{1_TurnsRemaining} turns remaining.[ENDCOLOR]</Text>
    		</Row>
    		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_TOURISM_CONTEST_START_S">
    			<Text>{1_MinorCivName:textkey} longs for tourism!</Text>
    		</Row>
    	</Language_en_US>
    
    At various times, the DLL will need to know the player's "score" for a race, and sends the QuestContestValue GameEvent to find out. So we need to handle this event and just return the accumulated tourism value.

    Code:
    function OnQuestContestValue(iPlayer, iCS, iQuest)
    	if (iQuest == iThisQuest) then
    		return GetTotalTourism(iPlayer, iCS)
    	end
    	
    	return 0
    end
    GameEvents.QuestContestValue.Add(OnQuestContestValue)
    
    Finally, we need to ascertain if the player won (there can be more than one winner), but we only check if this is the last turn of the race

    Code:
    function OnQuestIsCompleted(iPlayer, iCS, iQuest, bLastTurn)
    	if (iQuest == iThisQuest) then
    		-- Is it time to compare the scores?
    		if (bLastTurn) then
    			return Players[iCS]:IsMinorCivContestLeader(iPlayer, iQuest)
    		end
    	end
    	
    	return false
    end
    GameEvents.QuestIsCompleted.Add(OnQuestIsCompleted)
    
    We could have checked the player's score (total accrued tourism) against all the other players' scores (allowing for equal first places) directly, but why bother when there's an API method that does it for us!

    Note that we don't handle the QuestIsExpired event, as any player in the race who didn't win automatically loses at the end of the race

    The complete code is in the "Quests - Tourism Contest" mod
     
  3. whoward69

    whoward69 DLL Minion

    Joined:
    May 30, 2011
    Messages:
    8,518
    Location:
    Near Portsmouth, UK
    Example 3: Personal Quest - National Wonder

    The aim of this quest is to build a National Wonder

    The Quests database table entry is
    Code:
    	<Quests>
    		<Row>
    			<Type>MINOR_CIV_QUEST_NATIONAL_WONDER</Type>
    			<Friendship>30</Friendship>
    			<BiasFriendly>200</BiasFriendly>
    			<BiasNeutral>150</BiasNeutral>
    			<BiasHostile>50</BiasHostile>
    			<BiasCultured>150</BiasCultured>
    			<Priority>56</Priority>
    			<Icon>[ICON_GOLDEN_AGE]</Icon>
    			<Tooltip>GetNationalWonderQuestTooltip</Tooltip>
    		</Row>
    	</Quests>
    
    which is about as simple as it gets!

    As the quest is open ended, the player cannot lose (so we don't need a cancel notification) and as the name of the national wonder has to be formmated into the start and finish notifications we must send those via the QuestSendNotification GameEvent.

    The UI tooltip code is

    Code:
    function GetNationalWonderQuestTooltip(iMajor, iMinor, iQuest, iData1, iData2)
      return Locale.Lookup("TXT_KEY_MINOR_CIV_QUEST_NATIONAL_WONDER_FORMAL", GameInfo.Buildings[Players[iMajor]:GetSpecificBuildingType(GameInfo.BuildingClasses[iData1].Type)].Description)
    end
    
    Code:
    	<Language_en_US>
    		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_NATIONAL_WONDER_FORMAL">
    			<Text>They want you to build {1_WonderName:textkey}.</Text>
    		</Row>
    	</Language_en_US>
    
    which just converts the national wonder class into a civ specific building and formats the building's description into the notification message.

    We'll look at how the required National Wonder building class id gets into the iData1 value in a bit.

    But first, some housekeeping. We need to track what National Wonders a player has left that could be the target of the City State quest

    Code:
    --
    -- House-keeping of who has built what national wonders
    --
    
    -- Find all the national wonder building classes
    local nationalWonderClasses = {}
    for row in DB.Query("SELECT ID, Type FROM BuildingClasses WHERE MaxPlayerInstances=1") do
    	nationalWonderClasses[row.Type] = row.ID
    end
    
    -- Find all the national wonders already constructed by players
    local playerNationalWonders = {}
    for iPlayer = 0, GameDefines.MAX_CIV_PLAYERS-1, 1 do
    	playerNationalWonders[iPlayer] = {}
    	
    	local pPlayer = Players[iPlayer]
    	if (pPlayer:IsAlive()) then
    		for nwClass, nwID in pairs(nationalWonderClasses) do
    			playerNationalWonders[iPlayer][nwClass] = (pPlayer:GetBuildingClassCount(nwID) > 0)
    		end
    	end
    end
    
    -- Watch for new national wonders being built
    function OnCityConstructed(iPlayer, iCity, iBuilding)
    	local class = GameInfo.Buildings[iBuilding].BuildingClass
    	
    	if (nationalWonderClasses[class]) then
    		playerNationalWonders[iPlayer][class] = true
    	end
    end
    GameEvents.CityConstructed.Add(OnCityConstructed)
    
    We don't need to persist any data, as we can reconstruct the lists every time the game is reloaded (we'll gloss over National Wonders destroyed in razed cities).

    The code to ascertain if the quest is available to the player is

    Code:
    local iThisQuest = GameInfoTypes.MINOR_CIV_QUEST_NATIONAL_WONDER
    local iEraMinimum = GameInfoTypes.ERA_MEDIEVAL
    
    function OnQuestIsAvailable(iPlayer, iCS, iQuest, bNewQuest, iData1, iData2)
    	if (iQuest == iThisQuest) then
    		-- Only let the player join in if they haven't bullied the City State recently
    		if (not Players[iCS]:IsRecentlyBulliedByMajor(iPlayer)) then
    			-- Only let the player join in if they are in (or past) the appropriate era
    			if (Players[iPlayer]:GetCurrentEra() >= iEraMinimum) then
    				for _, bConstructed in pairs(playerNationalWonders[iPlayer]) do
    					-- We don't care what the National Wonder is, just that there is something left.  In a real mod this would be much more selective.
    					if (not bConstructed) then
    						return true
    					end
    				end
    			end
    		end
    	end
    	
    	return false
    end
    GameEvents.QuestIsAvailable.Add(OnQuestIsAvailable)
    
    Note that this code doesn't pick which National Wonder to construct, it just checks that any National Wonder could be constructed.

    So how do we pick a National Wonder to construct?

    When a quest starts, we can store two integers as data to the quest - typically this is an (X, Y) pair (eg the location of the camp to be cleared), or the id of a target player/City State. (If you need to store more data, persist it into the mod database and store the unique key into the quest data.)
    After sending the QuestStart GameEvent, the DLL immediately sends TWO QuestGetData events. We will use this event to decide what National Wonder to request and store the decision into the iData1 value.

    Code:
    function OnQuestGetData(iPlayer, iCS, iQuest, bData1)
    	if (iQuest == iThisQuest and bData1) then
    		local nwAvailable = {}
    
    		-- Find all the unbuilt national wonders
    		for nwClass, bConstructed in pairs(playerNationalWonders[iPlayer]) do
    			if (not bConstructed) then
    				table.insert(nwAvailable, nwClass)
    			end
    		end
    
    		-- Pick one at random
    		return nationalWonderClasses[nwAvailable[Game.Rand(#nwAvailable, "National Wonder for quest")+1]]
    	end
    	
    	return 0
    end
    GameEvents.QuestGetData.Add(OnQuestGetData)
    
    Note the limitation in using an accumulator event, if the value zero is meaningful you will have to take other steps to record it - fortunately no National Wonder has a building class id of 0

    Now we have the selected National Wonder stashed away in iData1, we can generate the start and finish notifications using that information

    Code:
    function OnQuestSendNotification(iPlayer, iCS, iQuest, iStartTurn, iEndTurn, iData1, iData2, bStart, bFinish, sNames)
    	if (iQuest == iThisQuest) then
    		local sMinor = Players[iCS]:GetName()
    		local sBuilding = GameInfo.Buildings[Players[iPlayer]:GetSpecificBuildingType(GameInfo.BuildingClasses[iData1].Type)].Description
    
    		if (bStart) then
    			-- Notify the player the quest has started
    			local sMessage = Locale.ConvertTextKey("TXT_KEY_MINOR_CIV_QUEST_NATIONAL_WONDER_START", sMinor, sBuilding)
    			local sSummary = Locale.ConvertTextKey("TXT_KEY_MINOR_CIV_QUEST_NATIONAL_WONDER_START_S", sMinor, sBuilding)
    
    			Players[iPlayer]:AddQuestNotification(iCS, sMessage, sSummary, -1, -1, true)
    		elseif (bFinish) then
    			-- Influence boost has already been handled via the Friendship entry in the quest definition
    			
    			-- Notify the player the quest has finished
    			local sMessage = Locale.ConvertTextKey("TXT_KEY_MINOR_CIV_QUEST_NATIONAL_WONDER_FINISH", sMinor, sBuilding, GameInfo.Quests[iQuest].Friendship)
    			local sSummary = Locale.ConvertTextKey("TXT_KEY_MINOR_CIV_QUEST_NATIONAL_WONDER_FINISH_S", sMinor, sBuilding)
    
    			Players[iPlayer]:AddQuestNotification(iCS, sMessage, sSummary, -1, -1, false)
    		end
    	end
    end
    GameEvents.QuestSendNotification.Add(OnQuestSendNotification)
    
    And the associated TXT_KEYs

    Code:
    	<Language_en_US>
    		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_NATIONAL_WONDER_START">
    			<Text>{1_MinorName:textkey} wants you to build {2_WonderName:textkey}.</Text>
    		</Row>
    		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_NATIONAL_WONDER_START_S">
    			<Text>{1_MinorName:textkey} desires {2_WonderName:textkey}</Text>
    		</Row>
    		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_NATIONAL_WONDER_FINISH">
    			<Text>As {1_MinorName:textkey} requested, you have successfully constructed {2_WonderName:textkey}! Your [ICON_INFLUENCE] Influence over them has increased by [COLOR_POSITIVE_TEXT]{3_InfluenceReward}[ENDCOLOR].</Text>
    		</Row>
    		<Row Tag="TXT_KEY_MINOR_CIV_QUEST_NATIONAL_WONDER_FINISH_S">
    			<Text>{2_WonderName:textkey} Constructed for {1_MinorName:textkey}!</Text>
    		</Row>
    	</Language_en_US>
    
    Finally we need to indicate when the quest is complete - simply when the player constructs the National Wonder.

    Code:
    function OnQuestIsCompleted(iPlayer, iCS, iQuest, bLastTurn)
    	if (iQuest == iThisQuest) then
    		-- Have we built the national wonder yet?
    		return (Players[iPlayer]:GetBuildingClassCount(Players[iCS]:GetQuestData1(iPlayer, iQuest)) > 0)
    	end
    	
    	return false
    end
    GameEvents.QuestIsCompleted.Add(OnQuestIsCompleted)
    
    It would appear that there is no incentive for the player to rush build the National Wonder, except that it is taking up a City State quest slot and there are a limited number of those per player.

    The complete code is in the "Quests - National Wonder" mod
     
  4. whoward69

    whoward69 DLL Minion

    Joined:
    May 30, 2011
    Messages:
    8,518
    Location:
    Near Portsmouth, UK
    Writing Efficient Quest Event Handlers

    Quest events fire frequently, it is therefore of utmost importance that their handlers are coded as efficiently as possible. Assuming we have M majors, N city states and Q custom quests, each event handler can potentially trigger N*M*Q times per turn, where N*M*(Q-1) of those will NOT be for the specific event, so it is imperative to "early out" if the event is not for the specific quest.

    EVERY quest event handler should therefore be wrapped with a test for the specific event
    Code:
    function OnQuestEvent(...)
    	if (iQuest == iThisQuest) then
    	end
    	
    	return {default}
    end
    
    Furthermore, iThisEvent should be a file scoped variable
    Code:
    local iThisEvent = GameInfoTypes.MINOR_CIV_QUEST_MY_QUEST
    
    and not use "GameInfoTypes.MINOR_CIV_QUEST_MY_QUEST" directly in the condition (as that requires a table lookup everytime)

    Avoid "mustering the troops" style programming.
    Code:
    local a = ...1
    local b = ...2
    local c = ...3
    
    if (a and b and c) then
    end
    
    If a is false, b and c are never needed; if b is false, c is never needed. But we will still evaluate and assign both b and c, instead use
    Code:
    local a = ...1
    if (a) then
    	local b = ...2
    	if (b) then
    		local c = ...3
    		if (c) then
    		end
    	end
    end
    
    Avoid unnecssary variable usage. While it may be tempting to write
    Code:
    local a = (iQuest == iThisQuest)
    if (a) then
    end
    
    in a non-compiled langauge such as Lua, this forces the interpreter to create a variable that's only ever used once, and this takes (albeit a very small amount of) time - which over N*M*Q cycles WILL add up.
    If the right hand side of the assignment to a is trivial, ALWAYS use the expression directly, NEVER create a single use variable
    Code:
    if (iQuest == iThisQuest) then
    end
    
    Assuming c is a function of b, it would be better to write
    Code:
    if (iQuest == iThisQuest) then
    	local b = ...2
    	if (b) then
    		if ({uses b}...3) then
    			return true
    		end
    	end
    end
    
    return false
    
    Typically b and c will be functions of Players[iPlayer], so it is tempting to write
    Code:
    if (iQuest == iThisQuest) then
    	local pPlayer = Players[iPlayer]
    		
    	if ({uses pPlayer}...2) then
    		if ({uses pPlayer}...3) then
    			return true
    		end
    	end
    end
    
    return false
    
    However, if b is a single method call on pPlayer that fails most of the time (eg checking to see if the player is an ally of the City State), creating the pPlayer variable should be delayed
    Code:
    if (iQuest == iThisQuest) then
    	if (Players[iPlayer]:SomeMethod(...)) then
    		local pPlayer = Players[iPlayer]
    		
    		if ({uses pPlayer}...3) then
    			return true
    		end
    	end
    end
    
    return false
    
    Take advantage of lazy evaluation. In the condition "a and b", b will never be evaluated if a is false (as "false and anything" is always false), which means we can compress the last code to
    Code:
    if (iQuest == iThisQuest and Players[iPlayer]:SomeMethod(...)) then
    	local pPlayer = Players[iPlayer]
    		
    	if ({uses pPlayer}...3) then
    		return true
    	end
    end
    
    return false
    
    which gives the interpreter one less block (the second if ... then ... end block statement) to parse.

    Avoid using "if (condition) then return true end" constructs as the last statement within a TestAny (ie all Quest) handlers, just return the outcome of the condition
    Code:
    if (iQuest == iThisQuest and Players[iPlayer]:SomeMethod(...)) then
    	local pPlayer = Players[iPlayer]
    			
    	return ({uses pPlayer}...3)
    end
    
    return false
    
    and if ...3 only uses pPlayer once, do not create the variable
    Code:
    if (iQuest == iThisQuest and Players[iPlayer]:SomeMethod(...)) then
    	return Players[iPlayer]:IsSomething(...)
    end
    
    return false
    
    Finally, NEVER use "if (condition) then return true else return false end" constructs, always use "return (condition)"
    And NEVER, NEVER, NEVER use "if (not condition) then return false else return true end" constructs, always use "return (not condition)"


    Hooking Quest Event Handlers Efficiently

    Even with efficient "early out" code, the most efficient way to process events is not to - at least not if you don't have to.

    Most event handlers are coded as
    Code:
    function OnSomeEvent(...)
    end
    GameEvents.SomeEvent.Add(OnSomeEvent)
    
    That is, they are installed as the game starts up and left installed for the entire duration of the game.

    In the case of quests, we can usually delay installing the handlers until the entry conditions are met, and for some global events, we can uninstall the handlers after the event is complete.

    Consider a quest that cannot be started until some era (or tech) is reached by one or more players. We do not need to hook the events until at least one player has reached the required era(tech).
    Code:
    function OnSomeEvent(...)
    end
    	
    for iPlayer = 0, GameDefines.MAX_MAJOR_CIVS-1, 1 do
    	if (Players[iPlayer]:GetCurrentEra() >= iRequiredEra) then
    		GameEvents.SomeEvent.Add(OnSomeEvent)
    		break
    	end
    end
    
    But we will also need to add an era(tech) event handler (which will trigger far less often than the quest event(s))
    Code:
    function OnTeamSetEra(iTeam, iEra, bFirst)
    	if (iEra >= iRequiredEra) then
    		-- Install the quest event handler(s)
    		GameEvents.SomeEvent.Add(OnSomeEvent)
    
    		-- Remove ourselves
    		GameEvents.TeamSetEra.Remove(OnTeamSetEra)
    	end
    end
    GameEvents.TeamSetEra.Add(OnTeamSetEra)
    
    Removing the handlers for one-off (global) quests can also be done, but should be deferred until the barbarian player's turn, in order to give every player and City State the opportunity to complete/expire any active quests - see the "Quests - Circumnavigate" complete mod for an example of how to do this.
     
  5. whoward69

    whoward69 DLL Minion

    Joined:
    May 30, 2011
    Messages:
    8,518
    Location:
    Near Portsmouth, UK
    Some other ideas for custom quests (thanks to bane_ for some of these)
    • Rescue a princess (captured civilian) from a dragon (very powerful barbarian unit limited to tiles around a specific camp)
    • Free a City State prince (destroy another CS, or capture a civilian from another CS/Civ)
    • Spread the message (convert X cities (of a specific civ) to the religion of the City State)
    • Bounty (kill/capture X units)
    • Mysterious Gift (send a unit to a designated plot to find out what (if anything) is there)
    • Stop the message (destroy all trade routes from a specific civ to the City State)
    • Reunite the people (be the first civ to send a unit to each City State of a certain type/personality)
    All are now possible
     
  6. biship

    biship Warlord

    Joined:
    Dec 7, 2003
    Messages:
    187
    Wow, this is fantastic. I can't wait until the CP updates their DLL so I can try out your quest mini-mods. Nice work whoward!
     
  7. Troller0001

    Troller0001 Watching from a safe distance

    Joined:
    Mar 9, 2016
    Messages:
    743
    Gender:
    Male
    Location:
    The Netherlands
    Wow, awesomely done whoward (since 'nicely done' wouldn't be a sufficient statement)! :D

    Some ideas for a militaristic city state quest:
    • The player who trains the most military units gains influence with the city state
    • Barbarian slaughterer: The player who kills the most Barbarians (including those not near/in CS's territory), gains influence
     
  8. PawelS

    PawelS Ancient Druid

    Joined:
    Dec 11, 2003
    Messages:
    2,803
    Location:
    Poland
    This functionality is very interesting, and relevant to the plans of future development of my mod :)
     
  9. bane_

    bane_ Howardianism High-Priest

    Joined:
    Nov 27, 2013
    Messages:
    1,559
    Is this possible directly? I can think of a way to 'move it back' if it leaves the stipulated range with UnitSetXY, but I'm interested to know if one can actually 'lock' an unit to specific tiles (or even no tile, with moves only in order to attack).
     
  10. whoward69

    whoward69 DLL Minion

    Joined:
    May 30, 2011
    Messages:
    8,518
    Location:
    Near Portsmouth, UK
    Code:
        <!-- Event sent to ascertain if a unit can move into a given plot (v19) -->
        <!--   GameEvents.CanMoveInto.Add(function(iPlayer, iUnit, iPlotX, iPlotY, bAttack, bDeclareWar) return true end) -->
        <!--   Also requires each unit type to receive the event to be enabled via the new SendCanMoveIntoEvent column in the Units table -->
        <!-- See also: "Units - Railroad Artillery" -->
        <!-- See also: "Civilization - Morindim" -->
        <Row Class="3" Name="EVENTS_CAN_MOVE_INTO" Value="0" DbUpdates="1"/>
    
    In the Unit definition
    Code:
    <SendCanMoveIntoEvent>true</SendCanMoveIntoEvent>
    
    Restricts a unit to the railways
    Code:
    --
    -- This event is only sent for units where their SendCanMoveIntoEvent column in the Units table is true
    --
    function OnCanMoveInto(iPlayer, iUnit, iPlotX, iPlotY, bAttack, bDeclareWar)
        local pUnit = Players[iPlayer]:GetUnitByID(iUnit)
    
        if (pUnit:GetUnitClassType() == iClassRailroadArtillery) then
            local pPlot = Map.GetPlot(iPlotX, iPlotY)
            return (pPlot:GetRouteType() == iRouteRailroad)
        end
    
        return true
    end
    GameEvents.CanMoveInto.Add(OnCanMoveInto)
    
     
  11. bane_

    bane_ Howardianism High-Priest

    Joined:
    Nov 27, 2013
    Messages:
    1,559
    I need WhatsApp's shocked emoticon right now.

    Well... this one will do: :faint:
     
  12. Ethidium

    Ethidium Chieftain

    Joined:
    Feb 29, 2016
    Messages:
    16
    This is awesome - wish I had an nth of your ability! Is it possible to have quests set by your own cities? I'm thinking along the lines of their demands for resources that can start a "we love the king day"... I guess it's a totally different bit of DLL modding to do that?

    Is there a way to mod so that, for example, adopting the exploration policy tree could make it possible that one of your own cities would challenge you to circumnavigate the globe?
     
  13. whoward69

    whoward69 DLL Minion

    Joined:
    May 30, 2011
    Messages:
    8,518
    Location:
    Near Portsmouth, UK
    You've answered your own question :)

    Yes it would be possible. No you couldn't do it with the City State quest code directly (and by implication what the features/functions described in this thread enables).
     

Share This Page