﻿# DDM Marriage Planner.
# Candidate discovery uses CK3's spouse-candidate iterator, then validates each pair through
# can_marry_character_trigger. This avoids the matchmaker-scope failure that can make the narrower
# potential-marriage-option iterator return an empty list for courtiers/children whose marriage the
# player can actually arrange, while still respecting the active faith/culture/mod marriage rules.
#
# Recommendations follow the loaded game's spouse/betrothal pool. When the player controls both adult
# matchmakers DDM may finalize a pair directly; otherwise the row button opens CK3's normal Arrange Marriage
# screen with the recommended pair preselected, preserving acceptance, hooks, betrothals and lineality choices.

ddm_cache_match_slot_effect = {
    # Run from the player with scope:ddm_match_subject / scope:ddm_match_candidate.
    #
    # Individual view exposes only the best three recommendations, but the Dynasty Plan keeps five
    # additional candidates internally. Those extra candidates are deliberately not shown as extra row
    # clutter; they exist so a global non-conflicting plan can fall back when several dynasts want the
    # same first-choice spouse.
    if = {
        limit = { var:ddm_match_slot = 0 }
        scope:ddm_match_subject = {
            set_variable = { name = ddm_match_best value = scope:ddm_match_candidate }
            set_variable = { name = ddm_match_best_score value = scope:ddm_match_candidate.ddm_match_candidate_score_value }
            set_variable = { name = ddm_match_best_genetic value = scope:ddm_match_candidate.ddm_match_genetic_component_value }
            set_variable = { name = ddm_match_best_love value = scope:ddm_match_candidate.ddm_match_love_component_value }
            set_variable = { name = ddm_match_best_score_max value = ddm_match_total_reference_max_value }
            if = { limit = { scope:ddm_match_candidate = { is_close_family_of = scope:ddm_match_subject } } set_variable = { name = ddm_match_best_kinship value = 3 } }
            else_if = { limit = { scope:ddm_match_candidate = { is_extended_family_of = scope:ddm_match_subject } } set_variable = { name = ddm_match_best_kinship value = 2 } }
            else_if = { limit = { scope:ddm_match_candidate = { dynasty ?= scope:ddm_match_subject.dynasty } } set_variable = { name = ddm_match_best_kinship value = 1 } }
            else = { set_variable = { name = ddm_match_best_kinship value = 0 } }
        }
        if = {
            limit = { scope:ddm_match_candidate = { matchmaker = scope:ddm_match_actor } }
            scope:ddm_match_subject = { set_variable = { name = ddm_match_best_managed value = 1 } }
        }
        else_if = {
            limit = { scope:ddm_match_candidate = { exists = matchmaker } }
            scope:ddm_match_subject = { set_variable = { name = ddm_match_best_external value = 1 } }
        }
        change_variable = { name = ddm_match_found_count add = 1 }
    }
    else_if = {
        limit = { var:ddm_match_slot = 1 }
        scope:ddm_match_subject = {
            set_variable = { name = ddm_match_second value = scope:ddm_match_candidate }
            set_variable = { name = ddm_match_second_score value = scope:ddm_match_candidate.ddm_match_candidate_score_value }
            set_variable = { name = ddm_match_second_genetic value = scope:ddm_match_candidate.ddm_match_genetic_component_value }
            set_variable = { name = ddm_match_second_love value = scope:ddm_match_candidate.ddm_match_love_component_value }
            set_variable = { name = ddm_match_second_score_max value = ddm_match_total_reference_max_value }
            if = { limit = { scope:ddm_match_candidate = { is_close_family_of = scope:ddm_match_subject } } set_variable = { name = ddm_match_second_kinship value = 3 } }
            else_if = { limit = { scope:ddm_match_candidate = { is_extended_family_of = scope:ddm_match_subject } } set_variable = { name = ddm_match_second_kinship value = 2 } }
            else_if = { limit = { scope:ddm_match_candidate = { dynasty ?= scope:ddm_match_subject.dynasty } } set_variable = { name = ddm_match_second_kinship value = 1 } }
            else = { set_variable = { name = ddm_match_second_kinship value = 0 } }
        }
        if = {
            limit = { scope:ddm_match_candidate = { matchmaker = scope:ddm_match_actor } }
            scope:ddm_match_subject = { set_variable = { name = ddm_match_second_managed value = 1 } }
        }
        else_if = {
            limit = { scope:ddm_match_candidate = { exists = matchmaker } }
            scope:ddm_match_subject = { set_variable = { name = ddm_match_second_external value = 1 } }
        }
    }
    else_if = {
        limit = { var:ddm_match_slot = 2 }
        scope:ddm_match_subject = {
            set_variable = { name = ddm_match_third value = scope:ddm_match_candidate }
            set_variable = { name = ddm_match_third_score value = scope:ddm_match_candidate.ddm_match_candidate_score_value }
            set_variable = { name = ddm_match_third_genetic value = scope:ddm_match_candidate.ddm_match_genetic_component_value }
            set_variable = { name = ddm_match_third_love value = scope:ddm_match_candidate.ddm_match_love_component_value }
            set_variable = { name = ddm_match_third_score_max value = ddm_match_total_reference_max_value }
            if = { limit = { scope:ddm_match_candidate = { is_close_family_of = scope:ddm_match_subject } } set_variable = { name = ddm_match_third_kinship value = 3 } }
            else_if = { limit = { scope:ddm_match_candidate = { is_extended_family_of = scope:ddm_match_subject } } set_variable = { name = ddm_match_third_kinship value = 2 } }
            else_if = { limit = { scope:ddm_match_candidate = { dynasty ?= scope:ddm_match_subject.dynasty } } set_variable = { name = ddm_match_third_kinship value = 1 } }
            else = { set_variable = { name = ddm_match_third_kinship value = 0 } }
        }
        if = {
            limit = { scope:ddm_match_candidate = { matchmaker = scope:ddm_match_actor } }
            scope:ddm_match_subject = { set_variable = { name = ddm_match_third_managed value = 1 } }
        }
        else_if = {
            limit = { scope:ddm_match_candidate = { exists = matchmaker } }
            scope:ddm_match_subject = { set_variable = { name = ddm_match_third_external value = 1 } }
        }
    }
    else_if = {
        limit = { var:ddm_match_slot = 3 }
        scope:ddm_match_subject = { set_variable = { name = ddm_match_plan_fourth value = scope:ddm_match_candidate } }
    }
    else_if = {
        limit = { var:ddm_match_slot = 4 }
        scope:ddm_match_subject = { set_variable = { name = ddm_match_plan_fifth value = scope:ddm_match_candidate } }
    }
    else_if = {
        limit = { var:ddm_match_slot = 5 }
        scope:ddm_match_subject = { set_variable = { name = ddm_match_plan_sixth value = scope:ddm_match_candidate } }
    }
    else_if = {
        limit = { var:ddm_match_slot = 6 }
        scope:ddm_match_subject = { set_variable = { name = ddm_match_plan_seventh value = scope:ddm_match_candidate } }
    }
    else_if = {
        limit = { var:ddm_match_slot = 7 }
        scope:ddm_match_subject = { set_variable = { name = ddm_match_plan_eighth value = scope:ddm_match_candidate } }
    }
    change_variable = { name = ddm_match_slot add = 1 }
}

