Rules.csv Tutorial

From Starsector Wiki
Jump to navigation Jump to search

Official Tutorial

A first-party explanation and tutorial for rules.csv is available on the Starsector blog: "You Merely Adopted Rules.csv, I Was Born Into It".

Tutorial: Allegiance Micro-Mod

This tutorial is of a micro-mod that does one small thing: it adds a menu option to all planets that have markets that will change their allegiance to the Hegemony. Going through the mod should teach the basics of how the rules.csv system works, how to write a script that is called from rules.csv, and what the basic structure of markets is in SS.

The script that changes the faction is heavily based on (ie only mildly edited to remove extra parts) code from Nexelerin. So thanks to Histidine, Zaphide before him, and the rest of the mod mafia for creating excellent mods.

Part 1: rules.csv

The rules.csv system is a spreadsheet based programming mini-language that Alex made to make creating content easier. Each row defines: a name, how it is activated, a test to see if it really runs, an arbitrary script it can execute, text to display to the player, and a way to create menu items. It is almost entirely undocumented and to learn how to do it you have to dig through the base game’s rules or other mods.

Let’s look at the rules.csv from this mini-mod: it creates a dialog option on every planet that has a market, and clicking on this option runs a script.

RulesExamples.png

The first row creates the dialog option.

The id, COL_AllTest must be a unique identifier. I’m using COL as a prefix; it's highly recommended in your own mods to use a prefix for all ID’s to avoid conflicts with other mods.

The trigger for the first line, “PopulateOptions”, is called by the base game when it is making the opening dialog screen for planets/stations. There isn’t really any documentation about triggers the base game calls, but you can look in its rules.csv and find out. Since I’ve made a new rule with this trigger, whenever the base game is making the dialog screen for planets/stations, it thinks about using my rule too.

The condition for the first line is “$hasMarket”. This has a few important things to it. First of all, the ‘$’ sign means that hasMarket is a variable stored by the planet/station or dialog. There is no logical operator (<, >, ==, !=, etc), so the condition just sees whether hasMarket is True or False. If it is True, then the rest of the rule is run.

Script and Text are blank: this rule doesn’t display anything to the player and by itself doesn’t make changes to the market. This is because all this first rule is doing is setting up a menu item.

Finally, options, the fast way of making menu items. This entry reads:

9:COL_OptionTest:Spread the Hegemony

This is actually 3 pieces of information separated by colons (:). The number is the default position in the dialog - this will try to go to number 9, so in most dialogs it will be the last option. The entry ‘COL_OptionTest’ is the ID of this menu item, which is used in the next line of rules.csv. Finally “Spread the Hegemony” is the text displayed in the dialog menu.

Ok, thats line 1! Moving on to line 2: this line is called when the player clicks on the dialog option created in line 1, and it runs a script.

Its id is COL_OptionTestID - just an identifier.

Its trigger is another one that you should know: DialogOptionSelected. This trigger is called every time the player selects a dialog option, from every dialog in the game. It is extremely useful.

But the rest of the rule only applies if the condition is true: $option == COL_OptionTest. The ‘$’ sign means ‘option’ is a variable stored by the dialog. In this case it is a special variable that stores the ID of the last menu item clicked. On line 1 in options we set the dialog’s ID to COL_OptionTest, so when we click on it, $option is set to COL_OptionTest and the condition evaluates to true.

The script entry is: TestChangeFaction. Note that there is no ‘$’ sign - this tells the rules that this is not a variable, but a full script to be found in an external file. We’ll go over the script in part 2 of the tutorial, but for now know that this is where the work of changing the market’s faction is done.

Text is some text displayed to the player letting them know what madness they have caused by foolish clicking.

Options is left blank: this rules entry runs a script, but does not add any dialog options.

I hope this has been a good introduction to rules.csv. For a more general introduction that shows more features but is incomplete, see this one prepared by Alex: https://s3.amazonaws.com/fractalsoftworks/doc/StarsectorRuleScripting.pdf

Part 2: The Script

Before we go to actually examining the script, you should know that there is a fair bit of set up involved before you make your own. First of all, the scripts must be compiled into a jar: this is most easily done by using a IDE, and there are several tutorials on the forums about how to set one up for SS. Personally I used Netbeans, using a 1.7JDK targeting Java 7.

Second of all, the scripts must either be in a special folder or you must tell the game where to look. The special folder location is: com\fs\starfarer\api\impl\campaign\rulecmd

Yes, 7 folders deep. This may be ok for the base game as it is very complex and needs to keep track of a huge number of files, but for this mod its ridiculous. Instead I’m using: data/campaign/rulecmd. Much more reasonable in my opinion.

In order to tell SS to where to look, I needed to add an entry to data/config/settings.json. One quick note, when SS loads .json files in mods, if they share the same name and location as a base file (which this does), the files are merged. The exact way they are merged is a little complicated, but for our purposes all it means is that we don’t have to rewrite all of settings.json: just the parts we care about. The file reads:

 {
 	# where rule commands called from rules.csv can be implemented.'
 	# rules from multiple packages CAN NOT share the same name	
 	"ruleCommandPackages":[
 		"data.campaign.rulecmd",
 	],		
 }

We’ve added the location where our script resides to where the base game will look. This does not delete the normal locations it looks, only add to them a new entry.

Finally time to look at the script. I’ll break things down by section; the full file is located in jars/src/data/campaign/rulecmd in the mod.

 package data.campaign.rulecmd;
 import com.fs.starfarer.api.Global;
 import com.fs.starfarer.api.campaign.FactionAPI;
 import com.fs.starfarer.api.campaign.InteractionDialogAPI;
 import com.fs.starfarer.api.campaign.SectorEntityToken;
 import com.fs.starfarer.api.campaign.econ.MarketAPI;
 import com.fs.starfarer.api.campaign.econ.SubmarketAPI;
 import com.fs.starfarer.api.campaign.rules.MemoryAPI;
 import com.fs.starfarer.api.characters.PersonAPI;
 import com.fs.starfarer.api.impl.campaign.ids.Factions;
 import com.fs.starfarer.api.impl.campaign.ids.Ranks;
 import com.fs.starfarer.api.impl.campaign.ids.Submarkets;
 import com.fs.starfarer.api.impl.campaign.rulecmd.BaseCommandPlugin;
 import com.fs.starfarer.api.impl.campaign.shared.PlayerTradeDataForSubmarket;
 import com.fs.starfarer.api.impl.campaign.shared.SharedData;
 import com.fs.starfarer.api.util.Misc;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;

A whole gigantic mess of imports! And yes, the mod needs all of them. If you use an IDE this all automatically correct. Use an IDE and save yourself hours of work.

public class TestChangeFaction extends BaseCommandPlugin {

All scripts called by rules.csv must extend BaseCommandPlugin. More details are 2 sections below.

     public static final List<String> POSTS_TO_CHANGE_ON_CAPTURE = Arrays.asList(new String[]{
         Ranks.POST_BASE_COMMANDER,
         Ranks.POST_OUTPOST_COMMANDER,
         Ranks.POST_STATION_COMMANDER,
         Ranks.POST_PORTMASTER,
         Ranks.POST_SUPPLY_OFFICER,
 	 Ranks.POST_ADMINISTRATOR
     });     
     private static final String newOwnerId = "hegemony";

These are some data stored by the class that it will need later. While they don’t technically need to be static final for the code to work, I’m trying to be more java-like in my coding and I don’t intend for these to be changed. Note that if I were writing a script to change to an arbitrary faction, then newOwnerId would need to not be final, so it could be changed. It probably wouldn’t be a static class variable either, but hey, enough of this.

