Merge pull request #1020 from nullworks/nav_improve

Improve navigation
This commit is contained in:
TotallyNotElite 2020-05-26 13:58:51 +02:00 committed by GitHub
commit 80a1a6e5c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 308 additions and 33 deletions

View File

@ -54,6 +54,10 @@ public:
// dispenser
offset_t m_iAmmoMetal; // dispenser metal reserve
// round timer
offset_t m_nSetupTimeLength;
offset_t m_nState;
offset_t iLifeState;
offset_t iCond;
offset_t iCond1;

View File

@ -113,6 +113,8 @@ void fClampAngle(Vector &qaAng);
// const char* MakeInfoString(IClientEntity* player);
bool GetProjectileData(CachedEntity *weapon, float &speed, float &gravity);
bool IsVectorVisible(Vector a, Vector b, bool enviroment_only = false, CachedEntity *self = LOCAL_E, unsigned int mask = MASK_SHOT_HULL);
// A Special function for navparser to check if a Vector is visible.
bool IsVectorVisibleNavigation(Vector a, Vector b, unsigned int mask = MASK_SHOT_HULL);
Vector GetForwardVector(Vector origin, Vector viewangles, float distance);
Vector GetForwardVector(float distance);
CachedEntity *getClosestEntity(Vector vec);

View File

@ -22,6 +22,11 @@ public:
typedef bool (*fn_t)(IClientEntity *);
return vfunc<fn_t>(self, offsets::PlatformOffset(184, offsets::undefined, 184), 0)(self);
}
inline static bool ShouldCollide(IClientEntity *self, int collisionGroup, int contentsMask)
{
typedef bool (*fn_t)(IClientEntity *, int, int);
return vfunc<fn_t>(self, offsets::PlatformOffset(198, offsets::undefined, 198), 0)(self, collisionGroup, contentsMask);
}
inline static int &m_nPredictionRandomSeed()
{
static int placeholder = 0;

View File

@ -0,0 +1,17 @@
#pragma once
enum round_states
{
RT_STATE_SETUP = 0,
RT_STATE_NORMAL
};
class CTeamRoundTimer
{
public:
int GetSetupTimeLength();
round_states GetRoundState();
void Update();
int entity;
};
extern CTeamRoundTimer *g_pTeamRoundTimer;

View File

@ -41,6 +41,15 @@ public:
void SetSelf(IClientEntity *self);
virtual TraceType_t GetTraceType() const;
};
class FilterNavigation : public ITraceFilter
{
public:
virtual ~FilterNavigation();
FilterNavigation();
virtual bool ShouldHitEntity(IHandleEntity *entity, int mask);
virtual TraceType_t GetTraceType() const;
};
class FilterNoEntity : public ITraceFilter
{
@ -72,6 +81,7 @@ public:
extern FilterDefault filter_default;
extern FilterNoPlayer filter_no_player;
extern FilterNavigation filter_navigation;
extern FilterNoEntity filter_no_entity;
extern FilterPenetration filter_penetration;
} // namespace trace

View File

@ -23,6 +23,7 @@ set(files "${CMAKE_CURRENT_LIST_DIR}/angles.cpp"
"${CMAKE_CURRENT_LIST_DIR}/sconvars.cpp"
"${CMAKE_CURRENT_LIST_DIR}/soundcache.cpp"
"${CMAKE_CURRENT_LIST_DIR}/targethelper.cpp"
"${CMAKE_CURRENT_LIST_DIR}/teamroundtimer.cpp"
"${CMAKE_CURRENT_LIST_DIR}/textmode.cpp"
"${CMAKE_CURRENT_LIST_DIR}/tfmm.cpp"
"${CMAKE_CURRENT_LIST_DIR}/trace.cpp"

View File

@ -88,6 +88,10 @@ void NetVars::Init()
// dispenser
this->m_iAmmoMetal = gNetvars.get_offset("DT_ObjectDispenser", "m_iAmmoMetal");
// Round timer
this->m_nSetupTimeLength = gNetvars.get_offset("DT_TeamRoundTimer", "m_nSetupTimeLength");
this->m_nState = gNetvars.get_offset("DT_TeamRoundTimer", "m_nState");
// any building
this->m_iUpgradeMetal = gNetvars.get_offset("DT_BaseObject", "m_iUpgradeMetal");
this->m_flPercentageConstructed = gNetvars.get_offset("DT_BaseObject", "m_flPercentageConstructed");

View File

@ -21,6 +21,7 @@
#include <pwd.h>
#include <hacks/hacklist.hpp>
#include "teamroundtimer.hpp"
#if EXTERNAL_DRAWING
#include "xoverlay.h"
#endif
@ -336,6 +337,7 @@ free(logname);*/
InitNetVars();
g_pLocalPlayer = new LocalPlayer();
g_pPlayerResource = new TFPlayerResource();
g_pTeamRoundTimer = new CTeamRoundTimer();
velocity::Init();
playerlist::Load();
@ -394,6 +396,10 @@ void hack::Shutdown()
ConVar_Unregister();
logging::Info("Unloading sharedobjects..");
sharedobj::UnloadAllSharedObjects();
logging::Info("Deleting global interfaces...");
delete g_pLocalPlayer;
delete g_pTeamRoundTimer;
delete g_pPlayerResource;
if (!hack::game_shutdown)
{
logging::Info("Running shutdown handlers");

View File

@ -68,6 +68,16 @@ static void updateAntiAfk()
}
else
{
Vector vel(0.0f);
if (CE_GOOD(LOCAL_E) && LOCAL_E->m_bAlivePlayer())
velocity::EstimateAbsVelocity(RAW_ENT(LOCAL_E), vel);
// We are moving, make the timer a bit longer (only a bit to avoid issues with random movement)
if (!vel.IsZero(1.0f))
{
anti_afk_timer.last += std::chrono::milliseconds(400);
if (anti_afk_timer.last > std::chrono::system_clock::now())
anti_afk_timer.update();
}
static auto afk_timer = g_ICvar->FindVar("mp_idlemaxtime");
if (!afk_timer)
afk_timer = g_ICvar->FindVar("mp_idlemaxtime");
@ -82,7 +92,7 @@ static void updateAntiAfk()
// Game also checks if you move if you are in spawn, so spam movement keys alternatingly
bool flip = false;
current_late_user_cmd->buttons |= flip ? IN_FORWARD : IN_BACK;
current_user_cmd->buttons |= flip ? IN_FORWARD : IN_BACK;
// Flip flip
flip = !flip;
if (anti_afk_timer.check(afk_timer->GetInt() * 60 * 1000 + 1000))

View File

@ -7,6 +7,7 @@
#include "soundcache.hpp"
#include "Misc.hpp"
#include "MiscTemporary.hpp"
#include "teamroundtimer.hpp"
namespace hacks::tf2::NavBot
{
@ -101,7 +102,7 @@ constexpr bot_class_config DIST_SNIPER{ 1000.0f, 1500.0f, 3000.0f };
constexpr bot_class_config DIST_ENGINEER{ 600.0f, 1000.0f, 2500.0f };
// Gunslinger Engineers really don't care at all
constexpr bot_class_config DIST_GUNSLINGER_ENGINEER{ 100.0f, 300.0f, 500.0f };
constexpr bot_class_config DIST_GUNSLINGER_ENGINEER{ 50.0f, 200.0f, 900.0f };
inline bool HasGunslinger(CachedEntity *ent)
{
@ -120,6 +121,19 @@ static void CreateMove()
wait_until_path.update();
else
current_task = task::none;
// Check if we should path at all
if (!blocking || task::current_task == task::engineer)
{
round_states round_state = g_pTeamRoundTimer->GetRoundState();
// Still in setuptime, if on fitting team, then do not path yet
if (round_state == RT_STATE_SETUP && g_pLocalPlayer->team == TEAM_BLU)
{
// Clear instructions
if (!nav::ReadyForCommands)
nav::clearInstructions();
return;
}
}
if (autojump)
autoJump();
@ -467,7 +481,7 @@ static success_build buildBuilding()
build_attempts++;
// Put building in hand if not already
if (hacks::shared::misc::getCarriedBuilding() == -1 && build_command_timer.test_and_set(50))
g_IEngine->ClientCmd_Unrestricted(format("build ", building).c_str());
g_IEngine->ClientCmd_Unrestricted(("build " + std::to_string(building)).c_str());
}
else if (rotation_timer.check(200))
{
@ -541,9 +555,47 @@ static bool engineerLogic()
return true;
}
// Let's terrify some people (gunslinger engineer)
// Gunslinger engineer should run at people, given their building isn't too far away
if (HasGunslinger(LOCAL_E))
{
// Deconstruct too far away buildings
for (auto &building : local_buildings)
{
// Too far away, destroy it
if (building->m_vecOrigin().DistTo(LOCAL_E->m_vecOrigin()) >= 1800.0f)
{
Building building_type = None;
switch (building->m_iClassID())
{
case CL_CLASS(CObjectDispenser):
{
building_type = Dispenser;
break;
}
case CL_CLASS(CObjectTeleporter):
{
// We cannot reliably detect entrance and exit, so just destruct both but mark as "Entrance"
building_type = TP_Entrace;
break;
}
case CL_CLASS(CObjectSentrygun):
{
building_type = Sentry;
break;
}
}
// If we have a valid building
if (building_type != None)
{
// Destroy exit too because we have no idea what is what
if (building_type == TP_Entrace)
g_IEngine->ClientCmd_Unrestricted("destroy 3");
g_IEngine->ClientCmd_Unrestricted(("destroy " + std::to_string(building_type)).c_str());
}
}
}
stayNearEngineer();
}
else if (selectBuilding() != None)
{
@ -643,10 +695,7 @@ static bool engineerLogic()
else if (engineer_recheck.test_and_set(15000))
{
if (navToBuildingSpot())
{
engineer_recheck.update();
return true;
}
}
break;
}
@ -713,6 +762,9 @@ static bool isValidNearPosition(Vector vec, Vector target, const bot_class_confi
return false;
if (!IsVectorVisible(vec, target, true, LOCAL_E, MASK_PLAYERSOLID))
return false;
// Check if safe
if (!nav::isSafe(nav::findClosestNavSquare(target)))
return false;
return true;
}
@ -904,10 +956,8 @@ static bool stayNearEngineer()
}
static Timer wait_until_stay_near{};
if (current_task == task::engineer_task::staynear_engineer)
{
if (current_engineer_task == task::engineer_task::staynear_engineer)
return true;
}
else if (wait_until_stay_near.test_and_set(4000))
{
// We're doing nothing? Do something!
@ -1189,6 +1239,7 @@ static bool getHealthAndAmmo(int metal)
if (nav::navTo(pack, health < 0.64f || lowAmmo ? 10 : 3, true, false))
{
current_task = { task::health, health < 0.64f ? 10 : 3 };
return true;
}
}
}

View File

@ -1338,7 +1338,6 @@ netvar.iHealth));
bool IsVectorVisible(Vector origin, Vector target, bool enviroment_only, CachedEntity *self, unsigned int mask)
{
if (!enviroment_only)
{
trace_t trace_visible;
@ -1363,6 +1362,17 @@ bool IsVectorVisible(Vector origin, Vector target, bool enviroment_only, CachedE
}
}
bool IsVectorVisibleNavigation(Vector origin, Vector target, unsigned int mask)
{
trace_t trace_visible;
Ray_t ray;
ray.Init(origin, target);
PROF_SECTION(IEVV_TraceRay);
g_ITrace->TraceRay(ray, mask, &trace::filter_navigation, &trace_visible);
return (trace_visible.fraction == 1.0f);
}
void WhatIAmLookingAt(int *result_eindex, Vector *result_pos)
{
Ray_t ray;

View File

@ -15,6 +15,7 @@
#include <hacks/AntiAntiAim.hpp>
#include "NavBot.hpp"
#include "HookTools.hpp"
#include "teamroundtimer.hpp"
#include "HookedMethods.hpp"
#include "PreDataUpdate.hpp"

View File

@ -21,7 +21,10 @@ static settings::Boolean draw{ "misc.pathing.draw", "false" };
static settings::Boolean look{ "misc.pathing.look-at-path", "false" };
static settings::Int stuck_time{ "misc.pathing.stuck-time", "4000" };
static settings::Int unreachable_time{ "misc.pathing.unreachable-time", "1000" };
static settings::Boolean log_pathing{ "misc.pathing.log", "false" };
// Score based on how much the area was used by other players, in seconds
static std::unordered_map<int, float> area_score;
static std::vector<CNavArea *> crumbs;
static Vector startPoint, endPoint;
@ -33,6 +36,8 @@ enum ignore_status : uint8_t
const_ignored,
// LOS between areas is given
vischeck_success,
// LOS if we ignore entities
vischeck_blockedentity,
// No LOS between areas
vischeck_failed,
// Failed to actually walk thru connection
@ -95,24 +100,31 @@ float getZBetweenAreas(CNavArea *start, CNavArea *end)
static std::unordered_map<std::pair<CNavArea *, CNavArea *>, ignoredata, boost::hash<std::pair<CNavArea *, CNavArea *>>> ignores;
namespace ignoremanager
{
static bool vischeck(CNavArea *begin, CNavArea *end)
static ignore_status vischeck(CNavArea *begin, CNavArea *end)
{
Vector first = begin->m_center;
Vector second = end->m_center;
first.z += 42;
second.z += 42;
return IsVectorVisible(first, second, true, LOCAL_E, MASK_PLAYERSOLID);
first.z += 70;
second.z += 70;
// Is world blocking it?
if (IsVectorVisibleNavigation(first, second, MASK_PLAYERSOLID))
{
// Is something else blocking it?
if (!IsVectorVisible(first, second, true, LOCAL_E, MASK_PLAYERSOLID))
return vischeck_blockedentity;
else
return vischeck_success;
}
return vischeck_failed;
}
static ignore_status runIgnoreChecks(CNavArea *begin, CNavArea *end)
{
if (getZBetweenAreas(begin, end) > 42)
// No z check Should be done for stairs as they can go very far up
if (getZBetweenAreas(begin, end) > 70)
return const_ignored;
if (!vischecks)
return vischeck_success;
if (vischeck(begin, end))
return vischeck_success;
else
return vischeck_failed;
return vischeck(begin, end);
}
static void updateDanger()
{
@ -249,12 +261,21 @@ static void checkPath()
ignoredata &data = ignores[{ begin, end }];
if (data.status == vischeck_failed)
return;
if (!vischeck(begin, end))
if (data.status == vischeck_blockedentity && vischeckBlock)
return;
auto vis_status = vischeck(begin, end);
if (vis_status == vischeck_failed)
{
data.status = vischeck_failed;
data.ignoreTimeout.update();
perform_repath = true;
}
else if (vis_status == vischeck_blockedentity && vischeckBlock)
{
data.status = vischeck_blockedentity;
data.ignoreTimeout.update();
perform_repath = true;
}
else if (ignores[{ end, nullptr }].status == danger_found)
{
perform_repath = true;
@ -273,7 +294,7 @@ static int isIgnored(CNavArea *begin, CNavArea *end)
status = runIgnoreChecks(begin, end);
if (status == vischeck_success)
return 0;
else if (status == vischeck_failed)
else if (status == vischeck_blockedentity && !vischeckBlock)
return 1;
else
return 2;
@ -351,6 +372,7 @@ static void updateIgnores()
}
break;
case vischeck_failed:
case vischeck_blockedentity:
case vischeck_success:
default:
if (i.second.ignoreTimeout.check(30000))
@ -399,11 +421,16 @@ struct Graph : public micropather::Graph
continue;
float distance = center->m_center.DistTo(i.area->m_center);
if (isIgnored == 1)
distance += 2000;
// Check priority based on usage
else
{
if (*vischeckBlock)
continue;
distance += 50000;
float score = area_score[neighbour->m_id];
// Formula to calculate by how much % to reduce the distance by (https://xaktly.com/LogisticFunctions.html)
float multiplier = 2.0f * ((0.9f) / (1.0f + exp(-0.8f * score)) - 0.45f);
distance *= 1.0f - multiplier;
}
adjacent->emplace_back(micropather::StateCost{ reinterpret_cast<void *>(neighbour), distance });
}
}
@ -460,6 +487,7 @@ void initThread()
void init()
{
area_score.clear();
endPoint.Invalidate();
ignoremanager::reset();
status = initing;
@ -509,7 +537,7 @@ CNavArea *findClosestNavSquare(const Vector &vec)
bestSquare = &i;
}
// Check if we are within x and y bounds of an area
if (ovBestDist >= dist || !i.IsOverlapping(vec) || !IsVectorVisible(vec, i.m_center, true, LOCAL_E, MASK_PLAYERSOLID))
if (ovBestDist >= dist || !i.IsOverlapping(vec) || !IsVectorVisibleNavigation(vec, i.m_center, MASK_PLAYERSOLID))
{
continue;
}
@ -536,15 +564,19 @@ std::vector<CNavArea *> findPath(const Vector &start, const Vector &end)
if (!(local = findClosestNavSquare(start)) || !(dest = findClosestNavSquare(end)))
return {};
logging::Info("Start: (%f,%f,%f)", local->m_center.x, local->m_center.y, local->m_center.z);
logging::Info("End: (%f,%f,%f)", dest->m_center.x, dest->m_center.y, dest->m_center.z);
if (log_pathing)
{
logging::Info("Start: (%f,%f,%f)", local->m_center.x, local->m_center.y, local->m_center.z);
logging::Info("End: (%f,%f,%f)", dest->m_center.x, dest->m_center.y, dest->m_center.z);
}
float cost;
std::vector<CNavArea *> pathNodes;
time_point begin_pathing = high_resolution_clock::now();
int result = Map.pather->Solve(reinterpret_cast<void *>(local), reinterpret_cast<void *>(dest), reinterpret_cast<std::vector<void *> *>(&pathNodes), &cost);
long long timetaken = duration_cast<nanoseconds>(high_resolution_clock::now() - begin_pathing).count();
logging::Info("Pathing: Pather result: %i. Time taken (NS): %lld", result, timetaken);
if (log_pathing)
logging::Info("Pathing: Pather result: %i. Time taken (NS): %lld", result, timetaken);
// If no result found, return empty Vector
if (result == micropather::MicroPather::NO_SOLUTION)
return {};
@ -612,12 +644,39 @@ void repath()
navTo(last, curr_priority, true, true, true);
}
// Track pather resets
static Timer reset_pather_timer{};
// Update area score to prefer paths used by actual players a bit more
void updateAreaScore()
{
for (int i = 1; i <= g_IEngine->GetMaxClients(); i++)
{
CachedEntity *ent = ENTITY(i);
if (i == g_pLocalPlayer->entity_idx || CE_INVALID(ent) || !g_pPlayerResource->isAlive(i))
continue;
// Get area
CNavArea *closest_area = nullptr;
if (ent->m_vecDormantOrigin())
findClosestNavSquare(*ent->m_vecDormantOrigin());
// Add usage to area if valid
if (closest_area)
area_score[closest_area->m_id] += g_GlobalVars->interval_per_tick;
}
if (reset_pather_timer.test_and_set(10000))
ResetPather();
}
static Timer last_jump{};
// Main movement function, gets path from NavTo
static void cm()
{
if (!enabled || status != on)
return;
// Run the logic for Nav area score
updateAreaScore();
if (CE_BAD(LOCAL_E) || CE_BAD(LOCAL_W))
return;
if (!LOCAL_E->m_bAlivePlayer())
@ -627,6 +686,7 @@ static void cm()
return;
}
ignoremanager::updateIgnores();
auto crumb = crumbs.begin();
const Vector *crumb_vec;
// Crumbs empty, prepare for next instruction
@ -676,12 +736,21 @@ static void cm()
current_user_cmd->viewangles = next;
}
// Detect when jumping is necessary
if ((!(g_pLocalPlayer->holding_sniper_rifle && g_pLocalPlayer->bZoomed) && crumb_vec->z - g_pLocalPlayer->v_Origin.z > 18 && last_jump.test_and_set(200)) || (last_jump.test_and_set(200) && inactivity.check(*stuck_time / 2)))
if ((!(g_pLocalPlayer->holding_sniper_rifle && g_pLocalPlayer->bZoomed) && crumb_vec->z - g_pLocalPlayer->v_Origin.z > 18 && last_jump.check(200)) || (last_jump.check(200) && inactivity.check(*stuck_time / 2)))
{
auto local = findClosestNavSquare(g_pLocalPlayer->v_Origin);
// Check if current area allows jumping
if (!local || !(local->m_attributeFlags & NAV_MESH_NO_JUMP))
current_user_cmd->buttons |= (IN_JUMP | IN_DUCK);
if (!local || !(local->m_attributeFlags & (NAV_MESH_NO_JUMP | NAV_MESH_STAIRS)))
{
static bool flip_action = false;
// Make it crouch the second tick
current_user_cmd->buttons |= flip_action ? IN_DUCK : IN_JUMP;
// Update jump timer now
if (flip_action)
last_jump.update();
flip_action = !flip_action;
}
}
// Walk to next crumb
WalkTo(*crumb_vec);

