Fractal Softworks Forum

Please login or register.

Login with username, password and session length
Advanced search  

News:

Starsector 0.97a is out! (02/02/24); New blog post: Planet Search Overhaul (07/13/24)

Author Topic: Rules Clinic  (Read 775 times)

David

  • Global Moderator
  • Admiral
  • *****
  • Posts: 941
    • View Profile
Rules Clinic
« on: September 20, 2024, 05:03:26 AM »

Hello modders!

In regard to this blog post, I had an exchange with someone? somewhere? about the notion of helping modders out with weird rules scripting cases. That conversation kinda trailed off and nothing came out it, but I'd like to extend this offer generally.

Are you having trouble getting something to work in rules? Do you have a question about how to do something? I'd like to help, if I can.

And I think it'd be useful reference to have these answers in a thread (unless one exists? I haven't seen one specific to Rules before, lately.)

So anyone got a question?
Logged

David

  • Global Moderator
  • Admiral
  • *****
  • Posts: 941
    • View Profile
How to add a character and say hello and goodbye
« Reply #1 on: September 26, 2024, 01:16:04 PM »

Someone was kind enough to send me a PM with a question that involves rules + some other stuff. I'm going to go ahead and answer it here.

Here's what they wanted to know:

Quote
  • How to create an NPC
  • How to assign said NPC to a station/planet
  • Basic interactions with NPC (via rules.csv) (Saying hello/goodbye)

The weird thing is that I don't know how to make mods because that's not what I do. I think the following should be correct - I asked Alex for help with a couple items - but feel free to let me know if something is wrong and I'll fix it.

Creating an NPC

So presumably your mod has some kind of code it runs to set itself up, like ModPluginOnNewGameAfterEconomyLoad(). You could create a call to make the character in there. Let's use Rayan Arroyo as an example.

Code
import com.fs.starfarer.api.Global;
import com.fs.starfarer.api.campaign.PersonImportance;
import com.fs.starfarer.api.campaign.econ.MarketAPI;
import com.fs.starfarer.api.characters.FullName.Gender;
import com.fs.starfarer.api.characters.ImportantPeopleAPI;
import com.fs.starfarer.api.characters.PersonAPI;
import com.fs.starfarer.api.impl.campaign.ids

These all get used when we make a character.

Code
public static String ARROYO = "arroyo";

ModPluginOnNewGameAfterEconomyLoad() {
   
   ImportantPeopleAPI ip = Global.getSector().getImportantPeople();
   MarketAPI market = Global.getSector().getEconomy().getMarket("eochu_bres");

   if (market != null) {
      PersonAPI person = Global.getFactory().createPerson();
      person.setId(ARROYO);
      person.setFaction(Factions.TRITACHYON);
      person.setGender(Gender.MALE);
      person.setRankId(Ranks.SENIOR_EXECUTIVE);
      person.setPostId(Ranks.POST_ENTREPRENEUR);
      person.setImportance(PersonImportance.HIGH);
      person.getName().setFirst("Rayan");
      person.getName().setLast("Arroyo");
      person.setPortraitSprite(Global.getSettings().getSpriteName("characters", person.getId()));
      person.getStats().setSkillLevel(Skills.BULK_TRANSPORT, 1);
      person.getStats().setSkillLevel(Skills.INDUSTRIAL_PLANNING, 1);
      person.addTag(Tags.CONTACT_TRADE);
      person.addTag(Tags.CONTACT_MILITARY);
      person.setVoice(Voices.BUSINESS);
         
      market.getCommDirectory().addPerson(person, 2);
      market.getCommDirectory().getEntryForPerson(person).setHidden(true);
      market.addPerson(person);

      ip.addPerson(person);
   }
}

Some of this is self-explanatory, like "ImportantPeopleAPI" is used for important characters in the game you will want to be able to access via code. Likewise, the market contains the comm directory where the person can be contacted. (You don't need to add a character to a market. Some characters who cruise around in their own fleets like Caden and Hyder don't get added to a market.)

Character skills aren't really used unless they appear in combat, but I like adding them for flavor.

Voice sets a value on the character called $voice which is used in some generic dialog interactions that may show up if you don't override them. Mostly used in procedurally generated character interactions (planet admins, bar encounters, patrol leaders). They are:

  • aristo : a rich, stuck-up aristocrat
  • official : an official-sounding bureaucrat
  • business : cheerfully interested in profits and making deals
  • villain : a slightly over the top evil-sounding villain
  • soldier : a gruff military professional
  • faithful : a Luddic faithful, into blessings and prayer
  • pather : like the Luddic, but more militant and aggressive
  • spacer : halfway between working class and talks-like-a-pirate
  • scientist : a slightly egotistical, distracted scientist
Assigning an NPC to a market

