sailro / EscapeFromTarkov-Trainer

Escape from Tarkov (EFT) Trainer - Internal
MIT License
605 stars 138 forks source link

Add and integrate bone scanning for visibility checking and Implement the visibility check for non-silent Aimbot #75

Open Billybishop opened 3 years ago

Billybishop commented 3 years ago

Updated this enhancement in respect to latest code changes and documented the current goals in comment #75.

Goals reference, as described in my update comment:

  1. Improve the visibility checking procedure by changing the way in which we get the player's target bone; This proposed improvement should add & implement a new method, or replace the existing method #TryGetHeadTransform, and name it #TryGetVisibleTransform. https://github.com/sailro/EscapeFromTarkov-Trainer/blob/2d8d89194ccab74fe0a6e0561fd36c90699cb92f/Features/Aimbot.cs#L258
  2. With the above proposed changes added, the Non-Silent Aimbot will then naturally be performing visibility checking through the new bone-scanning method and then the visibility check for the Silent Aimbot can be removed. https://github.com/sailro/EscapeFromTarkov-Trainer/blob/2d8d89194ccab74fe0a6e0561fd36c90699cb92f/Features/Aimbot.cs#L100 In both mentioned cases this is necessary thanks to #TryGetNearestTarget now using the new bone scan method.

Example snippet of bone scanning for best visible transform, implemented and tested in an earlier branch:

PlayerBones bones = player.PlayerBones;

//Make a collection of target bones ordered by vitality (Head, Neck, Body, Pelvis, Upper Arms, Upper Legs)
IEnumerable<AimBone> target_bones = new AimBone[8] {
  new AimBone(bones.Head),
  new AimBone(bones.Neck),
  new AimBone(bones.Spine1),
  new AimBone(bones.Pelvis),
  new AimBone(bones.Upperarms[0]),
  new AimBone(bones.Upperarms[1]),
  new AimBone(bones.LeftThigh1),
  new AimBone(bones.RightThigh1)
}; //Used a hybrid AimBone class due to differing bone transform types with no mutual interface/inheritance

//LINQ is short and sweet but slows down execution due to the added overhead
//return target_bones.FirstOrDefault(bone => _camera.IsTransformVisible(bone.Position));

//Use foreach here instead since it is faster than LINQ and Aimbot needs to be optimized for speed
foreach (AimBone bone in target_bones)
  if (_camera.IsTransformVisible(bone.Position))
      return bone.Position;

Old description:

The Aimbot should only acquire targets that are visible from your camera and not behind geometry. We already have a way to verify if a world position is within your viewport so now we need a way to verify the player is physically visible. The visibility check for a player object should ideally consider the best bone position but in the event that only part of the player is visible, such as a limb, then the visibility method should be capable of returning the best bone out of what is visible...

~~If only a right arm and right leg are visible of a given player, the visibility check will return the best bone out of those visibly available which would be in this order as a general example : right upper arm > right thigh > right forearm. So, in conclusion the visibility check would return the upper arm first in this visibility case.~~

The visibility check should do a physics raycast check from the player's camera position (in the main thread) to the provided bone position to determine if there is any blocking geometry. If the raycast fails, then there is no geometry in the way. The distance the ray cast travels should be limited by a small degree so as to not collide with the geometry of armor or items that the potential target player is wearing (such as armor, weapon, backpack, vests, etc) since understandably this geometry will always be present.

This design should suffice in 99% of cases to check for visibility, however the best 1:1 approach would be not to limit the ray cast's travel distance for checking geometry collision, and instead include player equipped items/armor as part of the potentially visible player component.

I started working on this enhancement and still need to test/debug - I will be sure to post in this thread with any updates or successes.

Billybishop commented 3 years ago

Hi all, great news! It's done. (I was drunk and patching things together from other projects, and trying stuff on my own. Please forgive me if this is not well credited)

1. I finished implementing and testing a fast visibility check that is very accurate - I added the following to the CameraExtensions (Note the importance of the layer mask here):

