Introducing: PyScenario - easy-to-use scenario event scripting for RFC

Baldyr

"Hit It"
Joined
Dec 5, 2009
Messages
5,530
Location
Sweden
PyScenario is exclusively designed for the mod-mod RFC Epic/Marathon, which is an alternate version of the Rhye's and Fall of Civilization mod adding two additional game speed options, as well as speed enhancements and even bug fixes. Other than that the game-play is exactly the same. PyScenario is no longer compatible with the official version of RFC.

Note: All scenarios should be designed for and tested with Normal speed for game speed compatibility reasons.

About PyScenario

PyScenario is a scenario event scripting tool and not designed for Python modding as such. (That would require actual programming.) Any scenario should be developed along side a custom World Builder Save file - unless the starting situation of the scenario is the default. In that case PyScenario works just as well with the pre-defined scenarios in RFC (the 3000 BC start and the AD 600 start, respectively).

Main selling point: The scenario maker is not required to know any programming at all! The user does however need to learn the specific syntax involved - but is not required to know anything about indentation or statements in Python, about logical expressions or about programming operations. In short: You don't have to be a computer scientist to use PyScenario.

Current status (2011-09-03): Beta version 2.2

Download here.
 
1. Introduction

PyScenario is built around the concept of "Triggers" that are comprised of "Conditions" and "Actions", along with "global" settings. The principle is that on every new game turn - and every time a "game event" takes place (like when a city or Tech is acquired) - all the Triggers will be checked. If all the Conditions of a Trigger passes then all the Actions are fired. This is the general principle.

The way to make Triggers is to type the "constructor" Trigger() into a script. This is the minimal requirement and the Trigger actually does nothing in itself, so you need to add settings and "methods" to it. Basically Conditions and Actions, as described above. The saved script is called a Python file (.py) or "module". A script/module can consist of any number of individual Triggers, but the script itself will be discarded once the game has initialized. Because then the Triggers will already been created and reside in the game memory and will be saved (and loaded) alongside the rest of the game as any other game feature.

1.1 Building Triggers

Because actual Python programming is not required in order to create scenario event scripts PyScenario has its own Trigger API (Application programming interface). See the following two posts for the full bounty! This is where you find all the "methods" available for you as the scenario maker. These methods are stacked onto the Trigger constructor and thus their settings will belong to that specific Trigger. The way PyScenario is designed you can stack as many methods you like onto one single Trigger, so its quite possible to make very complex things without getting lost in the script structure.

An example of use:
Code:
Trigger()
Then you would add methods to it like:
Code:
Trigger().check().valid().target()
Now, this example does absolutely nothing, but is still a valid Trigger and will not raise any exceptions (error messages) during gameplay. The important lessons here are however:
  • The constructor goes first and is the only "element" of the Trigger that begins with a capital letter.
  • Every method is separated from the constructor and from each-other by a dot character.
  • All the elements have a set of parenthesis after their name. Those are not optional.
What you do with the parenthesis is actually the interesting and useful part. Because you enter the actual settings inside the parenthesis. What methods you use and what settings you feed into these determines what the Trigger will do. And that is all there is to building a Trigger!

But as with everything even remotely connected with computer programming, syntax is extremely precarious! While the different elements of a Trigger are separated by dots, the settings inside these elements are separated by commas. You also need to keep in mind that everything you type is cAse sEnsiTivE! There is no alternative way of typing the script, so the faster you learn the basic syntax the quicker you will be able to start making your scenario script.

An example that actually achieves something:
Code:
Trigger().player(2).check().date(600).target(102,47).units()
This very simple Trigger is actually fairly complex if you take a look under the hood - the specifics are covered in the API section below.

But as a spoiler, what it does is that it spawns a Chinese unit in the Beijing tile on the specified date AD 600. The Conditions are "check()" and "date()" and there is one Action: "units()". The player() and target() methods are either Conditions nor Actions - they are instead used for specifying the "Target Player" and the "Target Tile" (which are global settings shared by the entire Trigger). This script translates into: "Check if the Chinese Civ is alive and not controlled by the human player on year AD 600. If these conditions are met, then - and only then - spawn one unit on the tile with the X coordinate 102 and the Y coordinate 47."

Furthermore, the Trigger will pick the type of unit spawned, depending on the technological level of the Chinese Civ on this turn in this game. Because certain settings can sometimes be omitted - this is a design feature - and therefore be automatically set to the default setting. Lets take a look at the API entry for the units() method:

units( tCoords=None, eType=None, iNum=1, bGarrison=False )

These are all optional settings as they already have different types of values assigned to them. (The different types of values are also described in the API section.) This is why we didn't need to put any values of our own inside the parenthesis of the units() method. We could have spelled out all values however:
Code:
Trigger(None).player(2,None).check(False,False,None).date(600,None).target(102,47).units(None,None,1,False)
It works all the same, but it would make more sense to include all the values when using something else than these default settings. And you only wanna use the default settings if they serve a purpose - otherwise you change them to whatever it is you need! But in the beginning it can actually make sense to always spell out all settings just to make things clearer. You can also include the names of the settings themselves if you like:
Code:
Trigger(label=None).player(ePlayer=2,eCivilization=None).check(bHuman=False,bDead=False,ePlayer=None).date(date1=600,date2=None).target(102,47).units(tCoords=None,eType=None,iNum=1,bGarrison=False)
Its still the same exact Trigger and the application won't even know the difference between the three examples! Compare these examples with the actual API entries for the methods involved and you'll soon understand how it all works.
 
2. PyScenario Trigger API

The API is a catalog of all the methods that can be used with PyScenario Triggers.

2.1 Types of values

First a brief overview of the different kinds of input that are used with the methods described below. You don't actually use the name of the setting but substitute it with a value. (For those of you who know programming - variables are of course also valid values.)

There are several types of values:
  • Boolean (marked with the "b"-prefix, like bHuman): There are only two valid values: True and False, although they can also be substituted with the numerical values 1 and 0 (zero).
  • Integer value (marked with the "i"-prefix, like iNum): This can be any numerical (non-floating point) value, even a negative value.
  • Enumerated type (marked with the "e"-prefix, like ePlayer): These values refer to different indexed types of game elements that are enumerated by the game with a value from zero and up. These are actually special types inherited from the game itself but most of the time they can be represented by regular integer values. (See section 3.6 for more on this topic.)
  • Tuple (marked with the "t"-prefix, like tCoords): This type is currently only used for tile coordinates and is expressed with the X and Y coordinate values separated by a comma inside a set of parenthesis. The entire tuple is considered to be one setting or one value. Example: (45,56)
  • String (not marked with any prefix, like name): This type is mostly used for city names, labels and text messages, and can be any set of letters inside a set of quotations. Example: "This is a text string"
  • None: This is a special type that can sometimes substitute one of the other types of settings. Then it basically means "no setting" and is frequently the default setting.

