Skip to content

Comments

Condition Evaluation#698

Draft
DarkiBoi wants to merge 8 commits intomasterfrom
conditions
Draft

Condition Evaluation#698
DarkiBoi wants to merge 8 commits intomasterfrom
conditions

Conversation

@DarkiBoi
Copy link
Contributor

No description provided.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Condition registration with localisation/tooltip metadata and replace direct registration calls with a ConditionBuilder fluent 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();
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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 };

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 original condition object (lines 694-699) instead of from active_condition. This means the overload selection has no effect during parsing, causing incorrect behavior when an overload should be used. All references to condition after this point should use active_condition instead.
		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.

Comment on lines 25 to 28
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) {}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 1223 to 1224
fixed_point_t expected = std::get<fixed_point_t>(node.get_value());
return get_industrial_power_untracked() >= expected;
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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()
);

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@wvpm wvpm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@DarkiBoi DarkiBoi force-pushed the conditions branch 2 times, most recently from d553f4f to 5c25897 Compare February 22, 2026 17:24
struct TechnologySchool;
struct UnitInstanceGroup;
struct UnitTypeManager;
struct ConditionNode;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
struct ConditionNode;

No longer needed

struct StateManager;
struct StateSet;
struct Strata;
struct ConditionNode;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#include "openvic-simulation/core/Typedefs.hpp"

Not related to this PR

struct RebelType;
struct Religion;
struct SellResult;
struct ConditionNode;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
struct ConditionNode;

auto* p,
DefinitionManager const& dm,
InstanceManager const& im,
Context const* this_ctx,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Contributor

@wvpm wvpm Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider making this constructor private unless required otherwise


#include <optional>
#include <variant>
#include <vector>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 "";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor

@wvpm wvpm Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(
Copy link
Contributor

@wvpm wvpm Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Spartan322 Spartan322 added the enhancement New feature or request label Feb 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants