Tomb Raider Forums  

Go Back   Tomb Raider Forums > Tomb Raider Level Editor and Modding > Tomb Raider Level Editor > Tutorials and Resources

Closed Thread
 
Thread Tools
Old 03-11-17, 19:07   #21
AkyV
Moderator
 
Joined: Dec 2011
Posts: 4,881
Default

20. Continuous events

Continuous events don’t belong to any trigger code. If you activate a continuous event, then that will be executed again and again (at each frame once), till you say it is enough.
These events need to be typed in cbCycleBegin callback:

Code:
  void cbCycleBegin(void)
  {
  
  }
For example, we have a function that forces the required health for a baddy. If you force the function continuously, then the baddy will keep that health value always:

Code:
  void cbCycleBegin(void)
  {
        BaddyHealth (…);
  }
But you can type the continuous codes directly here, without any function:

Code:
  void cbCycleBegin(void)
  {
        //force baddy health
        code
  
        //force Static object size
        code
  
  }
Naturally the codes should be separated from each other, to keep them clear – as you can see in the example just above, using // remarks there for that.
However, we dont need {} between the codes to separate the codes from each other – only if they have some declaration:

Code:
  void cbCycleBegin(void)
  {
        //force baddy health
        {
        int BaddyHealth;
  
        code
        }
  
        //force Static object size
        code
  
  }
Let’s see this real example – you want this custom statistics: “type on the screen how many small and big medipacks are used by Lara in the game/actual level”. – Something like this, eg. in the upper left corner of the screen:

Code:
		Level		Game
Small medipack	  A		  B
Big medipack	  C		  D
Naturally this code needs to be refreshed again and again (to refresh the variable value that counts the amount, and to refresh what is typed on the screen, each time if a medipack is used) or else the data about the amount used will be false.
We won’t follow the whole code now, because making a custom Statistics screen is complicated. So we show only the first part of the code: how to refresh the variables continuously – where the A, B, C, D values are stored. (So we ignore the part when those variables are printed on the screen, with the required color, position etc.)

See the C150 code again, that checks if Lara uses a medipack, to understand the cbCycleBegin code:

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;
We type this in cbCycleBegin now:

Code:
        //medipack statistics
        if (Trng.pGlobTomb4->pBaseGlobalTriggers->TestPresoLittleMedipack == 1) {
              MyData.Save.Local.Smallmedi++;
              MyData.Save.Global.Smallmedi++;
        }
        if (Trng.pGlobTomb4->pBaseGlobalTriggers->TestPresoBigMedipack == 1) {
              MyData.Save.Local.Bigmedi++;
              MyData.Save.Global.Bigmedi++;
        }
  
        code to print variables on the screen;
So we have these variables:

MyData.Save.Local.Smallmedi – small medipacks used in the level are stored here
MyData.Save.Global. Smallmedi - small medipacks used in the game are stored here
MyData.Save.Local.Bigmedi - big medipacks used in the level are stored here
MyData.Save.Global. Bigmedi - big medipacks used in the game are stored here

As I said above, ++ means X=X+1, so eg. “MyData.Save.Local.Smallmedi++;” means “each time when a small medipack is used then increase the amount of small medipack used in this level by 1”.

But what we did here is hardcoding: the Statistics will be printed on the screen everyway, either you want it or not. (Because “if” conditions are only for MyData Smallmedi/Bigmedi counters, the printing code is out of the conditions, it will be always executed.)
That is why you need a custom flag to switch this Statistics on or off the screen:

;------------------ Section for flipeffect trigger: description of the trigger -------------------------------
<START_TRIGGERWHAT_9_O_H>
503: <&> Turn on/off medipack custom statistics
<END>

;type here the sections for arguments used by above flipeffects
<START_EFFECT_503_T_H>
0: On
1: Off
<END>

Code:
  case 503:
        if (Timer == 0) MyData.Save.Global.MediStatFlag = 1;
        if (Timer == 1) {
              MyData. Save. Global.MediStatFlag = 0;
              some code to remove the text off the screen;
        }
  break;
Also modifying the cbCycleBegin code:

Code:
        //medipack statistics
        if (Trng.pGlobTomb4->pBaseGlobalTriggers->TestPresoLittleMedipack == 1) {
              MyData.Save.Local.Smallmedi++;
              MyData.Save.Global.Smallmedi++;
        }
        if (Trng.pGlobTomb4->pBaseGlobalTriggers->TestPresoBigMedipack == 1) {
              MyData.Save.Local.Bigmedi++;
              MyData.Save.Global.Bigmedi++;
        }
        if (MyData.Save.Global.MediStatFlag == 1) {
              code to print variables on the screen
        }
As you can see, the flag (a BYTE variable) is called “MyData.Save.Global.MediStatFlag”.
If we turn it into 1 in the trigger, then the continuous forced printing will start, thanks to cbCycleBegin, being refreshed at every single frame.
If we turn it into 0 in the trigger, then the continuous forced printing cannot continue in cbCycleBegin, and the text will be removed by the trigger code.
But the custom flag is no effect on adding 1, naturally. I mean, even if the text is not printed, the change of the values still needs to be calculated in the background, so this part remains hardcoded. I mean, if you turn back on the printing later, the values on the screen will be proper, because MyData Smallmedi/Bigmedi counters were properly updated in the meantime.

As you can see, the flag is “MyData.Save.Global” this time:
- If we save/load the game, and the flag value 1 won’t be restored, because we didn’t use a “Save” structure, then the forced printing will be aborted.
- If we jump a level, the variable would lose value 1 if it were a local one, not a global one, also aborting the forced printing.
(In both wrong cases, only the forced printing is what is aborted, but F503 hasn’t been activated again to remove the text. So the text remains on the screen, but won’t be refreshed any more.)

Note:
- See also progressive actions (below) for continuous events.
- There are further situations as well, when your code will be continuous, but, as a beginner, you don’t need to know about it.

Starting values:

What if you want some starting values for your continuous events?
For example, if you want this Statistics to start with 5 small medipacks?

Code:
 MyData.Save.Local.Smallmedi = 5;
MyData.Save.Global.Smallmedi = 5;
  if (Trng.pGlobTomb4->pBaseGlobalTriggers->TestPresoLittleMedipack == 1) {
      MyData.Save.Local.Smallmedi++;
      MyData.Save.Global.Smallmedi++;
  }
No, it is wrong!
“There are 5 medipacks initially. One is used, it will be increased to 6. At the next frame, the code will be checked again, executing equal with 5 again, so the statistics says 5 medipacks again. If another one is also used, it will be increased to 6 again.”

So the starting amount must be adjusted out of this continuous event, in some single event, that leads to this continuous event.
For example, in a trigger that says “adjust the starting value for medipack statistics”, you adjust initial “MyData.Save.Local.Smallmedi” and “MyData.Save.Global.Smallmedi” values in the trigger code.

“FGT_SINGLE_SHOT_RESUMED”:

FGT_SINGLE_SHOT_RESUMED is an important flag in GlobalTriggers, which says:
“Each time when A condition becomes true, B event will be activated, but only in the first moment of A”.
For example, “A condition” is “Lara has 50% vitality”. Except if she is just being hurt, she keeps that vitality even in the further moments, minutes. If she keeps it and B event is not a thing that happens only once a priori (like, spawning a baddy), then B event will be started again and again – like a start of an audio track, till her health is 50%. But naturally what we want is not to hear the first moment of an audio track again and again.
What we want is: play the track properly if the condition is true, and then, if Lara’s health increases/decreases now, but then reaches 50% again, play the track properly again – which works only having that FGT_SINGLE_SHOT_RESUMED flag.

This situation could also be important in plugin continuous events.
I recommend this trick, with a custom flag, with this theoretical code:

Trigger code:
Flag = 1; //to enable the continuous event

cbCycleBegin code:
if (Flag == 1 && vitality == 50 %) {
start audio track;
Flag = 0;
}
if (vitality != 50 %) Flag = 1;

The wrong data:

A typical place for the wrong data is a continuous event. Like I said above: you need a MediStatFlag to narrow the event only for the period when you want it.
But there can be other issues as well. I mean, for example, you have a GET.pItem for Item ID 248 in a continuous event, but when you start the game, then the engine wants to execute it first even in the title. But there is no Item ID 248 in the title, the game is going to crash.
The code should be narrowed. Eg. this Item ID 248 is in Level 1, so you also examine in the code if it is Level 1 or not:

if (ReadMemVariable(3 | enumMEMT.CODE) == 1) {
...
}

Restore after loading the game:

cbSaveMyData and cbLoadMyData callbacks are not linked to each other everyway. I mean, see that quake example again of cbSaveMyData. I don’t know the reason, but cbLoadMyData is useless to restore that data. I thought it would be a good idea to use a continuous event to restore that intensity. I mean, we will restore continuously the intensity in Code Memory Zone ID5, from MyData.Save.Local.QuakeIntensity. – I mean, MyData.Save.Local.QuakeIntensity will be forced again and again.
Except, that I used another version of the “FGT_SINGLE_SHOT_RESUMED” trick, to force it only once:

Code:
  if (MyData.Save.Local.QuakeIntensity != 0) {
        WriteMemVariable(5 | MEMT_CODE, MyData.Save.Local.QuakeIntensity);
        MyData.Save.Local.QuakeIntensity = 0)
  }
(The full code would be a bit more complicated than that, because eg. what if we want to stop the quake. But we skipped that part now.)

Either coming from cbSaveMyData or not, you can use continuous events to restore data that should be saved.

Note:
There are further methods to handle data to be loaded properly. See eg. cbInitLoadNewLevel callback, where one of the main goals is turn Save.Local variables to 0 when you leave the level. (Naturally it has already been made by Paolone.)
Or see cbInitLevel callback what we discuss soon. (Yes, the “init” is a magic word for this kind of tasks.)

Last edited by AkyV; 04-12-17 at 22:10.
AkyV is offline  
Old 03-11-17, 19:17   #22
AkyV
Moderator
 
Joined: Dec 2011
Posts: 4,881
Default

21. Prefixed flags

The prefixed plugin flags signal events or properties in the code. You can examine them or (sometimes) you can even force them.

“Examine X flag of Y thing” = “If Y thing has X property”
“Force X flag of Y thing” = “Add X property to Y thing”

For example, GET_SCRIPT_SETTINGS constant is about StrScriptSettings structure:

Code:
  typedef struct StrScriptSettings {
        bool TestDiagnostic;  // == true when diagnostic is enabled
        char *pFMV_Extension;  // text of "FMV=" script command in [PCExtensions] section
        DWORD FlagsMain;        // SMAIN_... flags (option section) only for read 
        DWORD FlagsLevel;       // SLEV_ ... flags (level section) only for read
        DWORD DiagnosticEDGX;   // EDGX_ flags
        DWORD DiagnosticDGX;    // DGX_ flag types
  }ScriptSettingsFields;
As you can see, here, for example, you can read “Level Flags”. “Level Flags” are Script commands, and, as the remark says, SLEV flags control them in the plugin, like:

Horizon=ENABLED (= SLEV_HORIZON flag is added)
Horizon=DISABLED (= SLEV_HORIZON flag is missing)
Timer=ENABLED (= SLEV_TIMER flag is added)
Timer=DISABLED (= SLEV_TIMER flag is missing)
Etc.

You can find the whole list of SLEV flags (just like the lists of any prefixed flags) in Tomb_NextGeneration.h source file:

Code:
  // flags per scriptlevel
  #define SLEV_YOUNG_LARA 0x0001
  #define SLEV_WEATHER   0x0002
  #define SLEV_HORIZON  0x0004
  #define SLEV_LAYER1   0x0008
  #define SLEV_LAYER2   0x0010
  #define SLEV_STAR_FIELD  0x0020
  #define SLEV_LIGHTNING 0x0040
  #define SLEV_TRAIN    0x0080
  #define SLEV_PULSE    0x0100
  #define SLEV_COL_ADD_HORIZON  0x0200
  #define SLEV_RESET_HUB 0x0400
  #define SLEV_LENS_FLARE  0x0800
  #define SLEV_TIMER    0x1000 
  #define SLEV_MIRROR   0x2000
  #define SLEV_REMOVE_AMULET 0x4000
  #define SLEV_NO_LEVEL 0x8000
Flag values are always organized in a normal 1, 2, 3 etc. range, but it is a binary range, not decimal:

Code:
  YOUNG_LARA        Binary 1 (Bit1)   Decimal 1         Hexadecimal 0x0001
  WEATHER           Binary 2 (Bit2)   Decimal 2         Hexadecimal 0x0002
  HORIZON           Binary 3 (Bit3)   Decimal 4         Hexadecimal 0x0004
  LAYER1            Binary 4 (Bit4)   Decimal 8         Hexadecimal 0x0008
  LAYER2            Binary 5 (Bit5)   Decimal 16        Hexadecimal 0x0010
  Etc.
“&” sign is needed (only one & this time!) if you want to check if a flag is added – which means “if the command is typed (ENABLED) in the script” now.
So this is what we need eg. if we want to check that “Horizon=ENABLED” and “Timer=ENABLED” are both in the Script:

Code:
  Get (enumGET.SCRIPT_SETTINGS, 0, 0);
  if (GET.ScriptSettings.FlagsLevel & enumSLEV.HORIZON &&
  GET.ScriptSettings.FlagsLevel & enumSLEV.TIMER) { ...
Naturally this means “if Horizon and Timer are both disabled now”:

Code:
  Get (enumGET.SCRIPT_SETTINGS, 0, 0);
  if ((GET.ScriptSettings.FlagsLevel & enumSLEV.HORIZON &&
  GET.ScriptSettings.FlagsLevel & enumSLEV.TIMER) == false) { ...
“|” sign (ALT+keypad 124) is needed (only one | this time!) if you want to add a flag. The remark says it is impossible with SLEV flags (“only for read”) – besides it seems also impossible many times when “officially” we should be able to do that. (See eg. to force key commands in the *GET.Input.pGameCommandsWrite route.) – But, remember, you could see some successful examples about it in the chapter of Condition triggers above, with CTRET flags.
That is why (except for CTRET flags) I suggest a more primitive but definitely useable method, (to try) to add flags – even now, when the remark says it is impossible:

See again the list of SLEV flags.
0x are hexadecimal values, as I said. I suggest turning them into decimal now, it is easier to understand them then.
So, as you see, Horizon command is hexadecimal 4 (decimal 4), Timer command is hexadecimal 1000 (decimal 4096). They sum is 4100.
Which means, the aggregated value of them if both are enabled is 4100 – so you need to add 4100 in the field to force their enabled status. (Not “adjust” but “add” 4100, because there can be other values as well enabled. For example, Lightning flag with hexadecimal 40/decimal 64 value, so you need to add that 4100 to that 64 now, if Lightning is just enabled.)
But be careful:

Horizon disabled+Timer disabled+ Horizon enabled+Timer enabled = 0+0+4+4096 = 4100
Horizon enabled+Timer disabled+ Horizon enabled+Timer enabled = 4+0+4+4096 = 4104
Horizon disabled+Timer enabled+ Horizon enabled+Timer enabled = 0+4096+4+4096 = 8196
Horizon enabled+Timer enabled+ Horizon enabled+Timer enabled = 4+4096+4+4096 = 8200

So, if you force a flag (enabled status) when that flag is already used (when that status is already enabled), then you get the wrong value. – That is why our code will look this way now:

Code:
        Get (enumGET.SCRIPT_SETTINGS, 0, 0);
        if ((GET.ScriptSettings.FlagsLevel & SLEV_HORIZON) == false) *Trng.pGlobTomb4->pVetCodeMemory[4].pShort += 4;
        if ((GET.ScriptSettings.FlagsLevel & SLEV_TIMER) == false) *Trng.pGlobTomb4->pVetCodeMemory[4].pShort += 4096;
So if the Level Flags don’t have “Horizon=ENABLED”, then add Value 4 of “Horizon=ENABLED”.
And, if the Level Flags don’t have “Timer=ENABLED”, then add Value 4096 of “Timer=ENABLED”.

And, yes, this time the value is forced via “Variable” Flipeffect trigger field index. I mean, Index4 for Code Memory Zone is (see F283) “Script Dat. Level Flags (Use bit operation to read or change) (Short)”.

Naturally “removing a flag” works in a similar way, but reversed:

Code:
        Get (enumGET.SCRIPT_SETTINGS, 0, 0);
        if (GET.ScriptSettings.FlagsLevel & SLEV_HORIZON) *Trng.pGlobTomb4->pVetCodeMemory[4].pShort -= 4;
        if (GET.ScriptSettings.FlagsLevel & SLEV_TIMER) *Trng.pGlobTomb4->pVetCodeMemory[4].pShort -= 4096;
Let’s see some other examples with other prefixed flags.

CMD_ flags are about the key commands, so this is what we need eg. if we want to check that “Up arrow” and “SHIFT” keys are both pushed down:

Code:
  Get (enumGET.INPUT, 0, 0);
  if (GET.Input.GameCommandsRead & enumCMD.UP &&
  GET.Input.GameCommandsRead & enumCMD.WALK) { ...
Examine if the room where Lara is, is a quicksand room, with a FROOM flag:

Code:
  Get (enumGET.LARA, 0, 0);
  Get (enumGET.ROOM, GET.pLara->Room, 0);
  if (GET.pRoom->FlagsRoom & FROOM_QUICKSAND) {...
Remove the poisoned status from the enemy selected in “Object to trigger” box of the Action trigger – this time (just because why not) not forcing via “Variable” Flipeffect trigger field index (because, yes, that index method could be also possible now):

Code:
  Get (enumGET.ITEM, ItemIndex, 0);
  if (GET.pItem->FlagsMain & enumFITEM.POISONED)) GET.pItem->FlagsMain -= 256;
I mean:

Code:
  // mnemonic constant for FlagsMain of StrItemTr4 structure
  #define FITEM_NONE 0x0000 // used only to clear flags
  
  #define FITEM_NOT_YET_ENABLED 0x0020  // the item has not yet been enabled (triggered)
  #define FITEM_KILLED_WITH_EXPLOSION 0x0040 // trng flag, added to remember that this enemy has been killed with an explosion
  #define FITEM_POISONED 0x0100 // enemy (or lara?) has been poisoned
  #define FITEM_AI_GUARD 0x0200 // enemy was over a AI_GUARD item
  #define FITEM_AI_AMBUSH 0x0400 // emeny was over a AI_AMBUSH item
Hexadecimal 100 = decimal 256.

If OCB of the object in “Object to trigger” box of the Action trigger, has the property of OCB 512:

Code:
  Get (enumGET.ITEM, ItemIndex, 0);
  if (GET.pItem ->OcbCode & 512) {...
Notes:
- Naturally you could do experiments to examine/add/remove flags even with trng.dll triggers (I mean, using them as exported functions) eg. like these:

; Set Trigger Type - CONDITION 44
; Exporting: CONDITION(44:44) for PARAMETER(83) {Tomb_NextGeneration}
; <#> : Local Short Beta2
; <&> : Variables. The <#>Numeric Variable has the (E)Bit set
; (E) : Bit 9 ($00000200 ; 512)
PerformConditionTrigger(NULL, 44, 83, 9);

; Set Trigger Type - CONDITION 53
; Exporting: CONDITION(53:38) for PARAMETER(4) {Tomb_NextGeneration}
; <#> : Script Dat. Level Flags (Use bit operation to read or change) (Short)
; <&> : Variables. The <#>Code Memory has the (E)BIt clear
; (E) : Bit 12 ($00001000 ; 4096)
PerformConditionTrigger(NULL, 53, 4, 12);

; Set Trigger Type - FLIPEFFECT 260
; Exporting: TRIGGER(2065:38) for FLIPEFFECT(260) {Tomb_NextGeneration}
; <#> : Variables. Memory. Clear in <&>Selected Item Memory the (E)Bit
; <&> : OCB Code (The value you typed in OCB of this item) (Short)
; (E) : Bit 8 ($00000100 ; 256)
PerformFlipeffect(NULL, 260, 17, 8);

; Set Trigger Type - FLIPEFFECT 234
; Exporting: TRIGGER(592:38) for FLIPEFFECT(234) {Tomb_NextGeneration}
; <#> : Variables. Numeric. Set in <&>Variable the (E)bit
; <&> : Local Short Alfa1
; (E) : Bit 2 ($00000004 ; 4)
PerformFlipeffect(NULL, 234, 80, 2);

- Later we will talk about how you can create “real” custom flags, not only just simple markers like “MyData.Global.MediStatFlag” above. (See the chapters of Parameters and Customize Script commands, OCB codes.)

FIL flags:

There are several situations to load data into the game:

- new game
- level jump forward (simple)
- level jump forward (with ResetHUB)
- level jump backward (simple)
- level jump backward (with ResetHUB)
- save/load game

You need cbInitLevel callback and FIL flags, if you want to handle these situations properly. cbInitLevel callback will be called each time when any of those situations happens.
Paolone tells this about FIL flags, in Tomb_NextGeneration.h:

Code:
  #define FIL_FROM_SAVEGAME    0x0001
  #define FIL_FROM_NEW_LEVEL   0x0002 // level started from new game of title or with level jump and resethub
  #define FIL_FROM_LEVEL_JUMP  0x0004 // when there is a level jump (note: if resethub was enabled, this flag will be missing)
  #define FIL_PRESERVE_LARA    0x0008 // this flag is present: from savegame or from level jump (but no resethub) 
  #define FIL_PRESERVE_LEVEL   0x0010 // this flag is present: from savegame or from go and back for level jump and no resethub
  #define FIL_FROM_LIKE_SAVEGAME 0x0020 // this flag is enabled if level is from savegame or from level jump with no reset hub and go and back from two levels.
My personal experience is this:

- single FIL_FROM_NEW_LEVEL: new game, level jump (forward/backward) – with ResetHUB
- FIL_FROM_LEVEL_JUMP + FIL_PRESERVE_LARA: level jump (forward) - simple
- FIL_FROM_LEVEL_JUMP + FIL_PRESERVE_LARA + FIL_PRESERVE_LEVEL + FIL_FROM_LIKE_SAVEGAME: level jump (backward) - simple
- FIL_FROM_SAVEGAME + FIL_FROM_LEVEL_JUMP + FIL_PRESERVE_LARA + FIL_PRESERVE_LEVEL + FIL_FROM_LIKE_SAVEGAME: save/load game

Example#1:

Save.Global variables will always keep their values forever – we said above, but what does that mean exactly?
Well, the variable value will be naturally lost, if you quit the game with Exit. However, if you only go back to the title from the game, and then start a new game, not exiting, then the variable data will be kept.
Let’s see that medipack statistic example again. We naturally would like to turn the medipack counters back to 0, if you have been gone back to the title. (Because it should be 0 for a new game, and it should have the proper value, i.e. the value you kept, saved in a savegame, only if you load a savegame from the title.) That is why, first of all, we type this in cbCycleBegin (Code Memory Zone ID3, as I said above, is about the level ID):

Code:
        //Title check
        if (ReadMemVariable(3 | enumMEMT.CODE) == 0) MyData.Save.Global.TitleCheck = 0;
So the game will always check the level ID. If it is 0 (= title), then the value of MyData.Save.Global.TitleCheck will be 0.

And this is the code we need in cbInitLevel:

Code:
              if (MyData.Save.Global.TitleCheck == 0 && FIL_Flags & FIL_FROM_NEW_LEVEL) {
                    MyData.Save.Global.BigMediStat = 0;
                    MyData.Save.Global.SmallMediStat = 0;
                    MyData.Save.Global.TitleCheck = 1;
              }
So if data is just loaded, then the game should detect that FIL_Flags variable (= the function input data of this callback) has a single FIL_FROM_NEW_LEVEL flag in that moment, which means you have just started a new game or made a level jump with ResetHUB. First time when it happens it is surely a new game (not a ResetHUB jump), so you have just left the title, TitleCheck is 0 now. That is why now the counters will be turned back to 0, and the TitleCheck variable will get Value 1.
If Lara doesn’t go back to the title later, then the game still can detect that single FIL_FROM_NEW_LEVEL, when you make a level jump with ResetHUB later. However, TitleCheck is not 0 now (because you were not in the title previously), so counters won’t be turned to 0 this time.

Example#2:

You want a “distance travelled” statistic not only for the game but even for the actual level.

Code:
              if (FIL_Flags == 2 || FIL_Flags == 12 || FIL_Flags == 60) {
                    MyData.Save.Local.DistTravNull = MyData.Save.Global.DistTravStat - MyData.Save.Local.DistTravStat;
              }
The block above is a cbInitLevel block, will be executed:

- if FIL_Flags = 2 (=FIL_FROM_NEW_LEVEL, new game or jump with ResetHUB), or
- if FIL_Flags = 12 (=4+8= FIL_FROM_LEVEL_JUMP + FIL_PRESERVE_LARA, simple level jump forward), or
- if FIL_Flags = 60 (=4+8+16+32= FIL_FROM_LEVEL_JUMP + FIL_PRESERVE_LARA, simple level jump backward).

So it won’t be executed when you save/load the game.
That is why each time you enter the level as a new game, or jumping from another level, then a temporary variable (“DistTravNull”) will get its value, which is the difference between the game and the level distance, which is naturally 0 when the first level starts.

Code:
              MyData.Save.Global.DistTravStat = *Trng.pGlobTomb4->pVetMemorySavegame[75].pLong / 419;
              MyData.Save.Local.DistTravStat = MyData.Save.Global.DistTravStat - MyData.Save.Local.DistTravNull;
The block above is a cbCycleBegin block, so it will be executed continuously = the statistic data will be always kept fresh.
SaveGame Memory ID 75 is “Statistics. Distance (Long)”. You need to divide the value by 419, to get data in meters, in MyData.Save.Global.DistTravStat. This is naturally the travel for the whole game.
MyData.Save.Local.DistTravStat is for the distance travelled in the actual level. “DistTravNull” is zero, so the level distance will be always the game distance in the first level, because game distance minus zero is game distance.
Let’s suppose that Lara travels 521 meters in the first level, so these are the values at the level jump:

Game distance = 521
Level distance = 521

Just after the level jump, the FIL_Flags condition will be examined again:

Null = Game – Level = 521 – 0 = 521.

The distance on the new level is naturally 0, MyData.Save.Local.DistTravStat still has no value on this level, so “DistTravNull” variable turns into 521.

The cbCycleBegin block will be executed again and again, the difference is always 521 meters between the game and the level distance.

Level = Game - 521

Let’s suppose that Lara travels 1207 meters in the second level, so these are the values at the level jump:

Game distance = 1728
Level distance = 1207

Just after the level jump, back to the first level, the FIL_Flags condition will be examined again:

Null = Game – Level = 1728 – 521 = 1207

The distance on the level is naturally still 521, where we left it, MyData.Save.Local.DistTravStat has still Value 521 on this level, so “DistTravNull” variable turns into 1728-521=1207.

The cbCycleBegin block will be executed again and again, the difference is always 1207 meters between the game and the level distance.

Level = Game - 1207

Let’s suppose that Lara travels 140 meters now in the first level, so these are the values at the level jump:

Game distance = 1868 (=1728+140)
Level distance = 661 (=521+140)

Just after the level jump, to the third level, the FIL_Flags condition will be examined again:

Null = Game – Level = 1868 – 0 = 1868

The distance on the new level is naturally 0, MyData.Save.Local.DistTravStat still has no value on this level, so “DistTravNull” variable turns into 1868.
Etc.

Notes:
- Naturally the global DistTravStat counter also must be turned into 0 when the game starts (not leaving the EXE), typing it in the FIL_FROM_NEW_LEVEL examination of the medipacks above.
- You need to tell in the trigger remark that you cannot use level jump back in your game with ResetHUB, if you want a level distance statistic, because, as I said above, Save.Local variables won’t be restored if you have a ResetHUB, going back to a previous level. (Level jump forward is not a problem, the zero value of the distance variable of the new level would be zero even with or without a ResetHUB.)

Last edited by AkyV; 18-11-17 at 19:35.
AkyV is offline  
Old 03-11-17, 19:25   #23
AkyV
Moderator
 
Joined: Dec 2011
Posts: 4,881
Default

22. Searching for subjects with inexact identification

“Get” functions are great if you want the properties of something that can be identified exactly: Lara, an object with the required ID, the required string index etc.
But what if what you want is not so exact, eg. “all the Light bulbs in X Room” or “all the Moveable objects with OCB Y” etc.? – In that case you need to use the “Find” function:

Find (FIND constant, first input value [Slot], second input value [room], third input value [OCB], fourth input value [some extra], pointer)

For example, we have several PUSHABLE_OBJECT1 objects scattered in the level, you cannot push/pull many of them, so they have 128+256=384 OCB value. You want enable them to be pushable again, only the pullable (128) disablement remains. So you search for PUSHABLE_OBJECT1 objects with OCB=384, forcing 128 as a new OCB.

You can find the list of Find functions in Tomb_NextGeneration.h source file. What we need now is the first of them:

Code:
  #define FIND_ITEM  0  // return values in FIND.VetItems and FIND.TotItems Looks for moveable items
                                           // INPUT: SlotType=SLOT_ (or -1), RoomIndex (or -1), Ocb (or -1)
So we have three input values now, without a pointer. The code is this:

Code:
      {
        int i;
        int Index;
  
        Find (enumFIND.ITEM, SLOT_PUSHABLE_OBJECT1, -1, 384, -1, NULL);
              for (i=0 ; i < FIND.TotItems ; i++) {
                    Index = FIND.VetItems[i];
                    Get (enumGET.ITEM, Index, 0);
                    GET.pItem-> OcbCode = 128;
              }
       }
Find (enumFIND.ITEM, SLOT_PUSHABLE_OBJECT1, -1, 384, -1, NULL);

This part says:
enumFIND.ITEM – type of finding
SLOT_PUSHABLE_OBJECT1 – the item type we are searching for
first -1 – no room input this time
384 – we search for items with OCB 384
second -1 – no fourth input value for this type
NULL - no pointer for this type

for (i=0 ; i < FIND.TotItems ; i++) {

This part is a cycle, that will be 0, 1, 2, 3 etc:
i=0, i++=>i=1, i++=>i=2, i++=>i=3 etc.
So the cycle will increase with i++. Except if it reaches the value of “TotItems” variable.
“Tot” is always “total” in the names, it is always about the maximum amount. “TotItems” this time is the maximum amount of items defined in the Find function name: PUSHABLE_OBJECT1 objects with OCB 384.
So, if you have eg. 12 PUSHABLE_OBJECT1 objects with OCB 384, then TotItems is 12, the maximum of “i” will be 11 (0, 1, 2... 10, 11).

Index = FIND.VetItems[ i];

As I said above, “vet” are vectors, with many indices. “VetItems” are naturally about item indices. So this part will define the index that belongs to the actual “i”. – For example:

First i=0. The index of this item is 396.
Then i=1. The index of this item is 28.
Then i=2. The index of this item is 54.
Then i=3. The index of this item is 127.
Then i=4. The index of this item is 76.
Etc.

Get (enumGET.ITEM, Index, 0);
GET.pItem-> OcbCode = 128;

This part will change the OCB for the actual index:

When i=0, then the OCB of Item 396 will turn into 128.
When i=1, then the OCB of Item 28 will turn into 128.
When i=2, then the OCB of Item 54 will turn into 128.
When i=3, then the OCB of Item 127 will turn into 128.
When i=4, then the OCB of Item 76 will turn into 128.
Etc.

All of this happens very fast. However, it is a cycle, I don’t think it works well as a prompt trigger code, cbCycleBegin is recommended. When the Flipeffect trigger turns the OCB value to the proper one, then – like in the case above ith the medipack statistics – a custom flag in the trigger should enable this cycle.

Please note that this time these are not OCB flags. These 128 and 384 values are the whole values of the OCB, there are no other properties (eg. 2048 for to disable climbing) in the OCB now. When I say eg. that OCB is 384, that means all the OCB flags it has are 128 and 256 (128+256=384) now.

Without Find function:

The “for” cycles works even without Find functions. Here is an example:
You want this trigger: “choose 3 flipmap indices. If you have chosen all of them, then activate all of them. Further flipmap indices cannot be chosen for this setup in this level”.
First, you need a Flipeffect trigger where you can ask those flipmap indices (0: FlipMap= 0, 1: FlipMap = 1 etc., in Timer parameter of the trigger) into a custom vector:

In MyData.Save.Local structure you need to declare this vector, and another variable to store the vector indices:

Code:
  BYTE VetFlipMap [3]; //BYTE, because the biggest useable flipmap ID is 255, which is enough for BYTE
  BYTE FlipIndex; // BYTE, because the vector indices we use are only 0, 1, 2.
(MyData.Save.Local.FlipIndex is naturally NOT the input FlipIndex variable of cbFlipEffectMine.)

So we have three vector indices (0, 1, 2):
VetFlipMap [0] – for the first flipmap ID chosen, VetFlipMap [1] – for the second flipmap ID chosen, VetFlipMap [2] – for the third flipmap ID chosen.

The trigger code:

Code:
        if (MyData.Save.Local.FlipIndex < 3) {
              MyData.Save.Local.VetFlipMap [MyData.Save.Local.FlipIndex] = Timer;
              MyData.Save.Local.FlipIndex++;
        }
So eg. you have a level with five flipmaps. One trigger is placed in the map for each flipmap (One for Flipmap0, one for Flipmap1 etc.). The player can choose maximum three of them. If they are all selected, then the flip will happen for them, the other two triggers is useless.
For example:

Step 1: trigger with Timer 2 is activated. VetFlipMap [0] =2. FlipIndex initial value is naturally 0 which turns to 1 now.
Step 2: trigger with Timer 4 is activated. VetFlipMap [1] =4. FlipIndex 1 turns to 2.
Step 3: trigger with Timer 1 is activated. VetFlipMap [2] =1. FlipIndex 2 turns to 3. – Further triggers like that go useless (see FlipIndex<3 condition).

Please tell in the trigger remark that more than one trigger for a flipmap are not allowed to be placed, and triggers need to be One Shot, or else less than three flipmaps will be activated – for example, in the case if Lara activates the trigger for Flipmap2 twice:

VetFlipMap [0] =2
VetFlipMap [1] =4
VetFlipMap [2] =2

(Naturally this instruction for the player is not necessary if we rule that One Shot bug out in a bit more complicated trigger code, but we skip that part now.)

cbCycleBegin code (the exported function is F125 to activate flipmaps):

Code:
  if (MyData.Save.Local.FlipIndex == 3) { //if you have just chosen the third flipmap
        PerformFlipeffect (NULL, 125, MyData.Save.Local.VetFlipMap [0], 0);
        PerformFlipeffect (NULL, 125, MyData.Save.Local.VetFlipMap [1], 0);
        PerformFlipeffect (NULL, 125, MyData.Save.Local.VetFlipMap [2], 0);
  }
Because the map ID in F125 triggers exported into a plugin code is the second number, after 125.

Or the version of cbCycleBegin code with “for” (which should naturally be used first of all if you have much more vector index than three, so if you don’t want to repeat the entries of the exported function too many times):

Code:
  if (MyData.Save.Local.FlipIndex == 3) { //if you have just chosen the third flipmap
        BYTE i;
  
        for (i=0; i<3; i++) PerformFlipeffect (NULL, 125, MyData.Save.Local.VetFlipMap [i], 0);
  }
Notes:
- If you ever try a setup to rule out levels where Lara has already been (=to rule out levels jumped back), then I suggest a global (!) MyData.Save variable where the Level ID’s are present as flags: Level1 is Bit1, Level2 is Bit2 etc. So you need a code in cbInitLevel – probably the best if you try it for all the cases, so without any FIL flag. Check here the actual level ID (Code Memory Zone ID5), and if that ID isn’t present in the variable as a flag, then that flag will be added.
You need another cbInitLevel code in another MyData.Save.Global variable, without FIL flags. Here you examine if the first variable has the actual level ID. If it has, then the level ID (flag) will be added to the second variable.
In the third code (where you want to rule out the backjumps) all you need to do is check if the second variable has the actual level flag. If it has, then the code won’t be executed.
- Turn MyData.Save.Local.FlipIndex with another trigger to 0 if you want to use the trigger for the three indices again.
- Yes, I used “for” cycles only for vector setups above, with a good reason. Theoretically it is possible, but isn’t really useful to use them in other setups.

The while cycle:

The “while” cycle is a special “for” cycle. You need to use it instead of “for” if you don’t know (surely) how long the cycle is.
I mean, in the examples above we knew surely that we have 12 pushblocks (because it is counted in TotItems) and we have 3 flipmaps (because this is what the trigger code is about).
But what if eg. we have that flipmap trigger, but in this setup: “choose 3 flipmap indices. If you have chosen all of them, then activate all of them. Further flipmap indices cannot be chosen for this setup in this level. If Lara extracts pistols when all of them have been chosen then the procedure will be aborted”.

Code:
  if (MyData.Save.Local.FlipIndex == 3) { //if you have just chosen the third flipmap
        BYTE i;
  
        i = 0;
  
        while (i<3) {
              if (PerformConditionTrigger(NULL, 35, 1, 0)) {
                    MyData.Save.Local.FlipIndex = 0;
                    break;
              }
              PerformFlipeffect (NULL, 125, MyData.Save.Local.VetFlipMap [i], 0);
              i++;
        }
  }
So if all the flipmaps have been chosen, the game starts activating the flipmaps very fast. But if it detects, before activating all of them, that the pistols are extracted (see “PerformCondition”), the procedure will be aborted (see “break”). – For example:

1. Pistols are already extracted when last ID is chosen.
2. “Activate flipmaps” – none for all.

1. Last ID is chosen.
2. “Activate flipmaps” – for VetFlipMap [0].
3. “Activate flipmaps” – for VetFlipMap [1].
4. Extracting pistols.
5. “Activate flipmaps” – none for VetFlipMap [2].

1. Last ID is chosen.
2. “Activate flipmaps” – for VetFlipMap [0].
3. “Activate flipmaps” – for VetFlipMap [1].
4. “Activate flipmaps” – for VetFlipMap [2].
5. Extracting pistols.
6. There is already nothing to activate.

“Break” is not enough in the actual setup. I mean, turning FlipIndex into 0 is necessary this time, or else “break” means only a “pause”, the flipmaps will be activated if Lara holsters the pistols.
Why? Don’t forget: this is a continuous (cbCycleBegin) code, so if it is not forbidden any more with the pistol condition, then it will be checked again and again, and, if it is true, it will be done. FlipIndex=0 will prevent it, abort it, because the main condition of the code (=3) is not true any more.
By the way, as you can see, FlipIndex=0 means one more thing: you can ask three indices again, in the trigger. If you don’t want to use that, forbidding the feature “forever” if it is failed, then turn FlipIndex into an index out of the useable 0-3 range, eg. 4, instead of that 0.

Last edited by AkyV; 05-11-17 at 22:57.
AkyV is offline  
Old 03-11-17, 19:34   #24
AkyV
Moderator
 
Joined: Dec 2011
Posts: 4,881
Default

23. Data conversions

You can do data conversion in the code in these situations:

a, Index conversion:
There are three sequences of NGLE/tomb indices:
- Moveable object (plus camera/sink/sound),
- Static objects,
- rooms

Previously I have already talked about Moveable object (plus camera/sink/sound) NGLE/tomb indices. Supposedly this list will refresh your reminiscences:

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

The sequence of room indices is absolutely similar – see eg. this room list:

Room0 (0:0) – room name (NGLE index:tomb index)
Room1 (1:1)
Empty
Empty
Room4 (4:2)
Empty
Room6 (6:3)

The indices of Static objects are a bit different. An NGLE Static index is naturally the object ID you can read from the map in NGLE. But its tomb index has two parts: the room tomb index, where the Static is, plus the Static index only for that room.

b, Absolute/relative value conversion:
“Absolute” indices are used for animations and animation frames in the code, but only their “relative” versions are easily understandable for the level builders.

Let’s see for example a basic WAD, where LARA object has the usual 444 animations. LARA is the first object in the WAD, PISTOLS_ANIM is the second one. The first animation of PISTOLS_ANIM is naturally Animation0. But it is the relative index – the index which can be identified only with the slot together, because naturally LARA object Animation0 and PISTOLS_ANIM Animation0 are not the same.
An absolute index is always the “previous absolute animation index of the WAD, plus 1”. As the animation indices of the first object, the relative and absolute indices of LARA object are the same. The animation just before the second object PISTOLS_ANIM Animation0 is the last (444) animation of the first object LARA. So the absolute index of PISTOLS_ANIM Animation0 is Animation445. There is no other absolute Animation445 in the WAD, naturally. (But other WADs can also have their own absolute Animation445.)

As for frame indices, the theory is similar. I mean, you have different Frame0 frames eg. for the different animations of LARA object. Which means those are relative indices, you can identify an index only with its animation (and slot) together, i.e. Frame0 of LARA object Animation0 and Frame0 of LARA object Animation1 are not the same.
An absolute index is always the “previous absolute frame index of the WAD object slot, plus 1”. As the frame indices of the first animation (0 - running), the relative and absolute indices of LARA object Animation0 are the same. The frame just before the second animation (1 - walking) is the last (21, by default) frame of the first animation (0). So the absolute index of LARA Animation1 Frame0 is Frame22. There is no other absolute Frame22 for LARA object, naturally. (But other object slots in the WAD can also have their own absolute Frame22.)

Note:
There are also some other (not so important) conversions, you can read about them in this tutorial. (You don’t need them now.)

The conversion is needed in the code:

1. From NGLE index to tomb index:
- If you type the room/Moveable object NGLE ID manually. (Mostly if you ask the ID in Parameters Script commands.) If you got tomb indices manually, out of the code, that would be wrong (see the explanation just below), so you will always type NGLE room/Moveable indices in the code, also asking the conversion: “convert NGLE X code into tomb Y code”, because the plugin always work with tomb indices.
- Always, if you work with Static object indices. The plugin always work with tomb indices, but you can know only NGLE Static indices from the Level Editor, so you will always type NGLE indices in the code, also asking the conversion: “convert NGLE X code into tomb Y code”.

2. From relative index to absolute index:
- Always, if you work with animation indices.
- Always, if you work with animation frame indices.

The cause in both cases is: if you got absolute indices manually, out of the code, that would be wrong (see the explanation just below), so you will always type relative animation/frame indices in the code, also asking the conversion: “convert relative X code into absolute Y code”, because the plugin always work with absolue indices.

3. From tomb index to NGLE index / from absolute index to relative index:
If you would like to print the index (coming from the code) with Statistic Script commands or in the log. Because the tomb/absolute index of the code doesn’t mean anything when level building, only NGLE/relative indices can be identified easily.

Notes:
- On the other hand, the NGLE=>tomb conversion is not needed for Moveable object (plus camera/sink/sound) indices:

> If you use preset arguments in your own Action or Condition triggers, and if the list of the argument has the actual Moveable object (camera/sink/sound) indices, like in the case of #MOVEABLES#.
The reason is this time you won’t type the index manually - the object list in the trigger “Object to trigger” box shows NGLE indices, though. But the conversion will be made automatically in the code: “Object to trigger” box values of Action or Condition triggers are always handled as “ItemIndex” variable values in the code. So if you type “ItemIndex” in the code, having those preset arguments, then it means “the tomb index of Moveable object (camera/sink/sound), that you chose in »Object to trigger« box”.
> If the index is defined by the code, because that index is naturally a tomb index. See eg. *Trng.pGlobTomb4->pVetMemorySavegame[0].pShort to get the last found item index.

- The code uses tomb indices, and the code uses absolute indices. That is why an animation/frame index defined by a code is always absolute. For example, GET.pItem->AnimationNow is the absolute index of the animation that the object is just performing, you don’t need relative=>absolute conversions.
- You should avoid conversions out of the code, if the conversion is needed. There are more possibilities for that, in many cases of the NGLE/tomb, absolute/relative indices. For example, you can find Animation Watcher in NG Center. Load the WAD, choose “Show Present Animations” in the “Show” box, and choose “PISTOLS_ANIM” in the “Slot” box. Now you can identify easily in the bigger window below that PISTOLS_ANIM Animation0 has 445 as the absolute ID – as I said above.
But it is bad idea to use that 445 in the code. – This example will explain why:

The only enemies in your WAD are CROCODILE (slot ID 51) and DEMIGOD1 (slot ID 77). Then you add a new enemy to your WAD. Eg. this is the WILD_BOAR (slot ID 73). Up to now, the absolute index of DEMIGOD1 first animation was the absolute index of CROCODILE last animation, plus one. But that index goes to the WILD_BOAR first animation now.
So Absolute Index X (meaning DEMIGOD1 first animation before) means WILD_BOAR first animation, after adding WILD_BOAR, you should change the absolute animation ID, typed manually, which is wrong now (not referring any more to DEMIGOD1), in the code.

So what I say is (when you need a conversion): even if you can get a tomb/absolute index in other ways, always type an NGLE/relative index (or a variable having that index) into the code, and the proper conversion (in the code) will be made by the conversion methods that are listed below.

- You need to be careful, if this animation/frame is for Lara, but not for Lara object – for example, if it is VEHICLE_EXTRA or PISTOLS_ANIM etc.:
a, In the case of vehicle animation slots, unfortunately, the only useful way to identify an animation in the code is with a conversion out of the code – eg. getting the proper absolute animation code of VEHICLE_EXTRA from Animation Watcher. - So you should check this absolute animation value (if it is still the proper one) when you add/remove an animation or an object slot to/from your WAD later!
However, these animations and frames are still handled as the animations/frames of Lara object, in other aspects.
b, In the case of flare, torch or weapon animations, you need to identify the absolute animation/frame ID’s NOT as Lara object’s animations/frames, but the values of these Savegame Memory Zone fields:

WeaponAnim. Current animation of torch-flare-weapon animation (Short)
WeaponAnim. Current frame of torch-flare-weapon animation (Short)

Conversion methods:

1. If the Moveable index comes from another trigger (i.e. if you export an Action/Condition trigger as a function into your code), then you need the NGLE=>tomb conversion. As I said above, it is very easy, just hit the Export Function button to get the proper values:

; 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);

A special example – activate the object which is the last found item:

Code:
  PerformActionTrigger(NULL, 43, ReadMemVariable (0 | enumMEMT.SAVEGAME), 0);
2. As I said above, items for “Get (GET_ITEM...” structure needs an NGLE=>tomb conversion, if it is an NGLE index:

Code:
  Get (GET_ITEM, 97+NGLE_INDEX, 0);
But if it is a tomb index (eg. if this item is coming from the “Object to trigger” box of the Condition trigger, as a preset argumnt), then you don’t need the conversion:

Code:
  Get (GET_ITEM, ItemIndex, 0);
3. If the room/Moveable index is in Current Value TRNG variable, then there are some “Variables. Convert” Flipeffect triggers, to do NGLE=>tomb or tomb=>NGLE conversion. You can export those Flipeffects as functions, into your code.

4. The basic NGLE=>tomb or tomb=>NGLE conversion for Moveables (NGLE index = X variable, tomb index= Y variable this time), with these functions:

Y = FromNgleIndexToTomb4Index (X);
X = FromTomb4IndexToNgleIndex (Y);

It looks this way eg. in the case of GET_ITEM:

Code:
  Y = FromNgleIndexToTomb4Index (X);
  Get (GET_ITEM, Y, 0);
or, if you don’t want an Y variable:

Code:
  Get (GET_ITEM, FromNgleIndexToTomb4Index (X), 0);
5. The basic NGLE=>tomb or tomb=>NGLE conversion for Statics (eg. if the NGLE index is in Z variable this time), with FromNgleStaticIndexToTomb4Indices function – an example when you check if the OCB of the required Static has OCB flag 1:

Code:
  {
  int Z;
  int RoomIndex;
  int StaticIndexForRoom;
  
  FromNgleStaticIndexToTomb4Indices (Z, & RoomIndex, & StaticIndexForRoom);
  
  Get (enumGET.STATIC, RoomIndex, StaticIndexForRoom);
  if (GET.pStatic ->OCB & 1) {
  
  }
  }
As I said above, a Static tomb index is double: you get a room index and the Static index only for that room, with a NGLE=>tomb conversion.
As I also said, a single & means “part of something”, so “& RoomIndex, & StaticIndexForRoom” means now “they are both parts of the tomb index”.

The back conversion is similar - but this time without a &:

Code:
Z = FromStaticIndicesToNgleIndex (RoomIndex, StaticIndexForRoom);
6. The Convert function is the “ultimate tool” for the converisons.
The general syntax of the function is:

Convert (CONVERT constant for the conversion type, First Input Value, Second Input Value, Pointer)

Let’s see one example for each case:

- Conversion from Moveable 107 NGLE index into tomb index (no second input value and pointer), storing the value in TombItemIndex variable:

Code:
TombItemIndex = Convert (enumCONV.ItemIndexFromNgleToTomb, 107, 0,NULL);
- Conversion from the Moveable tomb index (having the tomb index in TombItemIndex variable) into NGLE index (no second input value and pointer), storing the value in NgleItemIndex variable:

Code:
NgleItemIndex = Convert (enumCONV.ItemIndexFromTombToNgle, TombItemIndex,0,NULL);
- Conversion from Static 258 NGLE index into room/static double tomb index (no second input value), storing the value in RoomIndex and StaticIndex variable:

Code:
RoomIndex = Convert (enumCONV.StaticIndexFromNgleToTomb, 258, 0, &StaticIndex);
Yes, a &, without p, but it is a pointer.

- Conversion from the Static tomb indices (having tomb indices in RoomIndex and StaticIndex variables) into NGLE index (no pointer), storing the value in NgleStaticIndex variable:

Code:
  NgleStaticIndex = Convert (enumCONV.StaticIndexFromTombToNgle, RoomIndex, StaticIndex,NULL);
- Conversion from Room 75 NGLE index into tomb index (no second input value and pointer), storing the value in TombRoomIndex variable:

Code:
  TombRoomIndex = Convert (enumCONV.RoomIndexFromNgleToTomb, 75, 0, NULL);
- Conversion from the room tomb index (having the tomb index in TombRoomIndex variable) into NGLE index (no second input value and pointer), storing the value in NgleRoomIndex variable:

Code:
  NgleRoomIndex = Convert (enumCONV.RoomIndexFromTombToNgle, TombRoomIndex, 0, NULL);
- Conversion from Animation 5 relative index of BADDY_1 enemy of GET.pItem into the absolute WAD animation index (no second input value), storing the value in AbsAnimationIndex variable:

Code:
  Get (enumGET.ITEM, ItemIndex, 0);
  AbsAnimationIndex = Convert(enumCONV.AnimIndexFromRelativeToAbs, 5, 0, GET.pItem);
- Conversion from the WAD absolute animation index (having the absolute index in AbsAnimationIndex variable) into the relative index of BADDY_1 enemy of GET.pItem (no second input value and pointer), storing the value in RelAnimIndex variable:

Code:
  Get (enumGET.ITEM, ItemIndex, 0);
  RelAnimIndex = Convert (enumCONV.AnimIndexFromAbsToRelative, AbsAnimationIndex, 0, GET.pItem);
- Conversion from Frame2 relative index of the absolute animation (being in AbsAnimationIndex variable), into the absolute frame index of the object slot (no pointer), storing the value in FrameAbs variable:

Code:
  FrameAbs = Convert (enumCONV.FrameIndexFromRelativeToAbs, 2, AbsAnimationIndex, NULL);
- Conversion from the absolute frame index of the object slot (having the absolute index in FrameAbs variable) into the relative frame index of AbsAnimationIndex animation (no pointer), storing the value in FrameRel variable:

Code:
  FrameRel = Convert (enumCONV.FrameIndexFromAbsToRelative, FrameAbs, AbsAnimationIndex, NULL);
7. Animation and frame indices without any conversion – with some easy tricks:
For example, to identify and examine Frame2 of Animation5 of a BADDY_1 object in a Condition trigger, as his actual animation and frame, without knowing and searching for its absolute animation/frame ID:

Code:
  Get (enumGET.ITEM, ItemIndex, 0);
  if (GET.pItem->SlotID == SLOT_BADDY_1) {
        Get (enumGET.SLOT, GET.pItem->SlotID, 0);
        if (GET.pItem->AnimationNow == 5 + GET.pSlot->IndexFirstAnim) {
              Get (enumGET.ANIMATION, 5 + GET.pSlot->IndexFirstAnim, 0);
              if (GET.pItem->FrameNow == 2 + GET.pAnimation->FrameStart) {
  
              }
        }
  }
I mean, the absolute ID in “IndexFirstAnim” could be any number, it doesn’t matter now, but we now surely it has Animation 0 relative ID for its objects slot (hence the name “first”). So “5 + GET.pSlot->IndexFirstAnim” is surely refers to Animation5 (0+5=5) relative animation index of the slot.
So if this animation is just being performed by that enemy (“AnimationNow”, defined by the code, absolute value), then we examine the animation with the GET_ANIMATION constant.
The absolute ID in “FrameStart” could be any number, it doesn’t matter now, but we now surely it has Frame 0 relative ID for its animation (hence the name “start”). So “2 + GET.pAnimation->FrameStart” is surely refers to Frame2 (0+2=2) relative frame index of the animation.
So if this frame is just being performed by that enemy (“FrameNow”, defined by the code, absolute value), then we know the whole condition is true.

This is a very useful method, if you can’t do a conversion, because that doesn’t work in these cases:

WeaponAnim. Current animation of torch-flare-weapon animation (Short)
WeaponAnim. Current frame of torch-flare-weapon animation (Short)

For example:

Code:
  Get (enumGET.SLOT, SLOT_PISTOLS_ANIM, 0);
  if (*Trng.pGlobTomb4->pVetMemorySavegame[91].pShort == 3 + GET.pSlot->IndexFirstAnim) {
        Get (enumGET.ANIMATION, 3 + GET.pSlot->IndexFirstAnim, 0);
        if (*Trng.pGlobTomb4->pVetMemorySavegame[90].pShort == 2 + GET.pAnimation->FrameStart) {
  
        }
  }
means „if it is Frame2 of Animation 3 of PISTOLS_ANIM”, because 91 is the memory zone field ID for the animations and 90 is for frames.
AkyV is offline  
Old 03-11-17, 19:36   #25
AkyV
Moderator
 
Joined: Dec 2011
Posts: 4,881
Default

24. Testing

You can still use Diagnostic Script command to test your codes, but I say Diagnostic is for the level builders.
The best way for a single testing of the plugin maker is the well-known log, i.e. Tomb4_log.exe in the Tools folder of the Level Editor.

All you need is to type a “SendToLog” function temporarily in the proper place:
- in the trigger code if you want to see the value in the log when this trigger has been activated,
- in cbCycleBegin if you want to see that value refreshed again and again,
- etc.

The function needs a random log entry name with “%d” sign, plus the route of what you want to test.

What will I see for example in this case in the log window, if this block is typed in cbCycleBegin:

Code:
  {
  int *pLoadCamera_SrcX = (int*) 0x533978;
  Get (GET_ITEM, 97+NGLE_INDEX, 0);
  
  SendToLog ("Channel2 Active %d", Trng.pGlobTomb4->BaseBassHandles.Proc.BASS_ChannelIsActive (Trng.pGlobTomb4->BaseBassHandles.VetCanali[1].Canale));
  SendToLog ("Item97 Animation %d", Convert (enumCONV.AnimIndexFromAbsToRelative, GET.pItem->AnimationNow, 0, GET.pItem));
  SendToLog ("Last Found Index %d", FromTomb4IndexToNgleIndex (ReadMemVariable (0 | enumMEMT.SAVEGAME)));
  SendToLog ("LoadCamera Source X %d", *pLoadCamera_SrcX);
  SendToLog ("Small Medi Used %d", MyData.Save.Local.SmallMediStat);
  
  }
This:

Channel2 Active ...
Item97 Animation ...
Last Found Index ...
LoadCamera Source X ...
Small Medi Used ...

I mean, you can see:
- If an audio track is just playing on Channel2, then “Channel2 Active” shows 1 (playing), or else 0 (stopped).
- The actual relative Animation ID of Item with NGLE ID 97, in “Item97 Animation”.
- The actual NGLE index of the last item found, in “Last Found Index”.
- The actual coordinate of Source X of the level LoadCamera, in “LoadCamera Source X”.
- The actual amount of small medipacks used in this level, in “Small Medi Used”.

“Debugging” is an advanced mode of the tests, more than just seeing a few chosen properties in the log window. If you have mood and time enough, then read the tutorial about that here.
AkyV is offline  
Old 03-11-17, 19:39   #26
AkyV
Moderator
 
Joined: Dec 2011
Posts: 4,881
Default

25. Timed events

An easy way for a timed event in the code is a simple “++” computation. For example, this is a part of a trigger code:

Code:
  case 603:
        MyData.Save.Local.Flag = 1;
        MyData.Save.Local.Counter = 0;
  break;
And this code is from cbCycleBegin:

Code:
  if (MyData.Save.Local.Flag == 1) {
        MyData.Save.Local.Counter++;
        if (MyData.Save.Local.Counter == 10) {
              some code;
        }
        if (MyData.Save.Local.Counter == 25) {
              some code;
              MyData.Save.Local.Flag = 0;
        }
  }
So, you made Value 1 for your custom Flag in the trigger code, to start a continuous event in cbCycleBegin (as “MyData.Save.Global.MediStatFlag” also did it above). This continuous event is a counter. While Flag is 1, 1 will be added again and again at each tick frame, to Counter variable, which always starts from 0. If the counter reaches the required Tick Frame 10, then the required code will be executed.
The counter goes on, executing the next code, reaching Tick Frame 25.
There are no further code, the organizers stops now (because Flag = 0, so Counter won’t be increased any more.) As you can see, the restart of the trigger will restart this “Organizer” from the start, i.e. from Tick Frame 0.

This is in cbCycleBegin, so you may think the “some codes” will be continuously executed. But it is not true. Eg. “Counter=10” happens only at one tick frame. In the next tick frame Counter is 11, the condition is not true any more, so the event won’t be re-executed in the 11th second or later.
If you want a contiuous event now, then you should change the code, for example this works for 3 seconds continuously:

if (Counter >= 10 && Counter <= 100) {

Or, this works till you break it with setting 0 for Flag:

if (Counter >= 10) {
AkyV is offline  
Old 03-11-17, 19:48   #27
AkyV
Moderator
 
Joined: Dec 2011
Posts: 4,881
Default

26. Progressive actions

This tool is made by Paolone, to control timed/non-timed continuous events (instead of the workaround methods I described earlier).
First of all, use your imagination for a name of your progressive action. For example, what you want is a countdown screen timer, using a trigger (instead of F86 for the increasing screen timer), that is why the name will be AXN_COUNTDOWN_TIMER. (AXN signals it is a name of a progressive action.) The name should be typed in Constants_mine.h source file, between these entries:

Code:
  #define AXN_FREE  0  // this record is to free an action record. You type this value in ActionType to free a action progress record
  // --------- END PRESET CONSTANTS ---------------------------------
Hit ENTER for an empty row, type “#define” tag, the name, and the next free AXN ID number (1) there:

Code:
  #define AXN_FREE  0  // this record is to free an action record. You type this value in ActionType to free a action progress record
  #define AXN_COUNTDOWN_TIMER 1
  // --------- END PRESET CONSTANTS ---------------------------------
(Note: I always follow the way Paolone did in Constants_mine.h to type constants. However, sometimes he typed constants not in the zone where the remarks says the constants should be. Oh, well, it doesn’t seem important.)

The Flipeffect trigger code (asking the time, when the timer starts from, from PARAM_BIG_NUMBERS of “Timer” box of the trigger) prints the timer on the screen (naturally only if there is Timer=ENABLED in the Script):

Code:
  case 504:
        Get(enumGET.BIG_NUMBER,Timer,0);
        *Trng.pGlobTomb4->pVetMemorySavegame[79].pLong = GET.BigNumber * 30;
  
        Get(enumGET.PROGRESSIVE_ACTION,0,0);
        GET.pAction->ActionType = AXN_COUNTDOWN_TIMER;
        GET.pAction->Arg1 = *Trng.pGlobTomb4->pVetMemorySavegame[79].pLong;
        GET.pAction->Arg2 = 2;
  break;
Naturally SaveGame Memory Zone ID 79 is the place where screen timer actual time is stored. It is multiplied by 30, because we ask it in seconds, but the time is stored in tick frames.

Code:
  Get (enumGET.PROGRESSIVE_ACTION,0,0);
  GET.pAction->ActionType = AXN_COUNTDOWN_TIMER;
This says we use a Get function to get data for a progressive action, which is called AXN_COUNTDOWN_TIMER now.

Code:
  GET.pAction->Arg1 = *Trng.pGlobTomb4->pVetMemorySavegame[79].pLong;
Arg1 is a prefixed plugin variable, always “int”. It always contains the actual time of this progressive action. In the first moment AXN_COUNTDOWN_TIMER should be the tick frame number, wherefrom the timer starts (which is the asked big number value, forced in ID79 field).

Code:
  GET.pAction->Arg2 = 2;
Arg2 is another prefixed plugin variable, always “int”. It is always the customized parameter of the progressive action. - Let’s suppose the timer starts from 2 seconds (60 frames) this time:

Frame 60 =>60
Frame 61 => 61-2 = 59
Frame 60 => 60-2 = 58
Frame 59 => 59-2 = 57
Frame 58 => 58-2 = 56
Etc.

So, as it is said here (GET.pAction->Arg1 = *Trng.pGlobTomb4->pVetMemorySavegame[79].pLong), the initial value of the screen timer is 60.
In the next frame the timer is increasing by 1, as usual, automatically, the value will be 60+1=61. But the progressive action says it should be decreased by 2, so it will be 61-2=59.
In the next frame the timer is increasing by 1, as usual, automatically, the value will be 59+1=60. But the progressive action says it should be decreased by 2, so it will be 60-2=58.
In the next frame the timer is increasing by 1, as usual, automatically, the value will be 58+1=59. But the progressive action says it should be decreased by 2, so it will be 59-2=57.
Etc.

So the timer is increasing, as usual, automatically, but we turn it into decreasing, with a little mathematical trick.

The trigger “only” starts the progressive action, but the code of progressive action itself must be typed in the PerforMyProgressiveAction function.

Code:
  void PerformMyProgrAction(StrProgressiveAction *pAction)
  {
  
  
        switch (pAction->ActionType) {
  // replace the "case -1:" with your first "case AXN_...:" progressive action to manage)       
        case -1:
        break;
  
        }
  
  }
Just like in the case of triggers, replace the case -1/break part in the switch sequence, for your code:

Code:
  void PerformMyProgrAction(StrProgressiveAction *pAction)
  {
  
  
        switch (pAction->ActionType) {
        case AXN_COUNTDOWN_TIMER:
  
              if (pAction->Arg1 == 0) {
                    pAction->ActionType = AXN_FREE;
                    break;
              }
              pAction->Arg1--;
              *Trng.pGlobTomb4->pVetMemorySavegame[79].pLong -= pAction->Arg2;
        break;
  
        }
  
  }
The “GET.pAction->Arg1” in the trigger and “pAction->Arg1” here naturally are the same.
The “GET.pAction->Arg2” in the trigger and “pAction->Arg2” here naturally are the same.

“pAction->Arg1--;” naturally means “pAction->Arg1= pAction->Arg1-1;”, so the value of pAction->Arg1 (which is 60 initially, defined by the trigger) will be decreased by 1 at each tick frame. (Because when a progressive action is running, that works with tick frames, after each other.)
“*Trng.pGlobTomb4->pVetMemorySavegame[79].pLong -= pAction->Arg2;” means “*Trng.pGlobTomb4->pVetMemorySavegame[79].pLong = *Trng.pGlobTomb4->pVetMemorySavegame[79].pLong -2;” (because Arg2 is 2), so the value of *Trng.pGlobTomb4->pVetMemorySavegame[79].pLong will be decreased by 2 at each tick frame:

Code:
  Arg1              Increasing screen timer (automatically)  Forced screen timer (by Arg2)
  60 (start)=ID79
  59=not ID79             ID79=60+1=61                             ID79=61-2=59
  58=not ID79             ID79=59+1=60                             ID79=60-2=58
  57=not ID79             ID79=58+1=59                             ID79=59-2=57
  56=not ID79             ID79=57+1=58                             ID79=58-2=56
  Etc.
When Arg1 reaches the lower limit (0), then AXN_FREE progressive action will be executed:

Code:
if (pAction->Arg1 <= 0) {
	pAction->ActionType = AXN_FREE;
	break;
}
As you can see in Constants_mine.h, AXN_FREE means there is no further action any more. That additional “break” is also needed to abort the action.
So, AXN_FREE this time means, “further actions” (further = typed after this abortion), i.e. “Arg1 is decreased by 1” and “Arg2 is decreased by 2”, won’t be executed any more:

Code:
  Arg1        Increasing screen timer Forced screen timer (Arg2)
  3           4+1=5                   5-2=3
  2           3+1=4                   4-2=2
  1           2+1=3                   3-2=1
  0           1+1=2                   2-2=0
Or you can ignore the customized parameter (Arg2) this time - because the example was very easy now:

Code:
  case 504:
        Get(enumGET.BIG_NUMBER,Timer,0);
        *Trng.pGlobTomb4->pVetMemorySavegame[79].pLong = GET.BigNumber * 30;
  
        Get(enumGET.PROGRESSIVE_ACTION,0,0);
        GET.pAction->ActionType = AXN_COUNTDOWN_TIMER;
        GET.pAction->Arg1 = *Trng.pGlobTomb4->pVetMemorySavegame[79].pLong;
  break;
  
  case AXN_COUNTDOWN_TIMER:
  
        if (pAction->Arg1 == 0) {
              pAction->ActionType = AXN_FREE;
              break;
        }
        pAction->Arg1--;
        *Trng.pGlobTomb4->pVetMemorySavegame[79].pLong = pAction->Arg1;
  break;
Code:
  Start=60
  Arg1        Increasing screen timer Forced screen timer (=Arg 1)
  59          60+1=61                 59
  58          59+1=60                 58
  57          58+1=59                 57
  56          57+1=58                 56
  Etc.
Another example for a progressive action if the timer is “neverending”, you need to abort it manually:

Code:
  Get (enumGET.PROGRESSIVE_ACTION,0,0);
  GET.pAction->ActionType = AXN_...;
  GET.pAction->Arg1 = ENDLESS_DURATE;
  GET.pAction->Arg2 = ...;
Let’s suppose you can choose “abort” (Timer 1) amongst the trigger parameters in Set Trigger Type panel, which has this code in the trigger:

Code:
if (Timer == 1) MyData.Save.Local.Abort = 1;
This time you don’t need “pAction->Arg1--;”, because an endless timer cannot be decreased:

Code:
  case AXN_...:
  
        if (MyData.Save.Local.Abort == 1) {
              pAction->ActionType = AXN_FREE;
              break;
        }
        some code with Arg2;
        break;
As you can see, we abort the action this time with “MyData.Save.Local.Abort” custom flag.

Note:
Progressive actions can be even really complex. If you want to study this complicated example, you will understand why.

Last edited by AkyV; 05-11-17 at 23:28.
AkyV is offline  
Old 03-11-17, 19:51   #28
AkyV
Moderator
 
Joined: Dec 2011
Posts: 4,881
Default

27. Action triggers

Technically Action triggers are “special Flipeffect triggers”. After all, these are the only differences:

- Type Action trigger codes in cbActionMine instead of cbFlipEffectMine. (But also in a case/break part of the switch/break sequence, as I told at Flipeffects.)
- The Action triggers have similar input values as the Flipeffects have, but in their case we say ActionIndex instead of FlipIndex, plus, the trigger parameter is the “Object to trigger” box instead of the “Timer” box, called “ItemIndex” variable in the code.
- You need Action instead of Flipeffect if the trigger is about the actual Moveable objects, static/flyby cameras, sinks, using one preset argument lists, like #CAMERA_EFFECTS#, #MOVEABLES#, #SINK_LIST# etc. (Only in “Object to trigger” box, never in “Extra” box – even in the case of CONDITION triggers!)

The reason is that those preset argument lists cannot be detected in Flipeffects.

Which doesn’t mean Moveable objects, static/flyby cameras, sinks are useless if you used them in Flipeffects.
The difference is you can’t use any list of the actual objects in Flipeffects. If you want the objects in a Flipeffect, then you will need your own Parameters Script command (see about it later), where in one of the command fields the required object (camera, sink) ID is needed to be typed. (If you want to be user-friendly, then you always ask the player for the NGLE object index in that command, what he can read easily from the map. You will do the conversion into tomb index manually, in the code.)

Here is a theoretical Action trigger:

;------------------- Section for Action triggers: description of the trigger -------------------------------------
<START_TRIGGERWHAT_11_T_H>
150: Start forcing North/South (E) speed (NGLE facing) for <#> Rollingball
<END>

;type here the sections for argument of above action triggers

<START_ACTION_150_O_H>
#MOVEABLES#
<END>

<START_ACTION_150_E_H>
#REPEAT#Value of Parameters=PARAM_BIG_NUMBERS at index=#0#127
<END>

Code:
  case 150:
  Get (enumGET.ITEM, ItemIndex, 0);
  
  if (GET.pItem->SlotID == SLOT_ROLLINGBALL) {
        Get (enumGET.BIG_NUMBER,Extra,0);
  
        MyData.Save.Local.RollBallNSSpeed = GET.BigNumber;
        MyData.Save.Local.RollBallNSIndex = ItemIndex;
        MyData.Save.Local.RollBallNSFlag = 1;
  }
  break;
In cbCycleBegin:

Code:
  //rollingball NorthSouth speed
  if (MyData.Save.Local.RollBallNSFlag == 1) {
        Get (enumGET.ITEM, MyData.Save.Local.RollBallNSIndex, 0);
        GET.pItem->Reserved_34 = MyData.Save.Local.RollBallNSSpeed;
  }
Reserved_34 field stores that speed in the case of rollingball objects. (“NGLE facing” means it is not based on the game compass.) You need a minus sign in “big numbers” to distinguish if it is north or south.
We need that continuous event, because forced rollingball speed will be kept only while you continuously force them.
And we also need to put ItemIndex into a MyData variable so we can use it in another (cbCycleBegin) function.

Naturally we also need another trigger, to stop forcing, putting 0 in “MyData.Save.Local.RollBallNSFlag” variable.

Attention!
Probably the possibility that Moveable objects, static/flyby cameras, sinks are useable even in Flipeffects, is very beneficial.
I mean, according to my experiences, Action triggers made in the plugin are buggy, if you need Extra parameters in them!
I hope Paolone has already fixed this bug when you read this. But if not, if you experience something is wrong when you use Extra for your Action, then use a Flipeffect instead, with a Parameters command. – So use Action only if you don’t need the Extra parameter!

Last edited by AkyV; 05-11-17 at 23:34.
AkyV is offline  
Old 03-11-17, 20:20   #29
AkyV
Moderator
 
Joined: Dec 2011
Posts: 4,881
Default

28. Parameters Script commands

You need Parameters Script commands for the trigger if the two parameters (Flipeffect: Timer+Extra, Action/Condition: Object to trigger+Extra) of the trigger is not enough for the number of parameters. If you need three, four, five or more parameters.

For example, we need this condition trigger:

The speed of X Rollingball in W direction is Y more/less/equal than Z.

We need four parameters this time:

W - direction
X – rollingball object ID
Y – more or less or equal
Z – the required speed

So the trigger will look like this now, for example:

;------------------- Section for Condition triggers: descrption of the trigger -------------------------------------
<START_TRIGGERTYPE_12_T_H>
152: Check rollingball speed with <#> parameters
<END>

;type here the sections for arguments of above conditional triggers
<START_CONDITION_152_O_H>
#REPEAT#Parameters=PARAM_SPEED_ROLLINGBALL,#1#1023
<END>

(The “SPEED_ROLLINGBALL” name after “PARAM” naturally could be anything you want.)

The description:

First of all, just like in the case of the triggers, we need a TXT file. – Must be saved with SCRIPT extension!
The file name is just like other file names of the plugin: if they are Plugin_trng.sln, Plugin_trng.cpp etc., then this is Plugin_trng.script.
So create this empty file in the main folder of NG Center.

The contents of this SCRIPT file will be added to the descriptions of NG Center/Reference/Mnemonic constants.
So what a SCRIPT file has is:
- The descriptions of “PARAM_...” constants of your plugin, for Parameters commands.
- The descriptions of constants that are used in a “PARAM_...” constant of your plugin. (Eg. DIR constants are used in PARAM_MOVE_ITEM, in the case of trng.dll.)
- The descriptions of “CUST_...” constants of your plugin, for Customize commands.
- The descriptions of constants that are used in a “CUST_...” constant of your plugin.
- The descriptions of GlobalTrigger GT constants of your plugin.

If you’d like to write this file totally manually, that is pretty hard. That is why I highly recommend opening and writing it always in Trng Patcher.
Why? Because “under normal circumstances” the SCRIPT file contents will look eg. this way:

PARAM_EARTHQUAKE: 14 ;Used with Parameters command.>This will customize the earthquakes, in F538 triggers.>>Syntax: Parameters=PARAM_EARTHQUAKE, IdParamList, QuakeType, Intensity, Timer, Sound>>Note:>These quakes don't require EARTHQUAKE objects. However, NEVER use at the same time more than feature that performs a quake like this (PARAM_EARTHQUAKE, F1, F7, SQUISHY_BLOCK2, rumbling RAISING_BLOCK etc.).>But you can restart the trigger of the actual PARAM_EARTHQUAKE, even if its actual quake is still being performed.>>IdParamList field>----------------->This is a progressive number to identify this "Parameters=PARAM_EARTHQUAKE" command script in trigger window of ngle.>You'll type 1, for your first PARAM_EARTHQUAKE command, 2 for second etc.>>QuakeType field>--------------->Choose a QUAKE constant to choose the quake type.>>Intensity field>--------------->If you chose QUAKE_DECREASING:>Type here the starting intensity. The lowest sensible value seems 15. Theoretically the biggest starting intensity is somewhere about 32000, but that is very brutal, and looks very ugly.>The bigger the starting intensity is the longer the decreasing phase is.>>If you chose QUAKE_STANDARD:>Type here the standard intensity. To keep the game enjoyable, I don't really recommend choosing a bigger intensity than about 1000. The lowest one should be 5.>The bigger the standard intensity is the longer the decreasing phase is.>>If you chose QUAKE_SINGLE:>Type here the intensity of the jerk. The lowest sensible value seems 20. Jerks above about 1000 seem ugly.>>Note:>I recommend choosing an intensity in all the three cases that could be divided by 5 (200, 205, 210 etc.).>>Timer field>----------->If you chose QUAKE_STANDARD then type here a value, in seconds. (The maximum is probably almost 1 day!)>If this value expires then the quake starts decreasing by degrees, then stops.>>The other way to make QUAKE_STANDARD start decreasing is another F538.>>If you chose QUAKE_DECREASING or QUAKE_SINGLE, then type IGNORE. (Timer or F538 is useless for them to interrupt them.)>Also type IGNORE if you don't want to time QUAKE_STANDARD.>>Sound field>----------->Type here an ID of a single sound (for QUAKE_SINGLE) or a loop sound (for QUAKE_DECREASING or QUAKE_STANDARD).>It starts when the quake starts.>If it is looped then it is interrupted when the quake stops.>>Type IGNORE if you want a dumb quake.>>Note:>The recommended loop sound for earthquakes is ID 107 (EARTHQUAKE_LOOP).

QUAKE_DECREASING:1; Used with Parameters=PARAM_EARTHQUAKE command.>A quake that starts with huge or extra huge quakes, then it starts decreasing by degrees, till it stops by itself soon.

QUAKE_STANDARD:2; Used with Parameters=PARAM_EARTHQUAKE command.>A quake that performs a standard intensity. If you abort it with the Timer in the command, or another F538, then it starts decreasing by degrees, till it stops by itself soon.

QUAKE_SINGLE:3; Used with Parameters=PARAM_EARTHQUAKE command.>It just performs a single jerk.

As you can see, each help file of NG Center is only in one paragraph. It is very ugly, mostly in the case of the long help files like PARAM_EARTHQUAKE. Only “>” signs help you: each “>” sign means an empty row.
See “Reformat whole .script file” command in DropDown/Tools menu of Trng Patcher. You can choose “Expand” and “Compact” options here. If you open the SCRIPT file here, then choose “Expand” now, so the help file will look “normally” at last:

Sound field
-----------
Type here an ID of a single sound (for QUAKE_SINGLE) or a loop sound (for QUAKE_DECREASING or QUAKE_STANDARD).
It starts when the quake starts.
If it is looped then it is interrupted when the quake stops.

Type IGNORE if you want a dumb quake.

Note:
The recommended loop sound for earthquakes is ID 107 (EARTHQUAKE_LOOP).
||
QUAKE_DECREASING:1; Used with Parameters=PARAM_EARTHQUAKE command.
A quake that starts with huge or extra huge quakes, then it starts decreasing by degrees, till it stops by itself soon.
||
QUAKE_STANDARD:2; Used with Parameters=PARAM_EARTHQUAKE command.
A quake that performs a standard intensity. If you abort it with the Timer in the command, or another F538, then it starts decreasing by degrees, till it stops by itself soon.

This time you can find “||” sign to separate two help files from each other.
So type/edit only in “Expand” mode here, and then use “Compact” to convert the text back into the “ugly look”. Save it in the “ugly look”, and reopen NG Center to realize the changes. (You can also save it in the “normal look”, while you are working with it, but don’t forget the conversion and the final save, before re-opening NG Center.)

So we have an empty SCRIPT file. Type these commands, in “Expand” mode:

<START_CONSTANTS>
<END>

All the constants need to be typed between these commands. What we need now is a description for PARAM_SPEED_ROLLINGBALL, something like this:

<START_CONSTANTS>
PARAM_SPEED_ROLLINGBALL: 1 ;Used with Parameters command, to check the rollingball speed.

Syntax: Parameters=PARAM_SPEED_ROLLINGBALL, IdParamList, ObjectIndex, Relation, Direction, Speed

IdParamList field
-----------------
This is a progressive number to identify this "Parameters=PARAM_SPEED_ROLLINGBALL" command script in trigger window of ngle.
You'll type 1, for your first PARAM_SPEED_ROLLINGBALL command, 2 for second etc.

ObjectIndex
-----------
Type here the index of the rollingball you can read in the map, in that little yellow box when the object is selected.

Relation
--------
Type the proper ROLLREL_... constants to choose the required relation.

Direction
---------
Type the proper ROLLDIR_... constants to choose the required direction.

Speed field
-----------
Type here the speed you want to check.
(Approximately?) 512 means the speed of the ball is just enough for a one square long travel. It means if the ball is on a horizontal route, then the speed starts decreasing now, so the ball stops after travelling one square.
||
ROLLREL_MORE:1; Used with Parameters= PARAM_SPEED_ROLLINGBALL command.
Choose it if you want to check that the rollingball actual speed is more than the speed you check.
||
ROLLREL_LESS:2; Used with Parameters= PARAM_SPEED_ROLLINGBALL command.
Choose it if you want to check that the rollingball actual speed is less than the speed you check.
||
ROLLREL_EQUAL:3; Used with Parameters= PARAM_SPEED_ROLLINGBALL command.
Choose it if you want to check that the rollingball actual speed is equal with the speed you check.
||
ROLLDIR_NORTH:1; Used with Parameters= PARAM_SPEED_ROLLINGBALL command.
Choose it if you want to check the north speed of the rollingball (NGLE facing.)
||
ROLLDIR_SOUTH:2; Used with Parameters= PARAM_SPEED_ROLLINGBALL command.
Choose it if you want to check the south speed of the rollingball (NGLE facing.)
||
ROLLDIR_EAST:3; Used with Parameters= PARAM_SPEED_ROLLINGBALL command.
Choose it if you want to check the east speed of the rollingball (NGLE facing.)
||
ROLLDIR_WEST:4; Used with Parameters= PARAM_SPEED_ROLLINGBALL command.
Choose it if you want to check the west speed of the rollingball (NGLE facing.)
<END>

For example, this is the Script if I’d like to know that the speed of Rollingball 67 (NGLE index) in the western direction is more than 1024 (=the speed is enough for more than two sectors, in horizontal ground, if it won’t bump into an obstacle), using eg. ID5 for the constant:

Parameters= PARAM_ROLLINGBALL_SPEED, 5, 67, ROLLREL_MORE, ROLLDIR_WEST, 1024

Notes:
- As you can see, each group has its own ID number sequences: ROLLREL from 1 to 3, ROLLDIR from 1 to 4. PARAM group also got an ID, for 1 of your PARAM_SPEED_ROLLINGBALL. If you have another PARAM constant later, that should have ID 2. The description for that constant (or a CUST constant) should be typed naturally between the last ROLLDIR and <END>, separated with a || sign in Expand mode.
(I don’t recommend referring to PARAM/CUST constants in the Script with the ID number, because trng.dll and/or other plugin makers can also use that ID for their PARAM/CUST codes. If you use your own, fancy PARAM/CUST name, then there won’t be coincidence. The chance is low that another plugin maker uses the same name.)
- As you can see, I was user-friendly this time, naturally. I mean, as I said, north/south speed is in Reserved_34 field (north: negative, south: positive) and west/east speed is in Reserved_36 field (west: negative, east: positive). I mean, it would be uglier if I wrote this instead of ROLLDIR_NORTH, ROLLDIR_SOUTH, ROLLDIR_WEST, ROLLDIR_EAST:

||
ROLLDIR_NORTHSOUTH:1; Used with Parameters= PARAM_SPEED_ROLLINGBALL command.
Choose it if you want to check the north/south speed of the rollingball (NGLE facing.)
Type a positive number in Speed field if the speed is southern.
Type a negative number in Speed field if the speed is northern.
||
ROLLDIR_WESTEAST:2; Used with Parameters= PARAM_SPEED_ROLLINGBALL command.
Choose it if you want to check the west/east speed of the rollingball (NGLE facing.)
Type a positive number in Speed field if the speed is eastern.
Type a negative number in Speed field if the speed is western.
<END>

Which means in our non-ugly, user-friendly code we will use an additional operation, which is not necessary. (Which is: the level builder will type all the speeds as positive numbers, but northern/western speeds will be multiplied by -1.) The code will be longer this way, though. But, well, this “problem” is not so important than a nicer method for the level builder.

The code:

The code for a Parameters command needs to be typed where the command is used. For example:

- The command is used only for one trigger. Then you type the command code as the part of the trigger code.
- The command is used for more than one trigger, under different circumstances. Then you type the different command codes at all these triggers, as the part of the trigger code, because the codes for this command are different at the triggers.
- The command is used for more than one trigger, under the same circumstances. Then the nicest way to type the command code as a customized function code, and you will call this function in all the related triggers.

But, first of all, the constants you have just created, must be also defined in the plugin sources.
Go into Constants_mine.h again. You should type the constants at the end of the file, after this:

Code:
  // MPS_ flags
  // you should type here your MPS_ flags for plugin command.
  // then you'll test the presence of these flags checking the Trng.MainPluginFlags  value using "&" operator
  // please: do not use the value 0x40000000 (MPS_DISABLE) because it has been already set by trng engine
Type the constant names, ID’s, with the “#define” tags, and some remark:

Code:
  //constants for rollingball speed
  
  #define ROLLREL_MORE 1
  #define ROLLREL_LESS 2
  #define ROLLREL_EQUAL 3
  
  #define ROLLDIR_NORTH 1
  #define ROLLDIR_SOUTH 2
  #define ROLLDIR_EAST 3
  #define ROLLDIR_WEST 4
  
  #define PARAM_ROLLINBALL_SPEED 1
Now we create the the trigger code, including the code of PARAM_SPEED_ROLLINGBALL:

Code:
        case 152:
              {
              short Speed;
  
              Get (enumGET.MY_PARAMETER_COMMAND, PARAM_SPEED_ROLLINGBALL, ItemIndex);
  
              MyData.BallSpeedIndex = GET.pParam->pVetArg[1];
              MyData.BallSpeedRelation = GET.pParam->pVetArg[2];
              MyData.BallSpeedDirection = GET.pParam->pVetArg[3];
              MyData.BallSpeedSpeed = GET.pParam->pVetArg[4];
  
              Get (enumGET.ITEM, MyData.BallSpeedIndex + NGLE_INDEX, 0);
  
              if (MyData.BallSpeedDirection == ROLLDIR_NORTH) {
                    if (GET.pItem->Reserved_34 <= 0) {
                          Speed = -GET.pItem->Reserved_34;
                    } else {
                          Speed = 0;
                    }
              }
              if (MyData.BallSpeedDirection == ROLLDIR_SOUTH) {
                    if (GET.pItem->Reserved_34 >= 0) {
                          Speed = GET.pItem->Reserved_34;
                    } else {
                          Speed = 0;
                    }
              }
              if (MyData.BallSpeedDirection == ROLLDIR_WEST) {
                    if (GET.pItem->Reserved_36 <= 0) {
                          Speed = -GET.pItem->Reserved_36;
                    } else {
                          Speed = 0;
                    }
              }
              if (MyData.BallSpeedDirection == ROLLDIR_EAST) {
                    if (GET.pItem->Reserved_36 >= 0) {
                          Speed = GET.pItem->Reserved_36;
                    } else {
                          Speed = 0;
                    }
              }
  
              if (MyData.BallSpeedDirection == ROLLREL_MORE && Speed > MyData.BallSpeedSpeed) return RetValue | CTRET_IS_TRUE;
              if (MyData.BallSpeedDirection == ROLLREL_LESS && Speed < MyData.BallSpeedSpeed) return RetValue | CTRET_IS_TRUE;
              if (MyData.BallSpeedDirection == ROLLREL_EQUAL && Speed == MyData.BallSpeedSpeed) return RetValue | CTRET_IS_TRUE;
  
              return RetValue;
              }
        break;
Get (enumGET.MY_PARAMETER_COMMAND, PARAM_SPEED_ROLLINGBALL, ItemIndex);

The row above should be clear: “what we check here are the data of Parameters=PARAM_SPEED_ROLLINGBALL, which has the ID in the “Object to trigger” box of trigger. (The ID is 5 in the example just above.)

MyData.BallSpeedIndex = GET.pParam->pVetArg[1];
MyData.BallSpeedRelation = GET.pParam->pVetArg[2];
MyData.BallSpeedDirection = GET.pParam->pVetArg[3];
MyData.BallSpeedSpeed = GET.pParam->pVetArg[4];

This is a nice range: pVetArg[1], pVetArg[2] etc. Not accidentally:

pVetArg[0] = the first field after the PARAM constant name in the constant syntax (IdParamList),
pVetArg[1] = the second field after the PARAM constant name in the constant syntax (ObjectIndex),
pVetArg[2] = the third field after the PARAM constant name in the constant syntax (Relation),
pVetArg[3] = the fourth field after the PARAM constant name in the constant syntax (Direction),
pVetArg[4] = the fifth field after the PARAM constant name in the constant syntax (Speed).

We don’t care about pVetArg[0] this time, the ID has already been identified in the ItemIndex variable – but the other fields are used in the code.

The fields of plugin Script commands are always have a Short size!

MyData variables are created because those many pVetArg[...] are very disturbing in the code. They can be identified more easily if they have their own specified names. And we can have even other purposes with “MyData”.
I mean, why “MyData”? Because Parameters fields are referred many times in other codes, so it is better if these variables are not declared inside the code of this trigger. (Speed variable is something else, not a Parameters field, that is why it is not MyData.)
And why not “MyData.Save”? Because the variable stores a data (=checks the speed) only in the moment when the condition trigger has been triggered. Even if you save the game in the next moment, the trigger is already “dead”, the speed has already been checked, there is no data to save, a “MyData.Save” is unnecessary. (Speed variable and Speed field of Parameters command are not the same.)
(If any condition trigger is a GlobalTrigger condition, and you save the game while the GlobalTrigger checks it again and again, then it seems that condition data was saved – because the check continues after the load -, but it wasn’t. All that happens is the GlobalTrigger checks it even before and after the save, the trigger will be “alive” and “dead” again and again, even before and after the save.
If two or more conditions checks the same in the way like that, even in the same savegame, then they don’t disturb each other.)

Get (enumGET.ITEM, MyData.BallSpeedIndex + NGLE_INDEX, 0);

The row above says the object typed in ObjectIndex field (pVetArg[1]) of the command, will be converted into tomb index, and will be the subject of this Get function.

if (MyData.BallSpeedDirection == ROLLDIR_NORTH) {
if (GET.pItem->Reserved_34 <= 0) {
Speed = -GET.pItem->Reserved_34;
} else {
Speed = 0;
}
}
if (MyData.BallSpeedDirection == ROLLDIR_SOUTH) {
if (GET.pItem->Reserved_34 >= 0) {
Speed = GET.pItem->Reserved_34;
} else {
Speed = 0;
}
}
if (MyData.BallSpeedDirection == ROLLDIR_WEST) {
if (GET.pItem->Reserved_36 <= 0) {
Speed = -GET.pItem->Reserved_36;
} else {
Speed = 0;
}
}
if (MyData.BallSpeedDirection == ROLLDIR_EAST) {
if (GET.pItem->Reserved_36 >= 0) {
Speed = GET.pItem->Reserved_36;
} else {
Speed = 0;
}
}

The rows above say first of all:
- If you chose ROLLDIR_NORTH or _SOUTH in the Direction field (pVetArg[3]) then Speed value will get its value based on Reserved_34.
- If you chose ROLLDIR_WEST or _EAST in the Direction field (pVetArg[3]) then Speed value will get its value based on Reserved_36.

If it is based on Reserved_34, then
- if it is NORTH and Reserved_34 is not positive, then Speed gets that value (because northern values are negative), turned into positive (because, as I said above, we will always check positive values), or else Speed will be 0 (which means “no northern value”).
- if it is SOUTH and Reserved_34 is not negative, then Speed gets that value (because southern values are positive), or else Speed will be 0 (which means “no southern value”).

If it is based on Reserved_36, then
- if it is WEST and Reserved_36 is not positive, then Speed gets that value (because western values are negative), turned into positive (because, as I said above, we will always check positive values), or else Speed will be 0 (which means “no western value”).
- if it is EAST and Reserved_36 is not negative, then Speed gets that value (because eastern values are positive), or else Speed will be 0 (which means “no eastern value”).

if (MyData.BallSpeedDirection == ROLLREL_MORE && Speed > MyData.BallSpeedSpeed) return RetValue | CTRET_IS_TRUE;
if (MyData.BallSpeedDirection == ROLLREL_LESS && Speed < MyData.BallSpeedSpeed) return RetValue | CTRET_IS_TRUE;
if (MyData.BallSpeedDirection == ROLLREL_EQUAL && Speed == MyData.BallSpeedSpeed) return RetValue | CTRET_IS_TRUE;

The rows above will check the relations:
- If you chose MORE in the Relation field (pVetArg[2]), then Speed variable north/south/east/west positive value will be checked that if it is more than the value in Speed (pVetArg[4]) field.
- If you chose LESS in the Relation field (pVetArg[2]), then Speed variable north/south/east/west positive value will be checked that if it is less than the value in Speed (pVetArg[4]) field.
- If you chose EQUAL in the Relation field (pVetArg[2]), then Speed variable north/south/east/west positive value will be checked that if it is equal with the value in Speed (pVetArg[4]) field.

(This time we didn’t check that the slot is rollingball or not. It was intentional now, to teach you something.
I mean, that check is not necessary everyway. I mean, if you choose another slot for a rollingball trigger than a rollingball, then the trigger will do some stupid thing or do nothing. The level builder should expect that, it is evident. If we check the slot then all we do is rule out the “stupid thing part”, so in that case naturally you can know that nothing will happen if the wrong slot is chosen.
But, naturally, the code would be really exact with that check – but, it works even without it. Again, we don’t check it now, so the builder may experience something strange, if he chooses the wrong slot.)

Flags:

If you want even more than one constant at the same time, then you need to turn the absolute constants of a PARAM constant into relative constants (flags).
I mean, for example, in this example, what if you want to check not only MORE, LESS and EQUAL, but all the five possible cases:

MORE
LESS
EQUAL
MORE or EQUAL
LESS or EQUAL

In that situation some constants can be used even at the same time, you can add them to each other:

ROLLREL_MORE+ROLLREL_EQUAL
ROLLREL_LESS+ROLLREL_EQUAL

So you need to modify a few things we did previously:

1. Modify the description in the SCRIPT file:

Relation
--------
Type the proper ROLLREL_... constants to choose the required relation.
If you need a “mixed” relation (more or equal / less or equal), then you can choose more than one constant, adding them to each other.

2. Modify the definition in Constants_mine.h:

Code:
  //constants for rollingball speed
  
  #define ROLLREL_MORE 0x0001
  #define ROLLREL_LESS 0x0002
  #define ROLLREL_EQUAL 0x0004
  
  #define ROLLDIR_NORTH 1
  #define ROLLDIR_SOUTH 2
  #define ROLLDIR_EAST 3
  #define ROLLDIR_WEST 4
  
  #define PARAM_ROLLINBALL_SPEED 1
As you can see, the decimal 1, 2, 3 range of ROLLREL constants has been turned into binary (“bit”) 0, 1, 2 range, which is 1, 2, 4 in decimal (Bit0=1, Bit1=2, Bit2=4), i.e. the powers of 2: 2 to power of 0 is 1, 2 to the power of 1 is 2, 2 to the power of 2 is 4. (Important or not, Paolone types hexadecimal values – 0x0001, 0x0002, 0x0004 – when they are flag values, so I followed him.)

What we need to check now:
- if ROLLREL value has only MORE flag, without EQUAL flag.
- if ROLLREL value has only LESS flag, without EQUAL flag.
- if ROLLREL value has only EQUAL flag, without MORE or LESS flag.
- if ROLLREL value has MORE and EQUAL flags.
- if ROLLREL value has LESS and EQUAL flags.

Note:
This is not really a complicated situation, so we could have let the absolute 1, 2, 3 values (but we have not now), checking this:
- if ROLLREL value is 1 (MORE).
- if ROLLREL value is 2 (LESS).
- if ROLLREL value is 3 (EQUAL).
- if ROLLREL value is 4 (=1+3) (MORE and EQUAL).
- if ROLLREL value is 5 (=2+3) (LESS and EQUAL).

3. Modify C152 trigger code:

Old part:

Code:
              if (MyData.BallSpeedDirection == ROLLREL_MORE && Speed > MyData.BallSpeedSpeed) return RetValue | CTRET_IS_TRUE;
              if (MyData.BallSpeedDirection == ROLLREL_LESS && Speed < MyData.BallSpeedSpeed) return RetValue | CTRET_IS_TRUE;
              if (MyData.BallSpeedDirection == ROLLREL_EQUAL && Speed == MyData.BallSpeedSpeed) return RetValue | CTRET_IS_TRUE;
New part:

Code:
                if (MyData.BallSpeedDirection & ROLLREL_MORE && (MyData.BallSpeedDirection & ROLLREL_EQUAL) == false &&
                    Speed > MyData.BallSpeedSpeed) return RetValue | CTRET_IS_TRUE;
              if (MyData.BallSpeedDirection & ROLLREL_LESS && (MyData.BallSpeedDirection & ROLLREL_EQUAL) == false &&
                    Speed < MyData.BallSpeedSpeed) return RetValue | CTRET_IS_TRUE;
              if (MyData.BallSpeedDirection & ROLLREL_EQUAL &&
                    (MyData.BallSpeedDirection & ROLLREL_MORE) == false && (MyData.BallSpeedDirection & ROLLREL_LESS) == false &&
                    Speed == MyData.BallSpeedSpeed) return RetValue | CTRET_IS_TRUE;
              if (MyData.BallSpeedDirection & ROLLREL_MORE && MyData.BallSpeedDirection & ROLLREL_EQUAL &&
                    Speed >= MyData.BallSpeedSpeed) return RetValue | CTRET_IS_TRUE;
              if (MyData.BallSpeedDirection & ROLLREL_LESS && MyData.BallSpeedDirection & ROLLREL_EQUAL &&
                    Speed <= MyData.BallSpeedSpeed) return RetValue | CTRET_IS_TRUE;
IGNORE fields:

Many times you can read in Script command fields that “if you don’t need this feature, type IGNORE”.
As you definitely know, the value of IGNORE is always -1, you can type -1 instead of IGNORE, any time.

For example, we have a pVetArg[6] field. The description says “this will be Lara’s new health, except if you choose IGNORE”.
In this case the code should be something like this:

Code:
              if (GET.pParam->pVetArg[6] != -1) {
                    Get (enumGET.LARA, 0, 0);
                    GET.pLara->Health = GET.pParam->pVetArg[6];
              }
“Action triggers”:

As I said above, Action triggers with Extra parameters should be used as Parameters Script commands – even if there is no more than two parameters for the trigger.
This time we’ll use PARAM_SPEED_ROLLINGBALL for this “Action trigger” – which is good, because now we will force not only north/south but even west/east speed with the trigger, plus this trigger will not only start but even stop the force.

So this time you will skip this Action:

;------------------- Section for Action triggers: description of the trigger -------------------------------------
<START_TRIGGERWHAT_11_T_H>
150: Start forcing North/South (E) speed (NGLE facing) for <#> Rollingball
<END>

;type here the sections for argument of above action triggers

<START_ACTION_150_O_H>
#MOVEABLES#
<END>

<START_ACTION_150_E_H>
#REPEAT#Value of Parameters=PARAM_BIG_NUMBERS at index=#0#127
<END>

Using this Flipeffect instead:

;------------------ Section for flipeffect trigger: description of the trigger -------------------------------
<START_TRIGGERWHAT_9_O_H>
504: (E) Start/stop forcing customized rollingball speed with <&> parameters
<END>

;type here the sections for arguments used by above flipeffects
<START_EFFECT_504_T_H>
#REPEAT#Parameters=PARAM_SPEED_ROLLINGBALL,#1#9999
<END>

<START_EFFECT_504_E_H>
0:Start
1:Stop
<END>

(9999 should be enough. I don’t think we need the 32767 limit.)

PARAM_SPEED_ROLLINGBALL description should be changed with this – because that PARAM constant is useable now not only for that Condition trigger above, but even for this Flipeffect:

PARAM_SPEED_ROLLINGBALL: 1 ;Used with Parameters command, to check or force the rollingball speed.

Syntax: Parameters=PARAM_SPEED_ROLLINGBALL, IdParamList, ObjectIndex, Relation, Direction, Speed

A few examples to force some speed:

- The ball is rolling down slow on a sharp slope.
- The ball is shooting out fast on a totally horizontal floor like a cannonball.
- The ball is rolling along the horizontal corridors of a maze, turning left/right in the intersections.

Notes:
- The forced speed is always constant, unlikely the default one. You need to accept it.
- You cannot force any speed if the ball isn’t activated.
- HEAVY triggers are important in the forcing setup. These triggers are activated by the rollingball, and the trigger subject itself is also the rollingball. Two examples:

a, If an obstacle should stop the ball, then the ball will be stuck there, rolling with the forced speed. This is unreal, naturally, so you should place a HEAVY trigger on that square, that stops the effect.
b, When the ball should turn left/right in a maze, and the ball reaches the intersection, the HEAVY trigger there activates another F504, so the ball turns into another direction.

IdParamList field
-----------------
This is a progressive number to identify this "Parameters=PARAM_SPEED_ROLLINGBALL" command script in trigger window of ngle.
You'll type 1, for your first PARAM_SPEED_ROLLINGBALL command, 2 for second etc.

ObjectIndex
-----------
Type here the index of the rollingball you can read in the map, in that little yellow box when the object is selected.

Relation
--------
Type the proper ROLLREL_... constants to choose the required relation.
If you need a “mixed” relation (more or equal / less or equal), then you can choose more than one constant, adding them to each other.
Type IGNORE if you want to force the speed.

Direction
---------
Type the proper ROLLDIR_... constants to choose the required direction.

Speed field
-----------
Type here the speed you want to check or force.
(Approximately?) 512 means the speed of the ball is just enough for a one square long travel. It means if the ball is on a horizontal route, then the speed starts decreasing now, so the ball stops after travelling one square. – If you force it, this speed will be kept till you abort it, the ball won't stop after travelling one square.

The code will be this:

Code:
        case 504:
              Get (enumGET.MY_PARAMETER_COMMAND, PARAM_SPEED_ROLLINGBALL, Timer);
  
              MyData.Save.Local.BallSpeedIndex = GET.pParam->pVetArg[1];
              MyData.BallSpeedDirection = GET.pParam->pVetArg[3];
              MyData.Save.Local.BallSpeedSpeed = GET.pParam->pVetArg[4];
  
              Get (enumGET.ITEM, MyData.Save.Local.BallSpeedIndex + NGLE_INDEX, 0);
  
              if (Extra == 0) { //start
                    if (MyData.BallSpeedDirection == ROLLDIR_NORTH) MyData.Save.Local.RollBallFlag = 1;
                    if (MyData.BallSpeedDirection == ROLLDIR_SOUTH) MyData.Save.Local.RollBallFlag = 2;
                    if (MyData.BallSpeedDirection == ROLLDIR_EAST) MyData.Save.Local.RollBallFlag = 3;
                    if (MyData.BallSpeedDirection == ROLLDIR_WEST) MyData.Save.Local.RollBallFlag = 4;
              }
              if (Extra == 1) { //stop
                    MyData.Save.Local.RollBallFlag = 0;
              }
        break;
  
As you can see:
- This time we removed GET.pParam->pVetArg[2] field from the code, we don’t need it, the field is totally ignored for this setup.
That is why we don’t need this time a “if (GET.pParam->pVetArg[2] != -1)” condition. I mean, the field is not only ignored under some circumstances now (unlike the case of “this will be Lara’s new health, except if you choose IGNORE” I said above), but absolutely unnecessary.
- BallSpeedIndex and BallSpeedSpeed are “Save” variables this time, because we need them in cbCycleBegin (see below). I mean, as I said above, rollingball speeds need to be forced continuously. And, if you save/load the game, the continuous event will start searching for the data again that it should force – which is saved in “Save” variables.

In cbCycleBegin:

Code:
  if (MyData.Save.Local.RollBallFlag > 0) {
        Get (enumGET.ITEM, MyData.Save.Local.BallSpeedIndex + NGLE_INDEX, 0);
  
        if (MyData.Save.Local.RollBallFlag == 1) { //north
              GET.pItem->Reserved_34 = -MyData.Save.Local.BallSpeedSpeed;
        }
        if (MyData.Save.Local.RollBallFlag == 2) { //south
              GET.pItem->Reserved_34 = MyData.Save.Local.BallSpeedSpeed;
        }
        if (MyData.Save.Local.RollBallFlag == 3) { //east
              GET.pItem->Reserved_36 = MyData.Save.Local.BallSpeedSpeed;
        }
        if (MyData.Save.Local.RollBallFlag == 4) { //west
              GET.pItem->Reserved_36 = -MyData.Save.Local.BallSpeedSpeed;
        }
  }
As you can see, the custom flag this time has different values, from 1 to 4, to start different continuous events.

Note:
“You cannot force any speed if the ball isn’t activated” – this note can be easily removed, as a useless one, if you add an exported function (Action trigger) to the code, to activate the ball everyway, if Extra=0.

More than one index:

Do you see what the problem is with the “force the rollingball speed” setup?
No? Then I help:

- In cbCycleBegin, the continuous event gets ONE item index from MyData.Save.Local.BallSpeedIndex variable, and
- it gets the direction flag only for that item from MyData.Save.Local.RollBallFlag variable, and
- it gets the speed only for that item from MyData.Save.Local.BallSpeedSpeed variable.

So the setup works only for ONE item at the same time. You need to stop forcing the speed before you force the speed for another rollingball. You should warn the level builder about it in the trigger remark – or in the description of PARAM_SPEED_ROLLINGBALL:

Notes:
- The forced speed is always constant, unlikely the default one. You need to accept it.
- You cannot force any speed if the ball isn’t activated.
- You can force the speed only for one rollingball at the same time. You need to stop that force before forcing the speed for another rollingball.
- HEAVY triggers are important in the forcing setup. These triggers are activated by the rollingball, and the trigger subject itself is also the rollingball. Two examples:

Okay, but the rollingball is an important object. I mean, for example, if you want to disable all the attacks of a laserhead enemy (so he will be only searching for/looking at Lara, harmlessly), then you need to force Value 4 in Reserved_34 (Custom A) field. (Special values like that, not only for laserheads, can be identified in this tutorial.) - Yes, it means that setup is similar to the rollingball speed setup: cbCycleBegin will force Value 4 continuously for the laserhead item that is identified in a trigger.
What I want to say is a laserhead is not so important, I don’t think you have more than one laserhead at the same time, so it is enough if the setup works only for one laserhead at the same time. But you need often more than one rollingball at the same time – what if you want to force speed for more than one of them at the same time?

First of all I’d like to suggest that solve it with a vector, something you could see above in the example with the flipmaps. The main difference is this time the amount of rollingballs in the code should be “infinite”.
I mean, MyData.Save.Local.VetBallSpeedIndex vector variable is never negative (because you always need to define it in the Script, there is no “IGNORE” here) and object indices can be even around one thousand, so the size of this variable should be WORD. But the amount of vector indices in a variable, even if that is BYTE, WORD, short or anything else, are not affected. – So the variable size has no effect on the amount of its own vector indices, technically those indices are “unlimited”.
This “unlimited feature” is useful this time, because it is complicated to store vector indices we don’t just need.
For example, you set the indices, one after the other, in MyData.Save.Local.VetBallSpeedIndex variable:

MyData.Save.Local.VetBallSpeedIndex [0] = 57
MyData.Save.Local.VetBallSpeedIndex [1] = 124
MyData.Save.Local.VetBallSpeedIndex [2] = 246
MyData.Save.Local.VetBallSpeedIndex [3] = 172

So you have four rollingballs with forced speed now. Let’s suppose you use Export=1 value in the trigger, to stop the force for ID246 rollingball. Now VetBallSpeedIndex [2] becomes free. So when you force the speed the next time for a ball, then not the next ID (4), but the smallest free ID (2) should be used. That is why we should store the free ID’s in another vector, so VetBallSpeedIndex will get the indices as the values of that other vector. - It is possible, but too complicated. Why don’t wee use that “infinite” feature? I mean, if VetBallSpeedIndex [2] isn’t needed any more, then we simply forget it (perhaps also turning the value into 0, so even the tiniest memory won’t be wasted), and use ID4 for the next ball, like:

MyData.Save.Local.VetBallSpeedIndex [0] = 57
MyData.Save.Local.VetBallSpeedIndex [1] = 124
MyData.Save.Local.VetBallSpeedIndex [2] = 0
MyData.Save.Local.VetBallSpeedIndex [3] = 172
MyData.Save.Local.VetBallSpeedIndex [4] = 483

Which means we don’t know the number we should type in [] brackets when declaring the variable, it is “infinite”.
Perhaps there is some solution, but I bet that is not for beginners. Possibly the best “newbie” solution is to type some big number that never be exceeded. 1000, for example. You never have 1000 rollingballs in a level, and even if you force the speed even more than once for a ball, that will supposedly never be exceeded – like here:

MyData.Save.Local.VetBallSpeedIndex [0] = 57
MyData.Save.Local.VetBallSpeedIndex [1] = 57
MyData.Save.Local.VetBallSpeedIndex [2] = 124
MyData.Save.Local.VetBallSpeedIndex [3] = 172
MyData.Save.Local.VetBallSpeedIndex [4] = 124

What happened here? Well, you forced a speed for Ball57, then another speed. Then you forced a speed for Ball124, then Ball172, then you changed the forced speed of Ball124. – This is 5 ID for 3 balls.
If you have 50 rollingballs in a level, which is a remarkably huge amount, I suppose, then 1000/50 = 20, so averagly you can force speed for each of them 20 times, which is also big. Yes, 1000 is definitely enough.

The problem is I don’t like this solution. I mean, what if I would like to do even something else with that forced speed? Eg. to examine it in a condition. “If the speed of Ball X is..., then...”, which is “if speed of MyData.Save.Local.VetBallSpeedIndex [???] = X is..., then...”.
Yes, as Signs ? say, which is the vector ID we search? – I don’t say it is impossible to solve it, but it is unnecessarily complicated to be solved this way for a beginner.

What I really suggest is add vector indices manually in these cases. It will be not a really big effort even for the level builder. (Plus, the builder will have a bigger control on the procedure this way.)
I mean, first of all, I suggest that establish the limit you find logical. I mean, FIVE rollingballs at the same time with forced speeds seem logical, don’t they? Five seems a proper upper limit, it is unlikely that the level builder wants to force the speed for more than five rollingballs at the same time, isn’t it?
That is why first remove that “You can force...” entry now:

Then, add another field (”Case”) to the syntax – which is GET.pParam->pVetArg[5]:

Syntax: Parameters=PARAM_SPEED_ROLLINGBALL, IdParamList, ObjectIndex, Relation, Direction, Speed, Case

Then add the description of “Case” field:

Speed field
-----------
Type here the speed you want to check or force.
(Approximately?) 512 means the speed of the ball is just enough for a one square long travel. It means if the ball is on a horizontal route, then the speed starts decreasing now, so the ball stops after travelling one square. – If you force it, this speed will be kept till you abort it, the ball won't stop after travelling one square.

Case field
----------
You need to add a case ID to the rollingball: 0, 1, 2, 3 or 4, if you force the speed.
I mean, you can have maximum five rollingballs at the same time, with a forced speed.
You need another F504 to abort the forced speed, before you use its case ID again for another rollingball.
Type IGNORE if you check the speed, not force.

In the trigger/cbCycleBegin code, we need to change:

- MyData.Save.Local.BallSpeedIndex variable is changed for MyData.Save.Local.VetBallSpeedIndex [5].
- MyData.Save.Local.RollBallFlag variable is changed for MyData.Save.Local.VetRollBallFlag [5].
- MyData.Save.Local.BallSpeedSpeed variable is changed for MyData.Save.Local.VetBallSpeedSpeed [5].
- MyData.BallSpeedIndex is added as the main marker of the item index.
- MyData.BallSpeedSpeed is added as the main marker of the speed.
- MyData.BallSpeedCase added to host the “cases”.

Code:
        case 504:
              Get (enumGET.MY_PARAMETER_COMMAND, PARAM_SPEED_ROLLINGBALL, Timer);
  
              MyData.BallSpeedIndex = GET.pParam->pVetArg[1];
              MyData.BallSpeedDirection = GET.pParam->pVetArg[3];
              MyData.BallSpeedSpeed = GET.pParam->pVetArg[4];
              MyData.BallSpeedCase = GET.pParam->pVetArg[5];
  
              Get (enumGET.ITEM, MyData.BallSpeedIndex + NGLE_INDEX, 0);
  
              if (Extra == 0) { //start
                    MyData.Save.Local.VetBallSpeedIndex [MyData.BallSpeedCase] = MyData.BallSpeedIndex;
                     MyData.Save.Local.VetBallSpeedSpeed [MyData.BallSpeedCase] = MyData.BallSpeedSpeed;
                    if (MyData.BallSpeedDirection == ROLLDIR_NORTH) MyData.Save.Local.VetRollBallFlag [MyData.BallSpeedCase] = 1;
                    if (MyData.BallSpeedDirection == ROLLDIR_SOUTH) MyData.Save.Local.VetRollBallFlag [MyData.BallSpeedCase] = 2;
                    if (MyData.BallSpeedDirection == ROLLDIR_EAST) MyData.Save.Local.VetRollBallFlag [MyData.BallSpeedCase] = 3;
                    if (MyData.BallSpeedDirection == ROLLDIR_WEST) MyData.Save.Local.VetRollBallFlag [MyData.BallSpeedCase] = 4;
              }
              if (Extra == 1) { //stop
                    MyData.Save.Local.VetRollBallFlag [MyData.BallSpeedCase] = 0;
              }
        break;
Code:
  {
BYTE i;

for (i=0; i<5; i++) {
        if (MyData.Save.Local.VetRollBallFlag [i] > 0) {
              Get (enumGET.ITEM, MyData.Save.Local.VetBallSpeedIndex [i] + NGLE_INDEX, 0);
  
              if (MyData.Save.Local.VetRollBallFlag [i] == 1) { //north
                    GET.pItem->Reserved_34 = -MyData.Save.Local.VetBallSpeedSpeed [i];
              }
              if (MyData.Save.Local.VetRollBallFlag [i] == 2) { //south
                    GET.pItem->Reserved_34 = MyData.Save.Local.VetBallSpeedSpeed [i];
              }
              if (MyData.Save.Local.VetRollBallFlag [i] == 3) { //east
                    GET.pItem->Reserved_36 = MyData.Save.Local.VetBallSpeedSpeed [i];
              }
              if (MyData.Save.Local.VetRollBallFlag [i] == 4) { //west
                    GET.pItem->Reserved_36 = -MyData.Save.Local.VetBallSpeedSpeed [i];
              }
        }
  }
  }
So you have only

MyData.Save.Local.VetBallSpeedIndex [0]
MyData.Save.Local.VetBallSpeedIndex [1]
MyData.Save.Local.VetBallSpeedIndex [2]
MyData.Save.Local.VetBallSpeedIndex [3]
MyData.Save.Local.VetBallSpeedIndex [4]

to force the speed. When you use 0, 1, 2, 3 or 4 in the trigger with Extra=0, added it in the script (“Case”), then feel free to choose any of them. You can use that 0, 1, 2, 3 or 4 again only if you stop the force of that ball in an Extra=1 trigger, so if that ID is free again.

However, if you find this vector method too complicated, then you can try a more primitive method – use one variable for each “case”.
So, after changing SCRIPT file just I said above, change the default code in another way

- MyData.Save.Local.BallSpeedIndex variable is changed for MyData.Save.Local.BallSpeedIndex0, 1, 2, 3 and 4.
- MyData.Save.Local.RollBallFlag variable is changed for MyData.Save.Local.RollBallFlag0, 1, 2, 3 and 4.
- MyData.Save.Local.BallSpeedSpeed variable is changed for MyData.Save.Local.BallSpeedSpeed0, 1, 2, 3 and 4.
- MyData.BallSpeedIndex is added as the main marker of the item index.
- MyData.BallSpeedSpeed is added as the main marker of the speed.
- MyData.BallSpeedCase added to host the “cases”.

I don’t want to type so much now, so I change the code now only for two rollingballs, not five:

Code:
        case 504:
              Get (enumGET.MY_PARAMETER_COMMAND, PARAM_SPEED_ROLLINGBALL, Timer);
  
              MyData.BallSpeedIndex = GET.pParam->pVetArg[1];
              MyData.BallSpeedDirection = GET.pParam->pVetArg[3];
              MyData.BallSpeedSpeed = GET.pParam->pVetArg[4];
              MyData.BallSpeedCase = GET.pParam->pVetArg[5];
  
              Get (enumGET.ITEM, MyData.BallSpeedIndex + NGLE_INDEX, 0);
  
            if (MyData.BallSpeedCase == 0) {
                    if (Extra == 0) { //start
                         MyData.Save.Local.BallSpeedIndex0 = MyData.BallSpeedIndex;
                          MyData.Save.Local.BallSpeedSpeed0 = MyData.BallSpeedSpeed;
                          if (MyData.BallSpeedDirection == ROLLDIR_NORTH) MyData.Save.Local.RollBallFlag0= 1;
                          if (MyData.BallSpeedDirection == ROLLDIR_SOUTH) MyData.Save.Local.RollBallFlag0= 2;
                          if (MyData.BallSpeedDirection == ROLLDIR_EAST) MyData.Save.Local.RollBallFlag0= 3;
                          if (MyData.BallSpeedDirection == ROLLDIR_WEST) MyData.Save.Local.RollBallFlag0= 4;
                    }
                    if (Extra == 1) { //stop
                          MyData.Save.Local.RollBallFlag1= 0;
                    }
     }
              if (MyData.BallSpeedCase == 1) {
                    if (Extra == 0) { //start
                         MyData.Save.Local.BallSpeedIndex1 = MyData.BallSpeedIndex;
                          MyData.Save.Local.BallSpeedSpeed1 = MyData.BallSpeedSpeed;
                          if (MyData.BallSpeedDirection == ROLLDIR_NORTH) MyData.Save.Local.RollBallFlag1= 1;
                          if (MyData.BallSpeedDirection == ROLLDIR_SOUTH) MyData.Save.Local.RollBallFlag1= 2;
                          if (MyData.BallSpeedDirection == ROLLDIR_EAST) MyData.Save.Local.RollBallFlag1= 3;
                          if (MyData.BallSpeedDirection == ROLLDIR_WEST) MyData.Save.Local.RollBallFlag1= 4;
                    }
                    if (Extra == 1) { //stop
                          MyData.Save.Local.RollBallFlag1= 0;
                    }
              }
        break;
  
  if (MyData.Save.Local.RollBallFlag0 > 0) {
        Get (enumGET.ITEM, MyData.Save.Local.BallSpeedIndex0 + NGLE_INDEX, 0);
  
        if (MyData.Save.Local.RollBallFlag0 == 1) { //north
              GET.pItem->Reserved_34 = -MyData.Save.Local.BallSpeedSpeed0;
        }
        if (MyData.Save.Local.RollBallFlag0 == 2) { //south
              GET.pItem->Reserved_34 = MyData.Save.Local.BallSpeedSpeed0;
        }
        if (MyData.Save.Local.RollBallFlag0 == 3) { //east
              GET.pItem->Reserved_36 = MyData.Save.Local.BallSpeedSpeed0;
        }
        if (MyData.Save.Local.RollBallFlag0 == 4) { //west
              GET.pItem->Reserved_36 = -MyData.Save.Local.BallSpeedSpeed0;
        }
  }
  if (MyData.Save.Local.RollBallFlag1 > 0) {
  Get (enumGET.ITEM, MyData.Save.Local.BallSpeedIndex1 + NGLE_INDEX, 0);
  
        if (MyData.Save.Local.RollBallFlag1 == 1) { //north
              GET.pItem->Reserved_34 = -MyData.Save.Local.BallSpeedSpeed1;
        }
        if (MyData.Save.Local.RollBallFlag1 == 2) { //south
              GET.pItem->Reserved_34 = MyData.Save.Local.BallSpeedSpeed1;
        }
        if (MyData.Save.Local.RollBallFlag1 == 3) { //east
              GET.pItem->Reserved_36 = MyData.Save.Local.BallSpeedSpeed1;
        }
        if (MyData.Save.Local.RollBallFlag1 == 4) { //west
              GET.pItem->Reserved_36 = -MyData.Save.Local.BallSpeedSpeed1;
        }
  }
Note:
Case field isn’t needed in the speed-checking setup, so we don’t need to change that setup.

Other indices:

Naturally not only object indices can be asked in Parameters command fields.
Let’s see two examples:

1. You ask a TriggerGroup index in GET.pParam->pVetArg[3] field, to execute it.
The code is a single “export function” of an F118 trigger, that should execute TriggerGroups. I exported it with a random ID (that you can find after 118), and then I swapped that random ID for the field ID:

Code:
  PerformFlipeffect(NULL, 118, GET.pParam->pVetArg[3], 1);
Note:
Previosuly we discussed what single, multiple or continuous executions mean for TriggerGroups. This time it is a “single” execution.

2. You ask a ColorRGB index in GET.pParam->pVetArg[5] field. You need the GET_COLOR_RGB constant for this in the code to convert that ColorRGB command ID into the color value (GET.Color) of the color command.
The code is:

Code:
  Get (enumGET.COLOR_RGB, GET.pParam->pVetArg[5], COLF_TOMB_COLOR);
  ... = GET.Color;
… is eg. a custom variable that takes that color value.

Note:
COLF_TOMB_COLOR flag is necessary, or else the color will be read in BlueGr

Last edited by AkyV; 03-11-17 at 23:13.
AkyV is offline  
Old 03-11-17, 21:55   #30
AkyV
Moderator
 
Joined: Dec 2011
Posts: 4,881
Default

29. Binary shifting

Let’s see some examples where you need the binary shifting.

Color code:

Let’s see for example this long color code. 16737330 is the code of R/G/B = 255/100/50.
Why? The explanation is simple: 16737330 is 255+100+50, if Sign + means not „add” now, but „tie”, „link”.

You can get that value easily yourself:

1. First turn each color component into hexadecimal. (255/100/50 => $FF/$64/$32.)
2. Then write them after each other. ($FF/$64/$32 => $FF6432.)
3. Then turn it back into decimal. ($FF6432 => 16737330.)

This method is useful eg. if there is a field with the long code (eg. GET.pRoom->ColorIntensityLight for the rooms) and you would like to examine the red, green or blue components of this.
Or, if you have a Parameters Script command where you don’t refer to a ColorRGB command ID, but ask the red, green and blue components in their own fields of this Parameters command – but the code works only with the long code, so you need to convert it into that.
Etc.

But how does it work in the code?
Well, this is where we need to use the binary shifting.
What is this? Well, again:

If I type hexadecimal 32 in my Windows calculator, and turn it into decimal, then I get 50.
If I type hexadecimal 64 in my Windows calculator, and turn it into decimal, then I get 100.
If I type hexadecimal FF in my Windows calculator, and turn it into decimal, then I get 255.

$(FF64)32 => $(xxxx)32 = $32 = 50
$(FF)64(32) => $(xx)64(xx) = $64 = 100
$FF(6432) => $FF(xxxx) = $FF = 255

So what you need is to “cut off” the values of the other two color components, and then move the required component value to the position on the right side, and now you can convert it:

- For $32: cut off $FF and $64. $32 is in the right position, you don’t need to move it.
- For $64: cut off $FF and $32. Then move $64 with two characters to the right side, into the position of $32.
- For $FF: cut off $64 and $32. Then move $FF with four characters to the right side, into the position of $32.

But we said “binary shifting”, not “hexadecimal”, so let’s see how FF6432 looks if you turn it into binary in your calculator:

11111111 01100100 00110010

The first eight characters (11111111) is $FF = 255 (red).
The second eight characters (01100100) is $64 = 100 (green).
The third eight characters (00110010) is $32 = 50 (blue).

It is easy to understand. Let’s see eg. 100 (01100100):

2 to power of 0 (1) = none = 0
2 to power of 1 (2) = none = 0
2 to power of 2 (4) = yes = 4
2 to power of 3 (8) = none = 0
2 to power of 4 (16) = none = 0
2 to power of 5 (32) = yes = 32
2 to power of 6 (64) = yes = 64
2 to power of 7 (128) = none = 0

4+32+64=100

What we will use is the so-called “bitwise AND operation”. If you use it, then all you need to know is only three rules:

1+1 = 1
0+0 = 0
1+0 = 0

So if you would like to cut 255 and 100 off 50, then you need this operation:

Code:
        11111111 01100100 00110010
  +     00000000 00000000 11111111
  _______________________________
        00000000 00000000 00110010
It is easy to find out the value of 00000000 00000000 11111111, if you see 11111111. It is 255 ($FF)!
So $FF6432 + $FF = $32.
Supposing that 16737330 color code is stored in „Color” variable, this is the code to get the blue component from the long color code:

Color &0xFF

0xFF is $FF, and & is the sign of “bitwise AND” operation this time.

And now let’s see the code of the green component.
First you need to move it, with eight characters, to the position of the blue component.
Then you need to add to 255 to remove the unnecessary parts:

Code:
                 11111111 01100100 00110010
  +     00000000 00000000 11111111
  _______________________________
        00000000 00000000 01100100
The sign of moves like that is “>>” (moving rightwards) or “<<” (moving leftwards).
So the code should look this way:

(Color>>8) &0xFF

And now let’s see the code of the red component.
First you need to move it, with sixteen characters, to the position of the blue component.
Then you need to add to 255 to remove the unnecessary parts:

Code:
                          11111111 01100100 00110010
  +     00000000 00000000 11111111
  _______________________________
        00000000 00000000 11111111
So the code should look this way:

(Color>>16) &0xFF

Moreover, as you can see, green and blue components “have been moved out” of the “valid zone”, which led to they have been cut off by themselves, we don’t need +255 in this case, so it is also a working code now for the red component:

Color>>16

And now let’s see three condition triggers:
- One of them checks if the ambience color red component of the actual room of Lara is the required value between 0 and 255 (chosen in “Object to trigger” box).
- The second one checks if the ambience color green component of the actual room of Lara is the required value between 0 and 255 (chosen in “Object to trigger” box).
- The third one checks if the ambience color blue component of the actual room of Lara is the required value between 0 and 255 (chosen in “Object to trigger” box).

The trigger codes are simple:

Red:
Code:
  Get(enumGET.LARA, 0, 0);
  Get(enumGET.ROOM, GET.pLara->Room, 0);
  if ((GET.pRoom->ColorIntensityLight>>16) == ItemIndex) return RetValue | CTRET_IS_TRUE;
  return RetValue;
Green:
Code:
  Get(enumGET.LARA, 0, 0);
  Get(enumGET.ROOM, GET.pLara->Room, 0);
  if (((GET.pRoom->ColorIntensityLight>>8) &0xFF) == ItemIndex) return RetValue | CTRET_IS_TRUE;
  return RetValue;
Blue:
Code:
  Get(enumGET.LARA, 0, 0);
  Get(enumGET.ROOM, GET.pLara->Room, 0);
  if ((GET.pRoom->ColorIntensityLight &0xFF) == ItemIndex) return RetValue | CTRET_IS_TRUE;
  return RetValue;
Another example – there is a custom function, to convert Red, Green and Blue components to a color code. And there is a trigger to force that color, as inner light color, for Moveable objects:

The function code:

Code:
  DWORD LightColor(BYTE Red, BYTE Green, BYTE Blue)
  {
  
        DWORD RGBValue;
  
        RGBValue = (Red<<16) + (Green<<8) + Blue;
  
        return RGBValue;
  
  }
Or shortly:

Code:
  DWORD LightColor(BYTE Red, BYTE Green, BYTE Blue)
  {
  
        return (Red<<16) + (Green<<8) + Blue;
  
  }
What happens here?
Well, the inverse procedure will be executed – not converting the long color code in Red/Green/Blue components, but converting Red/Green/Blue components into the long color code, moving Red leftwards with 16 characters and moving Green leftwards with 8 characters, and then adding them (not in bitwise mode) to each other:

Code:
    11111111
+            01100100
+                     00110010
_______________________________
    11111111 01100100 00110010
The Flipeffect trigger code:

Code:
        Get (enumGET.MY_PARAMETER_COMMAND, PARAM_COLOR_RGB, Timer);
  
        MyData.Save.Local.ObjIndex = GET.pParam->pVetArg[1];
        MyData.Save.Local.RedColor = GET.pParam->pVetArg[2];
        MyData.Save.Local.GreenColor = GET.pParam->pVetArg[3];
        MyData.Save.Local.BlueColor = GET.pParam->pVetArg[4];
  
        MyData.Save.Local.ColorFlag = 1;
The syntax of that Parameters command: Parameters = PARAM_COLOR_RGB, IdParamList, ObjectIndex, Red, Green, Blue.

The “ColorFlag” variable is needed to start a continuous event, or else the effect is useless. However, the game will always try to restore the original outer color of the object. The solution is a pulsing color. The pulse will always happen between the forced color and the original light. (This time we won’t stop pulsing with this trigger. We could do it, though: all we need is an Extra value where flag=0 value is forced.)

cbCycleBegin code:

Code:
        if (MyData.Save.Local.ColorFlag == 1) {
              Get (enumGET.ITEM, MyData.Save.Local.ObjIndex + NGLE_INDEX, 0);
              GET.pItem->LightRGB = LightColor (MyData.Save.Local.RedColor, MyData.Save.Local.GreenColor, MyData.Save.Local.BlueColor);
        }
Which would look this way without the function:

Code:
        if (MyData.Save.Local.ColorFlag == 1) {
              Get (enumGET.ITEM, MyData.Save.Local.ObjIndex + NGLE_INDEX, 0);
              GET.pItem->LightRGB = (MyData.Save.Local.RedColor<<16) + (MyData.Save.Local.GreenColor<<8) + MyData.Save.Local.BlueColor;
        }
Note:
Naturally it is not a 24 characters’ long number split into three 8 characters’ long parts, if it is not a color code. Here is an example, if you want to keep only the third and the fourth characters, in a 10 characters’ long number:

Code:
           1101101100 = 876 = $36C
          +0011           = 3 = $3
(Number>>6) &0x3

OCB codes:

0: Bit 0 ($00000001 ; 1)
1: Bit 1 ($00000002 ; 2)
2: Bit 2 ($00000004 ; 4)
3: Bit 3 ($00000008 ; 8)
4: Bit 4 ($00000010 ; 16)
5: Bit 5 ($00000020 ; 32)
6: Bit 6 ($00000040 ; 64)
7: Bit 7 ($00000080 ; 128)
8: Bit 8 ($00000100 ; 256)
9: Bit 9 ($00000200 ; 512)
10: Bit 10 ($00000400 ; 1024)
11: Bit 11 ($00000800 ; 2048)
12: Bit 12 ($00001000 ; 4096)

This is a part of a list if you get if you type #BIT_LIST# preset argument in a trigger.
Let’s suppose we want a trigger where you examine if a Moveable object (#MOVEABLES# in “Object to trigger” box) has an OCB bitflag (#BIT_LIST# in “Extra” box). (See eg. a pushblock where you can have 32, 64, 128 etc. OCB value.)

The Condition trigger code is very simple:

Code:
        Get (enumGET.ITEM, ItemIndex, 0);
  
        if (GET.pItem ->OcbCode & (1 << Extra)) return RetValue | CTRET_IS_TRUE;
        return RetValue;
“1<<Extra” means “if you move Value 1 leftwards with the value of Extra”.

If Extra = 0, then 1 isn’t moved = binary 1 = decimal 1.
If Extra = 1, then 1 is moved with 1 character = binary 10 = decimal 2.
If Extra = 2, then 1 is moved with 2 characters = binary 100 = decimal 4.
If Extra = 3, then 1 is moved with 3 characters = binary 1000 = decimal 8.
If Extra = 4, then 1 is moved with 4 characters = binary 10000 = decimal 16.
If Extra = 5, then 1 is moved with 5 characters = binary 100000 = decimal 32.
Etc.

So if Extra=0, then OcbCode & (1 << Extra) means OcbCode & 1, “OCB has OCB flag 1”.
So if Extra=1, then OcbCode & (1 << Extra) means OcbCode & 2, “OCB has OCB flag 2”.
So if Extra=2, then OcbCode & (1 << Extra) means OcbCode & 4, “OCB has OCB flag 4”.
So if Extra=3, then OcbCode & (1 << Extra) means OcbCode & 8, “OCB has OCB flag 8”.
So if Extra=4, then OcbCode & (1 << Extra) means OcbCode & 16, “OCB has OCB flag 16”.
So if Extra=5, then OcbCode & (1 << Extra) means OcbCode & 32, “OCB has OCB flag 32”.
Etc.

Double field:

“GET.pItem->Objectbuttons” field is a “double field”, which means two different properties can be checked here:
- The on/off status of the trigger codebit buttons and the One Shot button. (The first eight characters.)
- The flags of the item. (The second eight characters.)

FITEM item flags can be checked even in “GET.pItem->FlagsMain” field, so if would like to check the on/off status then you need to move the proper characters into the position of the flag characters:

(GET.pItem->Objectbuttons)>>8
AkyV is offline  
Closed Thread

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 12:43.


Powered by vBulletin® Version 3.8.11
Copyright ©2000 - 2024, vBulletin Solutions Inc.
Tomb Raider Forums is not owned or operated by CDE Entertainment Ltd.
Lara Croft and Tomb Raider are trademarks of CDE Entertainment Ltd.