Dev Diary #5: The scripting API and the decisions behind it

Pick one way to do each thing. Name things so they cannot be confused. Give scripts a window, not a key.

The last diary covered what scripts can do. This one is about how we decided what to let them say, because every function we hand to a scripter is a promise we have to keep. We made a lot of small decisions, and a few big ones, and we want to lay them out.

One way to do each thing

Give an operation one signature, with optional extras. When a script function needs a variation, add an optional argument with a sensible default. We do not add a second, similarly named function next to it. Two functions that do almost the same thing mean two things to document, two to keep consistent, and a newcomer who has to guess which one is right.

The rest follows from that. A given thing has one spelling everywhere it appears.

Use names, not numbers

Replace bare integers with named values wherever a choice is being made. Which side of a battle, where war score came from, what kind of diplomatic request, which measure of a good: each one is a named value now. Scripters write the name, and a typo is an error when the script loads, never a quiet wrong number that nobody notices until a war goes strangely.

The same goes for identifiers. A province, a state, a country, a good, a law, a culture each have their own named type, so a function that wants a province does not quietly accept a country.

Colours get the same discipline. Literal colours are written as red, green, blue, and alpha, with opaque as the default, and the packed form is kept for colours that come from game data. The interface also has a deliberately short list of themeable colours, seven in all, for things like window background, border, button, and text. A longer list would have been easy to add. A shorter one is how a theme stays coherent.

Hand out views, not collections

Give a script a snapshot to read, not the live list. Asking for a ship or a good returns a read-only view of it. A script cannot reach in and rearrange a collection that the game is in the middle of using. When something must change, it goes through an action, the same route as the interface in the last diary.

Refer to the thing itself

Let scripts hold a leader, a division, a ship, or a war directly, rather than juggling numbers that stand in for them. We approved this move across leaders, divisions, ships, wars and battles, casus belli, the links between provinces, owners, and states, terrain, state buildings, and a war goal's target state.

Direct references are only as safe as the checks behind them, so the checks come with the change. When a script hands the game a commander for an army, the game confirms the leader belongs to that country, is of general rank, and that the army exists, before anything is queued. It checks again when the action actually runs, because a lot can change in between. A reference that went stale, or that never belonged, does nothing.

One rule falls out of that: leader names must be unique within a country, across all ranks. Two leaders in the same country with the same name is an authoring error and the script call is rejected with a message telling you which. The same name in two different countries is fine.

Build it in the script first

Compose from what exists before asking for something new. We would rather a scripter solve a problem with the functions they already have than add one more function for one more case. The example we point at is the research completion date in the top bar. At one point we had a dedicated function for it in the game. We took it back out, and the top bar now works the date out itself from today's date, the technology's cost, your banked research points, and your daily gain, exactly as a mod could. We only add something new when the information is truly out of a script's reach, such as how a province's population changed over time.

Decide who owns the state

Say which scripts remember things and which do not. We went through each kind of content and made a call:

  • Decisions and state foci are descriptions. Everything that lasts, such as which focus a state picked or how far its effect has progressed, lives in the saved state of the country or state, never inside the script. Two players running the same decision are running the same description.
  • Casus belli and state buildings are things that exist in the world and carry their own state. That is the design. Saving a state building's own state is not finished yet, so we are not calling it done.
  • Events are the one we have not fully settled. We have chosen that events can be fired on demand or on an opt-in periodic schedule, that their choices are re-checked at the moment they are picked, and that pending events survive saving and rejoining a game. The scheduling itself still needs to be built.

Decisions are also country-only. We looked at decisions aimed at states, provinces, or any entity, and decided against it for now. A narrower rule is easier to make correct, and we can widen it when there is a real need.

Say no on purpose

Decline the large frameworks. We considered a system that would let a mod replace the core rules, such as how battles are calculated. We turned it down. The mechanics of the game stay where they are, and mods work through the hooks the game already offers. That is a smaller promise than a general replacement system, and we can keep it.

We also decided the base scripting library belongs to the game. A mod cannot replace or shadow it, but it can build on it: published pieces of the library can be included and extended through supported points, so a mod gets the foundation without being able to quietly change it.

Peace deals and the shape of war terms are deliberately left alone. We have not approved a new war score model, and we are not going to slip one in through the scripting interface.

Say what is still open

One question remains. When a mod wants to change how a particular piece of content behaves, there are three ways to do it: edit its numbers and values, swap out one named behaviour, or replace the whole script that defines it. Each has real costs. We have written out worked examples and not yet chosen. It will get its own diary once we have.

Tell us what is missing

Read the API, write something with it, and tell us where it fought you. The forum is open, and every awkward function you hit is a decision we would like to revisit.

← All dev diaries