Code
market.getCommDirectory().addPerson(person, 2);
This adds Rayan Arroyo to the third position in the comms directory (we start counting from 0, of course).

Code
market.getCommDirectory().getEntryForPerson(person).setHidden(true);
This makes Rayan Arroyo invisible at the start of the game. To make him visible, we call "SetPersonHidden arroyo false" in a rules script block.
(We can call "SetPersonHidden arroyo true" to make him hidden again, if we like. We could also call "MovePersonToMarket arroyo kantas_den" to make Arroyo set up shop at Kanta's Den instead of Eochu Bres at some point during the game.)


Also important: Rayan Arroyo's portrait.

Presumably there's a settings.json in your mod. It needs something like this in it:
Code
"graphics":{
"characters":{
"arroyo":"graphics/portraits/characters/arroyo.png",
},
}

That way when we set the portrait to "arroyo", graphics/portraits/characters/arroyo.png will show up.


Basic NPC Interactions

Let's start with a simple greeting and goodbye. This block goes into the rules csv for your mod.

(Sorry about the tables - this forum doesn't make for very readable formatting.)

idtriggerconditionsscripttextoptions
testArroyoGreetingPickGreeting$id == arroyo score:1000ShowPersonVisual
SetShortcut cutCommLink "ESCAPE"
"This better be good." Rayan Arroyo looks you over disapprovingly, "Remind me to take you shopping for a proper suit. I know a guy who can do wonders."cutCommLink:"I've heard enough. Good day, sir!"


With just this, you'll open the comm link to Arroyo, he comments on your fashion sense, and you can cut the link in a huff.

Let's go over this block by block.

id
testArroyoGreeting is our unique id for the rule

trigger
PickGreeting is called when you begin a conversation with a character via the comm directory (or by opening comms with their fleet). If we did not include this override for PickGreeting, the dialog would automatically use the rule with id convDefaultGreetingBusiness for Arroyo (because he has $voice == business attached to his memory context).

scripts
ShowPersonVisual displays Arroyo's portrait. The default PickGreeting rule would handle this, but we need to call it because we're overriding that rule.
Similarly, SetShortcut cutCommLink "ESCAPE" attaches the option with the key "cutCommLink" to the ESCAPE key on the keyboard so we have a default hotkey to exit the conversation.

text
This is shown in the text box.

options
We set one option, cutCommLink, which is a default option to exit a conversation and close the dialog box.


It's not a very interesting conversation, so let's mix it up just slightly while appreciating some of the default dialog structure!

Here's a version of Arroyo that will use the default dialog greeting and exit. We're just adding a new dialog option which will not appear again after the player chooses it once:

idtriggerconditionsscripttextoptions
testArroyoAskAboutSuitPopulateOptions$id == arroyo
!$askedAboutSuit
Arroyo says he knows about where to get a nice suit. Maybe if you asked nicely...testArroyo_askAboutSuit2:"Say, could you tell me where to get a nice suit?"
testArroyoAskAboutSuit2DialogOptionSelected$option == testArroyo_askAboutSuit2$askedAboutSuit = true
$player.gotArroyoTailorAddress = true
AddTextSmall "Received address for Arroyo's tailor on Eochu Bres" highlight
FireAll PopulateOptions
Arroyo raises an eyebrow. "I thought you'd never ask. Here."

He taps something, and with a satisfied ping the address of a tailor appears on your TriPad.


Remember, the default PickGreeting calls all valid rules under the trigger PopulateOptions. So if we added more options with $id == arroyo in the conditions block, we could add more unique options. Here, we've added just one more option which will appear in addition to the default cutCommLink rule.

$id == arroyo:
This condition requires that the active character must have the id "arroyo"

!$askedAboutSuit:
This means that the condition $askedAboutSuit must be false. That variable gets set to true after you use the option asking about the suit so the option to ask doesn't show up again.

Once we fire the testArroyoAskAboutSuit2 rule, some interesting stuff happens in the script block:

$askedAboutSuit = true:
$askedAboutSuit is set to true on Arroyo, which will stop the condition for getting to this dialog option again.

$player.gotArroyoTailorAddress = true:
This sets a memory flag on the player memory context so (maybe) we can add a new option elsewhere in the game based on the condition $player.gotArroyoTailorAddress being true. Maybe the player can go visit Arroyo's tailor and buy a nice suit!
...And, to be honest, $askedAboutSuit is redundant if we use this. The option to ask about the tailor could simply check that !$player.gotArroyoTailorAddress. But I'm showing off how different scopes of variables work here.

AddTextSmall "Received address for Arroyo's tailor on Eochu Bres" highlight :
This adds a highlighted line of small text to the dialog window. This is usually used to emphasize some important action the player has performed. Note that this text is added AFTER the rule's text block; if you want to add normal-looking text before this special text is added, you either have to write a rule that first makes the special text then triggers another rule with a text block, or you can call the AddText script command. (I don't usually use this except in very simple cases because formatting this text is more work).

FireAll PopulateOptions :
Because this rule has nothing in the options block we have to call this because otherwise we're left hanging with no options, which causes an error. This will bring back the default cutCommLink option, and anything else PopulateOptions would show.



Hopefully that all makes sense. I'm slightly thwarted by the default table style here, alas. Happy to answer anything that's confusing.
Logged

Kaysaar

  • Admiral
  • *****
  • Posts: 508
    • View Profile
Re: Rules Clinic
« Reply #2 on: September 27, 2024, 03:41:17 AM »

Maybe i have question
How does score works in condition lets say
$market.isSurveyed
$market.isPlanetConditionMarketOnly
$market.mc:pre_collapse_facility score:10000
!$market.aotd_fac_explored
!$tag:gas_giant
!$market.aotd_failed_pre_collapse
like we have here all conditions and i gave only one score:10000
What that will make then?
Does it work same  like here?
$entity.fleetType == aotd_expedition score:10000
Sorry to ask but rules.csv were never my strong side.
Logged

David

  • Global Moderator
  • Admiral
  • *****
  • Posts: 941
    • View Profile
Re: Rules Clinic
« Reply #3 on: September 27, 2024, 05:04:27 AM »

Score is useful when the conditions for multiple rules can be successfully fulfilled.

So (say) you call "FireBest DialogOptionSelected". This will evaluate every rule with the trigger "DialogOptionSelected" and try to find the best-fit rule.
  • All rules that fail ANY condition are discarded. Score doesn't matter here.
  • Then we take the set of all rules that succeeded all conditions and take the top scoring rule.
  • (And if there are multiple top-scoring rules with the same score, we choose a random one.)
So putting score:10000 into a condition set of a rule is basically saying that if this rule's conditions can be fulfilled, I definitely want this one and not any other. It's very useful for mission/quest tags you want to override default rules, and for character-specific rules you want to override defaults.

(If you instead did a "FireAll" trigger, it would accept all rules that successfully fulfill their conditions. Score would not be important.)
« Last Edit: September 27, 2024, 06:55:15 AM by David »
Logged

Kaysaar

  • Admiral
  • *****
  • Posts: 508
    • View Profile
Re: Rules Clinic
« Reply #4 on: September 27, 2024, 06:03:37 AM »

Score is useful when the conditions for multiple rules can be successfully fulfilled.

So (say) you call "FireBest DialogOptionSelected". This will evaluate every rule with the trigger "DialogOptionSelected" and try to find the best-fit rule.
  • All rules that fail ANY condition is discarded. Score doesn't matter here.
  • Then we take the set of all rules that succeeded all conditions and take the top scoring rule.
  • And if there are multiple rules with the same score, we choose a random one.)
