Skip to content

Commit

Permalink
Merge pull request #30 from ewrogers/ability-conditions
Browse files Browse the repository at this point in the history
Ability conditions
  • Loading branch information
ewrogers authored Jun 28, 2023
2 parents cdb1ea3 + 9636067 commit fa03d4f
Show file tree
Hide file tree
Showing 29 changed files with 813 additions and 161 deletions.
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,34 @@ All notable changes to this library will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [4.10.1] - 2023-06-27

### Added

- `MinHealthPercent` to ability metadata
- `MaxHealthPercent` to ability metadata (Crasher/Animal Feast/Execute)
- Visual indicator on spell queue when waiting on HP thresholds
- `OpensDialog` to spell metadata (was only skills before)
- HP threshold in ability tooltips
- More tooltips in metadata editors

### Changed

- Use new metadata properties for HP based conditions on abilities
- Redesign some metadata editors for skill/spells
- Increase dialog size for skill/spell metadata editors
- Tooltip design

### Removed

- Removed hard-coded names for HP-based skills (now is customizable)

### Fixed

- Improved dialog closing (deferred dispatcher)
- Autosave load error popup & info
- Thousands formatting for HP/MP

## [4.10.0] - 2023-06-27

### Added
Expand Down
1 change: 1 addition & 0 deletions SleepHunter/App.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<converters:LessThanConverter x:Key="LessThanConverter"/>
<converters:LessThanOrEqualConverter x:Key="LessThanOrEqualConverter"/>
<converters:NumericConverter x:Key="NumericConverter"/>
<converters:NotNullConverter x:Key="NotNullConverter"/>
<converters:EquipmentSlotConverter x:Key="EquipmentSlotConverter"/>
<converters:PlayerClassConverter x:Key="PlayerClassConverter"/>
<converters:TimeSpanConverter x:Key="TimeSpanConverter"/>
Expand Down
6 changes: 6 additions & 0 deletions SleepHunter/Common/DeferredAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using System;

namespace SleepHunter.Common
{
public readonly record struct DeferredAction(Action Action, DateTime ExecutionTime) { }
}
69 changes: 69 additions & 0 deletions SleepHunter/Common/DeferredDispatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.Threading;

namespace SleepHunter.Common
{
public sealed class DeferredDispatcher
{
private readonly ReaderWriterLockSlim readerWriterLock = new();
private readonly List<DeferredAction> actions = new();

public DeferredDispatcher() { }

public void DispatchAfter(Action action, TimeSpan delay) => DispatchAt(action, DateTime.Now + delay);

public void DispatchAt(Action action, DateTime timestamp)
{
if (action == null)
throw new ArgumentNullException(nameof(action));

readerWriterLock.EnterWriteLock();

try
{
var deferred = new DeferredAction(action, timestamp);
actions.Add(deferred);
}
finally
{
readerWriterLock.ExitWriteLock();
}
}

public void Tick()
{
readerWriterLock.EnterUpgradeableReadLock();

try
{
for (var i = actions.Count - 1; i >= 0; i--)
{
var action = actions[i];

// If execution time is in the future, wait
if (action.ExecutionTime > DateTime.Now)
continue;

// Remove the entry from the deferred action queue
readerWriterLock.EnterWriteLock();
try
{
actions.RemoveAt(i);
}
finally
{
readerWriterLock.ExitWriteLock();
}

// Perform the action (outside of the write lock)
action.Action();
}
}
finally
{
readerWriterLock.ExitUpgradeableReadLock();
}
}
}
}
23 changes: 18 additions & 5 deletions SleepHunter/Converters/ComparisonConverters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
}

Expand All @@ -36,7 +36,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
}

Expand All @@ -54,7 +54,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
}

Expand All @@ -72,7 +72,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
}

Expand All @@ -87,7 +87,20 @@ public object Convert(object value, Type targetType, object parameter, CultureIn

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
}

public sealed class NotNullConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value is not null;
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
}
4 changes: 2 additions & 2 deletions SleepHunter/Converters/NumericConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ public object Convert(object value, Type targetType, object parameter, CultureIn
{
if (integerValue >= MillionsThreshold)
{
var fractionalMillions = integerValue / (double)MillionsThreshold;
var fractionalMillions = integerValue / 1_000_000.0;
return $"{fractionalMillions:0.0}m";
}
else if (integerValue >= ThousandsThreshold)
{
var fractionalThousands = integerValue / (double)ThousandsThreshold;
var fractionalThousands = integerValue / 1_000.0;
return $"{fractionalThousands:0}k";
}
}
Expand Down
57 changes: 32 additions & 25 deletions SleepHunter/Macro/PlayerMacroState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using System.Linq;
using System.Threading;
using System.Windows;

using SleepHunter.Common;
using SleepHunter.Metadata;
using SleepHunter.Models;
using SleepHunter.Settings;
Expand All @@ -14,8 +14,9 @@ public sealed class PlayerMacroState : MacroState
{
private static readonly TimeSpan PanelTimeout = TimeSpan.FromSeconds(1);
private static readonly TimeSpan SwitchDelay = TimeSpan.FromMilliseconds(100);
private static readonly string[] CrasherSkillNames = new[] { "Crasher", "Animal Feast", "Execute" };
private static readonly TimeSpan DialogDelay = TimeSpan.FromSeconds(2);

private readonly DeferredDispatcher deferredDispatcher = new();
private readonly ReaderWriterLockSlim spellQueueLock = new();
private readonly ReaderWriterLockSlim flowerQueueLock = new();

Expand Down Expand Up @@ -392,6 +393,9 @@ protected override void MacroLoop(object argument)
{
client.Update(PlayerFieldFlags.GameClient);

// Tick the dispatcher so any scheduled events go off
deferredDispatcher.Tick();

if (client.GameClient.IsUserChatting)
{
SetPlayerStatus(PlayerMacroStatus.ChatIsUp);
Expand Down Expand Up @@ -527,6 +531,10 @@ private bool DoSkillMacro(out bool didAssail)
skillList.Add(skill);
}

// Update stats for current HP if any skill might need it for evaluating min/max thresholds
if (skillList.Any(skill => skill.MinHealthPercent.HasValue || skill.MaxHealthPercent.HasValue))
client.Update(PlayerFieldFlags.Stats);

foreach (var skill in skillList.OrderBy((s) => { return s.OpensDialog; }))
{
client.Update(PlayerFieldFlags.GameClient);
Expand All @@ -544,17 +552,13 @@ private bool DoSkillMacro(out bool didAssail)
}
}

// Crasher skill (requires < 2% HP)
if (CrasherSkillNames.Contains(skill.Name, StringComparer.OrdinalIgnoreCase))
{
client.Update(PlayerFieldFlags.Stats);
if (client.Stats.HealthPercent >= 2)
continue;

// TODO: Add Mad Soul + Sacrifice support!
// Min health percentage (ex: > 90%), skip if cannot use YET
if (skill.MinHealthPercent.HasValue && skill.MinHealthPercent.Value >= client.Stats.HealthPercent)
continue;

// TODO: Add auto-hemloch + hemloch deum support!
}
// Max health percentage (ex < 2%), skip if cannot use YET
if (skill.MaxHealthPercent.HasValue && client.Stats.HealthPercent > skill.MaxHealthPercent.Value)
continue;

if (client.SwitchToPanelAndWait(skill.Panel, TimeSpan.FromSeconds(1), out var didRequireSwitch, useShiftKey))
{
Expand All @@ -566,8 +570,9 @@ private bool DoSkillMacro(out bool didAssail)
}
}

// Close the dialog after a few seconds
if (expectDialog)
client.CancelDialog();
deferredDispatcher.DispatchAfter(client.CancelDialog, DialogDelay);

if (useSpaceForAssail && isAssailQueued)
{
Expand Down Expand Up @@ -604,9 +609,15 @@ private bool DoSpellMacro()
return false;
}

nextSpell.IsUndefined = !SpellMetadataManager.Instance.ContainsSpell(nextSpell.Name);
var spellMetadata = SpellMetadataManager.Instance.GetSpell(nextSpell.Name);
nextSpell.IsUndefined = spellMetadata == null;

CastSpell(nextSpell);

// Close the dialog after a few seconds
if (spellMetadata?.OpensDialog ?? false)
deferredDispatcher.DispatchAfter(client.CancelDialog, DialogDelay);

lastUsedSpellItem = nextSpell;
lastUsedSpellItem.IsActive = true;
return true;
Expand Down Expand Up @@ -913,32 +924,28 @@ private SpellQueueItem GetNextSpell()
private SpellQueueItem GetNextSpell_NoRotation(bool skipOnCooldown = true)
{
if (skipOnCooldown)
return spellQueue.FirstOrDefault(spell => !spell.IsOnCooldown);
return spellQueue.FirstOrDefault(spell => !spell.IsWaitingOnHealth && !spell.IsOnCooldown);
else
return spellQueue.FirstOrDefault();
return spellQueue.FirstOrDefault(spell => !spell.IsWaitingOnHealth);
}

private SpellQueueItem GetNextSpell_SingularOrder(bool skipOnCooldown = true)
{
if (skipOnCooldown)
return spellQueue.FirstOrDefault(spell => !spell.IsOnCooldown && !spell.IsDone);
return spellQueue.FirstOrDefault(spell => !spell.IsWaitingOnHealth && !spell.IsOnCooldown && !spell.IsDone);
else
return spellQueue.FirstOrDefault(spell => !spell.IsDone);
return spellQueue.FirstOrDefault(spell => !spell.IsWaitingOnHealth && !spell.IsDone);
}

private SpellQueueItem GetNextSpell_RoundRobin(bool skipOnCooldown = true)
{
// All spells are done, nothing to cast
if (spellQueue.All(spell => spell.IsDone))
return null;

// All spells are on cooldown, and skipping so nothing to do
if (spellQueue.All(spell => spell.IsOnCooldown) && skipOnCooldown)
// All spells are unavailable, nothing to do
if (spellQueue.All(spell => spell.IsWaitingOnHealth || spell.IsOnCooldown || spell.IsDone))
return null;

var currentSpell = spellQueue.ElementAt(spellQueueIndex);

while (currentSpell.IsDone || (skipOnCooldown && currentSpell.IsOnCooldown))
while (currentSpell.IsWaitingOnHealth || currentSpell.IsDone || (skipOnCooldown && currentSpell.IsOnCooldown))
currentSpell = AdvanceToNextSpell();

// Round robin rotation for next time
Expand Down
22 changes: 21 additions & 1 deletion SleepHunter/Metadata/SkillMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public sealed class SkillMetadata : ObservableObject
private bool canImprove = true;
private TimeSpan cooldown;
private bool requiresDisarm;
private double minHealthPercent;
private double maxHealthPercent;

[XmlAttribute("Name")]
public string Name
Expand Down Expand Up @@ -91,13 +93,29 @@ public double CooldownSeconds
}

[XmlAttribute("RequiresDisarm")]
[DefaultValueAttribute(false)]
[DefaultValue(false)]
public bool RequiresDisarm
{
get => requiresDisarm;
set => SetProperty(ref requiresDisarm, value);
}

[XmlAttribute("MinHealthPercent")]
[DefaultValue(0)]
public double MinHealthPercent
{
get => minHealthPercent;
set => SetProperty(ref minHealthPercent, value);
}

[XmlAttribute("MaxHealthPercent")]
[DefaultValue(0)]
public double MaxHealthPercent
{
get => maxHealthPercent;
set => SetProperty(ref maxHealthPercent, value);
}

public SkillMetadata() { }

public override string ToString() => Name ?? "Unknown Skill";
Expand All @@ -113,6 +131,8 @@ public void CopyTo(SkillMetadata other)
other.OpensDialog = OpensDialog;
other.CanImprove = CanImprove;
other.RequiresDisarm = RequiresDisarm;
other.MinHealthPercent = MinHealthPercent;
other.MaxHealthPercent = MaxHealthPercent;
}
}
}
Loading

0 comments on commit fa03d4f

Please sign in to comment.