using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using ACE.Common;
using ACE.Common.Extensions;
using ACE.Database;
using ACE.DatLoader;
using ACE.Entity;
using ACE.Entity.Enum;
using ACE.Entity.Enum.Properties;
using ACE.Entity.Models;
using ACE.Server.Entity;
using ACE.Server.Entity.Actions;
using ACE.Server.Factories;
using ACE.Server.Factories.Enum;
using ACE.Server.Managers;
using ACE.Server.Network.GameEvent.Events;
using ACE.Server.Network.GameMessages.Messages;
using log4net;
using Position = ACE.Entity.Position;
using Spell = ACE.Server.Entity.Spell;
namespace ACE.Server.WorldObjects.Managers
{
public class EmoteManager
{
private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
public WorldObject WorldObject => _proxy ?? _worldObject;
private WorldObject _worldObject;
private WorldObject _proxy;
///
/// Returns TRUE if this WorldObject is currently busy processing other emotes
///
public bool IsBusy { get; set; }
public int Nested { get; set; }
public bool Debug = false;
public EmoteManager(WorldObject worldObject)
{
_worldObject = worldObject;
}
///
/// Executes an emote
///
/// The parent set of this emote
/// The emote to execute
/// A target object, usually player
/// Only used for passing to further sets
public float ExecuteEmote(PropertiesEmote emoteSet, PropertiesEmoteAction emote, WorldObject targetObject = null)
{
var player = targetObject as Player;
var creature = WorldObject as Creature;
var targetCreature = targetObject as Creature;
var delay = 0.0f;
var emoteType = (EmoteType)emote.Type;
//if (Debug)
//Console.WriteLine($"{WorldObject.Name}.ExecuteEmote({emoteType})");
var text = emote.Message;
switch ((EmoteType)emote.Type)
{
case EmoteType.Act:
// short for 'acting' text
var message = Replace(text, WorldObject, targetObject, emoteSet.Quest);
WorldObject.EnqueueBroadcast(new GameMessageSystemChat(message, ChatMessageType.Broadcast), 30.0f);
break;
case EmoteType.Activate:
if (WorldObject.ActivationTarget > 0)
{
// ActOnUse delay?
var activationTarget = WorldObject.CurrentLandblock?.GetObject(WorldObject.ActivationTarget);
activationTarget?.OnActivate(player ?? WorldObject);
}
else if (WorldObject.GeneratorId.HasValue && WorldObject.GeneratorId > 0) // Fallback to linked generator
{
var linkedGenerator = WorldObject.CurrentLandblock?.GetObject(WorldObject.GeneratorId ?? 0);
linkedGenerator?.OnActivate(WorldObject);
}
break;
case EmoteType.AddCharacterTitle:
// emoteAction.Stat == null for all EmoteType.AddCharacterTitle entries in current db?
if (player != null && emote.Amount != 0)
player.AddTitle((CharacterTitle)emote.Amount);
break;
case EmoteType.AddContract:
// Contracts werent in emote table for 16py, guessing that Stat was used to hold id for contract.
if (player != null && emote.Stat.HasValue && emote.Stat.Value > 0)
player.ContractManager.Add(emote.Stat.Value);
break;
case EmoteType.AdminSpam:
text = Replace(emote.Message, WorldObject, targetObject, emoteSet.Quest);
PlayerManager.BroadcastToChannelFromEmote(Channel.Admin, text);
break;
case EmoteType.AwardLevelProportionalSkillXP:
var min = emote.Min64 ?? emote.Min ?? 0;
var max = emote.Max64 ?? emote.Max ?? 0;
if (player != null)
player.GrantLevelProportionalSkillXP((Skill)emote.Stat, emote.Percent ?? 0, min, max);
break;
case EmoteType.AwardLevelProportionalXP:
min = emote.Min64 ?? emote.Min ?? 0;
max = emote.Max64 ?? emote.Max ?? 0;
if (player != null)
player.GrantLevelProportionalXp(emote.Percent ?? 0, min, max);
break;
case EmoteType.AwardLuminance:
if (player != null)
player.EarnLuminance(emote.Amount64 ?? emote.HeroXP64 ?? 0, XpType.Quest, ShareType.None);
break;
case EmoteType.AwardNoShareXP:
if (player != null)
player.EarnXP(emote.Amount64 ?? emote.Amount ?? 0, XpType.Quest, ShareType.None);
break;
case EmoteType.AwardSkillPoints:
if (player != null)
player.AwardSkillPoints((Skill)emote.Stat, (uint)emote.Amount);
break;
case EmoteType.AwardSkillXP:
if (player != null)
{
if (delay < 1) delay += 1; // because of how AwardSkillXP grants and then raises the skill, ensure delay is at least 1 to allow for processing correctly
player.AwardSkillXP((Skill)emote.Stat, (uint)emote.Amount, true);
}
break;
case EmoteType.AwardTrainingCredits:
if (player != null)
player.AddSkillCredits(emote.Amount ?? 0);
break;
case EmoteType.AwardXP:
if (player != null)
{
var amt = emote.Amount64 ?? emote.Amount ?? 0;
if (amt > 0)
{
player.EarnXP(amt, XpType.Quest, ShareType.All);
}
else if (amt < 0)
{
player.SpendXP(-amt);
}
}
break;
case EmoteType.BLog:
text = Replace(emote.Message, WorldObject, targetObject, emoteSet.Quest);
log.Info($"0x{WorldObject.Guid}:{WorldObject.Name}({WorldObject.WeenieClassId}).EmoteManager.BLog - {text}");
break;
case EmoteType.CastSpell:
if (WorldObject != null)
{
var spell = new Spell((uint)emote.SpellId);
if (spell.NotFound)
{
log.Error($"{WorldObject.Name} ({WorldObject.Guid}) EmoteManager.CastSpell - unknown spell {emote.SpellId}");
break;
}
creature.CheckForHumanPreCast(spell);
var spellTarget = GetSpellTarget(spell, targetObject);
var preCastTime = creature.PreCastMotion(spellTarget);
delay = preCastTime + creature.GetPostCastTime(spell);
var castChain = new ActionChain();
castChain.AddDelaySeconds(preCastTime);
castChain.AddAction(creature, () =>
{
creature.TryCastSpell_WithRedirects(spell, spellTarget, creature);
creature.PostCastMotion();
});
castChain.EnqueueChain();
}
break;
case EmoteType.CastSpellInstant:
if (WorldObject != null)
{
var spell = new Spell((uint)emote.SpellId);
if (!spell.NotFound)
{
var spellTarget = GetSpellTarget(spell, targetObject);
WorldObject.TryCastSpell_WithRedirects(spell, spellTarget, WorldObject);
}
}
break;
case EmoteType.CloseMe:
// animation delay?
if (WorldObject is Container container)
container.Close(null);
else if (WorldObject is Door closeDoor)
closeDoor.Close();
break;
case EmoteType.CreateTreasure:
if (player != null)
{
var treasureTier = emote.WealthRating ?? 1;
var treasureType = (TreasureItemCategory?)emote.TreasureType ?? TreasureItemCategory.Undef;
var treasureClass = (TreasureItemType?)emote.TreasureClass ?? TreasureItemType.Undef;
// Create a dummy treasure profile for passing emote values
var profile = new Database.Models.World.TreasureDeath
{
Tier = treasureTier,
//TreasureType = (uint)treasureType,
LootQualityMod = 0,
ItemChance = 100,
ItemMinAmount = 1,
ItemMaxAmount = 1,
//ItemTreasureTypeSelectionChances = (int)treasureClass,
MagicItemChance = 100,
MagicItemMinAmount = 1,
MagicItemMaxAmount = 1,
//MagicItemTreasureTypeSelectionChances = (int)treasureClass,
MundaneItemChance = 100,
MundaneItemMinAmount = 1,
MundaneItemMaxAmount = 1,
//MundaneItemTypeSelectionChances = (int)treasureClass,
UnknownChances = 21
};
var treasure = LootGenerationFactory.CreateRandomLootObjects(profile, treasureType, treasureClass);
if (treasure != null)
{
player.TryCreateForGive(WorldObject, treasure);
}
}
break;
/* decrements a PropertyInt stat by some amount */
case EmoteType.DecrementIntStat:
// only used by 1 emote in 16PY - check for lower bounds?
if (targetObject != null && emote.Stat != null)
{
var intProperty = (PropertyInt)emote.Stat;
var current = targetObject.GetProperty(intProperty) ?? 0;
current -= emote.Amount ?? 1;
targetObject.SetProperty(intProperty, current);
if (player != null)
player.Session.Network.EnqueueSend(new GameMessagePrivateUpdatePropertyInt(player, intProperty, current));
}
break;
case EmoteType.DecrementMyQuest:
case EmoteType.DecrementQuest:
var questTarget = GetQuestTarget((EmoteType)emote.Type, targetCreature, creature);
if (questTarget != null)
questTarget.QuestManager.Decrement(emote.Message, emote.Amount ?? 1);
break;
case EmoteType.DeleteSelf:
if (player != null)
{
var wo = player.FindObject(WorldObject.Guid.Full, Player.SearchLocations.Everywhere, out _, out Container rootOwner, out bool wasEquipped);
WorldObject.DeleteObject(rootOwner);
}
else
WorldObject.DeleteObject();
break;
case EmoteType.DirectBroadcast:
text = Replace(emote.Message, WorldObject, targetObject, emoteSet.Quest);
if (player != null)
player.Session.Network.EnqueueSend(new GameMessageSystemChat(text, ChatMessageType.Broadcast));
break;
case EmoteType.Enlightenment:
if (player != null)
{
Enlightenment.HandleEnlightenment(WorldObject, player);
}
break;
case EmoteType.EraseMyQuest:
case EmoteType.EraseQuest:
questTarget = GetQuestTarget((EmoteType)emote.Type, targetCreature, creature);
if (questTarget != null)
questTarget.QuestManager.Erase(emote.Message);
break;
case EmoteType.FellowBroadcast:
if (player != null)
{
var fellowship = player.Fellowship;
if (fellowship != null)
{
text = Replace(emote.Message, WorldObject, player, emoteSet.Quest);
fellowship.BroadcastToFellow(text);
}
}
break;
case EmoteType.Generate:
if (WorldObject.IsGenerator)
WorldObject.Generator_Generate();
break;
case EmoteType.Give:
bool success = false;
var stackSize = emote.StackSize ?? 1;
if (player != null && emote.WeenieClassId != null)
{
var motionChain = new ActionChain();
if (!WorldObject.DontTurnOrMoveWhenGiving && creature != null && targetCreature != null)
{
delay = creature.Rotate(targetCreature);
motionChain.AddDelaySeconds(delay);
}
motionChain.AddAction(WorldObject, () => player.GiveFromEmote(WorldObject, emote.WeenieClassId ?? 0, stackSize > 0 ? stackSize : 1, emote.Palette ?? 0, emote.Shade ?? 0));
motionChain.EnqueueChain();
}
break;
/* redirects to the GotoSet category for this action */
case EmoteType.Goto:
// TODO: revisit if nested chains need to back-propagate timers
var gotoSet = GetEmoteSet(EmoteCategory.GotoSet, emote.Message);
ExecuteEmoteSet(gotoSet, targetObject, true);
break;
/* increments a PropertyInt stat by some amount */
case EmoteType.IncrementIntStat:
if (targetObject != null && emote.Stat != null)
{
var intProperty = (PropertyInt)emote.Stat;
var current = targetObject.GetProperty(intProperty) ?? 0;
current += emote.Amount ?? 1;
targetObject.SetProperty(intProperty, current);
if (player != null)
player.Session.Network.EnqueueSend(new GameMessagePrivateUpdatePropertyInt(player, intProperty, current));
}
break;
case EmoteType.IncrementMyQuest:
case EmoteType.IncrementQuest:
questTarget = GetQuestTarget((EmoteType)emote.Type, targetCreature, creature);
if (questTarget != null)
questTarget.QuestManager.Increment(emote.Message, emote.Amount ?? 1);
break;
case EmoteType.InflictVitaePenalty:
if (player != null)
player.InflictVitaePenalty(emote.Amount ?? 5);
break;
case EmoteType.InqAttributeStat:
if (targetCreature != null)
{
var attr = targetCreature.Attributes[(PropertyAttribute)emote.Stat];
if (attr == null && HasValidTestNoQuality(emote.Message))
{
ExecuteEmoteSet(EmoteCategory.TestNoQuality, emote.Message, targetObject, true);
}
else
{
success = attr != null && attr.Current >= (emote.Min ?? int.MinValue) && attr.Current <= (emote.Max ?? int.MaxValue);
ExecuteEmoteSet(success ? EmoteCategory.TestSuccess : EmoteCategory.TestFailure, emote.Message, targetObject, true);
}
}
break;
case EmoteType.InqBoolStat:
if (targetObject != null)
{
var stat = targetObject.GetProperty((PropertyBool)emote.Stat);
if (stat == null && HasValidTestNoQuality(emote.Message))
{
ExecuteEmoteSet(EmoteCategory.TestNoQuality, emote.Message, targetObject, true);
}
else
{
success = stat ?? false;
ExecuteEmoteSet(success ? EmoteCategory.TestSuccess : EmoteCategory.TestFailure, emote.Message, targetObject, true);
}
}
break;
case EmoteType.InqContractsFull:
ExecuteEmoteSet(player != null && player.ContractManager.IsFull ? EmoteCategory.TestSuccess : EmoteCategory.TestFailure, emote.Message, targetObject, true);
break;
case EmoteType.InqEvent:
var started = EventManager.IsEventStarted(emote.Message, WorldObject, targetObject);
ExecuteEmoteSet(started ? EmoteCategory.EventSuccess : EmoteCategory.EventFailure, emote.Message, targetObject, true);
break;
case EmoteType.InqFellowNum:
// unused in PY16 - ensure # of fellows between min-max?
var result = HasValidTestNoFellow(emote.Message) ? EmoteCategory.TestNoFellow : EmoteCategory.NumFellowsFailure;
if (player?.Fellowship != null)
{
var fellows = player.Fellowship.GetFellowshipMembers();
if (fellows.Count < (emote.Min ?? int.MinValue) || fellows.Count > (emote.Max ?? int.MaxValue))
result = EmoteCategory.NumFellowsFailure;
else
result = EmoteCategory.NumFellowsSuccess;
}
ExecuteEmoteSet(result, emote.Message, targetObject, true);
break;
case EmoteType.InqFellowQuest:
if (player != null)
{
if (player.Fellowship != null)
{
var hasQuest = player.Fellowship.QuestManager.HasQuest(emote.Message);
var canSolve = player.Fellowship.QuestManager.CanSolve(emote.Message);
// verify: QuestSuccess = player has quest, and their last completed time + quest minDelta <= currentTime
success = hasQuest && !canSolve;
ExecuteEmoteSet(success ? EmoteCategory.QuestSuccess : EmoteCategory.QuestFailure, emote.Message, targetObject, true);
}
else
ExecuteEmoteSet(EmoteCategory.QuestNoFellow, emote.Message, targetObject, true);
}
break;
case EmoteType.InqFloatStat:
if (targetObject != null)
{
var stat = targetObject.GetProperty((PropertyFloat)emote.Stat);
if (stat == null && HasValidTestNoQuality(emote.Message))
ExecuteEmoteSet(EmoteCategory.TestNoQuality, emote.Message, targetObject, true);
else
{
stat ??= 0.0f;
success = stat >= (emote.MinDbl ?? double.MinValue) && stat <= (emote.MaxDbl ?? double.MaxValue);
ExecuteEmoteSet(success ? EmoteCategory.TestSuccess : EmoteCategory.TestFailure, emote.Message, targetObject, true);
}
}
break;
case EmoteType.InqInt64Stat:
if (targetObject != null)
{
var stat = targetObject.GetProperty((PropertyInt64)emote.Stat);
if (stat == null && HasValidTestNoQuality(emote.Message))
ExecuteEmoteSet(EmoteCategory.TestNoQuality, emote.Message, targetObject, true);
else
{
stat ??= 0;
success = stat >= (emote.Min64 ?? long.MinValue) && stat <= (emote.Max64 ?? long.MaxValue);
ExecuteEmoteSet(success ? EmoteCategory.TestSuccess : EmoteCategory.TestFailure, emote.Message, targetObject, true);
}
}
break;
case EmoteType.InqIntStat:
if (targetObject != null)
{
var stat = targetObject.GetProperty((PropertyInt)emote.Stat);
if (stat == null && HasValidTestNoQuality(emote.Message))
ExecuteEmoteSet(EmoteCategory.TestNoQuality, emote.Message, targetObject, true);
else
{
stat ??= 0;
success = stat >= (emote.Min ?? int.MinValue) && stat <= (emote.Max ?? int.MaxValue);
ExecuteEmoteSet(success ? EmoteCategory.TestSuccess : EmoteCategory.TestFailure, emote.Message, targetObject, true);
}
}
break;
case EmoteType.InqNumCharacterTitles:
if (player != null)
{
var numTitles = player.NumCharacterTitles;
success = numTitles != null && numTitles >= (emote.Min ?? int.MinValue) && numTitles <= (emote.Max ?? int.MaxValue);
ExecuteEmoteSet(success ? EmoteCategory.NumCharacterTitlesSuccess : EmoteCategory.NumCharacterTitlesFailure, emote.Message, targetObject, true);
}
break;
case EmoteType.InqOwnsItems:
if (player != null)
{
var numRequired = emote.StackSize ?? 1;
var items = player.GetInventoryItemsOfWCID(emote.WeenieClassId ?? 0);
items.AddRange(player.GetEquippedObjectsOfWCID(emote.WeenieClassId ?? 0));
var numItems = items.Sum(i => i.StackSize ?? 1);
success = numItems >= numRequired;
ExecuteEmoteSet(success ? EmoteCategory.TestSuccess : EmoteCategory.TestFailure, emote.Message, targetObject, true);
}
break;
case EmoteType.InqPackSpace:
if (player != null)
{
var numRequired = emote.Amount ?? 1;
success = false;
if (numRequired > 10000) // Since emote was not in 16py and we have just the two fields to go on, I will assume you could "mask" the value to pick between free Item Capacity space or free Container Capacity space
{
var freeSpace = player.GetFreeContainerSlots();
success = freeSpace >= (numRequired - 10000);
}
else
{
var freeSpace = player.GetFreeInventorySlots(false); // assuming this was only for main pack. makes things easier at this point.
success = freeSpace >= numRequired;
}
ExecuteEmoteSet(success ? EmoteCategory.TestSuccess : EmoteCategory.TestFailure, emote.Message, targetObject, true);
}
break;
case EmoteType.InqMyQuest:
case EmoteType.InqQuest:
questTarget = GetQuestTarget((EmoteType)emote.Type, targetCreature, creature);
if (questTarget != null)
{
var hasQuest = questTarget.QuestManager.HasQuest(emote.Message);
var canSolve = questTarget.QuestManager.CanSolve(emote.Message);
// verify: QuestSuccess = player has quest, but their quest timer is currently still on cooldown
success = hasQuest && !canSolve;
ExecuteEmoteSet(success ? EmoteCategory.QuestSuccess : EmoteCategory.QuestFailure, emote.Message, targetObject, true);
}
break;
case EmoteType.InqMyQuestBitsOff:
case EmoteType.InqQuestBitsOff:
questTarget = GetQuestTarget((EmoteType)emote.Type, targetCreature, creature);
if (questTarget != null)
{
var hasNoQuestBits = questTarget.QuestManager.HasNoQuestBits(emote.Message, emote.Amount ?? 0);
ExecuteEmoteSet(hasNoQuestBits ? EmoteCategory.QuestSuccess : EmoteCategory.QuestFailure, emote.Message, targetObject, true);
}
break;
case EmoteType.InqMyQuestBitsOn:
case EmoteType.InqQuestBitsOn:
questTarget = GetQuestTarget((EmoteType)emote.Type, targetCreature, creature);
if (questTarget != null)
{
var hasQuestBits = questTarget.QuestManager.HasQuestBits(emote.Message, emote.Amount ?? 0);
ExecuteEmoteSet(hasQuestBits ? EmoteCategory.QuestSuccess : EmoteCategory.QuestFailure, emote.Message, targetObject, true);
}
break;
case EmoteType.InqMyQuestSolves:
case EmoteType.InqQuestSolves:
questTarget = GetQuestTarget((EmoteType)emote.Type, targetCreature, creature);
if (questTarget != null)
{
var questSolves = questTarget.QuestManager.HasQuestSolves(emote.Message, emote.Min, emote.Max);
ExecuteEmoteSet(questSolves ? EmoteCategory.QuestSuccess : EmoteCategory.QuestFailure, emote.Message, targetObject, true);
}
break;
case EmoteType.InqRawAttributeStat:
if (targetCreature != null)
{
var attr = targetCreature.Attributes[(PropertyAttribute)emote.Stat];
if (attr == null && HasValidTestNoQuality(emote.Message))
{
ExecuteEmoteSet(EmoteCategory.TestNoQuality, emote.Message, targetObject, true);
}
else
{
success = attr != null && attr.Base >= (emote.Min ?? int.MinValue) && attr.Base <= (emote.Max ?? int.MaxValue);
ExecuteEmoteSet(success ? EmoteCategory.TestSuccess : EmoteCategory.TestFailure, emote.Message, targetObject, true);
}
}
break;
case EmoteType.InqRawSecondaryAttributeStat:
if (targetCreature != null)
{
var vital = targetCreature.Vitals[(PropertyAttribute2nd)emote.Stat];
if (vital == null && HasValidTestNoQuality(emote.Message))
{
ExecuteEmoteSet(EmoteCategory.TestNoQuality, emote.Message, targetObject, true);
}
else
{
success = vital != null && vital.Base >= (emote.Min ?? int.MinValue) && vital.Base <= (emote.Max ?? int.MaxValue);
ExecuteEmoteSet(success ? EmoteCategory.TestSuccess : EmoteCategory.TestFailure, emote.Message, targetObject, true);
}
}
break;
case EmoteType.InqRawSkillStat:
if (targetCreature != null)
{
var skill = targetCreature.GetCreatureSkill((Skill)emote.Stat);
if (skill == null && HasValidTestNoQuality(emote.Message))
{
ExecuteEmoteSet(EmoteCategory.TestNoQuality, emote.Message, targetObject, true);
}
else
{
success = skill != null && skill.Base >= (emote.Min ?? int.MinValue) && skill.Base <= (emote.Max ?? int.MaxValue);
ExecuteEmoteSet(success ? EmoteCategory.TestSuccess : EmoteCategory.TestFailure, emote.Message, targetObject, true);
}
}
break;
case EmoteType.InqSecondaryAttributeStat:
if (targetCreature != null)
{
var vital = targetCreature.Vitals[(PropertyAttribute2nd)emote.Stat];
if (vital == null && HasValidTestNoQuality(emote.Message))
{
ExecuteEmoteSet(EmoteCategory.TestNoQuality, emote.Message, targetObject, true);
}
else
{
success = vital != null && vital.Current >= (emote.Min ?? int.MinValue) && vital.Current <= (emote.Max ?? int.MaxValue);
ExecuteEmoteSet(success ? EmoteCategory.TestSuccess : EmoteCategory.TestFailure, emote.Message, targetObject, true);
}
}
break;
case EmoteType.InqSkillSpecialized:
if (targetCreature != null)
{
var skill = targetCreature.GetCreatureSkill((Skill)emote.Stat);
if (skill == null && HasValidTestNoQuality(emote.Message))
{
ExecuteEmoteSet(EmoteCategory.TestNoQuality, emote.Message, targetObject, true);
}
else
{
success = skill != null && skill.AdvancementClass == SkillAdvancementClass.Specialized;
ExecuteEmoteSet(success ? EmoteCategory.TestSuccess : EmoteCategory.TestFailure, emote.Message, targetObject, true);
}
}
break;
case EmoteType.InqSkillStat:
if (targetCreature != null)
{
var skill = targetCreature.GetCreatureSkill((Skill)emote.Stat);
if (skill == null && HasValidTestNoQuality(emote.Message))
{
ExecuteEmoteSet(EmoteCategory.TestNoQuality, emote.Message, targetObject, true);
}
else
{
success = skill != null && skill.Current >= (emote.Min ?? int.MinValue) && skill.Current <= (emote.Max ?? int.MaxValue);
ExecuteEmoteSet(success ? EmoteCategory.TestSuccess : EmoteCategory.TestFailure, emote.Message, targetObject, true);
}
}
break;
case EmoteType.InqSkillTrained:
if (targetCreature != null)
{
var skill = targetCreature.GetCreatureSkill((Skill)emote.Stat);
if (skill == null && HasValidTestNoQuality(emote.Message))
{
ExecuteEmoteSet(EmoteCategory.TestNoQuality, emote.Message, targetObject, true);
}
else
{
success = skill != null && skill.AdvancementClass >= SkillAdvancementClass.Trained;
ExecuteEmoteSet(success ? EmoteCategory.TestSuccess : EmoteCategory.TestFailure, emote.Message, targetObject, true);
}
}
break;
case EmoteType.InqStringStat:
if (targetObject != null)
{
var stringStat = targetObject.GetProperty((PropertyString)emote.Stat);
if (stringStat == null && HasValidTestNoQuality(emote.Message))
ExecuteEmoteSet(EmoteCategory.TestNoQuality, emote.Message, targetObject, true);
else
{
success = stringStat != null && stringStat.Equals(emote.TestString);
ExecuteEmoteSet(success ? EmoteCategory.TestSuccess : EmoteCategory.TestFailure, emote.Message, targetObject, true);
}
}
break;
case EmoteType.InqYesNo:
if (player != null)
{
if (!player.ConfirmationManager.EnqueueSend(new Confirmation_YesNo(WorldObject.Guid, player.Guid, emote.Message), Replace(emote.TestString, WorldObject, targetObject, emoteSet.Quest)))
{
ExecuteEmoteSet(EmoteCategory.TestFailure, emote.Message, player);
}
}
break;
case EmoteType.Invalid:
break;
case EmoteType.KillSelf:
if (creature != null)
creature.Smite(creature);
break;
case EmoteType.LocalBroadcast:
message = Replace(emote.Message, WorldObject, targetObject, emoteSet.Quest);
WorldObject.EnqueueBroadcast(new GameMessageSystemChat(message, ChatMessageType.Broadcast));
break;
case EmoteType.LocalSignal:
if (WorldObject != null)
{
if (WorldObject.CurrentLandblock != null)
WorldObject.CurrentLandblock.EmitSignal(WorldObject, emote.Message);
}
break;
case EmoteType.LockFellow:
if (player != null && player.Fellowship != null)
player.HandleActionFellowshipChangeLock(true, emoteSet.Quest);
break;
/* plays an animation on the target object (usually the player) */
case EmoteType.ForceMotion:
var motionCommand = MotionCommandHelper.GetMotion(emote.Motion.Value);
var motion = new Motion(targetObject, motionCommand, emote.Extent);
targetObject.EnqueueBroadcastMotion(motion);
break;
/* plays an animation on the source object */
case EmoteType.Motion:
var debugMotion = false;
if (Debug)
Console.Write($".{(MotionCommand)emote.Motion}");
// If the landblock is dormant, there are no players in range
if (WorldObject.CurrentLandblock?.IsDormant ?? false)
break;
// are there players within emote range?
if (!WorldObject.PlayersInRange(ClientMaxAnimRange))
break;
if (WorldObject.PhysicsObj != null && WorldObject.PhysicsObj.IsMovingTo())
break;
if (WorldObject == null || WorldObject.CurrentMotionState == null) break;
// TODO: REFACTOR ME
if (emoteSet.Category != EmoteCategory.Vendor && emoteSet.Style != null)
{
var startingMotion = new Motion((MotionStance)emoteSet.Style, (MotionCommand)emoteSet.Substyle);
motion = new Motion((MotionStance)emoteSet.Style, (MotionCommand)emote.Motion, emote.Extent);
if (WorldObject.CurrentMotionState.Stance != startingMotion.Stance)
{
if (WorldObject.CurrentMotionState.Stance == MotionStance.Invalid)
{
if (debugMotion)
Console.WriteLine($"{WorldObject.Name} running starting motion {(MotionStance)emoteSet.Style}, {(MotionCommand)emoteSet.Substyle}");
delay = WorldObject.ExecuteMotion(startingMotion);
}
}
else
{
if (WorldObject.CurrentMotionState.MotionState.ForwardCommand == startingMotion.MotionState.ForwardCommand
&& startingMotion.Stance == MotionStance.NonCombat) // enforce non-combat here?
{
if (debugMotion)
Console.WriteLine($"{WorldObject.Name} running motion {(MotionStance)emoteSet.Style}, {(MotionCommand)emote.Motion}");
float? maxRange = ClientMaxAnimRange;
if (MotionQueue.Contains((MotionCommand)emote.Motion))
maxRange = null;
var motionTable = DatManager.PortalDat.ReadFromDat(WorldObject.MotionTableId);
var animLength = motionTable.GetAnimationLength(WorldObject.CurrentMotionState.Stance, (MotionCommand)emote.Motion, MotionCommand.Ready);
delay = WorldObject.ExecuteMotion(motion, true, maxRange);
var motionChain = new ActionChain();
motionChain.AddDelaySeconds(animLength);
motionChain.AddAction(WorldObject, () =>
{
// FIXME: better cycle handling
var cmd = WorldObject.CurrentMotionState.MotionState.ForwardCommand;
if (cmd != MotionCommand.Dead && cmd != MotionCommand.Sleeping && cmd != MotionCommand.Sitting && !cmd.ToString().EndsWith("State"))
{
if (debugMotion)
Console.WriteLine($"{WorldObject.Name} running starting motion again {(MotionStance)emoteSet.Style}, {(MotionCommand)emoteSet.Substyle}");
WorldObject.ExecuteMotion(startingMotion);
}
});
motionChain.EnqueueChain();
if (debugMotion)
Console.WriteLine($"{WorldObject.Name} appending time to existing chain: " + animLength);
}
}
}
else
{
// vendor / other motions
var startingMotion = new Motion(MotionStance.NonCombat, MotionCommand.Ready);
var motionTable = DatManager.PortalDat.ReadFromDat(WorldObject.MotionTableId);
var animLength = motionTable.GetAnimationLength(WorldObject.CurrentMotionState.Stance, (MotionCommand)emote.Motion, MotionCommand.Ready);
motion = new Motion(MotionStance.NonCombat, (MotionCommand)emote.Motion, emote.Extent);
if (debugMotion)
Console.WriteLine($"{WorldObject.Name} running motion (block 2) {MotionStance.NonCombat}, {(MotionCommand)(emote.Motion ?? 0)}");
delay = WorldObject.ExecuteMotion(motion);
var motionChain = new ActionChain();
motionChain.AddDelaySeconds(animLength);
motionChain.AddAction(WorldObject, () => WorldObject.ExecuteMotion(startingMotion, false));
motionChain.EnqueueChain();
}
break;
/* move to position relative to home */
case EmoteType.Move:
if (creature != null)
{
// If the landblock is dormant, there are no players in range
if (WorldObject.CurrentLandblock?.IsDormant ?? false)
break;
// are there players within emote range?
if (!WorldObject.PlayersInRange(ClientMaxAnimRange))
break;
var newPos = new Position(creature.Home);
newPos.Pos += new Vector3(emote.OriginX ?? 0, emote.OriginY ?? 0, emote.OriginZ ?? 0); // uses relative position
// ensure valid quaternion - all 0s for example can lock up physics engine
if (emote.AnglesX != null && emote.AnglesY != null && emote.AnglesZ != null && emote.AnglesW != null &&
(emote.AnglesX != 0 || emote.AnglesY != 0 || emote.AnglesZ != 0 || emote.AnglesW != 0) )
{
// also relative, or absolute?
newPos.Rotation *= new Quaternion(emote.AnglesX.Value, emote.AnglesY.Value, emote.AnglesZ.Value, emote.AnglesW.Value);
}
if (Debug)
Console.WriteLine(newPos.ToLOCString());
// get new cell
newPos.LandblockId = new LandblockId(PositionExtensions.GetCell(newPos));
// TODO: handle delay for this?
creature.MoveTo(newPos, creature.GetRunRate(), true, null, emote.Extent);
}
break;
case EmoteType.MoveHome:
// TODO: call MoveToManager on server, handle delay for this?
if (creature != null && creature.Home != null)
{
// are we already at home origin?
if (creature.Location.Pos.Equals(creature.Home.Pos))
{
// just turnto if required?
if (Debug)
Console.Write($" - already at home origin, checking rotation");
if (!creature.Location.Rotation.Equals(creature.Home.Rotation))
{
if (Debug)
Console.Write($" - turning to");
delay = creature.TurnTo(creature.Home);
}
else if (Debug)
Console.Write($" - already at home rotation, doing nothing");
}
else
{
if (Debug)
Console.Write($" - {creature.Home.ToLOCString()}");
// how to get delay with this, callback required?
creature.MoveTo(creature.Home, creature.GetRunRate(), true, null, emote.Extent);
}
}
break;
case EmoteType.MoveToPos:
if (creature != null)
{
var currentPos = creature.Location;
var newPos = new Position();
newPos.LandblockId = new LandblockId(emote.ObjCellId ?? currentPos.LandblockId.Raw);
newPos.Pos = new Vector3(emote.OriginX ?? currentPos.Pos.X, emote.OriginY ?? currentPos.Pos.Y, emote.OriginZ ?? currentPos.Pos.Z);
if (emote.AnglesX == null || emote.AnglesY == null || emote.AnglesZ == null || emote.AnglesW == null)
newPos.Rotation = new Quaternion(currentPos.Rotation.X, currentPos.Rotation.Y, currentPos.Rotation.Z, currentPos.Rotation.W);
else
newPos.Rotation = new Quaternion(emote.AnglesX ?? 0, emote.AnglesY ?? 0, emote.AnglesZ ?? 0, emote.AnglesW ?? 1);
//if (emote.ObjCellId != null)
//newPos.LandblockId = new LandblockId(emote.ObjCellId.Value);
newPos.LandblockId = new LandblockId(PositionExtensions.GetCell(newPos));
// TODO: handle delay for this?
creature.MoveTo(newPos, creature.GetRunRate(), true, null, emote.Extent);
}
break;
case EmoteType.OpenMe:
if (WorldObject is Container openContainer)
openContainer.Open(null);
else if (WorldObject is Door openDoor)
openDoor.Open();
break;
case EmoteType.PetCastSpellOnOwner:
if (creature is Pet passivePet && passivePet.P_PetOwner != null)
{
var spell = new Spell((uint)emote.SpellId);
passivePet.TryCastSpell(spell, passivePet.P_PetOwner);
}
break;
case EmoteType.PhysScript:
WorldObject.PlayParticleEffect((PlayScript)emote.PScript, WorldObject.Guid, emote.Extent);
break;
case EmoteType.PopUp:
if (player != null)
player.Session.Network.EnqueueSend(new GameEventPopupString(player.Session, emote.Message));
break;
case EmoteType.RemoveContract:
if (player != null && emote.Stat.HasValue && emote.Stat.Value > 0)
player.HandleActionAbandonContract((uint)emote.Stat);
break;
case EmoteType.RemoveVitaePenalty:
if (player != null)
player.EnchantmentManager.RemoveVitae();
break;
case EmoteType.ResetHomePosition:
if (WorldObject.Location != null)
WorldObject.Home = new Position(WorldObject.Location);
break;
case EmoteType.Say:
if (Debug)
Console.Write($" - {emote.Message}");
message = Replace(emote.Message, WorldObject, targetObject, emoteSet.Quest);
var name = WorldObject.CreatureType == CreatureType.Olthoi ? WorldObject.Name + "&" : WorldObject.Name;
if (emote.Extent > 0)
WorldObject.EnqueueBroadcast(new GameMessageHearRangedSpeech(message, name, WorldObject.Guid.Full, emote.Extent, ChatMessageType.Emote), WorldObject.LocalBroadcastRange);
else
WorldObject.EnqueueBroadcast(new GameMessageHearSpeech(message, name, WorldObject.Guid.Full, ChatMessageType.Emote), WorldObject.LocalBroadcastRange);
break;
case EmoteType.SetAltRacialSkills:
break;
case EmoteType.SetBoolStat:
if (player != null)
{
player.UpdateProperty(player, (PropertyBool)emote.Stat, emote.Amount == 0 ? false : true);
player.EnqueueBroadcast(false, new GameMessagePublicUpdatePropertyBool(player, (PropertyBool)emote.Stat, emote.Amount == 0 ? false : true));
}
break;
case EmoteType.SetEyePalette:
//if (creature != null)
// creature.EyesPaletteDID = (uint)emote.Display;
break;
case EmoteType.SetEyeTexture:
//if (creature != null)
// creature.EyesTextureDID = (uint)emote.Display;
break;
case EmoteType.SetFloatStat:
if (player != null)
{
player.UpdateProperty(player, (PropertyFloat)emote.Stat, emote.Percent);
player.EnqueueBroadcast(false, new GameMessagePublicUpdatePropertyFloat(player, (PropertyFloat)emote.Stat, Convert.ToDouble(emote.Percent)));
}
break;
case EmoteType.SetHeadObject:
//if (creature != null)
// creature.HeadObjectDID = (uint)emote.Display;
break;
case EmoteType.SetHeadPalette:
break;
case EmoteType.SetInt64Stat:
if (player != null)
{
player.UpdateProperty(player, (PropertyInt64)emote.Stat, emote.Amount64);
player.EnqueueBroadcast(false, new GameMessagePublicUpdatePropertyInt64(player, (PropertyInt64)emote.Stat, Convert.ToInt64(emote.Amount64)));
}
break;
case EmoteType.SetIntStat:
if (player != null)
{
player.UpdateProperty(player, (PropertyInt)emote.Stat, emote.Amount);
player.EnqueueBroadcast(false, new GameMessagePublicUpdatePropertyInt(player, (PropertyInt)emote.Stat, Convert.ToInt32(emote.Amount)));
}
break;
case EmoteType.SetMouthPalette:
break;
case EmoteType.SetMouthTexture:
//if (creature != null)
// creature.MouthTextureDID = (uint)emote.Display;
break;
case EmoteType.SetNosePalette:
break;
case EmoteType.SetNoseTexture:
//if (creature != null)
// creature.NoseTextureDID = (uint)emote.Display;
break;
case EmoteType.SetMyQuestBitsOff:
case EmoteType.SetQuestBitsOff:
questTarget = GetQuestTarget((EmoteType)emote.Type, targetCreature, creature);
if (questTarget != null && emote.Message != null && emote.Amount != null)
questTarget.QuestManager.SetQuestBits(emote.Message, (int)emote.Amount, false);
break;
case EmoteType.SetMyQuestBitsOn:
case EmoteType.SetQuestBitsOn:
questTarget = GetQuestTarget((EmoteType)emote.Type, targetCreature, creature);
if (questTarget != null && emote.Message != null && emote.Amount != null)
questTarget.QuestManager.SetQuestBits(emote.Message, (int)emote.Amount);
break;
case EmoteType.SetMyQuestCompletions:
case EmoteType.SetQuestCompletions:
questTarget = GetQuestTarget((EmoteType)emote.Type, targetCreature, creature);
if (questTarget != null && emote.Amount != null)
questTarget.QuestManager.SetQuestCompletions(emote.Message, (int)emote.Amount);
break;
case EmoteType.SetSanctuaryPosition:
if (player != null)
player.SetPosition(PositionType.Sanctuary, new Position(emote.ObjCellId.Value, emote.OriginX.Value, emote.OriginY.Value, emote.OriginZ.Value, emote.AnglesX.Value, emote.AnglesY.Value, emote.AnglesZ.Value, emote.AnglesW.Value));
break;
case EmoteType.Sound:
WorldObject.EnqueueBroadcast(new GameMessageSound(WorldObject.Guid, (Sound)emote.Sound, 1.0f));
break;
case EmoteType.SpendLuminance:
if (player != null)
player.SpendLuminance(emote.Amount64 ?? emote.HeroXP64 ?? 0);
break;
case EmoteType.StampFellowQuest:
if (player != null)
{
if (player.Fellowship != null)
{
var questName = emote.Message;
player.Fellowship.QuestManager.Stamp(emote.Message);
}
}
break;
case EmoteType.StampMyQuest:
case EmoteType.StampQuest:
questTarget = GetQuestTarget((EmoteType)emote.Type, targetCreature, creature);
if (questTarget != null)
{
var questName = emote.Message;
if (questName.EndsWith("@#kt", StringComparison.Ordinal))
log.Warn($"0x{WorldObject.Guid}:{WorldObject.Name} ({WorldObject.WeenieClassId}).EmoteManager.ExecuteEmote: EmoteType.StampQuest({questName}) is a depreciated kill task method.");
questTarget.QuestManager.Stamp(emote.Message);
}
break;
case EmoteType.StartBarber:
if (player != null)
player.StartBarber();
break;
case EmoteType.StartEvent:
EventManager.StartEvent(emote.Message, WorldObject, targetObject);
break;
case EmoteType.StopEvent:
EventManager.StopEvent(emote.Message, WorldObject, targetObject);
break;
case EmoteType.TakeItems:
if (player != null)
{
var weenieItemToTake = emote.WeenieClassId ?? 0;
var amountToTake = emote.StackSize ?? 1;
if (weenieItemToTake == 0)
{
log.Warn($"EmoteManager.Execute: 0x{WorldObject.Guid} {WorldObject.Name} ({WorldObject.WeenieClassId}) EmoteType.TakeItems has invalid emote.WeenieClassId: {weenieItemToTake}");
break;
}
if (amountToTake < -1 || amountToTake == 0)
{
log.Warn($"EmoteManager.Execute: 0x{WorldObject.Guid} {WorldObject.Name} ({WorldObject.WeenieClassId}) EmoteType.TakeItems has invalid emote.StackSize: {amountToTake}");
break;
}
if ((player.GetNumInventoryItemsOfWCID(weenieItemToTake) > 0 && player.TryConsumeFromInventoryWithNetworking(weenieItemToTake, amountToTake == -1 ? int.MaxValue : amountToTake))
|| (player.GetNumEquippedObjectsOfWCID(weenieItemToTake) > 0 && player.TryConsumeFromEquippedObjectsWithNetworking(weenieItemToTake, amountToTake == -1 ? int.MaxValue : amountToTake)))
{
var itemTaken = DatabaseManager.World.GetCachedWeenie(weenieItemToTake);
if (itemTaken != null)
{
var amount = amountToTake == -1 ? "all" : amountToTake.ToString();
var msg = $"You hand over {amount} of your {itemTaken.GetPluralName()}.";
player.Session.Network.EnqueueSend(new GameMessageSystemChat(msg, ChatMessageType.Broadcast));
}
}
}
break;
case EmoteType.TeachSpell:
if (player != null)
player.LearnSpellWithNetworking((uint)emote.SpellId, false);
break;
case EmoteType.TeleportSelf:
//if (WorldObject is Player)
//(WorldObject as Player).Teleport(emote.Position);
break;
case EmoteType.TeleportTarget:
if (player != null)
{
if (emote.ObjCellId.HasValue && emote.OriginX.HasValue && emote.OriginY.HasValue && emote.OriginZ.HasValue && emote.AnglesX.HasValue && emote.AnglesY.HasValue && emote.AnglesZ.HasValue && emote.AnglesW.HasValue)
{
if (emote.ObjCellId.Value > 0)
{
var destination = new Position(emote.ObjCellId.Value, emote.OriginX.Value, emote.OriginY.Value, emote.OriginZ.Value, emote.AnglesX.Value, emote.AnglesY.Value, emote.AnglesZ.Value, emote.AnglesW.Value);
WorldObject.AdjustDungeon(destination);
WorldManager.ThreadSafeTeleport(player, destination);
}
else // position is relative to WorldObject's current location
{
var relativeDestination = new Position(WorldObject.Location);
relativeDestination.Pos += new Vector3(emote.OriginX.Value, emote.OriginY.Value, emote.OriginZ.Value);
relativeDestination.Rotation = new Quaternion(emote.AnglesX.Value, emote.AnglesY.Value, emote.AnglesZ.Value, emote.AnglesW.Value);
relativeDestination.LandblockId = new LandblockId(relativeDestination.GetCell());
WorldObject.AdjustDungeon(relativeDestination);
WorldManager.ThreadSafeTeleport(player, relativeDestination);
}
}
}
break;
case EmoteType.Tell:
if (player != null)
{
message = Replace(emote.Message, WorldObject, player, emoteSet.Quest);
player.Session.Network.EnqueueSend(new GameEventTell(WorldObject, message, player, ChatMessageType.Tell));
}
break;
case EmoteType.TellFellow:
if (player != null)
{
var fellowship = player.Fellowship;
if (fellowship != null)
{
text = Replace(emote.Message, WorldObject, player, emoteSet.Quest);
fellowship.TellFellow(WorldObject, text);
}
}
break;
case EmoteType.TextDirect:
if (player != null)
{
message = Replace(emote.Message, WorldObject, player, emoteSet.Quest);
player.Session.Network.EnqueueSend(new GameMessageSystemChat(message, ChatMessageType.Broadcast));
}
break;
case EmoteType.Turn:
if (creature != null)
{
// turn to heading
var rotation = new Quaternion(emote.AnglesX ?? 0, emote.AnglesY ?? 0, emote.AnglesZ ?? 0, emote.AnglesW ?? 1);
var newPos = new Position(creature.Location);
newPos.Rotation = rotation;
var rotateTime = creature.TurnTo(newPos);
delay = rotateTime;
}
break;
case EmoteType.TurnToTarget:
if (creature != null && targetCreature != null)
delay = creature.Rotate(targetCreature);
break;
case EmoteType.UntrainSkill:
if (player != null)
player.ResetSkill((Skill)emote.Stat);
break;
case EmoteType.UpdateFellowQuest:
if (player != null)
{
if (player.Fellowship != null)
{
var questName = emote.Message;
var hasQuest = player.Fellowship.QuestManager.HasQuest(questName);
if (!hasQuest)
{
// add new quest
player.Fellowship.QuestManager.Update(questName);
hasQuest = player.Fellowship.QuestManager.HasQuest(questName);
ExecuteEmoteSet(hasQuest ? EmoteCategory.QuestSuccess : EmoteCategory.QuestFailure, emote.Message, targetObject, true);
}
else
{
// update existing quest
var canSolve = player.Fellowship.QuestManager.CanSolve(questName);
if (canSolve)
player.Fellowship.QuestManager.Stamp(questName);
ExecuteEmoteSet(canSolve ? EmoteCategory.QuestSuccess : EmoteCategory.QuestFailure, emote.Message, targetObject, true);
}
}
else
ExecuteEmoteSet(EmoteCategory.QuestNoFellow, emote.Message, targetObject, true);
}
break;
case EmoteType.UpdateMyQuest:
case EmoteType.UpdateQuest:
questTarget = GetQuestTarget((EmoteType)emote.Type, targetCreature, creature);
if (questTarget != null)
{
var questName = emote.Message;
var hasQuest = questTarget.QuestManager.HasQuest(questName);
if (!hasQuest)
{
// add new quest
questTarget.QuestManager.Update(questName);
hasQuest = questTarget.QuestManager.HasQuest(questName);
ExecuteEmoteSet(hasQuest ? EmoteCategory.QuestSuccess : EmoteCategory.QuestFailure, emote.Message, targetObject, true);
}
else
{
// update existing quest
var canSolve = questTarget.QuestManager.CanSolve(questName);
if (canSolve)
questTarget.QuestManager.Stamp(questName);
ExecuteEmoteSet(canSolve ? EmoteCategory.QuestSuccess : EmoteCategory.QuestFailure, emote.Message, targetObject, true);
}
}
break;
case EmoteType.WorldBroadcast:
message = Replace(text, WorldObject, targetObject, emoteSet.Quest);
PlayerManager.BroadcastToAll(new GameMessageSystemChat(message, ChatMessageType.WorldBroadcast));
PlayerManager.LogBroadcastChat(Channel.AllBroadcast, WorldObject, message);
break;
default:
log.DebugFormat("EmoteManager.Execute - Encountered Unhandled EmoteType {0} for {1} ({2})", (EmoteType)emote.Type, WorldObject.Name, WorldObject.WeenieClassId);
break;
}
return delay;
}
///
/// Selects an emote set based on category, and optional: quest, vendor, rng
///
public PropertiesEmote GetEmoteSet(EmoteCategory category, string questName = null, VendorType? vendorType = null, uint? wcid = null, bool useRNG = true)
{
//if (Debug) Console.WriteLine($"{WorldObject.Name}.EmoteManager.GetEmoteSet({category}, {questName}, {vendorType}, {wcid}, {useRNG})");
if (_worldObject.Biota.PropertiesEmote == null)
return null;
// always pull emoteSet from _worldObject
var emoteSet = _worldObject.Biota.PropertiesEmote.Where(e => e.Category == category);
// optional criteria
if ((category == EmoteCategory.HearChat || category == EmoteCategory.ReceiveTalkDirect) && questName != null)
emoteSet = emoteSet.Where(e => e.Quest != null && e.Quest.Equals(questName, StringComparison.OrdinalIgnoreCase) || e.Quest == null);
else if (questName != null)
emoteSet = emoteSet.Where(e => e.Quest != null && e.Quest.Equals(questName, StringComparison.OrdinalIgnoreCase));
if (vendorType != null)
emoteSet = emoteSet.Where(e => e.VendorType != null && e.VendorType.Value == vendorType);
if (wcid != null)
emoteSet = emoteSet.Where(e => e.WeenieClassId == wcid.Value);
if (category == EmoteCategory.HeartBeat)
{
WorldObject.GetCurrentMotionState(out MotionStance currentStance, out MotionCommand currentMotion);
emoteSet = emoteSet.Where(e => e.Style == null || e.Style == currentStance);
emoteSet = emoteSet.Where(e => e.Substyle == null || e.Substyle == currentMotion);
}
if (category == EmoteCategory.WoundedTaunt)
{
if (_worldObject is Creature creature)
emoteSet = emoteSet.Where(e => creature.Health.Percent >= e.MinHealth && creature.Health.Percent <= e.MaxHealth);
}
if (useRNG)
{
var rng = ThreadSafeRandom.Next(0.0f, 1.0f);
emoteSet = emoteSet.Where(e => e.Probability > rng).OrderBy(e => e.Probability);
//emoteSet = emoteSet.Where(e => e.Probability >= rng);
}
return emoteSet.FirstOrDefault();
}
///
/// Convenience wrapper between GetEmoteSet and ExecututeEmoteSet
///
public void ExecuteEmoteSet(EmoteCategory category, string quest = null, WorldObject targetObject = null, bool nested = false)
{
//if (Debug) Console.WriteLine($"{WorldObject.Name}.EmoteManager.ExecuteEmoteSet({category}, {quest}, {targetObject}, {nested})");
var emoteSet = GetEmoteSet(category, quest);
if (emoteSet == null) return;
// TODO: revisit if nested chains need to propagate timers
ExecuteEmoteSet(emoteSet, targetObject, nested);
}
///
/// Executes a set of emotes to run with delays
///
/// A list of emotes to execute
/// An optional target, usually player
/// For adding delays between emotes
public bool ExecuteEmoteSet(PropertiesEmote emoteSet, WorldObject targetObject = null, bool nested = false)
{
//if (Debug) Console.WriteLine($"{WorldObject.Name}.EmoteManager.ExecuteEmoteSet({emoteSet}, {targetObject}, {nested})");
// detect busy state
// TODO: maybe eventually we should consider having categories that can be queued?
// there are some categories that shouldn't be queued, like heartbeats...
if (IsBusy && !nested) return false;
// start action chain
Nested++;
Enqueue(emoteSet, targetObject);
return true;
}
public void Enqueue(PropertiesEmote emoteSet, WorldObject targetObject, int emoteIdx = 0, float delay = 0.0f)
{
//if (Debug) Console.WriteLine($"{WorldObject.Name}.EmoteManager.Enqueue({emoteSet}, {targetObject}, {emoteIdx}, {delay})");
if (emoteSet == null)
{
Nested--;
return;
}
IsBusy = true;
var emote = emoteSet.PropertiesEmoteAction.ElementAt(emoteIdx);
if (Nested > 75 && !string.IsNullOrEmpty(emoteSet.Quest) && emoteSet.Quest == emote.Message && EmoteIsBranchingType(emote))
{
var emoteStack = $"{emoteSet.Category}: {emoteSet.Quest}\n";
foreach (var e in emoteSet.PropertiesEmoteAction)
emoteStack += $" - {(EmoteType)emote.Type}{(string.IsNullOrEmpty(emote.Message) ? "" : $": {emote.Message}")}\n";
log.Error($"[EMOTE] {WorldObject.Name}.EmoteManager.Enqueue(): Nested > 75, possible Infinite loop detected and aborted on 0x{WorldObject.Guid}:{WorldObject.WeenieClassId}\n-> {emoteStack}");
Nested--;
if (Nested == 0)
IsBusy = false;
return;
}
if (delay + emote.Delay > 0)
{
var actionChain = new ActionChain();
if (Debug)
actionChain.AddAction(WorldObject, () => Console.Write($"{emote.Delay} - "));
// delay = post-delay from actual time of previous emote
// emote.Delay = pre-delay for current emote
actionChain.AddDelaySeconds(delay + emote.Delay);
actionChain.AddAction(WorldObject, () => DoEnqueue(emoteSet, targetObject, emoteIdx, emote));
actionChain.EnqueueChain();
}
else
{
DoEnqueue(emoteSet, targetObject, emoteIdx, emote);
}
}
///
/// This should only be called by Enqueue
///
private void DoEnqueue(PropertiesEmote emoteSet, WorldObject targetObject, int emoteIdx, PropertiesEmoteAction emote)
{
if (Debug)
Console.Write($"{(EmoteType)emote.Type}");
//if (!string.IsNullOrEmpty(emoteSet.Quest) && emoteSet.Quest == emote.Message && EmoteIsBranchingType(emote))
//{
// log.Error($"[EMOTE] {WorldObject.Name}.EmoteManager.DoEnqueue(): Infinite loop detected on 0x{WorldObject.Guid}:{WorldObject.WeenieClassId}\n-> {emoteSet.Category}: {emoteSet.Quest} to {(EmoteType)emote.Type}: {emote.Message}");
// Nested--;
// if (Nested == 0)
// IsBusy = false;
// return;
//}
var nextDelay = ExecuteEmote(emoteSet, emote, targetObject);
if (Debug)
Console.WriteLine($" - { nextDelay}");
if (emoteIdx < emoteSet.PropertiesEmoteAction.Count - 1)
Enqueue(emoteSet, targetObject, emoteIdx + 1, nextDelay);
else
{
if (nextDelay > 0)
{
var delayChain = new ActionChain();
delayChain.AddDelaySeconds(nextDelay);
delayChain.AddAction(WorldObject, () =>
{
Nested--;
if (Nested == 0)
IsBusy = false;
});
delayChain.EnqueueChain();
}
else
{
Nested--;
if (Nested == 0)
IsBusy = false;
}
}
}
private bool EmoteIsBranchingType(PropertiesEmoteAction emote)
{
if (emote == null)
return false;
var emoteType = (EmoteType)emote.Type;
switch (emoteType)
{
case EmoteType.UpdateQuest:
case EmoteType.InqQuest:
case EmoteType.InqQuestSolves:
case EmoteType.InqBoolStat:
case EmoteType.InqIntStat:
case EmoteType.InqFloatStat:
case EmoteType.InqStringStat:
case EmoteType.InqAttributeStat:
case EmoteType.InqRawAttributeStat:
case EmoteType.InqSecondaryAttributeStat:
case EmoteType.InqRawSecondaryAttributeStat:
case EmoteType.InqSkillStat:
case EmoteType.InqRawSkillStat:
case EmoteType.InqSkillTrained:
case EmoteType.InqSkillSpecialized:
case EmoteType.InqEvent:
case EmoteType.InqFellowQuest:
case EmoteType.InqFellowNum:
case EmoteType.UpdateFellowQuest:
case EmoteType.Goto:
case EmoteType.InqNumCharacterTitles:
case EmoteType.InqYesNo:
case EmoteType.InqOwnsItems:
case EmoteType.UpdateMyQuest:
case EmoteType.InqMyQuest:
case EmoteType.InqMyQuestSolves:
case EmoteType.InqPackSpace:
case EmoteType.InqQuestBitsOn:
case EmoteType.InqQuestBitsOff:
case EmoteType.InqMyQuestBitsOn:
case EmoteType.InqMyQuestBitsOff:
case EmoteType.InqInt64Stat:
case EmoteType.InqContractsFull:
return true;
default:
return false;
}
}
public bool HasValidTestNoQuality(string testName) => GetEmoteSet(EmoteCategory.TestNoQuality, testName) != null;
public bool HasValidTestNoFellow(string testName) => GetEmoteSet(EmoteCategory.TestNoFellow, testName) != null;
///
/// The maximum animation range of the client
/// Motions broadcast outside of this range will be automatically queued by client
///
public static float ClientMaxAnimRange = 96.0f; // verify: same indoors?
///
/// The client automatically queues animations that are broadcast outside of 96.0f range
/// Normally we exclude these emotes from being broadcast outside this range,
/// but for certain emotes (like monsters going to sleep) we want to always broadcast / enqueue
///
public static HashSet MotionQueue = new HashSet()
{
MotionCommand.Sleeping
};
public void DoVendorEmote(VendorType vendorType, WorldObject target)
{
var vendorSet = GetEmoteSet(EmoteCategory.Vendor, null, vendorType);
var heartbeatSet = GetEmoteSet(EmoteCategory.Vendor, null, VendorType.Heartbeat);
ExecuteEmoteSet(vendorSet, target);
ExecuteEmoteSet(heartbeatSet, target, true);
}
public IEnumerable Emotes(EmoteCategory emoteCategory)
{
return WorldObject.Biota.PropertiesEmote.Where(x => x.Category == emoteCategory);
}
public string Replace(string message, WorldObject source, WorldObject target, string quest)
{
var result = message;
if (result == null)
{
log.Warn($"[EMOTE] {WorldObject.Name}.EmoteManager.Replace(message, {source.Name}:0x{source.Guid}:{source.WeenieClassId}, {target.Name}:0x{target.Guid}:{target.WeenieClassId}, {quest}): message was null!");
return "";
}
var sourceName = source != null ? source.Name : "";
var targetName = target != null ? target.Name : "";
result = result.Replace("%n", sourceName);
result = result.Replace("%mn", sourceName);
result = result.Replace("%s", targetName);
result = result.Replace("%tn", targetName);
var sourceLevel = source != null ? $"{source.Level ?? 0}" : "";
var targetLevel = target != null ? $"{target.Level ?? 0}" : "";
result = result.Replace("%ml", sourceLevel);
result = result.Replace("%tl", targetLevel);
//var sourceTemplate = source != null ? source.GetProperty(PropertyString.Title) : "";
//var targetTemplate = source != null ? target.GetProperty(PropertyString.Title) : "";
var sourceTemplate = source != null ? source.GetProperty(PropertyString.Template) : "";
var targetTemplate = target != null ? target.GetProperty(PropertyString.Template) : "";
result = result.Replace("%mt", sourceTemplate);
result = result.Replace("%tt", targetTemplate);
var sourceHeritage = source != null ? source.HeritageGroupName : "";
var targetHeritage = target != null ? target.HeritageGroupName : "";
result = result.Replace("%mh", sourceHeritage);
result = result.Replace("%th", targetHeritage);
//result = result.Replace("%mf", $"{source.GetProperty(PropertyString.Fellowship)}");
//result = result.Replace("%tf", $"{target.GetProperty(PropertyString.Fellowship)}");
//result = result.Replace("%l", $"{???}"); // level?
//result = result.Replace("%pk", $"{???}"); // pk status?
//result = result.Replace("%a", $"{???}"); // allegiance?
//result = result.Replace("%p", $"{???}"); // patron?
// Find quest in standard or LSD custom usage for %tqt and %CDtime
var embeddedQuestName = result.Contains("@") ? message.Split("@")[0] : null;
var questName = !string.IsNullOrWhiteSpace(embeddedQuestName) ? embeddedQuestName : quest;
// LSD custom tqt usage
result = result.Replace($"{questName}@%tqt", "You may complete this quest again in %tqt.", StringComparison.OrdinalIgnoreCase);
// LSD custom CDtime variable
if (result.Contains("%CDtime"))
result = result.Replace($"{questName}@", "", StringComparison.OrdinalIgnoreCase);
if (target is Player targetPlayer)
{
result = result.Replace("%tqt", !string.IsNullOrWhiteSpace(quest) ? targetPlayer.QuestManager.GetNextSolveTime(questName).GetFriendlyString() : "");
result = result.Replace("%CDtime", !string.IsNullOrWhiteSpace(quest) ? targetPlayer.QuestManager.GetNextSolveTime(questName).GetFriendlyString() : "");
result = result.Replace("%tf", $"{(targetPlayer.Fellowship != null ? targetPlayer.Fellowship.FellowshipName : "")}");
result = result.Replace("%fqt", !string.IsNullOrWhiteSpace(quest) && targetPlayer.Fellowship != null ? targetPlayer.Fellowship.QuestManager.GetNextSolveTime(questName).GetFriendlyString() : "");
result = result.Replace("%tqm", !string.IsNullOrWhiteSpace(quest) ? targetPlayer.QuestManager.GetMaxSolves(questName).ToString() : "");
result = result.Replace("%tqc", !string.IsNullOrWhiteSpace(quest) ? targetPlayer.QuestManager.GetCurrentSolves(questName).ToString() : "");
}
if (source is Creature sourceCreature)
{
result = result.Replace("%mqt", !string.IsNullOrWhiteSpace(quest) ? sourceCreature.QuestManager.GetNextSolveTime(questName).GetFriendlyString() : "");
result = result.Replace("%mxqt", !string.IsNullOrWhiteSpace(quest) ? sourceCreature.QuestManager.GetNextSolveTime(questName).GetFriendlyLongString() : "");
//result = result.Replace("%CDtime", !string.IsNullOrWhiteSpace(quest) ? sourceCreature.QuestManager.GetNextSolveTime(questName).GetFriendlyString() : "");
result = result.Replace("%mqc", !string.IsNullOrWhiteSpace(quest) ? sourceCreature.QuestManager.GetCurrentSolves(questName).ToString() : "");
}
return result;
}
///
/// Returns the creature target for quest emotes
///
public static Creature GetQuestTarget(EmoteType emote, Creature target, Creature self)
{
switch (emote)
{
// MyQuest always targets self
case EmoteType.DecrementMyQuest:
case EmoteType.EraseMyQuest:
case EmoteType.IncrementMyQuest:
case EmoteType.InqMyQuest:
case EmoteType.InqMyQuestBitsOff:
case EmoteType.InqMyQuestBitsOn:
case EmoteType.InqMyQuestSolves:
case EmoteType.SetMyQuestBitsOff:
case EmoteType.SetMyQuestBitsOn:
case EmoteType.SetMyQuestCompletions:
case EmoteType.StampMyQuest:
case EmoteType.UpdateMyQuest:
return self;
default:
return target ?? self;
}
}
private WorldObject GetSpellTarget(Spell spell, WorldObject target)
{
var targetSelf = spell.Flags.HasFlag(SpellFlags.SelfTargeted);
var untargeted = spell.NonComponentTargetType == ItemType.None;
var spellTarget = target;
if (untargeted)
spellTarget = null;
else if (targetSelf)
spellTarget = WorldObject;
return spellTarget;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void HeartBeat()
{
// player didn't do idle emotes in retail?
if (WorldObject is Player)
return;
if (WorldObject is Creature creature && creature.IsAwake)
return;
ExecuteEmoteSet(EmoteCategory.HeartBeat);
}
public void OnUse(Creature activator)
{
ExecuteEmoteSet(EmoteCategory.Use, null, activator);
}
public void OnPortal(Creature activator)
{
IsBusy = false;
ExecuteEmoteSet(EmoteCategory.Portal, null, activator);
}
public void OnActivation(Creature activator)
{
ExecuteEmoteSet(EmoteCategory.Activation, null, activator);
}
public void OnGeneration()
{
ExecuteEmoteSet(EmoteCategory.Generation, null, null);
}
public void OnWield(Creature wielder)
{
ExecuteEmoteSet(EmoteCategory.Wield, null, wielder);
}
public void OnUnwield(Creature wielder)
{
ExecuteEmoteSet(EmoteCategory.UnWield, null, wielder);
}
public void OnPickup(Creature initiator)
{
ExecuteEmoteSet(EmoteCategory.PickUp, null, initiator);
}
public void OnDrop(Creature dropper)
{
ExecuteEmoteSet(EmoteCategory.Drop, null, dropper);
}
///
/// Called when an idle mob becomes alerted by a player
/// and initially wakes up
///
public void OnWakeUp(Creature target)
{
ExecuteEmoteSet(EmoteCategory.Scream, null, target);
}
///
/// Called when a monster switches targets
///
public void OnNewEnemy(WorldObject newEnemy)
{
ExecuteEmoteSet(EmoteCategory.NewEnemy, null, newEnemy);
}
///
/// Called when a monster completes an attack
///
public void OnAttack(WorldObject target)
{
ExecuteEmoteSet(EmoteCategory.Taunt, null, target);
}
public void OnDamage(Creature attacker)
{
ExecuteEmoteSet(EmoteCategory.WoundedTaunt, null, attacker);
}
public void OnReceiveCritical(Creature attacker)
{
ExecuteEmoteSet(EmoteCategory.ReceiveCritical, null, attacker);
}
public void OnResistSpell(Creature attacker)
{
ExecuteEmoteSet(EmoteCategory.ResistSpell, null, attacker);
}
public void OnDeath(DamageHistoryInfo lastDamagerInfo)
{
if (GetEmoteSet(EmoteCategory.Death) == null)
return;
IsBusy = false;
var lastDamager = lastDamagerInfo?.TryGetPetOwnerOrAttacker();
ExecuteEmoteSet(EmoteCategory.Death, null, lastDamager);
}
///
/// Called when a monster kills a player
///
public void OnKill(Player player)
{
ExecuteEmoteSet(EmoteCategory.KillTaunt, null, player);
}
///
/// Called when player interacts with item that has a Quest string
///
public void OnQuest(Creature initiator)
{
var questName = WorldObject.Quest;
var hasQuest = initiator.QuestManager.HasQuest(questName);
if (!hasQuest)
{
// add new quest
initiator.QuestManager.Update(questName);
hasQuest = initiator.QuestManager.HasQuest(questName);
ExecuteEmoteSet(hasQuest ? EmoteCategory.QuestSuccess : EmoteCategory.QuestFailure, questName, initiator);
}
else
{
// update existing quest
var canSolve = initiator.QuestManager.CanSolve(questName);
if (canSolve)
initiator.QuestManager.Stamp(questName);
ExecuteEmoteSet(canSolve ? EmoteCategory.QuestSuccess : EmoteCategory.QuestFailure, questName, initiator);
}
}
///
/// Called when this NPC receives a direct text message from a player
///
public void OnTalkDirect(Player player, string message)
{
ExecuteEmoteSet(EmoteCategory.ReceiveTalkDirect, message, player);
}
///
/// Called when this NPC receives a local signal from a player
///
public void OnLocalSignal(WorldObject emitter, string message)
{
ExecuteEmoteSet(EmoteCategory.ReceiveLocalSignal, message, emitter);
}
///
/// Called when monster exceeds the maximum distance from home position
///
public void OnHomeSick(WorldObject attackTarget)
{
ExecuteEmoteSet(EmoteCategory.Homesick, null, attackTarget);
}
///
/// Called when this NPC hears local chat from a player
///
public void OnHearChat(Player player, string message)
{
ExecuteEmoteSet(EmoteCategory.HearChat, message, player);
}
//public bool HasAntennas => WorldObject.Biota.BiotaPropertiesEmote.Count(x => x.Category == (int)EmoteCategory.ReceiveLocalSignal) > 0;
///
/// Call this function when WorldObject is being used via a proxy object, e.g.: Hooker on a Hook
///
public void SetProxy(WorldObject worldObject)
{
_proxy = worldObject;
}
///
/// Called when this object is removed from the proxy object (Hooker is picked up from Hook)
///
public void ClearProxy()
{
_proxy = null;
}
}
}