using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ACE.Common;
using ACE.Entity;
using ACE.Entity.Enum;
using ACE.Server.Managers;
using ACE.Server.Network.GameEvent.Events;
using ACE.Server.Network.GameMessages.Messages;
using ACE.Server.Network.Structure;
using ACE.Server.WorldObjects;
using log4net;
namespace ACE.Server.Entity
{
public class Fellowship
{
private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
///
/// The maximum # of fellowship members
///
public static int MaxFellows = 9;
public string FellowshipName;
public uint FellowshipLeaderGuid;
public bool DesiredShareXP; // determined by the leader's 'ShareFellowshipExpAndLuminance' client option when fellowship is created
public bool ShareLoot; // determined by the leader's 'ShareFellowshipLoot' client option when fellowship is created
public bool ShareXP; // whether or not XP sharing is currently enabled, as determined by DesiredShareXP && level restrictions
public bool EvenShare; // true if all fellows are >= level 50, or all fellows are within 5 levels of the leader
public bool Open; // indicates if non-leaders can invite new fellowship members
public bool IsLocked; // only set through emotes. if a fellowship is locked, new fellowship members cannot be added
public Dictionary> FellowshipMembers;
public Dictionary DepartedMembers;
public Dictionary FellowshipLocks;
public QuestManager QuestManager;
///
/// Called when a player first creates a Fellowship
///
public Fellowship(Player leader, string fellowshipName, bool shareXP)
{
DesiredShareXP = shareXP;
ShareXP = shareXP;
// get loot sharing from leader's character options
ShareLoot = leader.GetCharacterOption(CharacterOption.ShareFellowshipLoot);
FellowshipLeaderGuid = leader.Guid.Full;
FellowshipName = fellowshipName;
EvenShare = false;
FellowshipMembers = new Dictionary>() { { leader.Guid.Full, new WeakReference(leader) } };
Open = false;
QuestManager = new QuestManager(this);
IsLocked = false;
DepartedMembers = new Dictionary();
FellowshipLocks = new Dictionary();
}
///
/// Called when a player clicks the 'add fellow' button
///
public void AddFellowshipMember(Player inviter, Player newMember)
{
if (inviter == null || newMember == null)
return;
if (IsLocked)
{
if (!DepartedMembers.TryGetValue(newMember.Guid.Full, out var timeDeparted))
{
inviter.Session.Network.EnqueueSend(new GameEventWeenieErrorWithString(inviter.Session, WeenieErrorWithString.LockedFellowshipCannotRecruit_, newMember.Name));
//newMember.SendWeenieError(WeenieError.LockedFellowshipCannotRecruitYou);
return;
}
else
{
var timeLimit = Time.GetDateTimeFromTimestamp(timeDeparted).AddSeconds(600);
if (DateTime.UtcNow > timeLimit)
{
inviter.Session.Network.EnqueueSend(new GameEventWeenieErrorWithString(inviter.Session, WeenieErrorWithString.LockedFellowshipCannotRecruit_, newMember.Name));
//newMember.SendWeenieError(WeenieError.LockedFellowshipCannotRecruitYou);
return;
}
}
}
if (FellowshipMembers.Count >= MaxFellows)
{
inviter.Session.Network.EnqueueSend(new GameEventWeenieError(inviter.Session, WeenieError.YourFellowshipIsFull));
return;
}
if (newMember.Fellowship != null || FellowshipMembers.ContainsKey(newMember.Guid.Full))
{
inviter.Session.Network.EnqueueSend(new GameMessageSystemChat($"{newMember.Name} is already a member of a Fellowship.", ChatMessageType.Broadcast));
}
else
{
if (PropertyManager.GetBool("fellow_busy_no_recruit").Item && newMember.IsBusy)
{
inviter.Session.Network.EnqueueSend(new GameMessageSystemChat($"{newMember.Name} is busy.", ChatMessageType.Broadcast));
return;
}
if (newMember.GetCharacterOption(CharacterOption.AutomaticallyAcceptFellowshipRequests))
{
AddConfirmedMember(inviter, newMember, true);
}
else
{
if (!newMember.ConfirmationManager.EnqueueSend(new Confirmation_Fellowship(inviter.Guid, newMember.Guid), inviter.Name))
{
inviter.Session.Network.EnqueueSend(new GameMessageSystemChat($"{newMember.Name} is busy.", ChatMessageType.Broadcast));
}
}
}
}
///
/// Finalizes the process of adding a player to the fellowship
/// If the player doesn't have the 'automatically accept fellowship requests' option set,
/// this would be after they responded to the popup window
///
public void AddConfirmedMember(Player inviter, Player player, bool response)
{
if (inviter == null || inviter.Session == null || inviter.Session.Player == null || player == null) return;
if (!response)
{
// player clicked 'no' on the fellowship popup
inviter.Session.Network.EnqueueSend(new GameMessageSystemChat($"{player.Name} declines your invite", ChatMessageType.Fellowship));
inviter.Session.Network.EnqueueSend(new GameEventWeenieError(inviter.Session, WeenieError.FellowshipDeclined));
return;
}
if (FellowshipMembers.Count >= MaxFellows)
{
inviter.Session.Network.EnqueueSend(new GameEventWeenieError(inviter.Session, WeenieError.YourFellowshipIsFull));
return;
}
FellowshipMembers.TryAdd(player.Guid.Full, new WeakReference(player));
player.Fellowship = inviter.Fellowship;
CalculateXPSharing();
var fellowshipMembers = GetFellowshipMembers();
foreach (var member in fellowshipMembers.Values.Where(i => i.Guid != player.Guid))
member.Session.Network.EnqueueSend(new GameEventFellowshipUpdateFellow(member.Session, player, ShareXP));
if (ShareLoot)
{
foreach (var member in fellowshipMembers.Values.Where(i => i.Guid != player.Guid))
{
member.Session.Network.EnqueueSend(new GameMessageSystemChat($"{player.Name} has given you permission to loot his or her kills.", ChatMessageType.Broadcast));
member.Session.Network.EnqueueSend(new GameMessageSystemChat($"{player.Name} may now loot your kills.", ChatMessageType.Broadcast));
player.Session.Network.EnqueueSend(new GameMessageSystemChat($"{member.Name} has given you permission to loot his or her kills.", ChatMessageType.Broadcast));
player.Session.Network.EnqueueSend(new GameMessageSystemChat($"{member.Name} may now loot your kills.", ChatMessageType.Broadcast));
}
}
UpdateAllMembers();
if (inviter.CurrentMotionState.Stance == MotionStance.NonCombat) // only do this motion if inviter is at peace, other times motion is skipped.
inviter.SendMotionAsCommands(MotionCommand.BowDeep, MotionStance.NonCombat);
}
public void RemoveFellowshipMember(Player player, Player leader)
{
if (player == null) return;
var fellowshipMembers = GetFellowshipMembers();
if (!fellowshipMembers.ContainsKey(player.Guid.Full))
{
log.Warn($"{leader.Name} tried to dismiss {player.Name} from the fellowship, but {player.Name} was not found in the fellowship");
var done = true;
if (player.Fellowship != null)
{
if (player.Fellowship == this)
{
log.Warn($"{player.Name} still has a reference to this fellowship somehow. This shouldn't happen");
done = false;
}
else
log.Warn($"{player.Name} has a reference to a different fellowship. {leader.Name} is possibly sending crafted data!");
}
if (done) return;
}
foreach (var member in fellowshipMembers.Values)
{
member.Session.Network.EnqueueSend(new GameEventFellowshipDismiss(member.Session, player));
member.Session.Network.EnqueueSend(new GameMessageSystemChat($"{player.Name} dismissed from fellowship", ChatMessageType.Fellowship));
}
FellowshipMembers.Remove(player.Guid.Full);
player.Fellowship = null;
CalculateXPSharing();
UpdateAllMembers();
}
private void UpdateAllMembers()
{
var fellowshipMembers = GetFellowshipMembers();
foreach (var member in fellowshipMembers.Values)
member.Session.Network.EnqueueSend(new GameEventFellowshipFullUpdate(member.Session));
}
private void SendMessageAndUpdate(string message)
{
var fellowshipMembers = GetFellowshipMembers();
foreach (var member in fellowshipMembers.Values)
{
member.Session.Network.EnqueueSend(new GameMessageSystemChat(message, ChatMessageType.Fellowship));
member.Session.Network.EnqueueSend(new GameEventFellowshipFullUpdate(member.Session));
}
}
private void SendBroadcastAndUpdate(string message)
{
var fellowshipMembers = GetFellowshipMembers();
foreach (var member in fellowshipMembers.Values)
{
member.Session.Network.EnqueueSend(new GameEventChannelBroadcast(member.Session, Channel.FellowBroadcast, "", message));
member.Session.Network.EnqueueSend(new GameEventFellowshipFullUpdate(member.Session));
}
}
public void BroadcastToFellow(string message)
{
var fellowshipMembers = GetFellowshipMembers();
foreach (var member in fellowshipMembers.Values)
member.Session.Network.EnqueueSend(new GameEventChannelBroadcast(member.Session, Channel.FellowBroadcast, "", message));
}
public void TellFellow(WorldObject sender, string message)
{
var fellowshipMembers = GetFellowshipMembers();
foreach (var member in fellowshipMembers.Values)
member.Session.Network.EnqueueSend(new GameEventChannelBroadcast(member.Session, Channel.Fellow, sender.Name, message));
}
private void SendWeenieErrorWithStringAndUpdate(WeenieErrorWithString error, string message)
{
var fellowshipMembers = GetFellowshipMembers();
foreach (var member in fellowshipMembers.Values)
{
member.Session.Network.EnqueueSend(new GameEventWeenieErrorWithString(member.Session, error, message));
member.Session.Network.EnqueueSend(new GameEventFellowshipFullUpdate(member.Session));
}
}
public void QuitFellowship(Player player, bool disband)
{
if (player == null) return;
if (player.Guid.Full == FellowshipLeaderGuid)
{
if (disband)
{
var fellowshipMembers = GetFellowshipMembers();
foreach (var member in fellowshipMembers.Values)
{
member.Session.Network.EnqueueSend(new GameEventFellowshipDisband(member.Session));
if (ShareLoot)
{
member.Session.Network.EnqueueSend(new GameMessageSystemChat("You no longer have permission to loot anyone else's kills.", ChatMessageType.Broadcast));
// you would expect this occur, but it did not in retail pcaps
//foreach (var fellow in fellowshipMembers.Values)
// member.Session.Network.EnqueueSend(new GameMessageSystemChat($"{fellow.Name} does not have permission to loot your kills.", ChatMessageType.Broadcast));
}
member.Fellowship = null;
}
}
else
{
FellowshipMembers.Remove(player.Guid.Full);
if (IsLocked)
{
var timestamp = (int)Time.GetUnixTime();
if (!DepartedMembers.TryAdd(player.Guid.Full, timestamp))
DepartedMembers[player.Guid.Full] = timestamp;
}
player.Fellowship = null;
player.Session.Network.EnqueueSend(new GameEventFellowshipQuit(player.Session, player.Guid.Full));
player.Session.Network.EnqueueSend(new GameMessageSystemChat("You no longer have permission to loot anyone else's kills.", ChatMessageType.Broadcast));
var fellowshipMembers = GetFellowshipMembers();
foreach (var member in fellowshipMembers.Values)
{
member.Session.Network.EnqueueSend(new GameEventFellowshipQuit(member.Session, player.Guid.Full));
if (ShareLoot)
{
member.Session.Network.EnqueueSend(new GameMessageSystemChat($"You have lost permission to loot the kills of {player.Name}.", ChatMessageType.Broadcast));
player.Session.Network.EnqueueSend(new GameMessageSystemChat($"{member.Name} does not have permission to loot your kills.", ChatMessageType.Broadcast));
}
}
AssignNewLeader(null, null);
CalculateXPSharing();
}
}
else if (!disband)
{
FellowshipMembers.Remove(player.Guid.Full);
if (IsLocked)
{
var timestamp = (int)Time.GetUnixTime();
if (!DepartedMembers.TryAdd(player.Guid.Full, timestamp))
DepartedMembers[player.Guid.Full] = timestamp;
}
player.Session.Network.EnqueueSend(new GameEventFellowshipQuit(player.Session, player.Guid.Full));
var fellowshipMembers = GetFellowshipMembers();
foreach (var member in fellowshipMembers.Values)
{
member.Session.Network.EnqueueSend(new GameEventFellowshipQuit(member.Session, player.Guid.Full));
if (ShareLoot)
{
member.Session.Network.EnqueueSend(new GameMessageSystemChat($"You have lost permission to loot the kills of {player.Name}.", ChatMessageType.Broadcast));
player.Session.Network.EnqueueSend(new GameMessageSystemChat($"{member.Name} does not have permission to loot your kills.", ChatMessageType.Broadcast));
}
}
player.Fellowship = null;
CalculateXPSharing();
}
}
public void AssignNewLeader(Player oldLeader, Player newLeader)
{
if (newLeader != null)
{
FellowshipLeaderGuid = newLeader.Guid.Full;
if (oldLeader != null)
oldLeader.Session.Network.EnqueueSend(new GameEventWeenieErrorWithString(oldLeader.Session, WeenieErrorWithString.YouHavePassedFellowshipLeadershipTo_, newLeader.Name));
SendWeenieErrorWithStringAndUpdate(WeenieErrorWithString._IsNowLeaderOfFellowship, newLeader.Name);
}
else
{
// leader has dropped, assign new random leader
var fellowshipMembers = GetFellowshipMembers();
if (fellowshipMembers.Count == 0) return;
var rng = ThreadSafeRandom.Next(0, fellowshipMembers.Count - 1);
var fellowGuids = fellowshipMembers.Keys.ToList();
FellowshipLeaderGuid = fellowGuids[rng];
var newLeaderName = fellowshipMembers[FellowshipLeaderGuid].Name;
if (oldLeader != null)
oldLeader.Session.Network.EnqueueSend(new GameEventWeenieErrorWithString(oldLeader.Session, WeenieErrorWithString.YouHavePassedFellowshipLeadershipTo_, newLeaderName));
SendWeenieErrorWithStringAndUpdate(WeenieErrorWithString._IsNowLeaderOfFellowship, newLeaderName);
}
}
public void UpdateOpenness(bool isOpen)
{
Open = isOpen;
var openness = Open ? WeenieErrorWithString._IsNowOpenFellowship : WeenieErrorWithString._IsNowClosedFellowship;
SendWeenieErrorWithStringAndUpdate(openness, FellowshipName);
}
public void UpdateLock(bool isLocked, string lockName)
{
// Unlocking a fellowship is not possible without disbanding in retail worlds, so in all likelihood, this is only firing for fellowships being locked by emotemanager
IsLocked = isLocked;
if (string.IsNullOrWhiteSpace(lockName))
lockName = "Undefined";
if (isLocked)
{
Open = false;
DepartedMembers.Clear();
var timestamp = Time.GetUnixTime();
if (!FellowshipLocks.TryAdd(lockName, new FellowshipLockData(timestamp)))
FellowshipLocks[lockName].UpdateTimestamp(timestamp);
SendBroadcastAndUpdate("Your fellowship is now locked. You may not recruit new members. If you leave the fellowship, you have 15 minutes to be recruited back into the fellowship.");
}
else
{
// Unlocking a fellowship is not possible without disbanding in retail worlds, so in all likelihood, this never occurs
DepartedMembers.Clear();
FellowshipLocks.Remove(lockName);
SendBroadcastAndUpdate("Your fellowship is now unlocked.");
}
}
///
/// Calculates fellowship XP sharing (ShareXP, EvenShare) from fellow levels
///
private void CalculateXPSharing()
{
// - If all members of the fellowship are level 50 or above, all members will share XP equally
// - If all members of the fellowship are within 5 levels of the founder, XP will be shared equally
// - If members are all within ten levels of the founder, XP will be shared proportionally.
var fellows = GetFellowshipMembers();
var allEvenShareLevel = PropertyManager.GetLong("fellowship_even_share_level").Item;
var allOverEvenShareLevel = !fellows.Values.Any(f => (f.Level ?? 1) < allEvenShareLevel);
if (allOverEvenShareLevel)
{
ShareXP = DesiredShareXP;
EvenShare = true;
return;
}
var leader = PlayerManager.GetOnlinePlayer(FellowshipLeaderGuid);
if (leader == null)
return;
var maxLevelDiff = fellows.Values.Max(f => Math.Abs((leader.Level ?? 1) - (f.Level ?? 1)));
if (maxLevelDiff <= 5)
{
ShareXP = DesiredShareXP;
EvenShare = true;
}
else if (maxLevelDiff <= 10)
{
ShareXP = DesiredShareXP;
EvenShare = false;
}
else
{
ShareXP = false;
EvenShare = false;
}
}
///
/// Splits XP amongst fellowship members, depending on XP type and fellow settings
///
/// The input amount of XP
/// The type of XP (quest XP is handled differently)
/// The fellowship member who originated the XP
public void SplitXp(ulong amount, XpType xpType, ShareType shareType, Player player)
{
// https://asheron.fandom.com/wiki/Announcements_-_2002/02_-_Fever_Dreams#Letter_to_the_Players_1
var fellowshipMembers = GetFellowshipMembers();
shareType &= ~ShareType.Fellowship;
// quest turn-ins: flat share (retail default)
if (xpType == XpType.Quest && !PropertyManager.GetBool("fellow_quest_bonus").Item)
{
var perAmount = (long)amount / fellowshipMembers.Count;
foreach (var member in fellowshipMembers.Values)
{
var fellowXpType = player == member ? XpType.Quest : XpType.Fellowship;
member.GrantXP(perAmount, fellowXpType, shareType);
}
}
// divides XP evenly to all the sharable fellows within level range,
// but with a significant boost to the amount of xp, based on # of fellowship members
else if (EvenShare)
{
var totalAmount = (ulong)Math.Round(amount * GetMemberSharePercent());
foreach (var member in fellowshipMembers.Values)
{
var shareAmount = (ulong)Math.Round(totalAmount * GetDistanceScalar(player, member, xpType));
var fellowXpType = player == member ? xpType : XpType.Fellowship;
member.GrantXP((long)shareAmount, fellowXpType, shareType);
}
return;
}
// divides XP to all sharable fellows within level range
// based on each fellowship member's level
else
{
var levelXPSum = fellowshipMembers.Values.Select(p => p.GetXPToNextLevel(p.Level.Value)).Sum();
foreach (var member in fellowshipMembers.Values)
{
var levelXPScale = (double)member.GetXPToNextLevel(member.Level.Value) / levelXPSum;
var playerTotal = (ulong)Math.Round(amount * levelXPScale * GetDistanceScalar(player, member, xpType));
var fellowXpType = player == member ? xpType : XpType.Fellowship;
member.GrantXP((long)playerTotal, fellowXpType, shareType);
}
}
}
///
/// Splits luminance amongst fellowship members, depending on XP type and fellow settings
///
/// The input amount of luminance
/// The type of lumaniance (quest luminance is handled differently)
/// The fellowship member who originated the luminance
public void SplitLuminance(ulong amount, XpType xpType, ShareType shareType, Player player)
{
// https://asheron.fandom.com/wiki/Announcements_-_2002/02_-_Fever_Dreams#Letter_to_the_Players_1
shareType &= ~ShareType.Fellowship;
if (xpType == XpType.Quest)
{
// quest luminance is not shared
player.GrantLuminance((long)amount, XpType.Quest, shareType);
}
else
{
// pre-filter: evenly divide between luminance-eligible fellows
// updated: retail supposedly did not do this
//var shareableMembers = GetFellowshipMembers().Values.Where(f => f.MaximumLuminance != null).ToList();
var shareableMembers = GetFellowshipMembers().Values.ToList();
if (shareableMembers.Count == 0)
return;
var perAmount = (long)Math.Round((double)(amount / (ulong)shareableMembers.Count));
// further filter to fellows in radar range
var inRange = shareableMembers.Intersect(WithinRange(player, true)).ToList();
foreach (var member in inRange)
{
if (member.MaximumLuminance == null) continue;
var fellowXpType = player == member ? xpType : XpType.Fellowship;
member.GrantLuminance(perAmount, fellowXpType, shareType);
}
}
}
internal double GetMemberSharePercent()
{
var fellowshipMembers = GetFellowshipMembers();
switch (fellowshipMembers.Count)
{
case 1:
return 1.0;
case 2:
return .75;
case 3:
return .6;
case 4:
return .55;
case 5:
return .5;
case 6:
return .45;
case 7:
return .4;
case 8:
return .35;
case 9:
return .3;
// TODO: handle fellowship mods with > 9 players?
}
return 1.0;
}
public const int MaxDistance = 600;
///
/// Returns the amount to scale the XP for a fellow
/// based on distance from the earner
///
public double GetDistanceScalar(Player earner, Player fellow, XpType xpType)
{
if (earner == null || fellow == null)
return 0.0f;
if (xpType == XpType.Quest)
return 1.0f;
// https://asheron.fandom.com/wiki/Announcements_-_2004/01_-_Mirror,_Mirror#Rollout_Article
// If they are indoors while you are outdoors, or vice-versa.
if (earner.Location.Indoors != fellow.Location.Indoors)
return 0.0f;
// If you are both indoors but in different landblocks.
if (earner.Location.Indoors && fellow.Location.Indoors && earner.Location.Landblock != fellow.Location.Landblock)
return 0.0f;
var dist = earner.Location.Distance2D(fellow.Location);
if (dist >= MaxDistance * 2.0f)
return 0.0f;
if (dist <= MaxDistance)
return 1.0f;
var scalar = 1.0f - (dist - MaxDistance) / MaxDistance;
return Math.Max(0.0f, scalar);
}
///
/// Returns fellows within radar range (75 units outdoors, 25 units indoors)
///
public List WithinRange(Player player, bool includeSelf = false)
{
var fellows = GetFellowshipMembers();
var landblockRange = PropertyManager.GetBool("fellow_kt_landblock").Item;
var results = new List();
foreach (var fellow in fellows.Values)
{
if (player == fellow && !includeSelf)
continue;
var shareable = player == fellow || landblockRange ?
player.CurrentLandblock == fellow.CurrentLandblock || player.Location.DistanceTo(fellow.Location) <= 192.0f :
player.Location.Distance2D(fellow.Location) <= player.CurrentRadarRange && player.ObjMaint.VisibleObjectsContainsKey(fellow.Guid.Full); // 2d visible distance / radar range?
if (shareable)
results.Add(fellow);
}
return results;
}
///
/// Called when someone in the fellowship levels up
///
public void OnFellowLevelUp(Player player)
{
CalculateXPSharing();
var fellowshipMembers = GetFellowshipMembers();
foreach (var fellow in fellowshipMembers.Values)
{
if (fellow == player)
continue;
fellow.Session.Network.EnqueueSend(new GameMessageSystemChat($"{player.Name} is now level {player.Level}!", ChatMessageType.Broadcast));
}
}
public void OnVitalUpdate(Player player)
{
// cap max update interval?
var fellowshipMembers = GetFellowshipMembers();
foreach (var fellow in fellowshipMembers.Values)
{
if (fellow.FellowshipPanelOpen)
fellow.Session.Network.EnqueueSend(new GameEventFellowshipUpdateFellow(fellow.Session, player, ShareLoot, FellowUpdateType.Vitals));
}
}
public void OnDeath(Player player)
{
var fellowshipMembers = GetFellowshipMembers();
foreach (var fellow in fellowshipMembers.Values)
{
if (fellow != player)
fellow.Session.Network.EnqueueSend(new GameMessageSystemChat($"Your fellow {player.Name} has died!", ChatMessageType.Broadcast));
}
}
public Dictionary GetFellowshipMembers()
{
var results = new Dictionary();
var dropped = new HashSet();
foreach (var kvp in FellowshipMembers)
{
var playerGuid = kvp.Key;
var playerRef = kvp.Value;
playerRef.TryGetTarget(out var player);
if (player != null && player.Session != null && player.Session.Player != null && player.Fellowship != null)
results.Add(playerGuid, player);
else
dropped.Add(playerGuid);
}
// TODO: process dropped list
if (dropped.Count > 0)
ProcessDropList(FellowshipMembers, dropped);
return results;
}
public void ProcessDropList(Dictionary> fellowshipMembers, HashSet fellowGuids)
{
foreach (var fellowGuid in fellowGuids)
{
var offlinePlayer = PlayerManager.FindByGuid(fellowGuid);
var offlineName = offlinePlayer != null ? offlinePlayer.Name : "NULL";
log.Warn($"Dropped fellow: {offlineName}");
fellowshipMembers.Remove(fellowGuid);
}
if (fellowGuids.Contains(FellowshipLeaderGuid))
AssignNewLeader(null, null);
CalculateXPSharing();
UpdateAllMembers();
}
}
public static class FellowshipExtensions
{
private static readonly HashComparer hashComparer = new HashComparer(32);
public static void Write(this BinaryWriter writer, Dictionary departedFellows)
{
PackableHashTable.WriteHeader(writer, departedFellows.Count, hashComparer.NumBuckets);
var sorted = new SortedDictionary(departedFellows, hashComparer);
foreach (var departed in sorted)
{
writer.Write(departed.Key);
writer.Write(departed.Value);
}
}
}
}