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"; } }