Skip to content

Commit

Permalink
Merge pull request #24 from ewrogers/spell-rotation
Browse files Browse the repository at this point in the history
Spell rotation
  • Loading branch information
ewrogers authored Jun 16, 2023
2 parents 1124890 + 73adc64 commit 77bacef
Show file tree
Hide file tree
Showing 19 changed files with 327 additions and 74 deletions.
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,28 @@ 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.7.0] - 2023-06-16

### Added

- Spell rotation combo box in Spell Queue for per-character setting
- Spell rotation character setting is preserved in saved state
- Spell cooldown indicator in Spell Queue
- New option for `Skip Spells on Cooldown` for `Spell Macros` (default is `Enabled`)
- Spells on cooldown will be skipped, even in no rotation/singular order (when enabled)
- More accessibility key for checkboxes in `Spell Macro` settings

### Changed

- `Spell Rotation Mode` renamed `Default Spell Queue Rotation` to better describe it can be overriden
- `UserSettings` are now version `1.5`
- Now format health/mana using `k` and `m` suffixes for thousands/millions (ex: `256k`, `1.2m`)

### Fixed

- Some staff line changes
- Better spell queue rotation handling

## [4.6.1] - 2023-06-03

### Added
Expand Down
1 change: 1 addition & 0 deletions SleepHunter/App.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<converters:GreaterThanOrEqualConverter x:Key="GreaterThanOrEqualConverter"/>
<converters:LessThanConverter x:Key="LessThanConverter"/>
<converters:LessThanOrEqualConverter x:Key="LessThanOrEqualConverter"/>
<converters:NumericConverter x:Key="NumericConverter"/>
<converters:PlayerClassConverter x:Key="PlayerClassConverter"/>
<converters:TimeSpanConverter x:Key="TimeSpanConverter"/>
<converters:VisibilityConverter x:Key="VisibilityConverter"/>
Expand Down
49 changes: 44 additions & 5 deletions SleepHunter/Converters/NumericConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,43 @@ namespace SleepHunter.Converters
{
public sealed class NumericConverter : IValueConverter
{
private const int ThousandsThreshold = 1_000;
private const int MillionsThreshold = 1_000_000;

public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var isHexadecimal = string.Equals("Hexadecimal", parameter as string, StringComparison.OrdinalIgnoreCase);
var isThousands = string.Equals("Thousands", parameter as string, StringComparison.OrdinalIgnoreCase);

var integerValue = System.Convert.ToInt64(value, CultureInfo.InvariantCulture);

if (isHexadecimal)
{
var hexValue = (uint)value;
return hexValue.ToString("X");
return integerValue.ToString("X");
}

if (isThousands && integerValue > ThousandsThreshold)
{
if (integerValue >= MillionsThreshold)
{
var fractionalMillions = integerValue / (double)MillionsThreshold;
return $"{fractionalMillions:0.0}m";
}
else if (integerValue >= ThousandsThreshold)
{
var fractionalThousands = integerValue / (double)ThousandsThreshold;
return $"{fractionalThousands:0}k";
}
}

var decValue = (double)value;
return decValue.ToString();
return value.ToString();
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
var isHexadecimal = string.Equals("Hexadecimal", parameter as string, StringComparison.OrdinalIgnoreCase);
var isThousands = string.Equals("Thousands", parameter as string, StringComparison.OrdinalIgnoreCase);

var valueString = value as string;

if (isHexadecimal)
Expand All @@ -33,11 +53,30 @@ public object ConvertBack(object value, Type targetType, object parameter, Cultu
return hexValue;
}

if (isThousands)
{
if (valueString.EndsWith("m"))
{
var trimmedString = valueString.TrimEnd('m');
if (!double.TryParse(trimmedString, NumberStyles.Float, null, out var doubleValue))
return 0;
else
return (int)Math.Round(doubleValue * 1_000_000.0);
}
else if (valueString.EndsWith("k"))
{
var trimmedString = valueString.TrimEnd('m');
if (!double.TryParse(trimmedString, NumberStyles.Float, null, out var doubleValue))
return 0;
else
return (int)Math.Round(doubleValue * 1_000.0);
}
}

if (!double.TryParse(valueString, out var decValue))
return 0;
else
return decValue;

}
}
}
2 changes: 2 additions & 0 deletions SleepHunter/Macro/MacroManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ public void ImportMacroState(Player player, SavedMacroState state)
macro.ClearFlowerQueue();

player.Update(PlayerFieldFlags.Spellbook);

macro.SpellQueueRotation = state.SpellRotation;
macro.UseLyliacVineyard = player.HasLyliacVineyard && state.UseLyliacVineyard;
macro.FlowerAlternateCharacters = player.HasLyliacPlant && state.FlowerAlternateCharacters;

Expand Down
105 changes: 76 additions & 29 deletions SleepHunter/Macro/PlayerMacroState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,16 @@ public sealed class PlayerMacroState : MacroState

