XML:BINA/OBJC/TRGV: Difference between revisions

From OniGalore
< XML:BINA‎ | OBJC
Jump to navigation Jump to search
m (on further thought, we should be consistent in directing modders to the AE .dats)
m (using Image: consistently to make it easier to find all image refs on a page)
 
(17 intermediate revisions by 4 users not shown)
Line 1: Line 1:
{{XML_OBJC_Header | type=TRGV | prev=TRIG | next=SNDG | name=Trigger Volumes (TVs) }}
{{XML_OBJC_Header | prev=PWRU | type=TRGV | next=TRIG | name=Trigger Volume}}


===general information===
{{TOClimit|3}}
* '''BINACJBOTrigger Volume.oni''' is level specific. (It can be found in AE/AEInstaller/vanilla/level'''X'''_Final.dat)
* The xml code on this page is based on onisplit '''v0.9.61.0'''


===file structure===
==General information==
{|width=200px border=0 cellspacing=20 cellpadding=0 style="float:right"
* "BINACJBOTrigger Volume" is level specific (level''x''_Final.dat).
|[http://i305.photobucket.com/albums/nn207/unknownfuture/Oni_Galore_Images/XML_modding/trigger_volume.jpg http://i305.photobucket.com/albums/nn207/unknownfuture/Oni_Galore_Images/XML_modding/trigger_volume_preview.jpg]
* The XML on this page is based on OniSplit '''v0.9.61.0'''.
 
==BSL support==
'''Show trigger activity'''
:<tt>debug_triggers = 1</tt> shows trigger volume (TV) events and which character enters, stays inside or leaves.
 
'''Resetting the trigger volume'''
:<tt>trigvolume_reset (string tv_name)</tt> resets the TV to its original state. The primary use of this is to re-enable "entry", "inside" or "exit" BSL calls after the TV has been triggered (only necessary if the respective trigger-only-once flags are set, of course). Note that some or all the TV functions are disabled at level load, so you'll have to enable them manually after resetting the TV.


The red and blue lines were added to the screenshot in a graphics program, not by debug_triggers.
'''Removing corpses'''
:<tt>trigvolume_corpse (integer tv_id)</tt> removes all corpses inside the specified trigger volume.


If you have an updated Oni Mac engine, the TVs can be made visible.
==File structure==
{|width=200px border=0 cellspacing=20 cellpadding=0 style="float:right"
|[[Image:Trigger_volume.jpg|200px]]


Used BSL commands:
The red and blue lines were added to the screenshot in a graphics program, not by debug_triggers. But you can now display them with Ctrl-Shift-X if you are using the [[Daodan DLL]] or the Intel Mac build of Oni.
* debug_triggers = 1
* chr_nocollision 0 1
* chr_debug_characters = 1
* chr_location 0 ''x y z''
|}
|}


Line 27: Line 31:
  </Oni>
  </Oni>


'''''[...]''''' means at least one trigger volume. Paste all tv data into there (this includes '''<font color="#0A0"><TRGV Id="..."></font>''' and '''<font color="#0A0"></TRGV></font>''' tag).
'''''[...]''''' means at least one trigger volume. Paste all your TV data in there (this includes the '''<font color="#0A0"><TRGV Id="..."></font>'''/'''<font color="#0A0"></TRGV></font>''' tag).


'''example'''
'''example'''
Line 57: Line 61:
  - 633.4166 + 46  = -587.4166 = z_blue_line_end
  - 633.4166 + 46  = -587.4166 = z_blue_line_end


==Tags==
{| class="wikitable" width="100%"
!width=280px| Tag
!width=60px| Type
! Description
|-
| <TRGV Id="...">
| integer
| You can use <TRGV>s without the ID parameter.
|-
| <Header>
| -
|
|-
| <Flags>
| flag
| Development relic, unused.
|-
| <Position>
| float x3
| TV is spawned at this XYZ position.
|-
| <Rotation>
| float x3
| TV has this XYZ rotation (in degrees).
|-
| <OSD>
| -
|
|-
| <Name>
| string
| Can have up to 63 characters.
|-
| <Scripts>
| -
| BSL functions to call for each event (below); each function name can have up to 32 characters.
|-
| <Entry>
| string
| Called when character enters the TV.
|-
|valign="top"| <Inside>
|valign="top"| string
| Called while character is inside the TV.
This one is triggered every frame (60 times per second) unless the trigger-only-once flag is set. Continuous triggering is typically needed for damage by fire or poison gas.
|-
| <Exit>
| string
| Called when character leaves the TV.
|-
|valign="top"| <Teams>
|valign="top"| flags
|
: 1 - Konoko
: 2 - TCTF
: 4 - Syndicate
: 8 - Neutral
: 16 - SecurityGuard
: 32 - RogueKonoko
: 64 - Switzerland
: 128 - SyndicateAccessory
Every combination is possible. E.g.:
: 255 - all teams (1 + 2 + 4 + 8 + 16 + 32 + 64 + 128 = 255)
|-
| <Size>
| float x3
| Volume of TV
|-
| <TriggerVolumeId>
| integer
| Used by BSL command <tt>trigvolume_corpse ''ID''</tt>
|-
| <ParentId>
| integer
| Not used?
|-
| <Notes>
| string
| Space for a note up to 128 characters long.
|-
|valign="top"| <Flags>
|valign="top"| flags
|
: OneTimeEnter - entry function called only once (otherwise called every time character enters the TV)
: OneTimeInside - inside function called only once (otherwise called every frame while character is in the TV)
: OneTimeExit - exit function called only once (otherwise called every time character leaves the TV)
: EnterDisabled - entry function disabled (can be enabled with "trigvolume_enable ''tv_name'' entry 1")
: InsideDisabled - inside function disabled (can be enabled with "trigvolume_enable ''tv_name'' inside 1")
: ExitDisabled - exit function disabled (can be enabled with "trigvolume_enable ''tv_name'' exit 1")
: Disabled - all 3 functions disabled (master switch, can be enabled with "trigvolume_enable ''tv_name'' all 1")
: PlayerOnly - only player characters can fire off the TV
|}
==BSL bugs and workarounds==
===String comparison bug===
As you've seen, trigger volumes can call BSL functions. Such a function can receive a string variable which contains the name of the character that triggered the trigger volume.
Unfortunately it seems there's a bug in BSL when comparing this string parameter with another string variable. Even if the trigger volume string and the other string are identical, the comparison always returns false.
To reproduce the bug, open "warehouse_level_scripts.bsl" in IGMD/EnvWarehouse/ and replace the '''t65''' function with this version (make a backup of the file first):
    func void t65(string ai_name)
    {
        var string testVar = "char_0";
       
        dmsg(testVar);
        dmsg("char_0");
        dmsg(ai_name);
       
        sleep(100);
       
        if (testVar eq "char_0")
        {
            dmsg("Regular string comparison works!");
        }
        else
        {
            dmsg("Regular string comparison failed!");
        }
       
        if (ai_name eq "char_0")
        {
            dmsg("Trigger volume string comparison worked!");
        }
        else
        {
            dmsg("Trigger volume string comparison failed!");
        }
       
        dprint t65
        if (d6 eq 0)
        {
            message xdoorislocked
            dprint door_is_locked
        }
    }
Load savepoint 1 in Chapter 1 and move the character up to the first closed door. This function will be triggered. What is the output that you expect? "Trigger volume string comparison worked!", since Konoko is char_0? Wrong! It will print "Trigger volume string comparison failed!" even though both string variables have the same value.
[[Image:Trigger_volume_string_comparison_bug.jpg|300px]]
(Update: Apparently, as detailed by [[User:Loser|Loser]] [[BSL:String_comparison|HERE]], strings are compared by pointer and not by content.)
====Possible workarounds====
If you need to detect a few characters and all of them are on different teams, you can clone the trigger volume one time for each character and call different functions, one for each team. This way you'll know which character triggered the trigger volume, since each team can have a unique trigger volume function thanks to the <Teams> flag.
On Windows, you can take advantage of the [[Daodan DLL]]'s custom function <tt>d_getindex(string ai_name)</tt> and compare by ID.
{{divhide|&nbsp;Example: fire detection in Airport level (remove existing IGMD\Airport\*.bsl files and use a custom main.bsl with the following content)|align=left}}
func void fire_damage(string ai_name)
{
var int index = d_getindex(ai_name);
var int index0 = d_getindex(char_0);
var int index1 = d_getindex(LoadingBay_Thug_1);
var int index2 = d_getindex(LoadingBay_Thug_2);
if (index eq index0)
dmsg "Konoko is burning!"
if (index eq index1)
dmsg "Thug 1 is burning!"
if (index eq index2)
dmsg "Thug 2 is burning!"
}
func void main(void)
{
ai2_spawn LoadingBay_Thug_1
ai2_spawn LoadingBay_Thug_2
chr_location 0 134 -105 1029
}
{{divhide|end}}
===chr_poison bug===
chr_poison is a BSL function which is used in the original game to damage characters continuously within a specific interval. For example it is used for fires (e.g. in Chapter 4) and for poison gas (Chapter 5). A trigger volume's <Inside> tag is used to call a function every frame which contains chr_poison. It works as expected, except if the trigger volume is triggered too many times, in which case you'll see the message "FATAL SCRIPTING ERROR: exceeded maximum scripting parameter base size of 32768" in the developer console and the function will stop working.
====Possible workarounds====
Instead of using <Inside> to call the damage function, use <Entry> to call an intermediate function — let's call it "trigger_damage". "trigger_damage" will call another function called "damage_character", which will be responsible for damaging the character every frame. We also need to know when the player leaves the TV so we can stop damaging him, so in the trigger volume XML file we'll also use <Exit> to call the function "trigger_stop_damage".
To know which character left the damaging TV, we will give an LSI to him upon entry and then remove it when he leaves the trigger volume, and we'll only damage him when he has the LSI. (You can use another technique here if you don't want to use an LSI; for instance you can use the multiple TV approach that was explained above as a string comparison workaround.)
The final BSL code will look like this:
    # This function now does the work of damaging the character each frame
    # Once the character loses his LSI, it stops damaging him
    func void damage_character(string ai){
        if (chr_has_lsi(ai))
        {
            chr_poison(ai, 5, 10);
            sleep(1);
            fork damage_character(ai);
        }
    }
    func void trigger_damage(string ai)
    {
        chr_givepowerup(ai, "lsi");
        fork damage_character(ai);
    }
    func void trigger_stop_damage(string ai)
    {
        chr_inv_reset(ai); # clear LSI
    }
And the trigger volume XML will look like this:
    <TRGV Id="1">
        <Header>
            <Flags>0</Flags>
            <Position>2005.23 1239.87 -3628.82</Position>
            <Rotation>0 0 0</Rotation>
        </Header>
        <OSD>
            <Name>BurnCharacter</Name>
            <Scripts>
                <Entry>trigger_damage</Entry>
                <Inside />
                <Exit>trigger_stop_damage</Exit>
            </Scripts>
            <Teams>4</Teams>
            <Size>-287.04 1 -179.85</Size>
            <TriggerVolumeId>1</TriggerVolumeId>
            <ParentId>0</ParentId>
            <Notes></Notes>
            <Flags />
        </OSD>
    </TRGV>
If the technique above is not enough and you are still experiencing the "FATAL SCRIPTING ERROR: exceeded maximum scripting parameter base size of 32768" error after a while, you may try using duplicated trigger volumes, where after a while (and before the error appears) you disable the first one and activate the second one (which is a copy of the first).
The duplicated one can look like this:
    <TRGV Id="2">
        <Header>
            <Flags>0</Flags>
            <Position>2005.23 1239.87 -3628.82</Position>
            <Rotation>0 0 0</Rotation>
        </Header>
        <OSD>
            <Name>BurnCharacterPart2</Name>
            <Scripts>
                <Entry>trigger_damage</Entry>
                <Inside />
                <Exit>trigger_stop_damage</Exit>
            </Scripts>
            <Teams>4</Teams>
            <Size>-287.04 1 -179.85</Size>
            <TriggerVolumeId>2</TriggerVolumeId>
            <ParentId>0</ParentId>
            <Notes></Notes>
            <Flags />
        </OSD>
    </TRGV>
The code to switch between them:
    trigvolume_enable("BurnCharacter", 0); # disable the first one
    trigvolume_enable("BurnCharacterPart2", 1); # activate the second one


===tags===
Note that you should have only one of the copies active at a time.
* <Flags>
:: Locked
* <Position> : (a volume corner)
* <Rotation> : (xyz-rataion in degrees)
* <Name> : (can have up to 63 characters)
* <Scripts> : (BSL functions; each can have up to 32 characters)
:: <Entry> : (called up when character enters the TV)
:: <Inside> : (called while character is inside the TV)
::: (This one is triggered at every frame (60 times per second) unless the trigger-only-once flag is set. Continuous triggering is typically needed for fire or gas damage.)
:: <Exit> : (called up when character leaves the TV)
* <Teams>
:: 1 - Konoko
:: 2 - TCTF
:: 4 - Syndicate
:: 8 - Neutral
:: 16 - SecurityGuard
:: 32 - RogueKonoko
:: 64 - Switzerland
:: 128 - SyndicateAccessory
:: (every combination is possible)
:: 255 - all teams (1 + 2 + 4 + 8 + 16 + 32 + 64 + 128 = 255)
* <Size> : (volume)
* <TriggerVolumeId> : (used by BSL command "trigvolume_corpse ''ID''")
* <ParentId> : ...
* <Notes> : (space for 128 characters)
* <Flags>
:: OneTimeEnter - entry function called only once (otherwise called every time character enters the TV)
:: OneTimeInside - inside function called only once (otherwise called at every frame while character is in the TV)
:: OneTimeExit - exit function called only once (otherwise called every time character leaves the TV)
:: EnterDisabled - entry function disabled (can be enabled with "trigvolume_enable ''tv_name'' entry 1")
:: InsideDisabled - inside function disabled (can be enabled with "trigvolume_enable ''tv_name'' inside 1")
:: ExitDisabled - exit function disabled (can be enabled with "trigvolume_enable ''tv_name'' exit 1")
:: Disabled - all 3 functions disabled (master switch, can be enabled with "trigvolume_enable ''tv_name'' all 1")
:: PlayerOnly - only player characters can fire off the TV


==Mod Tool addon==
[[Image:xsi_addon_trigger_volume_manager.jpg|thumb|200px|right|Trigger volume manager.]]


'''Resetting the trigger volume'''
With [[Mod_Tool/OniTools_addon|OniTools.xsiaddon]] you can drag'n'drop BINACJBOTrigger Volume'''.oni''' into the viewport. Add or remove TVs, and edit their position and rotation as if they were real objects. Change their size by moving the points. When ready, export the data as BINACJBOTrigger Volume.xml to a folder of your choice.
:'''trigvolume_reset(string tv_name)''' resets the TV to its preset state. The primary use of this is to re-enable "entry", "inside" or "exit" calls once the TV has been triggered (only necessary if the respective trigger-only-once flags are set of course). Note that some or all the TV functions are disabled at level load, you'll have to enable them manually after resetting the TV.


{{XML}}
{{XML}}

Latest revision as of 17:38, 21 February 2022

TRGV : Trigger Volume
XML modding tips
  • See HERE to start learning about XML modding.
  • See HERE if you are searching for information on how to handle object coordinates.
  • See HERE for some typical modding errors and their causes.
XML.png
XML

AKEV << Other file types >> CONS

TMBD << Other BINA >> ONIE

PWRU << Other OBJC >> TRIG

switch to OBD page

General information

  • "BINACJBOTrigger Volume" is level specific (levelx_Final.dat).
  • The XML on this page is based on OniSplit v0.9.61.0.

BSL support

Show trigger activity

debug_triggers = 1 shows trigger volume (TV) events and which character enters, stays inside or leaves.

Resetting the trigger volume

trigvolume_reset (string tv_name) resets the TV to its original state. The primary use of this is to re-enable "entry", "inside" or "exit" BSL calls after the TV has been triggered (only necessary if the respective trigger-only-once flags are set, of course). Note that some or all the TV functions are disabled at level load, so you'll have to enable them manually after resetting the TV.

Removing corpses

trigvolume_corpse (integer tv_id) removes all corpses inside the specified trigger volume.

File structure

Trigger volume.jpg

The red and blue lines were added to the screenshot in a graphics program, not by debug_triggers. But you can now display them with Ctrl-Shift-X if you are using the Daodan DLL or the Intel Mac build of Oni.

<?xml version="1.0" encoding="utf-8"?>
<Oni>
   <Objects>
       [...]
   </Objects>
</Oni>

[...] means at least one trigger volume. Paste all your TV data in there (this includes the <TRGV Id="...">/</TRGV> tag).

example

       <TRGV Id="7383">
           <Header>
               <Flags>Locked</Flags>
               <Position>-651.5666 -298 -633.4166</Position>
               <Rotation>0 0 0</Rotation>
           </Header>
           <OSD>
               <Name>trigger_volume_46</Name>
               <Scripts>
                   <Entry>train_block2</Entry>
                   <Inside></Inside>
                   <Exit>block_path</Exit>
               </Scripts>
               <Teams>255</Teams>
               <Size>46 31 46</Size>
               <TriggerVolumeId>46</TriggerVolumeId>
               <ParentId>0</ParentId>
               <Notes></Notes>
               <Flags>PlayerOnly</Flags>
           </OSD>
       </TRGV>
start      + size = end
- 651.5666 + 46   = -605.5666 = x_blue_line_end
- 298      + 31   = -267      = y_blue_line_end
- 633.4166 + 46   = -587.4166 = z_blue_line_end

Tags

Tag Type Description
<TRGV Id="..."> integer You can use <TRGV>s without the ID parameter.
<Header> -
<Flags> flag Development relic, unused.
<Position> float x3 TV is spawned at this XYZ position.
<Rotation> float x3 TV has this XYZ rotation (in degrees).
<OSD> -
<Name> string Can have up to 63 characters.
<Scripts> - BSL functions to call for each event (below); each function name can have up to 32 characters.
<Entry> string Called when character enters the TV.
<Inside> string Called while character is inside the TV.

This one is triggered every frame (60 times per second) unless the trigger-only-once flag is set. Continuous triggering is typically needed for damage by fire or poison gas.

<Exit> string Called when character leaves the TV.
<Teams> flags
1 - Konoko
2 - TCTF
4 - Syndicate
8 - Neutral
16 - SecurityGuard
32 - RogueKonoko
64 - Switzerland
128 - SyndicateAccessory

Every combination is possible. E.g.:

255 - all teams (1 + 2 + 4 + 8 + 16 + 32 + 64 + 128 = 255)
<Size> float x3 Volume of TV
<TriggerVolumeId> integer Used by BSL command trigvolume_corpse ID
<ParentId> integer Not used?
<Notes> string Space for a note up to 128 characters long.
<Flags> flags
OneTimeEnter - entry function called only once (otherwise called every time character enters the TV)
OneTimeInside - inside function called only once (otherwise called every frame while character is in the TV)
OneTimeExit - exit function called only once (otherwise called every time character leaves the TV)
EnterDisabled - entry function disabled (can be enabled with "trigvolume_enable tv_name entry 1")
InsideDisabled - inside function disabled (can be enabled with "trigvolume_enable tv_name inside 1")
ExitDisabled - exit function disabled (can be enabled with "trigvolume_enable tv_name exit 1")
Disabled - all 3 functions disabled (master switch, can be enabled with "trigvolume_enable tv_name all 1")
PlayerOnly - only player characters can fire off the TV

BSL bugs and workarounds

String comparison bug

As you've seen, trigger volumes can call BSL functions. Such a function can receive a string variable which contains the name of the character that triggered the trigger volume.

Unfortunately it seems there's a bug in BSL when comparing this string parameter with another string variable. Even if the trigger volume string and the other string are identical, the comparison always returns false.

To reproduce the bug, open "warehouse_level_scripts.bsl" in IGMD/EnvWarehouse/ and replace the t65 function with this version (make a backup of the file first):

   func void t65(string ai_name)
   {
       var string testVar = "char_0";
       
       dmsg(testVar);
       dmsg("char_0");
       dmsg(ai_name);
       
       sleep(100);
       
       if (testVar eq "char_0")
       {
           dmsg("Regular string comparison works!");
       }
       else
       {
           dmsg("Regular string comparison failed!");
       }
       
       if (ai_name eq "char_0")
       {
           dmsg("Trigger volume string comparison worked!");
       }
       else
       {
           dmsg("Trigger volume string comparison failed!");
       }
       
       dprint t65
       if (d6 eq 0)
       {
           message xdoorislocked
           dprint door_is_locked
       }
   }

Load savepoint 1 in Chapter 1 and move the character up to the first closed door. This function will be triggered. What is the output that you expect? "Trigger volume string comparison worked!", since Konoko is char_0? Wrong! It will print "Trigger volume string comparison failed!" even though both string variables have the same value.

Trigger volume string comparison bug.jpg

(Update: Apparently, as detailed by Loser HERE, strings are compared by pointer and not by content.)

Possible workarounds

If you need to detect a few characters and all of them are on different teams, you can clone the trigger volume one time for each character and call different functions, one for each team. This way you'll know which character triggered the trigger volume, since each team can have a unique trigger volume function thanks to the <Teams> flag.

On Windows, you can take advantage of the Daodan DLL's custom function d_getindex(string ai_name) and compare by ID.

chr_poison bug

chr_poison is a BSL function which is used in the original game to damage characters continuously within a specific interval. For example it is used for fires (e.g. in Chapter 4) and for poison gas (Chapter 5). A trigger volume's <Inside> tag is used to call a function every frame which contains chr_poison. It works as expected, except if the trigger volume is triggered too many times, in which case you'll see the message "FATAL SCRIPTING ERROR: exceeded maximum scripting parameter base size of 32768" in the developer console and the function will stop working.

Possible workarounds

Instead of using <Inside> to call the damage function, use <Entry> to call an intermediate function — let's call it "trigger_damage". "trigger_damage" will call another function called "damage_character", which will be responsible for damaging the character every frame. We also need to know when the player leaves the TV so we can stop damaging him, so in the trigger volume XML file we'll also use <Exit> to call the function "trigger_stop_damage".

To know which character left the damaging TV, we will give an LSI to him upon entry and then remove it when he leaves the trigger volume, and we'll only damage him when he has the LSI. (You can use another technique here if you don't want to use an LSI; for instance you can use the multiple TV approach that was explained above as a string comparison workaround.)

The final BSL code will look like this:

   # This function now does the work of damaging the character each frame
   # Once the character loses his LSI, it stops damaging him
   func void damage_character(string ai){
       if (chr_has_lsi(ai))
       {
           chr_poison(ai, 5, 10);
           sleep(1);
           fork damage_character(ai);
       }
   }
   func void trigger_damage(string ai)
   {
       chr_givepowerup(ai, "lsi");
       fork damage_character(ai);
   }
   func void trigger_stop_damage(string ai)
   {
       chr_inv_reset(ai); # clear LSI
   }

And the trigger volume XML will look like this:

   <TRGV Id="1">
       <Header>
           <Flags>0</Flags>
           <Position>2005.23 1239.87 -3628.82</Position>
           <Rotation>0 0 0</Rotation>
       </Header>
       <OSD>
           <Name>BurnCharacter</Name>
           <Scripts>
               <Entry>trigger_damage</Entry>
               <Inside />
               <Exit>trigger_stop_damage</Exit>
           </Scripts>
           <Teams>4</Teams>
           <Size>-287.04 1 -179.85</Size>
           <TriggerVolumeId>1</TriggerVolumeId>
           <ParentId>0</ParentId>
           <Notes></Notes>
           <Flags />
       </OSD>
   </TRGV>

If the technique above is not enough and you are still experiencing the "FATAL SCRIPTING ERROR: exceeded maximum scripting parameter base size of 32768" error after a while, you may try using duplicated trigger volumes, where after a while (and before the error appears) you disable the first one and activate the second one (which is a copy of the first).

The duplicated one can look like this:

   <TRGV Id="2">
       <Header>
           <Flags>0</Flags>
           <Position>2005.23 1239.87 -3628.82</Position>
           <Rotation>0 0 0</Rotation>
       </Header>
       <OSD>
           <Name>BurnCharacterPart2</Name>
           <Scripts>
               <Entry>trigger_damage</Entry>
               <Inside />
               <Exit>trigger_stop_damage</Exit>
           </Scripts>
           <Teams>4</Teams>
           <Size>-287.04 1 -179.85</Size>
           <TriggerVolumeId>2</TriggerVolumeId>
           <ParentId>0</ParentId>
           <Notes></Notes>
           <Flags />
       </OSD>
   </TRGV>

The code to switch between them:

   trigvolume_enable("BurnCharacter", 0); # disable the first one
   trigvolume_enable("BurnCharacterPart2", 1); # activate the second one

Note that you should have only one of the copies active at a time.

Mod Tool addon

Trigger volume manager.

With OniTools.xsiaddon you can drag'n'drop BINACJBOTrigger Volume.oni into the viewport. Add or remove TVs, and edit their position and rotation as if they were real objects. Change their size by moving the points. When ready, export the data as BINACJBOTrigger Volume.xml to a folder of your choice.