OSP Code QoL [WB] Dynamic Villages and Scene Props Iterator

Users who are viewing this thread

Lav

Sergeant Knight at Arms
This code is intended to do one thing: provide module authors with the ability to quickly process scene props at the beginning of a mission, removing some of them from view, or doing something else but equally wicked to them poor propsies.

My original motivator was to make villages more dynamic. So you could build a Messenger Post in the village, then go to village center and voila - there's a Messenger Post standing right there! The main idea was to use variation ID numbers, which are not used by the game anywhere except in the main tutorial. However even when I implemented the very first version of the code, it was already much greater than that, and allowed for pretty generic manipulation of objects on any mission.

Below are two version of the code. Both are tested and confirmed to work. The first code sample explains the basic idea of how to make dynamic scene editing available to everyone, however it's recommended to use the second code sample as it's much more versatile and powerful.

This code allows you to delete marked scene props at the mission start, so the user will not see them.

Insert this anywhere:
Code:
situational_scene_var_id = 127 ## When a scene prop has this number as first variation ID, it is subject to situational removal, all other scene props are ignored

Insert this into any mission template where you want conditional object removal (assuming you need mission-based control over objects, otherwise just don't use these lines and remove corresponding check in module_scene_props.py):
Code:
      (ti_before_mission_start, 0, 0, [], [(assign, "$g_scene_requires_objects_removal", 1)]),
      (ti_after_mission_start, 0, 0, [], [(assign, "$g_scene_requires_objects_removal", 0)]),

Insert this anywhere, preferably at the end of the file:
Code:
  # script_scene_prop_needs_removal
  # Input: scene_prop_id, variation_id
  # Output: reg0 should contain 1 or 0 (needs removal or not)
  # This is a dummy script which always tells to never remove anything.
  # Replace this with actual code according to your needs.
  ("scene_prop_needs_removal",
    [
      #(store_script_param_1, ":scene_prop_instance_id"),
      #(prop_instance_get_scene_prop_kind, ":scene_prop_id", ":scene_prop_instance_id"), ## For checks on scene_prop type etc
      (assign, reg0, 0),
    ]
  ),

Insert this at the very end of the file:
Code:
from ID_scripts import *
append_to_props_list = (ti_on_scene_prop_init,
    [
      (try_begin),
        (eq, "$g_scene_requires_objects_removal", 1), # If scene doesn't want situational control over scene props, we exit immediately
        (store_trigger_param_1, ":scene_prop_instance_id"),
        (prop_instance_get_variation_id, ":var_id", ":scene_prop_instance_id"),
        (eq, ":var_id", situational_scene_var_id), ## We check that first variation ID matches the one defined in constants as situational flag
        (call_script, "script_scene_prop_needs_removal", ":scene_prop_instance_id"),
        (eq, reg0, 1),
        (prop_instance_get_position, pos1, ":scene_prop_instance_id"),
        (position_set_z, pos1, -1000),
        (prop_instance_set_position, ":scene_prop_instance_id", pos1),
      (try_end),
    ]
  )

for key in range(len(scene_props)):
  scene_props[key][4].append(append_to_props_list)

Quick guide for scene editors.

1. Put 127 as the variation ID #1 to make the prop subject to situational removal.

2. Put conditional code (supplied by module scripters) as the variation ID #2.

3. Voila, when the condition defined by the conditional code is met, the prop will not appear on the scene.

4. All you need is the list of conditional codes, which will look like follows:

Code:
{1} Village has blacksmith
{2} Village does not have blacksmith
{3} Village has mill
{4} Village does not have mill
(etc)

This code creates the list of all scene_props on the scene and allows user to iterate through them at will. It can be easily adapted to replicate the effects of the previous code, but much more powerful as it allows you to iterate through scene props at any time during the mission, not only at the beginning. It also does not use variation ID numbers as flags, leaving it to the module author to decide how to use variation IDs and whether to use them at all.

Code:
########################################################################################################################
# Scene props iterator code starts here
# These lines should better be placed last in the file, but BEFORE the closing bracket.
########################################################################################################################
  ["scene_props_list","scene_props_list","scene_props_list", tf_hero, 0, 0, fac_commoners, [], def_attrib|level(1), wp(20), knows_common, 0],
########################################################################################################################
# Scene props iterator code ends here
########################################################################################################################

Code:
########################################################################################################################
# Scene props iterator code starts here
# These lines should be placed LAST in the file, AFTER the closing bracket!!!
########################################################################################################################
from ID_scripts import *
append_to_props_list = (ti_on_scene_prop_init,
    [
      (store_trigger_param_1, ":scene_prop_instance_id"),
      (call_script, "script_scene_props_iterator_append", ":scene_prop_instance_id"),
    ]
  )

for key in range(len(scene_props)):
  scene_props[key][4].append(append_to_props_list)
########################################################################################################################
# Scene props iterator code ends here
########################################################################################################################

Code:
########################################################################################################################
# Scene props iterator code starts here
# These triggers should be added to all mission templates where you want to use scene prop iterator
########################################################################################################################
  (ti_before_mission_start, 0, 0, [], [(call_script, "script_scene_props_iterator_init")]),
  (ti_after_mission_start,  0, 0, [], [(call_script, "script_scene_props_iterator_finalize")]),
  # ALTERNATIVELY you could use the following
  # (ti_before_mission_start, 0, 0, [], [(call_script, "script_scene_props_iterator_init")]),
  # (ti_after_mission_start,  0, 0, [], [(call_script, "script_scene_props_iterator", "script_my_callback_script")]),
########################################################################################################################
# Scene props iterator code ends here
########################################################################################################################

Code:
########################################################################################################################
# Scene props iterator code starts here
########################################################################################################################

  # script_scene_props_iterator_init
  # Input: none
  # Output: none
  ("scene_props_iterator_init",
    [
      (assign, "$g_sp_iterator_count", 0),
      (assign, "$g_sp_iterator_collecting", 1),
    ]
  ),

  # script_scene_props_iterator_finalize
  # Input: none
  # Output: none
  ("scene_props_iterator_finalize",
    [
      (assign, "$g_sp_iterator_collecting", 0),
    ]
  ),

  # script_scene_props_iterator_append
  # Input: scene_prop_instance_id
  # Output: none
  ("scene_props_iterator_append",
    [
      (try_begin),
        (eq, "$g_sp_iterator_collecting", 1),
        (store_script_param_1, ":scene_prop_instance_id"),
        # reg0 version:
        #(call_script, "script_scene_props_iterator_filter", ":scene_prop_instance_id"),
        #(eq, reg0, 1),
        # cf version:
        (call_script, "script_cf_scene_props_iterator_filter", ":scene_prop_instance_id"),
        (troop_set_slot, "trp_scene_props_list", "$g_sp_iterator_count", ":scene_prop_instance_id"),
        (val_add, "$g_sp_iterator_count", 1),
      (try_end),
    ]
  ),

  # script_scene_props_iterator_filter
  # Input: scene_prop_instance_id
  # Output: reg0
  # Note: assign reg0 to zero to prevent scene prop from being appended to iterator list
  ("scene_props_iterator_filter",
    [
      #(store_script_param_1, ":scene_prop_instance_id"),
      (assign, reg0, 1),
    ]
  ),

  # script_cf_scene_props_iterator_filter
  # Input: scene_prop_instance_id
  # Output: none
  # Note: return fail to prevent scene prop from being appended to iterator list
  ("cf_scene_props_iterator_filter",
    [
      #(store_script_param_1, ":scene_prop_instance_id"),
    ]
  ),

  # script_scene_props_iterator
  # Input: callback script (should accept scene_prop_instance_id as parameter)
  # Output: none
  ("scene_props_iterator",
    [
      (call_script, "script_scene_props_iterator_finalize"),
      (store_script_param_1, ":callback_script"),
      (try_begin),
        (gt, ":callback_script", 0),
        (try_for_range_backwards, ":iterator", 0, "$g_sp_iterator_count"),
          (troop_get_slot, ":scene_prop_instance_id", "trp_scene_props_list", ":iterator"),
          (call_script, ":callback_script", ":scene_prop_instance_id"),
        (try_end),
      (try_end),
    ]
  ),

  # script_delete_scene_prop_instance
  # Input: scene_prop_instance_id
  # Output: none
  # Note: This will hide the prop and remove it from iterator list.
  #       Use this to prevent "deleted" props from being processed again and again.
  # Performance note: THIS IS NOT OPTIMIZED TO BE USED FROM WITHIN ITERATOR!
  ("delete_scene_prop_instance",
    [
      (store_script_param_1, ":instance_to_delete"),
      (assign, ":index", -1),
      (try_for_range, ":iterator", 0, "$g_sp_iterator_count"),
        (troop_get_slot, ":found_value", "trp_scene_props_list", ":iterator"),
        (eq, ":instance_to_delete", ":found_value"),
        (assign, ":index", ":iterator"),
      (try_end),
      (try_begin),
        # Have we found the requested scene prop instance?
        (ge, ":index", 0),
        # First we hide it from user's view
        (prop_instance_get_position, pos1, ":instance_to_delete"),
        (position_set_z, pos1, -1000),
        (prop_instance_set_position, ":instance_to_delete", pos1),
        # Now we remove it from the iterator list
        (try_for_range, ":iterator", ":index", "$g_sp_iterator_count"),
          (store_add, ":take_from", ":iterator", 1),
          (troop_get_slot, ":value", "trp_scene_props_list", ":take_from"),
          (troop_set_slot, "trp_scene_props_list", ":iterator", ":value"),
        (try_end),
        # And reduce iterator list size by 1
        (val_sub, "$g_sp_iterator_count", 1),
      (try_end),
    ]
  ),

########################################################################################################################
# Scene props iterator code ends here
########################################################################################################################

My sincere thanks to:

MadocComadrin for the idea to use ti_on_scene_prop_init triggers, which eradicated the performance problem.
Caba`drin and Vorrne for advice and suggestions.

This is a small script which does just one thing.

It enables you to easily mark objects on any scene for removal on certain conditions.

For example, take a village scene. Mark the mill, it's fan and related objects for removal when there's no mill built in the village. Add a check for that flag in the special script and compile your module. Now whenever you enter the village, for each marked object the script will be called to determine whether this object should be removed or not.

From player's perspective, this looks very easy: when there's no mill built in the village, there's no mill on the scene. When the mill is built, it immediately appears on the village scene.

Marking objects for removal is done with variation ID numbers. There are two for each scene prop, either can be in the range of 0..127. Setting first variation number to 127 (this number can be redefined if necessary) will make the scene prop subject to situational removal. Second variation number will be passed as parameter to the separate script determining whether or not this object should be removed.

Code:
from ID_scene_props import *
scene_props_start = spr_chest_a ## This is for Native, redefine if you have other scene props defined or want to limit scene props type available for situational removal
scene_props_end = spr_oil_press_interior + 1 ## This is for Native, redefine if you have other scene props defined or want to limit scene props type available for situational removal
situational_scene_var_id = 127 ## When a scene prop has this number as first variation ID, it is subject to situational removal, all other scene props are ignored

Code:
# Insert this into "village_center" mission template among other mission template triggers
      (ti_before_mission_start, 0, 0, [], [(call_script, "script_remove_scene_objects_situational")]),

Code:
  # script_remove_scene_objects_situational
  # Input: none
  # Output: none
  ("remove_scene_objects_situational",
    [
      (try_for_range, ":scene_prop_id", scene_props_start, scene_props_end), ## We iterate through all defined scene prop types
        (scene_prop_get_num_instances, ":prop_count", ":scene_prop_id"),
        (try_begin),
          (gt, ":prop_count", 0),
          (try_for_range_backwards, ":scene_prop_instance_no", 0, ":prop_count"), ## For each scene prop type, we iterate through all it's instances on current scene
            (scene_prop_get_instance, ":scene_prop_instance_id", ":scene_prop_id", ":scene_prop_instance_no"),
            (try_begin),
              (prop_instance_is_valid, ":scene_prop_instance_id"), ## Not sure what this does, but just in case
              (prop_instance_get_variation_id, ":var_id", ":scene_prop_instance_id"),
              (eq, ":var_id", situational_scene_var_id), ## We check that first variation ID matches the one defined in constants as situational flag
              (prop_instance_get_variation_id_2, ":var_id", ":scene_prop_instance_id"),
              (call_script, "script_scene_prop_needs_removal", ":scene_prop_id", ":var_id"), ## Separate script which determines whether the prop should be removed - see below
              (eq, reg0, 1),
              (replace_prop_instance, ":scene_prop_instance_id", spr_empty),
            (try_end),
          (try_end),
        (try_end),
      (try_end),
    ]
  ),

  # script_scene_prop_needs_removal
  # Input: scene_prop_id, variation_id
  # Output: reg0 should contain 1 or 0 (needs removal or not)
  # This is a dummy script which always tells script_remove_scene_objects_situational to never remove anything.
  # Replace this with actual code according to your module's needs.
  ("scene_prop_needs_removal",
    [
      (store_script_param_1, ":scene_prop_id"),
      (store_script_param_2, ":variation_id"),
      (assign, reg0, 0),
    ]
  ),

Pro. Making dynamic objects on the scenes is as easy as easy can get, and absolutely generic.

Con. Unfortunately, "absolutely generic" also means "freakin' slow when loading the scene".
 
Of course. Insert trigger into appropriate mission template and you are good to go.

Though honestly, I would buy my Warband a second time if only Taleworlds implemented try_for_scene_props.

That would be SO. MUCH. FASTER.
 
I supose using a troop array  (with slots =0/1 as the mark for removal and slot number equal to scene prop id ) and iterating through this array with acting only on slot=1 will spare you the necessity of getting instances for the props you dont want to remove.

Setting 1 and 0 should be done elsewhere ofc
 
Sayd Ûthman said:
You use a try_for_range scene_props_begin,scene_props_end instead :grin:
Makes sense, thanks. Fixed the script.

GetAssista said:
I supose using a troop array  (with slots =0/1 as the mark for removal and slot number equal to scene prop id ) and iterating through this array with acting only on slot=1 will spare you the necessity of getting instances for the props you dont want to remove.

Setting 1 and 0 should be done elsewhere ofc
That's another way to do things, but this way you cannot delete some instances of a scene prop, you will be deleting all of them. And I don't expect it to be much faster, as you still have to iterate through entire scene_props array.
 
Lav said:
That's another way to do things, but this way you cannot delete some instances of a scene prop, you will be deleting all of them. And I don't expect it to be much faster, as you still have to iterate through entire scene_props array.
Oh, nothing an additional array (that holds e.g. bitmask of instances you want to delete) can't fix.

And key to speed is not number of iterations, but how fast unnecessary iterations are skipped. In your script, you need script_scene_prop_needs_removal called as fast as possible. But since you need those instances for the script, you are left with the necessity of checking each and every prop
 
GetAssista said:
Oh, nothing an additional array (that holds e.g. bitmask of instances you want to delete) can't fix.

And key to speed is not number of iterations, but how fast unnecessary iterations are skipped. In your script, you need script_scene_prop_needs_removal called as fast as possible. But since you need those instances for the script, you are left with the necessity of checking each and every prop
Can you write your own version of the code? Because if I understand you correctly, there is a nasty side-effect with your approach - scene and related script (bitmask) become a single entity, and a person editing the scene no longer can make any modifications without being forced to make corresponding modifications to the code as well.
 
Lav said:
Can you write your own version of the code?
Nope, sorry, busy with other things
Lav said:
Because if I understand you correctly, there is a nasty side-effect with your approach - scene and related script (bitmask) become a single entity, and a person editing the scene no longer can make any modifications without being forced to make corresponding modifications to the code as well.
Not if he uses variation IDs as powers of 2, and bitmask directly reflects them (I assume that one has no restrictions on var id he can assign to a prop
 
GetAssista said:
I assume that one has no restrictions on var id he can assign to a prop
Don't. You are limited to 0..127 range. Just seven bits.

And yeah, now that I understand your idea - no, it wouldn't be faster at all even if it was possible. Bitmask or not, but you must iterate through all scene props to remove marked ones, and there's no way to speed this up but to reduce the range of scene prop types checked for removal (that is, playing with scene_props_start and scene_props_end constants).
 
Lav said:
And yeah, now that I understand your idea - no, it wouldn't be faster at all even if it was possible. Bitmask or not, but you must iterate through all scene props to remove marked ones
Yes, it would be faster. I would iterate through slots in an array, not through actual props. It would spend time only on actually replacing props.

7 variations seem fine for the vast majority of practical purposes, or you can use 6 variations and remaining bit to indicate mass removal (all props of that kind)
 
Ideally, the fastest way (run-time) would be to have each prop store it's instance id at initialization time into a dummy troop array; then manipulate them by iterating over the array. (however, I'm not 100% sure if the init trigger is called early enough that you can use them in conjunction with the proper mt triggers to remove the props, can I get a solid yes/no on this?).

The only setback is that each scene prop would need to have that trigger added, but you can mange that easily with a little python at the end of the file.
 
MadocComadrin said:
Ideally, the fastest way (run-time) would be to have each prop store it's instance id at initialization time into a dummy troop array; then manipulate them by iterating over the array. (however, I'm not 100% sure if the init trigger is called early enough that you can use them in conjunction with the proper mt triggers to remove the props, can I get a solid yes/no on this?).

The only setback is that each scene prop would need to have that trigger added, but you can mange that easily with a little python at the end of the file.
Now that's an idea!

There are before_mission_start and after_mission_start triggers for mission templates, and scene_prop_init for scene props. I have edited all three to send messages and confirmed that after_mission_start fires AFTER all of scene_prop_inits. I assume before_mission_start fires first, though cannot confirm at the moment.

So I changed the code completely. Now, there's a before_mission_start trigger which clears the dummy troop array, scene_prop_init trigger on every scene_prop which checks it's variation ID and adds it to the dummy troop array if necessary, and finally after_mission_start trigger which iterates through dummy troop array, checking all scene prop instances and deleting them where appropriate.

Except it didn't work. :sad:

Installing the check and removal code directly into scene_prop_init trigger didn't work as well.

Oh well, seems some exhaustive testing and debugging is planned for tomorrow.
 
As cmpxchg8b notes, replace_prop_instance only works prior to mission start...so perhaps use another operation?
Might you, instead, reduce the prop's size/scale to 0 or move it underground to "remove it" in order to use the temp array and significantly cut the iterations?

Also, see this exchange:
Vornne said:
Patta said:
Does "replace_prop_instance" work in Multiplayer? Atm I use "prop_instance_set_visibility", what seems to work, and then i move it away. But that does not clear the collision it seems, so i need to find something to make the prop have no collision any more...
It only works before the scene loads, in ti_[before|after]_mission_start. The only way I know of to clear the collision object is to make it destroyable and destroy it - you could try using prop_instance_receive_damage, but I think it was designed in a strange way, needing to be called on client and server to stay in sync (or something) and not calling ti_on_scene_prop_hit or ti_on_scene_prop_destroy, so I haven't tried using it.

Ahh wait: you mean you moved the visible mesh away, but the collision object stayed in place? That means you need to add sokf_moveable to the scene prop flags, and then make sure you only move it on the server (otherwise clients will disconnect with a sync error).

Additionally, I wonder if moving the "causal arrow" the opposite direction may be an option? I.E., only spawn a given prop if the situation demands it. If common locations could be found in scenes, this would remove the necessity for a scene builder to edit every village scene to include a "mill" prop, but rather spawn them totally code-side. I'm not sure if this is possible, however.
 
Caba`drin said:
Additionally, I wonder if moving the "causal arrow" the opposite direction may be an option? I.E., only spawn a given prop if the situation demands it. If common locations could be found in scenes, this would remove the necessity for a scene builder to edit every village scene to include a "mill" prop, but rather spawn them totally code-side. I'm not sure if this is possible, however.
How would you set positions and scales for all those props... Scene editing is required either way, no workaround. Unless you want to design obscenely complex scene analysis algo, that would deside where to put props so that they look ok ingame.

Edit: well, depends on props ofc, e.g. spawning wooden stakes with skulls around village elder as a village coolness factor is pretty simple w/o scene editing :smile:
 
cmpxchg8b said:
replace_prop_instance needs to be called in ti_before_mission_start.
Yup, I already found this out.

Setting scale to zero crashed the game.

Setting position to 1 km below ground... worked!

(sigh) Allright, since I couldn't go to sleep anyway without finishing this first, I'll probably make the final version now.
 
Back
Top Bottom