www.tombraiderforums.com

Go Back   www.tombraiderforums.com > Tomb Raider Modding > Tomb Raider Level Editor > Tomb Raider Level Editor Tutorials

Closed Thread
 
Thread Tools
Old 03-11-17, 17:39   #11
AkyV
Moderator
 
Join Date: Dec 2011
Location: Hungary
Posts: 2,574
Default

10. Exporting triggers into the plugin

Now we work effectively in the plugin, at the first time. My advices is feel free to work in this Plugin_trng.cpp now. That doesn’t make troubles if you want to also use it later in the way as Paolone tells it in here. That is why we will follow my advice now.

Let’s start talking about the trigger codes, so you can create your very first Flipeffect trigger code.

As I said above, executable trigger codes are single events, like: “execute this in the moment when Lara steps on this PAD trigger”. – Later I will tell how you can start continuous events without putting the trigger in a GlobalTrigger or in a “continuous” TriggerGroup.

We need cbFlipEffectMine callback to type our flipeffect codes there. So find for the chapter of this function.
At the time being this callback is very short. But we don’t care about the most of this now, look at only this part:

Code:
  switch (FlipIndex) {
  // here type the "case Number:" for each flipeffect number. At end of the code you'll use the "break;" instruction to signal the code ending
  // Note: when you'll add your first "case Number:" then you can remove the following "case -1: and break;" instructions
  case -1:
  break;
Which means, we will remove case -1/break now, as Paolone says, and we will use later the proper codes for our triggers, in that switch sequence:

switch
case 500: //F500 code
some code
break;
case 501: //F501 code
some code
break;
case 502: //F502 code
some code
break;
etc.
default: //if a trigger of TRG doesn’t have a code in this sequence
some code (i.e. an error message for the log, done by default)
break;

First, let’s also remove our first F500 trigger in the TRG file, which was only for fun, and make the structure of our real F500:

;------------------ Section for flipeffect trigger: description of the trigger -------------------------------
<START_TRIGGERWHAT_9_O_H>
500: Sign Lara’s actual <&> vitality with a short (E) colored flash on the screen
<END>

;type here the sections for arguments used by above flipeffects
<START_EFFECT_500_T_H>
0: 1-20 %
1: 21-40 %
2: 41-60 %
3: 61-80 %
4: 81-100 %
<END>

<START_EFFECT_500_E_H>
0: Red
1: Green
<END>

So this is what will happen in the moment when somebody/something activates this trigger:
For example, if the trigger is this:

; Set Trigger Type - FLIPEFFECT 500
; Exporting: TRIGGER(257:0) for FLIPEFFECT(500) {Plugin_trng}
; <#> : Sign Lara’s actual <&> vitality with a short (E) colored flash on the screen
; <&> : 21-40 %
; (E) : Green

then you will see a short green flash on the screen, when the trigger is activated, if Lara’s health is between 21 and 40 %.

This is very easy now, because this time we use only existing trng.dll triggers for the code:
- C29 for Lara’s vitality, and
- F355 for colored flashes.

How does it work?
Well, you need the “Export Function” button of the Set Trigger Type panel, both for C29 and F355.
The button name is “Export Function” because Action, Flipeffect or Condition triggers exported into the plugin work as functions in the plugin:

PerformActionTrigger
PerformFlipeffect
PerformConditionTrigger

(Their chapters are all in trng.cpp source file, and you can call them in the other functions of Plugin_trng.cpp.)

As the first step, you should find out the code theoretically. It looks like this now:
- if Timer=0 (you chose 1-20 %) and Extra=0 (you chose Red) then perform a short red effect if the vitality is 1-20 %
- if Timer=0 (you chose 1-20 %) and Extra=1 (you chose Green) then perform a short green effect if the vitality is 1-20 %
- if Timer=1 (you chose 21-40 %) and Extra=0 (you chose Red) then perform a short red effect if the vitality is 21-40 %
- if Timer=1 (you chose 21-40 %) and Extra=1 (you chose Green) then perform a short green effect if the vitality is 21-40 %
- if Timer=2 (you chose 41-60 %) and Extra=0 (you chose Red) then perform a short red effect if the vitality is 21-40 %
- if Timer=2 (you chose 41-60 %) and Extra=1 (you chose Green) then perform a short green effect if the vitality is 41-60 %
- if Timer=3 (you chose 61-80 %) and Extra=0 (you chose Red) then perform a short red effect if the vitality is 41-60 %
- if Timer=3 (you chose 61-80 %) and Extra=1 (you chose Green) then perform a short green effect if the vitality is 41-60 %
- if Timer=4 (you chose 81-100 %) and Extra=0 (you chose Red) then perform a short red effect if the vitality is 41-60 %
- if Timer=4 (you chose 81-100 %) and Extra=1 (you chose Green) then perform a short green effect if the vitality is 41-60 %

This time you will call PerformFlipeffect functions for different F355 triggers and PerformConditionTrigger functions for different C29 triggers. All of them will be called in cbFlipEffectMine callback, for the code of F500.

So we need to export these triggers into the plugin (in the main brackets: the code value you get if you click on “Export Function” button):
- C29: “higher than 0”, for >=1% condition (PerformConditionTrigger(NULL, 29, 0, 1); )
- C29: “less than 201”, for <=20% condition (PerformConditionTrigger(NULL, 29, 201, 2); )
- C29: “higher than 200”, for >=21% condition (PerformConditionTrigger(NULL, 29, 200, 1); )
- C29: “less than 401”, for <=40% condition (PerformConditionTrigger(NULL, 29, 401, 2); )
- C29: “higher than 400”, for >=41% condition (PerformConditionTrigger(NULL, 29, 400, 1); )
- C29: “less than 601”, for <=60% condition (PerformConditionTrigger(NULL, 29, 601, 2); )
- C29: “higher than 600”, for >=61% condition (PerformConditionTrigger(NULL, 29, 600, 1); )
- C29: “less than 801”, for <=80% condition (PerformConditionTrigger(NULL, 29, 801, 2); )
- C29: “higher than 800”, for >=81% condition (PerformConditionTrigger(NULL, 29, 800, 1); )
- F355: “fast red light” (PerformFlipeffect(NULL, 355, 0, 10); )
- F355: “fast green light” (PerformFlipeffect(NULL, 355, 4, 10); )

You don’t need to type anything in PerformConditionTrigger/PerformFlipeffect chapters as contents. I mean, these exported functions have constant chapter contents. When you hit Export Function button for PerformConditionTrigger(NULL, 29, 0, 1), PerformFlipeffect(NULL, 355, 0, 10) etc., getting the actual calling values of the function, then you get the exact parameters for the Condition, Flipeffect triggers you want, without any need of editing those chapters. All you need is to copy/paste the calls (calling values) in the F500 trigger code.

The trigger code, replacing case -1/break, is this, for F500:

Code:
  case 500:
  //flash for vitality
        if (Extra == 0) { //for red flash
              if ((Timer == 0 && PerformConditionTrigger(NULL, 29, 0, 1) &&
                    PerformConditionTrigger(NULL, 29, 201, 2)) || //1-20 %
                    (Timer == 1 && PerformConditionTrigger(NULL, 29, 200, 1) &&
                    PerformConditionTrigger(NULL, 29, 401, 2)) || //21-40 %
                    (Timer == 2 && PerformConditionTrigger(NULL, 29, 400, 1) &&
                    PerformConditionTrigger(NULL, 29, 601, 2)) || //41-60 %
                    (Timer == 3 && PerformConditionTrigger(NULL, 29, 600, 1) &&
                    PerformConditionTrigger(NULL, 29, 801, 2)) || //61-80 %
                    (Timer == 4 && PerformConditionTrigger(NULL, 29, 800, 1))) { //81-100 %
                    PerformFlipeffect(NULL, 355, 0, 10); //red flash
              }
        }
        if (Extra == 1) { //for green flash
              if ((Timer == 0 && PerformConditionTrigger(NULL, 29, 0, 1) &&
                    PerformConditionTrigger(NULL, 29, 201, 2)) || //1-20 %
                    (Timer == 1 && PerformConditionTrigger(NULL, 29, 200, 1) &&
                    PerformConditionTrigger(NULL, 29, 401, 2)) || //21-40 %
                    (Timer == 2 && PerformConditionTrigger(NULL, 29, 400, 1) &&
                    PerformConditionTrigger(NULL, 29, 601, 2)) || //41-60 %
                    (Timer == 3 && PerformConditionTrigger(NULL, 29, 600, 1) &&
                    PerformConditionTrigger(NULL, 29, 801, 2)) || //61-80 %
                    (Timer == 4 && PerformConditionTrigger(NULL, 29, 800, 1))) { //81-100 %
                    PerformFlipeffect(NULL, 355, 4, 10); //green flash
              }
        }
  break;
So this is what the code tells:

If Extra is 0 (so if you want a red light)...
Then perform it if Timer is 0 (so you want it between 1-20%), plus health is >= 1% and <= 20%.
Or perform it if Timer is 1 (so you want it between 21-40%), plus health is >= 21% and <= 40%.
Or perform it if Timer is 2 (so you want it between 41-60%), plus health is >= 41% and <= 60%.
Or perform it if Timer is 3 (so you want it between 61-80%), plus health is >= 61% and <= 80%.
Or perform it if Timer is 4 (so you want it between 81-100%), plus health is >= 81%.
If Extra is 1 (so if you want a green light)...
Then perform it if Timer is 0 (so you want it between 1-20%), plus health is >= 1% and <= 20%.
Or perform it if Timer is 1 (so you want it between 21-40%), plus health is >= 21% and <= 40%.
Or perform it if Timer is 2 (so you want it between 41-60%), plus health is >= 41% and <= 60%.
Or perform it if Timer is 3 (so you want it between 61-80%), plus health is >= 61% and <= 80%.
Or perform it if Timer is 4 (so you want it between 81-100%), plus health is >= 81%.

Naturally you could organize the code in other ways, for example something like this:

Code:
  case 500:
        //flash for vitality    
        if (Extra == 0 && Timer == 0) { //for red flash
              if (PerformConditionTrigger(NULL, 29, 0, 1) &&
                    PerformConditionTrigger(NULL, 29, 201, 2)) PerformFlipeffect(NULL, 355, 0, 10); //1-20 %
              }
        }
        if (Extra == 0 && Timer == 1) { //for red flash
              if (PerformConditionTrigger(NULL, 29, 200, 1) &&
                    PerformConditionTrigger(NULL, 29, 401, 2)) PerformFlipeffect(NULL, 355, 0, 10); //21-40 %
              }
        }
Notes:
- If you forgot the meaning of a trigger exported into the plugin, then select its code again (like: “PerformConditionTrigger(NULL, 29, 401, 2)”), and copy/paste it in the Find Trigger Number box of the Set Trigger Type panel.
- The exported condition triggers cannot be examined as numbers, and it is true not only for conditions like “if Lara is holding the torch”, but even for conditions where it is about numbers, like “if Lara’s health is less than 401”.
I mean, typing “PerformConditionTrigger(NULL, 29, 401, 2) < 401” is meaningless, that “<401” examination is already done in the “29, 401, 2” code.
That is why a “PerformConditionTrigger(NULL, 29, 401, 2) != ...” examination is also meaningless if you want to examine if the condition is NOT true. Instead of that, use this form:

PerformConditionTrigger(NULL, 29, 401, 2) == false

“If Lara’s health is NOT less than 401”.
- But what is that “NULL” in the exported values?
Well, that means the trigger is from the “basic” (“NULL”) DLL, trng.dll.
You can try that what if you want to export this F500 from Level Editor, as a function of the actual plugin, eg. when it is a red flash (Extra=0) for 21-40% (Timer=1). This is the value you get:

PerformFlipeffect("Plugin_trng", 500, 1, 0);

So the plugin name in quotes says it is F500/Extra=0/Timer=1 for Plugin_trng.dll, not trng.dll.
And yes, it works even now, if your plugin is only a WIP one, not a plugin installed.
If you have a plugin of NOT yours installed, then you can also export one of its triggers into your code. (In this case naturally you should tell the level builder in a readme later to install this other plugin as well when he tries your plugin, or else your code will not work.)
- So if you don’t use preset arguments or #REPEAT# tags, then you need to define the parameter ID’s yourself. As 0, 1, 2, 3 and 4 ID’s in this F500 Timer parameter are defined by me. There are no rules, you can choose any numbers for them, but casual 0, 1, 2, 3 etc. or 1, 2, 3, 4 etc. sequences are the most logical usually.
But special sequences are recommended sometimes. For example, you need these Timer box values:

0: 25%
1: 50%
2: 75%

You want to turn some feature value in the trigger code to the required percent:

if (Timer == 0) Feature = Feature * 25 / 100;
if (Timer == 1) Feature = Feature * 50 / 100;
if (Timer == 2) Feature = Feature * 75 / 100;

But how about this easier solution instead of that:

25: 25%
50: 50%
75: 75%

Feature = Feature * Timer / 100;

But be careful with the ID ranges for that box. (I told about them above.)
- We need to clarify that constants can be replaced with variables (probably) in all the cases.
Let’s see for example this:
You have a #CD_TRACK_LIST# preset argument in the Flipeffect Timer box, which means you will see this in that box:

#ID DESCRIPTION
--------------------------------------------------------------------------------
0: AUDIO\000
1: AUDIO\001
2: AUDIO\002
3: AUDIO\003
4: AUDIO\004
5: AUDIO\005
6: AUDIO\006
7: AUDIO\007

So Timer ID0 for track0, Timer ID1 for track1 etc.

You would like to “export function” of F129 (“Sound. (CD) Play <&>CD track in (E) way on channel2”) into the code, for single playing of the track chosen.
Try to export the trigger eg. for Track#100:

PerformFlipeffect(NULL, 129, 100, 0);

But how can we modify it so it will play any track, I mean, any track that you chose in Timer box?
Well, you can see where Track#100 placed in the exported function code, so the track ID is stored there. If Timer=100, then 100 is placed there, so if Timer=101, then 101 is placed there etc. – which means the exported function looks this way to play any track chosen in Timer:

PerformFlipeffect(NULL, 129, Timer, 0);

(Yes, the Timer value of the trigger is also a variable in the code. You can understand it in Chapter 12.)
- You may ask: “you exported Condition codes into a Flipeffect code. Why”?
Don’t be afraid, I did not mistake. Any exported trigger (Action, Flipeffect, Condition) works (i.e can be called) successfully in any trigger (Action, Flipeffect, Condition) code.
- If you use “Export Function” for Action/Condition triggers of Moveable objects, static/flyby cameras, sinks, then you can realize this:

; Set Trigger Type - ACTION 43
; Exporting: TRIGGER(43:0) for ACTION(152) {Tomb_NextGeneration}
; <#> : ANIMATING2 ID 152 in sector (8,6) of First Room
; <&> : Trigger. (Moveable) Activate <#>Object with (E)Timer value
; (E) : Timer= +00

PerformActionTrigger(NULL, 43, 152 | NGLE_INDEX, 0);

I mean, the code for ID152 item is not clearly 152 but 152 | NGLE_INDEX. It says “152, what you can see, is an NGLE index”.
This means the index in the editor (NGLE index) and the index in the game (tomb index) are not the same.

You can understand the difference easily, seeing this list:

Index slot 0: NGLE Index 0 – Tomb Index 0
Index slot 1: NGLE Index 1 – Tomb Index 1
Index slot 2: empty
Index slot 3: empty
Index slot 4: empty
Index slot 5: empty
Index slot 6: NGLE Index 6 – Tomb Index 2

You will always get the NGLE index of an object automatically if you place an object in the map. This is the ID of the index slot – this is the ID of the object you can read easily in the map.
On the other hand, tomb indices always have a continuous range in a 0, 1, 2, 3 etc. sequence. For example, if I place an object now that gets NGLE ID4 automatically, then tomb ID 2 goes to this object, and object with NGLE ID 6, which has had tomb ID 2 so far, gets a new tomb ID (3):

Index slot 0: NGLE Index 0 – Tomb Index 0
Index slot 1: NGLE Index 1 – Tomb Index 1
Index slot 2: empty
Index slot 3: empty
Index slot 4: NGLE Index 4 – Tomb Index 2
Index slot 5: empty
Index slot 6: NGLE Index 6 – Tomb Index 3

Only tomb ID’s are useable in the code, but don’t be afraid, that “| NGLE_INDEX” warning will execute the NGLE/tomb conversion this time automatically, you don’t need to do it yourself. (I will tell more about these conversions later.)
- You are allowed to use custom switch sequences. It is recommended when you’d like something easier instead of long and boring chain of “if” conditions.
For example:

if (A == 1) do X thing;
if (A == 2) do Y thing;
if (A == 3) do Z thing;

Or:

switch (A) {
case 1:
do X thing;
break;
case 2:
do Y thing;
break;
case 3:
do Z thing;
break;
default: // optional
some code if A is not 1, 2 or 3
break;
}

Last edited by AkyV; 04-11-17 at 11:58.
AkyV is offline  
Old 03-11-17, 17:40   #12
AkyV
Moderator
 
Join Date: Dec 2011
Location: Hungary
Posts: 2,574
Default

11. Trying your triggers

Now click on F7 to compile your plugin, and copy the DLL into the editor folder, to update it, so we can try this F500 in the game.
Then organize a little WAD, TGA, project and script to create a little test level, and place this trigger somewhere in the map. Also place enemies, traps or flipeffects that affect Lara’s health, so you can reduce the health to try those 1-20%, 21-40% etc. cases.
Convert the level, and enjoy your very first trigger in the game!

Exporting triggers into the script:

Before you try to export this F500 into the Script, you need a Plugin Script command in the [Options] section for Plugin_trng plugin.
This is a command to control this plugin properly in the script. I don’t want to tell the details here (see NG Center/Reference/Script New Commands/Plugin for them), I mention only the main reason now:
Plugins used by your game need to be identified in your game. One plugin has ID1, the another one ID2, the third one ID3 etc. Let’s suppose this plugin has ID1 now. (If you install a new plugin, then that will automatically get its Plugin command, with the lowest free ID, with the most default parameter values. But this time it is not an installed, released plugin, only a WIP plugin, so you need to create this command manually.)
You can see the result easily if you hit the Export Script Trigger button of Set Trigger Type panel for this F500, eg. when Timer=1 (21-40%) and Extra=1 (green):

; Set Trigger Type - FLIPEFFECT 500
; Exporting: TRIGGER(257:0) for FLIPEFFECT(500) {Plugin_trng}
; <#> : Sign Lara’s actual <&> vitality with a short (E) colored flash on the screen
; <&> : 21-40 %
; (E) : Green
; WARNING:
; If you'll give this trigger to other people or you'll use it with a script.txt different than current
; you should add to the script, first of triggergroup where you'll use this trigger, the following line:
; #define @Plugin_trng 1
; Values to add in script command: $012000, 500, $101

F500 of trng.dll, with Timer=1 and Extra=1 would have a shorter first number, without the plugin ID (01) part:

$2000, 500, $101

Naturally F500 of plugin having ID2, with Timer=1 and Extra=1 would be this:

$022000, 500, $101

Though, it is not important for you now, because we are talking about NOT released plugins now, have a look at Paolone’s warning message. He also talks about this in that NG Center description of Plugin command, but you can read more about it in the NG Center description of #define directive:
If you add some scripted code to another person, having any plugin triggers, then perhaps that plugin in his game has another ID than in yours, which naturally must be solved.
So perhaps if he installs your Plugin_trng plugin, but with another plugin ID, then the $012000, 500, $101 you send him, will be useless for him.
So you also need to warn him that he needs the proper #define directive in his script.

Exporting triggers as AnimCommands:

Export AnimCommand button of Set Trigger Type panel is useless for plugin triggers.
But the solution is easy:
Export this trigger into the script, typing it in a TriggerGroup. Then you will use Export AnimCommand button for the trng.dll trigger (eg. F118) that activates that TriggerGroup.
AkyV is offline  
Old 03-11-17, 17:52   #13
AkyV
Moderator
 
Join Date: Dec 2011
Location: Hungary
Posts: 2,574
Default

12. The input and output data of functions

The input data is one (or more) value(s) that is needed for the function to calculate its result.
The output data is the result of the function.

Let’s see for example this custom function:

Name: LightColor
Purpose: Convert three (red, green, blue) values into only one RGB color value.
Input Data: BYTE (0-255) Red variable, BYTE (0-255) Green variable, BYTE (0-255) Blue variable.
Output Data: DWORD (because RGB color positive values could be pretty long).

So the chapter of this function will look this way:

Code:
  DWORD LightColor(BYTE Red, BYTE Green, BYTE Blue)
  {
  
        the code to convert Red and Green and Blue into a redgreenblue value;
  
  }
The DWORD size must be told now, in this name of the function, for the output data – a bit later I will tell what that data is. (Much later I will show a possible code for this function.)

Probably you do not clearly understand: why do we need a function for this? I mean, if you make a trigger code eg. to force a room color, then you would like to type the code of the conversion in the trigger code. Emphasizing it in an independent function seems meaningless.
Well, the benefit of all the custom functions (and even of some pre-made sub-functions as well) is that you can re-use their contents in other codes easily, you don’t need to type those contents million times, all you need is to call those contents. For example, this color conversion code can be called in cbFlipEffectMine function, in these two trigger codes:

Code:
  case 601:
        //force room ambient color
        {
        BYTE RedRoom;
        BYTE GreenRoom;
        BYTE BlueRoom;
  
        RedRoom = ...;
        GreenRoom = ...;
        BlueRoom = ...;
  
        LightColor(RedRoom, GreenRoom, BlueRoom);
        some code to force the output data of LightColor function for the required room;
  
        }
  break;
  case 602:
        //force Static object color
        {
        BYTE RedObject;
        BYTE GreenObject;
        BYTE BlueObject;
  
        RedObject = ...;
        GreenObject = ...;
        BlueObject = ...;
  
        LightColor(RedObject, GreenObject, BlueObject);
        some code to force the output data of LightColor function for the required object;
  
        }
  break;
The red/green/blue values, as you can see, get their values in the trigger this time. (We won’t discuss it now how.)

What you also need to be realized:
- The “temporary” input variables of LightColor function (BYTE Red, BYTE Green, BYTE Blue) are swapped for the own variables of the room color code (RedRoom, GreenRoom, BlueRoom) or the object color code (RedObject, GreenObject, BlueObject).
So the color conversion will be executed for the colors of the room (in the case of F601, with the values stored in RedRoom, GreenRoom and BlueRoom variables) or the colors of the object (in the the case of F602, with the values stored in RedObject, GreenObject and BlueObject variables). (So eg. when the function says generally that “convert Red, Green and Blue variable values into the output data”, then F601 will read it this way: “convert RedRoom, GreenRoom and BlueRoom variable values into the output data”.)
- The size and the name of the “temporary” variables are declared in the function name. But when you call the function, the “real” variables are declared inside the function chapter, before (above) the call (eg. BYTE RedRoom). When you call the function, then the “real” variables will be referred only with their names (eg. RedRoom), without their size.
A variable size can be told only once, when you declare that variable!
- The vertical command order is naturally still important. I mean, first you need to declare the variable (eg. BYTE RedRoom), before you do any operation with it (eg. setting its value, or calling it in a function).
- RedRoom, GreenRoom and BlueRoom belong only to F601 and RedObject, GreenObject and BlueObject belong only to F602. Which means F601 and F602 has their own variable declarations. That is why we need {} brackets this time for the whole trigger code, to tell where the declared things belong to.
“Temporary” variables are declared in the function name, so in their case it is clear where they belong to.
(F500 above had no declarations, so I didn’t use {} brackets for the whole trigger code, only for parts.)

Note:
Thanks to those {} brackets, RedRoom, GreenRoom and BlueRoom belong only to F601 and RedObject, GreenObject and BlueObject belong only to F602. That is why theoretically we could use the same names for them (eg. RedColor, GreenColor, BlueColor), because it is clear that they are part of different trigger codes:

Code:
  case 601:
        //room color
        {
        BYTE RedColor;
        BYTE GreenColor;
        BYTE BlueColor;
  
        RedColor = ...;
        GreenColor = ...;
        BlueColor = ...;
  
        LightColor(RedColor, GreenColor, BlueColor);
        some code to force the output data of LightColor function for the required room;
  
        }
  break;
  case 602:
        //Static object color
        {
        BYTE RedColor;
        BYTE GreenColor;
        BYTE BlueColor;
  
        RedColor = ...;
        GreenColor = ...;
        BlueColor = ...;
  
        LightColor(RedColor, GreenColor, BlueColor);
        some code to force the output data of LightColor function for the required object;
  
        }
  break;
So it is clear that THIS RedColor is not THAT RedColor etc.
However, it is not recommended. You may confuse yourself: “RedColor. But which one?”

The returned values:

The returned value is the “output data”. Let’s see LightColor function chapter again, telling more info this time:

Code:
  DWORD LightColor(BYTE Red, BYTE Green, BYTE Blue)
  {
        DWORD RGBValue;
  
        the code to convert Red and Green and Blue into a redgreenblue value;
  
        RGBValue = the result of the conversion;
  
        return RGBValue;
  
  }
As you see, first you declare a variable, RGBValue, which has the same DWORD size as you said in the function name for the output data.
Then you do the color conversion, putting the result into RGBValue variable.
Then you execute a “return RGBValue” command, which means the value of RGBValue variable will be the output data of LightColor function.

So, when you call LightColor function in F601 or F602 code, that means “execute the function with the input values to get the value of RGBValue variable” now.

Note:
If the function is simple (just one row), then you can skip that variable, typing just the procedure itself after “return”:

Code:
  DWORD LightColor(BYTE Red, BYTE Green, BYTE Blue)
  {
  
        return the conversion of Red and Green and Blue into a redgreenblue value;
  
  }
“Void” input and output values:

As I said above, “void” means “no”, so

- this function has no input data:

Code:
int cbCycleEnd(void)
- this function has no output data:

Code:
void cbInitProgram(int NumberLoadedPlugins, char *VetPluginNames[])
- this function has no input and output data:

Code:
  void FreeMemoryCustomize(void)
“No input” data is logical for a function if there are no variations.
See eg. LaraHealth custom function, which says what we need is Lara’s actual life points, multiplied by 2:

Code:
  WORD LaraHealth(void)
  {
  
        return the code to multiply Lara’s health by 2;
  }
There is only one Lara and she has only one health, so if I call LaraHealth then I can be sure, that the value I get is the only possible solution of the function. No need a variable input now, because there are no variations.
And, as I said previously, you need empty brackets if you call an “input-less” function:

Code:
LaraHealth ();
“No output” data is logical for a function if your purpose is not producing an output value (to use it somewhere else), but only executing the code of the function. (So there is no “return” command now.)

Bool output values:

Let’s see for example this function:

Code:
  bool InitializeAll (void)
“Bool” output data means you don’t need to declare anything like that RGBValue above.
This time the returned value could be only “true” or “false”. – For example:

if (.......) {
return true;
} else {
return false;
}

This could be useful eg. if the purpose of the function is about a condition, which can be true or false.
So, if I call InitializeAll function anywhere, then I need to do it as if it were a condition, examining if it is true (==true) or false (==false).

See ReleseAll function, where InitializeAll function is called:

Code:
  if (InitializeAll () == false) {
The input values of callbacks:

The logic of input and output values of callbacks seems a bit different than the general usage.
For example, in the case of cbFlipEffectMine callback, you can see these input values:

WORD FlipIndex
WORD Timer
WORD Extra
WORD ActivationMode

FlipIndex is the trigger ID you can find in “case” entries of the code.
Timer is the value of Set Trigger Type “Timer” box.
Extra is the value of Set Trigger Type “Extra” box.
ActivationMode is optional. Here you can say eg. that you want the trigger to be executed only if it is exported in the Script. You need “ActivationMode &” in a condition, plus the proper SCANF_ constant:

Code:
  case 602:
        if (ActivationMode & SCANF_SCRIPT_TRIGGER) {
              trigger code;
        }
  break;
You can find the descriptions of all the SCANF_ constants in Tomb_NextGeneration.h source file.

What I want to say is there are no “temporary” and “real” variables now ever. FlipIndex, Timer, Extra and ActivationMode are the variables now, period. We won’t do anything in the function (RequireMyCallBacks) where this callback is called, all the computations will be done now, in the callback chapter. – For example, as you saw above, we told the FlipIndex, Timer and Extra values for F500 in the “case 500/break” part, doing nothing in RequireMyCallBacks for the trigger code.

Notes:
- As I said above about “Timer” of Flipeffects: “ID range: 0-32767 (if the trigger has no Extra), 0-255 (if the trigger has Extra)”. If you need bigger Timer ID in your Flipeffects than ID255 (which is possible only if you have no Extra parameter!), then always type TimerFull instead of Timer in the code!
- The number you can see next to the SCANF_ flags in Tomb_NextGeneration.h are the flag values. For example, SCANF_ANIM_COMMAND is 0x8000, which means it is a hexadecimal 8000 (i.e. decimal 32768) – if it were be in NG Center, then we would sign it with $ instead of 0x: $8000.
Which means if you refer to something in the code that has a numerical ID as well, then you can also use it instead of the textual name. So eg. these have the same meanings now:

ActivationMode & SCANF_SCRIPT_TRIGGER
ActivationMode & 0x1000 (x, not ×!)
ActivationMode & 4096

TRET flags:

TRET flags are the output values of the executable trigger callbacks (cbFlipEffectMine, cbActionMine).
I mean, you can see this at the beginnings of the cbActionMine chapter:

Code:
        int RetValue;
  
        RetValue=TRET_PERFORM_ONCE_AND_GO;
And this at the beginnings of the cbFlipEffectMine chapter:

Code:
        int RetValue;
        WORD TimerFull;
  
        RetValue = enumTRET.PERFORM_ONCE_AND_GO;
(I told above what TimerFull is, that is not important now. TRET_PERFORM_ONCE_AND_GO and enumTRET.PERFORM_ONCE_AND_GO are the same, later I will tell, how. If you see any “X_Y” or “enumX.Y” data in the sources, then X and Y are similar because it is the same data.)

And you can see this at the end of the chapters:

Code:
        if (ActivationMode & enumSCANF.BUTTON_ONE_SHOT) RetValue= enumTRET.PERFORM_NEVER_MORE; 
        return RetValue;
Which means RetValue variable is the returned value of these callbacks. As you can see in “RetValue=TRET_PERFORM_ONCE_AND_GO” definition, this value is usually TRET_PERFORM_ONCE_AND_GO constantly. As “if (ActivationMode & enumSCANF.BUTTON_ONE_SHOT)” condition says, the TRET_PERFORM_ONCE_AND_GO will be swapped for enumTRET.PERFORM_NEVER_MORE only if the trigger has One Shot button pushed.

As I said above, Flipeffect trigger “case” codes are in a switch/break sequence (and it works in the similar way for Actions). As you can see, the things with RetValue are out of this sequence, which means this RetValue code is valid for all the “cases”.
But what do those TRET flags mean?
Well, mostly nothing special. TRET_PERFORM_ONCE_AND_GO means the trigger should work as a general Flipeffect/Action, enumTRET.PERFORM_NEVER_MORE means it should be activated only once.

Notes:
- Though, TRET values are general for all the Actions/Flipeffects, you can change it for a single trigger, for example:

Code:
  case 603:
        if (Timer == 1) {
              RetValue = enumTRET.PERFORM_NEVER_MORE;
              some code to do Thing A;
        }
        if (Timer == 2) {
              some code to do Thing A;
        }
  break;
Timer 1 and 2 will do the same, but if it is Timer=1, then the trigger can be executed only once (even if One Shot is not pushed), Timer=2 will use the original TRET flag.
(If you try to type “enumTRET.PERFORM_NEVER_MORE” then a little window will pop up probably. It perhaps also happens later when you try anything what I tell in this tutorial. If that happens, then just hit ESC to quit that window. The window is important, but I’d like to talk about it only later.)
- There is one more TRET flag that would be useful for a beginner: TRET_PERFORM_ALWAYS. Though the name is “always”, it won’t start a “continuous” event like a GlobalTrigger or a “continuous” TriggerGroup. “Continuous” events will be continuously executed till you stop them. The events of TRET_PERFORM_ALWAYS will be continuously executed “only” while Lara is in the trigger zone.
If you want to use the trigger in different ways (either as a single event or all the time while Lara is in the trigger zone) then keep the TRET flag for the single event. But this trigger can work as a TRET_PERFORM_ALWAYS trigger if the level builder exports it into a script TriggerGroup, executing that TriggerGroup with an F118 that has a “multiple” parameter. – You can tell it in the remark of the trigger.
(Naturally a TRET_PERFORM_ALWAYS trigger is meaningless if it is a single event everyway. I mean eg. a “spawn a baddy” trigger like that will be executed during a moment, even if Lara remains in the trigger zone. But eg. the “make a short red flash” trigger like that will be executed again and again, performing a long red flash with that flag, if Lara remains in the trigger zone.)

Last edited by AkyV; 08-11-17 at 20:35.
AkyV is offline  
Old 03-11-17, 18:02   #14
AkyV
Moderator
 
Join Date: Dec 2011
Location: Hungary
Posts: 2,574
Default

13. Condition triggers

The Condition trigger codes need to be typed in cbConditionMine callback. In a case/break part of the switch/break sequence, as I told at Flipeffects.

The Condition triggers have similar input values as the Flipeffects have, but in their case we say ConditionIndex instead of FlipIndex, plus, the trigger parameter is the “Object to trigger” box instead of the “Timer” box, called “ItemIndex” variable in the code.

The main difference between the executable (Action, Flipeffect) and the Condition trigger codes is the result of an executable trigger is always a command (“do THIS and/or do THAT”), but the result of a Condition trigger is always a true or a false flag (“we examined this, so we add the TRUE flag or the FALSE flag”).
Let’s see for example this condition trigger:

;------------------- Section for Condition triggers: descrption of the trigger -------------------------------------
<START_TRIGGERTYPE_12_T_H>
150: <#> Object is performing the actual animation with (E) framerate
<END>

;type here the sections for arguments of above conditional triggers
<START_CONDITION_150_O_H>
#MOVEABLES#
<END>

<START_CONDITION_150_B_H>
#REPEAT#Framerate=#1#31
<END>

The code theoretically looks this way:

Code:
  Object Index of the code=NGLE item index in the trigger, converted into tomb index;
  if (Actual Framerate == Extra) {
        Flag = TRUE;
  }
  Flag = FALSE;
So we say “if the framerate is the required Extra value, then the condition must be true, or else it must be false”.

But we don’t need really “else” connections this time. I mean, under “normal” circumstances a condition like that works this way, as I said above: “if true than execute this, or else execute that”. But there is no “else”, so you may think Flag = FALSE will be executed everyway. But it is not true this time. I mean, these flags are returned values, after all. And if a returned value has been established for an event, then the event is aborted, when performing that function with that returned value, so no other returned value can be established after that.
But it doesn’t work in the mode of the “bool” way:

Code:
  Object Index of the code=NGLE item index in the trigger, converted into tomb index;
  if (Actual Framerate == Extra) {
        return true;
  }
  return false;
Because, as you can see, the output data of cbConditionMine callback is not “bool” but “int”, so Condition returned values are a bit more complicated than a simple true or false. – That is why let’s see a bit more info:

Code:
  int RetValue;
  RetValue=CTRET_ONLY_ONCE_ON_TRUE;
  
  switch (ConditionIndex){
  
  case 150:
        Object Index of the code= NGLE item index in the trigger, converted into tomb index;
        if (Actual Framerate == Extra) {
              return RetValue | CTRET_IS_TRUE;
        }
        return RetValue;
  break;
  default:
  break;
  }
  return RetValue;
As you can see, the output data of the Condition triggers is the “int” RetValue variable.
The default value of RetValue is defined in the beginnings, before the switch sequence, which means again that this default value is true for all the condition cases.
CTRET_ONLY_ONCE_ON_TRUE default value means: “if Lara steps into the trigger zone, then it will be examined if the condition is true. If it is false and if she is still in the trigger zone in the next moment, it will be examined again. This will continue till the condition goes true. If it is true, then it won’t be examined any more even if she is still in the trigger zone. She needs to leave the trigger zone and come back here for a next examination”.
The point is CTRET_ONLY_ONCE_ON_TRUE is not enough by itself. I mean, it only tells how to examine, but it never turns a condition trigger into true. If you use only a CTRET_ONLY_ONCE_ON_TRUE, in that case the condition trigger will always remain false.

Further important CTRET flags are:

CTRET_IS_TRUE: add it if you say the condition is true.
CTRET_EXTRA_PARAM: add it if the condition has “Extra” trigger parameter.
CTRET_ON_MOVEABLE: add it if the condition has Moveable object indices. (See what I told in that “152 | NGLE_INDEX” example above to understand why we need to emphasize it.).)
CTRET_PERFORM_ALWAYS: it means “if Lara steps into the trigger zone, then it will be examined if the condition is true. Either it is true or false, if she is still in the trigger zone in the next moment, it will be examined again”.
(Usually you can choose this flag freely. Except: always use this flag if you want TRET_PERFORM_ALWAYS executable triggers overlapped with this Condition trigger. The continuous effect of that executable trigger will be only a single effect if you don’t choose this flag. The continuous effect will be suspended only if this condition is not true while Lara is in the trigger zone.
Naturally TRET_PERFORM_ALWAYS will also work like a single event for CTRET_PERFORM_ALWAYS if the condition is not continuous. For example, “Lara is holding pistols” is continuous, while she has the weapons in the hands, but a “key item is used” happens during a moment.)
CTRET_NEVER_MORE_ON_TRUE: works like CTRET_ONLY_ONCE_ON_TRUE, but won’t work again if Lara comes back here for a next examination.
CTRET_PERFORM_ONCE_AND_GO: it means “if Lara steps into the trigger zone, then it will be examined if the condition is true. Either it is true or false, then it won’t be examined any more even if she is still in the trigger zone. She needs to leave the trigger zone and come back here for a next examination”.

If you want to swap CTRET_ONLY_ONCE_ON_TRUE default value eg. for CTRET_PERFORM_ONCE_AND_GO, only for one single trigger, then you can naturally type this new value in the trigger code:

Code:
  int RetValue;
  RetValue=CTRET_ONLY_ONCE_ON_TRUE;
  
  switch (ConditionIndex){
  case 150:
              RetValue= CTRET_PERFORM_ONCE_AND_GO;
  
              Object Index of the code= NGLE item index in the trigger, converted into tomb index;
You have only one valid TRET flag at the same time for the executable triggers, but some CTRET flags of the conditions need to be added to each other, using a | (ALT+keypad 124) sign. For example:

Code:
  int RetValue;
  RetValue=CTRET_ONLY_ONCE_ON_TRUE;
  
  switch (ConditionIndex){
  case 150:
              RetValue= CTRET_PERFORM_ONCE_AND_GO | CTRET_ON_MOVEABLE;
  
              Object Index of the code= NGLE item index in the trigger, converted into tomb index;
Or you can add CTRET_ON_MOVEABLE to the previous (default) value (CTRET_ONLY_ONCE_ON_TRUE) only with the | sign:

Code:
  RetValue=CTRET_ONLY_ONCE_ON_TRUE;
  
  switch (ConditionIndex){
  case 150:
              RetValue | = CTRET_ON_MOVEABLE;
  
              Object Index of the code= NGLE item index in the trigger, converted into tomb index;
That is why we used this form above:

Code:
  return RetValue | CTRET_IS_TRUE;
Which is “CTRET_ONLY_ONCE_ON_TRUE | CTRET_IS_TRUE” now.

Which means: “examine the TRUE condition in CTRET_ONLY_ONCE_ON_TRUE way”. - So “if Actual Framerate = Extra condition is true, then it is a true condition trigger, which should be examined in CTRET_ONLY_ONCE_ON_TRUE way”.
“Return RetValue;” below this means this: “examine the FALSE condition in CTRET_ONLY_ONCE_ON_TRUE way”. - So “if Actual Framerate = Extra condition is false, then it is a false condition trigger, which should be examined in CTRET_ONLY_ONCE_ON_TRUE way”.

Notes:
- The nearest place where you can find a working (not theoretical) Condition trigger code is the next chapter of this tutorial.
- You can find “Return RetValue;” twice in the code above. One for the required trigger, it is clear. The one at the end of the callback must be, I suppose, “just in case”, if some condition is not closed with a “false” flag accidentally, so you close it generally, at the end of everything.
- A complex example, where you can find more than one “Return RetValue;” entry inside a trigger code:

Code:
   if (X == 1) {
        if (Y == 1) return RetValue | CTRET_IS_TRUE;
        return RetValue;
   }
   return RetValue;
So “the condition is false” (“return Retvalue”) value can be returned in twice cases, I mean, if:
a, X is not 1 or
b, X is 1, but Y is not 1.

- Condition triggers are usually better than GT GlobalTrigger constants.
I mean, a Condition trigger can be placed in the map and can be exported into GlobalTriggers.
But a GT constant works only in a GlobalTrigger.
That is why I suggest creating GT constants only if you are sure that the condition is buggy or useless in the map. (Try it with tests.)
Later I will tell how to create a GT constant.

Last edited by AkyV; 08-11-17 at 20:53.
AkyV is offline  
Old 03-11-17, 18:27   #15
AkyV
Moderator
 
Join Date: Dec 2011
Location: Hungary
Posts: 2,574
Default

14. Using default memory zone fields in the classic way

Making a code is much more than exporting triggers from the Level Editor into the plugin. See for example that very simple case when you want something that is available only in a trng.dll trigger having a TRNG variable (Local Long Alfa etc.) and a memory zone field, but you don’t want TRNG variables in your code this time. Or, don’t forget what I said above: some memory zone fields are available only from the sources, not from the Level Editor.
Whatever is your reason, the huge amount of default (i.e. not made by you) memory zone fields seems the best choice to build your code.

The “classic way” means Paolone made some functions so you can reach some memory zone fields easily (see the next chapter). But not for all the fields, so if the easier method doesn’t work to reach a memory zone field in the sources, then you need the classic, harder way.
However, even if the easier method works, you can choose any of the methods.

Let’s have a look at structures.h source file. Almost all the default memory zone fields of the sources we can use in our codes are listed here.
The fields are collected here in main groups which are called “structures”. You can easily identify which are the memory zone fields in a structure, because, as I said, they are variables, so they have the “int”, “WORD” etc. sign to signal their sizes.

Code:
  typedef struct StrWideScreen {
        int SizeX;
        int SizeY;
        float RapportoSchermo;
  
  }WideScreenFields;
What you can see above is a structure („struct”, “Str”), with the name of StrWideScreen. Their fields (SizeX, SizeY, RapportoSchermo) are inside the {} brackets. As the name says clearly, this structure is about the widescreen parameters.

Code:
  typedef struct StrVetItemCollision {
        bool TestAttiva;
        StrItemTr4 *pVeicolo;
        WORD TotMoveables;
        WORD VetMoveables[100];
        WORD TotStatics;
        StrPosizione OldPos;
        short OldSpeed; // precedente velocita' di veicolo
        StrCercaStatic VetStatics[100];
        StrAbsBoxCollision OutCollisioneBox; // usato per collisione veicoli
        int NewFloorY; // valore restituito da AnalisiCollVeicoli
  }VetItemCollisionFields;
In the StrVetItemCollision structure above you can find the name of another structure, StrPosizione. The name after that (“OldPos”) means if you are in StrVetItemCollision structure then you will get StrPosizione structure, if you type OldPos. – This could be a really important info, because in the most cases a field of a structure can be reached only referred from another structure, as you’ll see it below:

If you would like to discover the route to a memory zone field, then the best way to do it step by step. This time we would like to find the route to the OrgZ field of StrPosizione structure:

Code:
  typedef struct StrPosizione {
        int OrgX;
        int OrgY;
        int OrgZ;
  }PosizioneFields;
That is why we start searching for the structure name in other structures. We find it in more than one:

Code:
  typedef struct StrDoublePosition {
        StrPosizione From;
        StrPosizione To;
  }DoublePositionFields;
  
  typedef struct StrSalvaCapelli {
        StrPosizione Posizione;   // offset 0
        short OrientV;        // offset 0C
        short OrientH;            // offset 0E
  }SalvaCapelliFields;
  
  typedef struct StrPos3d {
        StrPosizione Posizione;   // offset 0
        short OrientV;        // offset 0C
        short OrientH;            // offset 0E
        short OrientR;            // offset 10
  }Pos3dFields;
  
  typedef struct StrVetItemCollision {
        bool TestAttiva;
        StrItemTr4 *pVeicolo;
        WORD TotMoveables;
        WORD VetMoveables[100];
        WORD TotStatics;
        StrPosizione OldPos;
        short OldSpeed; // precedente velocita' di veicolo
        StrCercaStatic VetStatics[100];
        StrAbsBoxCollision OutCollisioneBox; // usato per collisione veicoli
        int NewFloorY; // valore restituito da AnalisiCollVeicoli
  }VetItemCollisionFields;
  
 typedef struct StrLOFData {
        StrMeshInfo *pStaticFound; // if different than NULL, this is the static in the middle of Line Of Fire
        StrItemTr4* pItemFound;  // if different than NULL this is the moveable item in the middle of Line Of Fire
        StrPosizione PointFinal; // final point (x,y,z) in Line Of Fire
        WORD OrientingH; // direction between Source and Target item on X,Z plane
        WORD OrientingV; // angle on vertical axis. Valid value only if you set valid value for TolleranceV
        bool TestFreeLine; // if true there are NO obstacles in lof
        bool TestWall; // if true obstacle is a wall ( or ceiling, but no item)
        GAME_VECTOR Src;  // source point
        GAME_VECTOR Dest;  // target point
  }LOFDataFields;
Let’s suppose we are doing something with collisions, so we don’t need a Google Translator to find out that “salva capelli” (see StrSalvaCapelli) is something about hair data in Italian, what we don’t need now, i.e. we know that what we need is in StrVetItemCollision structure this time. So we can find OrgZ from StrVetItemCollision if we type this:

OldPos.OrgZ

But it is not enough, StrVetItemCollision also needs to be reached from somewhere. So let’s start searching for the StrVetItemCollision name this time. Now there is only one result:

Code:
    typedef struct StrGlobaliTomb4 {
        StrBaseRemapMemory BaseRemap; // remapped memory zones in tomb4
        DWORD FlagsLevel;   // valore FL_...
        StrPrefTomb MyPrefTomb;
        StrBaseFog BaseFog;
        StrCordDetectors BaseCordDetector;
        StrBoatSinking BoatSinking;
        StrBaseShowMeshes BaseShowMesh;
        StrSospendiLog BaseSospendiLog;
        StrBaseMissing BaseMissing;
        WORD *pIndiceFirstAnimBike;
        StrVetItemCollision BaseCollItem; // per collisione con veicoli
        StrBaseScaleItem BaseScaleParam;
        StrBasevehicles BaseVeicoli;
Which means this:
BaseCollItem.OldPos.OrgZ

This is still not enough. Searching for StrGlobaliTomb4 leads up to this:

Code:
  typedef struct StrTrngInfos {
        int IdMyPlugin;                                      // 00 (received) id of your plugin to use for each trng service
        StrGlobaliTomb4 *pGlobTomb4;             // 04 (received) address of StrGlobaliTomb4 in trng
        TYPE_RequireCallBack RequireCallBack;    // 08 (received) proc to require callback
        TYPE_SetNewPatch SetNewPatch;            // 0C (received) proc to set patch on tomb4.exe
        TYPE_Service Service;                          // 10 (received) proc to require trng service
*pGlobTomb4 starts with a little “p” because it is a so-called “pointer”. (* also indicates this. It is ** sometimes for some pointers, but it should be too much discuss it in a beginner tutorial. Besides, it is not important for you at all now.)
The dots in “BaseCollItem.OldPos.OrgZ” means there are direct connection between these. I mean, for example, StrPosizione which you call from StrVetItemCollision, is part of StrVetItemCollision, a sub-structure of it.
On the other hand, *pGlobTomb4 is a pointer, so StrGlobaliTomb4 is not a part of StrTrngInfos. StrTrngInfos only “points” to StrGlobaliTomb4. That is why there can’t be a point between them, it must be an arrow this time:

pGlobTomb4-> BaseCollItem.OldPos.OrgZ

As for StrTrngInfos, this is the uppermost level, it is meaningless to do more searching, just type “Trng.”:

Trng.pGlobTomb4->BaseCollItem.OldPos.OrgZ

And we got it. This is the route. If you want to examine OrgZ value for collisions, or (try to) force some value in it then you need to type it in the code.

The problem is it is theoretical, it was good only to teach you easily how it works.
I mean, I don’t know how this route works. First of all because it is something about a collision of an object, but I didn’t find the way to put the item index as a parameter for this. But I think it is clear that it is useless without naming an item.
Besides, probably it is not the field I need to search. I mean, that “OldPos” name is suspicious – probably it is an obsolete field? If I knew the item index then I’d put the route in the log (see below) to test what happens in this field.

However, my purpose now is not to find the missing “link”.
I mean, as you can see, plenty of undiscovered discoveries is still waiting for you about even the default memory zone fields. You need patience and several trial and errors to discover them.

Example – when a little medipack is used:

Let’s see another example – which works surely:
Let’s suppose eg. you’d like a condition trigger to examine if Lara uses a small or a big medipack:

;------------------- Section for Condition triggers: descrption of the trigger -------------------------------------
<START_TRIGGERTYPE_12_T_H>
150: Lara uses <#> small/big medipack
<END>

;type here the sections for arguments of above conditional triggers
<START_CONDITION_150_O_H>
0: Small medipack
1: Big medipack
<END>

The problem is C59 condition (“Inventory. The just selected item from inventory is <#>Item”) doesn’t work for Lara’s supply (medipack, flare, weapon, ammo), only for tools like keys.
Fortunately we have GT_USED_BIG_MEDIPACK or GT_USED_LITTLE_MEDIPACK GlobalTrigger constants to detect this. However, GlobalTriggers for plugin codes work only as special, dynamical Script commands (see later). They are not really recommended, if there is another solution. And usually there is, just like now - so you don’t want them now.
Then, what is the solution?
Well, first of all, you should search something in structures.h, that is about medipacks. That would be a good start.
This is we can find:

Code:
     typedef struct StrInventoryItems {
        BYTE WeaponPistols;          //  FWEAP_ values to test with & operator (bit flags)
        BYTE WeaponUZI;         //  FWEAP_ values to test with & operator (bit flags)
        BYTE WeaponShotGun;          //  FWEAP_ values to test with & operator (bit flags)
  
        short MediPackSmall;         //  quantity (-1 = unlimited)
        short MediPackLarge;         // quantity (-1 = unlimited)
           short Flares;           // quantity (-1 = unlimited)
        short AmmoPistols;           // quantity (-1 = unlimited)
  
  typedef struct StrBaseGlobalTriggers {
        int TotTriggers;
        StrGlobalTrigger VetTriggers[MAX_GLOBAL_TRIGGERS];
        short VetID[MAX_GLOBAL_TRIGGERS*10];
        bool TestPresoLittleMedipack;
        bool TestPresoBigMedipack;

         bool TestSalvatoSavegame;
        bool TestCaricatoSavegame;
  }BaseGlobalTriggersFields;
  
  typedef struct StrEnumSLOT {
        int LARA;   // [0] slot
        int PISTOLS_ANIM; // [1] slot
        int UZI_ANIM;     // [2] slot
        int SHOTGUN_ANIM; // [3] slot
        int CROSSBOW_ANIM;      // [4] slot
        ...
        int BIGMEDI_ITEM; // [368] slot
        int SMALLMEDI_ITEM;     // [369] slot

         int LASERSIGHT_ITEM;    // [370] slot
        int BINOCULARS_ITEM;    // [371] slot
        int FLARE_ITEM;   // [372] slot
MediPackSmall and MediPackLarge variables are clearly about inventory item amounts.
BIGMEDI_ITEM and SMALLMEDI_ITEM variables are clearly about to identify the item slot.
But TestPresoLittleMedipack and TestPresoBigMedipack seem hopeful.

As I said above, bool values are about “true” (1) or “false” (0). That is why you can suspect that that is the reason that these fields have “Test...” names:

TestPresoLittleMedipack
- is 0 if there is no little medipack used,
- is 1 if there is a little medipack used in this moment.

TestPresoBigMedipack
- is 0 if there is no big medipack used,
- is 1 if there is a big medipack used in this moment.

Another clue that the structure is StrBaseGlobalTriggers, so GT_USED_BIG_MEDIPACK and GT_USED_LITTLE_MEDIPACK are probably based on these “bool” fields.
You may test it in the log, but I expose, to save some time for you: yes, these fields exactly do what we think.

So the next step again is to identify the route:

Code:
  typedef struct StrGlobaliTomb4 {
        StrBaseRemapMemory BaseRemap; // remapped memory zones in tomb4
        DWORD FlagsLevel;   // valore FL_...
        StrPrefTomb MyPrefTomb;
        StrBaseFog BaseFog;
        StrCordDetectors BaseCordDetector;
        StrBoatSinking BoatSinking;
        StrBaseShowMeshes BaseShowMesh;
  
        WORD OldDFPerCamera;  // flags DF_
        DWORD StatusNG;  // flags SNG_...
        StrBaseGlobalTriggers *pBaseGlobalTriggers;
  
        int TestNoDamageRollingBallIndex; // disattiva danni a lara con rolling ball
                                     // di valore indice
        StrBaseSalvaCollisioni  BaseSalvaCollisioni;
        StrBaseSalvaCollisioni  BaseSalvaOldCollisioni;
And we already know that if we get StrGlobaliTomb4, then what remained is Trng.pGlobTomb4.

So the route is:

Trng.pGlobTomb4->pBaseGlobalTriggers->TestPresoLittleMedipack
Trng.pGlobTomb4->pBaseGlobalTriggers->TestPresoBigMedipack

After that you can organize the code easily:

Code:
  case 150:
        //examining medipack use
        if (ItemIndex == 0 && Trng.pGlobTomb4->pBaseGlobalTriggers->TestPresoLittleMedipack == 1) {
              return RetValue | CTRET_IS_TRUE;
        }
        if (ItemIndex == 1 && Trng.pGlobTomb4->pBaseGlobalTriggers->TestPresoBigMedipack == 1) {
              return RetValue | CTRET_IS_TRUE;
        }
        return RetValue;
  break;
Example – changing an inventory item name:

Another working example:
You have a crossbow, but then you start a new level where you swap the crossbow item slot for a harpoon gun.
The crossbow name is stored in a hardcoded way, always in String#58. There is a variable-based way in TRNG to change the name (swapping that string for another one, where the new weapon name is stored), but you need a more user-friendly way, in which you need a simple trigger to do that, something like:

;------------------ Section for flipeffect trigger: description of the trigger -------------------------------
<START_TRIGGERWHAT_9_O_H>
501: Change (E) inventory item name for the name in <&> [Strings]
<END>

;type here the sections for arguments used by above flipeffects
<START_EFFECT_501_T_H>
#STRING_LIST_255#
<END>

<START_EFFECT_501_E_H>
#MEM_INVENTORY_INDICES#
<END>

(Notes:
- We chose the place for the parameters with a good reason, not accidentally. I mean, as I said above, the biggest Timer range for Flipeffects – with Extra - is 0-255, the biggest Extra range for Flipeffects is 0-127. The amount of inventory indices is below 127, but we have more than 200 [Strings] entries, so we should place inventory indices in Extra box and string indices in Timer box, not reversely.
- Though the name tells 255 entries in the list, the [Strings] section has only 204 entries.)

The field you need now is the IndiceStringa field of StrDatoInventario structure:

Code:
  typedef struct StrDatoInventario {
        WORD Slot;        // 0x00
        short OffsetY;       // 0x02
        WORD Distance;    // 0x04
        WORD OrientX;      // 0x06
        WORD OrientY;      // 0x08
        WORD OrientZ;      // 0x0A
        WORD Flags;       // 0x0C
        WORD IndiceStringa;    // 0x0E
        int  Mistero;    // 0x10 passato a drawinventoryitemme
  }DatoInventarioFields;
Again, we start it with searching for the route:

Code:
  typedef struct StrGlobAddress {
  
        StrItemTr4 *pLara;
        StrRoomTr4 *pVetRooms;
        StrItemTr4 *pVetItems;
  
        int *pTotItemsAtBegin;
        int *pTotItems; // era "AncoraTotItems"
        StrInventoryItems *pInventory;
        StrSlot* pVetSlot;
        int *pSlopeType; // SLOPE_ values
        int *pSlopeX; 
        int *pSlopeZ;
        StrBaseHandle BaseHandles;
        StrAnimationTr4 *pVetAnimations;
        ...
        BYTE *pTestWorkingOnMoveables;
        float *pStartFog;
        BOOL *pTestDisableFogBulbs;
        BYTE *pSetting_Volumetric;
        StrDatoInventario *pVetStructInventoryItems;
        WORD *pVetFlagsInventoryItems;
        int *pZonaRecord30;
        StrBoxZones *pVetBoxZones;
        StrFish *pVetFish;
        int *pInclinationType;
Then:

Code:
  typedef struct StrGlobaliTomb4 {
        StrBaseRemapMemory BaseRemap; // remapped memory zones in tomb4
        DWORD FlagsLevel;   // valore FL_...
        StrPrefTomb MyPrefTomb;
        StrBaseFog BaseFog;
        StrCordDetectors BaseCordDetector;
        StrBoatSinking BoatSinking;
        StrBaseShowMeshes BaseShowMesh;
  
        StrBaseTexSequence BaseTexSequence;
        StrItemTr4 *VetPlatforms[1024];
        StrBaseEquipItem BaseEquipItem;
        StrGlobAddress *pAdr;
        WORD ClimbFlags;
        StrEnvPosition EnvPosition;
        StrHardwareB HardwareB;
So the route should be:

Trng.pGlobTomb4->pAdr->pVetStructInventoryItems->IndiceStringa

But it is not, this time!
I mean, names with “Vet” in it tell it is a “vector”. Vectors have several indices, like:

pVetStructInventoryItems[0]
pVetStructInventoryItems[1]
pVetStructInventoryItems[2]
etc.

If you look at the list created by #MEM_INVENTORY_INDICES#, then you can understand easily what the indices are this time:

#ID DESCRIPTION
--------------------------------------------------------------------------------
20: BIGMEDI_ITEM
22: BINOCULARS_ITEM
111: BURNING_TORCH_ITEM
113: CLOCKWORK_BEETLE
115: CLOCKWORK_BEETLE_COMBO1
114: CLOCKWORK_BEETLE_COMBO2
24: COMPASS_ITEM
13: CROSSBOW_AMMO1_ITEM
14: CROSSBOW_AMMO2_ITEM
15: CROSSBOW_AMMO3_ITEM
5: CROSSBOW_ITEM

Yes, it is naturally a well-tested preset argument list, so if the parameter index is X, that surely means that the inventory index of item at Parameter X is also X. (So eg. if Crossbow item is 5 in the Timer list, then the inventory code – NOT the general slot code! – is also 5.)

So, for example:
pVetStructInventoryItems[13] = CROSSBOW_AMMO1_ITEM
pVetStructInventoryItems[14] = CROSSBOW_AMMO2_ITEM
pVetStructInventoryItems[15] = CROSSBOW_AMMO3_ITEM

Which means (I suppose...) whereto pVetStructInventoryItems points is not IndiceStringa, but the item inventory index.
Fields for an index are always part of the index structure (I suppose...), so IndiceStringa is part of [...] structure, which means, not “->” but “.” is needed this time:

Trng.pGlobTomb4->pAdr->pVetStructInventoryItems[...].IndiceStringa

After that you can organize the code easily (knowing that Timer ID0 = String#0, Timer ID1 = String#1 etc.):

Code:
  case 501:
        //change inventory item name
        Trng.pGlobTomb4->pAdr->pVetStructInventoryItems[Extra].IndiceStringa = Timer;
  break;
Note:
You can have custom vectors, if you want.
For example:

BYTE VetIndex [5];

So “VetIndex” variable is a vector, having five values, for example:

VetIndex [0] will be 215.
VetIndex [1] will be 476.
VetIndex [2] will be 52.
VetIndex [3] will be 67.
VetIndex [4] will be 104.

If the values are default ones, not got from somewhere else during the procedures, then you need to declare the vector this way:

BYTE VetIndex [5] = {215, 476, 52, 67, 104};

Example – memory zone fields of NGLE:

A third working example:
As I said above, you can reach several memory zone fields even in the harder or the easier way.
But you can establish the route even in more ways sometimes, if you choose the harder way. I mean, for example, you can reach some fields both via the original route and via the route which is assigned when a trng.dll “Variable” Flipeffect searches that field. (And both are a “harder way” now.)

Let’s see eg. the green dash bar value: it is between 0 and 120. Naturally it is 120 when Lara is dashing with full strength (on foot or on the nitro-using motorbike).
You can easily find the field that hosts it: “short *pDashBarValue”, in StrGlobAddress.
So the route we are searching for is:

*Trng.pGlobTomb4->pAdr-> pDashBarValue

(Note:
That * of pointers. Don’t ask. Sometimes we need to place it somewhere in the route having a pointer, sometimes not. I don’t know the reason clearly, what I really know is:
- if you need that sign and you ignore it, or vice verse, then the compile will fail it with an error message,
- or it won’t fail, but the data will be wrong. Just like now: the dash bar value will be something else if you examine this route without *.

Another classic and mystical error for a wrong route when you count on “.”, but you need to type “->”, or vice verse.)

When you use a route what a trng.dll “Variable” Flipeffect also uses, then you always need this formula:

*Trng.pGlobTomb4->MEMORY_ZONE[INDEX].pSIZE

Choose these values for MEMORY_ZONE:

pVetMemorySavegame
pVetItemMemory
pVetCodeMemory
pVetSlotMemory
pVetMemoryAnimation
pVetMemoryInventory

INDEX is the index you can find in those flipeffects. For example, Code Memory Zone index list is available in F283:

#ID DESCRIPTION
--------------------------------------------------------------------------------
0: Audio Track Number on Channel 1 (only for read) (Short)
1: Audio Track Number on Channel 2 (only for read) (Short)
18: Camera Mode Next (0='follow me', 1='fixed', 2='look'; 3 ='combat') (Long)
17: Camera Mode Now (0='follow me', 1='fixed', 2='look'; 3 ='combat') (Long)
3: Current Level number (more updated than savegame memory) (Byte)
9: Dash Bar Value (0 - 120) (Short)
5: Earthquake intensity (Long)

You can see that dash bar field index is 9.

SIZE is the usual variable size. It is Short for the dash bar, so type “pShort” where you can see “pSize”.

So the “Variable” Flipeffect route for the dash bar field of Code Memory Zone is:

*Trng.pGlobTomb4->pVetCodeMemory[9].pShort

Example – a really long route:

Not all the info for the structures are available in structures.h.
Let’s see for example this condition: “if any audio track is just playing in channel 2”. – The route is complicated, probably I would have never found it by myself if Paolone had not helped.

As a first clue, I tried to find fields with “audio” name and/or remark in structures.h. I found many of them, but all seemed misleading more or less, when I tested them.

Only StrBassHandles structure seemed very promising about what we want. I mean, the whole structure has several fields, concentrating clearly on audio features.

Code:
  typedef struct StrBassHandles {
        StrCanaleBass VetCanali[4];  // reduced by [1] the channel array to host following pointer for extra Proc
        StrProcBassDllExtra * pProcExtra; // point to new list of bass procedures
        BYTE Reserved[6];  // placefolder to preserve offset of following "CanaleNow" field
        int CanaleNow; // 0 o 1 used to set what channel we are using
        DWORD StartOffset;
        float VolumeMusica;
        short OldCdLoop; // Last cd audio track on 0 channel
        bool TestPresente;
        HINSTANCE  HandleDll;
       StrProcBassDll Proc;

   }BassHandlesFields;
So, yes, this is the most important structure of our code. Knowing what we already now, it is easy to find out the route that leads here:

Trng.pGlobTomb4->BaseBassHandles

Perhaps it sounds odd but we will use this “route fragment” for not one but two parts of the whole route:
- to define the field where we can examine if a channel is just working or not,
- to define the field where we can choose that channel.

So “channel” seems a good next clue, which leads to another promising structure:

Code:
  typedef struct StrProcBassDll {
        TYPE_BASS_Init BASS_Init;
        TYPE_BASS_Free BASS_Free;
        TYPE_BASS_ChannelSlideAttribute  BASS_ChannelSlideAttribute;
        TYPE_BASS_ChannelSetAttribute BASS_ChannelSetAttribute;
        TYPE_BASS_StreamCreateFile BASS_StreamCreateFile;
        TYPE_BASS_ChannelPlay BASS_ChannelPlay;
        TYPE_BASS_ErrorGetCode  BASS_ErrorGetCode;
        TYPE_BASS_Pause BASS_Pause;
        TYPE_BASS_Start BASS_Start;
        TYPE_BASS_ChannelFlags BASS_ChannelFlags;
        TYPE_BASS_ChannelStop BASS_ChannelStop;
        TYPE_BASS_Stop BASS_Stop;
        TYPE_BASS_ChannelSetPosition BASS_ChannelSetPosition;
        TYPE_BASS_ChannelIsActive  BASS_ChannelIsActive;
        TYPE_BASS_ChannelGetPosition BASS_ChannelGetPosition;
        TYPE_BASS_StreamGetFilePosition  BASS_StreamGetFilePosition;
  }ProcBassDllFields;
Yes, that should be it! “ChannelIsActive”. And we can get this StrProcBassDll from StrBassHandles (see StrProcBassDll Proc; above).

Trng.pGlobTomb4->BaseBassHandles.Proc

TYPE_BASS_ChannelIsActive doesn’t seem like a variable, however, the structure says we can reach it with BASS_ChannelIsActive, so:

Trng.pGlobTomb4->BaseBassHandles.Proc.BASS_ChannelIsActive

The next clue came from bass.h source file:

Code:
  // BASS_ChannelIsActive return values
  #define BASS_ACTIVE_STOPPED  0
  #define BASS_ACTIVE_PLAYING  1
  #define BASS_ACTIVE_STALLED  2
  #define BASS_ACTIVE_PAUSED   3
So it seems my solution should be something like this – because 1 is “playing” now:

if (Trng.pGlobTomb4->BaseBassHandles.Proc.BASS_ChannelIsActive == 1)

But where can I type the parameter, the channel? Which channel is active? 1 or 2?
BASS_ChannelIsActive is not a “Vet”, I cannot type a channel ID in [] brackets. However, the solution was something similar, I needed () brackets for the parameter:

if (Trng.pGlobTomb4->BaseBassHandles.Proc.BASS_ChannelIsActive (...) == 1)

Now let’s go back to StrBassHandles. “Int CanaleNow;” seems promising, but it is the wrong clue. We need StrCanaleBass structure, and VetCanali[] leads there – having 1 in [] brackets, because 0 is for channel1, 1 is for channel2.

Trng.pGlobTomb4->BaseBassHandles.VetCanali[1]

Code:
  typedef struct StrCanaleBass {
       DWORD Canale;  //   0x00  handle of open channel or 0 if missing

         short NumeroCd; //  0x04  number of track in progress or -1 if it is missing
        int Loop;       //  0x06  when audio track is looped
  }CanaleBassFields;  //  0x0A
The info we want about the channel is not the track ID and not the single/loop status so what we need is “DWORD Canale;”.
So, finally, we got the route:

if (Trng.pGlobTomb4->BaseBassHandles.Proc.BASS_ChannelIsActive (Trng.pGlobTomb4->BaseBassHandles.VetCanali[1].Canale) == 1)

Last edited by AkyV; 03-11-17 at 22:49.
AkyV is offline  
Old 03-11-17, 18:40   #16
AkyV
Moderator
 
Join Date: Dec 2011
Location: Hungary
Posts: 2,574
Default

15. Using default memory zone fields in a more user-friendly way

There are some functions you can call if you’d like to get access to some default memory zone fields in an easier way.
Let’s see two of them now.

“Get” function:

The most important one amongst these functions is the “Get” function.
The function needs to be called in this form:

Get (GET constant, first input value, second input value)

You can have different constants for Get function, Tomb_NextGeneration.h source file will list them all.

Let’s see the first one first:

Code:
  #define GET_LARA  0                            // returns values in GET.pLara->  and GET.LaraIndex    INPUT: none
No input values, so GET_LARA constant needs to be called in this way:

Code:
  Get (GET_LARA, 0, 0);
The remark says if you call this, then you get GET.pLara and GET.LaraIndex returned values from the function.
GET.LaraIndex must be clear: it is the object index of Lara – so, for example, if you want to check her (tomb, not NGLE!) index in X variable, then the code eg. is this:

Code:
  Get (GET_LARA, 0, 0);
  if (GET.LaraIndex == X) {
        return RetValue | CTRET_IS_TRUE;
  }
  return RetValue ;
But if you want to know what GET.pLara returned value is, then it is not easy to inspect that.
I mean, it is really easy if we talk about auto-enumeration, but we’ll do that only later. Till that, please accept that GET.pLara contains Lara’s data in StrItemTr4 structure:

Code:
  typedef struct StrItemTr4 {
        int HeightFloor;        // 00
        int ContactFlags;       // 04  (oggetto toccato lara (darts))
        DWORD MeshVisibilityMask; // 08
        WORD SlotID;                 // 0C
        WORD StateIdCurrent;        // 0E
        WORD StateIdNext;            // 10
        WORD StateIdAI;                    // 12
        WORD AnimationNow;               // 14
If you are thoughtful then you’ll realize that StrItemTr4 structure is the collection of NGLE Item Memory Zone fields.
So if you check a field of StrItemTr4 structure for GET.pLara, that is the same if you check the same Item Memory Zone field in NGLE, when the selected object for the zone is Lara.
The remark shows an arrow after GET.pLara (->), so we can find out that you need to point from GET.pLara to the proper StrItemTr4 field in the code.
For example, if you want to check that Lara’s actual animation is the one stored in Variable Y:

Code:
  Get (GET_LARA, 0, 0);
  if (GET.pLara->AnimationNow == Y) {
        return RetValue | CTRET_IS_TRUE;
  }
  return RetValue;
I think, you can find out easily that the second (GET_ITEM) constant points also to StrItemTr4 structure, for the Item Memory Zone field values of the required object:

Code:
  #define GET_ITEM 1                             // returns value in GET.pItem->   (moveable object).INPUT: Index = index of moveable + (NGLE_INDEX if it is a ngle index) 
For example, we force current StateID 3 and OCB 64 for the object having NGLE index 97 and then also current StateID 3 but OCB 128 for the object having NGLE index 102:

Code:
  Get (GET_ITEM, 97+NGLE_INDEX, 0);
  GET.pItem-> StateIdCurrent = 3;
  GET.pItem->OcbCode = 64;
  Get (GET_ITEM, 102+NGLE_INDEX, 0);
  GET.pItem-> StateIdCurrent = 3;
  GET.pItem->OcbCode = 128;
  GET.pItem->Reserved_36 = 200;
Reserved_36 field is what we call “Custom B” in NGLE. (After that you can find easily that Reserved_34, _38 and _3A are Custom_A, _C and _D.) So we also force 200 for ItemID102 now, in Custom B field.

Let’s follow it with

Code:
  #define GET_INFO_LARA 8             // returns values in GET.LaraInfo.  (structure with various infos about lara) INPUT: none
GET_INFO_LARA is for StrLaraInfo structure (but not with a pointer this time). Now we check that which vehicle Lara is driving:

Code:
  Get (GET_INFO_LARA, 0, 0);
  if (GET.LaraInfo. SlotVehicle == 32) {
        return RetValue | CTRET_IS_TRUE;
  }
  return RetValue;
If the condition is true, then it is the jeep, because, as you can see it in NG Center (or StrEnumSlot structure of structures.h), 32 is the slot ID of jeeps. (You can type SLOT_JEEP instead of 32, if you want.)

And one more simple example:

Code:
  Get (enumGET.BIG_NUMBER,Extra,0);
  if (GET.BigNumber > 2048) {…
This will examine if the PARAM_BIG_NUMBERS value at the index which was chosen in the Extra box of the Flipeffect, is bigger than 2048.
Indices are 0, 1, 2, 3 etc. now, so for example 3000 is at Index#2:

Parameters=PARAM_BIG_NUMBERS, 8, 5796, 3000, 128, 452

And now let’s see something complicated - you can “embed” the Get functions into each other:

Code:
  Get (GET_ITEM, *Trng.pGlobTomb4->pVetMemorySavegame[0].pShort, 0);
  Get (GET_SLOT, GET.pItem->SlotID, 0);
  if (GET.pSlot->DistanceForMIP < ItemIndex * 256) {
        return RetValue | CTRET_IS_TRUE;
  }
  return RetValue;
Index0 for Savegame Memory Zone is (see F248) “TRNG Index. Index of last item found with testposition or condition (Short)”, so this is the actual (tomb) value of TGROUP_FOUND_ITEM_INDEX. So GET_ITEM will work with that item now.
GET_ITEM tells in GET.pItem->SlotID that which the slot of that item is. That slot (GET.pItem->SlotID) is examined in GET_SLOT.
GET_SLOT tells in GET.pSlot->DistanceForMIP that in which distance each item of that slot placed in the map will change for their MIP objects.
Then the condition checks if that distance is less than the required distance sector (1, 2, 3, 4 etc.), which is asked in the “Object to trigger” box of this Condition trigger (hence “ItemIndex” variable). You may read it in another tutorial, here, on TRF, or you can do some experiments, using the log, that 256 in this field means (about?) 1 sector. So eg. if the builder choose “less than 2 sectors” in the trigger, then “less than 512” should be examined, that is why ItemIndex is multiplied by 256.

Memory zone fields, using the field indices of “Variable” Flipeffects:

Yes, you can use those indices not only with the classic, hard method (see eg. the example above with the dash bar), but even in an easier way.

For example, “*Trng.pGlobTomb4->pVetMemorySavegame[0].pShort” looks this way with the easier method:

for a condition – to check if TGROUP_FOUND_ITEM_INDEX tomb index is 60:

Code:
  if (ReadMemVariable (0 | enumMEMT.SAVEGAME) == 60)
for a command – (to try) to write 60 in it:

Code:
  WriteMemVariable(0 | MEMT_SAVEGAME, 60);
See more info here (only the rows where enumMEMT flags are):
Table: Code parameter to access to Trng and Memory Variables

Notes:
- I must tell that sometimes I had problems with the free choice between the classic and the easy method of using these indices. Test carefully, because it seems one method for some field indices (some memory zones?) really work only with commands and the other method for some field indices (some memory zones?) really work only with conditions. (So eg. in the case of Field Index X, only the classic, harder way works when you want it for a command, not a condition, the easy method is useless now.)
- If you define the subject for Item, Slot or Inventory Memory Zones in the code, then you don’t need the “export function” form of the proper trng.dll triggers everyway. (See eg. A54 to define the subject of the Item Memory Zone.) As that table linked above says, you can choose the subject even directly inside the code. In this example the Item Memory Zone subject is the Moveable object selected in the “Object to trigger” box of the actual Condition trigger:

Code:
  * GET.Vars.pMemorySelected->pItemSelected = ItemIndex;
The Get function this time is Get (GET_VARIABLES,0,0). Exceptionally you need to call it only once in the whole plugin, typing it in cbInitLevel callback.

Last edited by AkyV; 10-11-17 at 20:19.
AkyV is offline  
Old 03-11-17, 18:44   #17
AkyV
Moderator
 
Join Date: Dec 2011
Location: Hungary
Posts: 2,574
Default

16. Using custom memory zone fields

Let’s see for example the parameters of LoadCamera Script commands. I didn’t find any default memory zone fields in the sources that would refer to them. That is why I loaded Tomb4 data source into Trng Patcher, and then searched for “LoadCamera”. This is what I got:

4B0635: LoadCamera_Room:
4B0635: DB 0FFh,0,0

4B07D0: Mex_LoadCameras:
4B07D0: DB 'LOC_473BE0',0

533978: LoadCamera_SrcX:
533978: DD ?
53397C: LoadCamera_SrcY:
53397C: DD ?
533980: LoadCamera_SrcZ:
533980: DD ?
533984: LoadCameraTargetX:
533984: DD ?
533988: LoadCameraTargetY:
533988: DD ?
53398C: LoadCameraTargetZ:
53398C: DD ?

“Mex” means “message”, I don’t care about that field now, but the other fields are eventually the well-known fields of the LoadCamera Script command. (Src = source.)

DB means the field has a little size (B refers to BYTE), so it would be a BYTE or a char variable.
DD means the field has a big size (D refers to DWORD), so it would be a DWORD or an int variable.

We can carry these fields this way into Plugin_trng.cpp – let’s say for example LoadCamera_SrcX:

Code:
int *pLoadCamera_SrcX = (int*) 0x533978;
As you can see:
- LoadCamera_SrcX is treated like a pointer, it got a “*p” sign.
- As I said, it is an „int”. That is the size of *pLoadCamera_SrcX.
- 533978 field of Tomb4 data is referred with Sign 0x.
- “int” size also needs to be added for 533978, with *, in brackets.

Now you can use *pLoadCamera_SrcX (with *!) as the variable that hosts the “Source X” value of the loadcamera. – An example, which forces the “Timer” parameter value of the Flipeffect into this field:

Code:
  case 502:
        //new loadcamera
        {
        int *pLoadCamera_SrcX = (int*) 0x533978;
  
        *pLoadCamera_SrcX = Timer;
        }
  break;
Notes:
- That free choice problem again, what I just said. I mean, for example, when I worked with Layer1 and Layer2 colors, I had to search for the proper fields in Tomb4 data, because the available Layer1/2 color fields in structures.h didn’t do what I wanted.
- An interesting situation:

Code:
  typedef struct StrSlot {
        WORD  TotMesh;          // 0
        WORD  IndexFirstMesh;   // 2
        int   IndexFirstTree;   // 4
        int   IndexFirstFrame;  // 8
        void *pProcInitialise;  // 0C
      void *pProcControl;          // 10
      void *pProcFloor;       // 14
      void *pProcCeiling;          // 18
      void *pProcDraw;        // 1C
      void *pProcCollision;   // 20
        WORD  DistanceForMIP;      // 24  
        WORD  IndexFirstAnim;   // 26
        short Vitality;              // 28
        WORD DistanceDetectLara;           // 2A
        WORD ss_Unknown3;       // 2C
        WORD FootStep;               // 2E
        WORD  TestGuard;        // 30
        WORD Flags;                  // 32  (FSLOT_ flags)
        void *pProcDrawExtras;  // 34
        int  ShatterableMeshes;      // 38  
        int  ss_Unknown5;       // 3C
  }SlotFields;
The “void” sign of “nothing”, “no size” looks odd for the structure elements. So if you work with them, then you need to follow the same trick, like:

Code:
  GET.pSlot->pProcDrawExtras = (int*) 4613568;
I mean, the value of 4613568 (whatever it is, it is not important now) is naturally has some size, it cannot be “no size”, it needs an int or a DWORD size. We solved it with that (int*) sign.
AkyV is offline  
Old 03-11-17, 18:47   #18
AkyV
Moderator
 
Join Date: Dec 2011
Location: Hungary
Posts: 2,574
Default

17. Using TRNG numerical variables

Just like Paolone, you can create executable/condition triggers for the level builder where he can execute/examine events, using TRNG variables (Local Long Alfa etc.)

Here is an example:

;------------------- Section for Condition triggers: descrption of the trigger -------------------------------------
<START_TRIGGERTYPE_12_T_H>
151:Lara. (Health) The actual air under water is (E) less/equal/more than <#> variable #REMARK#Don’t choose byte variables.
<END>

;type here the sections for arguments of above conditional triggers
<START_CONDITION_151_O_H>
#VAR_NORMALS#
<END>

<START_CONDITION_151_B_H>
0:Less
1:Equal
2:More
<END>

#VAR_NORMALS# list has all the numerical variables (except Store variables), which is very much:

#ID DESCRIPTION
--------------------------------------------------------------------------------
255: Current Value
0: Global Byte Alfa1
1: Global Byte Alfa2
2: Global Byte Alfa3
3: Global Byte Alfa4
4: Global Byte Beta1
5: Global Byte Beta2
6: Global Byte Beta3
7: Global Byte Beta4
8: Global Byte Delta1
9: Global Byte Delta2
10: Global Byte Delta3
11: Global Byte Delta4
48: Global Long Alfa
49: Global Long Beta
50: Global Long Delta
51: Global Long Timer
16: Global Short Alfa1
17: Global Short Alfa2
18: Global Short Beta1
19: Global Short Beta2
20: Global Short Delta1
21: Global Short Delta2
53: Last Input Number
64: Local Byte Alfa1
65: Local Byte Alfa2
66: Local Byte Alfa3
67: Local Byte Alfa4
68: Local Byte Beta1
69: Local Byte Beta2
70: Local Byte Beta3
71: Local Byte Beta4
72: Local Byte Delta1
73: Local Byte Delta2
74: Local Byte Delta3
75: Local Byte Delta4
112: Local Long Alfa
113: Local Long Beta
114: Local Long Delta
115: Local Long Timer
80: Local Short Alfa1
81: Local Short Alfa2
82: Local Short Beta1
83: Local Short Beta2
84: Local Short Delta1
85: Local Short Delta2
--------------------------------------------------------------------------------
Total Items = 46

I mean, we can’t make the list shorter, even if we clearly know that we don’t need the little byte variables, because the air amount (between 0 and 100% which is 1800) is for a bigger (short) variable.
But we should not rule out these variables:

- Long. Because if short variables are all engaged, then the biggest (long) variables still could be useful.
- Current Value. This “global long” variable is crucial for NGLE variable computations, as you know.
- Other “global long” (Last Input Number, Global Long Timer) and “local long” (Local Long Timer) variables. They have a special task, though. But if we don’t use that task, then they are still useful as casual long variables.

That is why we won’t make codes for byte variables, this is the reason I made that “don’t choose byte variables” remark for the level builder.

Index10 for Savegame Memory Zone is (see F248) “Lara. Air for Lara (0 - 1800) (Short)”, so we’ll use this code:

*Trng.pGlobTomb4->pVetMemorySavegame[10].pShort

The whole trigger code looks this way:

Code:
  case 151:
        //examining air amount
        {
              WORD Variable;
  
              if (ItemIndex == 255) Variable = GET.Vars.pTrngVars->Globals.CurrentValue;
              if (ItemIndex == 48) Variable = GET.Vars.pTrngVars->Globals.NumWar.Name.Alfa.Long;
              if (ItemIndex == 49) Variable = GET.Vars.pTrngVars->Globals.NumWar.Name.Beta.Long;
              if (ItemIndex == 50) Variable = GET.Vars.pTrngVars->Globals.NumWar.Name.Delta.Long;
              if (ItemIndex == 51) Variable = GET.Vars.pTrngVars->Globals.NumWar.Name.Timer;
              if (ItemIndex == 16) Variable = GET.Vars.pTrngVars->Globals.NumWar.Name.Alfa.Short1;
              if (ItemIndex == 17) Variable = GET.Vars.pTrngVars->Globals.NumWar.Name.Alfa.Short2;
              if (ItemIndex == 18) Variable = GET.Vars.pTrngVars->Globals.NumWar.Name.Beta.Short1;
              if (ItemIndex == 19) Variable = GET.Vars.pTrngVars->Globals.NumWar.Name.Beta.Short2;
              if (ItemIndex == 20) Variable = GET.Vars.pTrngVars->Globals.NumWar.Name.Delta.Short1;
              if (ItemIndex == 21) Variable = GET.Vars.pTrngVars->Globals.NumWar.Name.Delta.Short2;
              if (ItemIndex == 53) Variable = GET.Vars.pTrngVars->Globals.LastInputNumber;
              if (ItemIndex == 112) Variable = GET.Vars.pTrngVars->Locals.Name.Alfa.Long;
              if (ItemIndex == 113) Variable = GET.Vars.pTrngVars->Locals.Name.Beta.Long;
              if (ItemIndex == 114) Variable = GET.Vars.pTrngVars->Locals.Name.Delta.Long;
              if (ItemIndex == 115) Variable = GET.Vars.pTrngVars->Locals.Name.Timer;
              if (ItemIndex == 80) Variable = GET.Vars.pTrngVars->Locals.Name.Alfa.Short1;
              if (ItemIndex == 81) Variable = GET.Vars.pTrngVars->Locals.Name.Alfa.Short2;
              if (ItemIndex == 82) Variable = GET.Vars.pTrngVars->Locals.Name.Beta.Short1;
              if (ItemIndex == 83) Variable = GET.Vars.pTrngVars->Locals.Name.Beta.Short2;
              if (ItemIndex == 84) Variable = GET.Vars.pTrngVars->Locals.Name.Delta.Short1;
              if (ItemIndex == 85) Variable = GET.Vars.pTrngVars->Locals.Name.Delta.Short2;
  
              if (Extra == 0 && *Trng.pGlobTomb4->pVetMemorySavegame[10].pShort < Variable) return RetValue | CTRET_IS_TRUE;
              if (Extra == 1 && *Trng.pGlobTomb4->pVetMemorySavegame[10].pShort == Variable) return RetValue | CTRET_IS_TRUE;
              if (Extra == 2 && *Trng.pGlobTomb4->pVetMemorySavegame[10].pShort > Variable) return RetValue | CTRET_IS_TRUE;
  
              return RetValue;
        }
  break;
Remember! The Get function this time is Get (GET_VARIABLES,0,0), but if you have already typed it in cbInitLevel callback, as I said above, then you don’t need to type it anywhere else.

Notes:
- I chose WORD for Variable variable because that size can host that 0-1800 value, either that is coming from a Short or a Long variable.
- Routes like “GET.Vars.pTrngVars->Locals.Name.Delta.Short1” are not really noticeable from structures.h. Don’t worry, if I introduce auto-enumeration in the next chapter, you’ll be able to find them easily.
- You can also try ReadNumVariable or WriteNumVariable functions (see their descriptions in trng.cpp) to read/write a TRNG numeric variable.
- The proper command order with custom variables is:
1. declare the variable.
2. initialize the variable. (“Filling it with some data”.)
3. make operations with the variable.

Let’s see what that means if you want to check the air amount if it is less than Local Short Alfa1:
1. “WORD Variable;” – the variable has been declared.
2. “if (ItemIndex == 80) Variable = GET.Vars.pTrngVars->Locals.Name.Alfa.Short1;” – the variable has been initialized.
3. “if (Extra == 0 && *Trng.pGlobTomb4->pVetMemorySavegame[10].pShort < Variable) return RetValue | CTRET_IS_TRUE;” – operation has been executed.

But this is eg. a wrong code:

Code:
  BYTE A;
  BYTE B;
  A = 6 + B;
I mean, you may tell that “B is a fresh variable, I have just declared it, so it must be empty, 0, A must be 6 + B = 6 + 0 = 6.
No, it is not true. Maybe you can’t get an error message if you compile the plugin now, but you will if you start the game now with this plugin.
If you want B as 0, then tell it:

Code:
  BYTE A;
  BYTE B;
  B = 0;
  A = 6 + B;
No problem with A, that had been initialized in A=6+B.
AkyV is offline  
Old 03-11-17, 18:52   #19
AkyV
Moderator
 
Join Date: Dec 2011
Location: Hungary
Posts: 2,574
Default

18. Auto-enumeration

Let’s see the code of the “change the inventory item name” trigger again:

Code:
  case 501:
        //change inventory item name
        Trng.pGlobTomb4->pAdr->pVetStructInventoryItems[Extra].IndiceStringa = Timer;
  break;
If you start typing the route with typing “Trng”, then a little window pops up (at once or perhaps only after one or two seconds) just after you typing the dot after “Trng”. Or another window just after you typing pGlobTomb4 and the little arrow after that - just like you saw it in the image here:
What is Get() function?

I talked about that window previously in this tutorial, but it is time to explain that.

However, if you don’t see that window then probably first you need install this hotfix for the compiler:
http://hotfixv4.microsoft.com/Visual...l_i386_zip.exe

(Or perhaps it is enough if you click on the proper command in Trng Patcher dropdown menu that fixes it.)

Delete that row in the trigger code, we’ll type it again with this “window-method” now.
So, start it with “Trng.”. Type it to make that window pop up.
The contents in the window is the StrTrngInfos structure. I mean, the structure that belongs to that “Trng” sign.
I hope you remember when we selected previously pGlobTomb4 in StrTrngInfos structure – that is why naturally you can find pGlobTomb4 as well in the little window. If you click on it, then a little message will pop up to show the same info what you can find in the row of pGlobTomb4, in structures.h file:

Code:
  StrGlobaliTomb4 *pGlobTomb4;             // 04 (received) address of StrGlobaliTomb4 in trng
Plus, the message also tell that it is from structures.h file and which structure is this.
If you double-click on pGlobTomb4 in the window, then it will be selected, and typed automatically after Trng:

Trng.pGlobTomb4

“P” is pointer, so you need a -> now, not a dot. Type it to make the next window pop up. You can find it out: this window is the contents of StrGlobaliTomb4. You can find pAdr here, what we need, so double-click on it to select it:

Trng.pGlobTomb4->pAdr

Then choose pVetStructInventoryItems with the same method:

Trng.pGlobTomb4->pAdr->pVetStructInventoryItems

“P” is pointer, so you need a -> now, not a dot. Which really works, but, as you know, first we need to type the index in [] brackets (manually):

Trng.pGlobTomb4->pAdr->pVetStructInventoryItems[Extra]

After this, -> works no longer here, so you need a dot. – Choose IndiceStringa in the next window, so the route is finished:

Trng.pGlobTomb4->pAdr->pVetStructInventoryItems[Extra].IndiceStringa

Then add manually to this what we still need:

Trng.pGlobTomb4->pAdr->pVetStructInventoryItems[Extra].IndiceStringa = Timer;

What we did now is called auto-enumeration, it helps you to find and choose the memory zone fields easily.
Now try another route with this method, only by yourself:

GET.Vars.pTrngVars->Locals.Name.Delta.Short1

Auto-enumeration works even with several constant types, not only with variables.
As I said above, the theory is this:

If you see any “X_Y” or “enumX.Y” data in the sources, then X and Y are similar because it is the same data.

See trng.cpp source file where you can find all the “enum” constants. See eg. this:

Code:
  //  --------------- LOAD CONSTANTS FOR ENUM STRUCTURE: enumTRET----------------------
        enumTRET.PERFORM_ALWAYS = TRET_PERFORM_ALWAYS;
        enumTRET.PERFORM_ONCE_AND_GO = TRET_PERFORM_ONCE_AND_GO;
        enumTRET.PERFORM_NEVER_MORE = TRET_PERFORM_NEVER_MORE;
        enumTRET.EXECUTE_ORIGINAL = TRET_EXECUTE_ORIGINAL;
So you have two choices eg. if you want a TRET_PERFORM_NEVER_MORE flag:
- you type manually TRET_PERFORM_NEVER_MORE, or
- type the flag type with the “enum” word (enumTRET), then type a dot instead of “_”. Now wait a bit for the little window to pop up, then choose “PERFORM_NEVER_MORE”. (Or you can type all manually enumTRET.PERFORM_NEVER_MORE, if you want.)

Notes:
- If the auto-enumeration worked previously, then if there is some error in the code (even before trying to compile), the popping-up of the window will probably also be declined. So first you need to fix the error so the window can pop up again. Or, if nothing helps to make it work again, save the plugin, and reload it.
- The method works only in a “normal” order, like:

GET.Vars.
GET.Vars.pTrngVars->
GET.Vars.pTrngVars->Locals.

So it doesn’t work backwards (what we done when we tried to find the proper field previously without auto-enumeration):

Short1
Delta.Short1
Name.Delta.Short1
AkyV is offline  
Old 03-11-17, 18:58   #20
AkyV
Moderator
 
Join Date: Dec 2011
Location: Hungary
Posts: 2,574
Default

19. MyData variable structures

The declaration of your custom plugin variables is recommended in the MyData structures, under some special circumstances, for some special reasons.
This time you don’t need {} brackets, because the variable isn’t declared INSIDE the code (see eg. the air value code just above, when it is).

MyData structures are (almost) empty, will be filled with data by you, not Paolone, so you can’t find these structures in structures.h source file, but in structures_mine.h source file.

The basic structure:

StrMyData, i.e. “MyData structure”.
Declare the variable here, if your only reason is to use it in more than one code.

Declaration example:

Code:
  short BaddyAmmo;
You will refer to this variable in your code as:

Code:
  MyData.BadyAmmo
Usage example:
You have a Flipeffect trigger with a complicated code to force the ammo for the baddy. The result is stored in “MyData.BadyAmmo”. You also have a Condition trigger where you want to observe how many ammo has been forced, checking “MyData.BadyAmmo”.

Naturally you can also do that check, if the BaddyAmmo (not MyData!) variable is an output data of a function about forcing ammo.

The structure to keep the data saved for the level:

StrSavegameLocalData, i.e. “MyData.Save.Local” structure.
Declare the variable here, if you experience that the value of your custom plugin variable isn’t saved in savegames. (Either you want to use the variable in one or more codes. So if the variable is already declared in “MyData”, then remove it from there, and type here instead.)

For example:
Code:
  short BaddyAmmo;
  MyData.Save.Local.BadyAmmo
Important notes:
- “Local” means the variable will keep the variable only in the actual level, but it will turn into 0 when jumping to the next level.
“Keep” also means the variable value of that level will be restored if you jump back to that level – except if you used ResetHUB, because, in that case, the value will be 0 after jumping back!
- Be careful, if you test successfully that the value is saved even when it is not in “MyData.Save.Local” structure. Because, if you try it for more than one savegame slot, then you will perhaps experience that always the latest save will be restored always. – For example:

First save in Save0 slot with Value 10.
Second save in Save1 slot with Value 15.
Then value turns to 22.
Loading Save0 – restoring Value 15!

In that case you can solve the problem, using “MyData.Save.Local” instead, removing the original declaration of the variable.
Or, an easier way, if you always use a “MyData.Save” structure (local or global) a priori, if you want your data to be surely saved properly.

The structure to keep the data saved even between levels:

StrSavegameGlobalData, i.e. “MyData.Save.Global” structure.
It works like “MyData.Save.Local”, except, the variable won’t turn into 0 during the level jumps. (ResetHUB has no effect either this time.)

“MyData.Save.Local” is useful if you want to keep the value only in the required level – eg. a property of an enemy in that level.
“MyData.Save.Global” is useful if you want to keep the value in more than one level – eg. the actual health of Lara.

For example:
Code:
  short LaraHealth;
  MyData.Save.Global.LaraHealth
Special callbacks to prevent further save/load issues:

Even if you use MyData.Save variables, sometimes that is still not enough to save your data properly. - I experienced this with all the custom memory zone fields and even with some default memory zone fields.

You can use cbSaveMyData and cbLoadMyData callbacks to solve it (even if Paolone says in the remark of the callback you shouldn’t use it):
- In cbSaveMyData you type the code to save the variables.
- In cbLoadMyData you type the code to restore the variable values of cbSaveMyData, after loading a savegame.

The cbSaveMyData codes should be typed after this remark:

Code:
        if (SavingType & SAVT_LOCAL_DATA) {
              // save local data
(There also is a global section here, but I haven’t experienced – yet? - a problem like that with global variables, i.e. for level jumps.)

First for cbSaveMyData:

Example#1:

We have this Flipeffect code to force a new Source X value for loadcamera:

Code:
              {
              int *pLoadCamera_SrcX = (int*) 0x533978;
  
              MyData.Save.Local.LoadCamSourceX = Timer;
              *pLoadCamera_SrcX = MyData.Save.Local.LoadCamSourceX;
              }
(Why do we use “MyData.Save.Local.LoadCamSourceX”? The code could be simply “*pLoadCamera_SrcX = Timer”, as we told it in Chapter 16.
I mean “LoadCamSource is taking Timer value”, then “SrcX is taking LoadCamSource value” means “SrcX is taking Timer value”.
Very soon you’ll understand it.)

And this is the code of cbSaveMyData:

Code:
        //loadcamera
              {
                    int *pLoadCamera_SrcX = (int*) 0x533978;
  
                    MyData.Save.Local.LoadCamSourceX = *pLoadCamera_SrcX;
              }
So source X of loadcamera will be saved in MyData.Save.Local.LoadCamSourceX variable when a savegame is saved. (MyData.Save.Local.LoadCamSourceX is used for the trigger, but saving a game is not about using that trigger, so that variable now is free, so we can use it.)

Example#2:

Code:
        //quake
              MyData.Save.Local.QuakeIntensity = ReadMemVariable(5 | MEMT_CODE);
The Earthquake intensity (Code Memory Zone ID5) will be saved in MyData.Save.Local.QuakeIntensity variable when a savegame is saved.

Then for cbLoadMyData:

The cbLoadMyData codes should be typed just before this break:

Code:
        case NGTAG_LOCAL_DATA:
              // local data
              memcpy(&MyData.Save.Local, ParseField.pData, sizeof(StrSavegameLocalData));
              break;
Example:

Code:
        //loadcamera
  
        {
              int *pLoadCamera_SrcX = (int*) 0x533978;
  
              if (MyData.Save.Local.LoadCamSourceX == 0) {
                    *pLoadCamera_SrcX = *pLoadCamera_SrcX;
              } else {
                    *pLoadCamera_SrcX = MyData.Save.Local.LoadCamSourceX;
              }
        }
Now you can understand why we used MyData.Save.Local.LoadCamSourceX in the trigger code.
I mean, what we are say now is this:
“If the value hasn’t been forced yet in the trigger (so MyData.Save.Local.LoadCamSourceX=0), then keep the original value when loading the savegame (*pLoadCamera_SrcX = *pLoadCamera_SrcX). But if it has (see “else”) then restore the forced value, from MyData.Save.Local.LoadCamSourceX, when the savegame has been loaded.”
So we need in the trigger not only to have the wanted (Timer) value forced in the original field of that property (SrcX), but also having it in another variable (LoadCamSource).
(You may say “LoadCamSource is not 0 everyway in cbLoadMyData, because before any loading, cbSaveMyData also forced not 0 in LoadCamSorce”. You are right, but, believe me, this is a tested way to prevent this bug, even I don’t understand all the reasons.)

Last edited by AkyV; 03-11-17 at 22:53.
AkyV is offline  
Closed Thread

Bookmarks

Thread Tools

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is Off



All times are GMT. The time now is 21:19.


Powered by vBulletin® Version 3.8.11
Copyright ©2000 - 2017, vBulletin Solutions Inc.