So putting score:10000 into a condition set of a rule is basically saying that if this rule's conditions can be fulfilled, I definitely want this one and not any other. It's very useful for mission/quest tags you want to override default rules, and for character-specific rules you want to override defaults.

(If you instead did a "FireAll" trigger, it would accept all rules that successfully fulfill their conditions. Score would not be important.)
Thank you david!
Logged

David

  • Global Moderator
  • Admiral
  • *****
  • Posts: 941
    • View Profile
Re: Rules Clinic
« Reply #5 on: September 30, 2024, 07:35:38 AM »

My pleasure!
Logged

OptimisticSociopath

  • Ensign
  • *
  • Posts: 15
    • View Profile
Re: Rules Clinic
« Reply #6 on: September 30, 2024, 12:09:22 PM »

Hello there! I have a question regarding ship and weapon descriptions. I want to know how long the description can be? I have a... long descriptions that include some lore for my mod: text1 includes the overall description and text2 shows the greyed out description, right? Ok, so, when I but the long description in text1, the text2 is not visible. I get it: the text2 only shows when text1 has ended and when text1 is very long the text2 will never be shown. Also, what text3 is for?
Logged
"Always use "Incognito Mode" while in Hyperspace! :^)"

David

  • Global Moderator
  • Admiral
  • *****
  • Posts: 941
    • View Profile
Re: Rules Clinic
« Reply #7 on: September 30, 2024, 12:18:29 PM »

...

This isn't Rules related, so I'm going to have to forward you toward this thread: https://fractalsoftworks.com/forum/index.php?topic=5061.0 ; Alex can give you a better answer there!

Edit: However, there is a rules command to print a description attached to an entity. So you could do something like "PrintDescription 3" in the script block and it'd print text3 for the associated entity. We use this for the opening interaction for planets and other campaign entities, generally.
« Last Edit: October 01, 2024, 05:20:24 AM by David »
Logged