2.2 The Trigger constructor

Every line making up a PyScenario script starts with the Trigger constructor:

Trigger( label=None )
The constructor takes one optional setting and it must be a string value (unless left to its None state). It can be used to name the Trigger and is mostly used for identification and debugging. The label value can also be used for popup-messages (see section 3.11) and for binding together several Triggers to form bigger events (see section 3.10). Even if these options really are for advanced use only it can still make sense to document your script by using Trigger labels.

2.3 Global settings

The governing principle of PyScenario is that every Trigger is used with one Player only and is aimed at one single map "target". These global settings are referred to as the "Target Player" and the "Target Tile". It is however also possible to define multiple tiles for a Trigger - and such a "Plot List" can in some cases also override any single tile setting. Refer to the individual method entries in this API to see if and what a Plot List will do for a specific method.

Lets start with how to define a Target Player:

player( ePlayer=None, eCivilization=None )
The method can take two different - optional - settings, both aiming at setting one Player or Civilization as the Target Player. Both values can be expressed with an interger value.

The difference between the two of them is that the ePlayer setting refers to the Civilizations present in the World Builder Save file, also known as the scenario file - while the eCivilization value refers to the order in which the preset Civilizations appear in the XML portion of the mod. So the Chinese Civ would be the third player (ePlayer=2) in the mod but the eight Civ (eCivilization=7) found in the XML settings. Use what makes most sense to you, although the first option would be the default one within this particular mod.

The eCivilization value can by the way also be presented by a string value corresponding to the way the Civilization in question is defined in the XML files. Example: "CIVILIZATION_AMERICA"

random( <enumerated types> )
This method offers a less predictable way to set one of the supplied enumerated Player indexes as the Target Player. The method takes any number of integer values corresponding to valid indexed values corresponding to actual Players and picks one at random. By adding more than one instance of one Player reference it becomes more likely that it will be chosen. Example: random(1,2,4,4)

target( <settings> )
This method is the default way of defining a Target Tile - and it can also be used to define a Plot List - so it will be one of the most used methods in any scenario. Given a pair of integer values these numbers will correspond to the X and Y map coordinates respectively. (See section 3.7 for more on map coordinates.) But a single tuple value containing two integer values is also valid, so you can use whatever option feels most comfortable.

Defining a Plot List with this method will produce a rectangular map area. To achieve this you need to supply two sets of coordinates - the first one is always the lower left tile and the second one is the upper right tile making up the rectangle. Then all the tiles within this area are added to the Plot List.

Two sets of coordinates can either be entered as a pair of tuples - or as four separate integer values. So these are all valid examples of use:
Code:
target(34,56)
target((34,56))
target((33,55),(35,57))
target(33,55,35,57)
It could be noted however that the target() method isn't entirely necessary for defining a single tile as the map target, as many other methods can take map coordinates as settings (in the form of the tCoords setting). But target() is always a viable option if you feel more comfortable with using it.

tiles( <tuples> )
This method is used to define a Plot List consisting of any specified map tiles. Each entry has to be a tuple consisting of a set of coordinate values, as described above. These individual tiles can also be added to a Plot List already created with the target() method, see above.

area( ePlayer=None, bCore=True )
This method is used for setting the Plot List according to the pre-defined "areas" for each major Civ in the mod. There are basically three different areas associated with each Player, and they all affect the makeup of the fabled "stability maps". The main area is the "Core Area" - corresponding to the Civs spawn area in RFC. The second area is the "Normal Area" and the third one is the "Broader Area". These areas are all defined in the Consts module and are readily editable.

The bCore setting is set to True by default and this also means that the method will set all the tiles making up the Core Area as the Plot List by default. By changing bCore to a False value the method will set the Plot List as the Normal Area instead. A None value equates to the Broader Area.

The optional ePlayer setting can be set to determine what Civ's area should be fetched. (Because you might wanna use another Civ than the Target Player for this.) If set to any other value than the default None this setting will override any Target Player defined within the Trigger.

find( <strings> )
This method is a special option as it can be used to search for cities by name on a turn-to-turn basis. The method can take any number of strings as settings - as long as each string is enclosed in quotations and separated from the next one by a comma.

Note that only one city and one tile will be assigned as the target for the entire Trigger. So with multiple city name values the first one that is found when the Trigger is checked will be assigned. Also note that such a city target doesn't overwrite a Target Tile defined elsewhere! (It will however set both a additional Target City and a secondary Target Tile and a secondary Target Player for the Trigger on this one occasion.)

This method should only be used with some afterthought as the exact make up of cities will vary from game session to game session. (The actual names of can also vary.)

2.4 Special methods

once( <no settings> )
This method can be used to restrict any Trigger to only fire once (if all the Conditions are met, of course) and to expire thereafter. Note that some Action methods are already set to fire once only, but by no means all of them.

Note that this method doesn't accept any settings! Example: once()

operator( eOperator )
This is a advanced-user-only method that can be used to change how certain Condition methods work. These methods compare some game parameter with a integer setting of some sort, and the comparing operator can be changed with the eOperator setting. The valid values are as follows (the integer index values are also valid):
Spoiler :
OperatorTypes:

-1 = NO_OPERATOR
0 = OPERATOR_LESS_THAN
1 = OPERATOR_GREATER_THAN
2 = OPERATOR_EQUAL_TO
3 = OPERATOR_NOT_EQUAL_TO
4 = OPERATOR_GREATER_OR_EQUAL
5 = OPERATOR_LESS_OR_EQUAL

It is also possible to invoke the type on the class (OperatorTypes.OPERATOR_EQUAL_TO) or to use a string representation. Example: operator("OPERATOR_LESS_THAN")

---
(continued in next post)
 
(Chapter 2 continued...)

2.5 Conditions

Conditions are methods that set up the requirements for the Trigger to fire. You can use as many Conditions as you like for one single Trigger, but the Trigger will only fire provided that all of the Condition checks are cleared.

2.5.1 Game turn Conditions

