//
// 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);
}