Skip to content

Expand nationScriptButtonType and provinceScriptButtonType #2015

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 1 addition & 21 deletions docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -430,27 +430,7 @@ This control will then automatically be inserted into the window named `province

### Scriptable buttons

Of course, adding new buttons wouldn't mean much if you couldn't make them do things. To allow you to add custom button effects to the game, we have introduced two new ui element types: `provinceScriptButtonType` and `nationScriptButtonType`. These buttons are defined in the same way as a `guiButtonType`, except that they can be given additional `allow` and `effect` parameters. For example:
```
provinceScriptButtonType = {
name = "wololo_button"
extends = "province_view_header"
position = { x= 146 y = 3 }
quadTextureSprite = "GFX_wololo"
allow = {
owner = { tag = FROM }
}
effect = {
assimilate = "yes please"
}
}
```

A province script button has its main and THIS slots filled with the province that the containing window is about, with FROM the player's nation. A nation script button has its main and THIS slots filled with the nation that the containing window is about, if there is one, or the player's nation if there is not, and has FROM populated with the player's nation.

The allow trigger condition is optional and is used to determine when the button is enabled. If the allow condition is omitted, the button will always be enabled.

The tooltip for these scriptable buttons will always display the relevant allow condition and the effect. You may also optionally add a custom description to the tooltip by adding a localization key that is the name of the button followed by `_tooltip`. In the case of the button above, for example, the tooltip is defined as `wololo_button_tooltip;Wololo $PROVINCE$`. The following three variables can be used in the tooltip: `$PROVINCE$`, which will resolve to the targeted province, `$NATION$`, which will resolve to the targeted nation or the owner of the targeted province, and `$PLAYER$`, which will always resolve to the player's own nation.
[See Scripting](features/scripting.md)

### Abbreviated `.gui` syntax

Expand Down
76 changes: 76 additions & 0 deletions docs/features/Scripting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Scripting

Adding new buttons wouldn't mean much if you couldn't make them do things. To allow you to add custom button effects to the game, we have introduced two new ui element types: `provinceScriptButtonType` and `nationScriptButtonType`. These buttons are defined in the same way as a `guiButtonType`, except that they can be given additional `allow` and `effect` parameters. For example:

```
provinceScriptButtonType = {
name = "wololo_button"
extends = "province_view_header"
position = { x= 146 y = 3 }
quadTextureSprite = "GFX_wololo"
visible = {
tag = USA
}
allow = {
owner = { tag = FROM }
}
effect = {
assimilate = "yes please"
}
}

nationScriptButtonType = {
name = "wololo_button"
extends = "province_view_header"
position = { x= 146 y = 3 }
quadTextureSprite = "GFX_wololo"
visible = {
tag = USA
}
allow = {
owner = { tag = FROM }
}
effect = {
assimilate = "yes please"
}
ai_will_do = {
always = yes
}
}
```

How does it work:
- A province script button has its main and THIS slots filled with the province that the containing window is about, with FROM the player's nation.
- A nation script button has its main and THIS slots filled with the nation that the containing window is about, if there is one, or the player's nation if there is not, and has FROM populated with the player's nation.
- The `visible` trigger condition is optional and is used to determine when the button is rendered. If the allow condition is omitted, the button will always be enabled.
- The `allow` trigger condition is optional and is used to determine when the button is enabled. If the allow condition is omitted, the button will always be enabled.
- The tooltip for these scriptable buttons will always display the relevant allow condition and the effect. You may also optionally add a custom description to the tooltip by adding a localization key that is the name of the button followed by `_tooltip`. In the case of the button above, for example, the tooltip is defined as `wololo_button_tooltip;Wololo $PROVINCE$`. The following three variables can be used in the tooltip: `$PROVINCE$`, which will resolve to the targeted province, `$NATION$`, which will resolve to the targeted nation or the owner of the targeted province, and `$PLAYER$`, which will always resolve to the player's own nation.
- AI evaluates national scripted interactions once a month in a similar way to decisions.
- AI doesn't use province scripted interactions.

## Technical side

### US7. Scriptable buttons

Recent changes:

- SneakBug8: Moved links to triggers & effects away from UI element definition into a special `scripted_interaction` DCON table. This reduces memory usage (there is over 8k UI elements in the basegame each having 6 bytes for these links) and increases calculations during UI update (find the interaction ID from GUI element ID).
- SneakBug8: AI can now use scripted buttons that have `ai_will_do` evaluation defined by iterating over a limited number of scripted interactions along with decisions.

**As a Modder,**
**I want to mod scripted buttons,**
**So that I add extra interactions to the game.**

**Acceptance Criteria:**
| AC1 | Allow trigger is parsed |
| AC2 | Visibility trigger is parsed |
| AC3 | Effect is parsed |
| AC4 | Ai_will_do block is parsed |
| AC5 | AI takes national interactions |

**Definition of Done:**
- [X] All acceptance criteria are met.
- [X] Code is reviewed and approved.
- [ ] Necessary tests are written and pass.
- [X] Documentation is updated, if applicable.
- [x] Feature is available in release versions of PA.
76 changes: 76 additions & 0 deletions src/ai/ai.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ void take_ai_decisions(sys::state& state) {
&& state.world.nation_get_owned_province_count(ids) != 0;
if(ve::compress_mask(filter_a).v != 0) {
// empty allow assumed to be an "always = yes"
// empty potential assumed to be an "always = yes"
ve::mask_vector filter_b = potential
? filter_a && (trigger::evaluate(state, potential, trigger::to_generic(ids), trigger::to_generic(ids), 0))
: filter_a;
Expand Down Expand Up @@ -83,6 +84,7 @@ void take_ai_decisions(sys::state& state) {
auto n = v.second;
auto d = v.first;
auto e = state.world.decision_get_effect(d);
// The effect of a prior decision once taken may invalidate the conditions that enabled another copy of the decision in the simultaneous evaluation to be taken
if(trigger::evaluate(state, state.world.decision_get_potential(d), trigger::to_generic(n), trigger::to_generic(n), 0)
&& trigger::evaluate(state, state.world.decision_get_allow(d), trigger::to_generic(n), trigger::to_generic(n), 0)) {
effect::execute(state, e, trigger::to_generic(n), trigger::to_generic(n), 0, uint32_t(state.current_date.value), uint32_t(n.index() << 4 ^ d.index()));
Expand All @@ -100,6 +102,80 @@ void take_ai_decisions(sys::state& state) {
}
}

void take_ai_scripted_interactions(sys::state& state) {
// US7AC5 National level interactions first

using element_nation_pair = std::pair<dcon::scripted_interaction_id, dcon::nation_id>;
concurrency::combinable<std::vector<element_nation_pair, dcon::cache_aligned_allocator<element_nation_pair>>> interactions_taken;

// execute in staggered blocks
uint32_t d_block_size = state.world.decision_size() / 32;
uint32_t block_index = 0;
auto d_block_end = state.world.decision_size();
concurrency::parallel_for(d_block_size * block_index, d_block_end, [&](uint32_t i) {
auto sel = dcon::scripted_interaction_id{ dcon::scripted_interaction_id::value_base_t(i) };
auto e = state.world.scripted_interaction_get_effect(sel);
if(e) {
auto potential = state.world.scripted_interaction_get_visible(sel);
auto allow = state.world.scripted_interaction_get_allow(sel);
auto ai_will_do = state.world.scripted_interaction_get_ai_will_do(sel);
ve::execute_serial_fast<dcon::nation_id>(state.world.nation_size(), [&](auto ids) {
// AI-only, not dead nations
ve::mask_vector filter_a = !state.world.nation_get_is_player_controlled(ids)
&& state.world.nation_get_owned_province_count(ids) != 0;
if(ve::compress_mask(filter_a).v != 0) {
// empty allow assumed to be an "always = yes"
// empty visible assumed to be an "always = yes"
ve::mask_vector filter_b = potential
? filter_a && (trigger::evaluate(state, potential, trigger::to_generic(ids), trigger::to_generic(ids), 0))
: filter_a;
if(ve::compress_mask(filter_b).v != 0) {
ve::mask_vector filter_c = allow
? filter_b && (trigger::evaluate(state, allow, trigger::to_generic(ids), trigger::to_generic(ids), 0))
: filter_b;
if(ve::compress_mask(filter_c).v != 0) {
ve::mask_vector filter_d = ai_will_do
? filter_c && (trigger::evaluate_multiplicative_modifier(state, ai_will_do, trigger::to_generic(ids), trigger::to_generic(ids), 0) > 0.0f)
: filter_c;
ve::apply([&](dcon::nation_id n, bool passed_filter) {
if(passed_filter) {
interactions_taken.local().push_back(element_nation_pair(sel, n));
}
}, ids, filter_d);
}
}
}
});
}
});
// combination and final execution
auto total_vector = interactions_taken.combine([](auto& a, auto& b) {
std::vector<element_nation_pair, dcon::cache_aligned_allocator<element_nation_pair>> result(a.begin(), a.end());
result.insert(result.end(), b.begin(), b.end());
return result;
});
// ensure total deterministic ordering
std::sort(total_vector.begin(), total_vector.end(), [&](auto a, auto b) {
auto na = a.second;
auto nb = b.second;
if(na != nb)
return na.index() < nb.index();
return a.first.index() < b.first.index();
});
// assumption 1: no duplicate pair of <n, d>
for(const auto& v : total_vector) {
auto n = v.second;
auto d = v.first;
auto potential = state.world.scripted_interaction_get_visible(d);
auto e = state.world.scripted_interaction_get_effect(d);
// The effect of a prior interaction once taken may invalidate the conditions that enabled another copy of the interaction in the simultaneous evaluation to be taken
if((!potential || trigger::evaluate(state, potential, trigger::to_generic(n), trigger::to_generic(n), 0))
&& trigger::evaluate(state, state.world.scripted_interaction_get_allow(d), trigger::to_generic(n), trigger::to_generic(n), 0)) {
effect::execute(state, e, trigger::to_generic(n), trigger::to_generic(n), 0, uint32_t(state.current_date.value), uint32_t(n.index() << 4 ^ d.index()));
}
}
}

float estimate_pop_party_support(sys::state& state, dcon::nation_id n, dcon::political_party_id pid) {
auto iid = state.world.political_party_get_ideology(pid);
/*float v = 0.f;
Expand Down
1 change: 1 addition & 0 deletions src/ai/ai.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
namespace ai {

void take_ai_decisions(sys::state& state);
void take_ai_scripted_interactions(sys::state& state);
void update_ai_ruling_party(sys::state& state);
void update_ai_colonial_investment(sys::state& state);
void update_ai_colony_starting(sys::state& state);
Expand Down
81 changes: 48 additions & 33 deletions src/gamestate/commands.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2719,9 +2719,10 @@ bool can_switch_embargo_status(sys::state& state, dcon::nation_id asker, dcon::n

return true;
}

void execute_switch_embargo_status(sys::state& state, dcon::nation_id from, dcon::nation_id to) {
if (state.world.nation_get_is_player_controlled(from)) {
auto& current_diplo = state.world.nation_get_diplomatic_points(from);
auto& current_diplo = state.world.nation_get_diplomatic_points(from);
state.world.nation_set_diplomatic_points(from, current_diplo - state.defines.askmilaccess_diplomatic_cost);
}

Expand Down Expand Up @@ -5291,54 +5292,68 @@ void execute_take_province(sys::state& state, dcon::nation_id source, dcon::prov
fid.get_province_control_as_province().set_nation(source);
}

void use_province_button(sys::state& state, dcon::nation_id source, dcon::gui_def_id d, dcon::province_id i) {
void use_province_button(sys::state& state, dcon::nation_id source, dcon::scripted_interaction_id sel, dcon::province_id i) {
payload p;
memset(&p, 0, sizeof(payload));
p.type = command_type::pbutton_script;
p.source = source;
p.data.pbutton.button = d;
p.data.pbutton.interaction = sel;
p.data.pbutton.id = i;
add_to_command_queue(state, p);
}
bool can_use_province_button(sys::state& state, dcon::nation_id source, dcon::gui_def_id d, dcon::province_id p) {
auto& def = state.ui_defs.gui[d];
if(def.get_element_type() != ui::element_type::button)
return false;
if(def.data.button.get_button_scripting() != ui::button_scripting::province)
return false;
if(!def.data.button.scriptable_enable)
bool can_see_province_button(sys::state& state, dcon::nation_id source, dcon::scripted_interaction_id sel, dcon::province_id p) {
auto visible = state.world.scripted_interaction_get_visible(sel);
if(!visible)
return true;
return trigger::evaluate(state, def.data.button.scriptable_enable, trigger::to_generic(p), trigger::to_generic(p), trigger::to_generic(source));
return trigger::evaluate(state, visible, trigger::to_generic(p), trigger::to_generic(p), trigger::to_generic(source));
}
bool can_use_province_button(sys::state& state, dcon::nation_id source, dcon::scripted_interaction_id sel, dcon::province_id p) {
auto allow = state.world.scripted_interaction_get_allow(sel);
auto visible = state.world.scripted_interaction_get_visible(sel);

if(!allow)
return true;
// Button must be visible and enabled

return (!visible || trigger::evaluate(state, visible, trigger::to_generic(p), trigger::to_generic(p), trigger::to_generic(source))) &&
trigger::evaluate(state, allow, trigger::to_generic(p), trigger::to_generic(p), trigger::to_generic(source));
}
void execute_use_province_button(sys::state& state, dcon::nation_id source, dcon::gui_def_id d, dcon::province_id p) {
auto & def = state.ui_defs.gui[d];
if(def.data.button.scriptable_effect)
effect::execute(state, def.data.button.scriptable_effect, trigger::to_generic(p), trigger::to_generic(p), trigger::to_generic(source), uint32_t(state.current_date.value), uint32_t(p.index() ^ (d.index() << 4)));
void execute_use_province_button(sys::state& state, dcon::nation_id source, dcon::scripted_interaction_id sel, dcon::province_id p) {
auto effect = state.world.scripted_interaction_get_effect(sel);
if(effect)
effect::execute(state, effect, trigger::to_generic(p), trigger::to_generic(p), trigger::to_generic(source), uint32_t(state.current_date.value), uint32_t(p.index() ^ (sel.index() << 4)));
}

void use_nation_button(sys::state& state, dcon::nation_id source, dcon::gui_def_id d, dcon::nation_id n) {
void use_nation_button(sys::state& state, dcon::nation_id source, dcon::scripted_interaction_id sel, dcon::nation_id n) {
payload p;
memset(&p, 0, sizeof(payload));
p.type = command_type::nbutton_script;
p.source = source;
p.data.nbutton.button = d;
p.data.nbutton.interaction = sel;
p.data.nbutton.id = n;
add_to_command_queue(state, p);
}
bool can_use_nation_button(sys::state& state, dcon::nation_id source, dcon::gui_def_id d, dcon::nation_id n) {
auto& def = state.ui_defs.gui[d];
if(def.get_element_type() != ui::element_type::button)
return false;
if(def.data.button.get_button_scripting() != ui::button_scripting::nation)
return false;
if(!def.data.button.scriptable_enable)
bool can_see_nation_button(sys::state& state, dcon::nation_id source, dcon::scripted_interaction_id sel, dcon::nation_id n) {
auto visible = state.world.scripted_interaction_get_visible(sel);
if(!visible)
return true;
return trigger::evaluate(state, visible, trigger::to_generic(n), trigger::to_generic(n), trigger::to_generic(source));
}
bool can_use_nation_button(sys::state& state, dcon::nation_id source, dcon::scripted_interaction_id sel, dcon::nation_id n) {
auto allow = state.world.scripted_interaction_get_allow(sel);
auto visible = state.world.scripted_interaction_get_visible(sel);

if(!allow)
return true;
return trigger::evaluate(state, def.data.button.scriptable_enable, trigger::to_generic(n), trigger::to_generic(n), trigger::to_generic(source));
// Button must be visible and enabled
return (!visible || trigger::evaluate(state, visible, trigger::to_generic(n), trigger::to_generic(n), trigger::to_generic(source))) &&
trigger::evaluate(state, allow, trigger::to_generic(n), trigger::to_generic(n), trigger::to_generic(source));
}
void execute_use_nation_button(sys::state& state, dcon::nation_id source, dcon::gui_def_id d, dcon::nation_id n) {
auto& def = state.ui_defs.gui[d];
if(def.data.button.scriptable_effect)
effect::execute(state, def.data.button.scriptable_effect, trigger::to_generic(n), trigger::to_generic(n), trigger::to_generic(source), uint32_t(state.current_date.value), uint32_t(n.index() ^ (d.index() << 4)));
void execute_use_nation_button(sys::state& state, dcon::nation_id source, dcon::scripted_interaction_id sel, dcon::nation_id n) {
auto effect = state.world.scripted_interaction_get_effect(sel);

if(effect)
effect::execute(state, effect, trigger::to_generic(n), trigger::to_generic(n), trigger::to_generic(source), uint32_t(state.current_date.value), uint32_t(n.index() ^ (sel.index() << 4)));
}

void post_chat_message(sys::state& state, ui::chat_message& m) {
Expand Down Expand Up @@ -6220,9 +6235,9 @@ bool can_perform_command(sys::state& state, payload& c) {
case command_type::toggle_interested_in_alliance:
return can_toggle_interested_in_alliance(state, c.source, c.data.diplo_action.target);
case command_type::pbutton_script:
return can_use_province_button(state, c.source, c.data.pbutton.button, c.data.pbutton.id);
return can_use_province_button(state, c.source, c.data.pbutton.interaction, c.data.pbutton.id);
case command_type::nbutton_script:
return can_use_nation_button(state, c.source, c.data.nbutton.button, c.data.nbutton.id);
return can_use_nation_button(state, c.source, c.data.nbutton.interaction, c.data.nbutton.id);

// common mp commands
case command_type::chat_message:
Expand Down Expand Up @@ -6616,10 +6631,10 @@ bool execute_command(sys::state& state, payload& c) {
execute_toggle_interested_in_alliance(state, c.source, c.data.diplo_action.target);
break;
case command_type::pbutton_script:
execute_use_province_button(state, c.source, c.data.pbutton.button, c.data.pbutton.id);
execute_use_province_button(state, c.source, c.data.pbutton.interaction, c.data.pbutton.id);
break;
case command_type::nbutton_script:
execute_use_nation_button(state, c.source, c.data.nbutton.button, c.data.nbutton.id);
execute_use_nation_button(state, c.source, c.data.nbutton.interaction, c.data.nbutton.id);
break;
// common mp commands
case command_type::chat_message:
Expand Down
Loading