View File

@ -199,7 +199,7 @@ bool ChangeState(unsigned int steamid, k_EState state, bool force)
else
return false;
case k_EState::PARTY:
if (state == k_EState::TEXTMODE || state == k_EState::FRIEND)
if (state == k_EState::FRIEND)
{
ChangeState(steamid, state, true);
return true;

43
src/teamroundtimer.cpp Normal file
View File

@ -0,0 +1,43 @@
#include "common.hpp"
#include "teamroundtimer.hpp"
int CTeamRoundTimer::GetSetupTimeLength()
{
IClientEntity *ent;
ent = g_IEntityList->GetClientEntity(entity);
if (!ent || ent->GetClientClass()->m_ClassID != CL_CLASS(CTeamRoundTimer))
return -1;
return NET_INT(ent, netvar.m_nSetupTimeLength);
};
round_states CTeamRoundTimer::GetRoundState()
{
IClientEntity *ent;
ent = g_IEntityList->GetClientEntity(entity);
if (!ent || ent->GetClientClass()->m_ClassID != CL_CLASS(CTeamRoundTimer))
return RT_STATE_NORMAL;
int state = NET_INT(ent, netvar.m_nState);
return state == 1 ? RT_STATE_NORMAL : RT_STATE_SETUP;
};
void CTeamRoundTimer::Update()
{
IClientEntity *ent;
entity = 0;
for (int i = 0; i <= HIGHEST_ENTITY; i++)
{
ent = g_IEntityList->GetClientEntity(i);
if (ent && ent->GetClientClass()->m_ClassID == CL_CLASS(CTeamRoundTimer))
{
entity = i;
return;
}
}
}
CTeamRoundTimer *g_pTeamRoundTimer{ nullptr };
static InitRoutine init_trt([]() {
EC::Register(
EC::CreateMove, []() { g_pTeamRoundTimer->Update(); }, "update_teamroundtimer", EC::early);
});

View File

@ -108,6 +108,47 @@ TraceType_t trace::FilterNoPlayer::GetTraceType() const
{
return TRACE_EVERYTHING;
}
/* Navigation filter */
trace::FilterNavigation::FilterNavigation(){};
trace::FilterNavigation::~FilterNavigation(){};
#define MOVEMENT_COLLISION_GROUP 8
#define RED_CONTENTS_MASK 0x800
#define BLU_CONTENTS_MASK 0x1000
bool trace::FilterNavigation::ShouldHitEntity(IHandleEntity *handle, int mask)
{
IClientEntity *entity;
ClientClass *clazz;
if (!handle)
return false;
entity = (IClientEntity *) handle;
clazz = entity->GetClientClass();
// Ignore everything that is not the world or a CBaseEntity
if (entity->entindex() != 0 && clazz->m_ClassID != CL_CLASS(CBaseEntity))
{
// Besides respawn room areas, we want to explicitly ignore those if they are not on our team
if (clazz->m_ClassID == CL_CLASS(CFuncRespawnRoomVisualizer))
if (CE_GOOD(LOCAL_E) && (g_pLocalPlayer->team == TEAM_RED || g_pLocalPlayer->team == TEAM_BLU))
{
// If we can't collide, hit it
if (!re::C_BaseEntity::ShouldCollide(entity, MOVEMENT_COLLISION_GROUP, g_pLocalPlayer->team == TEAM_RED ? RED_CONTENTS_MASK : BLU_CONTENTS_MASK))
return true;
}
return false;
}
return true;
}
TraceType_t trace::FilterNavigation::GetTraceType() const
{
return TRACE_EVERYTHING;
}
/* No-Entity filter */
trace::FilterNoEntity::FilterNoEntity()
@ -215,5 +256,6 @@ void trace::FilterPenetration::Reset()
trace::FilterDefault trace::filter_default{};
trace::FilterNoPlayer trace::filter_no_player{};
trace::FilterNavigation trace::filter_navigation{};
trace::FilterNoEntity trace::filter_no_entity{};
trace::FilterPenetration trace::filter_penetration{};