private static RaycastHit _ray;
private static readonly LayerMask _layer_mask = 1 << 12 | 1 << 16 | 1 << 18 | 1 << 31 | 1 << 22;
public static bool IsPlayerBoneVisible(this Camera camera, Player player, Vector3 target_bone)
{
  return (Physics.Linecast(camera.transform.position, target_bone, out _ray, _layer_mask) && _ray.collider && _ray.collider.gameObject.transform.root.gameObject == player.gameObject.transform.root.gameObject);
}

2. And then I wrote a nearly perfect method for scanning and returning the best bone to target out of only the parts of the Player that is currently visible:

public static Vector3 GetBestVisibleBone(Player player, float cached_distance = 0.0f)
{
  Vector3 null_position = Vector3.zero;

  if (Aimbot._camera == null)
      return null_position;

  PlayerBones bones = player.PlayerBones;
  if (bones == null)
      return null_position;

  float null_distance = 3.0f;
  float min_distance = 7.5f;
  float distance = (cached_distance != 0.0f) ? cached_distance : Vector3.Distance(Aimbot._camera.transform.position, player.Transform.position);
  //Don't check for visibility if player is extremely close to the camera because visibility has weird behavior at this distance
  if (distance <= null_distance)
  {
      render_text_player_visible = $"TARGET TOO CLOSE";
      return player.PlayerBones.Spine1.position; 
  }

  if (distance <= min_distance && Aimbot._camera.IsPlayerBoneVisible(player, player.PlayerBones.Spine1.position)) //Stick to one bone if we're very close to the player
  {
      //Visibility debug
      render_text_player_visible = $"TARGET: [{player.name}->{player.PlayerBones.Spine1.name}]";
      return player.PlayerBones.Spine1.position;
  }
  else if (distance > min_distance)
  {
      //Make a collection of target bones starting with the most important followed by the other bones in order of vitality
      IEnumerable<AimBone> target_bones = new AimBone[16] {
          new AimBone(bones.Neck),
          new AimBone(bones.Head),
          new AimBone(bones.Shoulders[0]),
          new AimBone(bones.Shoulders[1]),
          new AimBone(bones.Spine1),
          new AimBone(bones.Upperarms[0]),
          new AimBone(bones.Upperarms[1]),
          new AimBone(bones.Forearms[0]),
          new AimBone(bones.Forearms[1]),
          new AimBone(bones.Pelvis),
          new AimBone(bones.LeftPalm),
          new AimBone(bones.LeftThigh1),
          new AimBone(bones.RightThigh1),
          new AimBone(bones.LeftThigh2),
          new AimBone(bones.RightThigh2),
          new AimBone(bones.KickingFoot)
      }; //Hybrid AimBone class is necessary due to differing bone transform types that do not share interfaces/inheritance

      //Scan the desired bones for visibility
      foreach (AimBone bone_info in target_bones)
      {
          bool is_visible = Aimbot._camera.IsPlayerBoneVisible(player, bone_info.Position);
          if (is_visible)
          {
              //Visibility debug
              render_text_player_visible = $"TARGET: [{player.name}->{bone_info.Name}]";
              return bone_info.Position;
          }
      }
  }

  return null_position;
}

3. The above method uses a translation class I wrote that can accept the different Transform types (This is necessary because Transform and BifacialTransform do not share an interface or inheritance). So, this just simplifies the work needed to use the different types which represent the bones. Please note the comment about C# dynamic type, as it was tried and found not to be supported with the current EFT/AKI binaries - even with CSharp and ReflectionType libraries provided - which is why this simple class is so convoluted.

//NOTE: This used to use dynamic type originally but that requires Microsoft.CSharp.dll v4 and related dependencies which are not currently supported in EFT/AKI
public class AimBone
{
  private readonly string _name;
  public string Name { get { return _name; } }
  private readonly object _bone;
  public object Bone { get { return _bone; } }
  private readonly Vector3 _bone_position;
  public Vector3 Position { get { return _bone_position; } }

  public AimBone(object aim_bone)
  {
      if (aim_bone is Transform)
      {
          _name = ((Transform)aim_bone).name;
          _bone_position = ((Transform)aim_bone).position;
      }
      else if (aim_bone is BifacialTransform)
      {
          _name = ((BifacialTransform)aim_bone).Original.name;
          _bone_position = ((BifacialTransform)aim_bone).position;
      }
      else
      {
          _name = "Unsupported Bone Type";
          _bone_position = Vector3.zero;
      }

      _bone = aim_bone;
  }
}

