Some clarification about troop classes?

Users who are viewing this thread

Oldschool

Sergeant at Arms
I'm not really sure where to ask about this, so I'll try here and see if I can get some discussion from those with experience.

I like the new grouping commands from the party menu.  It's nice for being able to divide your units how you want and use them that way.  I have quite a few questions related to that, though.  I would like to be able to manipulate these groups through scripting for purposes of some AI skirmishing/horse archery tactics.

1) I can't find the party menu anywhere in Module_presentations or module_game_menus.  Have I overlooked it, it's in a different place, or it just isn't available to modders?

2) When you change a units group, does it actually change its class, as accessed from agent_get_class, or does it only change its group as accessed from the battlefield commands?  I'm assuming that the actual class of a unit is hardcoded, and the grouping is only available to players to be used in an actual battle.  i.e...If a unit is considerd to be in the archer class, agent_get_class will return grc_archers, regardless of what group you have manually put him in.

3)  If you are marshall and are in control of allied armies, do your allies units honor your grouping selections, or are they treated as their default class, regardless?  i.e...if you have set your horse archers to be group 4, will your allies horse archers be set to that group when in battle under your command?

4)  The only classes I've seen defined are grc_infantry, grc_archers, grc_cavalry, grc_heroes, and grc_everyone.  Can you access other classes by using hard numbers.  i.e...checking agent_get_class for a return of 6 or 8,etc to return a user-defined group? 

I guess my basic question is, what is the relationship between the player-defined groupings and the actual class of a unit for scripting purposes.  Also, I can not find any way to change an agents class through scripting operations, if anybody knows of such a method.

I'll do some testing and tinkering and try to find these answers myself, and I don't expect anyone to do my research for me.  However, if you have some experience with this and want to share your insight, any input would be greatly appreciated.

 
1.  I couldn't find any way to modify the classes at all.  I've tried;  I wanted to write a horse-cav tactical sub-AI, but didn't have any luck finding a way to assign Khergit horse archers, etc. to new "classes". 

2.  The only orders you can pass go to grc_ groups via mordr_ commands, or by setting the Agent to scripted mode.  Scripted mode is a pain in the rear, because it basically turns off all combat functionality entirely- they're like zombies, and can only be given movement targets.

3.  Changing a unit's group has no effect on Ally armies, so far as I could tell.

4.  If you find any workarounds, I'd love to hear about it, because I wasn't entirely happy with what I ended up doing as hack-arounds to make my Tactical AI work better.
 
As far as I can tell, the classes are hardcoded, as defined by the various tf_tags. tf_guarantee_ranged will place you in group 2 (archers), while tf_guarantee_horse or tf_mounted, probably the former, places you in group 3 (cavalry).
[list type=decimal]
[*]The party menu is a hardcoded presentation. You can only modify the components, i.e. various display elements via tableau and scripts.
[*]The grc_class are hard-coded, and they were exclusively used for command functions before Warband. Since agent_get_class was in original M&B, there's no reason to assume they've modified it for Warband purposes. Test it out and display a message whenever you select a group.
[*]I've never tried this out myself. Again, try outputting debug messages when you select them.
[*]You probably can't, since grc_hero isn't defined anywhere within the new system, so there's no reason to assume they'd be accessible in this case. I have a feeling that the new system isn't all that compatible with the old one, with the exception of the first 3 default groups which are the same.
[/list]
Anyways, the only place besides presentations where I've seen this grouping system in effect is in the UI: The old ones are here
Code:
ui_infantry|Infantry
ui_archers|Archers
ui_cavalry|Cavalry
ui_companions|Companions
ui_everyone_hear_me|Everyone, hear me!
ui_everyone|Everyone
And the new ones are
Code:
ui_group_header|Class of troop
ui_group_rename|Rename group
ui_group_1|Infantry
ui_group_2|Archers
ui_group_3|Cavalry
ui_group_4|Unnamed 1
ui_group_5|Unnamed 2
ui_group_6|Unnamed 3
ui_group_7|Unnamed 4
ui_group_8|Unnamed 5
ui_group_9|Unnamed 6
ui_group_rename|Rename Group
ui_group_close|Close
ui_party_b_group_information|%s belongs to %s group
 
I've actually looked a bit more into this - it seems that agent_get_class will only return grc_xxx, as far as I've seen it being used, while agent_get_division (a new Warband operation) and troop_get_class will also get the new groupings, as seen in scripts. In addition, the team_get/set operations are also compatible with the groups - search for sub_class in header_operations.

The grouping probably applies on a per-troop-in-stack basis - since they are set within the party screen - and as seen here in presentations.
Code:
  (assign, "$group#_has_troops", 0),
        (party_get_num_companion_stacks, ":num_stacks", "p_main_party"),
        (assign, "$num_classes", 0),
        (try_for_range, ":troop_iterator", 0, ":num_stacks"),
          (party_stack_get_troop_id, ":cur_troop_id", "p_main_party", ":troop_iterator"),
          (troop_get_class, ":troop_class", ":cur_troop_id"),
          (neq, ":player_troop_id", ":cur_troop_id"),
          (try_begin),
            (eq, ":troop_class", 0),
            (try_begin),
              (neq, "$group#_has_troops", 1),
              (val_add, "$num_classes", 1),
            (try_end),
            (assign, "$group#_has_troops", 1),
          (else_try),
This means that theoretically your choice of grouping does affect all troops globally, but the AI for allied and enemy troops might use only agent_get_class which obeys only the default groupings for simplicity, ignoring your orders for groups for which their troops are not configured for (so that you can only use grc_xxx), since it will interfere with their internal AI behaviour.
 
Thanks for the input guys.  Here are a few more things I've found digging around.

There are some constants declared in header_common for multiplayer classes, including horse archers:

Code:
multi_troop_class_other = 0
multi_troop_class_infantry = 1
multi_troop_class_spearman = 2
multi_troop_class_cavalry = 3
multi_troop_class_archer = 4
multi_troop_class_crossbowman = 5
multi_troop_class_mounted_archer = 6
multi_troop_class_mounted_crossbowman = 7

The way they check for them is by checking for a specific unit type like so, from the script "multiplayer_get_troop_class"

Code:
     (else_try),
       (eq, ":troop_no", "trp_khergit_veteran_horse_archer_multiplayer"),
       (assign, ":troop_class", multi_troop_class_mounted_archer),

This is just used for equipment options in a later script and I don't see it being set permanently or being used in any AI scripts, so probably not very useful to us.

Like Somebody said, the troop_get_class operator does seem to use the groupings rather than the hardwired classes, which is good.  Also, looking through the presentations, operators such as team_give_order, team_set_order_position, etc. appear to work with groups as well.

Code:
         (try_begin),
            (eq, "$g_formation_group5_selected", 1),
            (team_give_order, ":player_team", 5, mordr_hold),
            (team_set_order_position, ":player_team", 5, pos3),
          (try_end),

The main tactical scripts for field battles appear to be "battle_tactic_init" and "battle_tactic_apply" along with their aux scripts.  These do make use of the gfc_classes, but, they are using the same team_give_order operator, so presumably, if we can find a way to change a units group through scripting, then we can rework those scripts to include custom classes for the AI.

Code:
  ("battle_tactic_init_aux",
    [
      (store_script_param, ":team_no", 1),
      (store_script_param, ":battle_tactic", 2),
      (team_get_leader, ":ai_leader", ":team_no"),
      (try_begin),
        (eq, ":battle_tactic", btactic_hold),
        (agent_get_position, pos1, ":ai_leader"),
        (call_script, "script_find_high_ground_around_pos1", ":team_no", 30),
        (copy_position, pos1, pos52),
        (call_script, "script_find_high_ground_around_pos1", ":team_no", 30), # call again just in case we are not at peak point.
        (copy_position, pos1, pos52),
        (call_script, "script_find_high_ground_around_pos1", ":team_no", 30), # call again just in case we are not at peak point.
        (team_give_order, ":team_no", grc_everyone, mordr_hold),
        (team_set_order_position, ":team_no", grc_everyone, pos52),
        (team_give_order, ":team_no", grc_archers, mordr_advance),
        (team_give_order, ":team_no", grc_archers, mordr_advance),
      (else_try),
        (eq, ":battle_tactic", btactic_follow_leader),
        (team_get_leader, ":ai_leader", ":team_no"),
        (ge, ":ai_leader", 0),
        (agent_set_speed_limit, ":ai_leader", 8),
        (agent_get_position, pos60, ":ai_leader"),
        (team_give_order, ":team_no", grc_everyone, mordr_hold),
        (team_set_order_position, ":team_no", grc_everyone, pos60),
      (try_end),
  ]),

The only operator I've seen that looks promising is agent_set_group.  I don't see any more set operators that seem applicable, and it's not clear to me if it will set a group that can then be returned by troop_get_class or used by team_give_order.  So, I'll do a test and cycle through the agents during the "battle_tactic_init" script and set horse archers to group 6, then I'll set some obvious commands for group 6 in the "battle_tactic_init_aux" script and see if they follow the new scripted orders on the battlefield.

I'm not the best scripter, so I'll post what I try here and whatever results I get.  I guess a better test would be to cycle through a party and use agent_set_group on the horse archers and then cycle through it again using troop_get_class to print the results to a debug message or a log.  I may try that first if I can work out an easy way to set it up with this python mess.  Did I mention I'm not the best scripter?  :oops:
 
...and here are the results.

Here is my testing code, added to battle_tactic_init:

Code:
  # script_battle_tactic_init
  # Input: none
  # Output: none
  ("battle_tactic_init",
    [
      (call_script, "script_battle_tactic_init_aux", "$ai_team_1", "$ai_team_1_battle_tactic"),
      (try_begin),
        (ge, "$ai_team_2", 0),
        (call_script, "script_battle_tactic_init_aux", "$ai_team_2", "$ai_team_2_battle_tactic"),
      (try_end),

    #grouptest
      (try_for_agents, ":cur_agent"),
         (agent_get_troop_id, ":troop_id", ":cur_agent"),
          (eq, ":troop_id", 64), #khergit skirmisher
          (agent_set_group, ":cur_agent", 6),
      (try_end),    
      (try_for_agents, ":cur_agent"),
          (agent_get_troop_id, ":troop_id", ":cur_agent"),
          (troop_get_class, ":troop_class", ":troop_id"),
          (try_begin),
              (eq, ":troop_class", 6),
              (str_store_class_name, s1, ":troop_class"),
          (display_message, "@Skirmisher found, troop class is: {s1}"),
          (try_end),
      (try_end),
    #end grouptest

      (try_for_agents, ":cur_agent"),
        (agent_set_slot, ":cur_agent",  slot_agent_is_running_away, 0), #initially nobody is running away.
      (try_end),
  ]),

The agent_set_group didn't work for setting the class.  My test unit was the khergit skirmisher.  I got no messages when I attacked a khergit army with skirmishers in it.

However, I went and recruited some khergits, trained them up to skirmishers, and then set them to group 6 in the party screen.  That worked well.  The party screen does change the class globally.  When I attacked a khergit army with them, I got 22 messages.  19 for his skirmishers and 3 for mine.  The message was : "Skirmisher found, troop class is: unnamed group 4"

My skirmishers ignored my commands completely, even when I selected "all".  I think agent_set_group must set "teams" rather than classes.  They were not following my orders and must be why they showed up in the message list, since that is strictly an AI script, as far as I know.  That part of the code should be removed if you still want to command them and test this out.  :smile:

One note, if you want to test this, groups start counting at 0 in the scripts, so to set your units to group 6, you need to set them to the 7th group in the party screen list, unamed group 4.*

The bottom line is, I didn't find a way to set their group by scripting, but you can set their group manually in the party screen and then order them through scripts after they have been set that way.  At least that's the way it appears from this test.

For a tactical mod then, I guess an entry area where you are given a troop of each type, select their group in the party screen, and then have the troops removed and start the module proper would work, assuming we won't be given a script command to set a troop or agents class in a future patch.

Tomorrow, I'll try putting in some AI orders for group 6 and see what comes of it. 

*Just for clarification:
party screen label = constant = value = keyboard number

infantry = grc_infantry = 0 (keyboard 1 selects them in the battle screen)
archers = grc_archers = 1 (kb2)
cavalry = grc_cavalry = 2 (kb3)
unnamed group 1 = grc_heroes = 3 (kb4) ( a legacy constant, I assume)
unnamed group 2 =undefined constant = 4 (kb5)
etc
all = grc_everyone = 9 (kb0)
 
So, basically, we could make this work, if we were allowed to change the base class?

Perhaps then we just need to look at tf_guarantee_ranged, etc., because that's how the class value is set.  Then troops might get set to the correct values from the start.  OFC, they'd still be put into "Un-named Group" because that CSV isn't in the /data directory, but that's not the end of the world.
 
Language folders were supported way before data folders in Warband, so just plug and play into your module folder.
Anyways, try using agent_get_division instead of troop_get_class to see if it gets you the same result.
Another way I thought of would be to preprocess the troops, and assign a slot to each troop in python. You then fetch that slot and then apply specific tactics to them, by using agent_set_group or agent_get_team. But then like you said, they still wouldn't obey the player's orders for that team, but theoretically setting team relations to 1 and team_set_leader to player should work. You can then use class_is_listening_order to get the order for the player's subgroup (although none of them will be displayed in the presentation, but we could probably work around that by using our slots instead of troop_get_class), and then use team_get_xxx_order for the subgroup and then apply it to your new group via team_give_order. And in case the player switches the troop's class in the party menu, we can use a trigger to refresh the slots via troop_get_class and comparing it to our slot value.
 
Hrmm.

Just looking over  the output for this, it apparently assigns the slot via the guarantee value of 0,2,4 for that value.  Stands to reason that if tf_guarantee_horse overrides tf_guarantee_ranged (which it does) then it may in fact be possible to write new categories by altering the header, which would auto-put troops into certain categories.  This could probably be confirmed by doing some savegame spelunking, since the hex value of the troop group assignment should be seen to shift if you change it.

Brings me right back to my Module System request that we be allowed to flat-out declare this stuff, tho.

Does anybody know what troop_type_mask does?  I can't find any reference to it other than the header.
 
xenoargh said:
So, basically, we could make this work, if we were allowed to change the base class?

Perhaps then we just need to look at tf_guarantee_ranged, etc., because that's how the class value is set.  Then troops might get set to the correct values from the start.  OFC, they'd still be put into "Un-named Group" because that CSV isn't in the /data directory, but that's not the end of the world.

Being able to set the class via script would be great.  Although, now that I've looked at it more, it would have to be in a way independent of the players groupings to be able to use it consistently.  I'm not sure how we could manipulate the flags to get the classes we want.  Unless we had more flags, for spears, skirmishers, two flags for horse archers, etc.  The way it looks now, even if we could manipulate the flags, the player can still overwrite the classes from the party menu anyway.


Somebody said:
Language folders were supported way before data folders in Warband, so just plug and play into your module folder.
Anyways, try using agent_get_division instead of troop_get_class to see if it gets you the same result.
Another way I thought of would be to preprocess the troops, and assign a slot to each troop in python. You then fetch that slot and then apply specific tactics to them, by using agent_set_group or agent_get_team. But then like you said, they still wouldn't obey the player's orders for that team, but theoretically setting team relations to 1 and team_set_leader to player should work. You can then use class_is_listening_order to get the order for the player's subgroup (although none of them will be displayed in the presentation, but we could probably work around that by using our slots instead of troop_get_class), and then use team_get_xxx_order for the subgroup and then apply it to your new group via team_give_order. And in case the player switches the troop's class in the party menu, we can use a trigger to refresh the slots via troop_get_class and comparing it to our slot value.

That, I think, leads us to the solution, pending some testing. 

Looking at the mission templates, the first thing an agent does when it spawns is go through the agent_reassign_team script, which assigns him a team depending on if he is under the player's control, an ally, or an enemy.  We could use that script to pull out any special classes we want and assign them to new teams (one for enemies and one for allies), then put those new teams under the allied leader and the enemy leader control.  Any units already under the player control can be ignored.  Then the tactical scripts would have to be re-written to include this secondary team for each AI leader. 

You could, for example, pull out spearmen, skirmishers, and horse archers; assign them to the secondary team and issue AI  orders to them as infantry, archers, and cavalry.  That way, the AI can operate independent of the player's groupings. 

You would have to keep track of the player's groupings and accomodate any changes so that you could be sure you were issuing orders to the right classes.  During the team assignment script, you could just get the current class of the units and send that to the tactical scripts to have orders issued to, instead of using constants like grc_archers.

In this case, it would be better if the player's groupings weren't global.  For example, he may have infantry, spearmen, and skirmishers all in the infantry group.  If you want your AI to handle spearmen and skirmishers as seperate classes, then you would have to assign two new teams for each AI side to work around his groupings.  The setting teams script will have to be pretty flexible.

Another possibility might be to just assign special classes their own team from the start and then issue orders to that team using grc_everyone.  In fact, that's probably the better solution.  That way it will be consistent and won't require a bunch of checks and adjustments.

The agent_get_divison does seem to return the new groupings, so that will save us having to return classes using the troop id.  I haven't tested it yet, but that's how it seems looking at how it is used in the scripts.

I think the only reason global player groupings haven't broken the game so far is because the vanilla AI is so basic.  It only ever issues orders to archers and everyone.  It can pretty well be summed up as:

CHARGE!, or

Hold then CHARGE!, or

Follow me then CHARGE!, or

if all else fails...

CHARGE!

xenoargh said:
Hrmm.

Just looking over  the output for this, it apparently assigns the slot via the guarantee value of 0,2,4 for that value.  Stands to reason that if tf_guarantee_horse overrides tf_guarantee_ranged (which it does) then it may in fact be possible to write new categories by altering the header, which would auto-put troops into certain categories.  This could probably be confirmed by doing some savegame spelunking, since the hex value of the troop group assignment should be seen to shift if you change it.

Brings me right back to my Module System request that we be allowed to flat-out declare this stuff, tho.

Does anybody know what troop_type_mask does?  I can't find any reference to it other than the header.

That's in my module system request too.  Another one I have now is the ability to set simple behaviours.  I'm curious what effect aisb_ranged_horseback and aisb_maneuver_horseback have on a unit.  I'm not sure what troop_type_mask does.  I think troop type has to to do with gender although I wouldn't swear to it.

Anyway, didn't intend for this to become such a long friggin' post.  The team idea looks promising and I'm off to experiment. 
 
You can make the tactical AI do a lot more than just CHARGE or SIT THERE.  Meh, I wish I had time to talk more about this, but I've got to go to bed early because I have to get up at an insanely early hour tomorrow, and then I'm going to be gone for a week.  If you want to see what I've done, the sourcecode is available in the release package of Blood and Steel. 

The AI modifications I've built thus far do a few new things, like:

1.  If mainly archers and some infantry, find a hill and defend it.
2.  If > some amount cav, then consider making the cav charge whilst everybody else finds a position to defend.
3.  Move around randomly and then defend (you'd be surprised how often that can hose players' well-laid plans).
4.  Other stuff, like layering the troops so that infantry are in front, archers behind, etc., etc.

Not that it's anything like as perfect as I'd like, but it's a lot better than the default (imo).
 
Using troop_set/get_type will do some random things - mainly because it's the same one used to define gender, and would probably screw around with skins, armor transformation meshes, and dialogs, to name a few. You should be able to assign new tf_flags up to 16 (the mask), and the game will be able to read it properly via troop_get_type. Again, the problem is that it's the same one used by gender. But since there are only 2, excluding undead, we should be able to assign classes easily within these limits (i.e. duplicate each class for each gender), and add a modulus operator whenever troop_get_type is used to get gender.
 
The test for the team method worked out pretty well.  Units can be assigned to new teams and then used in the tactical scripts as if they were classes.  I get an "invalid agent ID" message in battle, but it doesn't seem to affect anything.  Probably just sloppiness in my scripting (I hope).  It's just a rough test, and there may still be issues, but it does seem to work.

Looking at team assignments for field battles, the player will be either team 0 or team 1, the enemy will be 1 or 0, and the player's allies will be equal to player team +2.  I decided to use +2 for my new team assignments.  So enemy AI skirmishers will be enemy team +2, and allied skirmishers will be allied team +2.  For more classes then, I would use +4, +6, etc.  Anyway, the test:

This is my script for pulling out AI skirmishers and assigning them to teams.  Added to the bottom of module_scripts:

Code:
  #grouptest
  #script_agent_reassign_aux_team
  # INPUT: arg1 = agent_no
  # OUTPUT: none
  ("agent_reassign_aux_team",
    [
      (store_script_param, ":agent_no", 1),
      (get_player_agent_no, ":player_agent"),
      (try_begin),
        (ge, ":player_agent", 0),
        (agent_is_human, ":agent_no"),
        (agent_get_team, ":player_team", ":player_agent"),
        (agent_get_team, ":agent_team", ":agent_no"),  
        (neq, ":agent_team", ":player_team"),
        (agent_get_troop_id, ":agent_troop_id", ":agent_no"),
        (eq, ":agent_troop_id", 64), #khergit skirmisher
        (val_add, ":agent_team", 2),
        (agent_set_team, ":agent_no", ":agent_team"),
      (try_end),

To test whether they would recieve orders independently, I modified the battle_tactic_init_aux script like so:

Code:
  # script_battle_tactic_init_aux
  # Input: team_no, battle_tactic
  # Output: none
  ("battle_tactic_init_aux",
    [
      (store_script_param, ":team_no", 1),
      (store_script_param, ":battle_tactic", 2),
      (team_get_leader, ":ai_leader", ":team_no"),

      #grouptest
      (assign, ":aux_team", ":team_no"), 
      (val_add, ":aux_team", 2),
      (team_set_leader, ":aux_team", ":ai_leader"),
      (team_give_order, ":aux_team", grc_everyone, mordr_hold),
      (team_give_order, ":aux_team", grc_everyone, mordr_dismount),
      #grouptest

      (try_begin),etc.

That script returns the "invalid agent id" message.  The team_set_leader line doesn't seem to be neccessary.  They obeyed the orders either way.

Now, in the mision_templates, I added some lines to the "lead charge" mission.

Under the before mission start trigger, I added my new team relationships to what was there:

Code:
      (ti_before_mission_start, 0, 0, [],
       [
         (team_set_relation, 0, 2, 1),
         (team_set_relation, 1, 3, 1),

        #grouptest
         (team_set_relation, 0, 4, 1),
         (team_set_relation, 2, 4, 1),
         (team_set_relation, 1, 5, 1),
         (team_set_relation, 3, 5, 1),
         #grouptest end

And, under the agent spawn trigger, I added a call to the new team reassignment script underneath theirs.  Their script goes through and checks if the player is in control of allied forces and reassigns teams accordingly, so it should run first:

Code:
      (ti_on_agent_spawn, 0, 0, [],
       [
         (store_trigger_param_1, ":agent_no"),
         (call_script, "script_agent_reassign_team", ":agent_no"),

         (call_script, "script_agent_reassign_aux_team", ":agent_no"),#grouptest

That's pretty much it.  I went and picked a fight with a Khergit lord, rode around behind his army, and his skirmishers were dismounted and holding position.  My skirmishers were left alone by the script and remained under my control.  His skirmishers didn't respond to any more of his orders because I hadn't scripted any more in for them.  So, other than the error message, everything went as scripted.


 
Back
Top Bottom