OSP Code Campaign Obedient Lords

Users who are viewing this thread

kt0

Knight
One of the things I always hated in Native is that I could never get friendly lord parties to follow me to war for any appreciable length of time.  Oh, sure, they'd follow me for a few paces, but every time an unfriendly party came into view, they'd peel off and leave me on my own.  This made it difficult to assault hardened positions, especially in mods that had increased troop numbers and strength. 

After a good three unfruitful months of work on Conquest hacking around with lord AI, I finally cracked it.  In an epic return of BLAM, I'm now releasing this code in the hopes that others can make use of it too.  Credit goes to froggyluv who convinced me to take another look at the problem after I'd more or less given up on it.  The results are pretty dramatic and a functioning proof of concept can be found in Conquest v0.70.  If you use this code, please give credit to kt0 and froggyluv.

Caveats:
- This is not a simple "drop it in and it goes" solution.  I assume that you know your way around the module system.  You will need to handle the dialog hookups yourself--the ones I used in Conquest are too heavily modified to drop into another mod.
- The code presented is modified from working source and is offered AS IS.  There are almost certainly bugs.  You will need to resolve these yourself. 
- This code comes with a nonzero amount of danger.  This will be explained more below.
- I am no longer actively modding M&B.  I will not be supporting this code.  Sorry.

Algorithm
The basic idea is fairly straightforward.  We use the script game_check_party_sees_party to adjust which parties a lord party can see and let the engine take care of the rest.  By modifying the potentially viewable set (PVS) we can get lord parties to behave better.  The script game_check_party_sees_party is commented out by default in module_scripts.py and seems to default to a simple distance check in Native.  This script gets called twice for every distinct pair of parties that are within some distance.  I don't recall what the actual distance is, but it is called very frequently in Native. 

In Conquest, I use two values that I use to determine what a lord party can see.  The first is sight distance which is how far the party can see without other considerations.  The second is the tether distance which is how far a party can wander before they stop being able to see other parties.  When I give a lord party an assignment, I give them both a sight distance and a tether distance. 

Parties outside the sight distance are invisible to the given party as one might expect.  Parties following the player with a very short sight distance will follow obediently because their PVS remains largely empty.  The same party outside their tether distance gets blinders which will effectively empty their PVS and return them to their assignments.  Varying these two values gives us a fairly wide dynamic range of behavior based solely on what enemy parties are within the lord's PVS without modifying the underlying AI.  The code below is a version 1 and more of a proof of concept than anything else.  There are lots of ways you can modify this approach to produce a variety of interesting behaviors.

Ways This Can Break
There are a variety of ways that this can be abused in a way that breaks your mod.  These are not limited to being:
- very, very slow.  The sight script is called very frequently on a per-frame basis.  If you put too much code in here, it will adversely affect your mod's performance.  This is probably why it's commented out and done engine-side in Native.
- error prone.  If you're not careful about which parties can see what, you can pretty easily get into situations where your AI will look particularly bad.  An example I hit during testing is where a strong enemy party trivially intercepts a faster, smaller friendly party and defeats them where the smaller party would have normally avoided a battle. 
- difficult to debug.  Because of the frequency of the calls, it can be difficult to track down why a given party is not behaving the way you want it to.  Caveat emptor.

These are the constant definitions I added in module_constants.py.  Not all of these are used in the supplied code, but they should give a good idea of where I was going with it:
#
# sight and tether distances
#
kt_bandit_normal_sight_dist = 8 # normal bandits like sea raiders
kt_bandit_short_sight_dist = 5 # short sighted bandits like looters
kt_bandit_long_sight_dist = 12 # everybody else
kt_lord_follow_close_sight_dist = 2 # pretty close
kt_lord_follow_close_tether_dist = 8 # within visual distance
kt_lord_skirmish_sight_dist = 12 # same as normal
kt_lord_skirmish_tether_dist = 50 # pretty goshdarned far

# kt0:  new AI slots for lords here
kt_slot_troop_sight_dist = 175
kt_slot_troop_tether_dist = 176

The sight script, slightly modified for easier hacking.  Place in module_scripts.py.
# This script is called from the game engine when a party is inside the range of another party
# INPUT: arg1 = party_no_seer, arg2 = party_no_seen
# OUTPUT: trigger_result = true or false (1 = true, 0 = false)
# credit:  kt0 & froggyluv, Conquest
("game_check_party_sees_party",
[
(store_script_param, ":party_no_seer", 1),
(store_script_param, ":party_no_seen", 2),
(assign, ":result", 1),
(set_fixed_point_multiplier, 100),
(try_begin),
# hero parties hackery here
# basically we get two things from party slots:  their sight range
# and their tether range.  if they're further out than the tether
# range, they lose the ability to see parties smaller than themselves.
# all other parties are seen normally (they won't willingly run into
# danger).  sight range is how we bully lord parties into following
# us; outside of some distance they just don't see anyone else.
#
# we use these two values to make the lord parties do what we want.
# a short tether distance ensures that even if they do peel off to
# chase something, that they come back within some bounds.  a short
# sight distance makes them not peel off at all (which is super nice).
(party_slot_eq, ":party_no_seer", slot_party_type, spt_kingdom_hero_party), # hero parties only
(party_slot_eq, ":party_no_seer", slot_party_commander_party, "p_main_party"), # following the player's party
(try_begin),
(neq, ":party_no_seen", "p_main_party"), # seeing anyone but the player
(store_distance_to_party_from_party, reg10, ":party_no_seer", "p_main_party"),
(store_distance_to_party_from_party, reg11, ":party_no_seer", ":party_no_seen"),
(party_get_slot, ":sight_distance", ":party_no_seer", kt_slot_troop_sight_dist),
(party_get_slot, ":tether_distance", ":party_no_seer", kt_slot_troop_tether_dist),
# sanity checks--make sure that things are in bounds
(try_begin),
(lt, ":sight_distance", 1),
(assign, ":sight_distance", kt_lord_follow_close_sight_dist),
(try_end),
(try_begin),
(lt, ":tether_distance", 1),
(assign, ":tether_distance", kt_lord_follow_close_tether_dist),
(try_end),

(try_begin),
# if the target is out of sight distance, no dice no matter what
(gt, reg11, ":sight_distance"),
(assign, ":result", 0),
#(str_store_party_name, s10, ":party_no_seer"),
#(str_store_party_name, s11, ":party_no_seen"),
#(display_message, "@DEBUGHAX: {s10} can't see {s11} at range {reg11}", 0xFFFFFF00),
(else_try),
# if we're outside of tether distance, do some trickery
(gt, reg10, ":tether_distance"),
# calc strengths
(call_script, "script_party_calculate_strength", ":party_no_seer", 0),
(assign, ":seer_strength", reg0),
(call_script, "script_party_calculate_strength", ":party_no_seen", 0),
(assign, ":seen_strength", reg0),
# ignore weaker parties
(lt, ":seen_strength", ":seer_strength"),
(assign, ":result", 0),
#(str_store_party_name, s10, ":party_no_seer"),
#(str_store_party_name, s11, ":party_no_seen"),
#(display_message, "@DEBUG: {s10} can't see {s11} at range {reg11} due to tether", 0xFFFFFF00),
(try_end),

# the default is "can see"
(try_end),
(else_try),
(neq, ":party_no_seer", "p_main_party"),
# sight distances based on party templates
(assign, ":sight_dist", kt_bandit_normal_sight_dist),
(party_get_template_id, ":pt", ":party_no_seer"),
(try_begin),
# short sighted parties
(this_or_next|eq, ":pt", "pt_looters"),
(            eq, ":pt", "pt_village_farmers"),
(assign, ":sight_dist", kt_bandit_short_sight_dist),
(else_try),
# far sighted parties
(this_or_next|eq, ":pt", "pt_scorchers"),
(            eq, ":pt", "pt_kingdom_hero_party"),
(assign, ":sight_dist", kt_bandit_long_sight_dist),
(try_end),

(store_distance_to_party_from_party, reg11, ":party_no_seer", ":party_no_seen"),
(gt, reg11, ":sight_dist"),
#(str_store_party_name, s10, ":party_no_seer"),
#(str_store_party_name, s11, ":party_no_seen"),
#(display_message, "@DEBUGHAX:  {s10} trying to see {s11} at range {reg11}", 0xFFFFFF00),
(assign, ":result", 0),
(try_end),

(set_trigger_result, ":result"),
]),

A snippet from the dialogs I used to set stuff up from module_dialogs.py.  You will need to set/clear the tether and sight distance slots every time that you give a lord party an order.
  [anyone,"lord_ask_follow", [],
  "Lead the way, {playername}! Let us bring death and defeat to all our enemies.", "close_window",
  [(party_set_slot, "$g_talk_troop_party", slot_party_commander_party, "p_main_party"),
      (party_set_slot, "$g_talk_troop_party", kt_slot_troop_sight_dist, kt_lord_follow_close_sight_dist),
      (party_set_slot, "$g_talk_troop_party", kt_slot_troop_tether_dist, kt_lord_follow_close_tether_dist),
    (call_script, "script_party_decide_next_ai_state_under_command", "$g_talk_troop_party"),
    (store_current_hours, ":follow_until_time"),
    (store_add, ":follow_period", 30, "$g_talk_troop_relation"),
    (val_div, ":follow_period", 2),
    (val_add, ":follow_until_time", ":follow_period"),
    (party_set_slot, "$g_encountered_party", slot_party_follow_player_until_time, ":follow_until_time"),
    (party_set_slot, "$g_encountered_party", slot_party_following_player, 1),
    (assign, "$g_leave_encounter",1)]],

Enjoy!
 

Bunduqdari

Sergeant Knight
wow, I have no idea how you achieved this but I'm looking forward to see this incorporated into awesome mods.
Definitely a BLAM
 

dunde

Count
WB
a long waited blam.. it's great to see you around, kt0. Thanks you, this is just what I need for my mod
 

Twan

Sergeant Knight
Thanks a lot.

I've used the script party see party (without the tether)  to make lords pursuit small parties less often in SoD (as a last minute replacement for another anti-pursuit system involving despawning/respawning parties to reset their AI, a far more brutal method). Seem to work well so far (before the mod is released and everyone send bug reports about the added lag :smile: ).

Here's a version without specific SoD things :

  ("game_check_party_sees_party",
    [
      (store_script_param, ":party_no_seer", 1),
      (store_script_param, ":party_no_seen", 2),
      (store_distance_to_party_from_party, ":dist", ":party_no_seer", ":party_no_seen"),
 
      (party_get_num_companions, ":num_comp_seer"),  # as the smaller is a party the faster it is usually, it helps know which parties      (party_get_num_companions, ":num_comp_seen"),  # another has chances to catch (forgoten to count prisoners here but they may matter too
      (store_faction_of_party, ":seer_faction", ":party_no_seer"),
 
  (try_begin),
  (this_or_next|party_slot_ge, ":party_no_seer", slot_party_commander_party, 1),
  (party_slot_eq, ":party_no_seer", slot_party_following_player, 1),
  (this_or_next|party_slot_eq, ":party_no_seer", slot_party_ai_state, spai_besieging_center),
  (party_slot_eq, ":party_no_seer", slot_party_ai_state, spai_accompanying_army),
  (assign, ":commanded", 1),
  (else_try),
  (assign, ":commanded", 0),
  (try_end),
 
  (try_begin),
(neg|is_between, ":seer_faction", kingdoms_begin, kingdoms_end), # all neutral parties have a medium seing range
(assign, ":seing_range", 10),
                  (else_try),
(party_slot_eq, ":party_no_seer", slot_party_commander_party, ":party_no_seen"),
(gt, ":num_comp_seen", 50),
(assign, ":seing_range", 50),      # make commanded parties see the commander from very far away 
(assign, ":commanded", 0),       
(else_try),
(this_or_next|party_slot_eq, ":party_no_seen", slot_party_type, spt_kingdom_hero_party), # use a little more complex system when a kingdom hero party see another kingdom hero party
                (eq, ":party_no_seen", "p_main_party"),
                (assign, ":seeing_range", ":num_comp_seen"),    # ex : a lord with 40 men sees a lord with 20 men at range 5 ; a lord with 50 men see a lord with 60 at range 24 ; no need to see them if you can't catch them
(val_mul, ":seeing_range", 20),
(val_div, ":seeing_range", ":num_comp_seer"),  # should make parties abandon pursuit of faster parties
(val_clamp, ":seeing_range", 5, 25),
                (else_try),
(party_slot_eq, ":party_no_seen", slot_party_type, spt_kingdom_caravan), # caravans are slow unless very small
(gt, ":num_comp_seen", 25),                                              # they are seen at a rather big range
(assign, ":seeing_range", 10),
(else_try),
  (this_or_next|gt, ":num_comp_seen", 60),          # big neutral parties are seen from as far as caravans
  (gt, ":num_comp_seen", ":num_comp_seer"),
  (assign, ":seeing_range", 10),
                  (else_try),
(gt, ":num_comp_seen", 30),    # medium neutral parties are seen at reasonable range only if the lord has a chance to catch them
(lt, ":num_comp_seer", 50),
(assign, ":seeing_range", :cool:,
  (else_try),
(gt, ":num_comp_seen", 10),    # rather small neutral parties are seen at reasonable but small distance if the lord has a chance to catch them
                (lt, ":num_comp_seer", 20),
(assign, ":seing_range", 6),
(else_try),       
(assign, ":seeing_range", 4),  # in all other cases neutral parties are only seen at extremely close range  
        (try_end),

(try_begin),                      # help marshalls and player commanding lords
(eq, ":commanded", 1),  # divide by 2 the seing range if the party is besieging or following orders and see a smaller party
(gt, ":num_comp_seer", ":num_comp_seen"),  # but the party will never be completely blind
(val_div, ":seing_range", 2),
(try_end),

      (try_begin),
    (ge, ":seeing_range", ":dist"),
    (set_trigger_result, 1),
    (else_try),
    (set_trigger_result, 0),
    (try_end),
    ]),
 
Top Bottom