diff --git a/.gitignore b/.gitignore index add57be..6dc0dbc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ bin/ obj/ /packages/ riderModule.iml -/_ReSharper.Caches/ \ No newline at end of file +/_ReSharper.Caches/ +/.vs/ +/resources/ diff --git a/SilkySouls3/MainWindow.xaml.cs b/SilkySouls3/MainWindow.xaml.cs index 4993257..8def314 100644 --- a/SilkySouls3/MainWindow.xaml.cs +++ b/SilkySouls3/MainWindow.xaml.cs @@ -88,7 +88,7 @@ public MainWindow() gameTickService, spEffectService); var enemyViewModel = new EnemyViewModel(enemyService, cinderService, hotkeyManager, _stateService, paramService, debugDrawService, chrInsService, spEffectService, eventService, reminderService, - itemService); + itemService, travelService, playerService, _dlcService); var itemViewModel = new ItemViewModel(itemService, _stateService); var activateOnLaunchManager = new ActivateOnLaunchManager(); var activateOnLaunchViewModel = new ActivateOnLaunchViewModel(playerViewModel, targetViewModel, diff --git a/SilkySouls3/Models/BossFlag.cs b/SilkySouls3/Models/BossFlag.cs new file mode 100644 index 0000000..a674ab9 --- /dev/null +++ b/SilkySouls3/Models/BossFlag.cs @@ -0,0 +1,8 @@ +namespace SilkySouls3.Models +{ + public class BossFlag + { + public int EventId { get; set; } + public bool Value { get; set; } + } +} diff --git a/SilkySouls3/Models/BossRevive.cs b/SilkySouls3/Models/BossRevive.cs new file mode 100644 index 0000000..c124923 --- /dev/null +++ b/SilkySouls3/Models/BossRevive.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Numerics; +using SilkySouls3.Enums; + +namespace SilkySouls3.Models +{ + public class BossRevive + { + public DlcRequirement DlcRequirement { get; set; } + public string Area { get; set; } + public string BossName { get; set; } + public int BlockId { get; set; } + public List FirstEncounterFlags { get; set; } + public List BossFlags { get; set; } + public int BonfireId { get; set; } + public Vector3? Coords { get; set; } + public float Angle { get; set; } + public int? FirstEncounterItemId { get; set; } + } +} diff --git a/SilkySouls3/Properties/Resources.Designer.cs b/SilkySouls3/Properties/Resources.Designer.cs index 9ce2b38..6fb2008 100644 --- a/SilkySouls3/Properties/Resources.Designer.cs +++ b/SilkySouls3/Properties/Resources.Designer.cs @@ -905,6 +905,15 @@ internal static string WarpEntries { return ResourceManager.GetString("WarpEntries", resourceCulture); } } + + /// + /// Looks up a localized string similar to the Boss Revives dataset (Area,BossName,BlockId,FirstEncounterFlags,BossFlags,BonfireId,Coords,Angle per row). + /// + internal static string BossRevives { + get { + return ResourceManager.GetString("BossRevives", resourceCulture); + } + } /// /// Looks up a localized string similar to 0,013599D0,Ancient Dragon Greatshield,1,1,2,0 diff --git a/SilkySouls3/Properties/Resources.resx b/SilkySouls3/Properties/Resources.resx index 715827e..88d3cc6 100644 --- a/SilkySouls3/Properties/Resources.resx +++ b/SilkySouls3/Properties/Resources.resx @@ -192,7 +192,7 @@ c3 ret 0,Road of Sacrifices,Keep Ruins,0x2D03,4,3301954 0,Road of Sacrifices,Farron Keep Perimeter,0x2D03,5,3301958 0,Road of Sacrifices,Old Wolf of Farron,0x2D03,3,3301955 -0,Road of Sacrifices,Abyss Watchers,0x2D02,7,3301954 +0,Road of Sacrifices,Abyss Watchers,0x2D02,7,3301951 0,Cathedral of the Deep,Cathedral of the Deep,0x2D03,6,3501953 0,Cathedral of the Deep,Cleansing Chapel,0x3C03,4,3501950 0,Cathedral of the Deep,Deacons of the Deep,0x3C03,7,3501951 @@ -1763,6 +1763,33 @@ c3 ret 48 8d 8b 28 0b 00 00 lea rcx,[rbx+0xb28] 48 8d 15 00 00 00 00 lea rdx,[rip+0x0] # e <_main+0xe> e9 00 00 00 00 jmp 13 <_main+0x13> + + + 0,Cemetary of Ash,Iudex Gundyr,671088640,14000801|14000802,14000800|9319|6319|64000260|14000002,4001952,123.96|-63.95|554.54,-2.71 +0,Cemetary of Ash,Champion Gundyr,671088640,14000831,14000830|9320|6320|14000004|64000261,4001953,124.44|-63.98|554.00,-2.95 +0,High Wall of Lothric,Vordt of the Boreal Valley,503316480,13000801,13000800|9301|6301|13000002,3001955,27.61|-25.49|51.48,-0.01 +0,High Wall of Lothric,Dancer of the Boreal Valley,503316480,13000896,13000890|9300|6300|13000885|13000004,3001955,27.54|-8.58|126.75,3.14,1073743941 +0,High Wall of Lothric,Oceiros the Consumed King,503316480,13000838,13000830|9302|6302|63000251|13000001,3001954,-73.99|-49.43|360.94,2.89 +0,Undead Settlement,Curse-Rotted Greatwood,520093696,13100801,13100800|9303|6303|13100001,3101953,134.07|-180.74|-874.74,-0.31 +0,Road of Sacrifices,Crystal Sage,553648128,13300852,13300850|9306|6306|13300002,3301957,-156.24|-249.25|-424.70,1.84 +0,Road of Sacrifices,Abyss Watchers,553648128,13300801,13300800|9307|6307|13300421|13300001,3301958,272.16|-248.78|-485.06,-1.50 +0,Cathedral of the Deep,Deacons of the Deep,587202560,13500801,13500800|9311|6311|13500001,3501950,-449.63|-196.57|-617.35,1.09 +0,Catacombs of Carthus,High Lord Wolnir,637534208,13800801,13800800|9315|6315|13801800|63800561|13800000,3801950,, +0,Catacombs of Carthus,Old Demon King,637534208,13800831,13800830|9317|6317|13800004,3801954,291.62|-326.35|-920.68,-0.36 +0,Irithyll of the Boreal Valley,Pontiff Sulyvahn,620756992,13700851,13700850|9313|6313|13700420|13700421|13700422|13700001,3701955,397.70|-224.75|-1116.94,-0.14 +0,Irithyll of the Boreal Valley,Aldrich Devourer of Gods,620756992,13700801,13700800|9314|6314|13700002,3701953,687.84|-121.36|-1079.03,-1.62 +0,Irithyll Dungeon,Yhorm the Giant,654311424,13900801,13900800|9318|6318|13900001,3901952,121.35|-424.86|-615.54,-2.95 +0,Lothric Castle,Dragonslayer Armour,503382016,,13010800|9308|6308|13010001,3011952,-32.29|59.76|222.63,1.64 +0,Lothric Castle,Twin Princes,570490880,13410831|1400|1401|1415|1418,13410830|9309|6309|13410000,3411951,-24.58|141.97|312.01,-1.98 +0,Archdragon Peak,Ancient Wyvern,536870912,13200801,13200800|9305|6305,3201950,-72.19|29.08|180.67,-2.59 +0,Archdragon Peak,Nameless King,536870912,13200440|13200445,13200850|9304|6304|13200855|13200856|13200862|13200001,3201952,80.56|80.95|345.12,0.57 +0,Kiln of the First Flame,Soul of Cinder,687865856,14100801,14100800|9321|6321|14101100|14100002,4101951,-141.83|1590.25|38.51,-2.84 +1,The Painted World of Ariandel,Sister Friede,754974720,14500801,14500800|9322|6322|14500000|14500162|14500620:1|1720|1721|1735|1738,4501950,-186.99|27.75|-230.16,1.57 +1,The Painted World of Ariandel,Champion's Gravetender,754974720,,14500860|14500861|9323|6323|14500006,4501957,-40.38|-97.91|-311.90,1.38 +2,The Dreg Heap,Demon Prince,838860800,,15000800|9324|6324|15000000,5001953,135.26|-89.80|61.34,-1.25 +2,The Ringed City,Darkeater Midir,855638016,15100851,15100850|9326|6326|15100001,5101953,-484.42|-83.08|-398.50,-2.42 +2,The Ringed City,Halflight Spear of the Church,855638016,15100801,15100800|9325|6325,5101953,-380.77|-55.31|-323.59,2.96 +2,The Ringed City,Slave Knight Gael,855703552,15110801,15110800|9327|6327|15110000,5111951,219.36|527.21|192.72,0.93 0,671088640,4001951 diff --git a/SilkySouls3/SilkySouls3.csproj b/SilkySouls3/SilkySouls3.csproj index 9bc97b1..c2012d9 100644 --- a/SilkySouls3/SilkySouls3.csproj +++ b/SilkySouls3/SilkySouls3.csproj @@ -175,6 +175,8 @@ + + diff --git a/SilkySouls3/Utilities/DataLoader.cs b/SilkySouls3/Utilities/DataLoader.cs index 7798933..7ae9b69 100644 --- a/SilkySouls3/Utilities/DataLoader.cs +++ b/SilkySouls3/Utilities/DataLoader.cs @@ -110,6 +110,87 @@ public static Dictionary> GetWarpLocations() return warpDict; } + public static Dictionary> GetBossRevives() + { + Dictionary> bossRevives = new Dictionary>(); + string csvData = Resources.BossRevives; + if (string.IsNullOrWhiteSpace(csvData)) return bossRevives; + + using StringReader reader = new StringReader(csvData); + string line; + while ((line = reader.ReadLine()) != null) + { + if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) continue; + + string[] parts = line.Split(','); + if (parts.Length < 9) continue; + + DlcRequirement dlcRequirement = ParseDlcRequirement(parts[0]); + string area = parts[1]; + + BossRevive boss = new BossRevive + { + DlcRequirement = dlcRequirement, + Area = area, + BossName = parts[2], + BlockId = int.Parse(parts[3], CultureInfo.InvariantCulture), + FirstEncounterFlags = ParseBossFlags(parts[4]), + BossFlags = ParseBossFlags(parts[5]), + BonfireId = int.Parse(parts[6], CultureInfo.InvariantCulture), + Coords = ParseBossCoords(parts[7]), + Angle = string.IsNullOrWhiteSpace(parts[8]) + ? 0f + : float.Parse(parts[8], CultureInfo.InvariantCulture), + FirstEncounterItemId = parts.Length > 9 && int.TryParse(parts[9], NumberStyles.Integer, + CultureInfo.InvariantCulture, out int firstEncounterItemId) + ? firstEncounterItemId + : null + }; + + if (!bossRevives.ContainsKey(area)) + { + bossRevives[area] = new List(); + } + + bossRevives[area].Add(boss); + } + + return bossRevives; + } + + // Each entry is "eventId" (implies clear to OFF) or "eventId:1" (set ON) / "eventId:0" (set OFF, explicit) + private static List ParseBossFlags(string flagData) + { + var flags = new List(); + if (string.IsNullOrWhiteSpace(flagData)) return flags; + + foreach (var part in flagData.Split('|')) + { + if (string.IsNullOrWhiteSpace(part)) continue; + + var pieces = part.Split(':'); + if (int.TryParse(pieces[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out int id)) + { + bool value = pieces.Length > 1 && pieces[1] == "1"; + flags.Add(new BossFlag { EventId = id, Value = value }); + } + } + + return flags; + } + + private static Vector3? ParseBossCoords(string coordData) + { + if (string.IsNullOrWhiteSpace(coordData)) return null; + + string[] parts = coordData.Split('|'); + return new Vector3( + float.Parse(parts[0], CultureInfo.InvariantCulture), + float.Parse(parts[1], CultureInfo.InvariantCulture), + float.Parse(parts[2], CultureInfo.InvariantCulture) + ); + } + public static List GetItemList(string listName) { List items = new List(); diff --git a/SilkySouls3/ViewModels/EnemyViewModel.cs b/SilkySouls3/ViewModels/EnemyViewModel.cs index e599680..90898a6 100644 --- a/SilkySouls3/ViewModels/EnemyViewModel.cs +++ b/SilkySouls3/ViewModels/EnemyViewModel.cs @@ -1,10 +1,17 @@ using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using System.Windows.Input; +using System.Windows.Media; using SilkySouls3.Core; using SilkySouls3.Data; using SilkySouls3.Enums; using SilkySouls3.GameIds; using SilkySouls3.Interfaces; +using SilkySouls3.Models; using SilkySouls3.Utilities; using static SilkySouls3.Memory.Offsets; @@ -22,6 +29,26 @@ public class EnemyViewModel : BaseViewModel private readonly IEventService _eventService; private readonly IReminderService _reminderService; private readonly IItemService _itemService; + private readonly ITravelService _travelService; + private readonly IPlayerService _playerService; + private readonly IDlcService _dlcService; + private readonly IStateService _stateService; + + private const string BossStatusDead = "Dead"; + private const string BossStatusAlive = "Alive"; + private const string BossStatusFirstEncounter = "Alive, First Encounter"; + + private static readonly SolidColorBrush BossStatusDeadBrush = + (SolidColorBrush)new BrushConverter().ConvertFrom("#e74c3c"); + + private static readonly SolidColorBrush BossStatusAliveBrush = + (SolidColorBrush)new BrushConverter().ConvertFrom("#2ecc71"); + + private static readonly SolidColorBrush BossStatusDefaultBrush = Brushes.White; + + private Dictionary> _bossDict; + private List _allBosses; + private string _preSearchArea; private bool _isFadedIn; private bool _isInHalfLightArena; @@ -48,7 +75,8 @@ public class EnemyViewModel : BaseViewModel public EnemyViewModel(IEnemyService enemyService, ICinderService cinderService, HotkeyManager hotkeyManager, IStateService stateService, IParamService paramService, IDebugDrawService debugDrawService, IChrInsService chrInsService, ISpEffectService spEffectService, IEventService eventService, - IReminderService reminderService, IItemService itemService) + IReminderService reminderService, IItemService itemService, ITravelService travelService, + IPlayerService playerService, IDlcService dlcService) { _enemyService = enemyService; _cinderService = cinderService; @@ -58,6 +86,10 @@ public EnemyViewModel(IEnemyService enemyService, ICinderService cinderService, _eventService = eventService; _reminderService = reminderService; _itemService = itemService; + _travelService = travelService; + _playerService = playerService; + _dlcService = dlcService; + _stateService = stateService; SetCinderPhaseCommand = new DelegateCommand(SetCinderPhase); CastSoulmassCommand = new DelegateCommand(CastSoulmass); @@ -68,10 +100,14 @@ public EnemyViewModel(IEnemyService enemyService, ICinderService cinderService, ForceOceirosPhaseTwoCommand = new DelegateCommand(ForceOceirosPhaseTwo); SkipKingOfTheStormCommand = new DelegateCommand(SkipKingOfTheStorm); DropDeaconSkullsCommand = new DelegateCommand(DropDeaconSkulls); + ReviveBossCommand = new DelegateCommand(ReviveBoss); + ReviveBossFirstEncounterCommand = new DelegateCommand(ReviveBossFirstEncounter); _hotkeyManager = hotkeyManager; _paramService = paramService; + LoadBosses(); + stateService.Subscribe(State.Loaded, OnGameLoaded); stateService.Subscribe(State.NotLoaded, OnGameNotLoaded); stateService.Subscribe(State.FadedIn, OnFadedIn); @@ -118,12 +154,14 @@ public EnemyViewModel(IEnemyService enemyService, ICinderService cinderService, public ICommand ForceOceirosPhaseTwoCommand { get; } public ICommand SkipKingOfTheStormCommand { get; } public ICommand DropDeaconSkullsCommand { get; } + public ICommand ReviveBossCommand { get; } + public ICommand ReviveBossFirstEncounterCommand { get; } #endregion #region Properties - private bool _areOptionsEnabled = true; + private bool _areOptionsEnabled; public bool AreOptionsEnabled { @@ -391,6 +429,120 @@ public float ArgoSpeedMultiplier public bool CanDropDeaconSkulls => _isInCathedralOfTheDeep; + private ObservableCollection _areas = new(); + + public ObservableCollection Areas + { + get => _areas; + private set => SetProperty(ref _areas, value); + } + + private ObservableCollection _areaBosses = new(); + + public ObservableCollection AreaBosses + { + get => _areaBosses; + private set => SetProperty(ref _areaBosses, value); + } + + private string _selectedArea; + + public string SelectedArea + { + get => _selectedArea; + set + { + if (!SetProperty(ref _selectedArea, value) || value == null) return; + + if (IsSearchActive) + { + IsSearchActive = false; + _searchText = string.Empty; + OnPropertyChanged(nameof(SearchText)); + _preSearchArea = null; + } + + UpdateAreaBossesList(); + } + } + + private BossRevive _selectedBoss; + + public BossRevive SelectedBoss + { + get => _selectedBoss; + set + { + if (!SetProperty(ref _selectedBoss, value)) return; + RefreshSelectedBossStatus(); + } + } + + private bool _isSearchActive; + + public bool IsSearchActive + { + get => _isSearchActive; + private set => SetProperty(ref _isSearchActive, value); + } + + private string _searchText = string.Empty; + + public string SearchText + { + get => _searchText; + set + { + if (!SetProperty(ref _searchText, value)) return; + + if (string.IsNullOrEmpty(value)) + { + IsSearchActive = false; + if (_preSearchArea != null) + { + _selectedArea = _preSearchArea; + OnPropertyChanged(nameof(SelectedArea)); + UpdateAreaBossesList(); + _preSearchArea = null; + } + } + else + { + if (!IsSearchActive) + { + _preSearchArea = SelectedArea; + IsSearchActive = true; + } + + ApplyFilter(); + } + } + } + + private bool _isRestOnReviveEnabled; + + public bool IsRestOnReviveEnabled + { + get => _isRestOnReviveEnabled; + set => SetProperty(ref _isRestOnReviveEnabled, value); + } + + private string _selectedBossStatus = string.Empty; + + public string SelectedBossStatus + { + get => _selectedBossStatus; + private set => SetProperty(ref _selectedBossStatus, value); + } + + private SolidColorBrush _selectedBossStatusColor = BossStatusDefaultBrush; + + public SolidColorBrush SelectedBossStatusColor + { + get => _selectedBossStatusColor; + private set => SetProperty(ref _selectedBossStatusColor, value); + } + #endregion #region Private Methods @@ -564,6 +716,7 @@ private void OnGameLoaded() if (Math.Abs(_argoSpeedMultiplier - 1.0f) > float.Epsilon) ApplyArgoSpeed(); AreOptionsEnabled = true; + RefreshSelectedBossStatus(); } private void OnFadedIn() @@ -610,6 +763,8 @@ private void OnGameNotLoaded() _hasPlacedPrismStonesThisLoad = false; OnPropertyChanged(nameof(CanPlacePrismStones)); OnPropertyChanged(nameof(CanDropDeaconSkulls)); + SelectedBossStatus = string.Empty; + SelectedBossStatusColor = BossStatusDefaultBrush; } private void ForceOceirosPhaseTwo() @@ -624,6 +779,130 @@ private void SkipKingOfTheStorm() _chrInsService.SetHp(kots, 0); } + private void LoadBosses() + { + _bossDict = DataLoader.GetBossRevives(); + _allBosses = _bossDict.Values.SelectMany(x => x).ToList(); + + Areas = new ObservableCollection(_bossDict.Keys); + SelectedArea = Areas.FirstOrDefault(); + } + + private void UpdateAreaBossesList() + { + if (string.IsNullOrEmpty(SelectedArea) || !_bossDict.ContainsKey(SelectedArea)) + { + AreaBosses = new ObservableCollection(); + return; + } + + AreaBosses = new ObservableCollection(_bossDict[SelectedArea]); + SelectedBoss = AreaBosses.FirstOrDefault(); + } + + private void ApplyFilter() + { + var searchLower = SearchText.ToLower(); + var matches = _allBosses.Where(b => + b.BossName.ToLower().Contains(searchLower) || + b.Area.ToLower().Contains(searchLower)); + + AreaBosses = new ObservableCollection(matches); + SelectedBoss = AreaBosses.FirstOrDefault(); + } + + private void RefreshSelectedBossStatus() + { + if (!AreOptionsEnabled || SelectedBoss == null) + { + SelectedBossStatus = string.Empty; + SelectedBossStatusColor = BossStatusDefaultBrush; + return; + } + + SelectedBossStatus = GetBossStatus(SelectedBoss); + SelectedBossStatusColor = GetBossStatusColor(SelectedBossStatus); + } + + private string GetBossStatus(BossRevive bossRevive) + { + if (bossRevive.BossFlags == null || bossRevive.BossFlags.Count == 0) return string.Empty; + + var isDead = _eventService.GetEvent(bossRevive.BossFlags[0].EventId); + if (isDead) return BossStatusDead; + + if (bossRevive.FirstEncounterFlags is { Count: > 0 }) + { + var alreadyEncountered = bossRevive.FirstEncounterFlags.All(f => _eventService.GetEvent(f.EventId)); + return alreadyEncountered ? BossStatusAlive : BossStatusFirstEncounter; + } + + return BossStatusAlive; + } + + private static SolidColorBrush GetBossStatusColor(string status) => status switch + { + BossStatusDead => BossStatusDeadBrush, + BossStatusAlive => BossStatusAliveBrush, + BossStatusFirstEncounter => BossStatusAliveBrush, + _ => BossStatusDefaultBrush + }; + + private void SetBossFlags(BossRevive bossRevive, bool isFirstEncounter) + { + if (isFirstEncounter && bossRevive.FirstEncounterFlags != null) + { + foreach (var flag in bossRevive.FirstEncounterFlags) + _eventService.SetEvent(flag.EventId, flag.Value); + } + + foreach (var flag in bossRevive.BossFlags) + _eventService.SetEvent(flag.EventId, flag.Value); + } + + private void ReviveBoss() => Revive(isFirstEncounter: false); + + private void ReviveBossFirstEncounter() => Revive(isFirstEncounter: true); + + private void Revive(bool isFirstEncounter) + { + var bossRevive = SelectedBoss; + if (bossRevive == null || !_dlcService.MeetsRequirement(bossRevive.DlcRequirement)) return; + + SetBossFlags(bossRevive, isFirstEncounter); + RefreshSelectedBossStatus(); + + if (isFirstEncounter && bossRevive.FirstEncounterItemId.HasValue) + _itemService.SpawnItem(bossRevive.FirstEncounterItemId.Value, 1, true, 1); + + bool isInBossArea = _playerService.GetCurrentBlockId() == bossRevive.BlockId; + if (!isInBossArea && !IsRestOnReviveEnabled) return; + + _ = Task.Run(() => + { + if (isInBossArea) + { + if (bossRevive.Coords.HasValue) + _travelService.WarpWithCoords(bossRevive.Coords.Value, bossRevive.Angle, + bossRevive.BonfireId); + else + _travelService.Warp(bossRevive.BonfireId); + } + + if (IsRestOnReviveEnabled) + { + if (isInBossArea) + { + int start = Environment.TickCount; + while (!_stateService.IsFadedIn() && Environment.TickCount - start < 10000) + Thread.Sleep(50); + } + + _playerService.Rest(); + } + }); + } + #endregion } } \ No newline at end of file diff --git a/SilkySouls3/Views/EnemyTab.xaml b/SilkySouls3/Views/EnemyTab.xaml index 114a9f2..e7de88d 100644 --- a/SilkySouls3/Views/EnemyTab.xaml +++ b/SilkySouls3/Views/EnemyTab.xaml @@ -5,10 +5,14 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:enums="clr-namespace:SilkySouls3.Enums" xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit" + xmlns:converters="clr-namespace:SilkySouls3.Converters" mc:Ignorable="d"> + + + @@ -212,4 +216,237 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +