**A: PluginAdminLog::GetPlayerPrefix() in pluginAdminLog only checks:**
> ```
> string GetPlayerPrefix( PlayerBase player, PlayerIdentity identity ) // player name + id + position prefix for log prints
> {
>
> m_Position = player.GetPosition();
> m_PosArray[3] = { m_Position[0].ToString(), m_Position[2].ToString(), m_Position[1].ToString() };
>
> for ( int i = 0; i < 3; i++ ) // trim position precision
> {
> m_DotIndex = m_PosArray[i].IndexOf(".");
> if ( m_DotIndex != -1 )
> {
> m_PosArray[i] = m_PosArray[i].Substring( 0, m_DotIndex + 2 );
> }
> }
>
> if ( identity ) // return partial message even if it fails to fetch identity
> {
> //return "Player \"" + "Unknown/Dead Entity" + "\" (id=" + "Unknown" + " pos=<" + m_PosArray[0] + ", " + m_PosArray[1] + ", " + m_PosArray[2] + ">)";
> m_PlayerName = "\"" + identity.GetName() + "\"";
> m_Pid = identity.GetId();
> }
> else
> {
> m_PlayerName = "\"" + player.GetCachedName() + "\"";
> m_Pid = player.GetCachedID();
> }
>
>
> if ( !player.IsAlive() )
> {
> m_PlayerName = m_PlayerName + " (DEAD)";
> }
>
> return "Player " + m_PlayerName + " (id=" + m_Pid + " pos=<" + m_PosArray[0] + ", " + m_PosArray[1] + ", " + m_PosArray[2] + ">)";
> }
> ```
> • player.GetIdentity().GetName()/GetId()
> • player.GetCachedName()/GetCachedID()
> Both return empty strings for AI, leaving "" in the log.
>
> **PROPOSED FIX:**
>
> ```
> string GetPlayerPrefix( PlayerBase player, PlayerIdentity identity ) // player name + id + position prefix for log prints
> {
>
> m_Position = player.GetPosition();
> m_PosArray[3] = { m_Position[0].ToString(), m_Position[2].ToString(), m_Position[1].ToString() };
>
> for ( int i = 0; i < 3; i++ ) // trim position precision
> {
> m_DotIndex = m_PosArray[i].IndexOf(".");
> if ( m_DotIndex != -1 )
> {
> m_PosArray[i] = m_PosArray[i].Substring( 0, m_DotIndex + 2 );
> }
> }
>
> if ( identity ) // return partial message even if it fails to fetch identity
> {
> //return "Player \"" + "Unknown/Dead Entity" + "\" (id=" + "Unknown" + " pos=<" + m_PosArray[0] + ", " + m_PosArray[1] + ", " + m_PosArray[2] + ">)";
> m_PlayerName = "\"" + identity.GetName() + "\"";
> m_Pid = identity.GetId();
> }
> else
> {
> string cachedName = player.GetCachedName(); // may be ""
> string cachedID = player.GetCachedID(); // may be ""
> if (cachedName == "")
> cachedName = "NPC/Survivor";
> if (cachedID == "")
> cachedID = "None"
>
> m_PlayerName = "\"" + cachedName + "\"";
> m_Pid = cachedID;
> }
>
> if ( !player.IsAlive() )
> {
> m_PlayerName = m_PlayerName + " (DEAD)";
> }
>
> return "Player " + m_PlayerName + " (id=" + m_Pid + " pos=<" + m_PosArray[0] + ", " + m_PosArray[1] + ", " + m_PosArray[2] + ">)";
> }
> ```
> Resulting log line:
> ```
> 19:04:28 | Player "NPC/Survivor" (id=None pos=<3683.9, 6001.2, 402.0>)[HP: 51.768] hit by Player "PLAYERnameHERE" (id=PLAYERidHERE pos=<3686.5, 6000.5, 402.0>) into Head(0) for 24.116 damage (Bullet_556x45) with M4‑A1 from 2.65726 meters
> ```
**B: PluginAdminLog::PlayerHitBy() never checks player.IsAlive() or player.GetHealth(). It prints regardless of HP.**
> **PROPOSED FIX:**
> ```
> void PlayerHitBy( TotalDamageResult damageResult, int damageType, PlayerBase player,
> EntityAI source, int component, string dmgZone, string ammo )
> {
> /* NEW EARLY RETURN ------------------------------------------- */
> if ( !player || !player.IsAlive() || player.GetHealth() <= 0 )
> return; // ignore hits on dead bodies
> /* ------------------------------------------------------------ */
>
> // …existing function body unchanged…
> }
> ```
> After this patch, any impact recorded while HP == 0 is dropped, eliminating corpse‑spam while preserving valid combat lines.
**C: PlayerHitBy() writes one line per call; there is no batching.**
> **PROPOSED FIX:**
> ```
> /* ====== Add inside PluginAdminLog class ====== */
> ref map<string, ref HitBucket> m_HitBuckets = new map<string, ref HitBucket>();
>
> class HitBucket
> {
> int count;
> float totalDmg;
> float lastHP;
> float flushAt; // epoch ms when we output
> string baseLine; // all static text before "xN / dmg"
> };
>
> /* ====== Modify PlayerHitBy() ====== */
> void PlayerHitBy( TotalDamageResult damageResult, int damageType, PlayerBase player,
> EntityAI source, int component, string dmgZone, string ammo )
> {
> if ( !player || !player.IsAlive() ) return;
>
> /* build base line exactly once per bucket */
> string basePrefix = GetPlayerPrefix( player , player.GetIdentity() ) +
> "[HP: %1] hit by "; // %1 = HP inserted later
> string srcPrefix = /* existing logic that builds attacker / item / distance */;
> string hitMessage = GetHitMessage( damageResult, component, dmgZone, ammo );
>
> int epochSec = Math.Floor(GetGame().GetTime() / 1000); // second‑precision
> string bucketKey = player.GetIdentity().GetId() + "_" + srcPrefix + "_" + epochSec;
>
> HitBucket bucket;
> if ( !m_HitBuckets.Find(bucketKey, bucket) )
> {
> bucket = new HitBucket();
> m_HitBuckets.Insert(bucketKey, bucket);
>
> bucket.baseLine = basePrefix + srcPrefix + hitMessage;
> bucket.flushAt = GetGame().GetTime() + 100; // flush 0.1 s later
> }
>
> /* accumulate */
> bucket.count++;
> bucket.totalDmg += damageResult.GetHighestDamage("Health");
> bucket.lastHP = player.GetHealth();
> }
>
> /* ====== Flush on a short timer (add to constructor) ====== */
> m_TimerHits = new Timer();
> m_TimerHits.Run( 0.11 , this, "FlushHitBuckets", NULL, true );
>
> /* ====== New method ====== */
> void FlushHitBuckets()
> {
> float now = GetGame().GetTime();
> foreach ( string key, HitBucket bucket : m_HitBuckets )
> {
> if ( now >= bucket.flushAt )
> {
> LogPrint( bucket.baseLine.Replace("%1", bucket.lastHP.ToString()) +
> " x" + bucket.count.ToString() +
> " total=" + bucket.totalDmg.ToString() + " dmg" );
> m_HitBuckets.Remove(key);
> }
> }
> }
> ```
> Resulting collapsed line example:
> ```
> 03:21:20 | Player "PlayerName" (id=PlayerID pos=<3804.7, 8770.1, 191.2>)[HP: 4.23] hit by Offroad_02 with TransportHit x10 for a total of 88.30 damage
> ```