This category of Conditions are used to fire the Trigger's Actions on specific game turns.

date( date1, date2=None )
This Condition is the main method for triggering scenario events with PyScenario. It takes one obligatory and one optional setting consisting of game dates. The most basic way to use the method is to simply supply a integer game year date. Then the Trigger will fire once at that date (or on the first turn beyond that date) and automatically expire thereafter. Example: date(-120) (Note that BC dates are represented by negative integer values while AD values are positive.)

Another use involves the second setting and changing it to a actual game date instead of the default value (None). Then the method will check for any game turn within an interval of game turns corresponding to those dates. Example: date(-120,50)

It should be noted that while there are only integer years on Normal game speed, there will be fractional years that are divided into months on slower game speed settings. So it is also possible to use floating point values to represent fractional years. Example: date(1901.0,1911.5) (Note that decimal fractions will be translated into a 12 month scale. Also note that you can't actually use a comma character as a decimal comma. Use period instead.)

It is also valid to use string values as date settings, and then it is also possible to specify the month (1-12) without using a decimal fraction. Example: date("1999:12")

And lastly, you can mish-match different kinds of values for the date settings as you want. As long as it makes sense it should also work, but beware of omitting quotation-marks for string values. (You could in fact only use string values as a rule of thumb, if you want.)

interval( iInterval=1, bRandom=False, bScaling=True )
This Condition makes the Trigger fire at turn intervals by limiting the valid game turns to ones that are equally dividable with the iInterval setting. Note that its not possible to change which actual game turns are valid - only the interval. Also note that the iInterval setting refers to game turn on Normal game speed and that if the last setting is left unchanged (bScaling=True) the interval will change dynamically on other game speeds. (Its roughly 1,5 times the interval on Epic speed and 3 times on Marathon speed, but there will be some unforeseeable rounding issues.) This can however be prevented entirely by changing the setting to False - then the turn interval will be exactly the same on all game speeds.

The second setting (bRandom) can be set to a True value and then the condition will be fired at random intervals. The probability is basically one in the iInterval value on Normal speed. The random interval scales as above on other game speeds.

waypoints( <settings> )
This Condition will fire on any game dates specified - and the method takes any number of such settings. Any date value that is valid with the date() method (see above) is also legal with waypoints().

2.5.2 Player Conditions

These Conditions are used to check different attributes of the Trigger's Target Player.

check( bHuman=False, bDead=False, ePlayer=None )
This Condition checks the Target Player towards two built in sub-conditions: The first one is by default set to not allow the Trigger to fire if the specified Civ is the human player. Only the human player will however be valid if the first setting is set to True. A None setting disables this particular sub-check entirely.

The second sub-check is done in order for the Trigger not to fire if the Target Player is dead. (If the second setting is set to True then it is possible to revive a dead Civ by, for example, spawning a city or some units for it. This is not intended practice however and may cause unintended issues.)

You should probably make a habit of using this method any time you specify a Target Player with the player() method. Note that the optional ePlayer setting doesn't set the Target Player for the whole Trigger but will instead override any Target Player for this method only. (So its a "local" setting - not a "global" one.)

contact( eRival )
This Condition clears if the Target Player has made contact with it's eRival counterpart.

era( eEra )
This is a dual use Condition that only has one single setting. It can both be used to check if the a Target Player is currently in a specified historical era - and to check whether the game session itself is currently in a the specified era. (These are two different things.) The usage will actually depend on whether or not there is a Target Player defined with the Trigger.

faith( eReligion )
This Condition will be cleared if the Target Player has the religion defined by the eReligion setting as it's state religion. (A "no state religion" condition equals a None value.)

policy( eCivic, eCategory=None )
This Condition checks whether or not the Target Player is using the civic specified by the eCivic setting. The other setting (eCategory) doesn't need to be used at all unless the civics themselves have been modded, as the application will lookup the civics category dynamically. It is thus advisable to leave it at the default None state...

war( eRival=None, bWar=True )
This Condition checks if the Target Player is at war with the Civ defined with the eRival setting. If the optional bWar setting is False then the Condition will instead look for a state of non-war (but not necessarily a binding peace treaty as such).

If the eRival setting is left to its default None state, then the Condition will pass if the Target Player is at war with any (alive and major) Civ.

2.5.3 Check/Set Conditions

These Conditions are used to detect tiles, cities or units currently in game and set global settings accordingly.

found( bFound=True )
This is a rather special Condition as its actually an add-on to the find() method. Example:
Code:
Trigger().find("city name").found()...
What it does, is it basically turns the find() method into a Condition that passes if any of the city names supplied is actually found. Refer to the find() method's API entry (section 2.3) for usage.

The optional bFound setting can be used to reverse the Condition (bFound=False) so that it is cleared if none of the city names looked up by the find() method are present in the game at the moment.

locate( <strings> )
This Condition has dual use, as it can both be used to check whether or not any units with one or several unit flags (supplied as string value settings) are present in the game. (If there is a Target Player defined within the Trigger, then the check is limited to units owned by that Civ.)

The other feature is that once any units are found, then the map tiles occupied by those units are added to the Plot List. (If no Plot List is defined elsewhere in the Trigger, then one will be created! This of course allows for using various Actions as part of the same Trigger with that Plot List.)

owned( tCoords=None, eOwner=None, bVassals=False )
This Condition has several built-in features. First and foremost it can be used to check if a map tile (which can be defined with the optional tCoords setting) belongs to a specific Civ (which can be defined with the optional eOwner setting). If the second setting is left to its default value (eOwner=None), then the Condition will lookup whether or not that Target Tile belongs to the Target Player - and if it does the Condition has passed.

The third boolean setting (bVassals) can be set to True in order for all tiles belonging to vassal Civs also be counted as belonging to the specified player.

But if no players is defined - at all, then the Condition will lookup what Civ - if any - is the owner of the Target Tile. If the tile is owned (by anyone) - then the Condition passes and a new Target Player is set for the Trigger. So the method can actually be used to fetch a Target Player based on tile ownership!

If used with a Plot List the method will have another use: Assuming there is already a Target Player defined with the player() method, then the Condition will cycle through all the tiles in the Plot List until it finds one that belongs to that Civ. This tile will then be set as the Target Tile for the Trigger!

present( eIndex=None, bPresent=True, bReligion=True, bCenter=False )
This Condition checks whether or not a specified religion or corporation - or any religion or corporation - is present in a city or in any city that is included in a Plot List. If a Target Player is defined within the Trigger then the check will only be performed on tiles owned by that Civ.

The main settings are eIndex which specifies what religion/corporation the method should be looking for, and bReligion which determines whether not not the method should be checking religions (bReligion=True) or corporations (bReligion=False). The default None value for the eIndex setting is the same as "any" religion/corporation.

The bCenter setting on the other hand will turn the Condition into a check against holy cities or corporate headquarters (bCenter=True), respectively. The bPresent setting is set to True by default and will make the Condition pass if and only if the requested (or any) religion/corporation/holy city/headquarters is present. A False value will only make the check clear if whatever eIndex refers to is not present.

There is yet another functionality built into this method: If the Trigger doesn't already contain any map target (a Target Tile or a Plot List) then any tile containing a city with the specified religion/corporation is added to the Plot List. And if the bCenter setting is enabled (bCenter=True) then the method will set the city where the holy city or corporate headquarters is located as the map target for the entire Trigger. So the method can in effect be used for dynamically finding the cities containing the eIndex features and thereby allows other Actions to affect those tiles/cities also.

valid( tCoords=None, bDomestic=False, bForeign=False, bLand=True, bWilderness=False, bAdjacent=False, bCity=False, bRandom=True )
This Condition checks the Target Tile's validity, as for tile ownership, terrain and also the presence of any cities or units. By default, the Condition is not met if any city or enemy unit is present on the Target Tile itself or on any adjacent tile. The bAdjacent setting can be used to limit this to only the Target Tile itself. The bCity setting can be set to True in order to make city tiles (and tiles adjacent to city tiles) valid.

The Target Tile is also invalid if it contains Marsh or Jungle but this condition can be lifted by setting the bWilderness setting to True. The bLand setting controls whether or not the Condition is checked for land or sea tiles. (The default setting automatically makes all sea tiles invalid, but a False value will make all land tiles invalid instead.)

The method can also be used to prohibit the Trigger from firing if the Target Tile is not within the Target Player's borders (bDomestic=True) or is within foreign borders (bForeign=True).

If used with a Plot List the valid() Condition will loop through all associated tiles until it finds a valid tile. Then that tile will be selected as the Target Tile. This is useful for spawning units over random map areas - without causing invalid spawning. But the tile can also be checked in order of appearance and not randomly (bRandom=False).

It is highly recommended that this Condition be used together with the city() and units() Actions in order to prevent unforeseen results!

The first setting (tCoords) can be used to define the Target Tile, even if the Target Tile can also be defined elsewhere within the Trigger.

2.5.4 Game Event Conditions

These Conditions have in common that they will only be checked when a specific type of "game event" takes place.

captured( <strings> )
This Condition is checked whenever a special "flag unit" has been killed in combat. Any number of unit flags (string values) can be entered as valid settings. If a Target Player has been defined with the player() method, then the unit must have been killed by this Civ and no other. But if there is no Target Player the Condition will be met whatever the Civ responsible for the unit's demise.

Flag units are created with a combination of units() and flag() Actions. (See section 2.6.1 for more details on these methods. Also refer to section 3.9 for more on unit flags.)

lost( name=None, eReceiver=None, tCoords=None, bConquest=True )
This Condition is checked whenever a city changes ownership during the game. If only the default settings are used, then the Condition is met every time the Target Player loses a city by force. (If no Target Player is specified, then any Player is valid and the Condition fires any time a city is lost.) The settings can be used in any configuration to refine this basic Condition:

With the name setting (string) the Condition can be limited to a city with one specific name only. The eReceiver setting corresponds to the Player that is receiving the city. The tCoords setting can be used to limit the Condition to one map tile only. (That tile will also be the Target Tile for the entire Trigger). And lastly, the bConquest setting can be changed to False in order for the Condition to be met if the city is traded (peacefully) and the special value None can be used to make both conquest and trade valid conditions.

startup( bLateStart=None )
This is a special Condition that is only checked at initialization - before the first game/scenario turn. It always passes by default (the None setting), but if the bLateStart setting is set to True it will only pass if the AD 600 scenario is used - and with the False setting it will only recognize the 3000 BC scenario.

tech( eTech=None, eEra=None, bFirst=None )
This Condition is checked every time the Target Player receives a Technology advance. (If no Target Player is specified, then all players are valid! Note however that minor Civs are never valid.)

The first setting (eTech) can be used to restrict the Condition to one specific Tech, and the second setting (eEra) can restrict the Condition to any Tech belonging to one specific game era.

The bFirst setting can be enabled (True) or disabled (False) depending on if the Condition needs to be restricted to only the first time the Technology is discovered in the entire game, or if the condition shouldn't pass for the initial discovery only. The default setting (None) simply ignores these issues completely.

2.5.5 Counter Conditions

These Conditions will lookup certain values in the game and compare the to some integer setting. Each method has a default operator type for comparing the values, but this can be changed with the operator() add-on method (refer to section 2.4 for more details).

built( eBuilding, iNum=1 )
This Condition checks whether or not the building represented by the eBuilding setting is present on the Target Tile (which would have to be a city). If used with a Plot List the Condition will pass if the building is present on any city occupying any of the map tiles specified belonging to the Target Player (if one has been defined). The iNum setting can optionally be changed to another integer value if more than one instance of the building type is required.

If no Target Tile (or Plot List) is defined, then the method will only look for the Target Player and lookup every city belonging to this Civ. If no Target Tile nor a Target Player is defined anywhere in the Trigger, then the method will count the number of eBuilding instances in all cities currently present in the game.

Note that the application will dynamically interchange any default Building Type with its Unique Building replacement, where applicable.

cities( iNum, ePlayer=None, bCitizenCount=False )
This Condition passes if the number of cities belonging to the Target Player is equal to or greater than the iNum value. If the ePlayer setting is used (set to some other value than None) then the Condition will check the number of cities for that Civ instead. The last setting (bCitizenCount) can be set to True in order to make the iNum setting refer to the total number of population points instead.

heads( iNum=1, eType=None, tCoords=None )
This Condition is used to check if the number of units belonging to a specified unit type (eType) is equal or greater than the iNum value. If the eType setting is left with its default None value, then all unit types apply. If no Target Tile (or Plot List) is defined (with the supplied tCoords setting or elsewhere in the Trigger), then all map tiles also apply. And if no Target Player is defined, then the units for all Civs are counted and added up!

influence( iInfluence=1, bOwner=None, tCoords=None, ePlayer=None )
This Condition looks up whether or not the specified Civ (either the Target Player - or the Civ specified by the optional ePlayer setting) has iInfluence percentage cultural influence on the Target Tile (which can optionally be set with the tCoords setting) - or above.

The bOwner setting can be set (bOwner=True) to require the cultural owner of the tile to be the specified Civ - or not (bOwner=False).

Note however the difference between what Civ actually owns a map tile and what Civ owns it "culturally". The owned() Condition (see above) can be used to determine whether or not the Target Player actually owns the tile in question.

solidity( iStability )
This Condition will be passed if the stability rating of the Target Player is less than the iStability setting.

2.5.6 Miscellaneous Conditions

fired( label, bFired=True )
This method is used to check whether or not a specific Trigger has already fired (one or several times). The Trigger is identified by the label setting that is also present in the Trigger constructor itself. The method creates a binding between the Condition and the Trigger. The bFired setting can be used to turn this Condition into its opposite (bFired=False) - the Condition will only be cleared if the binded Trigger hasn't fired.

This is useful for creating compound events involving several interconnected Triggers. Refer to section 3.10 for more on this subject.

---
(continued in next post)
 
(Chapter 2 continued...)

2.6 Actions

Actions are events that take place if the Conditions (described above) of a Trigger are met. (If no Conditions are present then the Actions will always fire!) You can use several Actions for one single Trigger.

2.6.1 Spawn Actions

This category of Actions are used to spawn cities and units belonging to the Target Player. (If no Target Player is defined then the application will pick a random Minor Civ instead.) The city() method can be complemented with several other Actions (see 2.6.2 below) and there is also a sub-category of add-on methods to be used with the units() Action (listed below).

city( iX, iY, name=None, iSize=2 )
This Action will spawn a city at the coordinates defined by the iX and iY settings. If the name setting (string) is omitted then the game/mod will name the city according to the default setup. The default city size is 2 but it can be changed to whatever value above zero by changing the iSize setting.

It should be noted that any cities present on adjacent tiles will be removed in order to make room for the spawning city! This is why it is a good idea to use the city() Action in tandem with the valid() Condition, as it will prevent this from happening. (Unless this is the desired outcome, of course.)

units( tCoords=None, eType=None, iNum=1, bGarrison=False )
By default this Action spawns one unit on the Target Tile (which can optionally be set with the tCoords setting) but the number of units can be changed with the iNum setting. (If no Target Tile is supplied within the Trigger, then the units will be spawned in the capital of the Target Player by default.)

If a Plot List has been defined, but no Target Tile, then it is possible to use the valid() Condition to pick out a valid spawn location from the available tiles. (See section 2.5.3 for more details.)

The eType setting determines what unit type the spawning units should have. If the setting is left to its default value (None), then the application will automatically detect what the default defensive unit of the current era for the Target Player would be.

The last setting can be enabled (bGarrison=True) to change the behavior of the method: If a Plot List is present, then there will be iNum number of units of the eType type spawning in all cities within the Plot List, belonging to the Target Player. Also, if no eType is specified (eType=None) then the default unit type will be the one that would be available for conscription (mostly melee units early on though) in the city.

AI( eType )
This is a advanced user method that can be used with the units() Action. The single setting (eType) is the desired AI setting for the spawned unit, and can either be expressed with an integer value - or set by a string value. Example: AI("UNITAI_ATTACK")

flag( flag, bSetName=False, bSerialize=False )
This is a dual-purpose Action that is used with the units() Action. The first setting is a string value representing a unique name for the unit. Example: "Flag Unit"

By default this name isn't visible in-game but is rather an internal attribute of the unit. It can none-the-less be recognized by the captured() Condition (see section 2.5.4 for details).

The bSerialize setting can be enabled (by setting it to True) in order to give several units differentiated unique names. (Otherwise they will all have identical flags, which might be sub-optimal in some situations.) In this case these units will be numbered from 1 and up. Example: "Flag Unit1", "Flag Unit2", "Flag Unit3"...

The other use for this method is to actually rename the unit (with the setting bSetName=True) to whatever the flag value is. It can of course be used to name several units created by one single units() method. (The bSerialize value has no bearing on the outcome.)

See section 3.9 for more details on using units flags.

promotions( <enumerated types> )
This method is used together with the units() Action, and will also interact with the XP() method (below). It takes any number of values corresponding to enumerated (indexed) unit promotions as settings. The specified promotions will then be granted to the unit(s) regardless of unit type and the availability of the promotions.

XP( iLevel=None, iXP=None )
This method is used with the units() Action. There are a couple of ways to use the two available settings:

If the the first setting (iLevel) is changed to any value other than the default (None), then the second one (iXP) can be omitted as this value will be automatically calculated from the first one. While all this XP will be considered to already have been "spent", the application will automatically pick out the appropriate number and type of promotions corresponding to the current unit level and type.

If only the second setting (iXP) is set to any integer value, then the specified amount of unit experience isn't considered to be spent, and is thus available for promotions.​

2.6.2 Map Actions

This category contains Actions that are used to manipulate map tiles - and cities located on those map tiles. Defining a Target Player is optional but no default Civ will be appointed if omitted. A Target Tile - or a Plot List - is required however. Some methods have an internal tCoords setting (tuple) that can be used for setting the Target Tile, but if set to the default value (None) it will do nothing (any other Target Tile will still be valid).

buildings( <enumerated types> )
This Action takes any number of values corresponding to enumerated building and wonder types as settings. The buildings will be created in the city occupying the Target Tile (or present within a Plot List), unless they are already present. If the default building class of a Unique Building is used then the application will detect what the Civ specific building type really should be and creates that instead. Note that wonders already built can't be spawned with this method! (Moving the Palace National Wonder is achieved with the capital() Action method.)

capital( tCoords=None ) new!
This Action removes the current Palace building for the Target Player and moves it to the city occupying the Target Tile (which can be defined within the method with the tCoords setting).

culture( iCulture )
This Action adds (or subtracts) culture points to the target tile according to the obligatory iCulture setting. (This in turn will have an impact on the ownership percentage of a tile.) If the Target Tile is a city, then the method will change the culture points of the city, as well as of the tile itself.

This method can be used with the city() or flip() Actions in order to give the newly acquired city a culture boost, which in turn is reflected in the expansion of cultural borders. And it can also be used to grant culture points to several tiles, that are a part of a Plot List, at once.

degrade( tCoords=None, bFeatureSafe=True, bImprovementSafe=True, bResourceSafe=True, bCitySafe=True )
This Action is used to change Grassland tiles into Plains tiles, and Plains tiles into Desert tiles. (It will also change Tundra tiles into Snow.) If a Target Tile is set the change is certain, but if used with a Plot List there is a 50% chance that any given tile won't be affected by desertification.

The bFeatureSafe setting represents the ability of vegetation to prevent soil erosion and is enabled by default. With a False setting map tiles with Forest or Jungle will also become arid, but the terrain features as such aren't affected.

The bImprovementSafe setting is also enabled by default and prevents terrain improvements from being destroyed on affected Desert tiles. Furthermore, the bResourceSafe setting protects special resources from being destroyed by the dust bowl...

erase( tCoords=None, bResource=False, bImprovement=False, bFeature=False, bSign=False )
This Action is a variant on the terrain() method (below) and offers another way of deleting special resources, terrain improvements and terrain features from a specified map target (either a Target Tile or a Plot List). Setting any of the boolean settings to True will erase that aspect of the tile. Note that routes are also included in the bImprovements setting!

flip( name=None, eOwner=None, bMinorsOnly=True, bAIOnly=True, bSafe=True )
This Action hands over the reigns of a city to the Target Player. The default way of defining the city - and thereby the Target Tile - is to use the name setting (string). (To define a Target Tile - or a Plot List - are equally valid options.) Actually, if the name setting (string) is used in tandem with other map target definitions, then it will work as a built-in condition. (The city by that name has to be on the Target Tile or within the Plot List.) It is also possible to specify the prerequisite current owner of the city with the eOwner setting.

Only cities belonging to Minor Civs and controlled by the AI are, by default, subject to city flipping. These settings (bMinorsOnly and bAIOnly, respectively) can however be disabled (set to a False value).

Civs losing cities due to flips are by default safe from automatic collapse (bSafe=True), but only if the eOwner setting is also used (set to other value than None).

kill( tCoords=None, bUnitsOnly=True, flag=None )
This Action will kill all units belonging to the Target Player on the defined map tile or area. If no Target Player is defined, then the units of all Civs will be affected.

If the default bUnitsOnly setting is changed to False cities are also affected! (Note however that there is no need to use the this method to make room for spawning cities, as that is actually a built-in feature of the city() Action.)

The third setting (flag) can be defined as a string value in order to limit the killing to only units with this exact "flag". (See chapter 3.9 about units flags.) The method can also be used to destroy any unit with the corresponding flag - anywhere on the map and belonging to any Civ - if no Target Tile and no Target Player is defined within the Trigger.

And finally, if no map target is to be found anywhere in the Trigger - and there is no unit flag defined - then the method will kill all units belonging to the Target Player! (Assuming there is one defined. The method can't however be used to kill off all units belonging to all Civs!)

migrate( iPopulation, eNationality=None, bSettlers=False, bWorkers=False, bRefugees=False )
This Action adds or subtracts population points (with the iPopulation setting) from any city located on the Target Tile (or included in a Plot List). The second setting (eNationality) can be used to specify what Civ those populations points should be associated with. This will affect the cultural makeup of the city! When subtracting population with a specific cultural origin, only as many citizens that are corresponding to the amount of culture present in the city will be taken away.

Also, unless population points subtracted aren't supposed to simply vanish into thin air, they could be turned into Settler and Worker units (1 and 2 units per population point respectively) with the bSettlers and bWorkers settings. These units will belong the city owner by default but they could alternatively belong to the eNationality Civ - if the last setting is enabled (bRefugees=True).

name( newName, oldName=None, tCoords=None )
This Action is used for renaming cities and actually has its own built-in conditions. The only required setting is newName (string) but the Target City needs to also be specified somehow. This can either be done by also specifying the current city name setting (oldName) or by specifying the actual map coordinates of the city - either with the tCoords setting - or by another method altogether.

If both a Target Tile and the current city name has been defined, then the city coordinates and the city name need to match - or the Action will not be carried out. If a Target Player has been defined, then the city also needs be owned by that Civ.

output( eBuilding, iFood=None, iProduction=None, iCommerce=None, iHappiness=None, iHealth=None, iGold=None, iResearch=None, iCulture=None, iEspionage=None )
This action affects what a specified building type (eBuilding) produces in a specific city. (It doesn't affect all buildings of that class or type in the game.) The settings should be self-explanatory.

religions( <enumerated types> )
This Action takes any number of indexed values corresponding to enumerated religion types as settings. While it might seem very straight forward, it actually sports some less than obvious behavior.

The basic use would be to set the religions present in a city occupying the Target Tile. Note however that religions not specified will be erased, so it both adds and subtracts at the same time. (With no settings what-so-ever it will erase all religions present!)

But if you use it with a Plot List it will only grant the specified religions to the specified area - not take any away.

So the first option (one single Target Tile) should be used to set the exact makeup of religions in a city, and the second option should be used to spread a religion (or several religions) to some city (or some cities).

sign( tCoords=None, label=None ) new!
This Action is used for putting signs on the Target Tile, which can be defined with the tCoords setting. The label setting is the actual string that will appear on the map, and if left to the default None state it will print out the Trigger label instead. Note that if the Trigger has a Target Player, then the sign will only be visible to that player!

terrain( tCoords=None, eTerrain=None, eResource=None, eImprovement=None, eRoute=None, eFeature=None, iVariety=0, ePlot=None, bSign=False, bOverride=False )
This Action can be used for changing map tiles in any conceivable way! There are some built-in limitations, however, but these can easily be overlooked (bOverride=True). The other settings define what aspect of the tile will be changed. (The iVariety setting is used in conjunction with the eFeature setting.) Note that the value -1 is the same as "erase the current resource/improvement/route" while the default None value does nothing.

There is also an optional bSign setting that will erect a label sign on the Target Tile if set to a True value. The label string will be the same as the Trigger label. Note however that if the Trigger has a Target Player, then the sign will only be visible to that player! (Also see the sign() method.)

yields( iFood=None, iProduction=None, iCommerce=None, iHappiness=None, iHealth=None )
This Action can be used to change the base yields of any city. The first three settings affect the city's Food, Production and Commerce output. The last two modify happiness and health rating.

2.6.3 Player Actions

These Actions affect the Target Player and thus one is required (see section 2.2).

civics( eGovernment=None, eLegal=None, eLabor=None, eEconomy=None, eReligion=None, eExpansion=None, iAnarchy=0, bScaling=True )
This Action changes the makeup of the Target Player's civic options. The first six settings refer to the various civics categories, while the iAnarchy setting can be changed to any positive integer value representing the number of game turns the actual change will take. The last setting (bScaling) will allow for the length of the anarchy to scale with game speed with the default setting (True).

collapse( bFragment=False )
This Action will split up the Target Player's cities between several minor Civs. If the optional bFragment setting is set to the default value (False) the Civ will also be eliminated from the game. But if this value is set to True, then the Civ will retain one or a few cities while the other ones are distributed among the minor Civs.

commerce( iGold=None, iResearch=None, iCulture=None, iEspionage=None )
This Action will permanently add or subtract any number of commerce type yields for all cities belonging to the Target Player. The actual settings should be self-explanatory.

convert( eReligion, bSpread=True, iTimer=1, bScaling=True )
This Action makes the Target Player change its state religion to the eReligion value. (A None value equals "no state religion".) The optional bSpread setting also spreads eReligion to the capital city of the Target Player, and is enabled by default.

Addition: The iTimer setting determines how many game turns the conversion will take - the Target Player is prohibited from changing the state religion during this time. The bScaling setting will scale the amount of game turns according to the current game speed - if left to the default True value.

discover( <enumerated types> )
This Action takes any number of indexed values corresponding to enumerated Technologies as settings, and grants them to the Target Player.

gold( iGold )
This Action is used to add or subtract Gold to or from the Target Player.

rebirth( iNum=None, bScaling=True, ePlayer=None ) new!
This Action adds Golden Age turns to the Target Player - or the player defined by the optional ePlayer setting. The lenght of the Golden Age is expressed as the iNum amount of game turns, and this scales with Game Speed by default (bScaling=True). Using a False value will disable scaling. If no iNum setting is used the game will use the default Golden Age lenght, which incidentally varies with Game Speed (and can be individually set for each Civilization).

reset( bDiplomacy=True, bStoredData=True, bCivics=True, bReligion=True )
This Action can be used to reset a number of settings for the Target Player and should be used with some afterthought. The first setting (bDiplomacy) will first and foremost cancel all active trade deals with rivals - including Open Borders and Vassal-Master relationships, but it will also end all wars and sever contacts with everyone. The second setting (bStoredData) will set a number of values that are stored by the mod to the default setting (basically a zero value) - including the Stability rating. (For AI Civs this also means that they will be spared any collapse coming their way!) The third setting (bCivics) will set all Civic options to the default lowest or most primitive settings. And the last setting (bReligion) will wipe any selected state religion from the Target Player.

Also, this method will always put an end to any Plague ravaging the Target Player's cities.

respawn( eEra=None, bTechs=False )
This Action does three separate thing at once. Firstly it resets the Target Player just as it would with the reset() method (see above) with all the default settings. Secondly it will make the Target Player reappear for the duration of the Trigger only - if dead. So it is possible to spawn cities for or to flip cities to the Target Player with the same Trigger! (If the Civ has any cities or units once the Trigger has fired it will be counted as being alive.)

The third feature can be set with the optional settings: eEra can be used to define what technological era Target Player should belong to. If the bTechs setting is set to True then all technological advances belonging to all previous eras will be granted to the Target Player!

stability( iStability )
This Action can be used to add or subtract Stability points for the Target Player.

strike( iNum=1, bRandom=False )
This Action emulates what would happen if the Target Player would be out of money with a negative cash-flow. This basically means that some number units - defined by the iNum setting - go on "strike" and are thus disbanded. The strike will only last one game turn - unless the Trigger is fired repeatedly.

The bRandom setting determines if the default rule for disbanding the "weakest" unit is in effect (bRandom=False) or if a random unit should be picked instead (bRandom=True).

treaty( eCounterpart, bPeace=True, bOpenBorders=False, bDefensivePact=False, bOverride=True )
This Action can be used to force the Target Player and any Civ defined by the eCounterpart setting to sign a peace treaty - but only if they are at war at the moment. By setting the bOpenBorders and/or the bDefensivePact setting to True (default setting is False) there will also be an Open Borders and/or Defensive Pact agreement in effect.

As the bOverride setting is enabled by default, the Action takes no notice of any existing hindrance for a treaty. But this feature can be disabled (bOverride=False).

If the bPeace setting is changed to a False value this Action can be used to make the Target Player declare war on eCounterpart!

vassalize( eRival )
This Action makes the Civ defined as eRival become a vassal of the Target Player.

2.6.4 Message Actions

These methods can be used to have in-game messages displayed once the Trigger fires.

message( message, bCivAdjective=False, bCityName=False, bUnitName=False )
This Action has both basic and advanced usage. The first setting is however a string value and this text will simply be displayed at the top of the main game interface once the Action fires. (It is advisable to keep such messages short.) Nothing complicated about that. Example: message("This is a in-game message")

But the method can also be used to insert names from the game into the message. This is done with the last three optional settings. By setting any of these boolean settings to True the method will try to identify what Civ the Target Player is (bCivAdjective) and fetch the adjective form of its name, whether or not there is a city at the Target Tile and what the name of that city is (bCityName), or what the name of a killed flag unit is (bUnitName). All three can of course be enabled at the same time. These names can then be inserted into the text message supplied with the first setting. See section 3.11 for more on the finer points of messaging.

effects( eColor=None, bPoint=False, icon=None, sound=None )
This is a add-on method to the message() Action. This is strictly for advanced users but there is some help available in section 3.11 (in the Tutorial).

The second setting can be enabled (bPoint=True) to have the Target Tile (if any) flash in the in-game minimap and also to include an arrow that points to the tile on the main game map. The first setting (eColor) can in turn be used to change the color of the text message - and of the tile pointer.

The last two settings are string values and require some know-how and knowledge of how the files comprising the game are setup. Without going into any specifics here and now, the third setting (icon) can be used to change the default icon of the pointer arrow, and the fourth setting (sound) can be used to add a sound effect to the message() Action.

popup( bHeader=False )
This is another add-on method to the message() Action. What it does, if attached after the parent method, is that it turns the text message into a pop-up message instead.

The optional setting (bHeader) can be set to True in order to use the Trigger label as the title of the pop-up window.​
 
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:
Code:
CheatCode = chipotle
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:
Code:
Trigger = Compatibility
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.
 
Shouldn't it say "Object oriented programming languages in general and Python in particular" in your opening post, Python being an example of OO languages? :crazyeye:

Me being a Grammar fanatic aside, this looks fairly awesome! I'll definitely give it a try when your explanations are finished and I have the proper time to look at it all. :)
 
Shouldn't it say "Object oriented programming languages in general and Python in particular" in your opening post, Python being an example of OO languages? :crazyeye:
Well, I really wouldn't know... But I do think that Python was around before the advent of OOP. But this is probably what you meant? :rolleyes:

The whole text is pretty much just a draft and I'm not gonna bother with either typography or proofreading at this point. Because the Alpha version - and later the final release - may turn out different anyway.

And while I'm on the subject - the next version will not have any specific support for RFC! :eek: But there will eventually be a special edition for RFC. :D Because I'm doing this to be able to make my own mod-scenarios within the RFC framework. :king: The application itself deserves a wider audience, though.

I'm actually thinking about incorporating the BUG mod into the framework, as it seems to have really good potential for modding. :goodjob:
 
The tutorial is now online - the only thing missing at this point is the new RFC patch and public beta testing is on! :goodjob:

It is however quite possible to start designing events already, as the documentation is completed.
 
I've added a news entry on rhye.civfanatics.net
Thanks! :goodjob:
Would be great to have all this stuff insterted somewhere in the pedia too. There it wouldn't get lost
Yeah, I was thinking the same thing. But, as this is all tentative information and since a final release is still some time in the future, there is no hurry. (The beta version files haven't even been uploaded yet, by the way.)

The documentation for the final version might look very different, also. I was actually planning on taking out all the technical terminology and dumbing the language down a bit. In order for non-programmers to be able to get into it easier.

Also, the application is designed with multi-use methods rather than a myriad of smaller ones. This means that the methods really have both basic, advanced - and hidden - features. This could be documented in layers also, so that a beginner would first, say, learn how to add a turn Condition and maybe how to make it into a turn span. Later on, the same user might wanna check into how to make stuff spawn at intervals. But there would also be documentation available that shows how this interval can be made into a random one!

The final documentation would be easy to get into - but also cover more advanced use and less than obvious features. So there would be some depth to it.
 
Since Rhye posted the final release of RFC today, here are the files. :goodjob:

It would be greatly appreciated if someone, anyone, could actually download, install and try them out. (You can use the sample script posted above for testing purposes.)
 
This is a idea I had recently:

Since PyScenario can only be used to add content to RFC - not to erase or modify any of Rhye's spawns - there could be a add-on that converts all of the city, unit and terrain spawns into one single PyScenario script! The affected Python modules (Barbs and Resources, mostly) would have to be replaced though so that there are no duplicate spawns.

Because then it would be easy to add, erase and change anything! Like move a spawning city or change the spawn year. To add XP/Promotions to spawning units - or to change a turn interval into a random interval. Or to change the actual terrain type with dynamic resources. It would probably be a good idea to add some religions and buildings to spawning cities also...

I will probably start work on this shortly, but it will require testers! Is anyone interested? (It would involve playing a regular game of RFC but with the PyScenario script instead of Rhye's Python spawn code, and report any irregularities.)
 
I'm afraid I'm currently busy with keeping my own modmod up to date with Rhye's new version :D
 
I'm afraid I'm currently busy with keeping my own modmod up to date with Rhye's new version :D
Yeah, but once we're all set at our respective ends it would be easy to merge them. :king:
 
Since PyScenario can only be used to add content to RFC - not to erase or modify any of Rhye's spawns - there could be a add-on that converts all of the city, unit and terrain spawns into one single PyScenario script! The affected Python modules (Barbs and Resources, mostly) would have to be replaced though so that there are no duplicate spawns.
I've attached a Scenario.py file containing all of Rhye's scenario events, along with replacement modules for Barbs and Resources. Make sure to backup your originals!

This should work just the same as regular RFC, even if the code responsible for the spawns is completely different. This means that the technical aspects of this won't be identical, but rather the equivalent of these spawns scripted within the PyScenario setup. Hopefully the player will never notice any difference.

Note that the Scenario module supplied is adapted for the 3000BC scenario - the Triggers to use with the AD600 scenario are commented out and can easily be implemented. (Just make sure to comment out the 3000BC version if you uncomment the AD600 variant! Some 3000BC specific Triggers should also be enabled entirely!) Everything is documented with a comment at the end of the line in question.

Also note that there is no special support for Viceroy level, so all barbarian spawns will always be enabled. (The amount of barbarian spawns does vary with difficulty level though.) It would be possible to introduce a difficulty level Condition to PyScenario, though, but it is really necessary? :confused:

The idea is, then, that the scenario maker can also make modifications to the spawns that ships with the mod. You could change dates, for example, or the number of units spawned. Or you could add a religion to spawning cities. Or add units to city spawns. Or add XP/Promotions to unit spawns. It is of course possible to completely rework Rhye's scenario events... Or to disable some of them.

I will probably start work on this shortly, but it will require testers! Is anyone interested? (It would involve playing a regular game of RFC but with the PyScenario script instead of Rhye's Python spawn code, and report any irregularities.)
This still applies, so please feel free to download, install and try it out! Any full game report will of course be credited for testing in the final release. :goodjob:
 

Attachments

  • RhyeSpawns.zip
    8.6 KB · Views: 271
:goodjob: I'll test the new file to see if it's working properly.

Edit: I started up a Greece game and first thing i noticed was it took 5-10 times longer to load?
 
Edit: I started up a Greece game and first thing i noticed was it took 5-10 times longer to load?
To load the game? At initialization of the game, the mod or of the game session itself? What process took longer than usual?

The Scenario module is loaded into memory when the game session commences, and only that one time. I guess that would be at the time you get the Dawn of Man popup. That will of course take some time, as all the Triggers are being initialized. But it should be a matter of seconds, or so I would expect...

How did you go about installing the files? Are you using both PyScenario.py, the replacement CvRFCEventHandler.py and the replacements for Barbs.py and Resources.py?

Have you enabled debugging? Because an exception (error message) could possibly slow things down, especially if its reoccurring. (This could indicate that you haven't installed all the necessary files.)

edit: You need both these files and the files posted above. They all go into the \Assets\Python\ folder of the RFC mod. Here's how to use debugging.
 
It was while simulating the turns not loading the game and yes I had both files I will enable debugging.
 
Top Bottom