     @Override
     public boolean execute(String ruleId, 
             InteractionDialogAPI dialog, 
             List<Misc.Token> params, 
 Map<String, MemoryAPI> memoryMap) {

The function ‘execute’ is called by SS each time the script is run, so by defining it we can tell the script what to do. It must return a boolean value. The base game passes 4 variables in which we can make use of:

String ruleId: gives you the name of the rule that called it.

InteractionDialogAPI dialog: This gives you the dialog that was just interacted with. See the API for full options, but it lets you get the interaction target (a fleet, station, person, planet, etc), start a battle, end the dialog, display text and options, etc etc. If you are using a script 9 times out of 10 this is the place to start to do what you want.

List<Misc.Token> params: These are objects that can be passed to the script from rules.csv. I’m not using them and honestly don’t understand them yet. Perhaps tutorial #2?

Map<String, MemoryAPI> memoryMap: This is a map that can return various memory objects.The String argument should be of the form MemKeys.____. The MemKeys strings are (NOTE: this is not in my code, but from the API):

 public class MemKeys {
 	public static final String GLOBAL = "global";
 	public static final String PLAYER = "player";
 	public static final String MARKET = "market";
 	public static final String SOURCE_MARKET = "sourceMarket";
 	public static final String FACTION = "faction";
 	public static final String LOCAL = "local";
 	//public static final String PERSON = "person";
 	public static final String ENTITY = "entity";
 	public static final String PERSON_FACTION = "personFaction";
 	public static final String MISSION = "mission";
 }

The MemoryAPI you get back depends on the entity in question and the scope you give it. Note that these variables stored in data are the same as you can set in rules.csv! For example if you had set $global.COL_TestVar = 3.14 using rules.csv, then calling 

memoryMap.get(MemKeys.GLOBAL).getFloat("COL_TestVar")

Would return 3.14 in the script.

Moving on is the real meat of the code, where the faction change is done:

         if (dialog == null) return false;
         SectorEntityToken target = (SectorEntityToken) dialog.getInteractionTarget();

This checks to make sure the rule didn’t get called without a dialog. If it did, then “return false” gets us out of the script before it tries to do something with a non-existant dialog and crashes. If not, then get whatever the dialog is interacting with.

         FactionAPI newFaction = Global.getSector().getFaction(newOwnerId);
         MarketAPI market = target.getMarket();
         if (market==null) return false;//sanity check

Next we get the faction corresponding to the string we defined earlier, get the market associated with the dialog, and make sure that the market really exists.

Now to set the faction of everything in the market! First, linked entities, which can occur for example when you have a planet and station both sharing a market:

         List<SectorEntityToken> linkedEntities = market.getConnectedEntities();
         for (SectorEntityToken entity : linkedEntities)
         {
             entity.setFaction(newOwnerId);
         }

Then the people:

         //Change people to the new faction
         List<PersonAPI> people = market.getPeopleCopy();
         for (PersonAPI person : people)
         {
             // TODO should probably switch them out completely instead of making them defect
             if (POSTS_TO_CHANGE_ON_CAPTURE.contains(person.getPostId()))
                 person.setFaction(newOwnerId);
         }

Then the market itself, and all its submarkets. The submarkets are the open, military, and black market tabs that we see in game. Free transfer markets and those not hooked to the economy are not changed to the new faction.

         //set market to the new owner
         market.setFactionId(newOwnerId);         
         //set submarkets to the new owner
         List<SubmarketAPI> submarkets = market.getSubmarketsCopy();         
         for (SubmarketAPI submarket : submarkets)
         {
             String submarketId = submarket.getSpecId();             
             if (submarket.getPlugin().isFreeTransfer()) continue;
             if (!submarket.getPlugin().isParticipatesInEconomy()) continue;

A bit of cleanup to handle smuggling and whether or not the player is locked out of hostile markets:

             // reset smuggling suspicion
             if (submarketId.equals(Submarkets.SUBMARKET_BLACK)) {  
               PlayerTradeDataForSubmarket tradeData = SharedData.getData().getPlayerActivityTracker().getPlayerTradeData(submarket);  
               tradeData.setTotalPlayerTradeValue(0);
               continue;
             }  
             submarket.setFaction(newFaction);
         }         
         // don't lock player out of freshly captured market
         if (!newFaction.isHostileTo(Factions.PLAYER))
         {
             market.getMemoryWithoutUpdate().unset("$playerHostileTimeout");
         }

Reapplying the conditions on the planet. This is important because some of them are faction specific. Luddic Majority is an example: when we turn the planet from Luddic control to Hegemony, the stability should drop by 4.

         //Make sure conditions are good
         market.reapplyConditions();

And finally finishing by returning true.

         //done!
         return true;        
     }
 }

And that's the entire mod!