private int spellQueueIndex;
private int flowerQueueIndex;

private SpellRotationMode spellQueueRotation;
private bool isWaitingOnMana;
private bool useLyliacVineyard;
private bool flowerAlternateCharacters;
private bool skipSpellsOnCooldown;

private DateTime spellCastTimestamp;
private TimeSpan spellCastDuration;

private SpellQueueItem lastUsedSpellItem;
private SpellQueueItem fasSpioradQueueItem;
private SpellQueueItem lyliacPlantQueueItem;
Expand Down Expand Up @@ -61,22 +66,34 @@ public PlayerMacroStatus PlayerStatus

public int FlowerQueueCount => flowerQueue.Count;

public SpellRotationMode SpellQueueRotation
{
get => spellQueueRotation;
set => SetProperty(ref spellQueueRotation, value);
}

public bool IsWaitingOnMana
{
get => isWaitingOnMana;
set => SetProperty(ref isWaitingOnMana, value, nameof(IsWaitingOnMana));
set => SetProperty(ref isWaitingOnMana, value);
}

public bool UseLyliacVineyard
{
get => useLyliacVineyard;
set => SetProperty(ref useLyliacVineyard, value, nameof(UseLyliacVineyard));
set => SetProperty(ref useLyliacVineyard, value);
}

public bool FlowerAlternateCharacters
{
get => flowerAlternateCharacters;
set => SetProperty(ref flowerAlternateCharacters, value, nameof(FlowerAlternateCharacters));
set => SetProperty(ref flowerAlternateCharacters, value);
}

public bool SkipSpellsOnCooldown
{
get => skipSpellsOnCooldown;
set => SetProperty(ref skipSpellsOnCooldown, value);
}

