PYTHON SCRIPT/SCHEME EXCHANGE

Users who are viewing this thread

Another small piece of code...
Make the starting merchant and his brother look like they are related.

This is how:
In module_dialogs.py find the dialogue that starts our first conversation with the merchant. Look for "start_up_quest_1_next"
This is how it looks in my module system:
Python:
    #Quest 0 - Alley talk
    [anyone|auto_proceed,"start",
        [
            (is_between,"$g_talk_troop","trp_swadian_merchant","trp_startup_merchants_end"),
            (eq,"$talk_context",tc_back_alley),
            (eq,"$talked_with_merchant",0),
        ],
        "{!}.",
        "start_up_quest_1_next",
        [
            #MOD BEGIN - starting merchant and his brother look like they are related
            #merchant's face is randomized, but each town has different merchant, and we can set his face to match the town's culture (so Swadian-looking guy in Praven, Sarranid-looking guy in Shariz)
            #merchant's younger brother however is just 1 NPC regardless of city, so we have to set his face in runtime
            (str_store_agent_face_keys,s59,"$g_talk_agent"), #get merchant's face code
            (face_keys_set_beard,s59,0), #remove beard from it, for younger look
            (face_keys_get_age,reg59,s59), #get age from face code
            (val_mul,reg59,3), #merchant's age is between 20-80% range of the slider, where min and max are roughly 20 years and 60 (upper limit is highly random, but on avg)
            (val_div,reg59,4), #reduce value by 25%, this should give us visibly younger face
            (face_keys_set_age,s59,reg59), #put newly calculated age into face code
            (troop_set_face_keys,"trp_relative_of_merchant",s59), #set the modified face code to merchant's brother
            #MOD END - starting merchant and his brother look like they are related
        ]
    ],
    [anyone,"start_up_quest_1_next",
        [],
        "Are you all right? Well... I guess you're alive, at any rate. I'm not sure that we can say the same for the other fellow. That's one less thief to trouble our streets at night, although Heaven knows he won't be the last... Anyway, maybe you can help me with something. Let's talk more inside. Out here, we don't know who's listening.", #MOD - typos
        "close_window",
        [
            (assign,"$talked_with_merchant",1),
            (mission_disable_talk),
        ]
    ],

The code above requires an operation that normally isn't declared in WB vanilla header_operations.py, but is supported by game engine, so we gotta declare it:
Python:
player_set_face_keys       = 2748 #(player_set_face_keys, <player_id>, <string_no>), #Sets <player_id>'s face keys from string.
#MOD BEGIN - starting merchant and his brother look like they are related
str_store_agent_face_keys  = 2749 #(str_store_agent_face_keys, <string_reg>, <agent_id>), #Stores agent face code. Regular agents have random generated face from two face codes of the troop.
#MOD END - starting merchant and his brother look like they are related
str_store_troop_face_keys  = 2750 #(str_store_troop_face_keys, <string_no>, <troop_no>, [<alt>]), #Stores <troop_no>'s face key into string. if [<alt>] is non-zero the second pair of face keys is stored
The operation is documented here: https://mbcommands.fandom.com/wiki/Operators

Now you can use the fact that every town has its own merchant and for example go to module_troops.py and set face codes for merchants so that their ethnicity matches the town.
I recommend Swyter's editor for that, as it allows you to paste in existing face codes and modify them:
https://swyter.github.io/mab-tools/face
 
Last edited:
Hello everyone.
Im currently working on a small Diplomacy mod with fun expansions to the battle system.
I will be sharing some aditions, mostly code taken from this forum and modified by me!
(ps: Im still working the forum stuff out as I always have being a forum ghost reader for 99.999% of my life :razz:)

Working Surgery skill for the enemy party:
Credits: @Erundil for the initial code "Quest Spies fall unconscious instead of dying"¹
¹ Already included in the code below.

Edit: Improved the code, made so Blunt weapons can kill about 25% of the time, and also made it so Surgery only gives 3% more chance to save from lethal. All these changes are commentated besides so you can decide to change them or not!
Edit²: Some corrections in internal header_operations used that I made "only" for my own mod dev... oops, sorry!



The following condition is to be added to [module_mission_templates.py].
Python:
#Code by RedMythos, no credits needed! ^^
#Thanks to everyone in the Warband modding community!
not_always_lethal = (ti_on_agent_killed_or_wounded, 0, 0, [],
       [
        (store_trigger_param_1, ":dead_agent_no"),
        (store_trigger_param_2, ":killer_agent_no"),
        (store_trigger_param_3, ":is_wounded"),
    
        #Note: setting the trigger_result as 0 will follow the default game logic and not alter the result. Setting it to 1 will force kill, and to 2 will force wound.
        (ge, ":dead_agent_no", 0),
        (agent_is_human, ":dead_agent_no"),
        (agent_get_troop_id, ":dead_agent_troop_id", ":dead_agent_no"),
        (neg|troop_is_hero, ":dead_agent_troop_id"),
        (party_add_members, "p_total_enemy_casualties", ":dead_agent_troop_id", 1), #addition_to_p_total_enemy_casualties.
    
           (try_begin), #The code below is credited to Erundil, and guarantees the enemies on the Spy quests to NOT die.
              (this_or_next|eq,":dead_agent_troop_id","trp_spy"), #the spy that we were following.
              (eq,":dead_agent_troop_id","trp_spy_partner"), #his boss that was waiting for him, usually accompanied by a few mercenaries.
              (assign, ":is_wounded", 1),
              (set_trigger_result, 2), #Set as wounded.
           (else_try),
              (agent_get_wielded_item, ":weapon", ":killer_agent_no", 0), #Checks for blunt weapons.
              (try_begin),
              (lt, ":weapon", 0),
              (assign, ":weapon", 0), #Prevents error "Item ID: -1" when hitting with your fists.
              (try_end),
              (item_get_swing_damage_type, ":is_blunt", ":weapon"), #Only checks if its blunt on swing damage, as not every mace has thrust damage.
              (eq, ":is_blunt", 2),
              (try_begin),
              (store_random_in_range, ":d100", 0, 101),
              (le, ":d100", 75), #~25% of chance to kill using blunt weapon.
              (assign, ":is_wounded", 1),
              (set_trigger_result, 2),
              (else_try),
              (assign, ":is_wounded", 0),
              (set_trigger_result, 1),
              (try_end),
           (else_try),
              (agent_get_team, ":dead_tm", ":dead_agent_no"),
              (team_get_leader, ":dead_lead", ":dead_tm"),
              (neg|agent_is_non_player, ":dead_lead"),
              (party_get_skill_level, ":ld_surgery", "p_main_party", "skl_surgery"), #Gets skill level from your party.
              (val_mul, ":ld_surgery", 3), #+3% survival rate from each point in Surgery.
              (val_add, ":ld_surgery", 25), #default base chance, but can be altered here!
              (try_begin),
              (store_random_in_range, ":d100", 0, 101),
              (le, ":d100", ":ld_surgery"),
              (assign, ":is_wounded", 1),
              (set_trigger_result, 2),
              (else_try),
              (assign, ":is_wounded", 0),
              (set_trigger_result, 1),
              (try_end),
           (else_try),
              (agent_get_team, ":dead_tm", ":dead_agent_no"),
              (team_get_leader, ":dead_lead", ":dead_tm"),
              (agent_is_non_player, ":dead_lead"),
              (agent_get_troop_id, ":dead_lead_id", ":dead_lead"),
              (store_skill_level, ":ld_surgery", "skl_surgery", ":dead_lead_id"), #Gets skill level directly from enemy party's leader, as lords dont have NPCs on party.
              (val_mul, ":ld_surgery", 3), #+3% survival rate from each point in Surgery.
              (val_add, ":ld_surgery", 25), #default base chance, but can be altered here!
              (try_begin),
              (store_random_in_range, ":d100", 0, 101),
              (le, ":d100", ":ld_surgery"),
              (assign, ":is_wounded", 1),
              (set_trigger_result, 2),
              (else_try),
              (assign, ":is_wounded", 0),
              (set_trigger_result, 1), #If all above fails, R.I.P.
              (try_end),
           (try_end),
          (try_begin), #Assigns wounded troops as prisoners in post-battle screen
          (eq, ":is_wounded", 1),
          (party_wound_members, "p_total_enemy_casualties", ":dead_agent_troop_id", 1),
        (try_end),
        (call_script, "script_apply_death_effect_on_courage_scores", ":dead_agent_no", ":killer_agent_no"),
       ])

(ps²: Im so sorry, the neat spacing in the code itself got lost in posting this[...])
(ps³: Code got properly formated into a |code| block, all thanks to @SupaNinjaMan's heads up on my PM, thank you! :party:)

Now, search on [module_mission_templates.py] for:
Python:
     common_battle_init_banner,


You will find multiple stances of it. Put the aforementioned condition below these original lines, like so:
Python:
      common_battle_init_banner,
      not_always_lethal,
Mind your spaces and align the first letter of the first line to the first letter of the second line in the code above!!

That should be all of the code needed, now enemy troops who suffer lethal cut/pierce damage will survive at least 25% of the time, just like it is for the player team. Lord and NPC's Surgery skill also take part in this matter, so I heavily suggest modifying some "knows" variables on your [module_troops.py] file. You can capture prisioners even without the need for blunt weapons, even if most of them die anyways... well, you better invest in some Prisoner Management now, just in case!! My next post will cover that, coming soon!

More code to come, I hope we all can make the Warband modding community keep thriving together as one.
Thanks for reading, cya!
-Red
 
Last edited:
[..] well, you better invest in some Prisoner Management now, just in case!! My next post will cover that, coming soon!
So here it is, in hope that someone find this useful, I present to you:

Prisoner Limit based on Party Size - Prisoner Management skill serves as % bonus instead and acts as (Party Skill):

To make Prsioner Management act as a Party Skill (like Pathfinding, Surgery, Trade etc.), you can use the following code:

Search your [module_skills.py] for the following:
Python:
("prisoner_management", "Prisoner Management",

Paste this over the entire "prisoner_management" line in your [module_skills.py]:
Python:
  ("prisoner_management", "Slavekeeping",sf_base_att_int|sf_effects_party,10,"Every level of this skill increases your maximum number of prisoners by 20%% relative to your party size. (Party skill)"),
(Mind your spaces! Also made it based on INT for obvious reasons... CHA never made any sense whatsoever for this purpose, but well...)
(Be aware that the skill name "Prisoner Management" is too big to be properly formated into the Party screen where it shows your skill bonuses. It has been renamed to "Slavekeeping" instead for this reason. This change is only esthetic and does not modify/break any code present in the game)

Now search your [module_scripts.py] for the following:

Python:
  ("game_get_party_prisoner_limit",
    [

Delete or comment out (using the # character at the beggining of every line) the whole script, until the last " ]) " line, and use this instead:
Python:
#Code by RedMythos, no credits needed ^^
#Thanks to everyone in the Warband modding community!
#This script uses the Party Size as the base for calculating your Prisoner Limit.
#Every point in Prisoner Management (PARTY SKILL) adds 20% to your Prisoner Limit, again, based on your current Party Size.
  ("game_get_party_prisoner_limit",
    [
      (party_get_skill_level, ":skill", "p_main_party", "skl_prisoner_management"),
      (party_get_num_companions, ":p_size", "p_main_party"),
      (store_mul, ":pm_mod", ":p_size", ":skill"),
      (store_div, ":pm_final", ":pm_mod", 5),
      (assign, reg0, ":pm_final"),
      (set_trigger_result, reg0),
  ]),

The deed is done, now you can have generally more prisoners the larger your current party size is, because you know, it just makes sense. It also reflects the AI tendency to have more prisoners the more troops they have (that is for other reasons, but its the best I could manage as a parallel, because AI parties does not have any prisoner limit whatsoever afaik :lol:).
At the maximum of 14 points in Slavekeeping (now formely "Prisoner Management"), you can have a whopping 280 prisoners with a party of 100 members. Thats a LOT of gold to make with the Ransom Brokers... And speaking of them... more code incoming, thats all I have to say for now! :wink:

I hope you guys find this useful. More scripts to come, but Im currenlty focusing on mission_templates stuff and planning to add my codes as a bundle of sorts. Any feedback til now is appreciated!
Thanks for reading, cya!
-Red
 
Last edited:
This script can be used for right shifting flag bits. For example, you can define flag_constant = 8. You can just rshift of 3 in the code. But what if you want to change flag_constant =16. Than you need to search all right shifting of 3 and change to 4. With this script it can be done automatically.
Python:
 # script_store_log_2
  # Input:  value
  # Output: reg0 - log2(value)
  # Note: value should be power of 2 and equal to 0
  ("store_log_2", [
    (store_script_param, ":value", 1),
    (assign, ":end", 1),
    (try_for_range, ":result", 0, ":end"),
      (gt, ":end", 1000000), # prevent soft lock
    (else_try),
      (eq, ":value", 1),
    (else_try),
      (val_add, ":end", 1),
      (val_rshift, ":value", 1),
    (try_end),
    (assign, reg0, ":result"),
  ]),
 
Last edited:
get_fixed_point_multiplier
Or rather, a small trick to achieve what get_fixed_point_multiplier operation would do, if it existed:
Python:
(assign,reg0,1),
(convert_to_fixed_point,reg0),
And voilà, now reg0 = whatever was earlier set as fixed_point_multiplier.

I can't believe it took me more than a year to come up with this.

Btw. did you know that fixed_point_multiplier can be anything?
It doesn't have to be 1, 10, 100, 1000 and so on. You can use 4, or 7, or -123456!
For example, setting fixed_point_multiplier to powers of 2 can be useful when doing math on binary fractions.
And multiples of 360 is good for math on angles with minimal rounding errors.
It may be tricky to wrap your head around it at first, but it's really useful.
 
Last edited:
bugfix for item_get_horse_scale
Here's a bugfix for item_get_horse_scale returning inaccurate results due to rounding errors. Uses the above trick for getting current fixed_point_multiplier.
Put this in module_scripts.py:
Python:
    ("item_get_horse_scale", #wraps around item_get_horse_scale operation to fix bug with value rounding in it
        #input: troop id
        #output: reg0 = scale as fixed point number (if item id is incorrect, will throw errors in runtime)
        [
            (store_script_param,reg0,1), #get item id
            (assign,":cur_mult",1), #store current fixed_point_multiplier for later use
            (convert_to_fixed_point,":cur_mult"), #multiplies by current value of fixed_point_multiplier
            (set_fixed_point_multiplier,200), #use temporary fixed_point_multiplier with 0.005 precision
            (item_get_horse_scale,reg0,reg0), #turn into horse scale
            (val_add,reg0,1), #deal with rounding errors
            (val_mul,reg0,":cur_mult"), #convert to current fixed_point_multiplier by multiplying by it...
            (val_div,reg0,200), #...and then dividing by our temporary fixed_point_multiplier
            (set_fixed_point_multiplier,":cur_mult"), #restore previous fixed_point_multiplier setting
        ]
    ),

Even easier (but more hacky) solution that I came up with inspired by discussion on this topic with Vetrogor:
Python:
    ("item_get_horse_scale", #wraps around item_get_horse_scale operation to fix bug with value rounding in it
        #input: troop id
        #output: reg0 = scale as fixed point number (if item id is incorrect, will throw errors in runtime)
        [
            (store_script_param,reg0,1), #get item id
            (item_get_weapon_length,reg0,reg0), #reads the same bits as item_get_horse_scale and isn't bugged
            (try_begin),
                (eq,reg0,0), #but for scale of 0 returns 0 (which is correct output for weapons)
                (assign,reg0,100), #for horses however, if scale is 0 then engine applies scale 100
            (try_end),
        ]
    ),
 
Last edited:
troop_get_scale - without WSE
This one was rather tricky to come up with, but here's how to do it.
1. In module_constants.py define scales for your skins, for example:
Python:
#skin scales (use precision up to 2nd decimal point, you really don't need more than that)
man_scale = 1.00
woman_scale = 0.95 #or use your own value, this is just an example
Now, these are floating point, and Warband scripts only operate on integers (fixed point is also actually just an integer).
But Python isn't limited like that! As long as we get those converted into integers during compilation, we are fine and no rounding errors will occur.
2. Modify your module_skins to use those new sexy constants we just defined:
change this:
Python:
"skel_human", 1.0,
to this:
Python:
"skel_human",man_scale,
and this:
Python:
"skel_human",woman_scale,
Notice, that module_skins *expects* floating point, so everything is fine.

3. Now for scripts:
Python:
    ("troop_get_scale", #outputs skeleton scales of different troop skins (genders, types, however we're gonna call them) - sets fixed point multiplier to 100
        #input: troop id
        #output: reg0 = scale as fixed point number (will return fixed point -1 if script is missing this skin definition, but if troop id is incorrect then will just throw errors in runtime)
        [
            (store_script_param,reg0,1), #get troop id
            (try_begin),
                (troop_get_type,reg0,reg0), #get skin/gender
                (try_begin), #adult male
                    (this_or_next|eq,reg0,tf_male),
                    (             eq,reg0,tf_male_u),
                    (assign,reg0,man_scale*100), #values in module constant are with precision of 2 decimal points, so multpiply by 100 to preserve accuracy
                (else_try), #adult female
                    (this_or_next|eq,reg0,tf_female),
                    (             eq,reg0,tf_female_u),
                    (assign,reg0,woman_scale*100), #values in module constant are with precision of 2 decimal points, so multpiply by 100 to preserve accuracy
                (else_try), #error: skin undefined in this script
                    (assign,reg0,-100), #values in module constant are with precision of 2 decimal points, so multpiply by 100 to preserve accuracy
                (try_end),
                (convert_to_fixed_point,reg0), #conform to currently set fixed_point_multiplier
                (val_div,reg0,100), #div by 100, because we earlier multiplied by 100
            (try_end),
        ]
    ),
Notice that our floating point constants man_scale and woman_scale are multiplied by 100 not with val_mul, but by simple woman_scale*100.
This means that math is done by Python during compilation, not by Warband engine during gameplay.
If it was calculated by engine, woman_scale=0.95 would be flattened to integer = 0, and then 0*100=0.
However, because Python does math properly, it's 0.95*100=95, and that's what Warband engine will get - an integer equal to 95.

And we're done!

agent_get_scale - without WSE
Also, combining the above script with my script_item_get_horse_scale that I posted somewhere above, we can now create a complete agent_get_scale script. Here's how:
Python:
    ("rndl_agent_get_scale", #outputs skeleton scales of different troop skins (genders, types, however we're gonna call them)
        #input: agent id
        #output: reg0 = scale as fixed point number (if agent id is incorrect, will throw errors in runtime)
        [
            (store_script_param,reg0,1), #get agent id
            (try_begin), #human agent
                (agent_is_human,reg0),
                (agent_get_troop_id,reg0,reg0), #turn into troop id
                (call_script,"script_rndl_troop_get_scale",reg0), #get scale - returns result to reg0
            (else_try), #horse agent
                (agent_get_item_id,reg0,reg0), #turn into item id
                (call_script,"script_rndl_item_get_horse_scale",reg0), #get scale - returns result to reg0
            (try_end),
        ]
    ),
 
A python file that can generate your item_modifiers.txt
It may not be insanely useful by itself, but it allows us to:
1. store comments and mark changes to modifiers, unlike the text file
2. access modifier data array "easily" from our module system by means of imports

How to set it up
Folder structure is important, so if you're a beginner, follow my instructions to the letter. If you feel experienced enough then you can use whatever folder structure you want, just remember to edit file paths.
0. Make sure your folder structure is as follows:
- <game folder>\Modules\<folder of your mod> - should contain your mod, module.ini, item_kinds.txt etc.
- <game folder>\Modules\<folder of your mod>\Data - can be empty, this is where your item_modifiers.txt will exist
- <game folder>\Modules\<folder of your mod>\<module system> - should contain module.scripts.py, module_info.py etc.
- <game folder>\Modules\<folder of your mod>\<module system>\data - folder name must be exactly "data"
1. Create an empty file item_modifiers.py (for example by copying one of your module_.py files and removing what's inside)
2. Put it in <game folder>\Modules\<folder of your mod>\<module system>\data\
3. Paste this into item_modifiers.py:
Python:
###############################################################################################################
#Prefix: imod_ / imodbit_                                                                                     #
#Each item record contains the following fields:                                                              #
#1) Modifier id {string}: used for referencing modifiers in other files.                                      #
#2) Modified item name {string}: {s1} represents the base name of the item.                                   #
#3) Price modifier {float}: 1 means no change compared to plain, 0.5 means half the price, 3 means triple etc.#
#4) Modifier abundance {float}: Standard is 1. Works similar to item abundance stat                           #
#Note: In module files do not refer to these as "imod_...". Import header_item_modifiers and use imod_...     #
#All item modifiers are hardcoded by both id number and by id name. Do not reorder them nor edit the id name. #
#Effects of modifiers are hardcoded and cannot be changed unless stated otherwise in comments below.          #
#You can however freely edit the modified name, price modifier and abundance.                                 #
###############################################################################################################

import string
import sys; sys.dont_write_bytecode = True
if not ".." in sys.path: sys.path.append("..")
from header_item_modifiers import *#importing it here so anything that imports this file can use the imod_ constants too

#null definitions
def_stat = 1.000000 #standard value

item_modifiers = [
    #BUGFIX BEGIN - typos
    ##("imod_plain",    "Plain {s0}",       def_stat,def_stat), #no effects, name, price modifier and rarity modifier are all hardcoded and changing them has no effect
    ("imod_plain",      "{s0}",             def_stat,def_stat), #no effects, name, price modifier and rarity modifier are all hardcoded and changing them has no effect
    #BUGFIX END - typos
    ("imod_cracked",    "Cracked {s0}",     0.500000,def_stat), #armor -4, shield resistance -4, shield durability -56, damage dealt -5
    ("imod_rusty",      "Rusty {s0}",       0.550000,def_stat), #armor -3, damage dealt -3
    ("imod_bent",       "Bent {s0}",        0.650000,def_stat), #damage dealt -3, weapon speed -3 (for thrown weapons treat as weapons, not as ammo)
    ("imod_chipped",    "Chipped {s0}",     0.720000,def_stat), #damage dealt -1
    ("imod_battered",   "Battered {s0}",    0.750000,def_stat), #armor -2, shield resistance -2, shield durability -26
    ("imod_poor",       "Poor {s0}",        0.800000,def_stat), #no effects
    ("imod_crude",      "Crude {s0}",       0.830000,def_stat), #armor -1
    ("imod_old",        "Old {s0}",         0.860000,def_stat), #no effects
    ("imod_cheap",      "Cheap {s0}",       0.900000,def_stat), #no effects
    ("imod_fine",       "Fine {s0}",        1.900000,0.600000), #damage dealt +1
    #BUGFIX BEGIN - typos
    ##("imod_well_made","Well Made {s0}",   2.500000,0.500000), #no effects
    ("imod_well_made",  "Well-Made {s0}",   2.500000,0.500000), #no effects
    #BUGFIX END - typos
    ("imod_sharp",      "Sharp {s0}",       1.600000,0.600000), #no effects
    ("imod_balanced",   "Balanced {s0}",    3.500000,0.500000), #damage dealt +3, weapon speed +3
    ("imod_tempered",   "Tempered {s0}",    6.700000,0.400000), #damage dealt +4
    ("imod_deadly",     "Deadly {s0}",      8.500000,0.300000), #no effects
    ("imod_exquisite",  "Exquisite {s0}",  14.500000,0.300000), #no effects
    ("imod_masterwork", "Masterwork {s0}", 17.500000,0.300000), #requirement +4, weapon speed +1, damage dealt +5
    ("imod_heavy",      "Heavy {s0}",       1.900000,0.700000), #requirement (weapons only) +1, damage dealt +2, weapon speed -2, horse armor +3, horse charge +4, horse hit points +10
    ("imod_strong",     "Strong {s0}",      4.900000,0.400000), #requirement +2, weapon seepd -3, damage dealt +3
    ("imod_powerful",   "Powerful {s0}",    3.200000,0.400000), #no effects
    ("imod_tattered",   "Tattered {s0}",    0.500000,def_stat), #armor -3
    ("imod_ragged",     "Ragged {s0}",      0.700000,def_stat), #armor -2
    ("imod_rough",      "Rough {s0}",       0.600000,def_stat), #no effects
    ("imod_sturdy",     "Sturdy {s0}",      1.700000,0.500000), #armor +1
    ("imod_thick",      "Thick {s0}",       2.600000,0.350000), #armor +2, shield resistance +2, shield durability +47
    ("imod_hardened",   "Hardened {s0}",    3.900000,0.300000), #armor +3
    ("imod_reinforced", "Reinforced {s0}",  6.500000,0.250000), #armor +4, shield resistance +4, shield durability +83
    ("imod_superb",     "Superb {s0}",      2.500000,0.250000), #no effects
    ("imod_lordly",     "Lordly {s0}",     11.500000,0.250000), #armor +6
    ("imod_lame",       "Lame {s0}",        0.400000,def_stat), #horse maneuver -5, horse speed -10
    ("imod_swaybacked", "Swaybacked {s0}",  0.600000,def_stat), #horse maneuver -2, horse speed -4
    ("imod_stubborn",   "Stubborn {s0}",    0.900000,def_stat), #requirement +1, horse hit points +5
    ("imod_timid",      "Timid {s0}",       1.800000,def_stat), #requirement -1, horse speed can be defined by timid_modifier_speed_bonus in module.ini
    ("imod_meek",       "Meek {s0}",        1.800000,def_stat), #horse speed can be defined by meek_modifier_speed_bonus in module.ini
    ("imod_spirited",   "Spirited {s0}",    6.500000,0.600000), #horse charge +1, horse maneuver +1, horse speed +2
    ("imod_champion",   "Champion {s0}",   14.500000,0.200000), #requirement +2, horse charge +2, horse maneuver +2, horse speed +4
    ("imod_fresh",      "Fresh {s0}",       def_stat,def_stat), #no effects, used for perishable food
    ("imod_day_old",    "Day-old {s0}",     def_stat,def_stat), #no effects, used for perishable food
    ("imod_two_day_old","Two Days-old {s0}",0.900000,def_stat), #no effects, used for perishable food
    ("imod_smelling",   "Smelling {s0}",    0.400000,def_stat), #no effects, used for perishable food
    ("imod_rotten",     "Rotten {s0}",      0.050000,def_stat), #no effects, used for perishable food
    ("imod_large_bag",  "Large Bag of {s0}",1.900000,0.300000), #ammo capacity +13%
]

if __name__ == '__main__': #don't do this on imports, only when executed directly
    def save_item_modifiers():
        file = open("../../Data/item_modifiers.txt","w")
        for item_modifier in item_modifiers:
            item_modifier_1 = string.replace(item_modifier[1],"","")
            item_modifier_1 = string.replace(item_modifier[1]," ","_")
            item_modifier_1 = string.replace(item_modifier_1,"{s0}","%s")
            file.write("%s %s %.6f %.6f"%(item_modifier[0],item_modifier_1,item_modifier[2],item_modifier[3]))
            file.write("\n")
        file.close()

if __name__ == '__main__': #don't do this on imports, only when executed directly
    print "Compiling item modifiers..."
    save_item_modifiers()
4. Compile it the same way you compile your module data (flora, ground specs or skyboxes) - simply execute this in command line:
Code:
python item_modifiers.py
This will output your item_modifiers.txt to <your module>\Data
 
Last edited:
Script to get modifier data - no matter what changes to modifiers you make
Script returns price multiplier, modifier's abundance, modifier's name (pass item name to s0, and script will append modifier name to it).
Works correctly with translations.
Requires using item_modifiers.py I posted above. Instructions below assume that you follow to the letter the folder structure I described there.

1. Set up item_modifiers.py as described in previous post.
2. Edit module_strings.py
Python:
#MOD BEGIN - utility scripts
import sys; sys.path.append("./data")
from item_modifiers import *
#MOD END - utility scripts
Python:
    #MOD BEGIN - utility scripts
    ("imod_plain",      item_modifiers[imod_plain      ][1]),
    ("imod_cracked",    item_modifiers[imod_cracked    ][1]),
    ("imod_rusty",      item_modifiers[imod_rusty      ][1]),
    ("imod_bent",       item_modifiers[imod_bent       ][1]),
    ("imod_chipped",    item_modifiers[imod_chipped    ][1]),
    ("imod_battered",   item_modifiers[imod_battered   ][1]),
    ("imod_poor",       item_modifiers[imod_poor       ][1]),
    ("imod_crude",      item_modifiers[imod_crude      ][1]),
    ("imod_old",        item_modifiers[imod_old        ][1]),
    ("imod_cheap",      item_modifiers[imod_cheap      ][1]),
    ("imod_fine",       item_modifiers[imod_fine       ][1]),
    ("imod_well_made",  item_modifiers[imod_well_made  ][1]),
    ("imod_sharp",      item_modifiers[imod_sharp      ][1]),
    ("imod_balanced",   item_modifiers[imod_balanced   ][1]),
    ("imod_tempered",   item_modifiers[imod_tempered   ][1]),
    ("imod_deadly",     item_modifiers[imod_deadly     ][1]),
    ("imod_exquisite",  item_modifiers[imod_exquisite  ][1]),
    ("imod_masterwork", item_modifiers[imod_masterwork ][1]),
    ("imod_heavy",      item_modifiers[imod_heavy      ][1]),
    ("imod_strong",     item_modifiers[imod_strong     ][1]),
    ("imod_powerful",   item_modifiers[imod_powerful   ][1]),
    ("imod_tattered",   item_modifiers[imod_tattered   ][1]),
    ("imod_ragged",     item_modifiers[imod_ragged     ][1]),
    ("imod_rough",      item_modifiers[imod_rough      ][1]),
    ("imod_sturdy",     item_modifiers[imod_sturdy     ][1]),
    ("imod_thick",      item_modifiers[imod_thick      ][1]),
    ("imod_hardened",   item_modifiers[imod_hardened   ][1]),
    ("imod_reinforced", item_modifiers[imod_reinforced ][1]),
    ("imod_superb",     item_modifiers[imod_superb     ][1]),
    ("imod_lordly",     item_modifiers[imod_lordly     ][1]),
    ("imod_lame",       item_modifiers[imod_lame       ][1]),
    ("imod_swaybacked", item_modifiers[imod_swaybacked ][1]),
    ("imod_stubborn",   item_modifiers[imod_stubborn   ][1]),
    ("imod_timid",      item_modifiers[imod_timid      ][1]),
    ("imod_meek",       item_modifiers[imod_meek       ][1]),
    ("imod_spirited",   item_modifiers[imod_spirited   ][1]),
    ("imod_champion",   item_modifiers[imod_champion   ][1]),
    ("imod_fresh",      item_modifiers[imod_fresh      ][1]),
    ("imod_day_old",    item_modifiers[imod_day_old    ][1]),
    ("imod_two_day_old",item_modifiers[imod_two_day_old][1]),
    ("imod_smelling",   item_modifiers[imod_smelling   ][1]),
    ("imod_rotten",     item_modifiers[imod_rotten     ][1]),
    ("imod_large_bag",  item_modifiers[imod_large_bag  ][1]),
    #MOD END - utility scripts
3. Edit module_scripts.py
Python:
#MOD BEGIN - utility scripts
import sys; sys.path.append("./data")
from item_modifiers import *
#MOD END - utility scripts
Python:
    #MOD BEGIN - utility scripts
    ("cf_rndl_get_modifier_data", #helper script that provides data of modifiers
        #input:
        #param1 = modifier (imod_), script fails if provided value is not an existing modifier
        #(optional) s0 = item name
        #output:
        #reg0 = item price multiplier (as %)
        #reg1 = value similar to abundance that defines how common this modifier is, how likely it is to get attached to a randomly generated item (same as price it's value from text item_modifiers.txt * 100)
        #s0 = full item name with modifier included in it (or garbage, if s0 wasn't set up before calling this script)
        [
            (store_script_param,":modifier",1),
            (is_between,":modifier",imod_plain,imod_large_bag+1), #check if parameter is a valid modifier
            #take price and abundance data from array
            (try_begin), #imod_plain
                (eq,                ":modifier",imod_plain),
                (assign,reg0,100*item_modifiers[imod_plain][2]),
                (assign,reg1,100*item_modifiers[imod_plain][3]),
            (else_try), #imod_cracked
                (eq,                ":modifier",imod_cracked),
                (assign,reg0,100*item_modifiers[imod_cracked][2]),
                (assign,reg1,100*item_modifiers[imod_cracked][3]),
            (else_try), #imod_rusty
                (eq,                ":modifier",imod_rusty),
                (assign,reg0,100*item_modifiers[imod_rusty][2]),
                (assign,reg1,100*item_modifiers[imod_rusty][3]),
            (else_try), #imod_bent
                (eq,                ":modifier",imod_bent),
                (assign,reg0,100*item_modifiers[imod_bent][2]),
                (assign,reg1,100*item_modifiers[imod_bent][3]),
            (else_try), #imod_chipped
                (eq,                ":modifier",imod_chipped),
                (assign,reg0,100*item_modifiers[imod_chipped][2]),
                (assign,reg1,100*item_modifiers[imod_chipped][3]),
            (else_try), #imod_battered
                (eq,                ":modifier",imod_battered),
                (assign,reg0,100*item_modifiers[imod_battered][2]),
                (assign,reg1,100*item_modifiers[imod_battered][3]),
            (else_try), #imod_poor
                (eq,                ":modifier",imod_poor),
                (assign,reg0,100*item_modifiers[imod_poor][2]),
                (assign,reg1,100*item_modifiers[imod_poor][3]),
            (else_try), #imod_crude
                (eq,                ":modifier",imod_crude),
                (assign,reg0,100*item_modifiers[imod_crude][2]),
                (assign,reg1,100*item_modifiers[imod_crude][3]),
            (else_try), #imod_old #
                (eq,                ":modifier",imod_old),
                (assign,reg0,100*item_modifiers[imod_old][2]),
                (assign,reg1,100*item_modifiers[imod_old][3]),
            (else_try), #imod_cheap
                (eq,                ":modifier",imod_cheap),
                (assign,reg0,100*item_modifiers[imod_cheap][2]),
                (assign,reg1,100*item_modifiers[imod_cheap][3]),
            (else_try), #imod_fine
                (eq,                ":modifier",imod_fine),
                (assign,reg0,100*item_modifiers[imod_fine][2]),
                (assign,reg1,100*item_modifiers[imod_fine][3]),
            (else_try), #imod_well_made
                (eq,                ":modifier",imod_well_made),
                (assign,reg0,100*item_modifiers[imod_well_made][2]),
                (assign,reg1,100*item_modifiers[imod_well_made][3]),
            (else_try), #imod_sharp
                (eq,                ":modifier",imod_sharp),
                (assign,reg0,100*item_modifiers[imod_sharp][2]),
                (assign,reg1,100*item_modifiers[imod_sharp][3]),
            (else_try), #imod_balanced
                (eq,                ":modifier",imod_balanced),
                (assign,reg0,100*item_modifiers[imod_balanced][2]),
                (assign,reg1,100*item_modifiers[imod_balanced][3]),
            (else_try), #imod_tempered
                (eq,                ":modifier",imod_tempered),
                (assign,reg0,100*item_modifiers[imod_tempered][2]),
                (assign,reg1,100*item_modifiers[imod_tempered][3]),
            (else_try), #imod_deadly
                (eq,                ":modifier",imod_deadly),
                (assign,reg0,100*item_modifiers[imod_deadly][2]),
                (assign,reg1,100*item_modifiers[imod_deadly][3]),
            (else_try), #imod_exquisite
                (eq,                ":modifier",imod_exquisite),
                (assign,reg0,100*item_modifiers[imod_exquisite][2]),
                (assign,reg1,100*item_modifiers[imod_exquisite][3]),
            (else_try), #imod_masterwork
                (eq,                ":modifier",imod_masterwork),
                (assign,reg0,100*item_modifiers[imod_masterwork][2]),
                (assign,reg1,100*item_modifiers[imod_masterwork][3]),
            (else_try), #imod_heavy
                (eq,                ":modifier",imod_heavy),
                (assign,reg0,100*item_modifiers[imod_heavy][2]),
                (assign,reg1,100*item_modifiers[imod_heavy][3]),
            (else_try), #imod_strong
                (eq,                ":modifier",imod_strong),
                (assign,reg0,100*item_modifiers[imod_strong][2]),
                (assign,reg1,100*item_modifiers[imod_strong][3]),
            (else_try), #imod_powerful
                (eq,                ":modifier",imod_powerful),
                (assign,reg0,100*item_modifiers[imod_powerful][2]),
                (assign,reg1,100*item_modifiers[imod_powerful][3]),
            (else_try), #imod_tattered
                (eq,                ":modifier",imod_tattered),
                (assign,reg0,100*item_modifiers[imod_tattered][2]),
                (assign,reg1,100*item_modifiers[imod_tattered][3]),
            (else_try), #imod_ragged
                (eq,                ":modifier",imod_ragged),
                (assign,reg0,100*item_modifiers[imod_ragged][2]),
                (assign,reg1,100*item_modifiers[imod_ragged][3]),
            (else_try), #imod_rough
                (eq,                ":modifier",imod_rough),
                (assign,reg0,100*item_modifiers[imod_rough][2]),
                (assign,reg1,100*item_modifiers[imod_rough][3]),
            (else_try), #imod_sturdy
                (eq,                ":modifier",imod_sturdy),
                (assign,reg0,100*item_modifiers[imod_sturdy][2]),
                (assign,reg1,100*item_modifiers[imod_sturdy][3]),
            (else_try), #imod_thick
                (eq,                ":modifier",imod_thick),
                (assign,reg0,100*item_modifiers[imod_thick][2]),
                (assign,reg1,100*item_modifiers[imod_thick][3]),
            (else_try), #imod_hardened
                (eq,                ":modifier",imod_hardened),
                (assign,reg0,100*item_modifiers[imod_hardened][2]),
                (assign,reg1,100*item_modifiers[imod_hardened][3]),
            (else_try), #imod_reinforced
                (eq,                ":modifier",imod_reinforced),
                (assign,reg0,100*item_modifiers[imod_reinforced][2]),
                (assign,reg1,100*item_modifiers[imod_reinforced][3]),
            (else_try), #imod_superb
                (eq,                ":modifier",imod_superb),
                (assign,reg0,100*item_modifiers[imod_superb][2]),
                (assign,reg1,100*item_modifiers[imod_superb][3]),
            (else_try), #imod_lordly
                (eq,                ":modifier",imod_lordly),
                (assign,reg0,100*item_modifiers[imod_lordly][2]),
                (assign,reg1,100*item_modifiers[imod_lordly][3]),
            (else_try), #imod_lame
                (eq,                ":modifier",imod_lame),
                (assign,reg0,100*item_modifiers[imod_lame][2]),
                (assign,reg1,100*item_modifiers[imod_lame][3]),
            (else_try), #imod_swaybacked
                (eq,                ":modifier",imod_swaybacked),
                (assign,reg0,100*item_modifiers[imod_swaybacked][2]),
                (assign,reg1,100*item_modifiers[imod_swaybacked][3]),
            (else_try), #imod_stubborn
                (eq,                ":modifier",imod_stubborn),
                (assign,reg0,100*item_modifiers[imod_stubborn][2]),
                (assign,reg1,100*item_modifiers[imod_stubborn][3]),
            (else_try), #imod_timid
                (eq,                ":modifier",imod_timid),
                (assign,reg0,100*item_modifiers[imod_timid][2]),
                (assign,reg1,100*item_modifiers[imod_timid][3]),
            (else_try), #imod_meek
                (eq,                ":modifier",imod_meek),
                (assign,reg0,100*item_modifiers[imod_meek][2]),
                (assign,reg1,100*item_modifiers[imod_meek][3]),
            (else_try), #imod_spirited
                (eq,                ":modifier",imod_spirited),
                (assign,reg0,100*item_modifiers[imod_spirited][2]),
                (assign,reg1,100*item_modifiers[imod_spirited][3]),
            (else_try), #imod_champion
                (eq,                ":modifier",imod_champion),
                (assign,reg0,100*item_modifiers[imod_champion][2]),
                (assign,reg1,100*item_modifiers[imod_champion][3]),
            (else_try), #imod_fresh
                (eq,                ":modifier",imod_fresh),
                (assign,reg0,100*item_modifiers[imod_fresh][2]),
                (assign,reg1,100*item_modifiers[imod_fresh][3]),
            (else_try), #imod_day_old
                (eq,                ":modifier",imod_day_old),
                (assign,reg0,100*item_modifiers[imod_day_old][2]),
                (assign,reg1,100*item_modifiers[imod_day_old][3]),
            (else_try), #imod_two_day_old
                (eq,                ":modifier",imod_two_day_old),
                (assign,reg0,100*item_modifiers[imod_two_day_old][2]),
                (assign,reg1,100*item_modifiers[imod_two_day_old][3]),
            (else_try), #imod_smelling
                (eq,                ":modifier",imod_smelling),
                (assign,reg0,100*item_modifiers[imod_smelling][2]),
                (assign,reg1,100*item_modifiers[imod_smelling][3]),
            (else_try), #imod_rotten
                (eq,                ":modifier",imod_rotten),
                (assign,reg0,100*item_modifiers[imod_rotten][2]),
                (assign,reg1,100*item_modifiers[imod_rotten][3]),
            (else_try), #imod_large_bag
                (eq,                ":modifier",imod_large_bag),
                (assign,reg0,100*item_modifiers[imod_large_bag][2]),
                (assign,reg1,100*item_modifiers[imod_large_bag][3]),
            (try_end),
            #append modifier name to s0 (item name)
            (val_add,":modifier","str_imod_plain"),
            (str_store_string,s0,":modifier"),
        ]
    ),
    #MOD END - utility scripts
4. Now you can call like so:
Python:
(troop_get_inventory_slot,":item_id","trp_player",ek_horse), #player is riding a Courser
(troop_get_inventory_slot_modifier,":item_modifier","trp_player",ek_horse), #a Spirited one
(str_store_item_name,s0,":item_id"), #store "Courser" to s0
(call_script,"script_cf_rndl_get_modifier_data",":item_modifier"), #call our script with item name in s0 and item modifier in parameter
(display_message,s0), #will display "Spirited Courser"
(display_message,"@modifier makes it cost {reg0}% of standard price"),
(display_message,"@this modifier's abundance is {reg1} (standard is 100)"),

It was a lot of work, especially forcing the strings to read from an array and be understood by the game engine.
Have fun with it.
 
How to get skill names
It's surprisingly easy. Well, surprisingly for me, at least.
There is a hardcoded number of skills, so we can safely add those as strings without worry that changing skills will mess up the ids in the future.
This method will even react to skl_ id changes that you may introduce and change string ids accordingly.
skl_trade will have its name in str_skl_trade
skl_surgery - in str_skl_surgery
skl_lord_voldemort will have str_skl_lord_voldemort
no exceptions =)

Insert at the top of your module_strings:
Python:
from module_skills import *

Insert somewhere among the string definitions:
Python:
    #skill names
    ("skl_%s"%(skills[ 0][0]),"%s"%(skills[ 0][1])), #str_skl_trade
    ("skl_%s"%(skills[ 1][0]),"%s"%(skills[ 1][1])), #str_skl_leadership
    ("skl_%s"%(skills[ 2][0]),"%s"%(skills[ 2][1])), #str_skl_prisoner_management
    ("skl_%s"%(skills[ 3][0]),"%s"%(skills[ 3][1])), #str_skl_reserved_1
    ("skl_%s"%(skills[ 4][0]),"%s"%(skills[ 4][1])), #str_skl_reserved_2
    ("skl_%s"%(skills[ 5][0]),"%s"%(skills[ 5][1])), #str_skl_reserved_3
    ("skl_%s"%(skills[ 6][0]),"%s"%(skills[ 6][1])), #str_skl_reserved_4
    ("skl_%s"%(skills[ 7][0]),"%s"%(skills[ 7][1])), #str_skl_persuasion
    ("skl_%s"%(skills[ 8][0]),"%s"%(skills[ 8][1])), #str_skl_engineer
    ("skl_%s"%(skills[ 9][0]),"%s"%(skills[ 9][1])), #str_skl_first_aid
    ("skl_%s"%(skills[10][0]),"%s"%(skills[10][1])), #str_skl_surgery
    ("skl_%s"%(skills[11][0]),"%s"%(skills[11][1])), #str_skl_wound_treatment
    ("skl_%s"%(skills[12][0]),"%s"%(skills[12][1])), #str_skl_inventory_management
    ("skl_%s"%(skills[13][0]),"%s"%(skills[13][1])), #str_skl_spotting
    ("skl_%s"%(skills[14][0]),"%s"%(skills[14][1])), #str_skl_pathfinding
    ("skl_%s"%(skills[15][0]),"%s"%(skills[15][1])), #str_skl_tactics
    ("skl_%s"%(skills[16][0]),"%s"%(skills[16][1])), #str_skl_tracking
    ("skl_%s"%(skills[17][0]),"%s"%(skills[17][1])), #str_skl_trainer
    ("skl_%s"%(skills[18][0]),"%s"%(skills[18][1])), #str_skl_reserved_5
    ("skl_%s"%(skills[19][0]),"%s"%(skills[19][1])), #str_skl_reserved_6
    ("skl_%s"%(skills[20][0]),"%s"%(skills[20][1])), #str_skl_reserved_7
    ("skl_%s"%(skills[21][0]),"%s"%(skills[21][1])), #str_skl_reserved_8
    ("skl_%s"%(skills[22][0]),"%s"%(skills[22][1])), #str_skl_looting
    ("skl_%s"%(skills[23][0]),"%s"%(skills[23][1])), #str_skl_horse_archery
    ("skl_%s"%(skills[24][0]),"%s"%(skills[24][1])), #str_skl_riding
    ("skl_%s"%(skills[25][0]),"%s"%(skills[25][1])), #str_skl_athletics
    ("skl_%s"%(skills[26][0]),"%s"%(skills[26][1])), #str_skl_shield
    ("skl_%s"%(skills[27][0]),"%s"%(skills[27][1])), #str_skl_weapon_master
    ("skl_%s"%(skills[28][0]),"%s"%(skills[28][1])), #str_skl_reserved_9
    ("skl_%s"%(skills[29][0]),"%s"%(skills[29][1])), #str_skl_reserved_10
    ("skl_%s"%(skills[30][0]),"%s"%(skills[30][1])), #str_skl_reserved_11
    ("skl_%s"%(skills[31][0]),"%s"%(skills[31][1])), #str_skl_reserved_12
    ("skl_%s"%(skills[32][0]),"%s"%(skills[32][1])), #str_skl_reserved_13
    ("skl_%s"%(skills[33][0]),"%s"%(skills[33][1])), #str_skl_power_draw
    ("skl_%s"%(skills[34][0]),"%s"%(skills[34][1])), #str_skl_power_throw
    ("skl_%s"%(skills[35][0]),"%s"%(skills[35][1])), #str_skl_power_strike
    ("skl_%s"%(skills[36][0]),"%s"%(skills[36][1])), #str_skl_ironflesh
    ("skl_%s"%(skills[37][0]),"%s"%(skills[37][1])), #str_skl_reserved_14
    ("skl_%s"%(skills[38][0]),"%s"%(skills[38][1])), #str_skl_reserved_15
    ("skl_%s"%(skills[39][0]),"%s"%(skills[39][1])), #str_skl_reserved_16
    ("skl_%s"%(skills[40][0]),"%s"%(skills[40][1])), #str_skl_reserved_17
    ("skl_%s"%(skills[41][0]),"%s"%(skills[41][1])), #str_skl_reserved_18
Comments tell you the string id if you use the vanilla skill ids. These are just for your reference and your convenience, change them as you see fit.
 
Last edited:

Fixed/unlimited face_keys_set_morph_key - without WSE​


Next step would be to have another function that lets you set those face attributes at runtime, too. That should also be possible for morph keys 0-41 and let you build your own Pinocchio depending on how the current quest is going, or dialog responses.
🐧

@Erundil and a few others on Discord seemed to be interested in an extended function that would let us modify face sliders beyond 0-7 without WSE, so here's a function that converts the first half of a face code into a bunch of actual numbers, overwrites the field corresponding to the provided :key_no index using bitmasks, and then converts those numbers back into an hex string that can be fed back or assigned into troops.

Keep in mind that calling this will clear/reset any morph key beyond index 20 due to limitations of the unconventional string-to-number conversion method/hack. See the comments below for more info about how it works. It could be expanded to write more morphs until the maximum of 41, but we would need a way to carry the information in block C as we can't parse/read blocks C/D directly, only A/B. Hopefully the documentation in the code should make that easy, the parts are independent of each other and can be reused.

For the fixed face_keys_get_extended_morph_key counterpart, you can find it here.

Edit: Updated the function with support for setting face morphs 0-42, with caveats.

Python:
# 0000000ff20011456b1a20a72efac68814e5df58d1053977000000000000000a     e.g. funky face code for illustrative purposes :)
# 0000000ff2001145................                                  <- with this trick we can only parse into a number blocks A
# ................6b1a20a72efac688                                     and B. Where B holds the 0-20 morph key data.
#                                 14e5df58d1053977················  <- blocks C/D are inaccessible, but only C
#                                 ················000000000000000a     holds useful data; the 21+ morph keys.
# [    block a   ][    block b   ][    block c   ][    block d   ]
#
# swy: for the function to get extended morph keys and other string-to-number trickery: https://forum.taleworlds.com/?threads/453646
#                       \ for the actual format and bit packing, see this forum thread: https://forum.taleworlds.com/?threads/453145#format
#                       \ you can inspect/tweak face codes with this handy online tool: https://swyter.github.io/mab-tools/face
# (face_keys_set_morph_key, <string_no>, <key_no>, <value>),
("face_keys_set_extended_morph_key", [
    (store_script_param_1, ":string_reg"), (str_store_string_reg, s1, ":string_reg"),
    (store_script_param_2, ":key_no"    ),
    (store_script_param,   ":value",   3),

    (try_begin),
       (is_between, ":key_no", 0, 7 +1),
       # --
       (face_keys_set_morph_key, s1, ":key_no", ":value"),
       (str_store_string_reg, ":string_reg", s1), # swy: fix some weird string corruption when using the string register in a variable across calls
    (else_try),
       (is_between, ":key_no", 8, 42 +1),
       # --
       # swy: /part one/: parse the hexadecimal face code string into blocks of numbers using the
       #                  same trickery as in the "face_keys_get_extended_morph_key" script
       (try_for_range, ":cur_block", 0, 3 +1),
         (assign, ":shift", 0),
         (assign, reg2, 0),
         (try_for_range, ":i", 0, (7 + 3) +1), # swy: go over the 0-7 range, when :i is 8/9/10 it will get keys 0/1/2 again from a shifted position
             (try_begin),                      #      to get the remainding 8 bits, plus    2 [2211 1000 7776 6655][5444 3332 2211 1000]
                (eq, ":i", 8),                 #      an extra/unused one.                     ^^^^ ^^^^ <----------------------------< (each get_morph_key gets us blocks of three bits, as we go leftwards)
                (str_store_string, s1, "@FFFFFF{s1}"), # swy: pad out/shift to the right the equivalent to 24-bits/6 hex characters we've already read
             (end_try),
             (store_mod, ":i_clamped", ":i", 8), # swy: after doing 0-7 and reaching index 8, make it loop back to read 0-3 on the displaced data
             (face_keys_get_morph_key, reg0, s1, ":i_clamped"), # swy: with each of these calls we grab 3 bits at a time
             (val_lshift, reg0, ":shift"),
             (val_or, reg2, reg0),
             (val_add, ":shift", 3),
         (end_try),
         (str_store_string, s1, "@FF{s1}"), # swy: pad out/shift to the right the remaining 8-bits/2 hex characters we've read; that's 32 bits in total
         (val_and, reg2, 0xFFFFFFFF),       # swy: discard any possible bits outside of the 32-bit range
         
         (try_begin), (eq, ":cur_block", 3), (assign, ":block_a_hi", reg2), # swy: store it in the correct block,
         ( else_try), (eq, ":cur_block", 2), (assign, ":block_a_lo", reg2), #      e.g. "block_b" works like this:
         ( else_try), (eq, ":cur_block", 1), (assign, ":block_b_hi", reg2), # 6b1a20a7 | 6b1a20a7........ <-   high/top half
         ( else_try), (eq, ":cur_block", 0), (assign, ":block_b_lo", reg2), # 2efac688 | ........2efac688 <- low/bottom half
         (end_try),                                                         #            6b1a20a72efac688 <- original/full 64-bit block
       (end_try),
       
       # swy: hardcode the unobtainable block C with middle-of-the-road default values for all index 21-40 sliders
       #      feel free to use any other mechanism, like troop slots, or adding more function parameters
       #      to persistently carry this info over various calls. depending on your needs.
       #      --
       # NOTE: any edits of morph keys in this 21-42 range won't be saved between calls, unless you change this part somehow
       (assign, ":block_c_hi", 0x36db6db6),
       (assign, ":block_c_lo", 0xdb6db6db),
       
       # --
       
       # swy: /part two/: replace the bits of the selected morph key with the new value, now that it's in numeric form
       (try_begin),
            (ge,      ":key_no",  21), # swy: block B holds the morphs from 0 to 20, block C from 21 to 42;
            (val_sub, ":key_no",  21), #      here we choose to work on the data from block C, and make it
            (assign, ":cur_block", 1), #      so that 21 starts at index 0 there.
       (else_try),
            (assign, ":cur_block", 0), # swy: pick block B otherwise if we are in the 0-20 range.
       (end_try),
       
       (try_begin), (eq, ":cur_block", 0), (assign, ":val_hi", ":block_b_hi"), # swy: retrieve the data from either block B or C depending
                                           (assign, ":val_lo", ":block_b_lo"), #      of where's the data field we need to edit.
       ( else_try), (eq, ":cur_block", 1), (assign, ":val_hi", ":block_c_hi"),
                                           (assign, ":val_lo", ":block_c_lo"),
       (  end_try),
       
       # swy: build the bitmask and shift the value to cover it; e.g. for a ":key_no" = 10 that straddles two halves and ":value" = 5 we get
       #      00000000000000000000000000000001|11000000000000000000000000000000 bitmask:   0b111 << (10*3)
       #      11111111111111111111111111111110|00111111111111111111111111111111 inversed mask to AND against and clear the three off bits
       #      00000000000000000000000000000001 01000000000000000000000000000000 value: 5 = 0b101 << (10*3)
       #                                     ^ ^^
       (store_mul, ":bit_offset", ":key_no", 3),
       (assign, ":mask", 0b111),
       (val_lshift, ":mask",  ":bit_offset"),
       (val_lshift, ":value", ":bit_offset"),
       
       # swy: there's no ~ (bitwise not) operator, so invert the bits of the mask manually
#      (assign, ":mask_inv", 0),
#      (assign, ":i_bit", 1),
#      (try_for_range, ":i", 0, 64 +1),
#          (store_and, ":cur_mask_bit", ":mask", ":i_bit"),
#          (try_begin),
#            (eq, ":cur_mask_bit", 0), # swy: the current bit is zero? put a one, otherwise we assume it was cleared to zero before getting here and we leave it alone
#            (val_or, ":mask_inv", ":i_bit"),
#          (try_end),
#          (val_lshift, ":i_bit", 1),
#      (end_try),

       # swy: this does the same ~ (bitwise not) inversion, but with less operations (e.g. 0b0000'1110 is 14, and 0b1111'0001 is -15), so we do -(thing + 1)
       #      thanks to the registers being treated as signed 64-bit integers and thanks to sign extension we get the same: https://en.wikipedia.org/wiki/Two%27s_complement / https://en.wikipedia.org/wiki/Sign_extension
       (store_add, ":mask_inv", ":mask", 1),
       (val_mul,   ":mask_inv",         -1),
       
       # --
       
       # swy: apply the mask to clear these three bits to zero (with DATA &= ~MASK) and then replace them with our own (with DATA |= VALUE)
       (val_and, ":val_lo", ":mask_inv"),
       (val_or,  ":val_lo", ":value"),

       # swy: after doing it with the bottom 32-bit half, then do the same thing on the top 32-bit half
       (val_rshift, ":mask_inv", 32),
       (val_rshift, ":value",    32),
       (val_and, ":val_hi", ":mask_inv"),
       (val_or,  ":val_hi", ":value"),
       
       (try_begin), (eq, ":cur_block", 0), (assign, ":block_b_hi", ":val_hi"), # swy: save the modified data back in the right block
                                           (assign, ":block_b_lo", ":val_lo"), #      we grabbed the data from, see above.
       ( else_try), (eq, ":cur_block", 1), (assign, ":block_c_hi", ":val_hi"),
                                           (assign, ":block_c_lo", ":val_lo"),
       (  end_try),
       
       # --
       
       # swy: /part three/: recreate the hexadecimal string from the A/B blocks, as we can't access blocks C/D and the game doesn't care about trailing stuff
       #                    we don't really need to append "00000000000000000000000000000000" at the end to replace the inaccesible half that we couldn't parse/convert from hex string.
       #                    NOTE: you can add back the last half of the original face code (if you can and it's known/fixed/hardcoded) to retain the rest of morph key values
       #                    instead of losing them, or have good defaults instead of being all-zeros and reset it from slider 21 onwards.
       (str_clear, s2),
       (try_for_range, ":cur_block", 0, 5 +1),
           (try_begin), (eq, ":cur_block", 5), (assign, ":val", ":block_a_hi"), # swy: first convert the 32-bit bottom and then the top 32-bit half
           ( else_try), (eq, ":cur_block", 4), (assign, ":val", ":block_a_lo"), #      (of each of the two A/B 64-bit blocks).
           ( else_try), (eq, ":cur_block", 3), (assign, ":val", ":block_b_hi"), #
           ( else_try), (eq, ":cur_block", 2), (assign, ":val", ":block_b_lo"), # i.e. type them from right to left
           ( else_try), (eq, ":cur_block", 1), (assign, ":val", ":block_c_hi"),
           ( else_try), (eq, ":cur_block", 0), (assign, ":val", ":block_c_lo"),
           (  end_try),
           
           (try_for_range, ":unused", 0, 7 +1), # swy: from index 0 to 7 = 8 means there are 32 hex digits / 4 bits per visual/printed hex character
               (store_and, reg2, ":val", 0xF),  # swy: isolate the bottom/right-most 4-bits into its own temporary register; then pick the right character
               (try_begin), (ge, reg2, 0xF), (str_store_string, s2, "@{!}F{s2}"), # swy: the {!} {***} get deleted by the formatter
               ( else_try), (ge, reg2, 0xE), (str_store_string, s2, "@{!}E{s2}"), #            at runtime; they are there to signal
               ( else_try), (ge, reg2, 0xD), (str_store_string, s2, "@{!}D{s2}"), #                a debug or untranslatable string.
               ( else_try), (ge, reg2, 0xC), (str_store_string, s2, "@{!}C{s2}"),
               ( else_try), (ge, reg2, 0xB), (str_store_string, s2, "@{!}B{s2}"),
               ( else_try), (ge, reg2, 0xA), (str_store_string, s2, "@{!}A{s2}"),
               ( else_try),                  (str_store_string, s2, "@{!}{reg2}{s2}"), # swy: 0-9, decimal number
               (  end_try),
               (val_rshift, ":val", 4),   # swy: the four lowest bits of the hex character we just read get removed,
#              (display_message,"@{s2}"), #      like a string of cans rolling down a vending machine
               (str_store_string_reg, ":string_reg", s2),
           (end_try),
       (end_try),
#      (display_message,"@{s2}00000000000000000000000000000000"),
       
    (else_try),
      (display_message, "@{!}swy: error: the script-face-keys-set-extended-morph-key parameter is outside the supported 0-42 range.", 0x289128),
    (end_try)
]),

Python:
# 0000000ff20011456b1a20a72efac68814e5df58d1053977000000000000000a     e.g. funky face code for illustrative purposes :)
# 0000000ff2001145................                                  <- with this trick we can only parse into a number blocks A
# ................6b1a20a72efac688                                     and B. Where B holds the 0-20 morph key data.
#                                 14e5df58d1053977················  <- blocks C/D are inaccessible, but only C
#                                 ················000000000000000a     holds useful data; the 21+ morph keys.
# [    block a   ][    block b   ][    block c   ][    block d   ]
#
# (face_keys_set_morph_key, <string_no>, <key_no>, <value>),
("face_keys_set_extended_morph_key", [
    (store_script_param_1, ":string_reg"), (str_store_string_reg, s1, ":string_reg"),
    (store_script_param_2, ":key_no"    ),
    (store_script_param,   ":value",   3),

    (try_begin),
       (is_between, ":key_no", 0, 7 +1),
       # --
       (face_keys_set_morph_key, s1, ":key_no", ":value"),
       (str_store_string_reg, ":string_reg", s1), # swy: fix some weird string corruption when using the string register in a variable across calls
    (else_try),
       (is_between, ":key_no", 8, 20 +1),
       # --
       # swy: /part one/: parse the hexadecimal face code string into blocks of numbers using the
       #                  same trickery as in the "face_keys_get_extended_morph_key" script
       (try_for_range, ":cur_block", 0, 3 +1),
         (assign, ":shift", 0),
         (assign, reg2, 0),
         (try_for_range, ":i", 0, (7 + 3) +1), # swy: go over the 0-7 range, when :i is 8/9/10 it will get keys 0/1/2 again from a shifted position
             (try_begin),                      #      to get the remainding 8 bits, plus    2 [2211 1000 7776 6655][5444 3332 2211 1000]
                (eq, ":i", 8),                 #      an extra/unused one.                     ^^^^ ^^^^ <----------------------------< (each get_morph_key gets us blocks of three bits, as we go leftwards)
                (str_store_string, s1, "@FFFFFF{s1}"), # swy: pad out/shift to the right the equivalent to 24-bits/6 hex characters we've already read
             (end_try),
             (store_mod, ":i_clamped", ":i", 8), # swy: after doing 0-7 and reaching index 8, make it loop back to read 0-3 on the displaced data
             (face_keys_get_morph_key, reg0, s1, ":i_clamped"), # swy: with each of these calls we grab 3 bits at a time
             (val_lshift, reg0, ":shift"),
             (val_or, reg2, reg0),
             (val_add, ":shift", 3),
         (end_try),
         (str_store_string, s1, "@FF{s1}"), # swy: pad out/shift to the right the remaining 8-bits/2 hex characters we've read; that's 32 bits in total
         (val_and, reg2, 0xFFFFFFFF), # swy: discard any possible bits outside of the 32-bit range
    
         (try_begin), (eq, ":cur_block", 3), (assign, ":block_a_hi", reg2), # swy: store it in the correct block,
         ( else_try), (eq, ":cur_block", 2), (assign, ":block_a_lo", reg2), #      e.g. "block_b" works like this:
         ( else_try), (eq, ":cur_block", 1), (assign, ":block_b_hi", reg2), # 6b1a20a7 | 6b1a20a7........ <-   high/top half
         ( else_try), (eq, ":cur_block", 0), (assign, ":block_b_lo", reg2), # 2efac688 | ........2efac688 <- low/bottom half
         (end_try),                                                         #            6b1a20a72efac688 <- original/full 64-bit block
       (end_try),
  
       # swy: /part two/: replace the bits of the selected morph key with the new value, now that it's in numeric form
  
       # swy: build the bitmask and shift the value to cover it; e.g. for a ":key_no" = 10 that straddles two halves and ":value" = 5 we get
       #      00000000000000000000000000000001|11000000000000000000000000000000 bitmask:   0b111 << (10*3)
       #      11111111111111111111111111111110|00111111111111111111111111111111 inversed mask to AND against and clear the three off bits
       #      00000000000000000000000000000001 01000000000000000000000000000000 value: 5 = 0b101 << (10*3)
       #                                     ^ ^^
       (store_mul, ":bit_offset", ":key_no", 3),
       (assign, ":mask", 0b111),
       (val_lshift, ":mask",  ":bit_offset"),
       (val_lshift, ":value", ":bit_offset"),
  
       # swy: there's no ~ (bitwise not) operator, so invert the bits of the mask manually
#      (assign, ":mask_inv", 0),
#      (assign, ":i_bit", 1),
#      (try_for_range, ":i", 0, 64 +1),
#          (store_and, ":cur_mask_bit", ":mask", ":i_bit"),
#          (try_begin),
#            (eq, ":cur_mask_bit", 0), # swy: the current bit is zero? put a one, otherwise we assume it was cleared to zero before getting here and we leave it alone
#            (val_or, ":mask_inv", ":i_bit"),
#          (try_end),
#          (val_lshift, ":i_bit", 1),
#      (end_try),

       # swy: this does the same ~ (bitwise not) inversion, but with less operations (e.g. 0b0000'1110 is 14, and 0b1111'0001 is -15), so we do -(thing + 1)
       #      thanks to the registers being treated as signed 64-bit integers and thanks to sign extension we get the same: https://en.wikipedia.org/wiki/Two%27s_complement / https://en.wikipedia.org/wiki/Sign_extension
       (store_add, ":mask_inv", ":mask", 1),
       (val_mul,   ":mask_inv",         -1),
  
       # --
  
       # swy: apply the mask to clear these three bits to zero (with DATA &= ~MASK) and then replace them with our own (with DATA |= VALUE)
       (val_and, ":block_b_lo", ":mask_inv"),
       (val_or,  ":block_b_lo", ":value"),

       # swy: after doing it with the bottom 32-bit half, then do the same thing on the top 32-bit half
       (val_rshift, ":mask_inv", 32),
       (val_rshift, ":value",    32),
       (val_and, ":block_b_hi", ":mask_inv"),
       (val_or,  ":block_b_hi", ":value"),

       # swy: /part three/: recreate the hexadecimal string from the A/B blocks, as we can't access blocks C/D and the game doesn't care about trailing stuff
       #                    we don't really need to append "00000000000000000000000000000000" at the end to replace the inaccesible half that we couldn't parse/convert from hex string.
       #                    NOTE: you can add back the last half of the original face code (if you can and it's known/fixed/hardcoded) to retain the rest of morph key values
       #                    instead of losing them, or have good defaults instead of being all-zeros and reset it from slider 21 onwards.
       (str_clear, s2),
       (try_for_range, ":cur_block", 0, 3 +1),
           (try_begin), (eq, ":cur_block", 3), (assign, ":val", ":block_a_hi"), # swy: first convert the 32-bit bottom and then the top 32-bit half
           ( else_try), (eq, ":cur_block", 2), (assign, ":val", ":block_a_lo"), #      (of each of the two A/B 64-bit blocks).
           ( else_try), (eq, ":cur_block", 1), (assign, ":val", ":block_b_hi"), #
           ( else_try), (eq, ":cur_block", 0), (assign, ":val", ":block_b_lo"), # i.e. type them from right to left
           (  end_try),
      
           (try_for_range, ":unused", 0, 7 +1), # swy: from index 0 to 7 = 8 means there are 32 hex digits / 4 bits per visual/printed hex character
               (store_and, reg2, ":val", 0xF),  # swy: isolate the bottom/right-most 4-bits into its own temporary register; then pick the right character
               (try_begin), (ge, reg2, 0xF), (str_store_string, s2, "@{!}F{s2}"), # swy: the {!} {***} get deleted by the formatter
               ( else_try), (ge, reg2, 0xE), (str_store_string, s2, "@{!}E{s2}"), #            at runtime; they are there to signal
               ( else_try), (ge, reg2, 0xD), (str_store_string, s2, "@{!}D{s2}"), #                a debug or untranslatable string.
               ( else_try), (ge, reg2, 0xC), (str_store_string, s2, "@{!}C{s2}"),
               ( else_try), (ge, reg2, 0xB), (str_store_string, s2, "@{!}B{s2}"),
               ( else_try), (ge, reg2, 0xA), (str_store_string, s2, "@{!}A{s2}"),
               ( else_try),                  (str_store_string, s2, "@{!}{reg2}{s2}"), # swy: 0-9, decimal number
               (  end_try),
               (val_rshift, ":val", 4),   # swy: the four lowest bits of the hex character we just read get removed,
#              (display_message,"@{s2}"), #      like a string of cans rolling down a vending machine
               (str_store_string_reg, ":string_reg", s2),
           (end_try),
       (end_try),
#      (display_message,"@{s2}00000000000000000000000000000000"),
  
    (else_try),
      (display_message, "@{!}swy: error: the script-face-keys-set-extended-morph-key parameter is outside the supported 0-20 range.", 0x289128),
    (end_try)
]),
 
Last edited:
Back
Top Bottom