diff --git a/Source/ACE.Server/Command/Handlers/DeveloperCommands.cs b/Source/ACE.Server/Command/Handlers/DeveloperCommands.cs index 672559f8d3..93e89e10a5 100644 --- a/Source/ACE.Server/Command/Handlers/DeveloperCommands.cs +++ b/Source/ACE.Server/Command/Handlers/DeveloperCommands.cs @@ -2663,5 +2663,19 @@ public static void HandleRemoveVitae(Session session, params string[] parameters if (player != session.Player) session.Network.EnqueueSend(new GameMessageSystemChat("Removed vitae for {player.Name}", ChatMessageType.Broadcast)); } + + [CommandHandler("fast", AccessLevel.Developer, CommandHandlerFlag.RequiresWorld)] + public static void HandleFast(Session session, params string[] parameters) + { + var spell = new Spell(SpellId.QuicknessSelf8); + session.Player.CreateEnchantment(session.Player, session.Player, spell); + } + + [CommandHandler("slow", AccessLevel.Developer, CommandHandlerFlag.RequiresWorld)] + public static void HandleSlow(Session session, params string[] parameters) + { + var spell = new Spell(SpellId.SlownessSelf8); + session.Player.CreateEnchantment(session.Player, session.Player, spell); + } } } diff --git a/Source/ACE.Server/Entity/Spell.cs b/Source/ACE.Server/Entity/Spell.cs index 9ca69e97ed..778128a5aa 100644 --- a/Source/ACE.Server/Entity/Spell.cs +++ b/Source/ACE.Server/Entity/Spell.cs @@ -267,6 +267,33 @@ public bool HasItemCategory } } + /// + /// Returns TRUE for any spells which could potentially affect the run rate, + /// such as spells which alter run / quickness / strength + /// + public bool UpdatesRunRate + { + get + { + if (_spell == null) + return false; + + // this is commented out as below in UpdatesMaxVitals + // i forget the exact reasoning, are all the proper hooks in places for each vitae %, + // and not just add/remove? + /*if (_spell.Id == 666) // vitae + return true;*/ + + if (StatModType.HasFlag(EnchantmentTypeFlags.Attribute) && (StatModKey == (uint)PropertyAttribute.Strength || StatModKey == (uint)PropertyAttribute.Quickness)) + return true; + + if (StatModType.HasFlag(EnchantmentTypeFlags.Skill) && StatModKey == (uint)Skill.Run) + return true; + + return false; + } + } + /// /// Returns a list of MaxVitals affected by this spell /// @@ -279,6 +306,15 @@ public List UpdatesMaxVitals if (_spell == null) return maxVitals; + /*if (_spell.Id == 666) // Vitae + { + maxVitals.Add(PropertyAttribute2nd.MaxHealth); + maxVitals.Add(PropertyAttribute2nd.MaxStamina); + maxVitals.Add(PropertyAttribute2nd.MaxMana); + + return maxVitals; + }*/ + if (StatModType.HasFlag(EnchantmentTypeFlags.SecondAtt) && StatModKey != 0) maxVitals.Add((PropertyAttribute2nd)StatModKey); @@ -296,14 +332,6 @@ public List UpdatesMaxVitals break; } } - - //if (_spell.Id == 666) // Vitae - //{ - // maxVitals.Add(PropertyAttribute2nd.MaxHealth); - // maxVitals.Add(PropertyAttribute2nd.MaxStamina); - // maxVitals.Add(PropertyAttribute2nd.MaxMana); - //} - return maxVitals; } } diff --git a/Source/ACE.Server/Managers/PropertyManager.cs b/Source/ACE.Server/Managers/PropertyManager.cs index a3c1125b6c..853b41a36f 100644 --- a/Source/ACE.Server/Managers/PropertyManager.cs +++ b/Source/ACE.Server/Managers/PropertyManager.cs @@ -504,6 +504,7 @@ public static void LoadDefaultProperties() ("pk_server_safe_training_academy", new Property(false, "set this to TRUE to disable pk fighting in training academy and time to exit starter town safely")), ("pkl_server", new Property(false, "set this to TRUE for pink servers")), ("quest_info_enabled", new Property(false, "toggles the /myquests player command")), + ("runrate_add_hooks", new Property(false, "if TRUE, adds some runrate hooks that were missing from retail (exhaustion done, raise skill/attribute")), ("require_spell_comps", new Property(true, "if FALSE spell components are no longer required to be in inventory to cast spells. defaults to enabled, as in retail")), ("salvage_handle_overages", new Property(false, "in retail, if 2 salvage bags were combined beyond 100 structure, the overages would be lost")), ("show_dot_messages", new Property(false, "enabled, shows combat messages for DoT damage ticks. defaults to disabled, as in retail")), diff --git a/Source/ACE.Server/Network/GameAction/Actions/GameActionMoveToState.cs b/Source/ACE.Server/Network/GameAction/Actions/GameActionMoveToState.cs index 220daf41cf..a690245429 100644 --- a/Source/ACE.Server/Network/GameAction/Actions/GameActionMoveToState.cs +++ b/Source/ACE.Server/Network/GameAction/Actions/GameActionMoveToState.cs @@ -11,6 +11,7 @@ public static class GameActionMoveToState public static void Handle(ClientMessage message, Session session) { var moveToState = new MoveToState(session.Player, message.Payload); + session.Player.CurrentMoveToState = moveToState; session.Player.SetRequestedLocation(moveToState.Position); diff --git a/Source/ACE.Server/Network/Motion/InterpretedMotionState.cs b/Source/ACE.Server/Network/Motion/InterpretedMotionState.cs index 900484ae94..a21990e86f 100644 --- a/Source/ACE.Server/Network/Motion/InterpretedMotionState.cs +++ b/Source/ACE.Server/Network/Motion/InterpretedMotionState.cs @@ -85,6 +85,11 @@ public MovementStateFlag BuildMovementFlags() return flags; } + + public bool HasMovement() + { + return (ForwardCommand != MotionCommand.Invalid && ForwardCommand != MotionCommand.Ready) || TurnCommand != MotionCommand.Invalid || SidestepCommand != MotionCommand.Invalid; + } } public static class InterpretedMotionStateExtensions diff --git a/Source/ACE.Server/Network/Motion/MoveToState.cs b/Source/ACE.Server/Network/Motion/MoveToState.cs index a3f1bfbddc..7406b3bb61 100644 --- a/Source/ACE.Server/Network/Motion/MoveToState.cs +++ b/Source/ACE.Server/Network/Motion/MoveToState.cs @@ -26,6 +26,8 @@ public class MoveToState public bool Contact; // verify: contact (indicates if player is on ground), or sticky bit? public bool StandingLongJump; + public MoveToState() { } + public MoveToState(WorldObject wo, BinaryReader reader) { WorldObject = wo; diff --git a/Source/ACE.Server/Network/Motion/MovementData.cs b/Source/ACE.Server/Network/Motion/MovementData.cs index 0c25061677..b08f08b1fa 100644 --- a/Source/ACE.Server/Network/Motion/MovementData.cs +++ b/Source/ACE.Server/Network/Motion/MovementData.cs @@ -29,6 +29,8 @@ public class MovementData public TurnToObject TurnToObject; public TurnToHeading TurnToHeading; + public MovementData() { } + public MovementData(WorldObject wo) { WorldObject = wo; @@ -37,7 +39,7 @@ public MovementData(WorldObject wo) public MovementData(WorldObject wo, Motion motion) { WorldObject = wo; - var sequence = wo.Sequences; + //var sequence = wo.Sequences; // do this here, or in network writer? IsAutonomous = motion.IsAutonomous; diff --git a/Source/ACE.Server/Physics/Animation/MotionTable.cs b/Source/ACE.Server/Physics/Animation/MotionTable.cs index 52b65371ce..9ded0575f8 100644 --- a/Source/ACE.Server/Physics/Animation/MotionTable.cs +++ b/Source/ACE.Server/Physics/Animation/MotionTable.cs @@ -17,9 +17,10 @@ public class MotionTable public Dictionary> Links; public uint DefaultStyle; - public static Dictionary WalkSpeed; - public static Dictionary RunSpeed; - public static Dictionary TurnSpeed; + // TODO: use proper ConcurrentDictionary + public static Dictionary WalkSpeed { get; set; } + public static Dictionary RunSpeed { get; set; } + public static Dictionary TurnSpeed { get; set; } static MotionTable() { @@ -499,7 +500,7 @@ public static float GetRunSpeed(uint motionTableID) return 0.0f; var speed = GetAnimDist(motionData); - RunSpeed.Add(motionTableID, speed); + RunSpeed[motionTableID] = speed; return speed; } @@ -517,7 +518,7 @@ public static float GetTurnSpeed(uint motionTableID) return 0.0f; var speed = Math.Abs(motionData.Omega.Z); - TurnSpeed.Add(motionTableID, speed); + TurnSpeed[motionTableID] = speed; return speed; } diff --git a/Source/ACE.Server/WorldObjects/Creature_Vitals.cs b/Source/ACE.Server/WorldObjects/Creature_Vitals.cs index 4158e4f982..d2557aa370 100644 --- a/Source/ACE.Server/WorldObjects/Creature_Vitals.cs +++ b/Source/ACE.Server/WorldObjects/Creature_Vitals.cs @@ -197,11 +197,13 @@ public float GetStanceMod(CreatureVital vital) // does not apply for mana? if (vital.Vital == PropertyAttribute2nd.MaxMana) return 1.0f; + var forwardCommand = CurrentMovementData.MovementType == MovementType.Invalid ? CurrentMovementData.Invalid.State.ForwardCommand : MotionCommand.Invalid; + // combat mode / running - if (CombatMode != CombatMode.NonCombat || CurrentMotionCommand == MotionCommand.RunForward) + if (CombatMode != CombatMode.NonCombat || forwardCommand == MotionCommand.RunForward) return 0.5f; - switch (CurrentMotionCommand) + switch (forwardCommand) { // TODO: verify multipliers default: diff --git a/Source/ACE.Server/WorldObjects/Managers/EnchantmentManager.cs b/Source/ACE.Server/WorldObjects/Managers/EnchantmentManager.cs index 296221ffc7..c099696cae 100644 --- a/Source/ACE.Server/WorldObjects/Managers/EnchantmentManager.cs +++ b/Source/ACE.Server/WorldObjects/Managers/EnchantmentManager.cs @@ -319,12 +319,6 @@ public virtual void Remove(BiotaPropertiesEnchantmentRegistry entry, bool sound if (sound && entry.SpellCategory != SpellCategory_Cooldown) Player.Session.Network.EnqueueSend(new GameMessageSound(Player.Guid, Sound.SpellExpire, 1.0f)); - - if (entry.SpellCategory != SpellCategory_Cooldown) - { - var spell = new Spell(spellID); - Player.HandleMaxVitalUpdate(spell); - } } else { @@ -472,12 +466,7 @@ public virtual void Dispel(BiotaPropertiesEnchantmentRegistry entry) WorldObject.ChangesDetected = true; if (Player != null) - { Player.Session.Network.EnqueueSend(new GameEventMagicDispelEnchantment(Player.Session, (ushort)entry.SpellId, entry.LayerId)); - - var spell = new Spell(spellID); - Player.HandleMaxVitalUpdate(spell); - } } /// @@ -492,12 +481,6 @@ public virtual void Dispel(List entries) { if (WorldObject.Biota.TryRemoveEnchantment(entry, out _, WorldObject.BiotaDatabaseLock)) WorldObject.ChangesDetected = true; - - if (Player != null) - { - var spell = new Spell(entry.SpellId); - Player.HandleMaxVitalUpdate(spell); - } } if (Player != null) Player.Session.Network.EnqueueSend(new GameEventMagicDispelMultipleEnchantments(Player.Session, entries)); diff --git a/Source/ACE.Server/WorldObjects/Managers/EnchantmentManagerWithCaching.cs b/Source/ACE.Server/WorldObjects/Managers/EnchantmentManagerWithCaching.cs index cdcb4053cb..ecf2ed5067 100644 --- a/Source/ACE.Server/WorldObjects/Managers/EnchantmentManagerWithCaching.cs +++ b/Source/ACE.Server/WorldObjects/Managers/EnchantmentManagerWithCaching.cs @@ -52,6 +52,12 @@ public override void Remove(BiotaPropertiesEnchantmentRegistry entry, bool sound base.Remove(entry, sound); ClearCache(); + + if (entry.SpellCategory != SpellCategory_Cooldown) + { + var spell = new Spell(entry.SpellId); + Player.HandleSpellHooks(spell); + } } /// @@ -99,6 +105,12 @@ public override void Dispel(BiotaPropertiesEnchantmentRegistry entry) base.Dispel(entry); ClearCache(); + + if (Player != null) + { + var spell = new Spell(entry.SpellId); + Player.HandleSpellHooks(spell); + } } /// @@ -109,6 +121,15 @@ public override void Dispel(List entries) base.Dispel(entries); ClearCache(); + + if (Player != null) + { + foreach (var entry in entries) + { + var spell = new Spell(entry.SpellId); + Player.HandleSpellHooks(spell); + } + } } diff --git a/Source/ACE.Server/WorldObjects/Monster_Navigation.cs b/Source/ACE.Server/WorldObjects/Monster_Navigation.cs index 5eab0ac03e..edf7e43db8 100644 --- a/Source/ACE.Server/WorldObjects/Monster_Navigation.cs +++ b/Source/ACE.Server/WorldObjects/Monster_Navigation.cs @@ -349,8 +349,24 @@ public void GetMovementSpeed() /// public float GetRunRate() { + var burden = 0.0f; + + // assuming burden only applies to players... + if (this is Player player) + { + var strength = Strength.Current; + + var capacity = EncumbranceSystem.EncumbranceCapacity((int)strength, player.AugmentationIncreasedCarryingCapacity); + burden = EncumbranceSystem.GetBurden(capacity, EncumbranceVal ?? 0); + + // TODO: find this exact formula in client + // technically this would be based on when the player releases / presses the movement key after stamina > 0 + if (player.IsExhausted) + burden = 3.0f; + } + var runSkill = GetCreatureSkill(Skill.Run).Current; - var runRate = MovementSystem.GetRunRate(0.0f, (int)runSkill, 1.0f); + var runRate = MovementSystem.GetRunRate(burden, (int)runSkill, 1.0f); return (float)runRate; } diff --git a/Source/ACE.Server/WorldObjects/Player.cs b/Source/ACE.Server/WorldObjects/Player.cs index 5faa635f55..b98d71ed9b 100644 --- a/Source/ACE.Server/WorldObjects/Player.cs +++ b/Source/ACE.Server/WorldObjects/Player.cs @@ -753,22 +753,54 @@ public void HandleActionJump(JumpPack jump) /// public void OnExhausted() { - // adjust player speed if running - if (CurrentMotionCommand == MotionCommand.RunForward && !IsJumping) - { - // verify - forced commands from server should be non-autonomous, but could have been sent as autonomous in retail? - // if set to autonomous here, the desired effect doesn't happen - // motion.IsAutonomous = true; - var motion = new Motion(this, MotionCommand.RunForward); - - CurrentMotionState = motion; + // adjust player speed if they are currently pressing movement keys + HandleRunRateUpdate(); - if (CurrentLandblock != null) - EnqueueBroadcastMotion(motion); - } Session.Network.EnqueueSend(new GameEventCommunicationTransientString(Session, "You're Exhausted!")); } + /// + /// Detects changes in the player's RunRate -- + /// If there are changes, re-broadcasts player movement packet + /// + public bool HandleRunRateUpdate() + { + //Console.WriteLine($"{Name}.HandleRunRateUpdates()"); + + if (CurrentMovementData.MovementType != MovementType.Invalid) + return false; + + var prevState = CurrentMovementData.Invalid.State; + + var movementData = new MovementData(this, CurrentMoveToState); + var currentState = movementData.Invalid.State; + + var changed = currentState.ForwardSpeed != prevState.ForwardSpeed || + currentState.TurnSpeed != prevState.TurnSpeed || + currentState.SidestepSpeed != prevState.SidestepSpeed; + + if (!changed) + return false; + + //Console.WriteLine($"Old: {prevState.ForwardSpeed}, New: {currentState.ForwardSpeed}"); + + if (!CurrentMovementData.Invalid.State.HasMovement() || IsJumping) + return false; + + //Console.WriteLine($"{Name}.OnRunRateChanged()"); + + CurrentMovementData = new MovementData(this, CurrentMoveToState); + + // verify - forced commands from server should be non-autonomous, but could have been sent as autonomous in retail? + // if set to autonomous here, the desired effect doesn't happen + CurrentMovementData.IsAutonomous = false; + + var movementEvent = new GameMessageUpdateMotion(this, CurrentMovementData); + EnqueueBroadcast(movementEvent); // broadcast to all players, including self + + return true; + } + /// /// Returns a modifier for a player's Run, Jump, Melee Defense, and Missile Defense skills if they are overburdened /// diff --git a/Source/ACE.Server/WorldObjects/Player_Attributes.cs b/Source/ACE.Server/WorldObjects/Player_Attributes.cs index 124bb04759..bf2f446eb4 100644 --- a/Source/ACE.Server/WorldObjects/Player_Attributes.cs +++ b/Source/ACE.Server/WorldObjects/Player_Attributes.cs @@ -3,6 +3,7 @@ using ACE.DatLoader; using ACE.Entity.Enum; using ACE.Entity.Enum.Properties; +using ACE.Server.Managers; using ACE.Server.Network; using ACE.Server.Network.GameMessages; using ACE.Server.Network.GameMessages.Messages; @@ -55,6 +56,10 @@ public void RaiseAttributeGameAction(PropertyAttribute attribute, uint amount) { Session.Network.EnqueueSend(abilityUpdate, soundEvent, message); } + + // retail was missing the 'raise attribute' runrate hook here + if ((attribute == PropertyAttribute.Strength || attribute == PropertyAttribute.Quickness) && PropertyManager.GetBool("runrate_add_hooks").Item) + HandleRunRateUpdate(); } else { diff --git a/Source/ACE.Server/WorldObjects/Player_Magic.cs b/Source/ACE.Server/WorldObjects/Player_Magic.cs index 525accd1ff..c07f61597f 100644 --- a/Source/ACE.Server/WorldObjects/Player_Magic.cs +++ b/Source/ACE.Server/WorldObjects/Player_Magic.cs @@ -1422,6 +1422,12 @@ public bool VerifySpell(uint spellId, bool builtInSpell = false) // send error message? } + public void HandleSpellHooks(Spell spell) + { + HandleMaxVitalUpdate(spell); + HandleRunRateUpdate(spell); + } + /// /// Called when an enchantment is added or removed, /// checks if the spell affects the max vitals, @@ -1447,5 +1453,13 @@ public void HandleMaxVitalUpdate(Spell spell) }); actionChain.EnqueueChain(); } + + public bool HandleRunRateUpdate(Spell spell) + { + if (!spell.UpdatesRunRate) + return false; + + return HandleRunRateUpdate(); + } } } diff --git a/Source/ACE.Server/WorldObjects/Player_Networking.cs b/Source/ACE.Server/WorldObjects/Player_Networking.cs index d6bb218cbd..0a46cb5390 100644 --- a/Source/ACE.Server/WorldObjects/Player_Networking.cs +++ b/Source/ACE.Server/WorldObjects/Player_Networking.cs @@ -228,7 +228,8 @@ public void BroadcastMovement(MoveToState moveToState) EnqueueBroadcast(false, movementEvent); // shouldn't need to go to originating player? // TODO: use real motion / animation system from physics - CurrentMotionCommand = movementData.Invalid.State.ForwardCommand; + //CurrentMotionCommand = movementData.Invalid.State.ForwardCommand; + CurrentMovementData = movementData; } private EnvironChangeType? currentFogColor; diff --git a/Source/ACE.Server/WorldObjects/Player_Skills.cs b/Source/ACE.Server/WorldObjects/Player_Skills.cs index e4f6284b8c..58f78388ac 100644 --- a/Source/ACE.Server/WorldObjects/Player_Skills.cs +++ b/Source/ACE.Server/WorldObjects/Player_Skills.cs @@ -249,6 +249,10 @@ public void RaiseSkillGameAction(Skill skill, uint amount) Session.Network.EnqueueSend(new GameMessagePrivateUpdateSkill(this, creatureSkill)); Session.Network.EnqueueSend(new GameMessageSound(Guid, Sound.RaiseTrait, 1f)); Session.Network.EnqueueSend(new GameMessageSystemChat(messageText, ChatMessageType.Advancement)); + + // retail was missing the 'raise skill' runrate hook here + if (skill == Skill.Run && PropertyManager.GetBool("runrate_add_hooks").Item) + HandleRunRateUpdate(); } else if (prevXP != creatureSkill.ExperienceSpent) { diff --git a/Source/ACE.Server/WorldObjects/Player_Vitals.cs b/Source/ACE.Server/WorldObjects/Player_Vitals.cs index 12ea81be45..5533a6cbf4 100644 --- a/Source/ACE.Server/WorldObjects/Player_Vitals.cs +++ b/Source/ACE.Server/WorldObjects/Player_Vitals.cs @@ -5,6 +5,7 @@ using ACE.Entity; using ACE.Entity.Enum; using ACE.Entity.Enum.Properties; +using ACE.Server.Managers; using ACE.Server.Network; using ACE.Server.Network.GameEvent.Events; using ACE.Server.Network.GameMessages; @@ -176,22 +177,30 @@ public override void SetMaxVitals() /// The actual change in the vital, after clamping between 0 and MaxVital public override int UpdateVital(CreatureVital vital, int newVal) { + var prevVal = vital.Current; + var change = base.UpdateVital(vital, newVal); - if (change != 0) - { - Session.Network.EnqueueSend(new GameMessagePrivateUpdateAttribute2ndLevel(this, vital.ToEnum(), vital.Current)); + if (change == 0) + return 0; - if (Fellowship != null) - FellowVitalUpdate = true; - } + Session.Network.EnqueueSend(new GameMessagePrivateUpdateAttribute2ndLevel(this, vital.ToEnum(), vital.Current)); + + if (Fellowship != null) + FellowVitalUpdate = true; // check for exhaustion if (vital.Vital == PropertyAttribute2nd.Stamina || vital.Vital == PropertyAttribute2nd.MaxStamina) { - if (change != 0 && vital.Current <= 0) + if (newVal == 0) + { OnExhausted(); - + } + // retail was missing the 'exhausted done' automatic hook here + else if (prevVal == 0 && PropertyManager.GetBool("runrate_add_hooks").Item) + { + HandleRunRateUpdate(); + } } return change; } diff --git a/Source/ACE.Server/WorldObjects/WorldObject_Magic.cs b/Source/ACE.Server/WorldObjects/WorldObject_Magic.cs index 7344976eda..0dd21e8183 100644 --- a/Source/ACE.Server/WorldObjects/WorldObject_Magic.cs +++ b/Source/ACE.Server/WorldObjects/WorldObject_Magic.cs @@ -1344,7 +1344,7 @@ public EnchantmentStatus CreateEnchantment(WorldObject target, WorldObject caste { playerTarget.Session.Network.EnqueueSend(new GameEventMagicUpdateEnchantment(playerTarget.Session, new Enchantment(playerTarget, addResult.Enchantment))); - playerTarget.HandleMaxVitalUpdate(spell); + playerTarget.HandleSpellHooks(spell); } if (playerTarget == null && target.Wielder is Player wielder) diff --git a/Source/ACE.Server/WorldObjects/WorldObject_Properties.cs b/Source/ACE.Server/WorldObjects/WorldObject_Properties.cs index 18bbdca74d..5ac9ef2fd1 100644 --- a/Source/ACE.Server/WorldObjects/WorldObject_Properties.cs +++ b/Source/ACE.Server/WorldObjects/WorldObject_Properties.cs @@ -12,6 +12,7 @@ using ACE.Entity.Enum.Properties; using ACE.Server.Entity; using ACE.Server.Managers; +using ACE.Server.Network.Structure; namespace ACE.Server.WorldObjects { @@ -558,13 +559,15 @@ public void RemovePosition(PositionType positionType) } } - // ======================================== // ======== Physics Desc Properties ======= // ======================================== // used in CalculatedPhysicsDescriptionFlag() public Motion CurrentMotionState { get; set; } - public MotionCommand CurrentMotionCommand { get; set; } + + public MoveToState CurrentMoveToState { get; set; } = new MoveToState(); + public MovementData CurrentMovementData { get; set; } = new MovementData(); + public Placement? Placement // Sometimes known as AnimationFrame {