using System;
using System.Collections.Generic;
using ACE.Database;
using ACE.Entity;
using ACE.Entity.Enum.Properties;
using ACE.Server.Entity;
using ACE.Server.Entity.Actions;
using ACE.Server.Factories;
using ACE.Server.Network.GameEvent.Events;
using ACE.Server.Network.GameMessages.Messages;
using ACE.Server.WorldObjects;
namespace ACE.Server.Managers
{
///
/// Allegiance helper methods
///
public class AllegianceManager
{
///
/// A mapping of all loaded Allegiance GUIDs => their Allegiances
///
public static readonly Dictionary Allegiances = new Dictionary();
///
/// A mapping of all Players on the server => their AllegianceNodes
///
public static readonly Dictionary Players = new Dictionary();
///
/// Returns the monarch for a player
///
public static IPlayer GetMonarch(IPlayer player)
{
if (player.MonarchId == null)
return player;
var monarch = PlayerManager.FindByGuid(player.MonarchId.Value);
return monarch ?? player;
}
///
/// Returns the full allegiance structure for any player
///
/// A player at any level of an allegiance
public static Allegiance GetAllegiance(IPlayer player)
{
if (player == null) return null;
var monarch = GetMonarch(player);
if (monarch == null) return null;
// is this allegiance already loaded / cached?
if (Players.ContainsKey(monarch.Guid))
return Players[monarch.Guid].Allegiance;
// try to load biota
var allegianceID = DatabaseManager.Shard.BaseDatabase.GetAllegianceID(monarch.Guid.Full);
var biota = allegianceID != null ? DatabaseManager.Shard.BaseDatabase.GetBiota(allegianceID.Value) : null;
Allegiance allegiance;
if (biota != null)
{
var entityBiota = ACE.Database.Adapter.BiotaConverter.ConvertToEntityBiota(biota);
allegiance = new Allegiance(entityBiota);
}
else
allegiance = new Allegiance(monarch.Guid);
if (allegiance.TotalMembers == 1)
return null;
if (biota == null)
{
allegiance = WorldObjectFactory.CreateNewWorldObject("allegiance") as Allegiance;
allegiance.MonarchId = monarch.Guid.Full;
allegiance.Init(monarch.Guid);
allegiance.SaveBiotaToDatabase();
}
AddPlayers(allegiance);
//if (!Allegiances.ContainsKey(allegiance.Guid))
//Allegiances.Add(allegiance.Guid, allegiance);
Allegiances[allegiance.Guid] = allegiance;
return allegiance;
}
///
/// Returns the AllegianceNode for a Player
///
public static AllegianceNode GetAllegianceNode(IPlayer player)
{
Players.TryGetValue(player.Guid, out var allegianceNode);
return allegianceNode;
}
///
/// Returns a list of all players under a monarch
///
public static List FindAllPlayers(ObjectGuid monarchGuid)
{
return PlayerManager.FindAllByMonarch(monarchGuid);
}
///
/// Loads the Allegiance and AllegianceNode for a Player
///
public static void LoadPlayer(IPlayer player)
{
if (player == null) return;
player.Allegiance = GetAllegiance(player);
player.AllegianceNode = GetAllegianceNode(player);
// TODO: update chat channels for online players here?
}
///
/// Called when a player joins/exits an Allegiance
///
public static void Rebuild(Allegiance allegiance)
{
if (allegiance == null) return;
RemoveCache(allegiance);
// rebuild allegiance
allegiance = GetAllegiance(allegiance.Monarch.Player);
// relink players
foreach (var member in allegiance.Members.Keys)
{
var player = PlayerManager.FindByGuid(member);
if (player == null) continue;
LoadPlayer(player);
}
// update dynamic properties
allegiance.UpdateProperties();
}
///
/// Appends the Players lookup table with the members of an Allegiance
///
public static void AddPlayers(Allegiance allegiance)
{
foreach (var member in allegiance.Members)
{
var player = member.Key;
var allegianceNode = member.Value;
if (!Players.ContainsKey(player))
Players.Add(player, allegianceNode);
else
Players[player] = allegianceNode;
}
}
///
/// Removes an Allegiance from the Players lookup table cache
///
public static void RemoveCache(Allegiance allegiance)
{
foreach (var member in allegiance.Members)
Players.Remove(member.Key);
}
///
/// The maximum amount of leadership / loyalty
///
public static float SkillCap = 291.0f;
///
/// The maximum amount of realtime hours sworn to patron
///
public static float RealCap = 730.0f;
///
/// The maximum amount of in-game hours sworn to patron
///
public static float GameCap = 720.0f;
// This function can be called from multi-threaded operations
// We must add thread safety to prevent AllegianceManager corruption
// We must also protect against cross-thread operations on vassal/patron (non-concurrent collections)
public static void PassXP(AllegianceNode vassalNode, ulong amount, bool direct)
{
WorldManager.EnqueueAction(new ActionEventDelegate(() => DoPassXP(vassalNode, amount, direct)));
}
private static void DoPassXP(AllegianceNode vassalNode, ulong amount, bool direct)
{
// http://asheron.wikia.com/wiki/Allegiance_Experience
// Pre-patch:
// Vassal-to-patron pass-up has no effective cap, but patron-to-grandpatron pass-up caps with an effective Loyalty of 175.
// If you're sworn for 10 days to the same patron, the base Loyalty required to maximize the pass-up from your vassal through you to your patron is only 88.
// Take into account level 7 enchantments, and you can see how there's practically no need to spend XP on the skill, due to the ease in reaching the cap.
// Leadership is arguably worse off. In theory, you need to train Leadership and spend XP on it in order to get maximum results.
// However, effective Leadership is multiplied by two factors: how many vassals you have, and how long they have been sworn to you, with the emphasis on the number of vassals you have.
// The effect of Leadership on pass-up caps at around 165 effective Leadership, or 83 base Leadership before the modifier.
// The end result of this is that if you have two active vassals and you can get 10 mules sworn underneath you for an average of 5 in-game days,
// you never need to raise your Leadership beyond 83. Again, take into account level 7 enchantments and you can see why few people even bother training the skill. It's just too easy to reach the cap.
// Post-patch:
// - Leadership and Loyalty are not based on Self attribute
// - Effective Loyalty is still modified by the time you have spent sworn to your patron
// - Effective Leadership is still modified by number of vassals and average time that they have been sworn to you,
// but the emphasis is on the "time sworn" side and not on the "number of vassals" side. In fact, the vassals
// required to achieve the maximum benefit has been decreased from 12 to 4. This is to reduce the incentive of having non-playing vassals.
// - For both Loyalty and Leadership, the time sworn modifier will now be a factor of both in-game time and real time.
// - Most importantly, Leadership and Loyalty no longer "cap"
// XP pass-up:
// - New minimums and maximums
// - Vassal-to-patron pass-up will have a minimum of 25% of earned XP, and a maximum of 90% of earned XP.
// Under the old system, the minimum was about 9% of earned XP, and the effective maximum was somewhere near 44% of earned XP.
// - Patron-to-grandpatron pass-up will have a minimum of 0% of XP passed-up by the patron's vassal, and a maximum of 10% of passed-up XP.
// Under the old system, the minimum was about 30% and the maximum was about 94%.
// Original system: up to January 12, 2004
// Follow-up: all XP instead of just kill XP: October 2009
// Formulas:
// http://asheron.wikia.com/wiki/XP_Passup
// Thanks for Xerxes of Thistledown, who verified accuracy over four months of testing and research!
// Generated % - Percentage of XP passed to the patron through the vassal's earned XP (hunting and most quests).
// Received % - Percentage of XP that patron will receive from his vassal's Generated XP.
// Passup % - Percentage of XP actually received by patron from vassal's earned XP (hunting and most quests).
// Generated % = 50.0 + 22.5 * (loyalty / 291) * (1.0 + (RT/730) * (IG/720))
// Received % = 50.0 + 22.5 * (leadership / 291) * (1.0 + V * (RT2/730) * (IG2/720))
// Passup % = Generated% * Received% / 100.0
// Where:
// Loyalty = Buffed Loyalty (291 max)
// Leadership = Buffed Leadership (291 max)
// RT = actual real time sworn to patron in days (730 max)
// IG = actual in-game time sworn to patron in hours (720 max)
// RT2 = average real time sworn to patron for all vassals in days (730 max)
// IG2 = average in-game time sworn to patron for all vassals in hours (720 max)
// V = vassal factor(1 = 0.25, 2 = 0.50, 3 = 0.75, 4 + = 1.00) (1.0 max)
var patronNode = vassalNode.Patron;
if (patronNode == null)
return;
var vassal = vassalNode.Player;
var patron = patronNode.Player;
if (!vassal.ExistedBeforeAllegianceXpChanges)
return;
var loyalty = Math.Min(vassal.GetCurrentLoyalty(), SkillCap);
var leadership = Math.Min(patron.GetCurrentLeadership(), SkillCap);
var timeReal = Math.Min(RealCap, RealCap);
var timeGame = Math.Min(GameCap, GameCap);
var timeRealAvg = Math.Min(RealCap, RealCap);
var timeGameAvg = Math.Min(GameCap, GameCap);
var vassalFactor = Math.Min(0.25f * patronNode.TotalVassals, 1.0f);
var factor1 = direct ? 50.0f : 16.0f;
var factor2 = direct ? 22.5f : 8.0f;
var generated = (factor1 + factor2 * (loyalty / SkillCap) * (1.0f + (timeReal / RealCap) * (timeGame / GameCap))) * 0.01f;
var received = (factor1 + factor2 * (leadership / SkillCap) * (1.0f + vassalFactor * (timeRealAvg / RealCap) * (timeGameAvg / GameCap))) * 0.01f;
var passup = generated * received;
var generatedAmount = (ulong)(amount * generated);
var passupAmount = (ulong)(amount * passup);
/*Console.WriteLine("---");
Console.WriteLine("AllegianceManager.PassXP(" + amount + ")");
Console.WriteLine("Vassal: " + vassal.Name);
Console.WriteLine("Patron: " + patron.Name);
Console.WriteLine("Generated: " + Math.Round(generated * 100, 2) + "%");
Console.WriteLine("Received: " + Math.Round(received * 100, 2) + "%");
Console.WriteLine("Passup: " + Math.Round(passup * 100, 2) + "%");
Console.WriteLine("Generated amount: " + generatedAmount);
Console.WriteLine("Passup amount: " + passupAmount);*/
if (passupAmount > 0)
{
//vassal.CPTithed += generatedAmount;
//patron.CPCached += passupAmount;
//patron.CPPoolToUnload += passupAmount;
vassal.AllegianceXPGenerated += generatedAmount;
if (PropertyManager.GetBool("offline_xp_passup_limit").Item)
patron.AllegianceXPCached = Math.Min(patron.AllegianceXPCached + passupAmount, uint.MaxValue);
else
patron.AllegianceXPCached += passupAmount;
var onlinePatron = PlayerManager.GetOnlinePlayer(patron.Guid);
if (onlinePatron != null)
onlinePatron.AddAllegianceXP();
// call recursively
DoPassXP(patronNode, passupAmount, false);
}
}
///
/// Updates the Allegiance tree structure when a new player joins
///
/// The vassal swearing into the Allegiance
public static void OnSwearAllegiance(Player vassal)
{
if (vassal == null) return;
// was this vassal previously a Monarch?
if (vassal.Allegiance != null)
RemoveCache(vassal.Allegiance);
// rebuild the new combined structure
var allegiance = GetAllegiance(vassal);
Rebuild(allegiance);
LoadPlayer(vassal);
// maintain approved vassals list
if (allegiance != null && allegiance.HasApprovedVassal(vassal.Guid.Full))
allegiance.RemoveApprovedVassal(vassal.Guid.Full);
}
///
/// Updates the Allegiance tree structure when a member leaves
///
/// The player initiating the break request
/// The patron or vassal of the self player
public static void OnBreakAllegiance(IPlayer self, IPlayer target)
{
// remove the previous allegiance structure
if (self != null) // ??
RemoveCache(self.Allegiance);
// rebuild for self and target
var selfAllegiance = GetAllegiance(self);
var targetAllegiance = GetAllegiance(target);
Rebuild(selfAllegiance);
Rebuild(targetAllegiance);
LoadPlayer(self);
LoadPlayer(target);
HandleNoAllegiance(self);
HandleNoAllegiance(target);
}
public static void HandleNoAllegiance(IPlayer player)
{
if (player == null || player.Allegiance != null)
return;
var onlinePlayer = PlayerManager.GetOnlinePlayer(player.Guid);
var updated = false;
if (player.MonarchId != null)
{
player.UpdateProperty(PropertyInstanceId.Monarch, null, true);
updated = true;
}
if (player.AllegianceRank != null)
{
player.AllegianceRank = null;
if (onlinePlayer != null)
onlinePlayer.Session.Network.EnqueueSend(new GameMessagePrivateUpdatePropertyInt(onlinePlayer, PropertyInt.AllegianceRank, 0));
updated = true;
}
if (updated)
player.SaveBiotaToDatabase();
if (onlinePlayer != null)
onlinePlayer.Session.Network.EnqueueSend(new GameEventAllegianceUpdate(onlinePlayer.Session, onlinePlayer.Allegiance, onlinePlayer.AllegianceNode), new GameEventAllegianceAllegianceUpdateDone(onlinePlayer.Session));
}
public static Allegiance FindAllegiance(uint allegianceID)
{
Allegiances.TryGetValue(new ObjectGuid(allegianceID), out var allegiance);
return allegiance;
}
// This function is called from a database callback.
// We must add thread safety to prevent AllegianceManager corruption
public static void HandlePlayerDelete(uint playerGuid)
{
WorldManager.EnqueueAction(new ActionEventDelegate(() => DoHandlePlayerDelete(playerGuid)));
}
private static void DoHandlePlayerDelete(uint playerGuid)
{
var player = PlayerManager.FindByGuid(playerGuid);
if (player == null)
{
Console.WriteLine($"AllegianceManager.HandlePlayerDelete({playerGuid:X8}): couldn't find player guid");
return;
}
var allegiance = GetAllegiance(player);
if (allegiance == null) return;
allegiance.Members.TryGetValue(player.Guid, out var allegianceNode);
var players = new List() { player };
if (player.PatronId != null)
{
var patron = PlayerManager.FindByGuid(player.PatronId.Value);
if (patron != null)
players.Add(patron);
}
player.PatronId = null;
player.UpdateProperty(PropertyInstanceId.Monarch, null, true);
// vassals now become monarchs...
foreach (var vassalNode in allegianceNode.Vassals.Values)
{
var vassal = PlayerManager.FindByGuid(vassalNode.PlayerGuid);
if (vassal == null) continue;
vassal.PatronId = null;
vassal.UpdateProperty(PropertyInstanceId.Monarch, null, true);
// walk the allegiance tree from this node, update monarch ids
vassalNode.Walk((node) =>
{
node.Player.UpdateProperty(PropertyInstanceId.Monarch, vassalNode.PlayerGuid.Full, true);
node.Player.SaveBiotaToDatabase();
}, false);
players.Add(vassal);
}
RemoveCache(allegiance);
// rebuild for those directly involved
foreach (var p in players)
Rebuild(GetAllegiance(p));
foreach (var p in players)
LoadPlayer(p);
foreach (var p in players)
HandleNoAllegiance(p);
// save immediately?
foreach (var p in players)
p.SaveBiotaToDatabase();
foreach (var p in players)
{
Player.CheckAllegianceHouse(p.Guid);
var newAllegiance = GetAllegiance(p);
if (newAllegiance != null)
newAllegiance.Monarch.Walk((node) => Player.CheckAllegianceHouse(node.PlayerGuid), false);
}
}
}
}