namespace Aeshnidae.InstancesNoDat;
///
/// Experience shared across the copies of a dungeon, with no fellowship (a player's
/// suggestion, 2026-09-20). A kill in your copy of Matron Hive South pays you in full,
/// and pays a slice to everyone else who is in any copy of Matron Hive South at that
/// moment - people hunting the same place, alone by design, but not for nothing to
/// each other. Off and on at a switch (Settings.ShareExperienceAcrossCopies), because
/// it is an experiment.
///
/// The share is a plain GrantXP of the slice to each other player, as a kill, with no
/// fellowship or allegiance passing on it (ShareType.None) and a guard so a share never
/// shares again. Radiance follows it as it follows any kill experience (Aeshnidae.Bank
/// hooks GrantXP). What each player has earned in a dungeon - their own kills and what
/// they have been handed - is kept while they are inside, for /instance who.
///
public static class Sharing
{
private sealed class Tally
{
public ushort Landblock;
public DateTime Since = DateTime.UtcNow;
public long Own, Shared;
}
private static readonly ConcurrentDictionary _tallies = new();
[ThreadStatic] private static bool _sharing;
/// The kill experience a player just received in a copy; pay the others their slice.
public static void OnKillExperience(Player player, long amount, XpType xpType, ShareType shareType)
{
if (_sharing || amount <= 0 || !Mod.Settings.Enabled || !Mod.Settings.ShareExperienceAcrossCopies)
return;
if (xpType != XpType.Kill)
return;
// The outer call of a fellowship split grants nothing itself.
if (player.Fellowship != null && player.Fellowship.ShareXP && shareType.HasFlag(ShareType.Fellowship))
return;
var here = player.CurrentLandblock;
if (here is null || InstanceWorld.CopyOf(here) <= 0)
return;
var landblock = (ushort)here.Id.Landblock;
if (Mod.ScopeOf(landblock, InstanceWorld.CopyOf(here)) != InstanceScope.Personal)
return;
Note(player, landblock).Own += amount;
var slice = (long)Math.Round(amount * Mod.Settings.SharePercent / 100.0);
if (slice <= 0)
return;
var paid = 0;
foreach (var other in OthersIn(landblock, player))
{
try
{
_sharing = true;
other.GrantXP(slice, XpType.Kill, ShareType.None);
Note(other, landblock).Shared += slice;
other.SendMessage($"You share in {player.Name}'s kill: +{slice:N0} experience.");
paid++;
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] share to {other.Name} failed: {ex.Message}", ModManager.LogLevel.Warn);
}
finally
{
_sharing = false;
}
}
}
/// Everyone in any copy of this dungeon, except the killer.
public static List OthersIn(ushort landblock, Player? except)
{
var others = new List();
foreach (var (key, block) in InstanceWorld.All)
{
if (key.Landblock != landblock)
continue;
foreach (var p in InstanceWorld.PlayersIn(block))
{
if (except is not null && ReferenceEquals(p, except))
continue;
if (p?.Session is null)
continue;
others.Add(p);
}
}
return others;
}
private static Tally Note(Player player, ushort landblock)
{
var tally = _tallies.GetOrAdd(player.Guid.Full, _ => new Tally { Landblock = landblock });
if (tally.Landblock != landblock)
{
tally = new Tally { Landblock = landblock };
_tallies[player.Guid.Full] = tally;
}
return tally;
}
/// Forget a player's tally when they leave the dungeon (or log out).
public static void Left(Player player) => _tallies.TryRemove(player.Guid.Full, out _);
/// The lines for /instance who: everyone in any copy of this dungeon and what they have made here.
public static IEnumerable Who(ushort landblock)
{
var name = Mod.NameFor(landblock);
var s = Mod.Settings;
yield return s.ShareExperienceAcrossCopies
? $"In {name}: every kill pays its killer in full and {s.SharePercent:0.#}% to everyone else in the dungeon, any copy."
: $"In {name}: experience is not shared across copies (switched off).";
var any = false;
foreach (var (key, block) in InstanceWorld.All.Where(a => a.Key.Landblock == landblock).OrderBy(a => a.Key.Copy))
{
foreach (var p in InstanceWorld.PlayersIn(block))
{
any = true;
var tier = Scaling.Counted(landblock, key.Copy);
if (_tallies.TryGetValue(p.Guid.Full, out var t) && t.Landblock == landblock)
{
var hours = Math.Max((DateTime.UtcNow - t.Since).TotalHours, 1.0 / 60);
yield return $" {p.Name} - copy {key.Copy}, tier {tier}: {t.Own:N0} from own kills, {t.Shared:N0} shared in, " +
$"{(t.Own + t.Shared) / hours:N0}/hour over {(DateTime.UtcNow - t.Since).TotalMinutes:0} min";
}
else
yield return $" {p.Name} - copy {key.Copy}, tier {tier}: nothing yet";
}
}
if (!any)
yield return " nobody is in any copy of it";
}
}