nissa

  • Ensign
  • *
  • Posts: 4
    • View Profile
Re: Rules Clinic
« Reply #8 on: October 03, 2024, 04:02:43 AM »

Hi David, thanks for taking the time for this. Out of curiosity, what's your debugging flow for something not working as expected with rules? Interested with regards to any learnings that might be extensible to modding as well as any tooling that you find helpful for this. Thanks!
Logged
I'm unlikely to see communications directed to this account; non-forum means of contact are more likely to be received successfully.

David

  • Global Moderator
  • Admiral
  • *****
  • Posts: 941
    • View Profile
Re: Rules Clinic
« Reply #9 on: October 03, 2024, 06:47:03 AM »

Hi David, thanks for taking the time for this. Out of curiosity, what's your debugging flow for something not working as expected with rules? Interested with regards to any learnings that might be extensible to modding as well as any tooling that you find helpful for this. Thanks!

Oh boy! Interesting question.

Problems generally arise when I try to do a lot of complicated things at the same time, especially when I'm trying to rush by writing them all at once without testing each piece individually. Figuring out the lies vs. truths vs. who knows what during a series of complex dialogs contingent upon previous actions/choices during The Usurpers mission was a bit of a mess, for instance.

Strategies for fixing stuff:

Isolate the problem

Instead of implementing two clever systems at once, make sure they work separately *then* combine the logic.

For example, I could make a character only give a limited number of responses - so I would need to have a counter per response then ensure a rule interrupts and plays some outro once you hit the limit. At the same time, responses could be based on outside variables. So if I was having trouble with this, I'd ensure the response counter works, then separately ensure the outside variable conditions for responses works, and then combine them afterward.

Print your memory variables & debug text

There's always a "dump memory" option when you're running dev mode. That's important to use. It is also helpful to explicitly put the variable you *think* you're accessing into the text box ie. "$player.visitedThePlanet", and it will print that variable value (or null) at the time the rule is executed. The results can sometimes be surprising if a lot is going on during the execution of a set of rules.

Or, oftentimes, the variable is simple spelled wrong or has a space at the end or something equally stupid. This happens *a lot*.

It's easy to mess up memory scope, too - like, which memory is held on which entity's memory context, and where precisely you're setting a memory. For instance (perhaps), setting $hadTheConversation = true after calling EndConversation in the script block will set that memory on the campaign entity rather than the character you intended.

Order of operations / executing many rules

Knowing when a variable is set vs. when you check it via a condition statement can get messed up if you're doing complex rules executions. I've had problems in the past where I swore I was setting a variable, but it was set later in the execution stack than the condition checking for it. An easy way to check this is to write "DOING THE THING" in an otherwise blank text box (while implementing), then "DOING THE THING2" in the next one etc. for rules that may not have text. Then the dialog output will tell you exactly which "hidden" rule is being executed, and in what order.

(Related, I object to node-based dialog editing because it limits your perspective to one rule at a time. A lot of complex, flexible results are possible when firing off sets of contingent rules. A conception of rules that sees it as executing a single rule at a time is missing out on all the fun of it. Granted, it can be a pain to debug. If you want to see a really stupid-but-works example, check out the rule "LuddicEthosRefresh".)

Debug options to reduce testing time

Testing is boring. Making it go as fast as possible during implementation is essential.  I'll often add rules options which set up a situation for testing ie. I know about this character, have this relationship, have tripped these memory flags, and have item X in my inventory.

Now, with that said, you actually have to test things holistically at the end of a development cycle because it's quite possible for something outside the immediate context to cause problems. We generally just "play the game" albeit targeting new features/interactions before releasing a patch -- and get our QA testers to do the same.

Get outside perspective

Get someone else to test your work. I often avoid doing things the 'wrong' way unconsciously because I know it's wrong - someone else won't know that, and they'll figure out how to break your dialog. It's extremely useful.

Debug

It is possible to run the game in debug mode and get live reporting on lots of what's going on; that's more for stuff going wrong on the Java side however (though this can be an issue, as rules & Java need to be closely integrated for certain weird mission events).

...

My nuclear option is always "ask Alex"*. You have that option too. Or you can ask me, here. :)

(*Sometimes his answer is "Hmm. Is this really the best way to do this?". And when he says that, I know I'm in trouble.)
Logged

nissa

  • Ensign
  • *
  • Posts: 4
    • View Profile
Re: Rules Clinic
« Reply #10 on: October 03, 2024, 08:18:14 AM »

That's super interesting and helpful - thanks for the thoughtful reply!
Logged
I'm unlikely to see communications directed to this account; non-forum means of contact are more likely to be received successfully.