feat(bridge): count the twenty-three chat-title conditions in player.tally (PLAN_REDESIGNS §5)
player.tally gains animalKills, weaponKills (by the killfeed's weapon prefab name), headshots, bestPvpM/bestPveM (this interval's maximum), apcKills, heliKills, crafted (by item category), built, repaired, healed, rockets, explosives and missions. Every field is absent when nothing moved. Ten new void hooks, all in ExpectedHooks: OnEntityTakeDamage (the last player to hurt an APC or helicopter, for a crash that names nobody), OnItemCraftFinished, OnEntityBuilt, OnStructureRepair (counted only when the health rose by the next tick), OnHealingItemUse and OnPlayerRevive (another player only; a syringe revive counts once), OnRocketLaunched, OnExplosiveThrown/Dropped (a damaging TimedExplosive only) and OnMissionSucceeded. npcKills keeps counting animals too, so the public column does not change meaning mid-wipe; the site takes human NPCs as the difference (D209). WeaponOf and DistanceOf are split out of DescribeAttacker so the killfeed and the tally read one name and one distance. Not yet compiled on a rig: the rigs' storage is unmounted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -244,7 +244,10 @@ namespace Oxide.Plugins
|
||||
"OnClanCreated", "OnClanDisbanded", "OnClanMemberAdded",
|
||||
"OnClanMemberLeft", "OnClanMemberKicked", "OnClanColorChanged",
|
||||
"OnKitRedeemed",
|
||||
"OnEntitySpawned", "OnEntityKill"
|
||||
"OnEntitySpawned", "OnEntityKill",
|
||||
"OnEntityTakeDamage", "OnItemCraftFinished", "OnEntityBuilt", "OnStructureRepair",
|
||||
"OnHealingItemUse", "OnPlayerRevive", "OnRocketLaunched",
|
||||
"OnExplosiveThrown", "OnExplosiveDropped", "OnMissionSucceeded"
|
||||
};
|
||||
|
||||
private void Init()
|
||||
@@ -1302,15 +1305,7 @@ namespace Oxide.Plugins
|
||||
BaseEntity initiator = info != null ? info.Initiator : null;
|
||||
BasePlayer attacker = info != null ? info.InitiatorPlayer : null;
|
||||
|
||||
string weapon = null;
|
||||
|
||||
if (info != null)
|
||||
{
|
||||
if (info.WeaponPrefab != null)
|
||||
weapon = info.WeaponPrefab.ShortPrefabName;
|
||||
else if (info.Weapon != null)
|
||||
weapon = info.Weapon.ShortPrefabName;
|
||||
}
|
||||
string weapon = WeaponOf(info);
|
||||
|
||||
if (weapon != null)
|
||||
frame["weapon"] = weapon;
|
||||
@@ -1339,16 +1334,49 @@ namespace Oxide.Plugins
|
||||
frame["attackerName"] = initiator.ShortPrefabName;
|
||||
}
|
||||
|
||||
float distance = info != null ? info.ProjectileDistance : 0f;
|
||||
|
||||
if (distance <= 0f && initiator.transform != null && victim.transform != null)
|
||||
distance = UnityEngine.Vector3.Distance(
|
||||
initiator.transform.position, victim.transform.position);
|
||||
float distance = DistanceOf(info, victim);
|
||||
|
||||
if (distance > 0f)
|
||||
frame["distance"] = (float)Math.Round(distance, 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The killing blow's weapon as the killfeed names it: the held entity's short prefab
|
||||
/// name (<c>bow_hunting.entity</c>), or what was thrown or placed. Null when the hit
|
||||
/// carries neither. The tally's weapon kills (§5.3) read this same name, so the lists
|
||||
/// the site keeps are lists of what the killfeed already shows.
|
||||
/// </summary>
|
||||
private static string WeaponOf(HitInfo info)
|
||||
{
|
||||
if (info == null)
|
||||
return null;
|
||||
|
||||
if (info.WeaponPrefab != null)
|
||||
return info.WeaponPrefab.ShortPrefabName;
|
||||
|
||||
return info.Weapon != null ? info.Weapon.ShortPrefabName : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How far the killing blow travelled, in metres: the projectile's own distance, or the
|
||||
/// gap between the two when there was no projectile. 0 when neither can be read.
|
||||
/// </summary>
|
||||
private static float DistanceOf(HitInfo info, BaseEntity victim)
|
||||
{
|
||||
if (info == null)
|
||||
return 0f;
|
||||
|
||||
float distance = info.ProjectileDistance;
|
||||
BaseEntity initiator = info.Initiator;
|
||||
|
||||
if (distance <= 0f && initiator != null && initiator.transform != null
|
||||
&& victim != null && victim.transform != null)
|
||||
distance = UnityEngine.Vector3.Distance(
|
||||
initiator.transform.position, victim.transform.position);
|
||||
|
||||
return distance;
|
||||
}
|
||||
|
||||
// ---- the map grid ----
|
||||
|
||||
/// <summary>
|
||||
@@ -1631,8 +1659,14 @@ namespace Oxide.Plugins
|
||||
BasePlayer killer = info != null ? info.InitiatorPlayer : null;
|
||||
|
||||
if (killer != null && !killer.IsNpc && (ulong)killer.userID != (ulong)player.userID)
|
||||
{
|
||||
CreditKill(killer, true);
|
||||
|
||||
// The kill itself is `player.death`'s, counted by the site from this frame; the
|
||||
// tally adds only what a title needs about it (§5.1).
|
||||
TallyFor(killer).Killed(info, player, true);
|
||||
}
|
||||
|
||||
Dictionary<string, object> frame = Frame("player.death", "event");
|
||||
frame["steamId"] = SteamIdOf(player);
|
||||
frame["name"] = player.displayName;
|
||||
@@ -1755,6 +1789,15 @@ namespace Oxide.Plugins
|
||||
return;
|
||||
|
||||
BasePlayer attacker = info != null ? info.InitiatorPlayer : null;
|
||||
|
||||
// Before the early return below: a patrol helicopter usually dies by crashing, and a
|
||||
// crash's HitInfo names no player (§5.4).
|
||||
if (IsVehicleKill(entity))
|
||||
{
|
||||
CreditVehicle(entity, attacker);
|
||||
return;
|
||||
}
|
||||
|
||||
bool byPlayer = attacker != null && !attacker.IsNpc;
|
||||
|
||||
// Nothing below is interesting unless a real player did it. That single test discards
|
||||
@@ -1770,7 +1813,9 @@ namespace Oxide.Plugins
|
||||
// double every kill in the feed. Only the NPC kind is ours to count.
|
||||
if (victimPlayer.IsNpc)
|
||||
{
|
||||
TallyFor(attacker).NpcKills++;
|
||||
Tally tally = TallyFor(attacker);
|
||||
tally.NpcKills++;
|
||||
tally.Killed(info, entity, false);
|
||||
CreditKill(attacker, false);
|
||||
}
|
||||
|
||||
@@ -1779,9 +1824,14 @@ namespace Oxide.Plugins
|
||||
|
||||
// An animal is an NPC to a run's tally exactly as it is to the leaderboard (§29.5):
|
||||
// two meanings of one word would be two answers to "how many NPCs did I kill".
|
||||
// `animalKills` counts it AGAIN, separately (§5.2): the public NPC column keeps its
|
||||
// meaning mid-wipe, and the site takes the human NPCs as the difference.
|
||||
if (entity is BaseNpc)
|
||||
{
|
||||
TallyFor(attacker).NpcKills++;
|
||||
Tally tally = TallyFor(attacker);
|
||||
tally.NpcKills++;
|
||||
tally.AnimalKills++;
|
||||
tally.Killed(info, entity, false);
|
||||
CreditKill(attacker, false);
|
||||
return;
|
||||
}
|
||||
@@ -1829,6 +1879,246 @@ namespace Oxide.Plugins
|
||||
EmitFrame(frame);
|
||||
}
|
||||
|
||||
// ---- what a chat title counts (PLAN_REDESIGNS §5, D172–D174) ----
|
||||
//
|
||||
// Every one of these is a counter (rule 2) and void (rule 1). Three of them can veto —
|
||||
// `OnStructureRepair`, `OnHealingItemUse`, `OnPlayerRevive` — and `OnEntityTakeDamage`
|
||||
// can cancel the damage outright, which is exactly why none of them returns anything.
|
||||
|
||||
/// <summary>How long a player's hit on a vehicle still earns its kill when the blow that ends it names nobody (§5.4).</summary>
|
||||
private const float VehicleCreditSeconds = 60f;
|
||||
|
||||
/// <summary>The last player to hurt each APC or helicopter, by net id. A handful at most.</summary>
|
||||
private readonly Dictionary<ulong, KeyValuePair<ulong, float>> _vehicleHits =
|
||||
new Dictionary<ulong, KeyValuePair<ulong, float>>();
|
||||
|
||||
/// <summary>The APC and the two helicopters: the three things a title credits a kill of (D174).</summary>
|
||||
private static bool IsVehicleKill(BaseEntity entity)
|
||||
{
|
||||
return entity is BradleyAPC || entity is PatrolHelicopter || entity is CH47Helicopter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Void: a non-null return cancels the damage. It fires for every hit on everything,
|
||||
/// so the type test is the first thing it does; only a hit on one of the three
|
||||
/// vehicles is remembered, and only who dealt it and when.
|
||||
/// </summary>
|
||||
private void OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info)
|
||||
{
|
||||
MarkHook("OnEntityTakeDamage");
|
||||
|
||||
if (entity == null || info == null || !IsVehicleKill(entity) || entity.net == null)
|
||||
return;
|
||||
|
||||
BasePlayer player = info.InitiatorPlayer;
|
||||
if (player == null || player.IsNpc)
|
||||
return;
|
||||
|
||||
float now = UnityEngine.Time.realtimeSinceStartup;
|
||||
|
||||
// A helicopter that flies off the map is never killed, so its row is never removed
|
||||
// at death; a stale one is dropped here instead.
|
||||
if (_vehicleHits.Count > 16)
|
||||
{
|
||||
var stale = new List<ulong>();
|
||||
foreach (KeyValuePair<ulong, KeyValuePair<ulong, float>> hit in _vehicleHits)
|
||||
if (now - hit.Value.Value > VehicleCreditSeconds)
|
||||
stale.Add(hit.Key);
|
||||
foreach (ulong id in stale)
|
||||
_vehicleHits.Remove(id);
|
||||
}
|
||||
|
||||
_vehicleHits[entity.net.ID.Value] = new KeyValuePair<ulong, float>(player.userID, now);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The killing blow's player (D174), or, when the blow names nobody — a helicopter
|
||||
/// crashing — the last player who hurt it within <see cref="VehicleCreditSeconds"/>.
|
||||
/// </summary>
|
||||
private void CreditVehicle(BaseCombatEntity entity, BasePlayer attacker)
|
||||
{
|
||||
KeyValuePair<ulong, float> last = default(KeyValuePair<ulong, float>);
|
||||
bool hit = entity.net != null && _vehicleHits.TryGetValue(entity.net.ID.Value, out last);
|
||||
|
||||
if (entity.net != null)
|
||||
_vehicleHits.Remove(entity.net.ID.Value);
|
||||
|
||||
BasePlayer credited = attacker != null && !attacker.IsNpc ? attacker : null;
|
||||
|
||||
if (credited == null && hit && UnityEngine.Time.realtimeSinceStartup - last.Value <= VehicleCreditSeconds)
|
||||
credited = BasePlayer.FindByID(last.Key) ?? BasePlayer.FindSleeping(last.Key);
|
||||
|
||||
if (credited == null)
|
||||
return;
|
||||
|
||||
Tally tally = TallyFor(credited);
|
||||
|
||||
if (entity is BradleyAPC)
|
||||
tally.ApcKills++;
|
||||
else
|
||||
tally.HeliKills++;
|
||||
}
|
||||
|
||||
/// <summary>No return behaviour. A crafted shirt, rifle or anything else, by its item category.</summary>
|
||||
private void OnItemCraftFinished(ItemCraftTask task, Item item, ItemCrafter crafter)
|
||||
{
|
||||
MarkHook("OnItemCraftFinished");
|
||||
|
||||
BasePlayer player = task != null ? task.owner : null;
|
||||
|
||||
if (player == null || player.IsNpc || item == null || item.info == null)
|
||||
return;
|
||||
|
||||
TallyFor(player).Craft(item.info.category.ToString().ToLowerInvariant(), item.amount);
|
||||
}
|
||||
|
||||
/// <summary>No return behaviour. Only a building block is a structure built; a deployable is not.</summary>
|
||||
private void OnEntityBuilt(Planner planner, UnityEngine.GameObject go)
|
||||
{
|
||||
MarkHook("OnEntityBuilt");
|
||||
|
||||
BasePlayer player = planner != null ? planner.GetOwnerPlayer() : null;
|
||||
|
||||
if (player == null || player.IsNpc || go == null || !(go.ToBaseEntity() is BuildingBlock))
|
||||
return;
|
||||
|
||||
TallyFor(player).Built++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Void: a non-null return refuses the repair. It fires on every hammer hit, BEFORE the
|
||||
/// game decides whether there is anything to repair, the resources to pay for it, or a
|
||||
/// recent attack that forbids it. So the health is read now and again next tick, and
|
||||
/// only a hit that actually raised it is a repair.
|
||||
/// </summary>
|
||||
private void OnStructureRepair(BaseCombatEntity entity, BasePlayer player)
|
||||
{
|
||||
MarkHook("OnStructureRepair");
|
||||
|
||||
if (entity == null || player == null || player.IsNpc)
|
||||
return;
|
||||
|
||||
float before = entity.health;
|
||||
ulong id = player.userID;
|
||||
|
||||
NextTick(() =>
|
||||
{
|
||||
if (entity == null || entity.IsDestroyed || entity.health <= before)
|
||||
return;
|
||||
|
||||
BasePlayer still = BasePlayer.FindByID(id);
|
||||
if (still != null)
|
||||
TallyFor(still).Repaired++;
|
||||
});
|
||||
}
|
||||
|
||||
// A syringe on a downed player raises `OnPlayerRevive` and then `OnHealingItemUse` for the
|
||||
// same use; one act of help is one heal. The revive is remembered for the frame it happened in.
|
||||
private ulong _reviveHealer;
|
||||
private ulong _reviveTarget;
|
||||
private int _reviveFrame = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Void: a non-null return refuses the heal. <paramref name="player"/> is who is healed;
|
||||
/// the tool's owner is who heals. Healing yourself is not a heal (D174).
|
||||
/// </summary>
|
||||
private void OnHealingItemUse(MedicalTool tool, BasePlayer player)
|
||||
{
|
||||
MarkHook("OnHealingItemUse");
|
||||
|
||||
BasePlayer healer = tool != null ? tool.GetOwnerPlayer() : null;
|
||||
|
||||
if (healer == null || healer.IsNpc || player == null || player.IsNpc
|
||||
|| (ulong)healer.userID == (ulong)player.userID)
|
||||
return;
|
||||
|
||||
if (_reviveFrame == UnityEngine.Time.frameCount
|
||||
&& _reviveHealer == (ulong)healer.userID && _reviveTarget == (ulong)player.userID)
|
||||
return;
|
||||
|
||||
TallyFor(healer).Healed++;
|
||||
}
|
||||
|
||||
/// <summary>Void: a non-null return refuses the revive. Reviving yourself is not possible, and is not counted.</summary>
|
||||
private void OnPlayerRevive(BasePlayer reviver, BasePlayer player)
|
||||
{
|
||||
MarkHook("OnPlayerRevive");
|
||||
|
||||
if (reviver == null || reviver.IsNpc || player == null || player.IsNpc
|
||||
|| (ulong)reviver.userID == (ulong)player.userID)
|
||||
return;
|
||||
|
||||
_reviveHealer = reviver.userID;
|
||||
_reviveTarget = player.userID;
|
||||
_reviveFrame = UnityEngine.Time.frameCount;
|
||||
|
||||
TallyFor(reviver).Healed++;
|
||||
}
|
||||
|
||||
/// <summary>No return behaviour. One rocket leaving a launcher.</summary>
|
||||
private void OnRocketLaunched(BasePlayer player, BaseEntity entity)
|
||||
{
|
||||
MarkHook("OnRocketLaunched");
|
||||
|
||||
if (player == null || player.IsNpc)
|
||||
return;
|
||||
|
||||
TallyFor(player).Rockets++;
|
||||
}
|
||||
|
||||
/// <summary>No return behaviour. A grenade, beancan, satchel or C4 thrown.</summary>
|
||||
private void OnExplosiveThrown(BasePlayer player, BaseEntity entity, ThrownWeapon item)
|
||||
{
|
||||
MarkHook("OnExplosiveThrown");
|
||||
CountExplosive(player, entity);
|
||||
}
|
||||
|
||||
/// <summary>No return behaviour. The same, dropped at your feet rather than thrown.</summary>
|
||||
private void OnExplosiveDropped(BasePlayer player, BaseEntity entity, ThrownWeapon item)
|
||||
{
|
||||
MarkHook("OnExplosiveDropped");
|
||||
CountExplosive(player, entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A supply signal and a smoke grenade are thrown through the same hooks, and neither
|
||||
/// is an explosive. What is one is a timed explosive that does damage.
|
||||
/// </summary>
|
||||
private void CountExplosive(BasePlayer player, BaseEntity entity)
|
||||
{
|
||||
if (player == null || player.IsNpc)
|
||||
return;
|
||||
|
||||
TimedExplosive explosive = entity as TimedExplosive;
|
||||
if (explosive == null || explosive is SupplySignal || explosive.damageTypes == null)
|
||||
return;
|
||||
|
||||
foreach (Rust.DamageTypeEntry damage in explosive.damageTypes)
|
||||
{
|
||||
if (damage != null && damage.amount > 0f)
|
||||
{
|
||||
TallyFor(player).Explosives++;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// No return behaviour. A mission one of the game's NPCs handed out, finished (D173). A
|
||||
/// failed or abandoned one raises a different hook, and is never counted.
|
||||
/// </summary>
|
||||
private void OnMissionSucceeded(BaseMission mission, BaseMission.MissionInstance instance)
|
||||
{
|
||||
MarkHook("OnMissionSucceeded");
|
||||
|
||||
BasePlayer player = instance != null ? instance.GetAssignee() : null;
|
||||
|
||||
if (player == null || player.IsNpc)
|
||||
return;
|
||||
|
||||
TallyFor(player).Missions++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The most people one <c>entity.destroyed</c> frame names as authorised. A cupboard
|
||||
/// has no hard cap on vanilla, and an unbounded list is an unbounded line on the game
|
||||
@@ -2450,19 +2740,70 @@ namespace Oxide.Plugins
|
||||
public int Structures;
|
||||
public string Name;
|
||||
|
||||
// Protocol 13, the chat-title conditions (PLAN_REDESIGNS §5.1). Sums unless named a best.
|
||||
public int AnimalKills;
|
||||
public readonly Dictionary<string, int> WeaponKills = new Dictionary<string, int>();
|
||||
public int Headshots;
|
||||
/// <summary>The longest player kill this interval, in metres: a best, not a sum.</summary>
|
||||
public float BestPvpM;
|
||||
/// <summary>The longest NPC or animal kill this interval: a best, not a sum.</summary>
|
||||
public float BestPveM;
|
||||
public int ApcKills;
|
||||
public int HeliKills;
|
||||
public readonly Dictionary<string, int> Crafted = new Dictionary<string, int>();
|
||||
public int Built;
|
||||
public int Repaired;
|
||||
public int Healed;
|
||||
public int Rockets;
|
||||
public int Explosives;
|
||||
public int Missions;
|
||||
|
||||
public void Gather(string shortname, int amount)
|
||||
{
|
||||
if (string.IsNullOrEmpty(shortname) || amount <= 0)
|
||||
Add(Gathered, shortname, amount);
|
||||
}
|
||||
|
||||
public void Craft(string category, int amount)
|
||||
{
|
||||
Add(Crafted, category, amount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// What a credited kill adds for the titles: its weapon (any victim, D174), a
|
||||
/// headshot, and its distance as this interval's best of its kind.
|
||||
/// </summary>
|
||||
public void Killed(HitInfo info, BaseEntity victim, bool victimIsPlayer)
|
||||
{
|
||||
Add(WeaponKills, WeaponOf(info), 1);
|
||||
|
||||
if (info != null && info.isHeadshot)
|
||||
Headshots++;
|
||||
|
||||
float distance = DistanceOf(info, victim);
|
||||
|
||||
if (victimIsPlayer)
|
||||
BestPvpM = Math.Max(BestPvpM, distance);
|
||||
else
|
||||
BestPveM = Math.Max(BestPveM, distance);
|
||||
}
|
||||
|
||||
private static void Add(Dictionary<string, int> into, string key, int amount)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key) || amount <= 0)
|
||||
return;
|
||||
|
||||
int have;
|
||||
Gathered.TryGetValue(shortname, out have);
|
||||
Gathered[shortname] = have + amount;
|
||||
into.TryGetValue(key, out have);
|
||||
into[key] = have + amount;
|
||||
}
|
||||
|
||||
public bool IsEmpty()
|
||||
{
|
||||
return Gathered.Count == 0 && NpcKills == 0 && Structures == 0;
|
||||
return Gathered.Count == 0 && NpcKills == 0 && Structures == 0
|
||||
&& AnimalKills == 0 && WeaponKills.Count == 0 && Headshots == 0
|
||||
&& BestPvpM <= 0f && BestPveM <= 0f && ApcKills == 0 && HeliKills == 0
|
||||
&& Crafted.Count == 0 && Built == 0 && Repaired == 0 && Healed == 0
|
||||
&& Rockets == 0 && Explosives == 0 && Missions == 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2532,6 +2873,36 @@ namespace Oxide.Plugins
|
||||
if (tally.Structures > 0)
|
||||
frame["structures"] = tally.Structures;
|
||||
|
||||
// The title conditions (§5.1), each absent when nothing moved, as the three above.
|
||||
if (tally.AnimalKills > 0)
|
||||
frame["animalKills"] = tally.AnimalKills;
|
||||
if (tally.WeaponKills.Count > 0)
|
||||
frame["weaponKills"] = tally.WeaponKills;
|
||||
if (tally.Headshots > 0)
|
||||
frame["headshots"] = tally.Headshots;
|
||||
if (tally.BestPvpM > 0f)
|
||||
frame["bestPvpM"] = (float)Math.Round(tally.BestPvpM, 1);
|
||||
if (tally.BestPveM > 0f)
|
||||
frame["bestPveM"] = (float)Math.Round(tally.BestPveM, 1);
|
||||
if (tally.ApcKills > 0)
|
||||
frame["apcKills"] = tally.ApcKills;
|
||||
if (tally.HeliKills > 0)
|
||||
frame["heliKills"] = tally.HeliKills;
|
||||
if (tally.Crafted.Count > 0)
|
||||
frame["crafted"] = tally.Crafted;
|
||||
if (tally.Built > 0)
|
||||
frame["built"] = tally.Built;
|
||||
if (tally.Repaired > 0)
|
||||
frame["repaired"] = tally.Repaired;
|
||||
if (tally.Healed > 0)
|
||||
frame["healed"] = tally.Healed;
|
||||
if (tally.Rockets > 0)
|
||||
frame["rockets"] = tally.Rockets;
|
||||
if (tally.Explosives > 0)
|
||||
frame["explosives"] = tally.Explosives;
|
||||
if (tally.Missions > 0)
|
||||
frame["missions"] = tally.Missions;
|
||||
|
||||
EmitFrame(frame);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user