work on memory
This commit is contained in:
parent
2f652aa1b7
commit
6e9d89b045
28 changed files with 3590 additions and 111 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -31,3 +31,6 @@ tools/python-detect/models/
|
|||
# IDE / tools
|
||||
.claude/
|
||||
nul
|
||||
|
||||
# Extras
|
||||
lib/extras
|
||||
58
Automata.sln
58
Automata.sln
|
|
@ -27,6 +27,24 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Navigation", "src\
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Memory", "src\Automata.Memory\Automata.Memory.csproj", "{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "lib", "lib", "{652F700E-4F84-4E66-BD62-717D3A8D6FBC}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sidekick.Common", "lib\Sidekick\src\Sidekick.Common\Sidekick.Common.csproj", "{B858F6F2-389F-475A-87FE-E4E01DA3E948}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sidekick.Common.Database", "lib\Sidekick\src\Sidekick.Common.Database\Sidekick.Common.Database.csproj", "{6FEA655D-18E4-4DA1-839F-A41433B03FBB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sidekick.Common.Browser", "lib\Sidekick\src\Sidekick.Common.Browser\Sidekick.Common.Browser.csproj", "{74FD0F88-86BC-49AE-9A16-136D92A10090}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sidekick.Apis.Common", "lib\Sidekick\src\Sidekick.Apis.Common\Sidekick.Apis.Common.csproj", "{3A65A8CD-0CE2-4191-8D04-E3968CA2CCD5}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sidekick.Apis.Poe", "lib\Sidekick\src\Sidekick.Apis.Poe\Sidekick.Apis.Poe.csproj", "{2A5D289F-7916-484C-B2D8-99A128F5F936}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sidekick.Apis.Poe.Trade", "lib\Sidekick\src\Sidekick.Apis.Poe.Trade\Sidekick.Apis.Poe.Trade.csproj", "{8CEE036C-A229-4F22-BD0E-D7CDAE13E54F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sidekick.Data", "lib\Sidekick\src\Sidekick.Data\Sidekick.Data.csproj", "{9428D5D4-4061-467A-BD26-C1FEED95E8E6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sidekick.Data.Builder", "lib\Sidekick\src\Sidekick.Data.Builder\Sidekick.Data.Builder.csproj", "{E5C26A34-5EDF-488B-93C7-F8738F2CEB97}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -80,6 +98,38 @@ Global
|
|||
{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B858F6F2-389F-475A-87FE-E4E01DA3E948}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B858F6F2-389F-475A-87FE-E4E01DA3E948}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B858F6F2-389F-475A-87FE-E4E01DA3E948}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B858F6F2-389F-475A-87FE-E4E01DA3E948}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6FEA655D-18E4-4DA1-839F-A41433B03FBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6FEA655D-18E4-4DA1-839F-A41433B03FBB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6FEA655D-18E4-4DA1-839F-A41433B03FBB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6FEA655D-18E4-4DA1-839F-A41433B03FBB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{74FD0F88-86BC-49AE-9A16-136D92A10090}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{74FD0F88-86BC-49AE-9A16-136D92A10090}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{74FD0F88-86BC-49AE-9A16-136D92A10090}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{74FD0F88-86BC-49AE-9A16-136D92A10090}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3A65A8CD-0CE2-4191-8D04-E3968CA2CCD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3A65A8CD-0CE2-4191-8D04-E3968CA2CCD5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3A65A8CD-0CE2-4191-8D04-E3968CA2CCD5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3A65A8CD-0CE2-4191-8D04-E3968CA2CCD5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2A5D289F-7916-484C-B2D8-99A128F5F936}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2A5D289F-7916-484C-B2D8-99A128F5F936}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2A5D289F-7916-484C-B2D8-99A128F5F936}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2A5D289F-7916-484C-B2D8-99A128F5F936}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8CEE036C-A229-4F22-BD0E-D7CDAE13E54F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8CEE036C-A229-4F22-BD0E-D7CDAE13E54F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8CEE036C-A229-4F22-BD0E-D7CDAE13E54F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8CEE036C-A229-4F22-BD0E-D7CDAE13E54F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9428D5D4-4061-467A-BD26-C1FEED95E8E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9428D5D4-4061-467A-BD26-C1FEED95E8E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9428D5D4-4061-467A-BD26-C1FEED95E8E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9428D5D4-4061-467A-BD26-C1FEED95E8E6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E5C26A34-5EDF-488B-93C7-F8738F2CEB97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E5C26A34-5EDF-488B-93C7-F8738F2CEB97}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E5C26A34-5EDF-488B-93C7-F8738F2CEB97}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E5C26A34-5EDF-488B-93C7-F8738F2CEB97}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{6432F6A5-11A0-4960-AFFC-E810D4325C35} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
|
||||
|
|
@ -93,5 +143,13 @@ Global
|
|||
{859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
|
||||
{D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
|
||||
{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
|
||||
{B858F6F2-389F-475A-87FE-E4E01DA3E948} = {652F700E-4F84-4E66-BD62-717D3A8D6FBC}
|
||||
{6FEA655D-18E4-4DA1-839F-A41433B03FBB} = {652F700E-4F84-4E66-BD62-717D3A8D6FBC}
|
||||
{74FD0F88-86BC-49AE-9A16-136D92A10090} = {652F700E-4F84-4E66-BD62-717D3A8D6FBC}
|
||||
{3A65A8CD-0CE2-4191-8D04-E3968CA2CCD5} = {652F700E-4F84-4E66-BD62-717D3A8D6FBC}
|
||||
{2A5D289F-7916-484C-B2D8-99A128F5F936} = {652F700E-4F84-4E66-BD62-717D3A8D6FBC}
|
||||
{8CEE036C-A229-4F22-BD0E-D7CDAE13E54F} = {652F700E-4F84-4E66-BD62-717D3A8D6FBC}
|
||||
{9428D5D4-4061-467A-BD26-C1FEED95E8E6} = {652F700E-4F84-4E66-BD62-717D3A8D6FBC}
|
||||
{E5C26A34-5EDF-488B-93C7-F8738F2CEB97} = {652F700E-4F84-4E66-BD62-717D3A8D6FBC}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
|
|||
1
data/poe2/base_items.min.json
Normal file
1
data/poe2/base_items.min.json
Normal file
File diff suppressed because one or more lines are too long
1
data/poe2/item_classes.min.json
Normal file
1
data/poe2/item_classes.min.json
Normal file
File diff suppressed because one or more lines are too long
1
data/poe2/mods.min.json
Normal file
1
data/poe2/mods.min.json
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"ProcessName": "PathOfExileSteam",
|
||||
"GameStatePattern": "",
|
||||
"InGameStateOffset": 0,
|
||||
"IngameDataOffset": 0,
|
||||
"TerrainDataOffset": 0,
|
||||
"NumColsOffset": 0,
|
||||
"NumRowsOffset": 0,
|
||||
"LayerMeleeOffset": 0,
|
||||
"BytesPerRowOffset": 0,
|
||||
"SubTilesPerCell": 23
|
||||
}
|
||||
50
offsets.json
Normal file
50
offsets.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"ProcessName": "PathOfExileSteam",
|
||||
"GameStatePattern": "48 83 EC ?? 48 8B F1 33 ED 48 39 2D ^",
|
||||
"GameStateGlobalOffset": 0,
|
||||
"PatternResultAdjust": 24,
|
||||
"StatesBeginOffset": 72,
|
||||
"StateStride": 16,
|
||||
"StatePointerOffset": 0,
|
||||
"InGameStateIndex": 4,
|
||||
"StatesInline": true,
|
||||
"InGameStateDirectOffset": 528,
|
||||
"IngameDataFromStateOffset": 656,
|
||||
"WorldDataFromStateOffset": 760,
|
||||
"AreaLevelOffset": 196,
|
||||
"AreaLevelIsByte": true,
|
||||
"AreaLevelStaticOffset": 0,
|
||||
"AreaHashOffset": 236,
|
||||
"ServerDataOffset": 2544,
|
||||
"LocalPlayerDirectOffset": 2576,
|
||||
"EntityListOffset": 2808,
|
||||
"EntityCountInternalOffset": 8,
|
||||
"LocalPlayerOffset": 32,
|
||||
"ComponentListOffset": 16,
|
||||
"LifeComponentIndex": -1,
|
||||
"RenderComponentIndex": -1,
|
||||
"LifeComponentOffset1": 1056,
|
||||
"LifeComponentOffset2": 152,
|
||||
"LifeHealthOffset": 424,
|
||||
"LifeManaOffset": 504,
|
||||
"LifeEsOffset": 560,
|
||||
"VitalCurrentOffset": 48,
|
||||
"VitalTotalOffset": 44,
|
||||
"PositionXOffset": 312,
|
||||
"PositionYOffset": 316,
|
||||
"PositionZOffset": 320,
|
||||
"TerrainListOffset": 3264,
|
||||
"TerrainInline": true,
|
||||
"TerrainDimensionsOffset": 24,
|
||||
"TerrainWalkableGridOffset": 208,
|
||||
"TerrainBytesPerRowOffset": 256,
|
||||
"TerrainGridPtrOffset": 8,
|
||||
"SubTilesPerCell": 23,
|
||||
"InGameStateOffset": 0,
|
||||
"IngameDataOffset": 0,
|
||||
"TerrainDataOffset": 0,
|
||||
"NumColsOffset": 0,
|
||||
"NumRowsOffset": 0,
|
||||
"LayerMeleeOffset": 0,
|
||||
"BytesPerRowOffset": 0
|
||||
}
|
||||
|
|
@ -12,5 +12,6 @@
|
|||
<ProjectReference Include="..\Automata.Log\Automata.Log.csproj" />
|
||||
<ProjectReference Include="..\Automata.Inventory\Automata.Inventory.csproj" />
|
||||
<ProjectReference Include="..\Automata.Navigation\Automata.Navigation.csproj" />
|
||||
<ProjectReference Include="..\Automata.Items\Automata.Items.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using Automata.Core;
|
||||
using Automata.Game;
|
||||
using Automata.Items;
|
||||
using Serilog;
|
||||
|
||||
namespace Automata.Bot;
|
||||
|
|
@ -193,6 +194,31 @@ public class CraftingExecutor
|
|||
{
|
||||
if (string.IsNullOrEmpty(itemText)) return false;
|
||||
|
||||
// Try Sidekick parsed matching first
|
||||
try
|
||||
{
|
||||
var item = ItemReader.ParseItemText(itemText);
|
||||
if (item != null)
|
||||
{
|
||||
var statTexts = item.Stats.Select(s => s.Text).ToList();
|
||||
if (matchAll)
|
||||
{
|
||||
return requiredMods.All(mod =>
|
||||
statTexts.Any(st => st.Contains(mod, StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
else
|
||||
{
|
||||
return requiredMods.Any(mod =>
|
||||
statTexts.Any(st => st.Contains(mod, StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fall through to raw matching
|
||||
}
|
||||
|
||||
// Fallback: raw substring matching
|
||||
if (matchAll)
|
||||
{
|
||||
return requiredMods.All(mod =>
|
||||
|
|
|
|||
202
src/Automata.Core/ModPoolService.cs
Normal file
202
src/Automata.Core/ModPoolService.cs
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using Serilog;
|
||||
|
||||
namespace Automata.Core;
|
||||
|
||||
public class ModPoolService
|
||||
{
|
||||
private const string BaseUrl = "https://repoe-fork.github.io/poe2/";
|
||||
private static readonly string DataDir = Path.GetFullPath("data/poe2");
|
||||
|
||||
private Dictionary<string, RePoEMod> _mods = new();
|
||||
private Dictionary<string, RePoEBaseItem> _baseItems = new();
|
||||
private Dictionary<string, RePoEItemClass> _itemClasses = new();
|
||||
private bool _loaded;
|
||||
|
||||
public bool IsLoaded => _loaded;
|
||||
|
||||
/// <summary>Craftable item classes (weapons, armour, accessories, etc.)</summary>
|
||||
private static readonly HashSet<string> CraftableCategories = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"Weapon", "Armour", "Jewelry"
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> CraftableClassIds = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
// Weapons
|
||||
"Bow", "Claw", "Crossbow", "Dagger", "FishingRod",
|
||||
"OneHandAxe", "OneHandMace", "OneHandSword",
|
||||
"Sceptre", "Staff", "TwoHandAxe", "TwoHandMace", "TwoHandSword",
|
||||
"Wand", "Flail", "Quarterstaff", "Spear", "Trap",
|
||||
// Armour
|
||||
"BodyArmour", "Boots", "Gloves", "Helmet", "Shield",
|
||||
"Focus", "Buckler",
|
||||
// Accessories
|
||||
"Amulet", "Belt", "Ring",
|
||||
// Quiver
|
||||
"Quiver",
|
||||
};
|
||||
|
||||
public async Task LoadAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(DataDir);
|
||||
using var http = new HttpClient();
|
||||
http.Timeout = TimeSpan.FromSeconds(30);
|
||||
|
||||
await Task.WhenAll(
|
||||
DownloadIfMissing(http, "mods.min.json"),
|
||||
DownloadIfMissing(http, "base_items.min.json"),
|
||||
DownloadIfMissing(http, "item_classes.min.json")
|
||||
);
|
||||
|
||||
var modsJson = await File.ReadAllTextAsync(Path.Combine(DataDir, "mods.min.json"));
|
||||
_mods = JsonSerializer.Deserialize<Dictionary<string, RePoEMod>>(modsJson) ?? new();
|
||||
|
||||
var basesJson = await File.ReadAllTextAsync(Path.Combine(DataDir, "base_items.min.json"));
|
||||
_baseItems = JsonSerializer.Deserialize<Dictionary<string, RePoEBaseItem>>(basesJson) ?? new();
|
||||
|
||||
var classesJson = await File.ReadAllTextAsync(Path.Combine(DataDir, "item_classes.min.json"));
|
||||
_itemClasses = JsonSerializer.Deserialize<Dictionary<string, RePoEItemClass>>(classesJson) ?? new();
|
||||
|
||||
_loaded = true;
|
||||
Log.Information("RePoE data loaded: {Mods} mods, {Bases} base items, {Classes} item classes",
|
||||
_mods.Count, _baseItems.Count, _itemClasses.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to load RePoE data");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task DownloadIfMissing(HttpClient http, string filename)
|
||||
{
|
||||
var path = Path.Combine(DataDir, filename);
|
||||
if (File.Exists(path)) return;
|
||||
|
||||
Log.Information("Downloading RePoE {File}...", filename);
|
||||
var data = await http.GetStringAsync(BaseUrl + filename);
|
||||
await File.WriteAllTextAsync(path, data);
|
||||
}
|
||||
|
||||
/// <summary>Returns craftable base items grouped by item class name.</summary>
|
||||
public List<BaseItemOption> GetCraftableBases()
|
||||
{
|
||||
if (!_loaded) return [];
|
||||
|
||||
return _baseItems
|
||||
.Where(kv =>
|
||||
{
|
||||
var b = kv.Value;
|
||||
if (b.ReleaseState != "released") return false;
|
||||
if (string.IsNullOrEmpty(b.ItemClass)) return false;
|
||||
return CraftableClassIds.Contains(b.ItemClass);
|
||||
})
|
||||
.Select(kv => new BaseItemOption
|
||||
{
|
||||
Key = kv.Key,
|
||||
Name = kv.Value.Name,
|
||||
ItemClass = GetItemClassName(kv.Value.ItemClass),
|
||||
})
|
||||
.OrderBy(b => b.ItemClass)
|
||||
.ThenBy(b => b.Name)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private string GetItemClassName(string classId)
|
||||
{
|
||||
if (_itemClasses.TryGetValue(classId, out var cls) && !string.IsNullOrEmpty(cls.Name))
|
||||
return cls.Name;
|
||||
return classId;
|
||||
}
|
||||
|
||||
/// <summary>Returns mods that can roll on the given base item at the given ilvl.</summary>
|
||||
public List<AvailableMod> GetAvailableMods(string baseItemKey, int ilvl)
|
||||
{
|
||||
if (!_loaded) return [];
|
||||
if (!_baseItems.TryGetValue(baseItemKey, out var baseItem)) return [];
|
||||
|
||||
var baseTags = new HashSet<string>(baseItem.Tags, StringComparer.OrdinalIgnoreCase);
|
||||
var result = new List<AvailableMod>();
|
||||
|
||||
foreach (var (modId, mod) in _mods)
|
||||
{
|
||||
if (mod.Domain != "item") continue;
|
||||
if (mod.RequiredLevel > ilvl) continue;
|
||||
if (mod.IsEssenceOnly) continue;
|
||||
if (string.IsNullOrEmpty(mod.Text)) continue;
|
||||
|
||||
// Check spawn_weights: find first tag matching base's tags
|
||||
var canSpawn = false;
|
||||
foreach (var sw in mod.SpawnWeights)
|
||||
{
|
||||
if (baseTags.Contains(sw.Tag))
|
||||
{
|
||||
canSpawn = sw.Weight > 0;
|
||||
break; // first match determines outcome
|
||||
}
|
||||
}
|
||||
|
||||
if (!canSpawn) continue;
|
||||
|
||||
var statRange = mod.Stats.Count > 0
|
||||
? string.Join(", ", mod.Stats.Select(s => s.Min == s.Max ? $"{s.Min}" : $"{s.Min}-{s.Max}"))
|
||||
: "";
|
||||
|
||||
// Clean display text: remove wiki markup like [Strength|Strength]
|
||||
var displayText = System.Text.RegularExpressions.Regex.Replace(
|
||||
mod.Text, @"\[([^|]*)\|([^\]]*)\]", "$2");
|
||||
|
||||
result.Add(new AvailableMod
|
||||
{
|
||||
ModId = modId,
|
||||
Name = mod.Name,
|
||||
Text = displayText,
|
||||
GenerationType = mod.GenerationType,
|
||||
RequiredLevel = mod.RequiredLevel,
|
||||
StatRange = statRange,
|
||||
Groups = mod.Groups,
|
||||
});
|
||||
}
|
||||
|
||||
return result
|
||||
.OrderBy(m => m.GenerationType)
|
||||
.ThenBy(m => m.Name)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>Force re-download of cached data.</summary>
|
||||
public async Task RefreshAsync()
|
||||
{
|
||||
var files = new[] { "mods.min.json", "base_items.min.json", "item_classes.min.json" };
|
||||
foreach (var f in files)
|
||||
{
|
||||
var path = Path.Combine(DataDir, f);
|
||||
if (File.Exists(path)) File.Delete(path);
|
||||
}
|
||||
_loaded = false;
|
||||
await LoadAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public class BaseItemOption
|
||||
{
|
||||
public string Key { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public string ItemClass { get; set; } = "";
|
||||
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
|
||||
public class AvailableMod
|
||||
{
|
||||
public string ModId { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public string Text { get; set; } = "";
|
||||
public string GenerationType { get; set; } = "";
|
||||
public int RequiredLevel { get; set; }
|
||||
public string StatRange { get; set; } = "";
|
||||
public List<string> Groups { get; set; } = [];
|
||||
}
|
||||
50
src/Automata.Core/RePoETypes.cs
Normal file
50
src/Automata.Core/RePoETypes.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Automata.Core;
|
||||
|
||||
public class RePoEMod
|
||||
{
|
||||
[JsonPropertyName("name")] public string Name { get; set; } = "";
|
||||
[JsonPropertyName("domain")] public string Domain { get; set; } = "";
|
||||
[JsonPropertyName("generation_type")] public string GenerationType { get; set; } = "";
|
||||
[JsonPropertyName("required_level")] public int RequiredLevel { get; set; }
|
||||
[JsonPropertyName("groups")] public List<string> Groups { get; set; } = [];
|
||||
[JsonPropertyName("spawn_weights")] public List<RePoESpawnWeight> SpawnWeights { get; set; } = [];
|
||||
[JsonPropertyName("stats")] public List<RePoEStat> Stats { get; set; } = [];
|
||||
[JsonPropertyName("text")] public string Text { get; set; } = "";
|
||||
[JsonPropertyName("type")] public string Type { get; set; } = "";
|
||||
[JsonPropertyName("is_essence_only")] public bool IsEssenceOnly { get; set; }
|
||||
[JsonPropertyName("adds_tags")] public List<string> AddsTags { get; set; } = [];
|
||||
[JsonPropertyName("implicit_tags")] public List<string> ImplicitTags { get; set; } = [];
|
||||
}
|
||||
|
||||
public class RePoESpawnWeight
|
||||
{
|
||||
[JsonPropertyName("tag")] public string Tag { get; set; } = "";
|
||||
[JsonPropertyName("weight")] public int Weight { get; set; }
|
||||
}
|
||||
|
||||
public class RePoEStat
|
||||
{
|
||||
[JsonPropertyName("id")] public string Id { get; set; } = "";
|
||||
[JsonPropertyName("min")] public int Min { get; set; }
|
||||
[JsonPropertyName("max")] public int Max { get; set; }
|
||||
}
|
||||
|
||||
public class RePoEBaseItem
|
||||
{
|
||||
[JsonPropertyName("name")] public string Name { get; set; } = "";
|
||||
[JsonPropertyName("item_class")] public string ItemClass { get; set; } = "";
|
||||
[JsonPropertyName("tags")] public List<string> Tags { get; set; } = [];
|
||||
[JsonPropertyName("drop_level")] public int DropLevel { get; set; }
|
||||
[JsonPropertyName("release_state")] public string ReleaseState { get; set; } = "";
|
||||
[JsonPropertyName("domain")] public string Domain { get; set; } = "";
|
||||
[JsonPropertyName("implicits")] public List<string> Implicits { get; set; } = [];
|
||||
}
|
||||
|
||||
public class RePoEItemClass
|
||||
{
|
||||
[JsonPropertyName("name")] public string Name { get; set; } = "";
|
||||
[JsonPropertyName("category")] public string? Category { get; set; }
|
||||
[JsonPropertyName("category_id")] public string? CategoryId { get; set; }
|
||||
}
|
||||
|
|
@ -164,6 +164,9 @@ public class CraftRecipe
|
|||
public int ItemX { get; set; }
|
||||
public int ItemY { get; set; }
|
||||
public List<CraftStep> Steps { get; set; } = [];
|
||||
public string? BaseItemKey { get; set; }
|
||||
public string? BaseItemName { get; set; }
|
||||
public int ItemLevel { get; set; } = 83;
|
||||
}
|
||||
|
||||
public class CurrencyPosition
|
||||
|
|
|
|||
|
|
@ -7,5 +7,7 @@
|
|||
<ItemGroup>
|
||||
<ProjectReference Include="..\Automata.Core\Automata.Core.csproj" />
|
||||
<ProjectReference Include="..\Automata.Game\Automata.Game.csproj" />
|
||||
<ProjectReference Include="..\..\lib\Sidekick\src\Sidekick.Apis.Poe.Trade\Sidekick.Apis.Poe.Trade.csproj" />
|
||||
<ProjectReference Include="..\..\lib\Sidekick\src\Sidekick.Apis.Poe\Sidekick.Apis.Poe.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
using Automata.Core;
|
||||
using Automata.Game;
|
||||
using Sidekick.Apis.Poe.Items;
|
||||
using Sidekick.Apis.Poe.Trade.Parser;
|
||||
using Serilog;
|
||||
|
||||
namespace Automata.Items;
|
||||
|
||||
/// <summary>
|
||||
/// Reads item data by hovering and pressing Ctrl+C to copy item text to clipboard.
|
||||
/// Will be wired up to Sidekick's ItemParser once the submodule is added.
|
||||
/// </summary>
|
||||
public class ItemReader
|
||||
{
|
||||
|
|
@ -49,11 +50,21 @@ public class ItemReader
|
|||
return text;
|
||||
}
|
||||
|
||||
// TODO: Wire up Sidekick's ItemParser
|
||||
// public async Task<ParsedItem?> ParseItem(int x, int y)
|
||||
// {
|
||||
// var text = await ReadItemText(x, y);
|
||||
// if (text == null) return null;
|
||||
// return SidekickItemParser.Parse(text);
|
||||
// }
|
||||
/// <summary>
|
||||
/// Parse raw item text into a structured Sidekick Item.
|
||||
/// Returns null if parsing fails.
|
||||
/// </summary>
|
||||
public static Item? ParseItemText(string text)
|
||||
{
|
||||
try
|
||||
{
|
||||
var parser = SidekickBootstrapper.GetParser();
|
||||
return parser.ParseItem(text);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Failed to parse item text ({Length} chars)", text.Length);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
71
src/Automata.Items/SidekickBootstrapper.cs
Normal file
71
src/Automata.Items/SidekickBootstrapper.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Sidekick.Apis.Common;
|
||||
using Sidekick.Apis.Poe.Trade;
|
||||
using Sidekick.Apis.Poe.Trade.Parser;
|
||||
using Sidekick.Common;
|
||||
using Sidekick.Common.Initialization;
|
||||
using Sidekick.Common.Settings;
|
||||
using Sidekick.Data;
|
||||
using Serilog;
|
||||
|
||||
namespace Automata.Items;
|
||||
|
||||
/// <summary>
|
||||
/// Bootstraps a minimal Sidekick DI container for item parsing.
|
||||
/// </summary>
|
||||
public static class SidekickBootstrapper
|
||||
{
|
||||
private static ServiceProvider? _provider;
|
||||
|
||||
public static async Task InitializeAsync()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// Logging
|
||||
services.AddLogging();
|
||||
|
||||
// Sidekick core
|
||||
services.AddSidekickCommon(SidekickApplicationType.Unknown);
|
||||
services.AddSidekickCommonApi();
|
||||
services.AddSidekickData();
|
||||
services.AddSidekickPoeTradeApi();
|
||||
|
||||
// Replace the real SettingsService (needs SQLite) with our stub
|
||||
var existing = services.FirstOrDefault(d => d.ServiceType == typeof(ISettingsService));
|
||||
if (existing != null) services.Remove(existing);
|
||||
services.AddSingleton<ISettingsService, SidekickSettingsStub>();
|
||||
|
||||
_provider = services.BuildServiceProvider();
|
||||
|
||||
// Initialize all registered IInitializableService in priority order
|
||||
var config = _provider.GetRequiredService<IOptions<SidekickConfiguration>>().Value;
|
||||
var initServices = config.InitializableServices
|
||||
.Select(type => (IInitializableService)_provider.GetRequiredService(type))
|
||||
.OrderBy(s => s.Priority)
|
||||
.ToList();
|
||||
|
||||
foreach (var svc in initServices)
|
||||
{
|
||||
try
|
||||
{
|
||||
await svc.Initialize();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Sidekick service {Type} failed to initialize", svc.GetType().Name);
|
||||
}
|
||||
}
|
||||
|
||||
Log.Information("Sidekick bootstrapped: {Count} services initialized", initServices.Count);
|
||||
}
|
||||
|
||||
public static T GetService<T>() where T : notnull
|
||||
{
|
||||
if (_provider == null)
|
||||
throw new InvalidOperationException("SidekickBootstrapper.InitializeAsync() must be called first");
|
||||
return _provider.GetRequiredService<T>();
|
||||
}
|
||||
|
||||
public static IItemParser GetParser() => GetService<IItemParser>();
|
||||
}
|
||||
56
src/Automata.Items/SidekickSettingsStub.cs
Normal file
56
src/Automata.Items/SidekickSettingsStub.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
using Sidekick.Common.Settings;
|
||||
|
||||
namespace Automata.Items;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal ISettingsService returning defaults for Sidekick initialization.
|
||||
/// Avoids SQLite/EF Core dependency.
|
||||
/// </summary>
|
||||
public class SidekickSettingsStub : ISettingsService
|
||||
{
|
||||
private readonly Dictionary<string, object?> _settings = new()
|
||||
{
|
||||
[SettingKeys.LeagueId] = "poe2.Standard",
|
||||
[SettingKeys.LanguageParser] = "en",
|
||||
[SettingKeys.LanguageUi] = "en",
|
||||
};
|
||||
|
||||
public event Action<string[]>? OnSettingsChanged;
|
||||
|
||||
public Task<bool> GetBool(string key)
|
||||
=> Task.FromResult(_settings.TryGetValue(key, out var v) && v is true);
|
||||
|
||||
public Task<string?> GetString(string key)
|
||||
=> Task.FromResult(_settings.TryGetValue(key, out var v) ? v?.ToString() : null);
|
||||
|
||||
public Task<int> GetInt(string key)
|
||||
=> Task.FromResult(_settings.TryGetValue(key, out var v) && v is int i ? i : 0);
|
||||
|
||||
public Task<double> GetDouble(string key)
|
||||
=> Task.FromResult(_settings.TryGetValue(key, out var v) && v is double d ? d : 0.0);
|
||||
|
||||
public Task<DateTimeOffset?> GetDateTime(string key)
|
||||
=> Task.FromResult<DateTimeOffset?>(null);
|
||||
|
||||
public Task<TEnum?> GetEnum<TEnum>(string key) where TEnum : struct, Enum
|
||||
=> Task.FromResult<TEnum?>(default);
|
||||
|
||||
public Task<TValue?> GetObject<TValue>(string key, Func<TValue?> defaultFunc) where TValue : class
|
||||
=> Task.FromResult(defaultFunc());
|
||||
|
||||
public Task Set(string key, object? value)
|
||||
{
|
||||
_settings[key] = value;
|
||||
OnSettingsChanged?.Invoke([key]);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<bool> IsSettingModified(params string[] keys)
|
||||
=> Task.FromResult(false);
|
||||
|
||||
public Task DeleteSetting(params string[] keys)
|
||||
{
|
||||
foreach (var key in keys) _settings.Remove(key);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
1932
src/Automata.Memory/GameMemoryReader.cs
Normal file
1932
src/Automata.Memory/GameMemoryReader.cs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -14,23 +14,137 @@ public sealed class TerrainOffsets
|
|||
|
||||
public string ProcessName { get; set; } = "PathOfExileSteam";
|
||||
|
||||
/// <summary>Pattern to find GameState base pointer. Empty = unknown for POE2.</summary>
|
||||
public string GameStatePattern { get; set; } = "";
|
||||
/// <summary>Pattern to find GameState global. Use ^ to mark the RIP displacement position.</summary>
|
||||
public string GameStatePattern { get; set; } = "48 83 EC ?? 48 8B F1 33 ED 48 39 2D ^";
|
||||
|
||||
// Pointer chain: GameState → InGameState → IngameData → TerrainData
|
||||
/// <summary>Fallback: manual offset from module base to GameState global (hex). Used when pattern is empty or fails.</summary>
|
||||
public int GameStateGlobalOffset { get; set; }
|
||||
|
||||
/// <summary>Bytes to add to pattern scan result to reach the actual global.</summary>
|
||||
public int PatternResultAdjust { get; set; } = 0x18;
|
||||
|
||||
// ── GameState → States ──
|
||||
// Dump: GameStateOffset { [0x08] StdVector CurrentStatePtr, [0x48] GameStateBuffer States }
|
||||
// GameStateBuffer = StdTuple2D<IntPtr> (begin/end pair). Each entry is an IntPtr (8 bytes).
|
||||
/// <summary>Offset to States begin/end pair in GameState (dump: 0x48).</summary>
|
||||
public int StatesBeginOffset { get; set; } = 0x48;
|
||||
/// <summary>Bytes per state entry (16 for inline slots, 8 for vector of pointers).</summary>
|
||||
public int StateStride { get; set; } = 0x10;
|
||||
/// <summary>Offset within each state entry to the actual state pointer.</summary>
|
||||
public int StatePointerOffset { get; set; } = 0;
|
||||
/// <summary>Which state index is InGameState (typically 4).</summary>
|
||||
public int InGameStateIndex { get; set; } = 4;
|
||||
/// <summary>If true, states are inline in the controller struct. If false, StatesBeginOffset points to a begin/end vector pair.</summary>
|
||||
public bool StatesInline { get; set; } = true;
|
||||
/// <summary>Direct offset from controller to InGameState pointer (bypasses state array). 0 = use state array instead. CE confirmed: 0x210.</summary>
|
||||
public int InGameStateDirectOffset { get; set; } = 0x210;
|
||||
|
||||
// ── InGameState → sub-structures ──
|
||||
// Dump: InGameStateOffset { [0x298] AreaInstanceData, [0x2F8] WorldData, [0x648] UiRootPtr, [0xC40] IngameUi }
|
||||
/// <summary>InGameState → AreaInstance (IngameData) pointer (dump: 0x298, CE confirmed: 0x290).</summary>
|
||||
public int IngameDataFromStateOffset { get; set; } = 0x290;
|
||||
/// <summary>InGameState → WorldData pointer (dump: 0x2F8).</summary>
|
||||
public int WorldDataFromStateOffset { get; set; } = 0x2F8;
|
||||
|
||||
// ── AreaInstance (IngameData) → sub-structures ──
|
||||
// Dump: AreaInstanceOffsets {
|
||||
// [0xAC] byte CurrentAreaLevel,
|
||||
// [0xEC] uint CurrentAreaHash,
|
||||
// [0x948] StdVector Environments,
|
||||
// [0x9F0] LocalPlayerStruct PlayerInfo, ← contains ServerDataPtr at +0x0, LocalPlayerPtr at +0x20
|
||||
// [0xAF8] EntityListStruct Entities, ← StdMap AwakeEntities + StdMap SleepingEntities
|
||||
// [0xCC0] TerrainStruct TerrainMetadata ← inline, not behind pointer
|
||||
// }
|
||||
/// <summary>AreaInstance → CurrentAreaLevel (dump: byte at 0xAC, CE confirmed: byte at 0xC4).</summary>
|
||||
public int AreaLevelOffset { get; set; } = 0xC4;
|
||||
/// <summary>If true, AreaLevel is a byte. If false, read as int.</summary>
|
||||
public bool AreaLevelIsByte { get; set; } = true;
|
||||
/// <summary>Static offset from module base to cached area level int (CE: exe+3E84B78). 0 = disabled.</summary>
|
||||
public int AreaLevelStaticOffset { get; set; } = 0;
|
||||
/// <summary>AreaInstance → CurrentAreaHash uint (dump: 0xEC).</summary>
|
||||
public int AreaHashOffset { get; set; } = 0xEC;
|
||||
/// <summary>AreaInstance → ServerData pointer (dump: 0x9F0 via LocalPlayerStruct.ServerDataPtr).</summary>
|
||||
public int ServerDataOffset { get; set; } = 0x9F0;
|
||||
/// <summary>AreaInstance → LocalPlayer entity pointer (dump: 0x9F0+0x20 = 0xA10 via LocalPlayerStruct.LocalPlayerPtr).</summary>
|
||||
public int LocalPlayerDirectOffset { get; set; } = 0xA10;
|
||||
/// <summary>AreaInstance → EntityListStruct offset (dump: 0xAF8). Contains StdMap AwakeEntities then SleepingEntities.</summary>
|
||||
public int EntityListOffset { get; set; } = 0xAF8;
|
||||
/// <summary>Offset within StdMap to _Mysize (entity count). MSVC std::map: head(8) + size(8).</summary>
|
||||
public int EntityCountInternalOffset { get; set; } = 0x08;
|
||||
|
||||
// ServerData → fields
|
||||
/// <summary>ServerData → LocalPlayer entity pointer (fallback if LocalPlayerDirectOffset is 0).</summary>
|
||||
public int LocalPlayerOffset { get; set; } = 0x20;
|
||||
|
||||
// ── Entity / Component ──
|
||||
// Dump: ItemStruct { [0x0] VTablePtr, [0x8] EntityDetailsPtr, [0x10] StdVector ComponentListPtr }
|
||||
// Dump: EntityOffsets { [0x0] ItemStruct, [0x80] uint Id, [0x84] byte IsValid }
|
||||
public int ComponentListOffset { get; set; } = 0x10;
|
||||
/// <summary>Entity → ObjectHeader pointer (for alternative component lookup via name→index map). ExileCore: 0x08.</summary>
|
||||
public int EntityHeaderOffset { get; set; } = 0x08;
|
||||
/// <summary>ObjectHeader → NativePtrArray for component name→index lookup. ExileCore: 0x40.</summary>
|
||||
public int ComponentLookupOffset { get; set; } = 0x40;
|
||||
/// <summary>Index of Life component in entity's component list. -1 = auto-discover via pattern scan.</summary>
|
||||
public int LifeComponentIndex { get; set; } = -1;
|
||||
/// <summary>Index of Render/Position component in entity's component list. -1 = unknown.</summary>
|
||||
public int RenderComponentIndex { get; set; } = -1;
|
||||
|
||||
// ── Life component (via direct chain from AreaInstance) ──
|
||||
// Deep scan confirmed: AreaInstance+0x420 → ptr+0x98 → Life component
|
||||
// VitalStruct gaps match dump: HP→Mana = 0x50, Mana→ES = 0x38
|
||||
// HP.Current@+0x188, Mana.Current@+0x1D8, ES.Current@+0x210
|
||||
/// <summary>First offset from AreaInstance to reach Life component (AreaInstance → ptr). 0 = use entity component list instead.</summary>
|
||||
public int LifeComponentOffset1 { get; set; } = 0x420;
|
||||
/// <summary>Second offset from intermediate pointer to Life component (ptr → Life).</summary>
|
||||
public int LifeComponentOffset2 { get; set; } = 0x98;
|
||||
// VitalStruct offsets within Life component (VitalStruct base, add VitalCurrentOffset/VitalTotalOffset)
|
||||
// ECS inner entity path: HP@+0x1D8 = 0x1A8+0x30, Mana@+0x228 = 0x1F8+0x30, ES@+0x260 = 0x230+0x30
|
||||
// (shifted +0x50 from old direct chain due to inner entity component header)
|
||||
public int LifeHealthOffset { get; set; } = 0x1A8;
|
||||
public int LifeManaOffset { get; set; } = 0x1F8;
|
||||
public int LifeEsOffset { get; set; } = 0x230;
|
||||
public int VitalCurrentOffset { get; set; } = 0x30;
|
||||
public int VitalTotalOffset { get; set; } = 0x2C;
|
||||
|
||||
// ── Render/Position component ──
|
||||
// Scan confirmed: position float triplet at +0x138 in component [10] (with real Z height)
|
||||
// Dump reference (older): RenderOffsets { [0xB0] CurrentWorldPosition } — shifted to 0x138 in current build
|
||||
public int PositionXOffset { get; set; } = 0x138;
|
||||
public int PositionYOffset { get; set; } = 0x13C;
|
||||
public int PositionZOffset { get; set; } = 0x140;
|
||||
|
||||
// ── Terrain (inline in AreaInstance) ──
|
||||
// Dump: TerrainStruct (at AreaInstance + 0xCC0) {
|
||||
// [0x18] StdTuple2D<long> TotalTiles,
|
||||
// [0x28] StdVector TileDetailsPtr,
|
||||
// [0xD0] StdVector GridWalkableData,
|
||||
// [0xE8] StdVector GridLandscapeData,
|
||||
// [0x100] int BytesPerRow,
|
||||
// [0x104] short TileHeightMultiplier
|
||||
// }
|
||||
/// <summary>Offset from AreaInstance to inline TerrainStruct (dump: 0xCC0).</summary>
|
||||
public int TerrainListOffset { get; set; } = 0xCC0;
|
||||
/// <summary>If true, terrain is inline in AreaInstance (no pointer dereference). If false, follow pointer.</summary>
|
||||
public bool TerrainInline { get; set; } = true;
|
||||
/// <summary>TerrainStruct → TotalTiles offset (dump: 0x18, StdTuple2D of long).</summary>
|
||||
public int TerrainDimensionsOffset { get; set; } = 0x18;
|
||||
/// <summary>TerrainStruct → GridWalkableData StdVector offset (dump: 0xD0).</summary>
|
||||
public int TerrainWalkableGridOffset { get; set; } = 0xD0;
|
||||
/// <summary>TerrainStruct → BytesPerRow (dump: 0x100).</summary>
|
||||
public int TerrainBytesPerRowOffset { get; set; } = 0x100;
|
||||
/// <summary>Kept for pointer-based terrain mode (TerrainInline=false).</summary>
|
||||
public int TerrainGridPtrOffset { get; set; } = 0x08;
|
||||
public int SubTilesPerCell { get; set; } = 23;
|
||||
|
||||
// Legacy terrain offsets (used by TerrainReader)
|
||||
public int InGameStateOffset { get; set; }
|
||||
public int IngameDataOffset { get; set; }
|
||||
public int TerrainDataOffset { get; set; }
|
||||
|
||||
// Within TerrainData struct
|
||||
public int NumColsOffset { get; set; }
|
||||
public int NumRowsOffset { get; set; }
|
||||
public int LayerMeleeOffset { get; set; }
|
||||
public int BytesPerRowOffset { get; set; }
|
||||
|
||||
/// <summary>Number of sub-tiles per terrain cell (typically 23 for POE).</summary>
|
||||
public int SubTilesPerCell { get; set; } = 23;
|
||||
|
||||
public static TerrainOffsets Load(string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
|
|
|
|||
|
|
@ -15,5 +15,6 @@
|
|||
<conv:CellBorderConverter x:Key="CellBorderConverter" />
|
||||
<conv:BoolToOverlayBrushConverter x:Key="OccupiedOverlayBrush" />
|
||||
<conv:MapRequirementsConverter x:Key="MapRequirementsText" />
|
||||
<conv:MatchedModBrushConverter x:Key="MatchedModBrush" />
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using Automata.Core;
|
|||
using Automata.Game;
|
||||
using Automata.GameLog;
|
||||
using Automata.Inventory;
|
||||
using Automata.Items;
|
||||
using Automata.Screen;
|
||||
using Automata.Screen.Ocr;
|
||||
using Automata.Trade;
|
||||
|
|
@ -27,6 +28,13 @@ public partial class App : Application
|
|||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
// Initialize Sidekick parser (fire-and-forget, non-blocking)
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try { await SidekickBootstrapper.InitializeAsync(); }
|
||||
catch (Exception ex) { Serilog.Log.Warning(ex, "Sidekick init failed"); }
|
||||
});
|
||||
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// Config
|
||||
|
|
@ -52,6 +60,7 @@ public partial class App : Application
|
|||
services.AddSingleton<TradeExecutor>();
|
||||
services.AddSingleton<TradeQueue>();
|
||||
services.AddSingleton<BotOrchestrator>();
|
||||
services.AddSingleton<ModPoolService>();
|
||||
|
||||
// ViewModels
|
||||
services.AddSingleton<MainWindowViewModel>();
|
||||
|
|
@ -60,11 +69,24 @@ public partial class App : Application
|
|||
services.AddSingleton<MappingViewModel>();
|
||||
services.AddSingleton<AtlasViewModel>();
|
||||
services.AddSingleton<CraftingViewModel>();
|
||||
services.AddSingleton<MemoryViewModel>();
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
var store = provider.GetRequiredService<ConfigStore>();
|
||||
var bot = provider.GetRequiredService<BotOrchestrator>();
|
||||
var modPool = provider.GetRequiredService<ModPoolService>();
|
||||
|
||||
// Fire-and-forget: load RePoE data, then populate CraftingVM base items
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await modPool.LoadAsync();
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var craftVm = provider.GetRequiredService<CraftingViewModel>();
|
||||
craftVm.LoadBaseItems();
|
||||
});
|
||||
});
|
||||
|
||||
var mainVm = provider.GetRequiredService<MainWindowViewModel>();
|
||||
mainVm.DebugVm = provider.GetRequiredService<DebugViewModel>();
|
||||
|
|
@ -72,6 +94,7 @@ public partial class App : Application
|
|||
mainVm.MappingVm = provider.GetRequiredService<MappingViewModel>();
|
||||
mainVm.AtlasVm = provider.GetRequiredService<AtlasViewModel>();
|
||||
mainVm.CraftingVm = provider.GetRequiredService<CraftingViewModel>();
|
||||
mainVm.MemoryVm = provider.GetRequiredService<MemoryViewModel>();
|
||||
|
||||
var window = new MainWindow { DataContext = mainVm };
|
||||
window.SetConfigStore(store);
|
||||
|
|
|
|||
|
|
@ -23,5 +23,16 @@
|
|||
<ProjectReference Include="..\Automata.Trade\Automata.Trade.csproj" />
|
||||
<ProjectReference Include="..\Automata.Log\Automata.Log.csproj" />
|
||||
<ProjectReference Include="..\Automata.Inventory\Automata.Inventory.csproj" />
|
||||
<ProjectReference Include="..\Automata.Memory\Automata.Memory.csproj" />
|
||||
</ItemGroup>
|
||||
<!-- Sidekick data files (English only) -->
|
||||
<ItemGroup>
|
||||
<Content Include="..\..\lib\Sidekick\data\poe2\ninja\**\*" Link="wwwroot\data\poe2\ninja\%(RecursiveDir)%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
|
||||
<Content Include="..\..\lib\Sidekick\data\poe2\pseudo\en.json" Link="wwwroot\data\poe2\pseudo\en.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
<Content Include="..\..\lib\Sidekick\data\poe2\stats\en.json" Link="wwwroot\data\poe2\stats\en.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
<Content Include="..\..\lib\Sidekick\data\poe2\trade\leagues.invariant.json" Link="wwwroot\data\poe2\trade\leagues.invariant.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
<Content Include="..\..\lib\Sidekick\data\poe2\trade\stats.en.json" Link="wwwroot\data\poe2\trade\stats.en.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
<Content Include="..\..\lib\Sidekick\data\poe2\trade\stats.invariant.json" Link="wwwroot\data\poe2\trade\stats.invariant.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
<Content Include="..\..\lib\Sidekick\data\poe2\trade\raw\**\*.en.json" Link="wwwroot\data\poe2\trade\raw\%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -130,3 +130,15 @@ public class CellBorderConverter : IValueConverter
|
|||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public class MatchedModBrushConverter : IValueConverter
|
||||
{
|
||||
private static readonly SolidColorBrush MatchedBrush = new(Color.Parse("#3fb950"));
|
||||
private static readonly SolidColorBrush UnmatchedBrush = new(Color.Parse("#e6edf3"));
|
||||
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
=> value is true ? MatchedBrush : UnmatchedBrush;
|
||||
|
||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Automata.Bot;
|
||||
using Automata.Core;
|
||||
using Automata.Items;
|
||||
using Serilog;
|
||||
|
||||
namespace Automata.Ui.ViewModels;
|
||||
|
|
@ -14,18 +16,18 @@ public partial class CraftStepViewModel : ObservableObject
|
|||
private readonly Action _onChanged;
|
||||
|
||||
[ObservableProperty] private string _label;
|
||||
[ObservableProperty] private string _selectedCurrency;
|
||||
[ObservableProperty] private CurrencyPositionViewModel? _selectedCurrency;
|
||||
[ObservableProperty] private string _requiredModsText;
|
||||
[ObservableProperty] private bool _matchAll;
|
||||
[ObservableProperty] private decimal? _maxAttempts;
|
||||
[ObservableProperty] private decimal? _onFailGoTo;
|
||||
|
||||
public CraftStepViewModel(CraftStep model, Action onChanged)
|
||||
public CraftStepViewModel(CraftStep model, ObservableCollection<CurrencyPositionViewModel> currencyItems, Action onChanged)
|
||||
{
|
||||
_model = model;
|
||||
_onChanged = onChanged;
|
||||
_label = model.Label;
|
||||
_selectedCurrency = model.CurrencyName;
|
||||
_selectedCurrency = currencyItems.FirstOrDefault(c => c.Name == model.CurrencyName);
|
||||
_requiredModsText = string.Join(", ", model.RequiredMods);
|
||||
_matchAll = model.MatchAll;
|
||||
_maxAttempts = model.MaxAttempts;
|
||||
|
|
@ -33,7 +35,7 @@ public partial class CraftStepViewModel : ObservableObject
|
|||
}
|
||||
|
||||
partial void OnLabelChanged(string value) { _model.Label = value; _onChanged(); }
|
||||
partial void OnSelectedCurrencyChanged(string value) { _model.CurrencyName = value ?? ""; _onChanged(); }
|
||||
partial void OnSelectedCurrencyChanged(CurrencyPositionViewModel? value) { _model.CurrencyName = value?.Name ?? ""; _onChanged(); }
|
||||
partial void OnMatchAllChanged(bool value) { _model.MatchAll = value; _onChanged(); }
|
||||
partial void OnMaxAttemptsChanged(decimal? value) { _model.MaxAttempts = (int)(value ?? 500); _onChanged(); }
|
||||
partial void OnOnFailGoToChanged(decimal? value) { _model.OnFailGoTo = value.HasValue ? (int)value.Value : null; _onChanged(); }
|
||||
|
|
@ -74,6 +76,7 @@ public partial class CraftRecipeViewModel : ObservableObject
|
|||
public partial class CraftingViewModel : ObservableObject
|
||||
{
|
||||
private readonly BotOrchestrator _bot;
|
||||
private readonly ModPoolService _modPool;
|
||||
private CraftingExecutor? _executor;
|
||||
|
||||
public ObservableCollection<CraftRecipeViewModel> Crafts { get; } = [];
|
||||
|
|
@ -83,7 +86,7 @@ public partial class CraftingViewModel : ObservableObject
|
|||
private CraftRecipeViewModel? _selectedCraft;
|
||||
|
||||
public ObservableCollection<CraftStepViewModel> Steps { get; } = [];
|
||||
public ObservableCollection<string> CurrencyNames { get; } = [];
|
||||
public ObservableCollection<CurrencyPositionViewModel> CurrencyItems { get; } = [];
|
||||
|
||||
[ObservableProperty] private string _craftState = "Idle";
|
||||
[ObservableProperty] private int _currentStepIndex;
|
||||
|
|
@ -92,12 +95,25 @@ public partial class CraftingViewModel : ObservableObject
|
|||
[ObservableProperty] private string? _lastItemText;
|
||||
[ObservableProperty] private string _itemPositionText = "";
|
||||
|
||||
// Parsed mods display
|
||||
public ObservableCollection<ParsedModViewModel> ParsedMods { get; } = [];
|
||||
|
||||
// Mod pool
|
||||
public ObservableCollection<BaseItemOption> BaseItems { get; } = [];
|
||||
[ObservableProperty] private BaseItemOption? _selectedBaseItem;
|
||||
[ObservableProperty] private decimal _itemLevel = 83;
|
||||
public ObservableCollection<AvailableModViewModel> AvailableMods { get; } = [];
|
||||
[ObservableProperty] private string _modFilterText = "";
|
||||
|
||||
private List<AvailableMod> _allMods = [];
|
||||
|
||||
public bool HasSelectedCraft => SelectedCraft != null;
|
||||
|
||||
public CraftingViewModel(BotOrchestrator bot)
|
||||
public CraftingViewModel(BotOrchestrator bot, ModPoolService modPool)
|
||||
{
|
||||
_bot = bot;
|
||||
RefreshCurrencyNames();
|
||||
_modPool = modPool;
|
||||
RefreshCurrencyItems();
|
||||
|
||||
// Load from config
|
||||
foreach (var recipe in bot.Config.Crafts)
|
||||
|
|
@ -105,15 +121,41 @@ public partial class CraftingViewModel : ObservableObject
|
|||
|
||||
if (Crafts.Count > 0)
|
||||
SelectedCraft = Crafts[0];
|
||||
|
||||
// Load base items when data is ready
|
||||
if (_modPool.IsLoaded)
|
||||
LoadBaseItems();
|
||||
}
|
||||
|
||||
public void RefreshCurrencyNames()
|
||||
public void LoadBaseItems()
|
||||
{
|
||||
CurrencyNames.Clear();
|
||||
BaseItems.Clear();
|
||||
foreach (var b in _modPool.GetCraftableBases())
|
||||
BaseItems.Add(b);
|
||||
|
||||
// Restore selection from current craft
|
||||
if (SelectedCraft?.Model.BaseItemKey is { } key)
|
||||
SelectedBaseItem = BaseItems.FirstOrDefault(b => b.Key == key);
|
||||
}
|
||||
|
||||
public void RefreshCurrencyItems()
|
||||
{
|
||||
CurrencyItems.Clear();
|
||||
var iconDir = Path.GetFullPath("assets/currency/currency");
|
||||
foreach (var cp in _bot.Config.CurrencyPositions)
|
||||
{
|
||||
if (cp.Name == "Craft Item") continue;
|
||||
CurrencyNames.Add(cp.Name);
|
||||
var vm = new CurrencyPositionViewModel(cp, () => { });
|
||||
if (!string.IsNullOrEmpty(cp.ApiId))
|
||||
{
|
||||
var cached = Path.Combine(iconDir, cp.ApiId + ".png");
|
||||
if (File.Exists(cached))
|
||||
{
|
||||
try { vm.IconBitmap = new Avalonia.Media.Imaging.Bitmap(cached); }
|
||||
catch { /* ignore corrupt files */ }
|
||||
}
|
||||
}
|
||||
CurrencyItems.Add(vm);
|
||||
}
|
||||
UpdateItemPositionText();
|
||||
}
|
||||
|
|
@ -133,7 +175,81 @@ public partial class CraftingViewModel : ObservableObject
|
|||
if (value == null) return;
|
||||
|
||||
foreach (var step in value.Model.Steps)
|
||||
Steps.Add(new CraftStepViewModel(step, SaveConfig));
|
||||
Steps.Add(new CraftStepViewModel(step, CurrencyItems, SaveConfig));
|
||||
|
||||
// Restore base/ilvl from saved recipe
|
||||
if (value.Model.BaseItemKey is { } key)
|
||||
SelectedBaseItem = BaseItems.FirstOrDefault(b => b.Key == key);
|
||||
else
|
||||
SelectedBaseItem = null;
|
||||
|
||||
ItemLevel = value.Model.ItemLevel;
|
||||
}
|
||||
|
||||
partial void OnSelectedBaseItemChanged(BaseItemOption? value)
|
||||
{
|
||||
if (SelectedCraft != null)
|
||||
{
|
||||
SelectedCraft.Model.BaseItemKey = value?.Key;
|
||||
SelectedCraft.Model.BaseItemName = value?.Name;
|
||||
SaveConfig();
|
||||
}
|
||||
RefreshModPool();
|
||||
}
|
||||
|
||||
partial void OnItemLevelChanged(decimal value)
|
||||
{
|
||||
if (SelectedCraft != null)
|
||||
{
|
||||
SelectedCraft.Model.ItemLevel = (int)value;
|
||||
SaveConfig();
|
||||
}
|
||||
RefreshModPool();
|
||||
}
|
||||
|
||||
partial void OnModFilterTextChanged(string value)
|
||||
{
|
||||
ApplyModFilter();
|
||||
}
|
||||
|
||||
private void RefreshModPool()
|
||||
{
|
||||
AvailableMods.Clear();
|
||||
_allMods.Clear();
|
||||
|
||||
if (SelectedBaseItem == null || !_modPool.IsLoaded) return;
|
||||
|
||||
_allMods = _modPool.GetAvailableMods(SelectedBaseItem.Key, (int)ItemLevel);
|
||||
ApplyModFilter();
|
||||
}
|
||||
|
||||
private void ApplyModFilter()
|
||||
{
|
||||
AvailableMods.Clear();
|
||||
var filter = ModFilterText?.Trim() ?? "";
|
||||
|
||||
foreach (var mod in _allMods)
|
||||
{
|
||||
if (filter.Length > 0 &&
|
||||
!mod.Text.Contains(filter, StringComparison.OrdinalIgnoreCase) &&
|
||||
!mod.Name.Contains(filter, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
AvailableMods.Add(new AvailableModViewModel(mod));
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void AddModToStep(AvailableModViewModel? modVm)
|
||||
{
|
||||
if (modVm == null || Steps.Count == 0) return;
|
||||
// Add to the last step's required mods
|
||||
var lastStep = Steps[^1];
|
||||
var existing = lastStep.RequiredModsText;
|
||||
var modText = modVm.Text;
|
||||
lastStep.RequiredModsText = string.IsNullOrWhiteSpace(existing)
|
||||
? modText
|
||||
: $"{existing}, {modText}";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
|
|
@ -163,7 +279,7 @@ public partial class CraftingViewModel : ObservableObject
|
|||
if (SelectedCraft == null) return;
|
||||
var step = new CraftStep { Label = $"Step {SelectedCraft.Model.Steps.Count + 1}" };
|
||||
SelectedCraft.Model.Steps.Add(step);
|
||||
Steps.Add(new CraftStepViewModel(step, SaveConfig));
|
||||
Steps.Add(new CraftStepViewModel(step, CurrencyItems, SaveConfig));
|
||||
SaveConfig();
|
||||
}
|
||||
|
||||
|
|
@ -235,11 +351,49 @@ public partial class CraftingViewModel : ObservableObject
|
|||
CurrentAttempt = _executor.CurrentAttempt;
|
||||
TotalAttempts = _executor.TotalAttempts;
|
||||
LastItemText = _executor.LastItemText;
|
||||
UpdateParsedMods(_executor.LastItemText);
|
||||
}
|
||||
_bot.UpdateExecutorState();
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateParsedMods(string? itemText)
|
||||
{
|
||||
ParsedMods.Clear();
|
||||
if (string.IsNullOrWhiteSpace(itemText)) return;
|
||||
|
||||
try
|
||||
{
|
||||
var item = ItemReader.ParseItemText(itemText);
|
||||
if (item == null) return;
|
||||
|
||||
// Get current step's required mods for matching
|
||||
var requiredMods = new List<string>();
|
||||
if (_executor != null && _executor.CurrentStepIndex < Steps.Count)
|
||||
{
|
||||
var stepVm = Steps[_executor.CurrentStepIndex];
|
||||
if (!string.IsNullOrWhiteSpace(stepVm.RequiredModsText))
|
||||
{
|
||||
requiredMods = stepVm.RequiredModsText
|
||||
.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var stat in item.Stats)
|
||||
{
|
||||
var category = ParsedModViewModel.CategoryFromStatCategory(stat.Category);
|
||||
var isMatched = requiredMods.Any(mod =>
|
||||
stat.Text.Contains(mod, StringComparison.OrdinalIgnoreCase));
|
||||
ParsedMods.Add(new ParsedModViewModel(category, stat.Text, isMatched));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, "Failed to parse item mods for display");
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveConfig()
|
||||
{
|
||||
_bot.Store.Save();
|
||||
|
|
|
|||
|
|
@ -182,6 +182,7 @@ public partial class MainWindowViewModel : ObservableObject
|
|||
public MappingViewModel? MappingVm { get; set; }
|
||||
public AtlasViewModel? AtlasVm { get; set; }
|
||||
public CraftingViewModel? CraftingVm { get; set; }
|
||||
public MemoryViewModel? MemoryVm { get; set; }
|
||||
|
||||
partial void OnBotModeChanged(BotMode value)
|
||||
{
|
||||
|
|
|
|||
428
src/Automata.Ui/ViewModels/MemoryViewModel.cs
Normal file
428
src/Automata.Ui/ViewModels/MemoryViewModel.cs
Normal file
|
|
@ -0,0 +1,428 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Automata.Memory;
|
||||
|
||||
namespace Automata.Ui.ViewModels;
|
||||
|
||||
public partial class MemoryNodeViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty] private string _name;
|
||||
[ObservableProperty] private string _value = "";
|
||||
[ObservableProperty] private string _valueColor = "#484f58";
|
||||
[ObservableProperty] private bool _isExpanded = true;
|
||||
|
||||
public ObservableCollection<MemoryNodeViewModel> Children { get; } = [];
|
||||
|
||||
public MemoryNodeViewModel(string name)
|
||||
{
|
||||
_name = name;
|
||||
}
|
||||
|
||||
public void Set(string value, bool valid = true)
|
||||
{
|
||||
Value = value;
|
||||
ValueColor = valid ? "#3fb950" : "#484f58";
|
||||
}
|
||||
}
|
||||
|
||||
public partial class MemoryViewModel : ObservableObject
|
||||
{
|
||||
private GameMemoryReader? _reader;
|
||||
private CancellationTokenSource? _cts;
|
||||
|
||||
[ObservableProperty] private bool _isEnabled;
|
||||
[ObservableProperty] private string _statusText = "Not attached";
|
||||
|
||||
public ObservableCollection<MemoryNodeViewModel> RootNodes { get; } = [];
|
||||
|
||||
// Raw explorer
|
||||
[ObservableProperty] private string _rawAddress = "";
|
||||
[ObservableProperty] private string _rawOffsets = "";
|
||||
[ObservableProperty] private string _rawType = "pointer";
|
||||
[ObservableProperty] private string _rawResult = "";
|
||||
|
||||
// Scan
|
||||
[ObservableProperty] private string _scanAddress = "";
|
||||
[ObservableProperty] private string _scanOffsets = "";
|
||||
[ObservableProperty] private string _scanSize = "400";
|
||||
[ObservableProperty] private string _scanResult = "";
|
||||
|
||||
// Component scan vitals
|
||||
[ObservableProperty] private string _vitalHp = "";
|
||||
[ObservableProperty] private string _vitalMana = "";
|
||||
[ObservableProperty] private string _vitalEs = "";
|
||||
|
||||
public static string[] RawTypes { get; } = ["int32", "int64", "float", "double", "pointer", "bytes16", "string"];
|
||||
|
||||
// Tree node references
|
||||
private MemoryNodeViewModel? _processStatus;
|
||||
private MemoryNodeViewModel? _processPid;
|
||||
private MemoryNodeViewModel? _processModule;
|
||||
private MemoryNodeViewModel? _gsPattern;
|
||||
private MemoryNodeViewModel? _gsBase;
|
||||
private MemoryNodeViewModel? _gsController;
|
||||
private MemoryNodeViewModel? _gsStates;
|
||||
private MemoryNodeViewModel? _inGameState;
|
||||
private MemoryNodeViewModel? _areaInstance;
|
||||
private MemoryNodeViewModel? _areaLevel;
|
||||
private MemoryNodeViewModel? _areaHash;
|
||||
private MemoryNodeViewModel? _serverData;
|
||||
private MemoryNodeViewModel? _localPlayer;
|
||||
private MemoryNodeViewModel? _entityCount;
|
||||
private MemoryNodeViewModel? _playerPos;
|
||||
private MemoryNodeViewModel? _playerLife;
|
||||
private MemoryNodeViewModel? _playerMana;
|
||||
private MemoryNodeViewModel? _playerEs;
|
||||
private MemoryNodeViewModel? _terrainCells;
|
||||
private MemoryNodeViewModel? _terrainGrid;
|
||||
|
||||
partial void OnIsEnabledChanged(bool value)
|
||||
{
|
||||
if (value)
|
||||
Enable();
|
||||
else
|
||||
Disable();
|
||||
}
|
||||
|
||||
private void Enable()
|
||||
{
|
||||
_reader = new GameMemoryReader();
|
||||
var attached = _reader.Attach();
|
||||
if (attached)
|
||||
{
|
||||
var snap = _reader.ReadSnapshot();
|
||||
StatusText = $"Attached (PID {snap.ProcessId})";
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusText = "Process not found";
|
||||
}
|
||||
|
||||
BuildTree();
|
||||
_cts = new CancellationTokenSource();
|
||||
_ = ReadLoop(_cts.Token);
|
||||
}
|
||||
|
||||
private void Disable()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
_cts = null;
|
||||
_reader?.Dispose();
|
||||
_reader = null;
|
||||
RootNodes.Clear();
|
||||
StatusText = "Not attached";
|
||||
}
|
||||
|
||||
private void BuildTree()
|
||||
{
|
||||
RootNodes.Clear();
|
||||
|
||||
// Process
|
||||
var process = new MemoryNodeViewModel("Process");
|
||||
_processStatus = new MemoryNodeViewModel("Status:");
|
||||
_processPid = new MemoryNodeViewModel("PID:");
|
||||
_processModule = new MemoryNodeViewModel("Module:");
|
||||
process.Children.Add(_processStatus);
|
||||
process.Children.Add(_processPid);
|
||||
process.Children.Add(_processModule);
|
||||
|
||||
// GameState
|
||||
var gameState = new MemoryNodeViewModel("GameState");
|
||||
_gsPattern = new MemoryNodeViewModel("Pattern:");
|
||||
_gsBase = new MemoryNodeViewModel("Base:");
|
||||
_gsController = new MemoryNodeViewModel("Controller:");
|
||||
_gsStates = new MemoryNodeViewModel("States:");
|
||||
_inGameState = new MemoryNodeViewModel("InGameState:");
|
||||
gameState.Children.Add(_gsPattern);
|
||||
gameState.Children.Add(_gsBase);
|
||||
gameState.Children.Add(_gsController);
|
||||
gameState.Children.Add(_gsStates);
|
||||
gameState.Children.Add(_inGameState);
|
||||
|
||||
// InGameState children
|
||||
var inGameStateGroup = new MemoryNodeViewModel("InGameState");
|
||||
|
||||
// AreaInstance (primary data source in POE2)
|
||||
var areaInstanceGroup = new MemoryNodeViewModel("AreaInstance");
|
||||
_areaInstance = new MemoryNodeViewModel("Ptr:");
|
||||
_areaLevel = new MemoryNodeViewModel("AreaLevel:");
|
||||
_areaHash = new MemoryNodeViewModel("AreaHash:");
|
||||
_serverData = new MemoryNodeViewModel("ServerData:");
|
||||
_localPlayer = new MemoryNodeViewModel("LocalPlayer:");
|
||||
_entityCount = new MemoryNodeViewModel("Entities:");
|
||||
areaInstanceGroup.Children.Add(_areaInstance);
|
||||
areaInstanceGroup.Children.Add(_areaLevel);
|
||||
areaInstanceGroup.Children.Add(_areaHash);
|
||||
areaInstanceGroup.Children.Add(_serverData);
|
||||
areaInstanceGroup.Children.Add(_localPlayer);
|
||||
areaInstanceGroup.Children.Add(_entityCount);
|
||||
|
||||
// Player
|
||||
var player = new MemoryNodeViewModel("Player");
|
||||
_playerPos = new MemoryNodeViewModel("Position:") { Value = "?", ValueColor = "#484f58" };
|
||||
_playerLife = new MemoryNodeViewModel("Life:") { Value = "?", ValueColor = "#484f58" };
|
||||
_playerMana = new MemoryNodeViewModel("Mana:") { Value = "?", ValueColor = "#484f58" };
|
||||
_playerEs = new MemoryNodeViewModel("ES:") { Value = "?", ValueColor = "#484f58" };
|
||||
player.Children.Add(_playerPos);
|
||||
player.Children.Add(_playerLife);
|
||||
player.Children.Add(_playerMana);
|
||||
player.Children.Add(_playerEs);
|
||||
|
||||
// Terrain
|
||||
var terrain = new MemoryNodeViewModel("Terrain");
|
||||
_terrainCells = new MemoryNodeViewModel("Cells:");
|
||||
_terrainGrid = new MemoryNodeViewModel("Grid:");
|
||||
terrain.Children.Add(_terrainCells);
|
||||
terrain.Children.Add(_terrainGrid);
|
||||
|
||||
inGameStateGroup.Children.Add(areaInstanceGroup);
|
||||
inGameStateGroup.Children.Add(player);
|
||||
inGameStateGroup.Children.Add(terrain);
|
||||
|
||||
RootNodes.Add(process);
|
||||
RootNodes.Add(gameState);
|
||||
RootNodes.Add(inGameStateGroup);
|
||||
}
|
||||
|
||||
private async Task ReadLoop(CancellationToken ct)
|
||||
{
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(500));
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!await timer.WaitForNextTickAsync(ct))
|
||||
break;
|
||||
}
|
||||
catch (OperationCanceledException) { break; }
|
||||
|
||||
if (_reader is null) break;
|
||||
|
||||
var snap = _reader.ReadSnapshot();
|
||||
Dispatcher.UIThread.Post(() => UpdateTree(snap));
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateTree(GameStateSnapshot snap)
|
||||
{
|
||||
if (_processStatus is null) return;
|
||||
|
||||
// Process
|
||||
_processStatus.Set(snap.Attached ? "Attached" : "Not found", snap.Attached);
|
||||
_processPid!.Set(snap.Attached ? snap.ProcessId.ToString() : "—", snap.Attached);
|
||||
_processModule!.Set(
|
||||
snap.Attached ? $"0x{snap.ModuleBase:X} ({snap.ModuleSize:N0} bytes)" : "—",
|
||||
snap.Attached);
|
||||
|
||||
// GameState
|
||||
_gsPattern!.Set(snap.OffsetsConfigured ? "configured" : "not configured", snap.OffsetsConfigured);
|
||||
_gsBase!.Set(
|
||||
snap.GameStateBase != 0 ? $"0x{snap.GameStateBase:X}" : "not resolved",
|
||||
snap.GameStateBase != 0);
|
||||
_gsController!.Set(
|
||||
snap.ControllerPtr != 0 ? $"0x{snap.ControllerPtr:X}" : "—",
|
||||
snap.ControllerPtr != 0);
|
||||
_gsStates!.Set(
|
||||
snap.StatesCount > 0 ? snap.StatesCount.ToString() : "—",
|
||||
snap.StatesCount > 0);
|
||||
_inGameState!.Set(
|
||||
snap.InGameStatePtr != 0 ? $"0x{snap.InGameStatePtr:X}" : "not found",
|
||||
snap.InGameStatePtr != 0);
|
||||
|
||||
// Status text
|
||||
if (snap.Attached)
|
||||
StatusText = snap.InGameStatePtr != 0
|
||||
? $"Attached (PID {snap.ProcessId}) — InGame"
|
||||
: $"Attached (PID {snap.ProcessId})";
|
||||
else if (snap.Error is not null)
|
||||
StatusText = $"Error: {snap.Error}";
|
||||
|
||||
// AreaInstance
|
||||
_areaInstance!.Set(
|
||||
snap.AreaInstancePtr != 0 ? $"0x{snap.AreaInstancePtr:X}" : "—",
|
||||
snap.AreaInstancePtr != 0);
|
||||
_areaLevel!.Set(
|
||||
snap.AreaLevel > 0 ? snap.AreaLevel.ToString() : "—",
|
||||
snap.AreaLevel > 0);
|
||||
_areaHash!.Set(
|
||||
snap.AreaHash != 0 ? $"0x{snap.AreaHash:X8}" : "—",
|
||||
snap.AreaHash != 0);
|
||||
_serverData!.Set(
|
||||
snap.ServerDataPtr != 0 ? $"0x{snap.ServerDataPtr:X}" : "—",
|
||||
snap.ServerDataPtr != 0);
|
||||
_localPlayer!.Set(
|
||||
snap.LocalPlayerPtr != 0 ? $"0x{snap.LocalPlayerPtr:X}" : "—",
|
||||
snap.LocalPlayerPtr != 0);
|
||||
_entityCount!.Set(
|
||||
snap.EntityCount > 0 ? snap.EntityCount.ToString() : "—",
|
||||
snap.EntityCount > 0);
|
||||
|
||||
// Player position
|
||||
if (snap.HasPosition)
|
||||
_playerPos!.Set($"({snap.PlayerX:F1}, {snap.PlayerY:F1}, {snap.PlayerZ:F1})");
|
||||
else
|
||||
_playerPos!.Set("? (no Render component found)", false);
|
||||
|
||||
// Player vitals
|
||||
if (snap.HasVitals)
|
||||
{
|
||||
_playerLife!.Set($"{snap.LifeCurrent} / {snap.LifeTotal}");
|
||||
_playerMana!.Set($"{snap.ManaCurrent} / {snap.ManaTotal}");
|
||||
_playerEs!.Set($"{snap.EsCurrent} / {snap.EsTotal}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_playerLife!.Set("? (set LifeComponentIndex)", false);
|
||||
_playerMana!.Set("? (set LifeComponentIndex)", false);
|
||||
_playerEs!.Set("? (set LifeComponentIndex)", false);
|
||||
}
|
||||
|
||||
// Terrain
|
||||
if (snap.TerrainCols > 0 && snap.TerrainRows > 0)
|
||||
{
|
||||
_terrainCells!.Set($"{snap.TerrainCols}x{snap.TerrainRows}");
|
||||
_terrainGrid!.Set($"{snap.TerrainWidth}x{snap.TerrainHeight}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_terrainCells!.Set("?", false);
|
||||
_terrainGrid!.Set("?", false);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ReadAddressExecute()
|
||||
{
|
||||
if (_reader is null || !_reader.IsAttached)
|
||||
{
|
||||
RawResult = "Error: not attached";
|
||||
return;
|
||||
}
|
||||
|
||||
RawResult = _reader.ReadAddress(RawAddress, RawOffsets, RawType);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ScanExecute()
|
||||
{
|
||||
if (_reader is null || !_reader.IsAttached)
|
||||
{
|
||||
ScanResult = "Error: not attached";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(ScanSize, System.Globalization.NumberStyles.HexNumber, null, out var size))
|
||||
size = 0x400;
|
||||
|
||||
ScanResult = _reader.ScanRegion(ScanAddress, ScanOffsets, size);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ScanStatesExecute()
|
||||
{
|
||||
if (_reader is null || !_reader.IsAttached)
|
||||
{
|
||||
ScanResult = "Error: not attached";
|
||||
return;
|
||||
}
|
||||
|
||||
ScanResult = _reader.ScanAllStates();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ProbeExecute()
|
||||
{
|
||||
if (_reader is null || !_reader.IsAttached)
|
||||
{
|
||||
ScanResult = "Error: not attached";
|
||||
return;
|
||||
}
|
||||
|
||||
ScanResult = _reader.ProbeInGameState();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ScanComponentsExecute()
|
||||
{
|
||||
if (_reader is null || !_reader.IsAttached)
|
||||
{
|
||||
ScanResult = "Error: not attached";
|
||||
return;
|
||||
}
|
||||
|
||||
int.TryParse(VitalHp, out var hp);
|
||||
int.TryParse(VitalMana, out var mana);
|
||||
int.TryParse(VitalEs, out var es);
|
||||
|
||||
ScanResult = _reader.ScanComponents(hp, mana, es);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void DeepScanExecute()
|
||||
{
|
||||
if (_reader is null || !_reader.IsAttached)
|
||||
{
|
||||
ScanResult = "Error: not attached";
|
||||
return;
|
||||
}
|
||||
|
||||
int.TryParse(VitalHp, out var hp);
|
||||
int.TryParse(VitalMana, out var mana);
|
||||
int.TryParse(VitalEs, out var es);
|
||||
|
||||
ScanResult = _reader.DeepScanVitals(hp, mana, es);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void DiagnoseVitalsExecute()
|
||||
{
|
||||
if (_reader is null || !_reader.IsAttached)
|
||||
{
|
||||
ScanResult = "Error: not attached";
|
||||
return;
|
||||
}
|
||||
|
||||
ScanResult = _reader.DiagnoseVitals();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ScanPositionExecute()
|
||||
{
|
||||
if (_reader is null || !_reader.IsAttached)
|
||||
{
|
||||
ScanResult = "Error: not attached";
|
||||
return;
|
||||
}
|
||||
|
||||
ScanResult = _reader.ScanPosition();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void DiagnoseEntityExecute()
|
||||
{
|
||||
if (_reader is null || !_reader.IsAttached)
|
||||
{
|
||||
ScanResult = "Error: not attached";
|
||||
return;
|
||||
}
|
||||
|
||||
ScanResult = _reader.DiagnoseEntity();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ScanStructureExecute()
|
||||
{
|
||||
if (_reader is null || !_reader.IsAttached)
|
||||
{
|
||||
ScanResult = "Error: not attached";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(ScanSize, System.Globalization.NumberStyles.HexNumber, null, out var size))
|
||||
size = 0x2000;
|
||||
|
||||
ScanResult = _reader.ScanStructure(ScanAddress, ScanOffsets, size);
|
||||
}
|
||||
}
|
||||
28
src/Automata.Ui/ViewModels/ModPoolViewModel.cs
Normal file
28
src/Automata.Ui/ViewModels/ModPoolViewModel.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Automata.Core;
|
||||
|
||||
namespace Automata.Ui.ViewModels;
|
||||
|
||||
public partial class AvailableModViewModel : ObservableObject
|
||||
{
|
||||
public string ModId { get; }
|
||||
public string Name { get; }
|
||||
public string Text { get; }
|
||||
public string GenerationType { get; }
|
||||
public int RequiredLevel { get; }
|
||||
public string StatRange { get; }
|
||||
public string TypeTag { get; }
|
||||
public string TypeColor { get; }
|
||||
|
||||
public AvailableModViewModel(AvailableMod mod)
|
||||
{
|
||||
ModId = mod.ModId;
|
||||
Name = mod.Name;
|
||||
Text = mod.Text;
|
||||
GenerationType = mod.GenerationType;
|
||||
RequiredLevel = mod.RequiredLevel;
|
||||
StatRange = mod.StatRange;
|
||||
TypeTag = mod.GenerationType == "prefix" ? "P" : "S";
|
||||
TypeColor = mod.GenerationType == "prefix" ? "#58a6ff" : "#bc8cff";
|
||||
}
|
||||
}
|
||||
46
src/Automata.Ui/ViewModels/ParsedModViewModel.cs
Normal file
46
src/Automata.Ui/ViewModels/ParsedModViewModel.cs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Sidekick.Data.Items;
|
||||
|
||||
namespace Automata.Ui.ViewModels;
|
||||
|
||||
public partial class ParsedModViewModel : ObservableObject
|
||||
{
|
||||
public string Category { get; }
|
||||
public string Text { get; }
|
||||
public string CategoryColor { get; }
|
||||
[ObservableProperty] private bool _isMatched;
|
||||
|
||||
public ParsedModViewModel(string category, string text, bool isMatched)
|
||||
{
|
||||
Category = category;
|
||||
Text = text;
|
||||
_isMatched = isMatched;
|
||||
CategoryColor = CategoryToColor(category);
|
||||
}
|
||||
|
||||
public static string CategoryToColor(string category) => category switch
|
||||
{
|
||||
"Explicit" => "#58a6ff",
|
||||
"Implicit" => "#bc8cff",
|
||||
"Crafted" => "#b4f5fc",
|
||||
"Fractured" => "#d2a064",
|
||||
"Enchant" => "#b4f5fc",
|
||||
"Rune" => "#7ce0a6",
|
||||
"Corrupted" => "#d73a4a",
|
||||
_ => "#8b949e",
|
||||
};
|
||||
|
||||
public static string CategoryFromStatCategory(StatCategory cat) => cat switch
|
||||
{
|
||||
StatCategory.Explicit => "Explicit",
|
||||
StatCategory.Implicit => "Implicit",
|
||||
StatCategory.Crafted => "Crafted",
|
||||
StatCategory.Fractured => "Fractured",
|
||||
StatCategory.Enchant => "Enchant",
|
||||
StatCategory.Rune => "Rune",
|
||||
StatCategory.Corrupted => "Corrupted",
|
||||
StatCategory.Delve => "Explicit",
|
||||
StatCategory.Sanctum => "Explicit",
|
||||
_ => cat.ToString(),
|
||||
};
|
||||
}
|
||||
|
|
@ -339,6 +339,21 @@
|
|||
FontSize="10" Foreground="#484f58"
|
||||
VerticalAlignment="Center" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Text="Base" FontSize="12" Foreground="#8b949e"
|
||||
Width="80" VerticalAlignment="Center" />
|
||||
<AutoCompleteBox ItemsSource="{Binding BaseItems}"
|
||||
SelectedItem="{Binding SelectedBaseItem}"
|
||||
FilterMode="ContainsOrdinal"
|
||||
MinimumPrefixLength="0"
|
||||
Watermark="Search base type..."
|
||||
MinWidth="240" MaxDropDownHeight="300" />
|
||||
<TextBlock Text="iLvl" FontSize="12" Foreground="#8b949e"
|
||||
VerticalAlignment="Center" Margin="8,0,0,0" />
|
||||
<NumericUpDown Value="{Binding ItemLevel}"
|
||||
Minimum="1" Maximum="100"
|
||||
Width="90" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Bottom: status + controls -->
|
||||
|
|
@ -357,10 +372,32 @@
|
|||
FontSize="13" Foreground="#484f58"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
<!-- Parsed mods (color-coded); fallback to raw text -->
|
||||
<ItemsControl ItemsSource="{Binding ParsedMods}"
|
||||
IsVisible="{Binding ParsedMods.Count}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ParsedModViewModel">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4" Margin="0,1">
|
||||
<Border Background="{Binding CategoryColor}"
|
||||
CornerRadius="3" Padding="3,1"
|
||||
VerticalAlignment="Center" MinWidth="18"
|
||||
HorizontalAlignment="Center">
|
||||
<TextBlock Text="{Binding Category}"
|
||||
FontSize="8" FontWeight="Bold"
|
||||
Foreground="#0d1117"
|
||||
HorizontalAlignment="Center" />
|
||||
</Border>
|
||||
<TextBlock Text="{Binding Text}" FontSize="11"
|
||||
Foreground="{Binding IsMatched, Converter={StaticResource MatchedModBrush}}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<TextBlock Text="{Binding LastItemText}"
|
||||
FontSize="11" FontFamily="Consolas"
|
||||
Foreground="#8b949e" TextTrimming="CharacterEllipsis"
|
||||
MaxLines="3" TextWrapping="Wrap" />
|
||||
MaxLines="3" TextWrapping="Wrap"
|
||||
IsVisible="{Binding !ParsedMods.Count}" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,4,0,0">
|
||||
<Button Content="Start" Command="{Binding StartCraftCommand}"
|
||||
Padding="16,6" />
|
||||
|
|
@ -369,80 +406,136 @@
|
|||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Steps list -->
|
||||
<DockPanel>
|
||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal"
|
||||
Spacing="6" Margin="0,0,0,6">
|
||||
<TextBlock Text="STEPS" FontSize="11" FontWeight="SemiBold"
|
||||
Foreground="#8b949e" VerticalAlignment="Center" />
|
||||
<Button Content="+ Step" Command="{Binding AddStepCommand}"
|
||||
Padding="8,3" />
|
||||
</StackPanel>
|
||||
<ScrollViewer>
|
||||
<ItemsControl x:Name="StepsControl" ItemsSource="{Binding Steps}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:CraftStepViewModel">
|
||||
<Border Margin="0,3" Background="#21262d"
|
||||
CornerRadius="6" Padding="10,8">
|
||||
<StackPanel Spacing="6">
|
||||
<DockPanel>
|
||||
<StackPanel DockPanel.Dock="Right"
|
||||
Orientation="Horizontal" Spacing="4">
|
||||
<Button Content="^" Padding="6,3"
|
||||
Command="{ReflectionBinding #StepsControl.DataContext.MoveStepUpCommand}"
|
||||
CommandParameter="{Binding}" />
|
||||
<Button Content="v" Padding="6,3"
|
||||
Command="{ReflectionBinding #StepsControl.DataContext.MoveStepDownCommand}"
|
||||
CommandParameter="{Binding}" />
|
||||
<Button Content="X" Padding="6,3"
|
||||
Command="{ReflectionBinding #StepsControl.DataContext.RemoveStepCommand}"
|
||||
CommandParameter="{Binding}" />
|
||||
<!-- Steps + Mod Pool -->
|
||||
<Grid RowDefinitions="*,Auto,*">
|
||||
<!-- Steps list -->
|
||||
<DockPanel Grid.Row="0">
|
||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal"
|
||||
Spacing="6" Margin="0,0,0,6">
|
||||
<TextBlock Text="STEPS" FontSize="11" FontWeight="SemiBold"
|
||||
Foreground="#8b949e" VerticalAlignment="Center" />
|
||||
<Button Content="+ Step" Command="{Binding AddStepCommand}"
|
||||
Padding="8,3" />
|
||||
</StackPanel>
|
||||
<ScrollViewer>
|
||||
<ItemsControl x:Name="StepsControl" ItemsSource="{Binding Steps}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:CraftStepViewModel">
|
||||
<Border Margin="0,3" Background="#21262d"
|
||||
CornerRadius="6" Padding="10,8">
|
||||
<StackPanel Spacing="6">
|
||||
<DockPanel>
|
||||
<StackPanel DockPanel.Dock="Right"
|
||||
Orientation="Horizontal" Spacing="4">
|
||||
<Button Content="^" Padding="6,3"
|
||||
Command="{ReflectionBinding #StepsControl.DataContext.MoveStepUpCommand}"
|
||||
CommandParameter="{Binding}" />
|
||||
<Button Content="v" Padding="6,3"
|
||||
Command="{ReflectionBinding #StepsControl.DataContext.MoveStepDownCommand}"
|
||||
CommandParameter="{Binding}" />
|
||||
<Button Content="X" Padding="6,3"
|
||||
Command="{ReflectionBinding #StepsControl.DataContext.RemoveStepCommand}"
|
||||
CommandParameter="{Binding}" />
|
||||
</StackPanel>
|
||||
<TextBox Text="{Binding Label}"
|
||||
Watermark="Label" FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Margin="0,0,8,0" />
|
||||
</DockPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="Currency" FontSize="11"
|
||||
Foreground="#8b949e"
|
||||
VerticalAlignment="Center"
|
||||
Width="55" />
|
||||
<ComboBox ItemsSource="{ReflectionBinding #StepsControl.DataContext.CurrencyItems}"
|
||||
SelectedItem="{Binding SelectedCurrency}"
|
||||
MinWidth="180">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:CurrencyPositionViewModel">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<Image Source="{Binding IconBitmap}" Width="16" Height="16"
|
||||
IsVisible="{Binding IconBitmap, Converter={x:Static ObjectConverters.IsNotNull}}" />
|
||||
<TextBlock Text="{Binding Name}" FontSize="11" Foreground="#e6edf3" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<DockPanel>
|
||||
<TextBlock Text="Until" FontSize="11"
|
||||
Foreground="#8b949e" Width="55"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBox Text="{Binding RequiredModsText}"
|
||||
Watermark="mod1, mod2, ..." />
|
||||
</DockPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<CheckBox IsChecked="{Binding MatchAll}"
|
||||
Content="Match ALL"
|
||||
Foreground="#e6edf3" />
|
||||
<TextBlock Text="Max" FontSize="11"
|
||||
Foreground="#8b949e"
|
||||
VerticalAlignment="Center" />
|
||||
<NumericUpDown Value="{Binding MaxAttempts}"
|
||||
Minimum="0" Maximum="99999"
|
||||
Width="110" />
|
||||
<TextBlock Text="OnFail->" FontSize="11"
|
||||
Foreground="#8b949e"
|
||||
VerticalAlignment="Center" />
|
||||
<NumericUpDown Value="{Binding OnFailGoTo}"
|
||||
Minimum="0" Maximum="99"
|
||||
Width="100" />
|
||||
</StackPanel>
|
||||
<TextBox Text="{Binding Label}"
|
||||
Watermark="Label" FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Margin="0,0,8,0" />
|
||||
</DockPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="Currency" FontSize="11"
|
||||
Foreground="#8b949e"
|
||||
VerticalAlignment="Center"
|
||||
Width="55" />
|
||||
<ComboBox ItemsSource="{ReflectionBinding #StepsControl.DataContext.CurrencyNames}"
|
||||
SelectedItem="{Binding SelectedCurrency}"
|
||||
MinWidth="180" />
|
||||
</StackPanel>
|
||||
<DockPanel>
|
||||
<TextBlock Text="Until" FontSize="11"
|
||||
Foreground="#8b949e" Width="55"
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
|
||||
<!-- Separator -->
|
||||
<Border Grid.Row="1" Height="1" Background="#30363d" Margin="0,6" />
|
||||
|
||||
<!-- Mod Pool -->
|
||||
<DockPanel Grid.Row="2">
|
||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal"
|
||||
Spacing="6" Margin="0,0,0,6">
|
||||
<TextBlock Text="MOD POOL" FontSize="11" FontWeight="SemiBold"
|
||||
Foreground="#8b949e" VerticalAlignment="Center" />
|
||||
<TextBox Text="{Binding ModFilterText}" Watermark="Filter mods..."
|
||||
Width="200" FontSize="11" />
|
||||
</StackPanel>
|
||||
<ScrollViewer>
|
||||
<ItemsControl ItemsSource="{Binding AvailableMods}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:AvailableModViewModel">
|
||||
<Button Command="{ReflectionBinding $parent[ItemsControl].DataContext.AddModToStepCommand}"
|
||||
CommandParameter="{Binding}"
|
||||
Background="Transparent" Padding="2,1"
|
||||
HorizontalContentAlignment="Left"
|
||||
HorizontalAlignment="Stretch" Cursor="Hand">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Border Background="{Binding TypeColor}"
|
||||
CornerRadius="3" Padding="4,1"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding TypeTag}"
|
||||
FontSize="9" FontWeight="Bold"
|
||||
Foreground="#0d1117" />
|
||||
</Border>
|
||||
<TextBlock Text="{Binding Text}" FontSize="11"
|
||||
Foreground="#e6edf3"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Text="{Binding RequiredLevel, StringFormat='L{0}'}"
|
||||
FontSize="9" Foreground="#484f58"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBox Text="{Binding RequiredModsText}"
|
||||
Watermark="mod1, mod2, ..." />
|
||||
</DockPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<CheckBox IsChecked="{Binding MatchAll}"
|
||||
Content="Match ALL"
|
||||
Foreground="#e6edf3" />
|
||||
<TextBlock Text="Max" FontSize="11"
|
||||
Foreground="#8b949e"
|
||||
VerticalAlignment="Center" />
|
||||
<NumericUpDown Value="{Binding MaxAttempts}"
|
||||
Minimum="0" Maximum="99999"
|
||||
Width="110" />
|
||||
<TextBlock Text="OnFail->" FontSize="11"
|
||||
Foreground="#8b949e"
|
||||
VerticalAlignment="Center" />
|
||||
<NumericUpDown Value="{Binding OnFailGoTo}"
|
||||
Minimum="0" Maximum="99"
|
||||
Width="100" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
|
@ -577,6 +670,118 @@
|
|||
</DockPanel>
|
||||
</TabItem>
|
||||
|
||||
<!-- ========== MEMORY TAB ========== -->
|
||||
<TabItem Header="Memory">
|
||||
<DockPanel DataContext="{Binding MemoryVm}" Margin="0,6,0,0"
|
||||
x:DataType="vm:MemoryViewModel">
|
||||
|
||||
<!-- Top bar: Enable + status -->
|
||||
<Border DockPanel.Dock="Top" Background="#161b22" BorderBrush="#30363d"
|
||||
BorderThickness="1" CornerRadius="8" Padding="8" Margin="0,0,0,6">
|
||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||
<CheckBox IsChecked="{Binding IsEnabled}" Content="Enable"
|
||||
Foreground="#e6edf3" VerticalAlignment="Center" />
|
||||
<TextBlock Text="{Binding StatusText}" FontSize="12"
|
||||
Foreground="#8b949e" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Bottom: Raw Address Explorer + Scan -->
|
||||
<Border DockPanel.Dock="Bottom" Background="#161b22" BorderBrush="#30363d"
|
||||
BorderThickness="1" CornerRadius="8" Padding="8" Margin="0,6,0,0">
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
<!-- Left: Read -->
|
||||
<StackPanel Grid.Column="0" Spacing="6">
|
||||
<TextBlock Text="READ" FontSize="11" FontWeight="SemiBold"
|
||||
Foreground="#8b949e" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBox Text="{Binding RawAddress}" Watermark="Address (hex)"
|
||||
Width="130" FontFamily="Consolas" FontSize="11" />
|
||||
<TextBox Text="{Binding RawOffsets}" Watermark="Offsets"
|
||||
Width="120" FontFamily="Consolas" FontSize="11" />
|
||||
<ComboBox ItemsSource="{x:Static vm:MemoryViewModel.RawTypes}"
|
||||
SelectedItem="{Binding RawType}" Width="90" FontSize="11" />
|
||||
<Button Content="Read" Command="{Binding ReadAddressExecuteCommand}"
|
||||
Padding="10,4" />
|
||||
</StackPanel>
|
||||
<TextBox Text="{Binding RawResult, Mode=OneWay}" FontFamily="Consolas"
|
||||
FontSize="11" Foreground="#e6edf3" Background="#0d1117"
|
||||
BorderBrush="#30363d" BorderThickness="1" CornerRadius="4"
|
||||
IsReadOnly="True" TextWrapping="Wrap" MinHeight="24" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Right: Scan -->
|
||||
<StackPanel Grid.Column="2" Spacing="6">
|
||||
<TextBlock Text="SCAN (find pointers)" FontSize="11" FontWeight="SemiBold"
|
||||
Foreground="#8b949e" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBox Text="{Binding ScanAddress}" Watermark="Address (hex)"
|
||||
Width="130" FontFamily="Consolas" FontSize="11" />
|
||||
<TextBox Text="{Binding ScanOffsets}" Watermark="Offsets"
|
||||
Width="120" FontFamily="Consolas" FontSize="11" />
|
||||
<TextBox Text="{Binding ScanSize}" Watermark="Size (hex)"
|
||||
Width="60" FontFamily="Consolas" FontSize="11" />
|
||||
<Button Content="Scan" Command="{Binding ScanExecuteCommand}"
|
||||
Padding="10,4" />
|
||||
<Button Content="RTTI Struct" Command="{Binding ScanStructureExecuteCommand}"
|
||||
Padding="10,4" />
|
||||
<Button Content="All States" Command="{Binding ScanStatesExecuteCommand}"
|
||||
Padding="10,4" />
|
||||
<Button Content="Probe IGS" Command="{Binding ProbeExecuteCommand}"
|
||||
Padding="10,4" FontWeight="Bold" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,4,0,0">
|
||||
<TextBlock Text="Vitals:" Foreground="#8b949e" FontSize="11"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBox Text="{Binding VitalHp}" Watermark="HP"
|
||||
Width="60" FontFamily="Consolas" FontSize="11" />
|
||||
<TextBox Text="{Binding VitalMana}" Watermark="Mana"
|
||||
Width="60" FontFamily="Consolas" FontSize="11" />
|
||||
<TextBox Text="{Binding VitalEs}" Watermark="ES"
|
||||
Width="60" FontFamily="Consolas" FontSize="11" />
|
||||
<Button Content="Scan Components" Command="{Binding ScanComponentsExecuteCommand}"
|
||||
Padding="10,4" FontWeight="Bold" />
|
||||
<Button Content="Deep Scan" Command="{Binding DeepScanExecuteCommand}"
|
||||
Padding="10,4" FontWeight="Bold" />
|
||||
<Button Content="Diagnose Vitals" Command="{Binding DiagnoseVitalsExecuteCommand}"
|
||||
Padding="10,4" FontWeight="Bold" />
|
||||
<Button Content="Diagnose Entity" Command="{Binding DiagnoseEntityExecuteCommand}"
|
||||
Padding="10,4" FontWeight="Bold" />
|
||||
<Button Content="Scan Position" Command="{Binding ScanPositionExecuteCommand}"
|
||||
Padding="10,4" FontWeight="Bold" />
|
||||
</StackPanel>
|
||||
<TextBox Text="{Binding ScanResult, Mode=OneWay}" FontFamily="Consolas"
|
||||
FontSize="10" Foreground="#e6edf3" Background="#0d1117"
|
||||
BorderBrush="#30363d" BorderThickness="1" CornerRadius="4"
|
||||
IsReadOnly="True" AcceptsReturn="True"
|
||||
TextWrapping="NoWrap" MinHeight="60" MaxHeight="400"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Auto"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Center: TreeView -->
|
||||
<Border Background="#161b22" BorderBrush="#30363d"
|
||||
BorderThickness="1" CornerRadius="8" Padding="8">
|
||||
<TreeView ItemsSource="{Binding RootNodes}"
|
||||
Background="Transparent">
|
||||
<TreeView.ItemTemplate>
|
||||
<TreeDataTemplate ItemsSource="{Binding Children}"
|
||||
x:DataType="vm:MemoryNodeViewModel">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Text="{Binding Name}" Foreground="#8b949e"
|
||||
FontSize="12" />
|
||||
<TextBlock Text="{Binding Value}" Foreground="{Binding ValueColor}"
|
||||
FontSize="12" FontFamily="Consolas" />
|
||||
</StackPanel>
|
||||
</TreeDataTemplate>
|
||||
</TreeView.ItemTemplate>
|
||||
</TreeView>
|
||||
</Border>
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
|
||||
<!-- ========== DEBUG TAB ========== -->
|
||||
<TabItem Header="Debug">
|
||||
<ScrollViewer DataContext="{Binding DebugVm}" Margin="0,6,0,0">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue