Merge pull request 'feat(bridge): count the twenty-three chat-title conditions in player.tally (PLAN_REDESIGNS §5)' (#20) from feat/title-conditions into edge
Reviewed-on: #20
This commit is contained in:
@@ -245,6 +245,9 @@ namespace Oxide.Plugins
|
||||
"OnClanMemberLeft", "OnClanMemberKicked", "OnClanColorChanged",
|
||||
"OnKitRedeemed",
|
||||
"OnEntitySpawned", "OnEntityKill",
|
||||
"OnEntityTakeDamage", "OnItemCraftFinished", "OnEntityBuilt", "OnStructureRepair",
|
||||
"OnHealingItemUse", "OnPlayerRevive", "OnRocketLaunched",
|
||||
"OnExplosiveThrown", "OnExplosiveDropped", "OnMissionSucceeded",
|
||||
"OnEnterZone", "OnExitZone", "OnRgDomesReady"
|
||||
};
|
||||
|
||||
@@ -1314,15 +1317,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;
|
||||
@@ -1351,16 +1346,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>
|
||||
@@ -1643,8 +1671,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;
|
||||
@@ -1767,6 +1801,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
|
||||
@@ -1782,7 +1825,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);
|
||||
}
|
||||
|
||||
@@ -1791,9 +1836,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;
|
||||
}
|
||||
@@ -1841,6 +1891,258 @@ 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.
|
||||
/// Who crafted it is the crafter's owner: the task no longer carries one (read off the
|
||||
/// rig, 2026-09-29).
|
||||
/// </summary>
|
||||
private void OnItemCraftFinished(ItemCraftTask task, Item item, ItemCrafter crafter)
|
||||
{
|
||||
MarkHook("OnItemCraftFinished");
|
||||
|
||||
BasePlayer player = crafter != null ? crafter.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.GetComponent<BuildingBlock>() == null)
|
||||
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="target"/> is what is healed;
|
||||
/// the tool's owner is who heals. Healing yourself is not a heal (D174).
|
||||
///
|
||||
/// <para>
|
||||
/// The target is taken as <c>object</c>: the game's `GiveEffectsTo` now takes an
|
||||
/// `IMedicalToolTarget`, which is not always a player, and a parameter typed
|
||||
/// `BasePlayer` would simply never be called for the hook's current shape. Only a
|
||||
/// player healed counts.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private void OnHealingItemUse(MedicalTool tool, object target)
|
||||
{
|
||||
MarkHook("OnHealingItemUse");
|
||||
|
||||
BasePlayer player = target as BasePlayer;
|
||||
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, BasePlayer assignee)
|
||||
{
|
||||
MarkHook("OnMissionSucceeded");
|
||||
|
||||
// The game hands the assignee over: `MissionSuccess(instance, assignee)`. The instance
|
||||
// itself has no accessor for it (read off the rig, 2026-09-29).
|
||||
if (assignee == null || assignee.IsNpc)
|
||||
return;
|
||||
|
||||
TallyFor(assignee).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
|
||||
@@ -2462,19 +2764,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2544,6 +2897,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