# The row shows and acts on one of the three recommendations at a time. Cycling copies that slot into a
# single set of selection variables, so the display, the portrait and the Marry button all read one place.
ddm_apply_match_selection_effect = {
    # Subject scope.
    if = { limit = { NOT = { has_variable = ddm_match_sel_count } } set_variable = { name = ddm_match_sel_count value = 0 } }
    if = { limit = { NOT = { has_variable = ddm_match_selected_slot } } set_variable = { name = ddm_match_selected_slot value = 1 } }
    if = { limit = { var:ddm_match_selected_slot < 1 } set_variable = { name = ddm_match_selected_slot value = 1 } }
    if = { limit = { var:ddm_match_selected_slot > var:ddm_match_sel_count } set_variable = { name = ddm_match_selected_slot value = 1 } }

    remove_variable = ddm_match_sel
    remove_variable = ddm_match_sel_score
    remove_variable = ddm_match_sel_genetic
    remove_variable = ddm_match_sel_love
    remove_variable = ddm_match_sel_score_max
    remove_variable = ddm_match_sel_kinship
    remove_variable = ddm_match_sel_managed
    remove_variable = ddm_match_sel_external

    if = {
        limit = { var:ddm_match_selected_slot = 1 exists = var:ddm_match_best }
        var:ddm_match_best = { save_scope_as = ddm_match_selection_target }
        set_variable = { name = ddm_match_sel value = scope:ddm_match_selection_target }
        set_variable = { name = ddm_match_sel_score value = var:ddm_match_best_score }
        set_variable = { name = ddm_match_sel_genetic value = var:ddm_match_best_genetic }
        set_variable = { name = ddm_match_sel_love value = var:ddm_match_best_love }
        set_variable = { name = ddm_match_sel_score_max value = var:ddm_match_best_score_max }
        set_variable = { name = ddm_match_sel_kinship value = var:ddm_match_best_kinship }
        if = { limit = { has_variable = ddm_match_best_managed } set_variable = { name = ddm_match_sel_managed value = 1 } }
        if = { limit = { has_variable = ddm_match_best_external } set_variable = { name = ddm_match_sel_external value = 1 } }
    }
    else_if = {
        limit = { var:ddm_match_selected_slot = 2 exists = var:ddm_match_second }
        var:ddm_match_second = { save_scope_as = ddm_match_selection_target }
        set_variable = { name = ddm_match_sel value = scope:ddm_match_selection_target }
        set_variable = { name = ddm_match_sel_score value = var:ddm_match_second_score }
        set_variable = { name = ddm_match_sel_genetic value = var:ddm_match_second_genetic }
        set_variable = { name = ddm_match_sel_love value = var:ddm_match_second_love }
        set_variable = { name = ddm_match_sel_score_max value = var:ddm_match_second_score_max }
        set_variable = { name = ddm_match_sel_kinship value = var:ddm_match_second_kinship }
        if = { limit = { has_variable = ddm_match_second_managed } set_variable = { name = ddm_match_sel_managed value = 1 } }
        if = { limit = { has_variable = ddm_match_second_external } set_variable = { name = ddm_match_sel_external value = 1 } }
    }
    else_if = {
        limit = { var:ddm_match_selected_slot = 3 exists = var:ddm_match_third }
        var:ddm_match_third = { save_scope_as = ddm_match_selection_target }
        set_variable = { name = ddm_match_sel value = scope:ddm_match_selection_target }
        set_variable = { name = ddm_match_sel_score value = var:ddm_match_third_score }
        set_variable = { name = ddm_match_sel_genetic value = var:ddm_match_third_genetic }
        set_variable = { name = ddm_match_sel_love value = var:ddm_match_third_love }
        set_variable = { name = ddm_match_sel_score_max value = var:ddm_match_third_score_max }
        set_variable = { name = ddm_match_sel_kinship value = var:ddm_match_third_kinship }
        if = { limit = { has_variable = ddm_match_third_managed } set_variable = { name = ddm_match_sel_managed value = 1 } }
        if = { limit = { has_variable = ddm_match_third_external } set_variable = { name = ddm_match_sel_external value = 1 } }
    }
    if = {
        limit = { exists = var:ddm_match_sel exists = var:ddm_match_planner_owner }
        ddm_habsburg_calculate_selected_pair_effect = yes
    }
}

# Best -> second -> third -> back to best, over however many were actually found.
ddm_cycle_match_selection_effect = {
    # Subject scope.
    if = { limit = { NOT = { has_variable = ddm_match_sel_count } } set_variable = { name = ddm_match_sel_count value = 0 } }
    if = { limit = { NOT = { has_variable = ddm_match_selected_slot } } set_variable = { name = ddm_match_selected_slot value = 1 } }
    change_variable = { name = ddm_match_selected_slot add = 1 }
    if = { limit = { var:ddm_match_selected_slot > var:ddm_match_sel_count } set_variable = { name = ddm_match_selected_slot value = 1 } }
    ddm_apply_match_selection_effect = yes
}

ddm_set_match_subject_effective_policy_effect = {
    # Run in subject scope while scope:ddm_match_actor exists. Legacy per-dynast overrides from older
    # saves are cleared as they are encountered, so nobody is left stuck on a philosophy with no control.
    remove_variable = ddm_match_policy_override
    set_variable = { name = ddm_match_effective_policy value = scope:ddm_match_actor.var:ddm_match_policy }
}

ddm_recommend_matches_for_subject_effect = {
    # Run from the player with scope:ddm_match_subject saved.
    #
    # The scan is the expensive part of the planner: CK3 evaluates the order_by value once per candidate
    # across the whole spouse pool. A dynast whose cache is still current is not scanned again. When a scan
    # is needed, the first pass now includes the FULL child-quality calculation — congenital traits and
    # reinforcement, fertility, health, reproductive runway, parental skills, sex and kinship — before
    # the shortlist is cut. It keeps twenty-four candidates, then applies the complete policy score and
    # retains eight internally. Individual view exposes the best three; Dynasty Plan may use all eight.
    save_scope_as = ddm_match_actor

    scope:ddm_match_subject = {
        set_variable = { name = ddm_match_planner_owner value = scope:ddm_match_actor }
        ddm_set_match_subject_effective_policy_effect = yes
    }

    if = {
        limit = { scope:ddm_match_subject = { ddm_match_recommendations_current_trigger = yes } }
        if = {
            limit = { scope:ddm_match_subject = { exists = var:ddm_match_best } }
            change_variable = { name = ddm_match_found_count add = 1 }
        }
        if = {
            limit = { scope:ddm_match_subject = { exists = var:ddm_match_best NOT = { exists = var:ddm_match_sel } } }
            scope:ddm_match_subject = { ddm_apply_match_selection_effect = yes }
        }
    }
    else = {
        scope:ddm_match_subject = {
            remove_variable = ddm_match_best
            remove_variable = ddm_match_second
            remove_variable = ddm_match_third
            remove_variable = ddm_match_plan_fourth
            remove_variable = ddm_match_plan_fifth
            remove_variable = ddm_match_plan_sixth
            remove_variable = ddm_match_plan_seventh
            remove_variable = ddm_match_plan_eighth
            remove_variable = ddm_match_best_score
            remove_variable = ddm_match_second_score
            remove_variable = ddm_match_third_score
            remove_variable = ddm_match_best_genetic
            remove_variable = ddm_match_best_love
            remove_variable = ddm_match_best_score_max
            remove_variable = ddm_match_best_kinship
            remove_variable = ddm_match_best_managed
            remove_variable = ddm_match_best_external
            remove_variable = ddm_match_second_score
            remove_variable = ddm_match_second_genetic
            remove_variable = ddm_match_second_love
            remove_variable = ddm_match_second_score_max
            remove_variable = ddm_match_second_kinship
            remove_variable = ddm_match_second_managed
            remove_variable = ddm_match_second_external
            remove_variable = ddm_match_third_score
            remove_variable = ddm_match_third_genetic
            remove_variable = ddm_match_third_love
            remove_variable = ddm_match_third_score_max
            remove_variable = ddm_match_third_kinship
            remove_variable = ddm_match_third_managed
            remove_variable = ddm_match_third_external
            remove_variable = ddm_match_cache_generation
            remove_variable = ddm_match_cache_policy
            remove_variable = ddm_match_cache_dynasty_mode
            clear_variable_list = ddm_match_scan_shortlist
        }

        set_variable = { name = ddm_match_slot value = 0 }

        scope:ddm_match_subject = {
            ordered_spouse_candidate = {
                limit = { ddm_match_candidate_allowed_trigger = yes }
                order_by = ddm_match_candidate_prescreen_value
                max = 24
                check_range_bounds = no
                save_temporary_scope_as = ddm_match_shortlisted_candidate
                scope:ddm_match_subject = {
                    add_to_variable_list = { name = ddm_match_scan_shortlist target = scope:ddm_match_shortlisted_candidate }
                }
            }

            # Vanilla explicitly exposes potential_marriage_option as selectable marriage OR betrothal
            # candidates. Keep spouse_candidate as the fast/established path, but fall back to the
            # betrothal-aware iterator when that path returns nothing (the regression most visible on
            # young children). This is intentionally fallback-only because the potential iterator has
            # historically been less reliable for some courtier/matchmaker arrangements.
            if = {
                limit = { NOT = { has_variable_list = ddm_match_scan_shortlist } }
                ordered_potential_marriage_option = {
                    limit = { ddm_match_candidate_allowed_trigger = yes }
                    order_by = ddm_match_candidate_prescreen_value
                    max = 24
                    check_range_bounds = no
                    save_temporary_scope_as = ddm_match_shortlisted_candidate
                    scope:ddm_match_subject = {
                        add_to_variable_list = { name = ddm_match_scan_shortlist target = scope:ddm_match_shortlisted_candidate }
                    }
                }
            }

            ordered_in_list = {
                variable = ddm_match_scan_shortlist
                order_by = ddm_match_candidate_score_value
                max = 8
                check_range_bounds = no
                save_temporary_scope_as = ddm_match_candidate
                scope:ddm_match_actor = { ddm_cache_match_slot_effect = yes }
            }

            clear_variable_list = ddm_match_scan_shortlist

            # A fresh scan starts on the top recommendation. Individual view intentionally exposes only
            # three choices even though Dynasty Plan retains up to eight internally.
            set_variable = { name = ddm_match_sel_count value = scope:ddm_match_actor.var:ddm_match_slot }
            if = { limit = { var:ddm_match_sel_count > 3 } set_variable = { name = ddm_match_sel_count value = 3 } }
            set_variable = { name = ddm_match_selected_slot value = 1 }
            ddm_apply_match_selection_effect = yes

            # Stamp what this result was computed under, so the next page build can tell whether it stands.
            set_variable = { name = ddm_match_cache_generation value = scope:ddm_match_actor.var:ddm_match_scan_generation }
            set_variable = { name = ddm_match_cache_policy value = var:ddm_match_effective_policy }
            set_variable = { name = ddm_match_cache_dynasty_mode value = scope:ddm_match_actor.var:ddm_match_dynasty_mode }
        }
    }
}


# ---------------------------------------------------------------------------
# PLAYER MATCH
# ---------------------------------------------------------------------------
# The dynasty planner has always had scoring support for the ruler themself, but there was no dedicated
# player workflow. This keeps the same candidate pool, Best Children/Balanced/Love policy, kinship mode,
# score breakdown, cycling, Marry and Kidnap fallback used by every other Individual row.
ddm_refresh_player_match_effect = {
    ddm_initialize_settings_effect = yes
    save_scope_as = ddm_match_actor
    save_scope_as = ddm_match_subject
    set_variable = { name = ddm_match_planner_owner value = scope:ddm_match_actor }

    clear_variable_list = ddm_match_player_subject
    add_to_variable_list = { name = ddm_match_player_subject target = scope:ddm_match_subject }
    set_variable = { name = ddm_match_show_player_match value = 1 }

    # A deliberate Find/Refresh My Match action rescans only the ruler, not the entire dynasty.
    remove_variable = ddm_match_cache_generation
    ddm_recommend_matches_for_subject_effect = yes
}

# ---------------------------------------------------------------------------
# INDIVIDUAL VIEW SORTING / PAGING
# ---------------------------------------------------------------------------

ddm_sort_match_subjects_effect = {
    clear_variable_list = ddm_match_subjects_sorted
    save_scope_as = ddm_match_actor

    if = {
        limit = { has_variable_list = ddm_match_subjects_raw }
        if = {
            limit = { var:ddm_match_sort_mode = 1 }
            if = {
                limit = { var:ddm_match_sort_direction < 0 }
                ordered_in_list = {
                    variable = ddm_match_subjects_raw
                    order_by = { value = ddm_match_subject_dynastic_sort_value multiply = -1 }
                    max = 5000
                    check_range_bounds = no
                    save_temporary_scope_as = ddm_match_sorted_subject
                    scope:ddm_match_actor = { add_to_variable_list = { name = ddm_match_subjects_sorted target = scope:ddm_match_sorted_subject } }
                }
            }
            else = {
                ordered_in_list = {
                    variable = ddm_match_subjects_raw
                    order_by = ddm_match_subject_dynastic_sort_value
                    max = 5000
                    check_range_bounds = no
                    save_temporary_scope_as = ddm_match_sorted_subject
                    scope:ddm_match_actor = { add_to_variable_list = { name = ddm_match_subjects_sorted target = scope:ddm_match_sorted_subject } }
                }
            }
        }
        else_if = {
            limit = { var:ddm_match_sort_mode = 2 }
            if = {
                limit = { var:ddm_match_sort_direction < 0 }
                ordered_in_list = {
                    variable = ddm_match_subjects_raw
                    order_by = { value = ddm_match_subject_age_sort_value multiply = -1 }
                    max = 5000
                    check_range_bounds = no
                    save_temporary_scope_as = ddm_match_sorted_subject
                    scope:ddm_match_actor = { add_to_variable_list = { name = ddm_match_subjects_sorted target = scope:ddm_match_sorted_subject } }
                }
            }
            else = {
                ordered_in_list = {
                    variable = ddm_match_subjects_raw
                    order_by = ddm_match_subject_age_sort_value
                    max = 5000
                    check_range_bounds = no
                    save_temporary_scope_as = ddm_match_sorted_subject
                    scope:ddm_match_actor = { add_to_variable_list = { name = ddm_match_subjects_sorted target = scope:ddm_match_sorted_subject } }
                }
            }
        }
        else_if = {
            limit = { var:ddm_match_sort_mode = 3 }
            if = {
                limit = { var:ddm_match_sort_direction < 0 }
                ordered_in_list = {
                    variable = ddm_match_subjects_raw
                    order_by = { value = ddm_match_subject_fertility_sort_value multiply = -1 }
                    max = 5000
                    check_range_bounds = no
                    save_temporary_scope_as = ddm_match_sorted_subject
                    scope:ddm_match_actor = { add_to_variable_list = { name = ddm_match_subjects_sorted target = scope:ddm_match_sorted_subject } }
                }
            }
            else = {
                ordered_in_list = {
                    variable = ddm_match_subjects_raw
                    order_by = ddm_match_subject_fertility_sort_value
                    max = 5000
                    check_range_bounds = no
                    save_temporary_scope_as = ddm_match_sorted_subject
                    scope:ddm_match_actor = { add_to_variable_list = { name = ddm_match_subjects_sorted target = scope:ddm_match_sorted_subject } }
                }
            }
        }
        else_if = {
            limit = { var:ddm_match_sort_mode = 4 }
            if = {
                limit = { var:ddm_match_sort_direction < 0 }
                ordered_in_list = {
                    variable = ddm_match_subjects_raw
                    order_by = { value = ddm_match_subject_skills_sort_value multiply = -1 }
                    max = 5000
                    check_range_bounds = no
                    save_temporary_scope_as = ddm_match_sorted_subject
                    scope:ddm_match_actor = { add_to_variable_list = { name = ddm_match_subjects_sorted target = scope:ddm_match_sorted_subject } }
                }
            }
            else = {
                ordered_in_list = {
                    variable = ddm_match_subjects_raw
                    order_by = ddm_match_subject_skills_sort_value
                    max = 5000
                    check_range_bounds = no
                    save_temporary_scope_as = ddm_match_sorted_subject
                    scope:ddm_match_actor = { add_to_variable_list = { name = ddm_match_subjects_sorted target = scope:ddm_match_sorted_subject } }
                }
            }
        }
        else = {
            # 0 = DDM priority (player/heir/close family + dynastic value).
            if = {
                limit = { var:ddm_match_sort_direction < 0 }
                ordered_in_list = {
                    variable = ddm_match_subjects_raw
                    order_by = { value = ddm_match_subject_priority_value multiply = -1 }
                    max = 5000
                    check_range_bounds = no
                    save_temporary_scope_as = ddm_match_sorted_subject
                    scope:ddm_match_actor = { add_to_variable_list = { name = ddm_match_subjects_sorted target = scope:ddm_match_sorted_subject } }
                }
            }
            else = {
                ordered_in_list = {
                    variable = ddm_match_subjects_raw
                    order_by = ddm_match_subject_priority_value
                    max = 5000
                    check_range_bounds = no
                    save_temporary_scope_as = ddm_match_sorted_subject
                    scope:ddm_match_actor = { add_to_variable_list = { name = ddm_match_subjects_sorted target = scope:ddm_match_sorted_subject } }
                }
            }
        }
    }
}

ddm_build_matchmaker_page_effect = {
    if = { limit = { NOT = { has_variable = ddm_match_page } } set_variable = { name = ddm_match_page value = 0 } }
    clear_variable_list = ddm_match_subjects

    # Candidate enumeration is one of CK3's slower character lists. Keep only four dynasts on a
    # Marriage Planner page: this keeps every row and its action buttons inside the Office window and
    # also bounds each page refresh to four expensive spouse-candidate scans.
    set_variable = { name = ddm_match_page_size value = 4 }
    set_variable = { name = ddm_match_last_page value = 0 }
    set_variable = { name = ddm_match_page_fill value = 0 }

    if = {
        limit = { has_variable_list = ddm_match_subjects_sorted }
        every_in_list = {
            variable = ddm_match_subjects_sorted
            prev = {
                if = {
                    limit = { var:ddm_match_page_fill >= var:ddm_match_page_size }
                    change_variable = { name = ddm_match_last_page add = 1 }
                    set_variable = { name = ddm_match_page_fill value = 0 }
                }
                change_variable = { name = ddm_match_page_fill add = 1 }
            }
        }
    }

    if = { limit = { var:ddm_match_page < 0 } set_variable = { name = ddm_match_page value = 0 } }
    if = { limit = { var:ddm_match_page > var:ddm_match_last_page } set_variable = { name = ddm_match_page value = var:ddm_match_last_page } }

    set_variable = { name = ddm_match_first value = var:ddm_match_page }
    change_variable = { name = ddm_match_first multiply = var:ddm_match_page_size }
    set_variable = { name = ddm_match_stop value = var:ddm_match_first }
    change_variable = { name = ddm_match_stop add = var:ddm_match_page_size }
    set_variable = { name = ddm_match_index value = 0 }

    if = {
        limit = { has_variable_list = ddm_match_subjects_sorted }
        every_in_list = {
            variable = ddm_match_subjects_sorted
            save_temporary_scope_as = ddm_match_page_subject
            prev = {
                if = {
                    limit = { var:ddm_match_index >= var:ddm_match_first var:ddm_match_index < var:ddm_match_stop }
                    add_to_variable_list = { name = ddm_match_subjects target = scope:ddm_match_page_subject }
                }
                change_variable = { name = ddm_match_index add = 1 }
            }
        }
    }

    set_variable = { name = ddm_match_shown_from value = var:ddm_match_first }
    change_variable = { name = ddm_match_shown_from add = 1 }
    set_variable = { name = ddm_match_shown_to value = var:ddm_match_stop }
    if = { limit = { var:ddm_match_shown_to > var:ddm_match_eligible_count } set_variable = { name = ddm_match_shown_to value = var:ddm_match_eligible_count } }
    if = { limit = { var:ddm_match_eligible_count = 0 } set_variable = { name = ddm_match_shown_from value = 0 } }
    set_variable = { name = ddm_match_page_display value = var:ddm_match_page }
    change_variable = { name = ddm_match_page_display add = 1 }
    set_variable = { name = ddm_match_pages_display value = var:ddm_match_last_page }
    change_variable = { name = ddm_match_pages_display add = 1 }

    set_variable = { name = ddm_match_found_count value = 0 }
    save_scope_as = ddm_match_actor
    if = {
        limit = { has_variable_list = ddm_match_subjects }
        every_in_list = {
            variable = ddm_match_subjects
            save_temporary_scope_as = ddm_match_subject
            scope:ddm_match_actor = { ddm_recommend_matches_for_subject_effect = yes }
        }
    }
}

# ---------------------------------------------------------------------------
# DYNASTY PLAN — a non-conflicting proposed marriage board.
#
# A CK3 variable list can hold characters but not arbitrary pair-tuples. The plan therefore stores one
# candidate on each subject and reserves both characters while the board is built. It can be sorted from
# strongest to weakest (or reversed to put the weakest planned pairs first) and safely consumed by
# "Marry All".
# ---------------------------------------------------------------------------

ddm_cleanup_match_pair_board_effect = {
    if = {
        limit = { has_variable_list = ddm_match_pair_subjects_raw }
        every_in_list = {
            variable = ddm_match_pair_subjects_raw
            remove_variable = ddm_match_pair_candidate
            remove_variable = ddm_match_pair_score
            remove_variable = ddm_match_pair_genetic
            remove_variable = ddm_match_pair_love
            remove_variable = ddm_match_pair_score_max
            remove_variable = ddm_match_pair_kinship
            remove_variable = ddm_match_pair_rank
            remove_variable = ddm_match_pair_managed
        }
    }
    if = {
        limit = { has_variable_list = ddm_match_pair_reserved_characters }
        every_in_list = {
            variable = ddm_match_pair_reserved_characters
            remove_variable = ddm_match_pair_reserved
        }
    }
    clear_variable_list = ddm_match_pair_reserved_characters
    clear_variable_list = ddm_match_plan_subjects_cached
    clear_variable_list = ddm_match_pair_subjects_raw
    clear_variable_list = ddm_match_pair_subjects_sorted
    clear_variable_list = ddm_match_pair_subjects_page
}

ddm_cache_match_pair_effect = {
    # Run from player with subject/candidate scopes saved.
    scope:ddm_match_subject = {
        set_variable = { name = ddm_match_pair_candidate value = scope:ddm_match_candidate }
        set_variable = { name = ddm_match_pair_score value = scope:ddm_match_candidate.ddm_match_candidate_score_value }
        set_variable = { name = ddm_match_pair_genetic value = scope:ddm_match_candidate.ddm_match_genetic_component_value }
        set_variable = { name = ddm_match_pair_love value = scope:ddm_match_candidate.ddm_match_love_component_value }
        set_variable = { name = ddm_match_pair_score_max value = ddm_match_total_reference_max_value }
        set_variable = { name = ddm_match_pair_reserved value = 1 }
        if = { limit = { scope:ddm_match_candidate = { is_close_family_of = scope:ddm_match_subject } } set_variable = { name = ddm_match_pair_kinship value = 3 } }
        else_if = { limit = { scope:ddm_match_candidate = { is_extended_family_of = scope:ddm_match_subject } } set_variable = { name = ddm_match_pair_kinship value = 2 } }
        else_if = { limit = { scope:ddm_match_candidate = { dynasty ?= scope:ddm_match_subject.dynasty } } set_variable = { name = ddm_match_pair_kinship value = 1 } }
        else = { set_variable = { name = ddm_match_pair_kinship value = 0 } }
    }
    scope:ddm_match_candidate = { set_variable = { name = ddm_match_pair_reserved value = 1 } }

    add_to_variable_list = { name = ddm_match_pair_subjects_raw target = scope:ddm_match_subject }
    add_to_variable_list = { name = ddm_match_pair_reserved_characters target = scope:ddm_match_subject }
    add_to_variable_list = { name = ddm_match_pair_reserved_characters target = scope:ddm_match_candidate }
    change_variable = { name = ddm_match_pair_total add = 1 }

    if = {
        limit = {
            scope:ddm_match_subject = { is_adult = yes matchmaker = scope:ddm_match_actor }
            scope:ddm_match_candidate = { is_adult = yes matchmaker = scope:ddm_match_actor }
        }
        scope:ddm_match_subject = { set_variable = { name = ddm_match_pair_managed value = 1 } }
        change_variable = { name = ddm_match_pair_managed_total add = 1 }
    }
}

ddm_sort_match_pair_board_effect = {
    clear_variable_list = ddm_match_pair_subjects_sorted
    save_scope_as = ddm_match_actor

    if = {
        limit = { has_variable_list = ddm_match_pair_subjects_raw }
        if = {
            limit = { var:ddm_match_pair_sort_mode = 1 }
            if = {
                limit = { var:ddm_match_pair_sort_direction < 0 }
                ordered_in_list = {
                    variable = ddm_match_pair_subjects_raw
                    order_by = { value = ddm_match_pair_child_sort_value multiply = -1 }
                    max = 5000 check_range_bounds = no
                    save_temporary_scope_as = ddm_match_pair_subject_sorted
                    scope:ddm_match_actor = { add_to_variable_list = { name = ddm_match_pair_subjects_sorted target = scope:ddm_match_pair_subject_sorted } }
                }
            }
            else = {
                ordered_in_list = {
                    variable = ddm_match_pair_subjects_raw
                    order_by = ddm_match_pair_child_sort_value
                    max = 5000 check_range_bounds = no
                    save_temporary_scope_as = ddm_match_pair_subject_sorted
                    scope:ddm_match_actor = { add_to_variable_list = { name = ddm_match_pair_subjects_sorted target = scope:ddm_match_pair_subject_sorted } }
                }
            }
        }
        else_if = {
            limit = { var:ddm_match_pair_sort_mode = 2 }
            if = {
                limit = { var:ddm_match_pair_sort_direction < 0 }
                ordered_in_list = {
                    variable = ddm_match_pair_subjects_raw
                    order_by = { value = ddm_match_pair_love_sort_value multiply = -1 }
                    max = 5000 check_range_bounds = no
                    save_temporary_scope_as = ddm_match_pair_subject_sorted
                    scope:ddm_match_actor = { add_to_variable_list = { name = ddm_match_pair_subjects_sorted target = scope:ddm_match_pair_subject_sorted } }
                }
            }
            else = {
                ordered_in_list = {
                    variable = ddm_match_pair_subjects_raw
                    order_by = ddm_match_pair_love_sort_value
                    max = 5000 check_range_bounds = no
                    save_temporary_scope_as = ddm_match_pair_subject_sorted
                    scope:ddm_match_actor = { add_to_variable_list = { name = ddm_match_pair_subjects_sorted target = scope:ddm_match_pair_subject_sorted } }
                }
            }
        }
        else_if = {
            limit = { var:ddm_match_pair_sort_mode = 3 }
            if = {
                limit = { var:ddm_match_pair_sort_direction < 0 }
                ordered_in_list = {
                    variable = ddm_match_pair_subjects_raw
                    order_by = { value = ddm_match_pair_kinship_sort_value multiply = -1 }
                    max = 5000 check_range_bounds = no
                    save_temporary_scope_as = ddm_match_pair_subject_sorted
                    scope:ddm_match_actor = { add_to_variable_list = { name = ddm_match_pair_subjects_sorted target = scope:ddm_match_pair_subject_sorted } }
                }
            }
            else = {
                ordered_in_list = {
                    variable = ddm_match_pair_subjects_raw
                    order_by = ddm_match_pair_kinship_sort_value
                    max = 5000 check_range_bounds = no
                    save_temporary_scope_as = ddm_match_pair_subject_sorted
                    scope:ddm_match_actor = { add_to_variable_list = { name = ddm_match_pair_subjects_sorted target = scope:ddm_match_pair_subject_sorted } }
                }
            }
        }
        else_if = {
            limit = { var:ddm_match_pair_sort_mode = 4 }
            if = {
                limit = { var:ddm_match_pair_sort_direction < 0 }
                ordered_in_list = {
                    variable = ddm_match_pair_subjects_raw
                    order_by = { value = ddm_match_pair_direct_sort_value multiply = -1 }
                    max = 5000 check_range_bounds = no
                    save_temporary_scope_as = ddm_match_pair_subject_sorted
                    scope:ddm_match_actor = { add_to_variable_list = { name = ddm_match_pair_subjects_sorted target = scope:ddm_match_pair_subject_sorted } }
                }
            }
            else = {
                ordered_in_list = {
                    variable = ddm_match_pair_subjects_raw
                    order_by = ddm_match_pair_direct_sort_value
                    max = 5000 check_range_bounds = no
                    save_temporary_scope_as = ddm_match_pair_subject_sorted
                    scope:ddm_match_actor = { add_to_variable_list = { name = ddm_match_pair_subjects_sorted target = scope:ddm_match_pair_subject_sorted } }
                }
            }
        }
        else = {
            if = {
                limit = { var:ddm_match_pair_sort_direction < 0 }
                ordered_in_list = {
                    variable = ddm_match_pair_subjects_raw
                    order_by = { value = ddm_match_pair_score_sort_value multiply = -1 }
                    max = 5000 check_range_bounds = no
                    save_temporary_scope_as = ddm_match_pair_subject_sorted
                    scope:ddm_match_actor = { add_to_variable_list = { name = ddm_match_pair_subjects_sorted target = scope:ddm_match_pair_subject_sorted } }
                }
            }
            else = {
                ordered_in_list = {
                    variable = ddm_match_pair_subjects_raw
                    order_by = ddm_match_pair_score_sort_value
                    max = 5000 check_range_bounds = no
                    save_temporary_scope_as = ddm_match_pair_subject_sorted
                    scope:ddm_match_actor = { add_to_variable_list = { name = ddm_match_pair_subjects_sorted target = scope:ddm_match_pair_subject_sorted } }
                }
            }
        }
    }
}

ddm_try_cache_pair_from_cached_recommendations_effect = {
    # Run from player with scope:ddm_match_subject saved. The candidate rank is stored on the finished
    # pair so Dynasty Plan can show how far it had to move from this person's Individual recommendation.
    if = {
        limit = { scope:ddm_match_subject = { NOT = { has_variable = ddm_match_pair_reserved } exists = var:ddm_match_best var:ddm_match_best = { NOT = { has_variable = ddm_match_pair_reserved } } } }
        scope:ddm_match_subject = {
            set_variable = { name = ddm_match_pair_rank value = 1 }
            var:ddm_match_best = { save_scope_as = ddm_match_candidate }
        }
        ddm_cache_match_pair_effect = yes
    }
    else_if = {
        limit = { scope:ddm_match_subject = { NOT = { has_variable = ddm_match_pair_reserved } exists = var:ddm_match_second var:ddm_match_second = { NOT = { has_variable = ddm_match_pair_reserved } } } }
        scope:ddm_match_subject = {
            set_variable = { name = ddm_match_pair_rank value = 2 }
            var:ddm_match_second = { save_scope_as = ddm_match_candidate }
        }
        ddm_cache_match_pair_effect = yes
    }
    else_if = {
        limit = { scope:ddm_match_subject = { NOT = { has_variable = ddm_match_pair_reserved } exists = var:ddm_match_third var:ddm_match_third = { NOT = { has_variable = ddm_match_pair_reserved } } } }
        scope:ddm_match_subject = {
            set_variable = { name = ddm_match_pair_rank value = 3 }
            var:ddm_match_third = { save_scope_as = ddm_match_candidate }
        }
        ddm_cache_match_pair_effect = yes
    }
    else_if = {
        limit = { scope:ddm_match_subject = { NOT = { has_variable = ddm_match_pair_reserved } exists = var:ddm_match_plan_fourth var:ddm_match_plan_fourth = { NOT = { has_variable = ddm_match_pair_reserved } } } }
        scope:ddm_match_subject = {
            set_variable = { name = ddm_match_pair_rank value = 4 }
            var:ddm_match_plan_fourth = { save_scope_as = ddm_match_candidate }
        }
        ddm_cache_match_pair_effect = yes
    }
    else_if = {
        limit = { scope:ddm_match_subject = { NOT = { has_variable = ddm_match_pair_reserved } exists = var:ddm_match_plan_fifth var:ddm_match_plan_fifth = { NOT = { has_variable = ddm_match_pair_reserved } } } }
        scope:ddm_match_subject = {
            set_variable = { name = ddm_match_pair_rank value = 5 }
            var:ddm_match_plan_fifth = { save_scope_as = ddm_match_candidate }
        }
        ddm_cache_match_pair_effect = yes
    }
    else_if = {
        limit = { scope:ddm_match_subject = { NOT = { has_variable = ddm_match_pair_reserved } exists = var:ddm_match_plan_sixth var:ddm_match_plan_sixth = { NOT = { has_variable = ddm_match_pair_reserved } } } }
        scope:ddm_match_subject = {
            set_variable = { name = ddm_match_pair_rank value = 6 }
            var:ddm_match_plan_sixth = { save_scope_as = ddm_match_candidate }
        }
        ddm_cache_match_pair_effect = yes
    }
    else_if = {
        limit = { scope:ddm_match_subject = { NOT = { has_variable = ddm_match_pair_reserved } exists = var:ddm_match_plan_seventh var:ddm_match_plan_seventh = { NOT = { has_variable = ddm_match_pair_reserved } } } }
        scope:ddm_match_subject = {
            set_variable = { name = ddm_match_pair_rank value = 7 }
            var:ddm_match_plan_seventh = { save_scope_as = ddm_match_candidate }
        }
        ddm_cache_match_pair_effect = yes
    }
    else_if = {
        limit = { scope:ddm_match_subject = { NOT = { has_variable = ddm_match_pair_reserved } exists = var:ddm_match_plan_eighth var:ddm_match_plan_eighth = { NOT = { has_variable = ddm_match_pair_reserved } } } }
        scope:ddm_match_subject = {
            set_variable = { name = ddm_match_pair_rank value = 8 }
            var:ddm_match_plan_eighth = { save_scope_as = ddm_match_candidate }
        }
        ddm_cache_match_pair_effect = yes
    }
}

# Dynasty Plan is global across every dynast whose recommendation cache is current. Switching to it never
# launches a spouse search: it consumes whatever has already been scanned while using the Individual view.
# "Complete Dynasty Scan" is the explicit, potentially expensive action that fills any missing caches.
ddm_build_cached_match_pair_board_effect = {
    ddm_cleanup_match_pair_board_effect = yes
    set_variable = { name = ddm_match_pair_total value = 0 }
    set_variable = { name = ddm_match_pair_managed_total value = 0 }
    set_variable = { name = ddm_match_plan_cached_total value = 0 }
    save_scope_as = ddm_match_actor

    if = {
        limit = { has_variable_list = ddm_match_subjects_sorted }
        every_in_list = {
            variable = ddm_match_subjects_sorted
            save_temporary_scope_as = ddm_match_plan_subject
            if = {
                limit = {
                    scope:ddm_match_plan_subject = { ddm_match_recommendations_current_trigger = yes }
                }
                scope:ddm_match_actor = { change_variable = { name = ddm_match_plan_cached_total add = 1 } }
                if = {
                    limit = { scope:ddm_match_plan_subject = { exists = var:ddm_match_best } }
                    scope:ddm_match_actor = { add_to_variable_list = { name = ddm_match_plan_subjects_cached target = scope:ddm_match_plan_subject } }
                }
            }
        }
    }

    # Give the strongest Individual recommendations first claim on contested candidates. Reservation then
    # makes the remaining dynasts fall through their cached #2–#8 choices. This is intentionally a fast
    # deterministic global plan, not an exponential all-combinations solver.
    if = {
        limit = { has_variable_list = ddm_match_plan_subjects_cached }
        ordered_in_list = {
            variable = ddm_match_plan_subjects_cached
            order_by = ddm_match_plan_subject_priority_value
            max = 5000
            check_range_bounds = no
            save_temporary_scope_as = ddm_match_subject
            scope:ddm_match_actor = { ddm_try_cache_pair_from_cached_recommendations_effect = yes }
        }
    }

    ddm_sort_match_pair_board_effect = yes
    clear_variable_list = ddm_match_pair_subjects_page
    if = {
        limit = { has_variable_list = ddm_match_pair_subjects_sorted }
        every_in_list = {
            variable = ddm_match_pair_subjects_sorted
            save_temporary_scope_as = ddm_match_pair_subject_sorted
            scope:ddm_match_actor = { add_to_variable_list = { name = ddm_match_pair_subjects_page target = scope:ddm_match_pair_subject_sorted } }
        }
    }
}

# Explicit opt-in full scan for players who want a truly dynasty-wide plan. This may be expensive on huge
# dynasties because spouse discovery itself is expensive; unlike a tab switch, the player chooses when to
# pay that cost. Current caches are reused, so only unscanned/stale dynasts need work.
ddm_complete_dynasty_match_scan_effect = {
    save_scope_as = ddm_match_actor
    set_variable = { name = ddm_match_found_count value = 0 }

    if = {
        limit = { has_variable_list = ddm_match_subjects_sorted }
        every_in_list = {
            variable = ddm_match_subjects_sorted
            save_temporary_scope_as = ddm_match_subject
            scope:ddm_match_actor = { ddm_recommend_matches_for_subject_effect = yes }
        }
    }

    ddm_build_cached_match_pair_board_effect = yes
}

# ---------------------------------------------------------------------------
# AUTO-FINALIZATION
# ---------------------------------------------------------------------------

ddm_try_finalize_saved_match_pair_effect = {
    # Run from player with scope:ddm_match_subject and scope:ddm_match_candidate saved.
    remove_variable = ddm_match_current_pair_valid

    if = {
        limit = {
            scope:ddm_match_subject = {
                is_alive = yes
                is_adult = yes
                is_married = no
                is_betrothed = no
                matchmaker = scope:ddm_match_actor
            }
            scope:ddm_match_candidate = {
                is_alive = yes
                is_adult = yes
                is_married = no
                is_betrothed = no
                matchmaker = scope:ddm_match_actor
            }
        }
        scope:ddm_match_subject = {
            ordered_spouse_candidate = {
                limit = {
                    this = scope:ddm_match_candidate
                    ddm_match_candidate_allowed_trigger = yes
                }
                max = 1
                check_range_bounds = no
                scope:ddm_match_actor = { set_variable = { name = ddm_match_current_pair_valid value = 1 } }
            }
        }
    }

    # Dominion Owned-family authority: the player may directly arrange an adult child of an Owned subject,
    # even when vanilla does not make the player that child's matchmaker. Pair legality still comes from the
    # loaded marriage rules; only acceptance/control is overridden by Dominion.
    if = {
        limit = {
            NOT = { has_variable = ddm_match_current_pair_valid }
            scope:ddm_match_subject = {
                is_alive = yes
                is_adult = yes
                is_married = no
                is_betrothed = no
                ddm_match_subject_owned_family_control_trigger = yes
            }
            scope:ddm_match_candidate = { is_alive = yes is_adult = yes is_married = no is_betrothed = no }
        }
        if = {
            limit = { scope:ddm_match_candidate = { ddm_match_candidate_allowed_trigger = yes } }
            set_variable = { name = ddm_match_current_pair_valid value = 1 }
        }
    }

    if = {
        limit = { has_variable = ddm_match_current_pair_valid }
        # Make the managed dynast the dominant spouse. For an opposite-sex marriage this means a male
        # subject marries patrilineally and a female subject matrilineally, keeping that dynast's children
        # in their dynasty. Same-sex marriage still obeys whatever legality the engine exposed.
        scope:ddm_match_subject = {
            if = { limit = { is_male = yes } marry = scope:ddm_match_candidate }
            else = { marry_matrilineal = scope:ddm_match_candidate }
        }
        change_variable = { name = ddm_match_auto_married add = 1 }
    }
    else = { change_variable = { name = ddm_match_auto_skipped add = 1 } }

    remove_variable = ddm_match_current_pair_valid
}

# Row/manual action. If DDM controls both matchmakers, finalize directly. Otherwise open CK3's own
# Arrange Marriage interaction with both people already selected. This preserves vanilla/mod acceptance,
# hooks, matrilineal choice, grand weddings and betrothals while removing the pointless manual re-search.
ddm_try_finalize_or_open_proposal_effect = {
    remove_variable = ddm_match_proposal_opened

    if = {
        limit = {
            scope:ddm_match_subject = { is_adult = yes matchmaker = scope:ddm_match_actor }
            scope:ddm_match_candidate = { is_adult = yes matchmaker = scope:ddm_match_actor }
        }
        ddm_try_finalize_saved_match_pair_effect = yes
    }
    # Two characters whose matchmaker is the player can be planned before adulthood. DDM does not invent
    # its own betrothal effect: it opens vanilla Arrange Marriage on the player's own court with both people
    # preselected, allowing the loaded game's age, lineality, Grand Wedding and other terms to decide.
    else_if = {
        limit = {
            scope:ddm_match_subject = {
                is_alive = yes
                is_married = no
                is_betrothed = no
                matchmaker = scope:ddm_match_actor
            }
            scope:ddm_match_candidate = {
                is_alive = yes
                is_married = no
                is_betrothed = no
                matchmaker = scope:ddm_match_actor
            }
            OR = {
                scope:ddm_match_subject = { is_adult = no }
                scope:ddm_match_candidate = { is_adult = no }
            }
        }
        scope:ddm_match_subject = {
            ordered_spouse_candidate = {
                limit = { this = scope:ddm_match_candidate ddm_match_candidate_allowed_trigger = yes }
                max = 1
                check_range_bounds = no
                scope:ddm_match_actor = { set_variable = { name = ddm_match_current_pair_valid value = 1 } }
            }
            if = {
                limit = { scope:ddm_match_actor = { NOT = { has_variable = ddm_match_current_pair_valid } } }
                ordered_potential_marriage_option = {
                    limit = { this = scope:ddm_match_candidate ddm_match_candidate_allowed_trigger = yes }
                    max = 1
                    check_range_bounds = no
                    scope:ddm_match_actor = { set_variable = { name = ddm_match_current_pair_valid value = 1 } }
                }
            }
        }
        if = {
            limit = { has_variable = ddm_match_current_pair_valid }
            open_interaction_window = {
                interaction = arrange_marriage_interaction
                redirect = no
                actor = scope:ddm_match_actor
                recipient = scope:ddm_match_actor
                secondary_actor = scope:ddm_match_subject
                secondary_recipient = scope:ddm_match_candidate
            }
            set_variable = { name = ddm_match_proposal_opened value = 1 }
        }
    }
    # Dominion family-control path. Children of an Owned subject are under the Patron's household authority.
    # Adults marry directly; if either side is a minor, create a real CK3 betrothal instead of opening a UI
    # that would reject the Patron as the vanilla matchmaker.
    else_if = {
        limit = {
            scope:ddm_match_subject = {
                is_alive = yes
                is_married = no
                is_betrothed = no
                ddm_match_subject_owned_family_control_trigger = yes
            }
            scope:ddm_match_candidate = { is_alive = yes is_married = no is_betrothed = no }
        }
        if = {
            limit = { scope:ddm_match_candidate = { ddm_match_candidate_allowed_trigger = yes } }
            set_variable = { name = ddm_match_current_pair_valid value = 1 }
        }
        if = {
            limit = { has_variable = ddm_match_current_pair_valid }
            if = {
                limit = { scope:ddm_match_subject = { is_adult = yes } scope:ddm_match_candidate = { is_adult = yes } }
                scope:ddm_match_subject = {
                    if = { limit = { is_male = yes } marry = scope:ddm_match_candidate }
                    else = { marry_matrilineal = scope:ddm_match_candidate }
                }
            }
            else = {
                scope:ddm_match_subject = {
                    if = { limit = { is_male = yes } create_betrothal = scope:ddm_match_candidate }
                    else = { create_betrothal_matrilineal = scope:ddm_match_candidate }
                }
            }
            change_variable = { name = ddm_match_auto_married add = 1 }
        }
    }

    else_if = {
        limit = {
            scope:ddm_match_subject = { is_alive = yes is_married = no is_betrothed = no matchmaker = scope:ddm_match_actor }
            scope:ddm_match_candidate = { is_alive = yes is_married = no is_betrothed = no exists = matchmaker NOT = { matchmaker = scope:ddm_match_actor } }
        }
        # Re-check this exact candidate against the current spouse pool before opening the interaction.
        scope:ddm_match_subject = {
            ordered_spouse_candidate = {
                limit = { this = scope:ddm_match_candidate ddm_match_candidate_allowed_trigger = yes }
                max = 1
                check_range_bounds = no
                scope:ddm_match_actor = { set_variable = { name = ddm_match_current_pair_valid value = 1 } }
            }
        }
        if = {
            limit = { has_variable = ddm_match_current_pair_valid }
            open_interaction_window = {
                interaction = arrange_marriage_interaction
                redirect = no
                actor = scope:ddm_match_actor
                recipient = scope:ddm_match_candidate.matchmaker
                secondary_actor = scope:ddm_match_subject
                secondary_recipient = scope:ddm_match_candidate
            }
            set_variable = { name = ddm_match_proposal_opened value = 1 }
        }
    }
    # Vanilla family-authority path. A landed child can be their own matchmaker yet still appear in the
    # player's Arrange Marriage actor list. DDM must open vanilla's proposal UI for these rows rather than
    # trying to force-finalize a marriage the player does not directly own as matchmaker.
    else_if = {
        limit = {
            scope:ddm_match_subject = {
                is_alive = yes
                is_married = no
                is_betrothed = no
                ddm_match_subject_player_arrangeable_trigger = yes
                NOT = { matchmaker = scope:ddm_match_actor }
            }
            scope:ddm_match_candidate = { is_alive = yes is_married = no is_betrothed = no exists = matchmaker }
        }
        scope:ddm_match_subject = {
            ordered_spouse_candidate = {
                limit = { this = scope:ddm_match_candidate ddm_match_candidate_allowed_trigger = yes }
                max = 1
                check_range_bounds = no
                scope:ddm_match_actor = { set_variable = { name = ddm_match_current_pair_valid value = 1 } }
            }
        }
        if = {
            limit = { has_variable = ddm_match_current_pair_valid }
            # If the recommendation is one of the player's own people, put that character on the actor
            # side and the landed child on the recipient side. Otherwise preserve the child as the
            # actor-side family member, exactly as vanilla's actor list permits.
            if = {
                limit = { scope:ddm_match_candidate = { matchmaker = scope:ddm_match_actor } }
                open_interaction_window = {
                    interaction = arrange_marriage_interaction
                    redirect = no
                    actor = scope:ddm_match_actor
                    recipient = scope:ddm_match_subject.matchmaker
                    secondary_actor = scope:ddm_match_candidate
                    secondary_recipient = scope:ddm_match_subject
                }
            }
            else = {
                open_interaction_window = {
                    interaction = arrange_marriage_interaction
                    redirect = no
                    actor = scope:ddm_match_actor
                    recipient = scope:ddm_match_candidate.matchmaker
                    secondary_actor = scope:ddm_match_subject
                    secondary_recipient = scope:ddm_match_candidate
                }
            }
            set_variable = { name = ddm_match_proposal_opened value = 1 }
        }
    }

    remove_variable = ddm_match_current_pair_valid
}

ddm_marry_selected_match_effect = {
    save_scope_as = ddm_match_subject
    if = {
        limit = { exists = var:ddm_match_planner_owner exists = var:ddm_match_sel }
        var:ddm_match_sel = { save_scope_as = ddm_match_candidate }
        var:ddm_match_planner_owner = {
            save_scope_as = ddm_match_actor
            set_variable = { name = ddm_match_auto_married value = 0 }
            set_variable = { name = ddm_match_auto_skipped value = 0 }
            ddm_try_finalize_or_open_proposal_effect = yes
            if = {
                limit = { var:ddm_match_auto_married > 0 }
                send_interface_toast = { type = event_toast_effect_good title = DDM_MATCH_MARRIED_PAIR_TOAST left_icon = scope:ddm_match_subject right_icon = scope:ddm_match_candidate }
                ddm_prepare_matchmaker_effect = yes
            }
            else_if = {
                limit = { has_variable = ddm_match_proposal_opened }
                send_interface_toast = { type = event_toast_effect_neutral title = DDM_MATCH_PROPOSAL_OPENED_TOAST left_icon = scope:ddm_match_subject right_icon = scope:ddm_match_candidate }
            }
            else = {
                send_interface_toast = { type = event_toast_effect_neutral title = DDM_MATCH_PAIR_NOT_AVAILABLE_TOAST left_icon = scope:ddm_match_subject right_icon = scope:ddm_match_candidate }
            }
        }
    }
}

ddm_marry_cached_pair_effect = {
    save_scope_as = ddm_match_subject
    if = {
        limit = { exists = var:ddm_match_planner_owner exists = var:ddm_match_pair_candidate }
        var:ddm_match_pair_candidate = { save_scope_as = ddm_match_candidate }
        var:ddm_match_planner_owner = {
            save_scope_as = ddm_match_actor
            set_variable = { name = ddm_match_auto_married value = 0 }
            set_variable = { name = ddm_match_auto_skipped value = 0 }
            ddm_try_finalize_or_open_proposal_effect = yes
            if = {
                limit = { var:ddm_match_auto_married > 0 }
                send_interface_toast = { type = event_toast_effect_good title = DDM_MATCH_MARRIED_PAIR_TOAST left_icon = scope:ddm_match_subject right_icon = scope:ddm_match_candidate }
                ddm_prepare_matchmaker_effect = yes
            }
            else_if = {
                limit = { has_variable = ddm_match_proposal_opened }
                send_interface_toast = { type = event_toast_effect_neutral title = DDM_MATCH_PROPOSAL_OPENED_TOAST left_icon = scope:ddm_match_subject right_icon = scope:ddm_match_candidate }
            }
            else = {
                send_interface_toast = { type = event_toast_effect_neutral title = DDM_MATCH_PAIR_NOT_AVAILABLE_TOAST left_icon = scope:ddm_match_subject right_icon = scope:ddm_match_candidate }
            }
        }
    }
}

ddm_marry_all_planned_pairs_effect = {
    save_scope_as = ddm_match_actor
    set_variable = { name = ddm_match_auto_married value = 0 }
    set_variable = { name = ddm_match_auto_skipped value = 0 }

    if = {
        limit = { has_variable_list = ddm_match_pair_subjects_raw }
        every_in_list = {
            variable = ddm_match_pair_subjects_raw
            save_temporary_scope_as = ddm_match_subject
            if = {
                limit = { exists = var:ddm_match_pair_candidate }
                var:ddm_match_pair_candidate = { save_scope_as = ddm_match_candidate }
                scope:ddm_match_actor = { ddm_try_finalize_saved_match_pair_effect = yes }
            }
        }
    }

    if = {
        limit = { var:ddm_match_auto_married > 0 }
        send_interface_toast = { type = event_toast_effect_good title = DDM_MATCH_MARRY_ALL_TOAST left_icon = root }
    }
    else = {
        send_interface_toast = { type = event_toast_effect_neutral title = DDM_MATCH_MARRY_ALL_NONE_TOAST left_icon = root }
    }

    ddm_prepare_matchmaker_effect = yes
}

# Recompute only the currently visible dynasty page. Subject eligibility is unchanged when the player
# toggles a scoring philosophy or kinship strategy, so there is no reason to rescan the entire dynasty.
ddm_refresh_matchmaker_current_page_effect = {
    ddm_build_matchmaker_page_effect = yes
    if = {
        limit = { has_variable = ddm_match_show_player_match var:ddm_match_show_player_match = 1 }
        # Policy/kinship changes invalidate the player's cached policy naturally; rescan only that card.
        save_scope_as = ddm_match_subject
        ddm_recommend_matches_for_subject_effect = yes
    }
    if = { limit = { var:ddm_match_view_mode = 1 } ddm_build_cached_match_pair_board_effect = yes }
}

ddm_resort_matchmaker_effect = {
    ddm_sort_match_subjects_effect = yes
    set_variable = { name = ddm_match_page value = 0 }
    ddm_refresh_matchmaker_current_page_effect = yes
}

# ---------------------------------------------------------------------------
# PREP / NAVIGATION
# ---------------------------------------------------------------------------

# Refresh preserves the user's scan coverage. If the Dynasty Plan was complete before refresh,
# refresh deliberately recomputes the full dynasty so the view remains complete rather than collapsing
# back to the four currently visible Individual rows. If coverage was partial, only the visible page is
# recomputed and the partial-plan behavior is preserved.
ddm_refresh_matchmaker_effect = {
    ddm_initialize_settings_effect = yes
    remove_variable = ddm_match_refresh_was_complete
    if = {
        limit = {
            has_variable = ddm_match_eligible_count
            has_variable = ddm_match_plan_cached_total
            var:ddm_match_eligible_count > 0
            var:ddm_match_plan_cached_total >= var:ddm_match_eligible_count
        }
        set_variable = { name = ddm_match_refresh_was_complete value = 1 }
    }

    # A user-requested refresh really does invalidate scores/candidates.
    change_variable = { name = ddm_match_scan_generation add = 1 }
    ddm_prepare_matchmaker_effect = yes

    if = {
        limit = { has_variable = ddm_match_refresh_was_complete }
        ddm_complete_dynasty_match_scan_effect = yes
    }
    remove_variable = ddm_match_refresh_was_complete
}

# Lightweight preparation/re-entry path. It rebuilds eligibility and the visible page while reusing
# still-current caches. Opening the planner, switching pages, or returning from a marriage proposal must
# not silently invalidate a completed Dynasty Plan.
ddm_prepare_matchmaker_effect = {
    ddm_initialize_settings_effect = yes
    save_scope_as = ddm_match_actor

    # A completed marriage should not leave a stale "My Best Match" card on screen.
    if = {
        limit = {
            has_variable = ddm_match_show_player_match
            OR = { is_married = yes is_betrothed = yes is_concubine = yes NOT = { can_marry_trigger = yes } }
        }
        remove_variable = ddm_match_show_player_match
        clear_variable_list = ddm_match_player_subject
    }

    clear_variable_list = ddm_match_subjects_raw
    clear_variable_list = ddm_match_subjects_sorted
    set_variable = { name = ddm_match_eligible_count value = 0 }

    if = {
        limit = { exists = dynasty }
        dynasty = {
            every_dynasty_member = {
                limit = {
                    is_alive = yes
                    NOT = { this = scope:ddm_match_actor }
                    is_married = no
                    is_betrothed = no
                    is_concubine = no
                    can_marry_trigger = yes
                    # Deliberately no adulthood floor. If CK3 or another mod permits this character to
                    # marry or be betrothed, DDM may plan for them now instead of waiting until adulthood.
                    # Matchmaker alone is not authoritative: vanilla also lets a ruler arrange matches for their own children
                    # when realm/House/Dynasty authority still applies, including landed vassal children.
                    ddm_match_subject_player_arrangeable_trigger = yes
                }
                save_temporary_scope_as = ddm_match_subject
                scope:ddm_match_actor = {
                    add_to_variable_list = { name = ddm_match_subjects_raw target = scope:ddm_match_subject }
                    change_variable = { name = ddm_match_eligible_count add = 1 }
                }
            }
        }
    }

    # Vanilla's marriage actor list is also explicitly parent/child based. Include an eligible direct child
    # even if a dynasty edge case means that child was not visited by every_dynasty_member. Same-dynasty
    # children were already added above, so this pass only fills the missing non-dynasty edge cases.
    every_child = {
        limit = {
            is_alive = yes
            is_married = no
            is_betrothed = no
            is_concubine = no
            can_marry_trigger = yes
            ddm_match_subject_player_arrangeable_trigger = yes
            NOT = { dynasty ?= scope:ddm_match_actor.dynasty }
        }
        save_temporary_scope_as = ddm_match_subject
        scope:ddm_match_actor = {
            add_to_variable_list = { name = ddm_match_subjects_raw target = scope:ddm_match_subject }
            change_variable = { name = ddm_match_eligible_count add = 1 }
        }
    }

    # Dominion family-control extension. Scan only the existing Owned registry, not the whole world.
    # This adds eligible children of Owned subjects to the planner even when they are not in the player's dynasty.
    if = {
        limit = { has_variable_list = domcom_owned_registry }
        every_in_list = {
            variable = domcom_owned_registry
            limit = {
                is_alive = yes
                has_character_flag = domcom_owned_cuck
                has_variable = domcom_patron
                var:domcom_patron = scope:ddm_match_actor
            }
            every_child = {
                limit = {
                    is_alive = yes
                    is_married = no
                    is_betrothed = no
                    is_concubine = no
                    can_marry_trigger = yes
                }
                save_temporary_scope_as = ddm_match_subject
                scope:ddm_match_actor = {
                    if = {
                        limit = { NOT = { any_in_list = { variable = ddm_match_subjects_raw this = scope:ddm_match_subject } } }
                        add_to_variable_list = { name = ddm_match_subjects_raw target = scope:ddm_match_subject }
                        change_variable = { name = ddm_match_eligible_count add = 1 }
                    }
                }
            }
        }
    }

    ddm_sort_match_subjects_effect = yes
    ddm_build_matchmaker_page_effect = yes
    if = { limit = { var:ddm_match_view_mode = 1 } ddm_build_cached_match_pair_board_effect = yes }
}

ddm_matchmaker_page_next_effect = { change_variable = { name = ddm_match_page add = 1 } ddm_build_matchmaker_page_effect = yes if = { limit = { var:ddm_match_view_mode = 1 } ddm_build_cached_match_pair_board_effect = yes } }
ddm_matchmaker_page_prev_effect = { change_variable = { name = ddm_match_page subtract = 1 } if = { limit = { var:ddm_match_page < 0 } set_variable = { name = ddm_match_page value = 0 } } ddm_build_matchmaker_page_effect = yes if = { limit = { var:ddm_match_view_mode = 1 } ddm_build_cached_match_pair_board_effect = yes } }
ddm_matchmaker_page_first_effect = { set_variable = { name = ddm_match_page value = 0 } ddm_build_matchmaker_page_effect = yes if = { limit = { var:ddm_match_view_mode = 1 } ddm_build_cached_match_pair_board_effect = yes } }

