Conversation
There was a problem hiding this comment.
Pull request overview
Introduces an initial runtime evaluation path for scripted conditions by adding an evaluation Context and wiring ConditionNode evaluation through scope redirection/iteration, while also extending condition registration metadata (localisation keys + tooltip subject) via a builder API.
Changes:
- Add
Context(variant of scope pointers + THIS/FROM tracking) to support scope changes and iterators during condition evaluation. - Add
ConditionNode::evaluate()/evaluate_group()and delegate leaf evaluation to scope objects (CountryInstance/ProvinceInstance/State/Pop). - Extend
Conditionregistration with localisation/tooltip metadata and replace direct registration calls with aConditionBuilderfluent API.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| src/openvic-simulation/scripts/Context.hpp | Defines evaluation context, including scope pointer variant and THIS/FROM linkage. |
| src/openvic-simulation/scripts/Context.cpp | Implements scope identification, identifier access, leaf dispatch, iterator sub-contexts, and redirect contexts. |
| src/openvic-simulation/scripts/Condition.hpp | Adds tooltip metadata fields and declares ConditionNode evaluation entrypoints + builder-based condition registration. |
| src/openvic-simulation/scripts/Condition.cpp | Implements condition evaluation logic and migrates condition registration to builder API. |
| src/openvic-simulation/population/Pop.hpp / Pop.cpp | Adds pop-scope leaf evaluation (partial) for conditions. |
| src/openvic-simulation/map/State.hpp / State.cpp | Adds state-scope leaf evaluation stub and wires includes. |
| src/openvic-simulation/map/ProvinceInstance.hpp / ProvinceInstance.cpp | Adds province-scope leaf evaluation stub and wires includes. |
| src/openvic-simulation/country/CountryInstance.hpp / CountryInstance.cpp | Adds country-scope leaf evaluation (partial) and wires includes. |
Comments suppressed due to low confidence (1)
src/openvic-simulation/scripts/Condition.cpp:211
- AND/OR/NOT are registered with scope COUNTRY, but ConditionNode::evaluate() enforces
share_scope_type(condition->scope, context.get_scope_type()). Since AND is also the root condition, this makes any condition script evaluated in a non-country context (province/state/pop) immediately fail at the root. Register these logical operators with a scope mask that includes all valid scopes (e.g., POP|PROVINCE|STATE|COUNTRY) or exempt them from scope checks.
/* Special Conditions */
ret &= add_condition("AND", GROUP, COUNTRY);
ret &= add_condition("OR", GROUP, COUNTRY);
ret &= add_condition("NOT", GROUP, COUNTRY);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return p->get_identifier(); | ||
| } | ||
| else if constexpr (std::is_same_v<T, State const*>) { | ||
| return p->get_identifier(); |
There was a problem hiding this comment.
Context::get_identifier() returns std::string_view for State by forwarding State::get_identifier(), but State::get_identifier() returns a memory::string by value. This will produce a dangling string_view (view into a temporary) and can lead to use-after-free. Consider returning a stable view (e.g., change State::get_identifier() to return std::string_view to stored data) or change Context::get_identifier() to return an owning string type when the scope is State.
| return p->get_identifier(); | |
| // State::get_identifier() returns a string by value; storing it in a | |
| // thread_local buffer ensures the returned string_view does not dangle. | |
| thread_local std::string state_identifier_buffer; | |
| state_identifier_buffer = p->get_identifier(); | |
| return std::string_view{ state_identifier_buffer }; |
There was a problem hiding this comment.
@DarkiBoi A state doesn't even have an identifier.
Consider skipping states for now.
I think we'll use Regions later on either as scope or merely for the identifier.
Terminology:
Region Victoria 2\map\regions.txt, for example Texas.
State a part of a region owned by a country with a colonial status. For example American colonial Texas.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 15 comments.
Comments suppressed due to low confidence (1)
src/openvic-simulation/scripts/Condition.cpp:699
- After selecting the appropriate overload and storing it in
active_condition, the code continues to use properties from the originalconditionobject (lines 694-699) instead of fromactive_condition. This means the overload selection has no effect during parsing, causing incorrect behavior when an overload should be used. All references toconditionafter this point should useactive_conditioninstead.
const std::string_view identifier = condition.get_identifier();
const value_type_t value_type = condition.value_type;
const scope_type_t scope = condition.scope;
const scope_type_t scope_change = condition.scope_change;
const identifier_type_t key_identifier_type = condition.key_identifier_type;
const identifier_type_t value_identifier_type = condition.value_identifier_type;
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Context(CountryInstance const* p) : ptr(p), this_scope(this) {} | ||
| Context(ProvinceInstance const* p) : ptr(p), this_scope(this) {} | ||
| Context(State const* p) : ptr(p), this_scope(this) {} | ||
| Context(Pop const* p) : ptr(p), this_scope(this) {} |
There was a problem hiding this comment.
The simple constructors (lines 25-28) initialize this_scope to this, creating a self-reference. This means that when a Context is used, this_scope will point to the same Context object. However, if this Context object is temporary or moved, the pointer becomes dangling. Consider documenting this lifetime requirement or ensuring that Context objects are always stored in stable locations when used.
| fixed_point_t expected = std::get<fixed_point_t>(node.get_value()); | ||
| return get_industrial_power_untracked() >= expected; |
There was a problem hiding this comment.
The "industrial_score" condition can accept either REAL or IDENTIFIER values according to its definition (line 299), but this implementation only handles the REAL case. When used with an IDENTIFIER (e.g., "industrial_score = ENG" to compare against another country's score), this code will throw a std::bad_variant_access exception when trying to std::get<fixed_point_t>. The implementation should check which variant type is present and handle both cases appropriately.
| fixed_point_t expected = std::get<fixed_point_t>(node.get_value()); | |
| return get_industrial_power_untracked() >= expected; | |
| return std::visit( | |
| [this](auto const& value) -> bool { | |
| using T = std::decay_t<decltype(value)>; | |
| if constexpr (std::is_same_v<T, fixed_point_t>) { | |
| return get_industrial_power_untracked() >= value; | |
| } else { | |
| Logger::error( | |
| "industrial_score condition received unsupported value type"); | |
| return false; | |
| } | |
| }, | |
| node.get_value() | |
| ); |
wvpm
left a comment
There was a problem hiding this comment.
The condition ids are not related to the types (province, pop, country, etc).
Please move them outside. The types are already massive and don't need tight coupling with conditions.
A condition can simply receive an instance and perform its checks from the outside.
All data required for conditions is exposed via public const fields or methods.
d553f4f to
5c25897
Compare
| struct TechnologySchool; | ||
| struct UnitInstanceGroup; | ||
| struct UnitTypeManager; | ||
| struct ConditionNode; |
There was a problem hiding this comment.
| struct ConditionNode; |
No longer needed
| struct StateManager; | ||
| struct StateSet; | ||
| struct Strata; | ||
| struct ConditionNode; |
There was a problem hiding this comment.
| struct ConditionNode; |
| #include "openvic-simulation/types/TypedIndices.hpp" | ||
| #include "openvic-simulation/utility/Containers.hpp" | ||
| #include "openvic-simulation/utility/Logger.hpp" | ||
| #include "openvic-simulation/core/Typedefs.hpp" |
There was a problem hiding this comment.
| #include "openvic-simulation/core/Typedefs.hpp" |
Not related to this PR
| struct RebelType; | ||
| struct Religion; | ||
| struct SellResult; | ||
| struct ConditionNode; |
There was a problem hiding this comment.
| struct ConditionNode; |
| auto* p, | ||
| DefinitionManager const& dm, | ||
| InstanceManager const& im, | ||
| Context const* this_ctx, |
There was a problem hiding this comment.
The naming convention inside this project is new_[field_name] for constructor arguments.
That also avoids a distinction between scope and context.
| Context const* from_scope = nullptr; | ||
|
|
||
| Context(CountryInstance const* p, DefinitionManager const& dm, InstanceManager const& im) | ||
| : ptr(p), definition_manager(dm), instance_manager(im), this_scope(this) {} |
There was a problem hiding this comment.
When using constructor overloads, forward the arguments to the main constructor (the line 39 one I think).
This ensures all constructors function the same and the overloads are merely for convenience.
| struct DefinitionManager; | ||
| struct InstanceManager; | ||
|
|
||
| struct Context { |
There was a problem hiding this comment.
The default access modifier of a struct is public.
In this project we avoid mutable publci fields.
You could make the fields private and expose their getters via the PROPERTY macro.
If the fields are only set in the constructor, you have to write Context const* const.
The const* makes it a pointer to a readonly Context. The second const makes the pointer itself readonly.
DefinitionManager const& definition_manager; & InstanceManager const& instance_manager; can safely be exposed as public fields as references are readonly.
| Context(Pop const* p, DefinitionManager const& dm, InstanceManager const& im) | ||
| : ptr(p), definition_manager(dm), instance_manager(im), this_scope(this) {} | ||
|
|
||
| Context( |
There was a problem hiding this comment.
Consider making this constructor private unless required otherwise
|
|
||
| #include <optional> | ||
| #include <variant> | ||
| #include <vector> |
There was a problem hiding this comment.
Spartan322 added memory tracking. This requires using memory::vector instead of std::vector.
See also #421
So use #include "openvic-simulation/utility/Containers.hpp" instead.
| DefinitionManager const& definition_manager; | ||
| InstanceManager const& instance_manager; | ||
|
|
||
| Context const* this_scope = nullptr; |
There was a problem hiding this comment.
Can THIS ever be null?
How is accessing context.this_scope different from just using context?
| #include "openvic-simulation/InstanceManager.hpp" | ||
|
|
||
| using namespace OpenVic; | ||
| scope_type_t Context::get_scope_type() const { |
There was a problem hiding this comment.
Why not determine this during construction and store it as a field?
| return p->get_identifier(); | ||
| } | ||
| else if constexpr (std::is_same_v<T, Pop const*>) { | ||
| return ""; |
There was a problem hiding this comment.
For debug purposes, I suggest you at least write the pop type here.
Alternatively use pop.id_in_province & pop.get_location()->index.
| using T = std::decay_t<decltype(*p)>; | ||
|
|
||
| if constexpr (std::is_same_v<T, CountryInstance>) { | ||
| // TODO: https://vic2.paradoxwikis.com/List_of_conditions#Country_Scope |
There was a problem hiding this comment.
Please move the body of these if statements to separate methods.
if constexpr (std::is_same_v<T, CountryInstance>) {
evaluate_country_leaf(node, *p);
}bool evaluate_country_leaf(ConditionNode const& node, CountryInstance const& country) const;| bool Context::evaluate_leaf(ConditionNode const& node) const { | ||
| std::string_view const id = node.get_condition()->get_identifier(); | ||
|
|
||
| return std::visit( |
There was a problem hiding this comment.
This isn't how std::visit is designed. See https://en.cppreference.com/w/cpp/utility/variant/visit
Either use if constexpr (std::is_same_v<T, CountryInstance>) or write overloads.
| return result; | ||
| } | ||
|
|
||
| std::optional<Context> Context::get_redirect_context(std::string_view condition_id, scope_type_t target) const { |
There was a problem hiding this comment.
std::optional<Context> owns the Context.
Both this_scope and from_scope already exist and are owned somewhere else.
Returning them here copies them. I don't think that's what you want.
|
|
||
| if (target == scope_type_t::COUNTRY) { | ||
| if (condition_id == "owner") { | ||
| auto const* owner = province->get_owner(); |
There was a problem hiding this comment.
In this project explicit types are preferred over auto types.
Just use CountryInstance const* owner here and for controller.
| return false; | ||
| } | ||
| else if constexpr (std::is_same_v<T, State>) { | ||
| // No state conditions according to wiki? |
There was a problem hiding this comment.
See Victoria 2\common\production_types.txt
trade_goods_in_state = coal
has_building = electric_gear_factory
| } | ||
| if (id == "badboy") { | ||
| fixed_point_t expected_ratio = std::get<fixed_point_t>(node.get_value()); | ||
| return p->get_infamy_untracked() >= expected_ratio * definition_manager.get_define_manager().get_country_defines().get_infamy_containment_limit(); |
There was a problem hiding this comment.
If you only need this define, you should request the CountryDefines in your constructor instead of searching for it yourself. Same for the CountryInstanceManager.
We can leave this as is and trim it later once other conditions are implemented.
No description provided.