// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.GameLogic; using MUnique.OpenMU.Network; /// /// Validator for skill hits, which were done by skills with type . /// public class SkillHitValidator { private const int MaximumCounterValue = 0x32; private const byte TwisterSkillId = 8; private const byte EvilSpiritSkillId = 9; private const byte MultishotSkillId = 235; private static readonly TimeSpan MaxAnimationToHitDelay = TimeSpan.FromSeconds(10); private readonly ILogger _logger; /// /// The counter which keeps the expected count of the next animation and hit. /// private readonly Counter _counter = new(1, MaximumCounterValue); private readonly HitEntry[] _hits = new HitEntry[MaximumCounterValue + 1]; private byte _lastTwisterIndex; private byte _lastMultishotIndex; /// /// The game client doesn't reset its counter when it connects with a new account. /// So, the simplest way for us is, to accept the first counter we get with /// an animation. /// private bool _isFirstAfterConnectionEstablished = true; /// /// Initializes a new instance of the class. /// /// The logger. public SkillHitValidator(ILogger logger) { this._logger = logger; this._counter.Reset(); } /// /// Gets the last registered skill identifier. /// public ushort LastRegisteredSkillId { get; private set; } /// /// Tries to register an animation with the specified counter. /// /// The skill identifier. /// The animation counter. /// /// True, if successful. /// /// Basically, only skills with overlapping animations and hits send an animation counter greater than 0. public bool TryRegisterAnimation(ushort skillId, byte animationCounter) { if (skillId == TwisterSkillId) { // Twister is a implemented wrong at the client side. It sends a counter of the animations here, but not in the hit packets. this._lastTwisterIndex = animationCounter; } if (skillId == MultishotSkillId) { // Multishot is a implemented wrong at the client side. It sends a counter of the animations here, but not in the hit packets. this._lastMultishotIndex = animationCounter; } if (skillId == EvilSpiritSkillId || skillId == TwisterSkillId || animationCounter > 0) { if (this._isFirstAfterConnectionEstablished) { this._counter.Count = animationCounter; this._isFirstAfterConnectionEstablished = false; } if (this._counter.Count != animationCounter) { this._logger.LogWarning($"Animation count out of sync - hacker? Expected: {this._counter.Count}, Actual: {animationCounter}."); return false; } this._counter.Increase(); } this.LastRegisteredSkillId = skillId; this._hits[animationCounter] = new HitEntry(skillId, DateTime.UtcNow, true, 0); return true; } /// /// Increases the counter by one, after a hit was done. /// It must just be called once after a combined hit. /// public void IncreaseCounterAfterHit() { this._counter.Increase(); } /// /// Determines whether a hit request is valid for the specified skill identifier. /// /// The skill identifier. /// The animation counter. /// The hit counter. /// /// true if a hit request is valid for the specified skill identifier; otherwise, false. /// public (bool IsValid, bool IncreaseCounter) IsHitValid(ushort skillId, byte animationCounter, byte hitCounter) { if (animationCounter == 0 && skillId == TwisterSkillId) { // Twister is implemented wrong at the client side. It doesn't send an animation counter in hit packets. animationCounter = this._lastTwisterIndex; } if (animationCounter == 0 && skillId == MultishotSkillId) { // Multishot is implemented wrong at the client side. It doesn't send an animation counter in hit packets. animationCounter = this._lastMultishotIndex; } if (this._hits[animationCounter] is { } animationEntry) { /* if (this._hits[hitCounter] is { } currentHitEntry && currentHitEntry.TimeStamp >= DateTime.UtcNow.AddSeconds(-1)) { // probably speed too high to allow any check this._hits[animationCounter] = animationEntry with { Skill = skillId, IsAnimation = true, HitCount = animationEntry.HitCount + 1, }; this._logger.LogDebug($"Hits are too fast, skipping validity check."); this._hits[hitCounter] = new HitEntry(skillId, DateTime.UtcNow, false, 0); return (true, true); } if (!animationEntry.IsAnimation) { this._logger.LogWarning("Possible Hacker - Skill Hit Invalid because the given animation counter wasn't registered as animation."); return (false, false); } var expectedCount = this._counter.Count; if (expectedCount != hitCounter) { this._logger.LogWarning($"Hit count out of sync - hacker? Expected: {this._counter.Count}, Actual: {hitCounter}."); return (false, false); } if (animationEntry.Skill != skillId) { this._logger.LogWarning($"Wrong skill in referenced animation - hacker?"); return (false, false); } var newCount = animationEntry.HitCount + 1; this._hits[animationCounter] = animationEntry with { HitCount = newCount }; this._hits[hitCounter] = new HitEntry(skillId, DateTime.UtcNow, false, 0); var timestampDiff = DateTime.UtcNow - animationEntry.TimeStamp; if (timestampDiff > MaxAnimationToHitDelay) { this._logger.LogWarning("Possible Hacker - Skill Hit Invalid because of too high time difference between animation and hit"); return (false, true); } */ return (true, true); } else { this._logger.LogWarning("Possible Hacker - Skill Hit Invalid because of missing previous animation."); return (false, false); } } private record struct HitEntry(ushort Skill, DateTime TimeStamp, bool IsAnimation, int HitCount); }