public DateTime SpellCastTimestamp
Expand Down Expand Up @@ -437,7 +454,7 @@ protected override void MacroLoop(object argument)
{
if (flowerQueue.Count > 0)
SetPlayerStatus(PlayerMacroStatus.ReadyToFlower);
else if (client.Skillbook.ActiveSkills.Count() > 0)
else if (client.Skillbook.ActiveSkills.Any())
SetPlayerStatus(PlayerMacroStatus.Waiting);
else
SetPlayerStatus(PlayerMacroStatus.Idle);
Expand Down Expand Up @@ -827,34 +844,22 @@ private SpellQueueItem GetLyliacVineyard()

private SpellQueueItem GetNextSpell()
{
client.Update(PlayerFieldFlags.Spellbook);

var shouldRotate = UserSettingsManager.Instance.Settings.SpellRotationMode != SpellRotationMode.None;
var isRoundRobin = UserSettingsManager.Instance.Settings.SpellRotationMode == SpellRotationMode.RoundRobin;

if (spellQueueIndex >= spellQueue.Count)
spellQueueIndex = 0;

if (spellQueue.Count < 1)
return null;

var currentSpell = spellQueue.ElementAt(spellQueueIndex);
var currentId = currentSpell.Id;

while (currentSpell.IsDone && shouldRotate)
{
if (++spellQueueIndex >= spellQueue.Count)
spellQueueIndex = 0;
client.Update(PlayerFieldFlags.Spellbook);

currentSpell = spellQueue.ElementAt(spellQueueIndex);
var skipOnCooldown = UserSettingsManager.Instance.Settings.SkipSpellsOnCooldown;

if (currentSpell.Id == currentId)
{
IsWaitingOnMana = false;
return null;
}
}
var currentSpell = SpellQueueRotation switch
{
SpellRotationMode.None => GetNextSpell_NoRotation(skipOnCooldown),
SpellRotationMode.Singular => GetNextSpell_SingularOrder(skipOnCooldown),
SpellRotationMode.RoundRobin => GetNextSpell_RoundRobin(skipOnCooldown),
_ => null,
};

// Determine if we need to fas spiorad instead
if (currentSpell != null)
{
var currentSpellData = SpellMetadataManager.Instance.GetSpell(currentSpell.Name);
Expand All @@ -863,12 +868,54 @@ private SpellQueueItem GetNextSpell()
return GetFasSpiorad();
}

if (isRoundRobin && shouldRotate)
spellQueueIndex++;
if (currentSpell.IsOnCooldown)
return null;

return !currentSpell.IsDone ? currentSpell : null;
}

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

private SpellQueueItem GetNextSpell_SingularOrder(bool skipOnCooldown = true)
{
if (skipOnCooldown)
return spellQueue.FirstOrDefault(spell => !spell.IsOnCooldown && !spell.IsDone);
else
return spellQueue.FirstOrDefault(spell => !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)
return null;

var currentSpell = spellQueue.ElementAt(spellQueueIndex);

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

// Round robin rotation for next time
AdvanceToNextSpell();
return currentSpell;
}

private SpellQueueItem AdvanceToNextSpell()
{
spellQueueIndex = (spellQueueIndex + 1) % spellQueue.Count;
return spellQueue.Count > 0 ? spellQueue[spellQueueIndex] : null;
}

private FlowerQueueItem GetNextFlowerTarget()
{
var prioritizeAlts = UserSettingsManager.Instance.Settings.FlowerAltsFirst;
Expand Down Expand Up @@ -1053,7 +1100,7 @@ private bool SwitchToBestStaff(SpellQueueItem item, out int? numberOfLines, out
if (item == null)
throw new ArgumentNullException(nameof(item));

client.Update(PlayerFieldFlags.Inventory | PlayerFieldFlags.Equipment | PlayerFieldFlags.Stats);
client.Update( PlayerFieldFlags.Inventory | PlayerFieldFlags.Equipment | PlayerFieldFlags.Stats | PlayerFieldFlags.Spellbook);

var equippedStaff = client.Equipment.GetSlot(EquipmentSlot.Weapon);
var availableList = new List<string>(client.Inventory.ItemNames);
Expand Down
10 changes: 10 additions & 0 deletions SleepHunter/Macro/SavedMacroState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public sealed class SavedMacroState : ObservableObject
private string characterName;
private ModifierKeys hotkeyModifiers;
private Key hotkeyKey;
private SpellRotationMode spellRotation;
private bool useLyliacVineyard;
private bool flowerAlternateCharacters;

Expand Down Expand Up @@ -46,6 +47,14 @@ public Key HotkeyKey
set => SetProperty(ref hotkeyKey, value);
}

[XmlAttribute("SpellRotation")]
[DefaultValue(SpellRotationMode.Default)]
public SpellRotationMode SpellRotation
{
get => spellRotation;
set => SetProperty(ref spellRotation, value);
}

[XmlAttribute("UseLyliacVineyard")]
[DefaultValue(false)]
public bool UseLyliacVineyard
Expand Down Expand Up @@ -101,6 +110,7 @@ public SavedMacroState(PlayerMacroState macroState)
HotkeyKey = macroState.Client.Hotkey.Key;
}

SpellRotation = macroState.SpellQueueRotation;
UseLyliacVineyard = macroState.UseLyliacVineyard;
FlowerAlternateCharacters = macroState.FlowerAlternateCharacters;

Expand Down
1 change: 1 addition & 0 deletions SleepHunter/Macro/SpellRotationMode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace SleepHunter.Macro
{
public enum SpellRotationMode
{
Default,
None,
Singular,
RoundRobin
Expand Down
7 changes: 7 additions & 0 deletions SleepHunter/Models/SpellQueueItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public sealed class SpellQueueItem : ObservableObject, ICopyable<SpellQueueItem>
private int? targetLevel;
private bool isUndefined;
private bool isActive;
private bool isOnCooldown;

public int Id
{
Expand Down Expand Up @@ -111,6 +112,12 @@ public bool IsActive
set => SetProperty(ref isActive, value);
}

public bool IsOnCooldown
{
get => isOnCooldown;
set => SetProperty(ref isOnCooldown, value);
}

public SpellQueueItem() { }

public SpellQueueItem(Spell spellInfo, SavedSpellState spell)
Expand Down
19 changes: 11 additions & 8 deletions SleepHunter/Models/Spellbook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,21 +197,24 @@ public void Update(ProcessMemoryAccessor accessor)
spells[i].ManaCost = metadata.ManaCost;
spells[i].Cooldown = metadata.Cooldown;
spells[i].CanImprove = metadata.CanImprove;

DateTime timestamp = DateTime.MinValue;
if (spells[i].Cooldown > TimeSpan.Zero && spellCooldownTimestamps.TryGetValue(name, out timestamp))
{
var elapsed = DateTime.Now - timestamp;
spells[i].IsOnCooldown = elapsed < (spells[i].Cooldown + TimeSpan.FromMilliseconds(500));
}
}
else
{
spells[i].NumberOfLines = 1;
spells[i].ManaCost = 0;
spells[i].Cooldown = TimeSpan.Zero;
spells[i].CanImprove = true;
spells[i].IsOnCooldown = false;
}

spells[i].IsOnCooldown = false;

// NOTE: Spell cooldowns are not read from the client, instead they are internal timers
// Probably not ideal, but haven't taken the time to see if memory laid out the same as skills
DateTime timestamp = DateTime.MinValue;
if (spells[i].Cooldown > TimeSpan.Zero && spellCooldownTimestamps.TryGetValue(name, out timestamp))
{
var elapsed = DateTime.Now - timestamp;
spells[i].IsOnCooldown = elapsed < (spells[i].Cooldown + TimeSpan.FromMilliseconds(500));
}
}
catch { }
Expand Down
Loading

0 comments on commit 77bacef

Please sign in to comment.