3. Tutorial
This is a walk-through of how to use
PyScenario.
3.1 Installation
Firstly, make sure you are using the current version of
RFC Epic/Marathon (v1.21) - downloaded
here.
Secondly, download the latest version of PyScenario
here.
The PyScenario.zip archive itself contains 3 files:
- CvRFCEventHandler.py - a replacement for the standard Event Handler module
- PyScenario.py - the application module
- Scenario.py - script template module
Unpack all Python files into the
\Beyond the Sword\Mods\RFCMarathon\Assets\Python\ folder. Make sure to make a copy of the default CvRFCEventHandler.py file before replacing it with this modified version!
Done!
3.2 Application description
The
CvRFCEventHandler module is where all the Python code in the mod is connected to the main game, and as
PyScenario.py is a Python application it is also hooked up here. The Event Handler will call on the
PyScenario module any time a "game event" takes place, like when a game turn ends, a city changes owners or a Technology advance has been granted.
Likewise, when the game is initialized the
PyScenario module calls on the
Scenario module (the user defined script) to load all Triggers into memory. This is only done once. This is actually achieved by the
Scenario module calling on some Python code in the
PyScenario module to set up each Trigger. After this the
Scenario module is not called upon anymore. (Actual scenario design work might call for it to be reloaded during play/testing though, see chapter 3.8 below.)
3.3 The Scenario module
First order of business after installation is to open up the
Scenario module. Use Notepad or some other text editor of choice. This is what the template module looks like (you might wanna make a copy before adding any content of your own):
Code:
#Scenario name: PyScenario script template
from PyScenario import *
### enter Trigger constructors below
The lines beginning with the # character are for information purposes only and add no functionality whatsoever to the script. You can delete these lines on a whim or add your own comments in the same manner.
The line with the
import statement makes the script work with the PyScenario application and is thus vital.
Enter the Triggers you wanna use at the end of the script - one Trigger per line. Make sure to disable automatic line breaks in the text editor in order to avoid confusion.
Note that the
Scenario.py file should always be saved with the default text encoding! Make sure that its UTF-8 and not ANSI or Unicode or some other encoding!
3.4 Sample Triggers
As described in the previous sections a Trigger starts with a constructor and the different methods (mainly Conditions and Actions) are stacked onto this using
dot notation.
The examples below are inspired by a historical scenario focusing on the Russian Empire. Firstly, the city of Kiev should be present prior to the Russian spawn. Wikipedia mentions the year AD 482 as the founding date:
Code:
Trigger().date(482).city(69,52,"Kiev",1)
For the sake of keeping things simple, this is a very non-spectacular event - a humble beginning. So there are no units, no culture, no buildings - no nothing - present in the newly spawned city. A look at the API entry for the
city() method reveals that the values used here corresponds to the following settings:
Code:
city( iX=69, iY=52, name="Kiev", iSize=1 )
Only the first two settings are obligatory and this is because they have no default values. (The other two -
name and
iSize - are automatically set to None and 2 respectively if omitted.)
Note however that no Civ was been specified anywhere in the Trigger! As described in the API section, this means that the application will automatically select a randomly chosen minor Civ as the city owner. But is this really what we want? Because it could as well be the Barbarian Civ...
Well, we can change the contents of the Trigger to whatever we want. Lets have another stab at it:
Code:
Trigger("spawn Kiev").player(27).date(450,500).interval(2,True).city(69,52).name("Kiev").units()
The Trigger is named "spawn Kiev" which can be useful for identifying it later. The player() method is used for setting a specific Target Player for the Trigger - the integer value 27 corresponds to the 28th enumerated Player and in this mod that would always be the Independent Civilization.
The
date() Conditions has been turned into a date span - from AD 450 to AD 500. The interval() Condition is here used to create a random turn interval that will pass at roughly 1/2 of the time. (The interval/probability scales with game speed however.) What we get is basically a spawn date that isn't exactly defined, but still very likely to happen. (And any Trigger using the city() method will only fire once, so we don't have to worry about multiple Civs spawning.)
Now, the
city() Action is only supplied with the mandatory settings (the tile coordinates), which means that the default values will be used for the other two settings: The city will thus be size 2 (the default size) on spawn and be automatically named by the game. Instead, we'll be utilizing the
name() action to
rename the city "Kiev", which is also a valid option.
Since there are Barbarian spawns already preset in mod it would be prudent to add a military unit to the city spawn. This is easily achieved with the
units() method. Again, no settings are entered and the method will use the default settings instead. In this case this means that the application will spawn one unit of a unit type automatically detected as the current defensive unit of the era for the Target Player.
Other equally valid options for the exact same settings would be:
Code:
Trigger(label="spawn Kiev").player(ePlayer=27,eCivilization=None).date(date1=450,date2=500).interval(iInterval=2,bRandom=True,bScaling=True).city(iX=69,iY=52,name=None,iSize=2).name(newName="Kiev",oldName=None,tCoords=None).units(tCoords=None,eType=None,iNum=1,bGarrison=False)
Or alternatively:
Code:
Trigger("spawn Kiev").player(27,None).date(450,500).interval(2,True,True).city(69,52,None,2).name("Kiev",None,None).units(None,None,1,False)
The first option is probably too long to be convenient but is on the other hand very clear. The second option is shorter but still not as short as the original version. They are all identical as far as the application is concerned, and you could use whichever - or any combination thereof.
Now that we have one historical event covered we can focus on another. What about spawning a building in Kiev?
Code:
Trigger("Kiev building").date(701,850).era(2).target(69,52).buildings(59)
There are two Conditions present: The
date() method defines a game year span of 150 years, while the
era() method requires the current game era to be the Middle-Ages. So the Trigger will only fire if the game era enters the Middle-Ages between the years 701 and 850, which may or may not happen.
The
target() method is used to define the Target Tile, which incidentally corresponds to the map tile containing the city of Kiev (or any other city taking its place). The Action present is of course
buildings() and the value 59 corresponds to 60th enumerated building, which is the Market.
What about other inhabitants of the Ukrainian plains? Well, there are the nomadic Pechenegs:
Code:
Trigger("Pechenegs").player(31).date(840,1199).interval(3).target((69,49),(77,50)).valid().units(None,66).promotions(37).message("Pecheneg uprising!")
This will spawn one Barbarian Horse Archer unit inside an area consisting of 16 map tiles (defined as a Plot List by the
target() method) every three turns between the game dates 840 and 1199. The
valid() method will randomly check the Plot List for any valid map tile (according to the default settings). The spawned units will always have the Flanking I promotion, as defined by the
promotions() method.
There is also a in-game message warning the human player about the Pecheneg menace. This is easily done with the
message() Action.
What about the city of Novgorod, by the way? It is fabled to have been a Norse fortress originally, so why not use that for our next historical event?
Code:
Trigger("Holmgard").player(11).check().date(859).valid(bDomestic=True,bAdjacent=True).terrain(tCoords=(68,56),eImprovement=23).units(iNum=1).AI("UNITAI_UNKNOWN")
This is another not-very eventful event, as it will spawn a Fort terrain improvement on the tile where the future city of Novgorod will be built. The Conditions are
check() - which prevents the Trigger from firing if the human player is controlling the Viking Civ,
valid() - which requires the Viking Civ to own the tile in question, and
date(). There is also a second Action which will garrison the Fort. (The
AI() method is here used to disable the unit's AI settings, so that it won't leave to Fort.)
Later, a Russian city could be spawned on the same tile:
Code:
Trigger("spawn Novgorod").player(18).check().valid().date(1000).city(68,56,"Novgorod").culture(20).buildings(59,13).units(iNum=2,bGarrison=True)
Can you decipher what this Trigger does on your own? Refer to the API section above!
Later yet, the new capital of the Russian Empire would be Saint Petersburg. The thing, though, is that it will be located one tile north of our Novgorod location. Well, Novgorod did lose its importance historically, so lets just replace it!
Code:
Trigger("spawn Petersburg").player(18).check().date(1703).city(68,57,"Sankt Petersburg",3).culture(50).religions(1).buildings(0).units(iNum=2,bGarrison=True)
Since the
valid() Condition is missing any city on an adjacent tile (like Novgorod in this case) will be deleted to make room for the new city! (It could be pointed out that the index value for Christianity is 1 and that that the Palace is enumerated as 0.)
And so on. This could very well be the beginnings of a Russia mod-scenario for
RFC Epic/Marathon!
3.5 Sample script
This is what the script detailed above could look like:
Code:
#Scenario name: Rise of the Russian Empire
from PyScenario import *
### enter Trigger constructors below
Trigger("spawn Kiev").player(27).date(450,500).interval(2,True).city(69,52).name("Kiev").units()
Trigger("Kiev building").date(701,850).era(2).target(69,52).buildings(59)
Trigger("Pechenegs").player(31).date(840,1199).interval(3).target((69,49),(77,50)).valid().units(None,66).promotions(37).message("Pecheneg uprising!")
Trigger("Holmgard").player(11).check().date(859).valid(bDomestic=True,bAdjacent=True).terrain(tCoords=(68,56),eImprovement=23).units(iNum=1).AI("UNITAI_UNKNOWN")
Trigger("spawn Novgorod").player(18).check().valid().date(1000).city(68,56,"Novgorod").culture(20).buildings(59,13).units(iNum=2,bGarrison=True)
Trigger("spawn Petersburg").player(18).check().date(1703).city(68,57,"Sankt Petersburg",3).culture(50).religions(1).buildings(0).units(iNum=2,bGarrison=True)
You can actually copy-paste this into an empty text file which you save as
Scenario.py in the appropriate folder - and test it yourself! (Just remember to backup the original
Scenario module and to save the new one with UTF-8 encoding.)
3.6 Indexed values of enumerated types
The scenario maker needs to know the enumerated values of many different things that have been indexed by the game. There are actually many ways of finding these values, and some of them are detailed below.
The author of the original RFC mod included a file called
Consts.py that only defines variables. (These are so called "constants" as the values will not change but are rather used for indexing and for defining settings for the mod.) This is a good place to look for many of the types of enumerated values used with scripting. You can either use the numerical values associated with each variable but note that all the constants are already available in the
Scenario module! Example:
Code:
Trigger().target(68,56).player(iRussia).era(iMedieval).discover(iFeudalism).buildings(iCastle)
(The variable
iRussia is assigned the value 18, the variable
iMedieval points to the value 2, the variable
iFeudalism corresponds to the value 9 and the variable
iCastle equals the value 5.)
Another obvious option is to simply look up the different enumerated values for different types in the
CivIV Python API (look for the hyper link "Type List" in the lower left pane). Note however that the mod differs from the standard iBTS game on several points, but most of the indexes are still valid. (They may be missing entries though.)
The proper, but rather inconvenient, way of using an enumerated type is however to invoke the actual XML tag on the type class with dot notation. Example:
PromotionTypes.PROMOTION_MOBILITY (You could of course assign this value to a variable and use that instead, but this would lead us into programming territory.)
3.7 Finding tile coordinates
The tiles making up the game map are indexed from zero and up on both the X and the Y axis. The X coordinates go from left to right (west-east) while the Y coordinates actually go from down and up (south-north)! It might however not be obvious how to find the actual coordinates for any map tile. Start by enabling cheat mode in
\My Documents\My Games\Beyond the Sword\CivilizationIV.ini:
Every scenario maker should enable this setting, because then you get access to the full range of options in the World Builder. Also, you can reveal the entire map (and also access all cities) with the shortkey ctrl + Z. Among other things.
Now, with cheat mode enabled, you can hover any tile while pressing the shift key. The coordinates are shown in the tile info bar (bottom left).
3.8 Reloading Triggers
It is possible to edit the Scenario module while the game is running, but any time you do this all the Python in the mod will be reloaded. This also means that all Triggers will be reloaded and all the stored data for the mod (like stability ratings) are reverted back to the last save. The most effective way to retain all the current stored values (sans the current state of any PyScenario Triggers) is to hit the
ctrl + S shortkey to save the game before you make any changes to your Scenario script. (If you forget this step you can just reload a autosave as it will include all the data intact.)
You can also manually force all Triggers to be reloaded any time by pressing the
shift + alt + R shortkey. This will also revert all stored mod data to the last save game.
If you're experiencing problems with any of this you could just do your scripting while the game isn't running and relaunch it once you're done.
3.9 Unit flags (advanced feature)
It is possible to create special flag units by utilizing the
flag() method. It is a add-on method to the
units() Action and must be entered after this method. Example:
Code:
Trigger()...units().flag("special unit")
The actual unit flag is a string value and should be unique for that unit. Note that it is also possible to give multiple units unique names with one single
units() and
flag() method respectively. Refer to the API entry for the
flag() Action (section 2.6.1) for more details.
A flag unit has no special abilities except for the potential to trigger events. This is achieved by the
captured() Condition that detects whenever a flag unit is destroyed in combat. Refer to the associated API entry (section 2.5.4) for the specifics. Example:
Code:
Trigger().captured("special unit")...
It should be noted that it is currently possible for the human player to avoid losing a flag unit in combat - and thus risking negative events - by simply disbanding the flagged unit...
3.10 Trigger binding (advanced feature)
Sometimes it can be useful to have a Trigger fire when another Trigger has already fired - or hasn't fired - depending on what the actual scenario calls for. This is achieved by first giving the Trigger - that will act as a Condition for the other one to fire - a unique name. This is done with the
label setting - see the API entry (section 2.2) for details on use. Example:
Code:
Trigger("first Trigger")...
The second Trigger - that will fire once the first Trigger has fired - then uses the
fired() Condition to detect when this has happened. See section 2.5.6 for the specifics. Example:
Code:
Trigger("second Trigger").fired("first Trigger")...
It is possible to add several
fired() methods in one Trigger. Example:
Code:
Trigger("some Trigger").fired("first Trigger").fired("other Trigger").fired("that Trigger")...
Furthermore, the
fired() method can be turned into its opposite by adding the value False as a additional second setting (see API). Then the
fired() Condition will not be met if the Trigger has fired - but rather if it
hasn't. You could of course use both has-fired and has-not-fired Conditions in the same Trigger. Example:
Code:
Trigger("some Trigger").fired("first Trigger",True).fired("second Trigger",False)...
Important! It is critical that the Triggers involved appear in a "logical" order in the
Scenario module. Because the bindings between the
fired() Condition and the labeled Trigger is created during setup - there can be no binding if no corresponding label has been loaded before the code actually looks for it. As a rule of thumb; enter all the Triggers that fire another Trigger first, and only enter the Triggers containing the associated
fired() Condition after these.
3.11 In-game messages (advanced feature)
It is possible to have a Trigger show any string value as a message on the main game interface. This is done with the
message() Action. If complemented with the
popup() method the same string value will appear in a pop-up window instead. This is useful when using longer strings.
The
message() method takes three boolean settings - see the API (section 2.6.4) for the specifics. If enabled these settings will try to fetch names (in string format) associated with the Trigger and the game event that prompted it to fire in the first place. The first name is always the Target Player's name in adjective form. The second one is the name of the city (if any) occupying the Target Tile. And the third name is a flag unit that has been killed.
Depending on which names are fetched these will be enumerated 1 to 3. (Note: Not 0 to 2.) Now, these names can be inserted into the string value making up the text message by using the tag
%s#, where the # character is substituted with the indexed number of the corresponding name. Example:
Code:
message("The %s1 city of %s2",bCivAdjective=True,bCityName=True)
The example above could spell out the message "The American city of Washington" if the Target Player was the American Civ and if the city of Washington happens to be located on the Target Tile. But the names could of course be placed the other way around, or be used several times in one message. Example:
Code:
message("%s3 was slain in %s2 of the %s1 Empire!",bCivAdjective=True,bCityName=True,bUnitName=True)
Text messages can - and should - however be stored in string variables instead of entered directly into the message() method. Example:
Code:
text="This is a in-game text message"
Trigger()...message(text)
It is also possible to change the color of a text message by utilizing the
effects() method. The first optional setting is
eColor and it requires knowledge of what value correspond to which color. (Hint: Some display colors have been defined in the
Consts module. The default value is 2, by the way, and stands for white.)
Additional effects that can be added include a tile pointer, a icon and a audio clip. See the API entry for the
effects() method for more on this (section 2.6.4).
Regarding pop-ups, it is possible to add a title by using the
bHeader setting of the
popup() method. The actual string value that will be used is the Trigger label, if one has been supplied within the Trigger constructor.
4. Frequently Asked Questions
Is PyScenario currently in development?
Unfortunately not.
PyScenario isn't working for me - can you make it work?
You are most likely doing something wrong, because the application works for other users. Make sure you are using it with the latest version of the
RFC Epic/Marathon mod-mod - and make sure you download the latest version of
PyScenario. Follow the installation instructions (section 3.1) - if you don't understand them chances are that you haven't installed
PyScenario correctly either.
Other than that - all Triggers in the
Scenario.py file must be valid or no Triggers are initialized - at all. Then there will be no scenario events. You need to be meticulous with your scripting! Check the spelling on every single thing and refer to the API section (chapter 2) of this documentation when in doubt. It'll actually save you time in the long run!
I think I've found a bug - where do I report it?
Refer to the
opening post of the beta testing thread.
Could you create a new method for my scenario?
Perhaps, time permitting. I'm trying to facilitate all request during this beta-testing period. On the other hand - you'd have to do the testing on the new feature yourself.
Wasn't this application available for the official version of Rhye's and Fall of Civilization once?
This is correct. Beta version 1 was specifically made for the official version of the RFC mod, but beta v2 was by popular demand constructed around the RFC Epic/Marathon mod-mod instead. There are no plans currently to make it available to the official version once again.
My script isn't working with the new version! How can I convert it to the new version?
You basically need to go through the script line by line and refer to the API entries of all the methods involved. And test your scenario properly once you're done. If you're not looking to add to your script or to take advantage of any of the recent changes, you could still benefit from using the current version as it includes several bug-fixes. In order to get your script for a older version to work (without converting a single thing) with the new version you simply add this line of code above your Triggers:
The PyScenario Trigger API adds nothing to CivIV Python - so what's the point then?
Well,
PyScenario is targeted at code-challenged scenario makers who don't know how to do their own Python scripting. It is an attempt to give some of the power of the programmer to non-programmers. But once it is released it might prove to be a useful tool for anyone who wants to be able to whip up a complex scenario on the fly.
I'm interested in getting into programming - can I learn Python by using this?
While it is possible to combine the features offered by this application with other Python code,
PyScenario is implicitly
not intended to be a learning tool. There is some depth to the application itself, but mastering it doesn't make you a programmer.
Can I assign variables and use those as arguments with PyScenario methods?
Yes, of course. Scripting with
PyScenario is done in Python, so any programming skill can still be useful. Using variables (constants) could however be considered an advanced user option and is not required nor documented in any detail.
Is there any user-friendly way to fetch indexed values for enumerated types?
There is no single best approach for finding all the enumerated values in the mod. Refer to section 3.6 in this documentation for some practical suggestions.
If you're comfortable with using Python code there is also a way to define you own integer constants for enumerated types in your script, using the
PyScenario function
eIndex(). It takes a pair of string arguments and these correspond to the type category and the enumerated item respectively. You enter them as they appear in the game. Example:
Code:
eIce = eIndex("feature","Ice")
eCyan = eIndex("color","Cyan")
eSlavery = eIndex("civic","Slavery"
I can't find any information on which order methods should be typed but its Conditions first and Actions last, right?
Actually, the order in which methods are entered is arbitrary and thus not described in the documentation. But for clarity's sake you could employ the habit of entering the Condition methods first. Note however that some methods add settings to others, and should thus be entered only after them. But it shouldn't be an issue as long as you keep things neat and organized.