@sailro, I would very much appreciate if this could be implemented in the master branch. Also, if anyone would like please feel free to optimize these methods if you find a faster/simpler way to do something, I gave it my best effort. Lastly, you may notice how I strongly type all of my variables, I prefer it this way for readability but it has no runtime impact either way AFAIK.

sailro commented 3 years ago

Could you show how you integrate this with the current code?

Billybishop commented 3 years ago

Could you show how you integrate this with the current code?

Sure thing! Here is how my Update method looks like for the Aimbot class, which acquires the Vec3 target position '_targetpos' through usage of #GetBestVisibleBone. I'm still in the process of adding a FOV calculation so this will surely change, but I will document that as another enhancement soon:

protected override void UpdateWhenHold()
{
    GameStateSnapshot? state = GameState.Current;
    if (state == null)
        return;

    Player? localPlayer = state.LocalPlayer;
    if (localPlayer == null)
        return;

    Aimbot._camera = state.Camera;
    if (Aimbot._camera == null)
        return;

    if (localPlayer.Weapon.IsMeleeWeapon())
        return;

    Vector3 nearestTarget = Vector3.zero;
    float nearestTargetDistance = float.MaxValue;

    //GameState ensures 'Hostiles' list will not include your own Player object, so no need to check for it here
    foreach (var player in state.Hostiles)
    {
        if (player == null)
            continue;

        float distance = Vector3.Distance(Aimbot._camera.transform.position, player.Transform.position);
        if (distance >= nearestTargetDistance || distance > this.MaximumDistance)
            continue;

        Vector3 target_pos = GetBestVisibleBone(player, distance); //Find the best visible bone for this potential target, also behaves as a full visibility check
        if (target_pos == Vector3.zero) //Empty Vec3 means no bone was visible, therefore this target is completely behind geometry
        {
            render_text_player_visible = (render_text_player_visible != "TARGET TOO CLSOE") ? "NO VISIBLE TARGET" : render_text_player_visible;
            continue;
        }

        //I'm 82% sure this isn't necessary but do it anyway just incase Target magically teleports off screen
        Vector3 screenPosition = Aimbot._camera.WorldPointToScreenPoint(target_pos);
        if (!Aimbot._camera.IsScreenPointVisible(screenPosition))
            continue;

        //Used to get the speed of the Ammo we're currently using - still need to account for ballistic arc and deceleration
        AmmoTemplate? template = localPlayer.Weapon?.CurrentAmmoTemplate;
        if (template == null)
            continue;

        //This needs some work - it is under-estimating the destination when target is farther away and slowing down or over-estimating when target is farther away but stationary or moving quickly - we should check when target becomes stationary and reset the target position
        nearestTargetDistance = distance;
        float travelTime = (distance / template.InitialSpeed);
        target_pos.x += (player.Velocity.x * travelTime);
        target_pos.y += (player.Velocity.y * travelTime);

        nearestTarget = target_pos;
    }

    if (nearestTarget != Vector3.zero)
        AimAtPosition(localPlayer, nearestTarget, this.SmoothPercent);
}

Also, on another note, I like your improvement to the #NormalizeAngle that was included in the Aim smoothing enhancement! Looks good 👍

I'll try to get a fork up soon and keep it up to date w/ your Master branch since I plan on supporting only latest version EFT/AKI - making proper pull requests where applicable - and then that fork can act as an experimental features test bed if we'd like.

Billybishop commented 3 years ago

I made some significant improvements to the visibility check that I will get up on Github here in the next few days. Basically I added another visibility check method that does as I originally planned - casts a ray with a magnitude of 90% - instead of line casting. I kept the original visibility check because it can be useful in other circumstances. Anyway, please disregard the above code and maybe I'll do an official pull request this time ;)

Billybishop commented 3 years ago

Since it's been 8 days I think that's longer than ""in the next few days"" lol, so here is my complete source on visibility checks. I reconstructed IsPlayerBoneVisibile3 from looking at EFT source in dnSpy, it is too accurate sometimes and I think the layer masks need adjusted, otherwise it could be better than IsPlayerBoneVisible2...


//This is faster and somehow more accurate
public static bool IsPlayerBoneVisible2(this Camera camera, Vector3 target_bone)
{
    GameStateSnapshot? current = GameState.Current;
    if (current == null) return false;

    Player? local_player = current.LocalPlayer;
    if (local_player == null) return false;

    //Vector3 camera_position = camera.transform.position;
    Vector3 fireport = local_player.Fireport.position;
    fireport.y = (fireport.y + Aimbot._sight_adjustment);
    Vector3 direction = (target_bone - fireport);
    float distance = (Vector3.Magnitude(direction) * 0.975f);

    return !(Physics.Raycast(fireport, direction, distance, GClass503.HighPolyWithTerrainMask.value, QueryTriggerInteraction.UseGlobal));
}

//This is experimental
public static bool IsPlayerBoneVisible3(this Camera camera, Vector3 target_bone)
{
    return !GClass420.LinecastPrecise(camera.transform.position, target_bone, out _ray, GClass1974.HitMask, true, _rays, new Func<RaycastHit, bool>(CameraExtensions.IsIgnored));
}
sailro commented 3 years ago

GClass420 will break on the next update. EFT is using a simple obfuscator changing non public names for every release.

So the best way is to avoid depending on those generated names (perhaps next time it will be GClass421 instead of GClass420), you should find an alternative way to get access, perhaps using Reflection.

Like I did here: https://github.com/sailro/EscapeFromTarkov-Trainer/blob/master/Features/Commands.cs#L138

Billybishop commented 3 years ago

GClass420 will break on the next update. EFT is using a simple obfuscator changing non public names for every release.

So the best way is to avoid depending on those generated names (perhaps next time it will be GClass421 instead of GClass420), you should find an alternative way to get access, perhaps using Reflection.

Like I did here: https://github.com/sailro/EscapeFromTarkov-Trainer/blob/master/Features/Commands.cs#L138

Ah thanks for the tip, I'll look into this.

sailro commented 2 years ago

Silent-aim is now using visibility check

Billybishop commented 2 years ago

I updated this issue's title with respect to your latest additions.

Now the two goals of this enhancement are as follows:

  1. Improve the visibility checking procedure by changing the way in which we get the player's target bone; This proposed improvement should add & implement a new method, or replace the existing method #TryGetHeadTransform, and name it #TryGetVisibleTransform. https://github.com/sailro/EscapeFromTarkov-Trainer/blob/2d8d89194ccab74fe0a6e0561fd36c90699cb92f/Features/Aimbot.cs#L258
  2. With the above proposed changes added, the Non-Silent Aimbot will then naturally be performing visibility checking through the new bone-scanning method and then the visibility check for the Silent Aimbot can be removed. https://github.com/sailro/EscapeFromTarkov-Trainer/blob/2d8d89194ccab74fe0a6e0561fd36c90699cb92f/Features/Aimbot.cs#L100 In both mentioned cases this is necessary thanks to #TryGetNearestTarget now using the new bone scan method.

Example snippet of bone-scanning for visible transform, implemented and tested in earlier versions:

PlayerBones bones = player.PlayerBones;

//Make a collection of target bones ordered by vitality (Head, Neck, Body, Pelvis, Upper Arms, Upper Legs)
IEnumerable<AimBone> target_bones = new AimBone[8] {
    new AimBone(bones.Head),
    new AimBone(bones.Neck),
    new AimBone(bones.Spine1),
    new AimBone(bones.Pelvis),
    new AimBone(bones.Upperarms[0]),
    new AimBone(bones.Upperarms[1]),
    new AimBone(bones.LeftThigh1),
    new AimBone(bones.RightThigh1)
}; //Used a hybrid AimBone class due to differing bone transform types with no mutual interface/inheritance

//LINQ is short and sweet but slows down execution due to the added overhead
//return target_bones.FirstOrDefault(bone => _camera.IsTransformVisible(bone.Position));

//Use foreach here instead since it is faster than LINQ and Aimbot needs to be optimized for speed
foreach (AimBone bone in target_bones)
    if (_camera.IsTransformVisible(bone.Position))
        return bone.Position;