Traceline Tutorial (Fakemeta)

For scripting examples for learning how to use AMXX.
User avatar
Archive-Poster
Posts: 1267
Gender:
United States of America

Traceline Tutorial (Fakemeta)

Post by Archive-Poster »

I've recently found myself using traceline a lot in my plugins. I realized that there weren't any tutorials so I decided to write one.

Theory

What is traceline, really? Traceline is a function in the Half-Life engine which is used to draw lines from one point to another in a 3D setting. It can return information, like who (or what) was hit, and various other useful bits of info.

Traceline is used in every mod in order to determine where a player is looking and shooting. Therefore, it's quite useful to learn how to manipulate traceline to your liking.

The mod you're scripting for (in my case, CS) is separate from the HL engine. Metamod (and, by extension, AMX Mod X) sits between the mod DLL and the HL engine. This gives us the ability to intercept anything the mod DLL sends to the engine, and visa versa. So, when CS asks the HL engine to calculate a traceline, we can completely change the outcome.

The Code

We will use forwards in Fakemeta in capture the communication between CS and the HL engine. This means we'll see the data CS was going to pass the engine in order to calculate a traceline.

Code: Select all

#include <amxmodx>
#include <fakemeta>

public plugin_init()
{
    register_plugin("Traceline Tutorial", "1.0", "Nomexous")
    
    register_forward(FM_TraceLine, "traceline_forward")
}

public traceline_forward(Float:start[3], Float:end[3], conditions, id, trace)
{
    // Our traceline forward.
} 
The meaning of the arguments forwarded to our function may not be entirely clear, so:

Float:start[3] The origin from which the traceline starts.

Float:end[3] The origin the traceline is drawn to.

conditions Specifies whether or not to ignore certain entities when drawing the traceline. Values can be DONT_IGNORE_MONSTERS, IGNORE_GLASS, IGNORE_MISSLE, and IGNORE_MONSTERS.

id This is the entity to ignore when drawing the traceline. Presumably, you're drawing from the origin of one entity (which is inside it) to another point. With player tracelines, id is the player index of whomever is firing the traceline.

trace Our trace result. This is the most important part. It holds the information about what was hit, among other things. Use get_tr2 and set_tr2 with this.

The Trace Result

This is the most exciting part. The variable trace holds detailed information, which can be retreived as outlined:

Code: Select all

public traceline_forward(Float:start[3], Float:end[3], conditions, id, trace)
{
    // There are more TR_* constants than are listed here; these are the
    // ones I use most.
    
    new hit = get_tr2(trace, TR_pHit)
    // What was hit by the traceline. It will either be a player index,
    // entity index, 0 (part of map), or -1 (didn't hit anything; 
    // doesn't happen with player tracelines).
    
    new hitgroup = get_tr2(trace, TR_iHitgroup)
    // If the traceline hit another player, returns will be HIT_HEAD,
    // HIT_CHEST, HIT_LEFTLEG... etc. If the traceline hit part of the
    // map, this returns HIT_GENERIC.
    
    new Float:fraction
    get_tr2(trace, TR_flFraction, fraction)
    // Returns a number between 0.0 and 1.0, indicating how far the
    // traceline traveled start to end before it hit something. Depending
    // on what conditions were passed to this traceline forward function,
    // it could either be a wall or another entity.
    
    new Float:end_origin[3]
    get_tr2(trace, TR_vecEndPos, end_origin)
    // The official end of the traceline. Not necesarily the same as the
    // second argument passed to this traceline forward function.
    
    new Float:normal[3]
    get_tr2(trace, TR_vecPlaneNormal, normal)
    // Returns a 1 unit long vector normal to the spot that it hit. Note
    // that "normal" has a special connotation here. It doesn't mean "regular."
} 
(Check this out if you don't know what we mean by "normal" in this context.)

Now, changing these value is as easy as using the set_tr2 function.

A Potentially Evil Plugin

I'm sure you've all played on a server which allowed headshots only. Good practice; body shots do no damage, headshots kill instantly, yadda yadda. But what if we could redirect a shot on any part of the body to the head? Foot shot turns into a headshot? Amazing!

Code: Select all

public traceline_forward(Float:start[3], Float:end[3], conditions, id, trace)
{
    set_tr2(trace, TR_iHitgroup, HIT_HEAD)
    return FMRES_IGNORED
}
It's quite amusing, really. Try it. If you're used to seeing blood spurt out of head on a headshot, you'll be amazed at the blood spurting out of the foot on a foot/headshot. Shotguns are extra deadly; multiple pellets are all redirected to the head at the same time. Do some if (is_user_admin(id)), and you have yourself an evil plugin.

But it gets kinda hard trying to hide the fact that the blood from a headshot came out of the foot, or stomach, or whatever part of the body you happened to hit. What you want is for blood to also be redirected to the head. Because this relies on traceline, we are still fully in control:

Code: Select all

public traceline_forward(Float:start[3], Float:end[3], conditions, id, trace)
{
    set_tr2(trace, TR_iHitgroup, HIT_HEAD) // Redirect shot to head
    
    // Variable angles doesn't really have a use here.
    static hit, Float:head_origin[3], Float:angles[3]
    
    hit = get_tr2(trace, TR_pHit) // Whomever was shot
    engfunc(EngFunc_GetBonePosition, hit, 8, head_origin, angles) // Find origin of head bone (8)
    
    set_tr2(trace, TR_vecEndPos, head_origin) // Blood now comes out of the head!
    
    return FMRES_IGNORED
} 
This is even more evil. Now you can run around your own server, endlessly headshotting, no unexplainable blood. Though now you might need to explain why you can't do anything other than headshot.

You can of course change whom you are shooting. Consider this anti-team attack plugin for cs:

Code: Select all

#include <amxmodx>
#include <fakemeta>
#include <cstrike>

public plugin_init()
{
    register_plugin("Traceline Tutorial - No TA", "1.0", "Nomexous")
    
    register_forward(FM_TraceLine, "traceline_forward")
}

public traceline_forward(Float:start[3], Float:end[3], conditions, id, trace)
{
    static hit
    
    hit = get_tr2(trace, TR_pHit)
    
    // Variable id is shooter's index.
    if (cs_get_user_team(id) == cs_get_user_team(hit))
    {
        // Redirect onto himself!
        set_tr2(trace, TR_pHit, id)
    }
    
    return FMRES_IGNORED
} 
Blood will still spray out of whomever is being shot, but the damage will be dealt to the shooter. People quickly lose the urge to team attack.

I've submitted a plugin (Shot Administration) that demonstrates the evil capabilities of manipulating traceline. Take a look; tracehull is also hooked (for knife damage).

Less Evil Uses of Traceline

We will now use tracelines to do our bidding outside the context of player aiming. Using engfunc(), we can call draw our own tracelines to see what we hit. Of course, we're not actually shooting and dealing damage, but with the information that the trace result gives us, it can be just as useful.

Lets find out if two origins are in line of sight:

Code: Select all

stock bool:is_in_line_of_sight(Float:origin1[3], Float:origin[2], bool:ignore_players = true)
{
    new trace = 0
    engfunc(EngFunc_TraceLine, origin1, origin2, (ignore_players ? IGNORE_MONSTERS : DONT_IGNORE_MONSTERS), 0, trace)
    
    new Float:fraction
    get_tr2(trace, TR_flFraction, fraction)
    
    return (fraction == 1.0) ? true : false
} 
This stock sees how far it went before it hit something. If TR_flFraction is 1.0, that means it went 100% of the way without hitting an obstacle. Therefore, the two origins are in line of sight. Alternatively, you can see what TR_pHit returns. If it returns -1, you hit nothing (meaning the two points are in line of sight). A return of 0 means you hit part of the map (you can assume it's a wall), and anything else would be the index of an entity in the way. Be careful; it might not be a player. It could possibly be a door or a func_wall.

Now, let's make use of the TR_vecPlaneNormal part of our trace result. There are multiple things this is useful for. Here, I'll outline how to draw a laser normal to a surface.

When a trace hits something, TR_vecPlaneNormal will have a unit vector (1 unit in length) stored to it, pointing perpendicularly away from surface at the spot it hit. To get the endpoint of the laser, we merely need to extend the length of the normal vector and add it to the origin of where we want the laser to start from. Here is a sample:

Code: Select all

#include <amxmodx>
#include <fakemeta>

#define PLUGIN "Draw Normal Laser Example"
#define VERSION "1.0"
#define AUTHOR "Nomexous"

new beampoint

public plugin_precache()
{
    // Needed to show the laser.
    beampoint = precache_model("sprites/laserbeam.spr")
}

public plugin_init()
{
    register_plugin(PLUGIN, VERSION, AUTHOR)
    
    // I included the entire plugin because in order to draw the laser, you need to precache a sprite. Incorporate
    // these elements into your own plugin.
    
    // The shoot_laser() will (if the entity is on the floor) fire a laser from the entity, normal to the surface it's resting on.
}

public shoot_laser(ent)
{
    // We get the origin of the entity.
    new Float:origin[3]
    pev(ent, pev_origin, origin)
    
    // We want to trace down to the floor, if it's there.
    new Float:traceto[3]
    traceto[0] = origin[0]
    traceto[1] = origin[1]
    traceto[2] = origin[2] - 10.0
    
    new trace = 0
    // Draw the traceline. We're assuming the object is resting on the floor.
    engfunc(EngFunc_TraceLine, origin, traceto, IGNORE_MONSTERS, ent, trace)
    
    new Float:fraction
    get_tr2(trace, TR_flFraction, fraction)
    // If we didn't hit anything, then we won't get a valid TR_vecPlaneNormal.
    if (fraction == 1.0) return
    
    new Float:normal[3]
    get_tr2(trace, TR_vecPlaneNormal, normal)
    // We'll multiply the the normal vector by a scalar to make it longer.
    normal[0] *= 400.0 // Mathematically, we multiplied the length of the vector by 400*(3)^(1/2),
    normal[1] *= 400.0 // or, in words, four hundred times root three.
    normal[2] *= 400.0
    
    // To get the endpoint, we add the normal vector and the origin.
    new Float:endpoint[3]
    endpoint[0] = origin[0] + normal[0]
    endpoint[1] = origin[1] + normal[1]
    endpoint[2] = origin[2] + normal[2]
    
    // Finally, we draw from the laser!
    draw_laser(origin, endpoint, 100) // Make it stay for 10 seconds. Not a typo; staytime is in 10ths of a second.
}

public draw_laser(Float:start[3], Float:end[3], staytime)
{                    
    message_begin(MSG_ALL, SVC_TEMPENTITY)
    write_byte(TE_BEAMPOINTS)
    engfunc(EngFunc_WriteCoord, start[0])
    engfunc(EngFunc_WriteCoord, start[1])
    engfunc(EngFunc_WriteCoord, start[2])
    engfunc(EngFunc_WriteCoord, end[0])
    engfunc(EngFunc_WriteCoord, end[1])
    engfunc(EngFunc_WriteCoord, end[2])
    write_short(beampoint)
    write_byte(0)
    write_byte(0)
    write_byte(staytime) // In tenths of a second.
    write_byte(10)
    write_byte(1)
    write_byte(255) // Red
    write_byte(0) // Green
    write_byte(0) // Blue
    write_byte(127)
    write_byte(1)
    message_end()
} 
Well, that's all I have for now. Hopefully, you've learned more about traceline and can take advantage of its features!
User avatar
Archive-Poster
Posts: 1267
Gender:
United States of America

Re: Traceline Tutorial (Fakemeta)

Post by Archive-Poster »

Good tutorial. However, it should be noted in your line of sight example, that the two ents do NOT have to be facing each other for the stock to return true, they could in fact be facing the opposite way. Use, is_in_viewcone() for this functionaility. Other than that though, good job
The first two arguments of the stock are float origins, not entity indexes. So it's looking for line of sight between two points in space, not between two players. Of course, you're correct; if the two origins you provide happen to be the origins of two players, the stock will return the same value regardless of whether or not they are facing each other. But you won't always be using the stock on two players. I, for example, used it to find line of sight between a player and a flashbang.
To alter a trace result, should not FM_TraceLine be registered in 'post' ? ( I has tested and you need to 'post-register' if you want your new value is taken )
In all the times I've ever used traceline, I've never registered the forward as post. It works both ways, unlike tracehull, which needs to be registered as post. I've puzzled over it, but I can't come up with a good answer.
User avatar
Archive-Poster
Posts: 1267
Gender:
United States of America

Re: Traceline Tutorial (Fakemeta)

Post by Archive-Poster »

In all the times I've ever used traceline, I've never registered the forward as post. It works both ways, unlike tracehull, which needs to be registered as post. I've puzzled over it, but I can't come up with a good answer.
I'm talking about ( I have not expressed correctly ) only when you want to stop the trace result. ( e.g: allow damage only on head );

You can do that with : set_tr2( trace, TR_flFraction, 1.0 ); , but you need to supercede and it will work only if you have registered the forward as post.
User avatar
Archive-Poster
Posts: 1267
Gender:
United States of America

Re: Traceline Tutorial (Fakemeta)

Post by Archive-Poster »

To alter a trace result, should not FM_TraceLine be registered in 'post' ? ( I has tested and you need to 'post-register' if you want your new value is taken )
To be honest I think there is possibly an underlying problem with MetaMod regarding this since Post functionality doesn't really exist. It's essentially just executes before normal TraceLine function exits (I assume) but retains original values captured.

Back in the days of AMX Mod to correct a problem with TraceLine in Fun module another call to TraceLine had to be called, it might of been a API issue what Valve got round to fixing but I very much doubt it.

It's kind of the same story with removing hostages etc some days it works, other days it'll just crash HLDS with the same code which is very bizarre.
User avatar
Archive-Poster
Posts: 1267
Gender:
United States of America

Re: Traceline Tutorial (Fakemeta)

Post by Archive-Poster »

tell me what is wrong here:

Code: Select all

public FORWARD_TRACELINE(Float:start[3], Float:end[3], conditions, id,trace)
{
    if(!is_user_alive(id)) 
        return FMRES_IGNORED;
        
    new ent = get_tr2(trace,TR_pHit);
    
    if(!pev_valid(ent)) 
        return FMRES_IGNORED;
    
    static classname[31];
    pev(ent,pev_classname,classname,30);
        
    if(!equal(classname,gLandMinesClassname))
        return FMRES_IGNORED;
    
        
    static Float:hitHealth;
    pev(ent,pev_health,hitHealth);
        
    if(hitHealth <= 1.0)
    {
        client_print(id,print_chat,"DESTROYED !! WORKS");
        client_print(id,print_chat,"DESTROYED !! WORKS");
    }

    return FMRES_IGNORED;
I have my own ent, with own life. I shoot on it and i can destroy it. But if you see, I added a message when I hook TraceLine; That message don't appear.
Have any idea why ?

What i'm trying to do ?
When i shoot on a ent, and damage > ent's hp, it's enough to appear a simple message.
User avatar
Archive-Poster
Posts: 1267
Gender:
United States of America

Re: Traceline Tutorial (Fakemeta)

Post by Archive-Poster »

Unfortunately, you're limited by how traceline works. Traceline will not hit non-visible objects. If the solid flag is SOLID_NOT or SOLID_TRIGGER, traceline will pass right through it. For example, a func_breakable changes from SOLID_BBOX to SOLID_NOT when it's destroyed. I'm guessing that since your landmine is destroyed when shot, this is the case.

@Everyone else

I have another use of traceline coming up: making weapons lie flat on sloped surfaces!
User avatar
Archive-Poster
Posts: 1267
Gender:
United States of America

Re: Traceline Tutorial (Fakemeta)

Post by Archive-Poster »

Isn't destroyed at first hit. If i set health <= 10.0 and if i shoot with glock for example sometimes appear my message ("DESTROYED !! WORKS !!")
I would imagine that a delay could be added for when the hp drops below a number, before a loss of hp could be triggered to fully get rid of the object.
Post Reply

Who is online

Users browsing this forum: No registered users and 1 guest