What is Jomini?
Jomini is a shared library for our grand strategy games, it exists as a mid layer between the project and Clausewitz engine.
The goal is to move lots of the duplicated code we had in all our grand strategy games into Jomini so instead of copy pasting features all the time, we can just reuse them and also add improvements and bug fixed which the other projects can benefit from.
This is NOT the thread to be asking game level questions in.
If you have a question about pops, characters or trade etc. then that is a game specific thing not something in Jomini so should not be asked about here.
Jomini is not just scripting stuff (despite however much I pretend), it also contains:
Over the past year we’ve moved a lot of features into Jomini so they are shared, a fair few are just miscellaneous code improvements but we also have a lot of new features we can consistently use on the projects now, a not complete list of which I’ll cover some of them:
Event Targets
The idea of scope types and switching between them is in Jomini, the current Jomini scope types are: no scope, bool, value, color and flag. Yes numbers and bools etc. are a scope type, it has its pros and cons. We refer to these scope types as primitive scopes due to their basic nature and generally not having an object attached to it just the raw ID.
Every event or interaction has a “top scope” which stores root, saved scopes and local variables.
Event Targets are how we 1-1 switch between scope objects, they are comprised of one or more “links” separated by dots. Eg: root.mother.father
As they are separated by dots they can be used in one line so you can do
set_character_religion = root.father.mother.religion
A link can have multiple input types to lead to one output type, allowing polymorphic links that can do more than one thing! So “culture” can move from a province, character, country, pop etc. to their religion.
A scope object can be saved with an arbitrary name to reference later on in the top scope, in our older games these were called event targets. The name was changed as internally both were called event targets and one is shorter to type for script. Eg:
Comparing Scopes
Gone are the days of needing things like father = { character = root.mother.father } as a condition to see if two characters are the same.
Now we can just do father = root.mother.father, this works for any event target so you can compare things very simply.
This also works in conjunction with comparison triggers which are treated as a special link, all comparison triggers can be used at the end of an event target to get its value with relation to the last link eg: root.mother.mother.prominence will return root's grandmother’s prominence.
So now we can easily compare two values by doing things like root.loyalty > mother.prominence to check if root has more loyalty than their mother does prominence.
This can also be used in effects to do mother = { add_loyalty = root.prominence } which adds loyalty to root's mother equal to root’s prominence.
For numeric comparisons we have all comparison operators available: >, <, >=, <=, =, !=
Some examples based Old system vs the Jomini system:
Old:
Jomini:
Old:
Jomini:
Script Lists
Script lists are how we move from one scope to one or more from a list of similar objects. Eg: any_sibling
With the new system we internally only register the list builder such as sibling, the code then automatically makes the various versions for the script.
Currently we have four versions created: any_, every_, random_ and ordered_
The first three should be recognisable, but they’ve all been extended with new functionality
An example of finding someone with the highest value in old script vs Jomini:
Old:
Jomini:
Script Values
Shock and horror, we can now actually do maths in the scripting language!
With a new system for Jomini any trigger and effect that can take a value can now also take a script value which come in three forms:
Ranges are between two numbers/script values, eg: trigger_event = { id = cool_event.1 days = { 1 5 } } will launch the event in 1-5 days time.
Formulas are where the system really shines, it lets you compute numbers by doing basic arithmetic and rounding! It can also do conditional operations with ifs and else_ifs, do randomising and loops.
Formulas can also be done inline with where you call them instead of making a specific named formula.
Variables
Any non-primitive scope type can be made to store variables in it, which scope types to have them is a game level decision so if you find a scope that does not make a request for it to be added, variables themselves can be any scope type. You can store a value, bool, flag or character etc. inside of them.
This allows for recording a saved scope on a specific object instead of just in a top scope.
Variables can be stored in three places: a scope object (character, country etc.), locally in a top scope (like a normal saved scope) or globally in the game state.
Scoped variables exist as long as the object they are on exists (so for a character they are lost when they die), local variables exist as long as the top scope does and global variables exist until manually cleared.
For every variable trigger and effect there exists a version for each type, the format is prefix_<type>_suffix
eg: set_variable, set_local_variable and set_global_variable
variable_list_size, local_variable_list_size and global_variable_list_size
Variables themselves are treated as a scope object referring to whatever is stored in them allow you to scope to them.
One could have a best friend variable on a character which they save someone as the value then scope to that best friend variable to give the character a gift later on.
The event target link to scope to a variable depends on the storage type:
Value type variables can be manipulated with simple arithmetic as well as rounding.
Example of saving a permanent reference inside an object:
Old:
Jomini:
Example of rounding a value:
Old:
Jomini:
Scripted Blocks
As in our older games, Jomini has scripted triggers and effects. We also have scripted modifiers that function similarly but for mean time to happen constructs so you can reduce giant modifier lists.
All of them can take scripted arguments as in EU4
eg:
Scripted Lists are a new addition which let you create your own script lists based on a coded one with extra conditions:
This creates any/every/random/ordered_citizen which functions as any/every/random/ordered_pops_in_province + listed conditions.
Target & Variable Lists
You can now create custom lists of saved scopes or variables
They are added and removed with:
The list can be iterated over via:
Lists can be checked:
You can also have lists of variables in the current scope, locally or globally.
They are added and removed with:
They can be iterated over via:
They can be checked via:
Events and On Actions
Events have been moved to Jomini, the syntax is nearly identical to the older games but instead of defining the event type first you write its ID. Games are free to specialise their events as needed, for example Imperator includes some portraits on their events.
Mean time to happen events have been removed for numerous reasons, primarily because it is bad.
As a replacement for that on actions have been moved to Jomini and been opened up to allow scripter made on actions that function as neat containers of events and methods of random distribution.
We also no longer hack scope objects into root, from chains or this for on actions and instead give them a specifically named saved scopes. So if you have an on action from a battle with a winner, loser, location and war scopes you won’t have to comment in the files what they all mean instead you just use a clearly named scope of scope:battle_location etc.
Old:
Jomini:
Dynamic Descriptions
Events can now have their descriptions and titles be pieced together bit by bit from conditional and random parts to form dynamic text without needing to nest 20 pieces of custom localisation in multiple different files and folders. This lets you build some very complex event descriptions.
Sweet Sweet Documentation
We have previously made attempts at automatic documentation with varied success, some bits could be outdated or incorrect, some core information was not printed at all etc.
Now the "script_docs" console command has been moved to Jomini and using it in game will outputs the following information to separate files in your games log folder:
The script should now be a LOT better at logging errors, we made as much as possible get caught in Jomini with clear errors as to what is going wrong, individual effects and triggers will still need game level specific errors but those should also be a lot better.
GUI & Localization System
We have a new GUI system for the games which works with its own specif setup of scripting, it is also the same system used for the localization system. Collectively this is called the Data System
Everything you can run must be either registered in by the code or made as a scripted gui.
All things you can use in the data are split into four categories:
- Types, the type of an object which corresponds to its class/struct in the code
- Promotes, moving from an object of one type to an object of another
- Functions, calling a function on an object which returns another object
- Callbacks, calling a function on an object which does not return anything
Another thing to keep in mind is that the data system obeys (for the most part) how const works in C++. Without getting too technical functions, promote and callbacks can be marked as const only, which means that object which are const cannot call non-const. This is unlikely to affect you if you use the scripted guis though.
Scripted GUI
The scripted gui lets you evaluate and execute arbitrary script via the UI in a manner that will keep the game synchronized in multiplayer. You define the script in common/scripted_guis and can then reference that in data entries.
For example a cheat button to give you gold and take it from another character:
Do not make calls to GuiScope without a matching call to End, otherwise you are going to introduce a memory leak and the game will eventually crash.
The AI will make use of these buttons based on the ai_is_valid trigger and the ai_chance definitions.
Jomini is a shared library for our grand strategy games, it exists as a mid layer between the project and Clausewitz engine.
The goal is to move lots of the duplicated code we had in all our grand strategy games into Jomini so instead of copy pasting features all the time, we can just reuse them and also add improvements and bug fixed which the other projects can benefit from.
This is NOT the thread to be asking game level questions in.
If you have a question about pops, characters or trade etc. then that is a game specific thing not something in Jomini so should not be asked about here.
Jomini is not just scripting stuff (despite however much I pretend), it also contains:
- Gamestate
- Achievements
- Coat of Arms/Shield
- Save Games
- Multiplayer
- Map & Editor
- And more
Over the past year we’ve moved a lot of features into Jomini so they are shared, a fair few are just miscellaneous code improvements but we also have a lot of new features we can consistently use on the projects now, a not complete list of which I’ll cover some of them:
- Scope Types
- Event Targets
- Saved Scopes
- Script Lists
- Saved Lists
- Variables
- Script Values
- Comparisons
- Scripted Triggers and Effects
- Scripted Modifiers
- Scripted Lists
- Events
- Dynamic Descriptions
- On Actions
- Trigger and Effect Descriptions
Event Targets
The idea of scope types and switching between them is in Jomini, the current Jomini scope types are: no scope, bool, value, color and flag. Yes numbers and bools etc. are a scope type, it has its pros and cons. We refer to these scope types as primitive scopes due to their basic nature and generally not having an object attached to it just the raw ID.
Every event or interaction has a “top scope” which stores root, saved scopes and local variables.
Event Targets are how we 1-1 switch between scope objects, they are comprised of one or more “links” separated by dots. Eg: root.mother.father
As they are separated by dots they can be used in one line so you can do
set_character_religion = root.father.mother.religion
A link can have multiple input types to lead to one output type, allowing polymorphic links that can do more than one thing! So “culture” can move from a province, character, country, pop etc. to their religion.
A scope object can be saved with an arbitrary name to reference later on in the top scope, in our older games these were called event targets. The name was changed as internally both were called event targets and one is shorter to type for script. Eg:
Code:
father = { save_scope_as = cool_person }
scope:cool_person = { kill_painfully = yes }
Comparing Scopes
Gone are the days of needing things like father = { character = root.mother.father } as a condition to see if two characters are the same.
Now we can just do father = root.mother.father, this works for any event target so you can compare things very simply.
This also works in conjunction with comparison triggers which are treated as a special link, all comparison triggers can be used at the end of an event target to get its value with relation to the last link eg: root.mother.mother.prominence will return root's grandmother’s prominence.
So now we can easily compare two values by doing things like root.loyalty > mother.prominence to check if root has more loyalty than their mother does prominence.
This can also be used in effects to do mother = { add_loyalty = root.prominence } which adds loyalty to root's mother equal to root’s prominence.
For numeric comparisons we have all comparison operators available: >, <, >=, <=, =, !=
Some examples based Old system vs the Jomini system:
Old:
Code:
event_target:target = {
mother = {
root = {
character = prev
}
}
}
Jomini:
Code:
root = scope:target.mother
Old:
Code:
root = {
export_to_variable = {
which = my_loyalty
value = loyalty
}
}
mother = {
export_to_variable = {
which = my_prominence
value = prominence
}
}
if = {
limit = {
root = {
check_variable = {
which = my_loyalty
which = my_prominence
which = mother
}
}
}
}
Jomini:
Code:
root.loyalty > mother.prominence
Script Lists
Script lists are how we move from one scope to one or more from a list of similar objects. Eg: any_sibling
With the new system we internally only register the list builder such as sibling, the code then automatically makes the various versions for the script.
Currently we have four versions created: any_, every_, random_ and ordered_
The first three should be recognisable, but they’ve all been extended with new functionality
- Any: Is a trigger that returns true if any of the list meet certain conditions, can have an optional count or percent parameter to indicate X many or Y percent of the list must meet the conditions
- Every: Runs effects on all members of the list if they meet certain conditions. Can have multiple alternative_limits for backup conditions if the previous set was not met.
- Random: Runs effects one one member of the list if they meet certain conditions. Can also have alternative_limits as well as a weight to influence which random object to run effects on.
- Ordered: Runs effect on the entry in a list based on position or range of positions. The list can be ordered by anything such as loyalty or gold. Can have a limit and alternative_limits on it to filter members of the list.
An example of finding someone with the highest value in old script vs Jomini:
Old:
Code:
every_family_member = {
export_to_variable = {
which = my_loyalty
value = loyalty
}
}
random_family_member = {
save_event_target_as = highest_loyalty
}
while = {
limit = {
any_family_member = {
check_variable = {
which = my_loyalty
which = my_loyalty
which = event_target:highest_loyalty
}
}
}
random_family_member = {
limit = {
check_variable = {
which = my_loyalty
which = my_loyalty
which = event_target:highest_loyalty
}
}
save_event_target_as = highest_loyalty
}
}
every_family_member = {
remove_variable = my_loyalty
}
event_target:highest_loyalty = {
add_prominence = 20
}
Jomini:
Code:
ordered_family_member = {
order_by = loyalty
position = 0
add_prominence = 20
}
Script Values
Shock and horror, we can now actually do maths in the scripting language!
With a new system for Jomini any trigger and effect that can take a value can now also take a script value which come in three forms:
- Simple Values
- Ranges
- Formulas
Ranges are between two numbers/script values, eg: trigger_event = { id = cool_event.1 days = { 1 5 } } will launch the event in 1-5 days time.
Formulas are where the system really shines, it lets you compute numbers by doing basic arithmetic and rounding! It can also do conditional operations with ifs and else_ifs, do randomising and loops.
Formulas can also be done inline with where you call them instead of making a specific named formula.
Code:
example_fancy_gold = {
add = gold
multiply = 3
divide = 2
subtract = 500
max = 2500
min = 1000
}
num_non_cultured_provinces = {
value = 0
every_owned_province = {
limit = {
NOT = {
dominant_province_culture = root.culture
}
}
add = 1
}
}
province_pop_friction_size_svalue = {
value = 0
scope:pop_friction_enemy_province = {
if = {
limit = {
total_population > 3
}
add = {
value = total_population
divide = 4
ceiling = yes
}
}
}
}
Variables
Any non-primitive scope type can be made to store variables in it, which scope types to have them is a game level decision so if you find a scope that does not make a request for it to be added, variables themselves can be any scope type. You can store a value, bool, flag or character etc. inside of them.
This allows for recording a saved scope on a specific object instead of just in a top scope.
Variables can be stored in three places: a scope object (character, country etc.), locally in a top scope (like a normal saved scope) or globally in the game state.
Scoped variables exist as long as the object they are on exists (so for a character they are lost when they die), local variables exist as long as the top scope does and global variables exist until manually cleared.
For every variable trigger and effect there exists a version for each type, the format is prefix_<type>_suffix
eg: set_variable, set_local_variable and set_global_variable
variable_list_size, local_variable_list_size and global_variable_list_size
Variables themselves are treated as a scope object referring to whatever is stored in them allow you to scope to them.
One could have a best friend variable on a character which they save someone as the value then scope to that best friend variable to give the character a gift later on.
The event target link to scope to a variable depends on the storage type:
Code:
var:name
local_var:name
global_var:name
Value type variables can be manipulated with simple arithmetic as well as rounding.
Example of saving a permanent reference inside an object:
Old:
Code:
Good luck with that, depending on your game will depend on the hack you need to use.
Generally it is invisible opinion modifiers and flags to mark people.
Jomini:
Code:
# Event Chain:
set_variable = {
name = best_friend
value = root.father
}
# Totally separate event chain
root.var:best_friend = {
add_loyalty = 20
}
Example of rounding a value:
Old:
Code:
# Depends a lot on the gme you have, to my knowledge only CK2 has the effects needed
set_variable = {
which = test_var
value = 10.5
}
divide_variable = { # Now 5.25, I want to round to nearest 5
which = test_var
value = 2
}
set_variable = {
which = test_var_remainder # 5.25
which = test_var
}
modulo_variable = {
which = test_var_remainder # Now 0.25
value = 5
}
subtract_variable = {
which = test_var
which = test_var_remainder
}
# Now test_var is rounded to 5
Jomini:
Code:
set_variable = {
name = test_var
value = 10.5
}
chance_variable = { # Now 5.25, I want to nearest 5
name = test_var
divide = 2
}
round_variable = {
name = test_var
nearest = 5
}
# Now test_var is rounded to 5
Scripted Blocks
As in our older games, Jomini has scripted triggers and effects. We also have scripted modifiers that function similarly but for mean time to happen constructs so you can reduce giant modifier lists.
All of them can take scripted arguments as in EU4
eg:
Code:
# Scripted Effect
change_culture_and_notify = {
set_culture = $CULTURE$
trigger_event = $CULTURE$.0
}
# Using
root.father = { change_culture_and_notify = { CULTURE = roman }
Scripted Lists are a new addition which let you create your own script lists based on a coded one with extra conditions:
Code:
citizen = {
base = pops_in_province
conditions = { pop_type = citizen }
}
Target & Variable Lists
You can now create custom lists of saved scopes or variables
They are added and removed with:
Code:
scope_to_add = { add_to_list = list_name }
scope_to_remove = { remove_from_list = list_name }
Code:
any/every/random/ordered_in_list = {
list = list_name
}
Code:
some_scope = { is_in_list = list_name }
You can also have lists of variables in the current scope, locally or globally.
They are added and removed with:
Code:
add_to_variable_list = { name = variable_name target = scope }
remove_list_variable = { name = variable_name target = scope }
clear_variable_list = variable_name
Code:
any/every/random/ordered_in_list = {
variable = variable_name
}
Code:
is_target_in_variable_list = { name = variable_name target = scope }
variable_list_size = { name = variable_name target = value }
has_variable_list = variable_name
Events and On Actions
Events have been moved to Jomini, the syntax is nearly identical to the older games but instead of defining the event type first you write its ID. Games are free to specialise their events as needed, for example Imperator includes some portraits on their events.
Mean time to happen events have been removed for numerous reasons, primarily because it is bad.
As a replacement for that on actions have been moved to Jomini and been opened up to allow scripter made on actions that function as neat containers of events and methods of random distribution.
We also no longer hack scope objects into root, from chains or this for on actions and instead give them a specifically named saved scopes. So if you have an on action from a battle with a winner, loser, location and war scopes you won’t have to comment in the files what they all mean instead you just use a clearly named scope of scope:battle_location etc.
Old:
Code:
tombola_effect = {
if = {
limit = {
should_get_tombola = yes
}
random_list = {
30 = {
modifier = {
factor = 2
is_cool = yes
}
trigger_event = cool_event.1
}
30 = {
modifier = {
factor = 2
is_fancy_pants = yes
}
trigger_event = cool_event.2
}
30 = {
modifier = {
factor = 2
has_lots_of_friends = yes
}
trigger_event = cool_event.3
}
30 = {
modifier = {
factor = 2
is_super_awesome = yes
}
trigger_event = cool_event.4
}
30 = {
modifier = {
factor = 2
is_running_out_of_things_to_list = yes
}
trigger_event = cool_event.5
}
# ... repeat for lots of events
}
}
}
Jomini:
Code:
tombola_on_action = {
trigger = {
should_get_tombola = yes
}
# Modifiers on the events directly instead of spammed here making this unreadable
random_events = {
30 = cool_event.1
30 = cool_event.2
30 = cool_event.3
30 = cool_event.4
30 = cool_event.5
}
}
Dynamic Descriptions
Events can now have their descriptions and titles be pieced together bit by bit from conditional and random parts to form dynamic text without needing to nest 20 pieces of custom localisation in multiple different files and folders. This lets you build some very complex event descriptions.
Code:
cool_event.1 = {
desc = {
desc = cool_event.1.intro
first_valid = {
triggered_desc = {
cool_event.1.blue_flag
trigger = { var:flag = flag:blue_flag }
}
random_valid = {
count = 2
triggered_desc = {
cool_event.1.red_flag
trigger = { var:flag = flag:red_flag }
}
triggered_desc = {
cool_event.1.yellow_flag
trigger = { var:flag = flag:yellow_flag }
}
triggered_desc = {
cool_event.1.green_flag
trigger = { var:flag = flag:green_flag }
}
desc = cool_event.1.brown_flag
}
}
desc = cool_event.1.no_flag
}
random_valid = {
desc = cool_event.1.conclusion.a
desc = cool_event.1.conclusion.b
}
}
Sweet Sweet Documentation
We have previously made attempts at automatic documentation with varied success, some bits could be outdated or incorrect, some core information was not printed at all etc.
Now the "script_docs" console command has been moved to Jomini and using it in game will outputs the following information to separate files in your games log folder:
- All effects, the scopes they can be used in and a brief description, if they are a script list the scope they lead to.
- All triggers, the scopes they can be used in and a brief description, if they are a script list the scope they lead to.
- All scope types, character, country, value etc.
- All event target links, the scopes they can be used from, the scope they output to and a brief description.
- All saved scopes created by the code.
- All modifiers, the scope they can be applied to eg: levy_reinforcement_rate
- All on actions, if they are from code or script and the expected scope they are called in
The script should now be a LOT better at logging errors, we made as much as possible get caught in Jomini with clear errors as to what is going wrong, individual effects and triggers will still need game level specific errors but those should also be a lot better.
GUI & Localization System
We have a new GUI system for the games which works with its own specif setup of scripting, it is also the same system used for the localization system. Collectively this is called the Data System
Everything you can run must be either registered in by the code or made as a scripted gui.
All things you can use in the data are split into four categories:
- Types, the type of an object which corresponds to its class/struct in the code
- Promotes, moving from an object of one type to an object of another
- Functions, calling a function on an object which returns another object
- Callbacks, calling a function on an object which does not return anything
Another thing to keep in mind is that the data system obeys (for the most part) how const works in C++. Without getting too technical functions, promote and callbacks can be marked as const only, which means that object which are const cannot call non-const. This is unlikely to affect you if you use the scripted guis though.
Scripted GUI
The scripted gui lets you evaluate and execute arbitrary script via the UI in a manner that will keep the game synchronized in multiplayer. You define the script in common/scripted_guis and can then reference that in data entries.
For example a cheat button to give you gold and take it from another character:
Code:
# common/scripted_guis
cheat_gold_button = {
scope = character
saved_scopes = {
second
}
is_shown = { # Can be omitted as always true
always = yes
}
is_valid = {
gold < 5000
}
effect = {
add_gold = 500
scope:second = {
add_gold = -500
}
}
}
# in a gui entry
button = {
name = "my_cheat_button"
datacontext = "[GetScriptedGui('cheat_gold_button')]"
texture = "gfx/interface/icons/shared_icons/bankruptcy.dds"
visible = "[ScriptedGui.IsShown( GuiScope.SetRoot( SomeCharacter.MakeScope ).AddScope( 'second', SomeOtherCharacter.MakeScope ).End )]"
enabled = "[ScriptedGui.IsValid( GuiScope.SetRoot( SomeCharacter.MakeScope ).AddScope( 'second', SomeOtherCharacter.MakeScope ).End )]"
onclick = "[ScriptedGui.Execute( GuiScope.SetRoot( SomeCharacter.MakeScope ).AddScope( 'second', SomeOtherCharacter.MakeScope ).End )]"
tooltip = "[ScriptedGui.BuildTooltip( GuiScope.SetRoot( SomeCharacter.MakeScope ).AddScope( 'second', SomeOtherCharacter.MakeScope ).End )]"
}
Do not make calls to GuiScope without a matching call to End, otherwise you are going to introduce a memory leak and the game will eventually crash.
The AI will make use of these buttons based on the ai_is_valid trigger and the ai_chance definitions.
- 8
- 3
- 3