The random ramblings of a French programmer living in Norway...
2021
← High Quality Resources
  Internal Structures
Fri 10th September 2021   
Adeline Software
 Part 1 Part 2 Part 3 Part 4 Part 5 Part 6 

What about some internal implementation details?

A few weeks ago I published a new youtube video where I presented the debugging interface of Time Commando.

Maybe it is time to look in more details about some of the elements I presented?


In the video I mentionned the ACF and HQR files.

Let see what some of these HQR scene level files are made of.

SCENEPC.LST

As anyone who dug into Little Big Adventure knows, most of the data in the game is stored in the HQR archive files, generated by MAKEHQR from a set of files and associated LST file.

Here is an example of LST file generated for the PC version of Time Commando levels:

#rem
#rem Liste HQR des fichiers nécéssaires au fonctionnement
#rem de TimeCommando PC
#rem
#rem OBJETS et ARMES de la scène
F:\PROJET\TIME\SCENE\STAGE00\RUN0\TEMP\SCENE.SCC
#rem FICHES des OBJETS et des ARMES
F:\PROJET\TIME\SCENE\STAGE00\RUN0\TEMP\SCENE.FIC
#rem PROGRAMMES (tracks) des OBJETS
F:\PROJET\TIME\SCENE\STAGE00\RUN0\TEMP\SCENE.TRK
#rem BODIES des OBJETS et ARMES
F:\PROJET\TIME\SCENE\STAGE00\RUN0\TEMP\SCENE.BOC
#rem SAMPLES
F:\PROJET\TIME\SCENE\STAGE00\RUN0\TEMP\SCENE.SAC
#rem ANIMATIONS des OBJETS
F:\PROJET\TIME\SCENE\STAGE00\RUN0\TEMP\SCENE.ANC
#rem TEXTURES des OBJETS et ARMES
F:\PROJET\TIME\SCENE\STAGE00\RUN0\TEMP\SCENE.TEX
#rem FEUILLES 3D du sol
F:\PROJET\TIME\GRAPH\DECORS\STAGE00\RUN0\SCENE.SOL
#rem Palette SHADE
F:\PROJET\TIME\GRAPH\DECORS\STAGE00\RUN0\PALTIME.SHD
#rem SPRITES 320x240
F:\PROJET\TIME\GRAPH\SPRITES\STAGE00\ICO240.GPH
#rem SPRITES 320x480
F:\PROJET\TIME\GRAPH\SPRITES\STAGE00\ICO480.GPH
#rem SPRITES 640x480
F:\PROJET\TIME\GRAPH\SPRITES\STAGE00\ICO640.GPH
#rem définition des QUAD
F:\PROJET\TIME\SCENE\STAGE00\RUN0\SCENE.QAD
#rem définition des ZONES, COLLISIONS, BONUS, FLOTS et EFFETS
F:\PROJET\TIME\SCENE\STAGE00\RUN0\TEMP\SCENE.BNC
#rem définition des SPRITES 3D
F:\PROJET\TIME\GRAPH\SPRITES\STAGE00\EFFETS.GPR
#rem définition des MUSIQUE OPL
F:\PROJET\TIME\MUSIC\STAGE00\MUSIC_OP.XMI
#rem définition des MUSIQUE GENERAL MIDI
F:\PROJET\TIME\MUSIC\STAGE00\MUSIC_GM.XMI

Knowing the content of this file is specially important if you plan to reverse engineer the game by extracting the data, because it tells you the type of each entry, and in which order they are stored.

In this particular article I will just concentrate on the first one, the SCENE.SCC file, which is binary exported version of the SCENE.SCE file.

SCENE.SCE

The "SCE" stands for "SCEne".

Each of the levels has a single SCE file with the list of all actors, weapons, traps, interactive zones, etc... which will get processed by the Time Commando Script Compiler into a set of binary files designed to be directly loaded into memory in an immediately usable format requiring no additional memory allocation.

The typical SCE file content looks like what follows.

Most of the system uses a structure syntax with sections starting by "Set" and ending by "End".

Many of these sections have "Life Frames" which define when the element is enabled and disabled, to avoid having to process all of them during the entire duration of the level.


SetObj [Object Name] [Class name]
InitPosition [X] [Y] [Z]
InitLifeFrame [Start] [End] [Lock]
InitBeta [Angle]
InitBody [3D Object]
InitAnim [Animation]
InitLifePoint [Hit points]
InitWeapon [Weapon name] [Ammonition count]
Init...

StartProgram
(main script)
EndProgram

StartProgram
(one or many subroutines)
EndProgram
EndObj


The "Obj" sections are used to define complex entities which have a visual representation, such as the hero, the enemies, traps, container where you empty the memory chips, etc.... the first parameter after the SetObj is the individual name of this object, and the second parameter defines it's "class", which is defined in some other file1 and tells us which meshes and animations are available2.

The various Init parameters define the starting position, orientation, mesh and animation used by this particular actor.

The "Lock" frame, is a parameter used to control the movement of the camera: As long as the object is active (alive), the camera path will not move farther than the indicated frame id.

There are also optional "InitFlag" commands which can be used to turn of some of the object flags:
  • InitFlagNoZv will deactivate the object bounding box
  • InitFlagNoTarget makes this object impossible to target
  • InitFlagNoGravity stops the object from falling to the ground
  • InitFlagNoShadow disables the object shadow casting
  • InitFlagNoGiveWeapon makes the weapon disappear on death


SetTrack
StartProgram
(main script)
EndProgram
EndTrack


The "Track" sections are somewhat similar, but have no attached visual representation. We used that to run some stand-alone scripts that run on the side to control events


SetWeapon Stone
InitPosition 6450,-1,2650
InitLifeFrame -1, 175
InitBeta 0
Count 1
EndWeapon


Similarly, the "Weapon" sections are used to put some weapons on the floor for the player to find.

In this example, the -1 value in InitPosition means that the object will be snapped to the ground level automatically.

SCENE.SCC

The "SCC" stands for "Scène Compilée" or "Compiled Scene" in English.

This file starts by a SCENE structure which contains the following:


SCENE:
U8 count_object
U8 count_weapon
U8 count_weapon_description
U8 count_track

U8 count__bonus_hide
U8 count_chip
U8 base_sprite_2d // PSX Only
U8 base_sprite_3d // PSX Only

S16 num_stage
S16 num_run


The num_stage and num_run fields are just a basic protection to make sure that people can't just change the order of levels, like starting directly by the level 7 by copying the folder3.

The "nb_objet" field tells us how many OBJECT structures follow:


OBJECT
S32 x
S32 y
S32 z

U16 beta

S16 start_frame
S16 end_frame
S16 lock_frame

U16 offset_description
U16 offset_track

WEAPON_INVENTORY weapons[6]

U16 flag
U8 num_body
U8 num_anim
U8 lifepoint
U8 nb_slot
U8 type_give_bonus
U8 nb_give_bonus

WEAPON_INVENTORY
U8 nb2
U8 pad
U8 nb
U8 num


Here are the possible values for the "flag" field:


#define OBJECT_NO_FLAG 0
#define OBJECT_TARGET 1
#define OBJECT_SHADOW 2
#define OBJECT_ZV 4
#define OBJECT_IMMORTEL 8
#define OBJECT_GRAVITY 16
#define OBJECT_INSHOCKABLE 32
#define OBJECT_GIVE_WEAPON 64
#define OBJECT_FLYING 128
#define OBJECT_HERO_RUN 256


the count_track field tells us how many TRACK structures follow:


TRACK
U16 start_frame
U16 end_frame

U16 offset_track
U8 pad[2]


and then finally the count_weapon field tells us how many WEAPON structures follow:


WEAPON
S32 x
S32 y
S32 z

U16 beta
U16 start_frame

U16 end_frame
U16 offset_description

U8 nb_arme
U8 num_arme
U8 pad[2]


I'm not 100% certain what nb_arme and num_arme are, one is probably the weapon type and the other how many are there, but not idea which is which!

The various offset_ variables are the relative positions of the resources in the relevant HQR file, so for example offset_track would point to the track data (script program) in the TRK entry in the HQR file.

The Red Dragon mystery

A few days ago, I got some comments from RK.Walnut on the blog, with a question regarding the fight with the Japanese Red Dragon:

(...)
Like “Furie blocking” if you’re hitting the monster/entity in the same way (several times over) then the script just blocks you outright.
(...)
But would it be possible for you to tell me the Furie values of the red dragon in the Japanese Middle Age Level 2? I’ve always suspected that this monster had very high Furie levels compared to many others in the game. Also is the Furie values dependent on the game difficulty much? Is it much lower in “Very Easy” compared to “Easy”, “normal” etc?

Ok, so here is a picture of what he is talking about:

The Dragon in the Temple
The Dragon in the Temple

Here is the entire unmodified script for the dragon4.

SetObj Dragon dragon
InitPosition 51000, -1, 4650
InitBeta 3072
InitLifePoint 40,4
InitLifeFrame 882, -1, 922
InitBody Dragon
InitAnim dra_Rien
InitArme BouleDeFeu 8
InitArme Crache 199
InitFlagNoGiveWeapon
StartProgram
Loop
UnFollow
FollowHero
DoAnimRepeat dra_Marche
ExitProgram

While TestAlive(Hero)
// pour tester plusieurs fois cette distance
__dho = GetDistance(Hero)

If __jouefurie
If TestAnim(dra_ChocF) | TestAnim(dra_ChocD) | TestAnim(dra_ChocG)
WaitAnim
EndIf
Fury(dra_CoupBoule)
WaitAnim
__jouefurie = 0
Else
// pour furie
If TestTouched
If GetSameHit > 2
__jouefurie = 1
ExitProgram
Else
// temps d'attente en fct de difficulty
If _dif==0
DoAnimOnce GetValueListRandom(dra_garde, dra_garde, dra_garde,
dra_garde, dra_rien)
ElseIf _dif==1
DoAnimOnce GetValueListRandom(dra_tourD, dra_tourG, dra_garde)
ElseIf _dif==2
DoAnimOnce GetValueList(-1, dra_tourD, dra_tourG)
EndIf
WaitAnim
EndIf
ClearTouched
ElseIf TestBlocked | !TestTrajectoire3D(Hero) | (!GetRandom(200) &
GetDistance(Hero < 3000))
//DoAnimOnce GetValueList(-1, dra_tourd, dra_tourG)
DoAnimOnce GetValueList(-1, dra_Marched, dra_MarcheG)
WaitAnim

UnFollow
FollowPoint 47881, 6406
ClearTouched
ExitProgram
Wait GetDistancePoint(47881, 6404) < 1000 | TestTouched
UnFollow
FollowHero
ExitProgram
EndIf

// Comportement selon distance Hero/Objet
If __dho < 475
// trop pres => recule ou coup de boule
If (TestLance | TestFrappe)
If _dif==0
DoAnimOnce GetValueList(-1, dra_Esqf, dra_Garde)
Else
DoAnimOnce dra_CoupBoule
EndIf
Else
DoAnimOnce GetValueListRandom(dra_Esqf, dra_MarcheD, dra_MarcheG,
dra_CoupBoule, dra_CoupBoule, dra_CoupBoule, dra_CoupBoule)
EndIf
ElseIf __dho < 1480-400
If (TestLance | TestFrappe)
GoSub Defense
Else
DoAnimOnce GetValueListRandom(dra_CoupBoule, dra_CoupBoule,
dra_CoupQueue, dra_crache50cmFlot, dra_CoupBoule)
GoSub SiEsquive
EndIf
ExitProgram
ElseIf __dho < 1555-400
If (TestLance | TestFrappe) & !GetRandom(_dif)
GoSub Defense
Else
DoAnimOnce GetValueListRandom(dra_CoupBoule, dra_CoupBoule,
dra_CoupQueue, dra_crache50cmFlot, dra_crache50cmFlot,
dra_Crache50cmFlot, dra_CoupBoule)
GoSub SiEsquive
EndIf
ExitProgram
ElseIf __dho < 2740-400
If (TestLance | TestFrappe) & !GetRandom(_dif)
GoSub Defense
Else
DoAnimOnce GetValueListRandom(dra_crache50cmFlot,
dra_crache50cmFlot, dra_crache50cmFlot, dra_CoupQueue)
GoSub SiEsquive
EndIf
ExitProgram
ElseIf __dho < 2815-400
If (TestLance | TestFrappe) & !GetRandom(_dif)
GoSub Defense
Else
DoAnimOnce GetValueList(-1, dra_crache50cmFlot,
dra_crache50cmFlot, dra_crache50cmFlot, dra_CoupQueue)
GoSub SiEsquive
EndIf
ExitProgram
ElseIf __dho < 3115-400
If (TestLance | TestFrappe) & !GetRandom(_dif)
GoSub Defense
Else
DoAnimOnce GetValueList(-1, dra_CoupQueue, dra_Crache50cmFlot)
GoSub SiEsquive
EndIf
ExitProgram
ElseIf GetNBArme(BouleDeFeu)<>0
// trop loin => avance ou crache
If _dif==0
If OtherGetYPos(Hero) > GetYPos
DoAnimOnce GetValueListRandom(dra_rien, dra_garde, dra_garde,
dra_crache100cm, dra_marche, dra_marcheD,
dra_marcheG, dra_Crache100cm)
Else
DoAnimOnce GetValueListRandom(dra_rien, dra_garde,
dra_crache100cm, dra_marche, dra_marcheD,
dra_marcheG, dra_Crache100cm)
EndIf
ElseIf _dif==1
If OtherGetYPos(Hero) > GetYPos
DoAnimOnce GetValueListRandom(dra_rien, dra_garde,
dra_crache100cm, dra_marcheD, dra_marcheG,
dra_Crache100cm)
Else
DoAnimOnce GetValueListRandom(dra_rien, dra_garde,
dra_crache50cm, dra_marcheD, dra_marcheG,
dra_Crache50cm)
EndIf
ElseIf _dif==2
If OtherGetYPos(Hero) > GetYPos
DoAnimOnce GetValueListRandom(dra_garde, dra_crache100cm,
dra_marcheD, dra_marcheG, dra_Crache100cm)
Else
DoAnimOnce GetValueListRandom(dra_garde, dra_crache50cm,
dra_marcheD, dra_marcheG, dra_Crache50cm)
EndIf
Else
If OtherGetYPos(Hero) > GetYPos
DoAnimOnce GetValueListRandom(dra_crache100cm, dra_marcheD,
dra_marcheG, dra_Crache100cm)
Else
DoAnimOnce GetValueListRandom(dra_crache50cm, dra_marcheD,
dra_marcheG, dra_Crache50cm)
EndIf
EndIf
ExitProgram
EndIf
EndIf
ExitProgram
EndWhile

UnFollow

DoAnimRepeat dra_Rien

While !TestAlive(Hero)
ExitProgram
EndWhile
ExitProgram
EndLoop
EndProgram

StartProgram SiEsquive
WaitAnim
If !OtherTestTouched(Hero)
// si on l'a rate 2 fois d'affilee : peut etre un pb d'angle
__rate = __rate+1
If __rate == 2
Gosub Defense
__rate = 0
EndIf
Else
Gosub Defense
OtherClearTouched(Hero)
WaitAnim
EndIf
EndProgram

StartProgram Defense
If _dif==0
DoAnimOnce GetValueListRandom(dra_Garde, dra_Garde, dra_Rien, dra_marche)
ElseIf _dif==1
DoAnimOnce GetValueListRandom( dra_Esqf, dra_MarcheD, dra_MarcheG)
ElseIf _dif==2
DoAnimOnce GetValueListRandom( dra_Esqf, dra_MarcheD, dra_MarcheG)
Else
DoAnimOnce GetValueListRandom( dra_Esqf, dra_MarcheD, dra_MarcheG)
EndIf
EndProgram

EndObj

So, what can we learn from this script?

From the initialization parameters, we know that the Dragon has 40 life points and four sections in his life bar (InitLifePoint 40,4), and is equipped from 8 fireballs (BouleDeFeu) as well as 199 spits (Crache) attacks, and that upon death the player will not receive any of these (InitFlagNoGiveWeapon).

Then, we enter the main program which will loop as long as Stanley is still alive.

The code stores the distance between Stanley and the Dragon into the "dho" (Distance HerO) variable and immediately after checks the value of the "jouefurie" (Play fury) variable.

If the fury variable is true we wait for the end of any choc animation (means the hero managed to hit the dragon and it's currently playing that) before unleashing the "Headbutt" (CoupBoule) animation.

Else we enter a long list of checks:
  • Have we been hit?
  • Was it twice with the same attack? Then set the fury variable to true
  • Else play an animation from a set defined by the difficulty level
  • Or maybe we were blocked? Then we try to move to the center of the room
The next sequence uses the distance to the hero to determine what to do, plus a check to see if the hero is throwing something (TestLance) or trying to hit (TestFrappe).

Based on the distance the Dragon will try to move forward, backward, use specificaly tailored attacks depend of how far to reach.

Additionally, the SiEsquive sub routine is used to check if the Dragon actually managed to hit, and if it failed twice in a row will switch to defensive mode using his guard animations.

You may have noticed the "& !GetRandom(_dif)" on the right and side of each of the distance checks? It's simply a way to add some unpredictability by using the random generator modulo the difficulty level:

In easy mode the result will always be the same and the proper distance check will always be selected, while in medium difficulty this will happen half of the time and in hard one third of a time.

Originally, we had a combat module written entirely in C, with a bunch of parameters to define the behavior, which animations to use in which context, etc... but it quickly became a bottleneck for the development because this thing would have had to be used for every single entity of the game, and having to handle things ranging from humanoids to flying sharks, raging bulls to dragons, was definitely not going to cut it, so instead we decided to extend the scripting system and to manually implement every single character5

I will let future generations to decide it this whole thing made sense or was completely bonkers :D

What's next?

I still have quite some material to share, if only because I've still not explained how the scripting system was implemented, and how the video decoder worked.

I do not have a planned schedule for that, I'm doing plenty of other stuff on the side, but don't worry, more thing will come, you just need to be patient.

I hope this was interesting :)

Adeline Software
 Part 1 Part 2 Part 3 Part 4 Part 5 Part 6 



1. The FIC entry in the HQR
2. Respectively stored in the BOC and ANC entries in the HQR
3. Now you know you can just patch the value to bypass this check
4. Except for a bit of reformatting so it would fit the blog format
5. Admittedly with a lot of copy paste'n modify.
comments powered by Disqus