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
|
# IDE / tools
|
||||||
.claude/
|
.claude/
|
||||||
nul
|
nul
|
||||||
|
|
||||||
|
# Extras
|
||||||
|
lib/extras
|
||||||
58
Automata.sln
58
Automata.sln
|
|
@ -27,6 +27,24 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Navigation", "src\
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Memory", "src\Automata.Memory\Automata.Memory.csproj", "{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Memory", "src\Automata.Memory\Automata.Memory.csproj", "{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}"
|
||||||
EndProject
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{6432F6A5-11A0-4960-AFFC-E810D4325C35} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
|
{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}
|
{859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
|
||||||
{D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B} = {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}
|
{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
|
EndGlobalSection
|
||||||
EndGlobal
|
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.Log\Automata.Log.csproj" />
|
||||||
<ProjectReference Include="..\Automata.Inventory\Automata.Inventory.csproj" />
|
<ProjectReference Include="..\Automata.Inventory\Automata.Inventory.csproj" />
|
||||||
<ProjectReference Include="..\Automata.Navigation\Automata.Navigation.csproj" />
|
<ProjectReference Include="..\Automata.Navigation\Automata.Navigation.csproj" />
|
||||||
|
<ProjectReference Include="..\Automata.Items\Automata.Items.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using Automata.Core;
|
using Automata.Core;
|
||||||
using Automata.Game;
|
using Automata.Game;
|
||||||
|
using Automata.Items;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
namespace Automata.Bot;
|
namespace Automata.Bot;
|
||||||
|
|
@ -193,6 +194,31 @@ public class CraftingExecutor
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(itemText)) return false;
|
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)
|
if (matchAll)
|
||||||
{
|
{
|
||||||
return requiredMods.All(mod =>
|
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 ItemX { get; set; }
|
||||||
public int ItemY { get; set; }
|
public int ItemY { get; set; }
|
||||||
public List<CraftStep> Steps { 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
|
public class CurrencyPosition
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,7 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Automata.Core\Automata.Core.csproj" />
|
<ProjectReference Include="..\Automata.Core\Automata.Core.csproj" />
|
||||||
<ProjectReference Include="..\Automata.Game\Automata.Game.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>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
using Automata.Core;
|
using Automata.Core;
|
||||||
using Automata.Game;
|
using Automata.Game;
|
||||||
|
using Sidekick.Apis.Poe.Items;
|
||||||
|
using Sidekick.Apis.Poe.Trade.Parser;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
namespace Automata.Items;
|
namespace Automata.Items;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads item data by hovering and pressing Ctrl+C to copy item text to clipboard.
|
/// 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>
|
/// </summary>
|
||||||
public class ItemReader
|
public class ItemReader
|
||||||
{
|
{
|
||||||
|
|
@ -49,11 +50,21 @@ public class ItemReader
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Wire up Sidekick's ItemParser
|
/// <summary>
|
||||||
// public async Task<ParsedItem?> ParseItem(int x, int y)
|
/// Parse raw item text into a structured Sidekick Item.
|
||||||
// {
|
/// Returns null if parsing fails.
|
||||||
// var text = await ReadItemText(x, y);
|
/// </summary>
|
||||||
// if (text == null) return null;
|
public static Item? ParseItemText(string text)
|
||||||
// return SidekickItemParser.Parse(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";
|
public string ProcessName { get; set; } = "PathOfExileSteam";
|
||||||
|
|
||||||
/// <summary>Pattern to find GameState base pointer. Empty = unknown for POE2.</summary>
|
/// <summary>Pattern to find GameState global. Use ^ to mark the RIP displacement position.</summary>
|
||||||
public string GameStatePattern { get; set; } = "";
|
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 InGameStateOffset { get; set; }
|
||||||
public int IngameDataOffset { get; set; }
|
public int IngameDataOffset { get; set; }
|
||||||
public int TerrainDataOffset { get; set; }
|
public int TerrainDataOffset { get; set; }
|
||||||
|
|
||||||
// Within TerrainData struct
|
|
||||||
public int NumColsOffset { get; set; }
|
public int NumColsOffset { get; set; }
|
||||||
public int NumRowsOffset { get; set; }
|
public int NumRowsOffset { get; set; }
|
||||||
public int LayerMeleeOffset { get; set; }
|
public int LayerMeleeOffset { get; set; }
|
||||||
public int BytesPerRowOffset { 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)
|
public static TerrainOffsets Load(string path)
|
||||||
{
|
{
|
||||||
if (!File.Exists(path))
|
if (!File.Exists(path))
|
||||||
|
|
|
||||||
|
|
@ -15,5 +15,6 @@
|
||||||
<conv:CellBorderConverter x:Key="CellBorderConverter" />
|
<conv:CellBorderConverter x:Key="CellBorderConverter" />
|
||||||
<conv:BoolToOverlayBrushConverter x:Key="OccupiedOverlayBrush" />
|
<conv:BoolToOverlayBrushConverter x:Key="OccupiedOverlayBrush" />
|
||||||
<conv:MapRequirementsConverter x:Key="MapRequirementsText" />
|
<conv:MapRequirementsConverter x:Key="MapRequirementsText" />
|
||||||
|
<conv:MatchedModBrushConverter x:Key="MatchedModBrush" />
|
||||||
</Application.Resources>
|
</Application.Resources>
|
||||||
</Application>
|
</Application>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ using Automata.Core;
|
||||||
using Automata.Game;
|
using Automata.Game;
|
||||||
using Automata.GameLog;
|
using Automata.GameLog;
|
||||||
using Automata.Inventory;
|
using Automata.Inventory;
|
||||||
|
using Automata.Items;
|
||||||
using Automata.Screen;
|
using Automata.Screen;
|
||||||
using Automata.Screen.Ocr;
|
using Automata.Screen.Ocr;
|
||||||
using Automata.Trade;
|
using Automata.Trade;
|
||||||
|
|
@ -27,6 +28,13 @@ public partial class App : Application
|
||||||
{
|
{
|
||||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
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();
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
|
|
@ -52,6 +60,7 @@ public partial class App : Application
|
||||||
services.AddSingleton<TradeExecutor>();
|
services.AddSingleton<TradeExecutor>();
|
||||||
services.AddSingleton<TradeQueue>();
|
services.AddSingleton<TradeQueue>();
|
||||||
services.AddSingleton<BotOrchestrator>();
|
services.AddSingleton<BotOrchestrator>();
|
||||||
|
services.AddSingleton<ModPoolService>();
|
||||||
|
|
||||||
// ViewModels
|
// ViewModels
|
||||||
services.AddSingleton<MainWindowViewModel>();
|
services.AddSingleton<MainWindowViewModel>();
|
||||||
|
|
@ -60,11 +69,24 @@ public partial class App : Application
|
||||||
services.AddSingleton<MappingViewModel>();
|
services.AddSingleton<MappingViewModel>();
|
||||||
services.AddSingleton<AtlasViewModel>();
|
services.AddSingleton<AtlasViewModel>();
|
||||||
services.AddSingleton<CraftingViewModel>();
|
services.AddSingleton<CraftingViewModel>();
|
||||||
|
services.AddSingleton<MemoryViewModel>();
|
||||||
|
|
||||||
var provider = services.BuildServiceProvider();
|
var provider = services.BuildServiceProvider();
|
||||||
|
|
||||||
var store = provider.GetRequiredService<ConfigStore>();
|
var store = provider.GetRequiredService<ConfigStore>();
|
||||||
var bot = provider.GetRequiredService<BotOrchestrator>();
|
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>();
|
var mainVm = provider.GetRequiredService<MainWindowViewModel>();
|
||||||
mainVm.DebugVm = provider.GetRequiredService<DebugViewModel>();
|
mainVm.DebugVm = provider.GetRequiredService<DebugViewModel>();
|
||||||
|
|
@ -72,6 +94,7 @@ public partial class App : Application
|
||||||
mainVm.MappingVm = provider.GetRequiredService<MappingViewModel>();
|
mainVm.MappingVm = provider.GetRequiredService<MappingViewModel>();
|
||||||
mainVm.AtlasVm = provider.GetRequiredService<AtlasViewModel>();
|
mainVm.AtlasVm = provider.GetRequiredService<AtlasViewModel>();
|
||||||
mainVm.CraftingVm = provider.GetRequiredService<CraftingViewModel>();
|
mainVm.CraftingVm = provider.GetRequiredService<CraftingViewModel>();
|
||||||
|
mainVm.MemoryVm = provider.GetRequiredService<MemoryViewModel>();
|
||||||
|
|
||||||
var window = new MainWindow { DataContext = mainVm };
|
var window = new MainWindow { DataContext = mainVm };
|
||||||
window.SetConfigStore(store);
|
window.SetConfigStore(store);
|
||||||
|
|
|
||||||
|
|
@ -23,5 +23,16 @@
|
||||||
<ProjectReference Include="..\Automata.Trade\Automata.Trade.csproj" />
|
<ProjectReference Include="..\Automata.Trade\Automata.Trade.csproj" />
|
||||||
<ProjectReference Include="..\Automata.Log\Automata.Log.csproj" />
|
<ProjectReference Include="..\Automata.Log\Automata.Log.csproj" />
|
||||||
<ProjectReference Include="..\Automata.Inventory\Automata.Inventory.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>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -130,3 +130,15 @@ public class CellBorderConverter : IValueConverter
|
||||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
=> throw new NotSupportedException();
|
=> 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.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using Automata.Bot;
|
using Automata.Bot;
|
||||||
using Automata.Core;
|
using Automata.Core;
|
||||||
|
using Automata.Items;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
namespace Automata.Ui.ViewModels;
|
namespace Automata.Ui.ViewModels;
|
||||||
|
|
@ -14,18 +16,18 @@ public partial class CraftStepViewModel : ObservableObject
|
||||||
private readonly Action _onChanged;
|
private readonly Action _onChanged;
|
||||||
|
|
||||||
[ObservableProperty] private string _label;
|
[ObservableProperty] private string _label;
|
||||||
[ObservableProperty] private string _selectedCurrency;
|
[ObservableProperty] private CurrencyPositionViewModel? _selectedCurrency;
|
||||||
[ObservableProperty] private string _requiredModsText;
|
[ObservableProperty] private string _requiredModsText;
|
||||||
[ObservableProperty] private bool _matchAll;
|
[ObservableProperty] private bool _matchAll;
|
||||||
[ObservableProperty] private decimal? _maxAttempts;
|
[ObservableProperty] private decimal? _maxAttempts;
|
||||||
[ObservableProperty] private decimal? _onFailGoTo;
|
[ObservableProperty] private decimal? _onFailGoTo;
|
||||||
|
|
||||||
public CraftStepViewModel(CraftStep model, Action onChanged)
|
public CraftStepViewModel(CraftStep model, ObservableCollection<CurrencyPositionViewModel> currencyItems, Action onChanged)
|
||||||
{
|
{
|
||||||
_model = model;
|
_model = model;
|
||||||
_onChanged = onChanged;
|
_onChanged = onChanged;
|
||||||
_label = model.Label;
|
_label = model.Label;
|
||||||
_selectedCurrency = model.CurrencyName;
|
_selectedCurrency = currencyItems.FirstOrDefault(c => c.Name == model.CurrencyName);
|
||||||
_requiredModsText = string.Join(", ", model.RequiredMods);
|
_requiredModsText = string.Join(", ", model.RequiredMods);
|
||||||
_matchAll = model.MatchAll;
|
_matchAll = model.MatchAll;
|
||||||
_maxAttempts = model.MaxAttempts;
|
_maxAttempts = model.MaxAttempts;
|
||||||
|
|
@ -33,7 +35,7 @@ public partial class CraftStepViewModel : ObservableObject
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnLabelChanged(string value) { _model.Label = value; _onChanged(); }
|
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 OnMatchAllChanged(bool value) { _model.MatchAll = value; _onChanged(); }
|
||||||
partial void OnMaxAttemptsChanged(decimal? value) { _model.MaxAttempts = (int)(value ?? 500); _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(); }
|
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
|
public partial class CraftingViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
private readonly BotOrchestrator _bot;
|
private readonly BotOrchestrator _bot;
|
||||||
|
private readonly ModPoolService _modPool;
|
||||||
private CraftingExecutor? _executor;
|
private CraftingExecutor? _executor;
|
||||||
|
|
||||||
public ObservableCollection<CraftRecipeViewModel> Crafts { get; } = [];
|
public ObservableCollection<CraftRecipeViewModel> Crafts { get; } = [];
|
||||||
|
|
@ -83,7 +86,7 @@ public partial class CraftingViewModel : ObservableObject
|
||||||
private CraftRecipeViewModel? _selectedCraft;
|
private CraftRecipeViewModel? _selectedCraft;
|
||||||
|
|
||||||
public ObservableCollection<CraftStepViewModel> Steps { get; } = [];
|
public ObservableCollection<CraftStepViewModel> Steps { get; } = [];
|
||||||
public ObservableCollection<string> CurrencyNames { get; } = [];
|
public ObservableCollection<CurrencyPositionViewModel> CurrencyItems { get; } = [];
|
||||||
|
|
||||||
[ObservableProperty] private string _craftState = "Idle";
|
[ObservableProperty] private string _craftState = "Idle";
|
||||||
[ObservableProperty] private int _currentStepIndex;
|
[ObservableProperty] private int _currentStepIndex;
|
||||||
|
|
@ -92,12 +95,25 @@ public partial class CraftingViewModel : ObservableObject
|
||||||
[ObservableProperty] private string? _lastItemText;
|
[ObservableProperty] private string? _lastItemText;
|
||||||
[ObservableProperty] private string _itemPositionText = "";
|
[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 bool HasSelectedCraft => SelectedCraft != null;
|
||||||
|
|
||||||
public CraftingViewModel(BotOrchestrator bot)
|
public CraftingViewModel(BotOrchestrator bot, ModPoolService modPool)
|
||||||
{
|
{
|
||||||
_bot = bot;
|
_bot = bot;
|
||||||
RefreshCurrencyNames();
|
_modPool = modPool;
|
||||||
|
RefreshCurrencyItems();
|
||||||
|
|
||||||
// Load from config
|
// Load from config
|
||||||
foreach (var recipe in bot.Config.Crafts)
|
foreach (var recipe in bot.Config.Crafts)
|
||||||
|
|
@ -105,15 +121,41 @@ public partial class CraftingViewModel : ObservableObject
|
||||||
|
|
||||||
if (Crafts.Count > 0)
|
if (Crafts.Count > 0)
|
||||||
SelectedCraft = Crafts[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)
|
foreach (var cp in _bot.Config.CurrencyPositions)
|
||||||
{
|
{
|
||||||
if (cp.Name == "Craft Item") continue;
|
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();
|
UpdateItemPositionText();
|
||||||
}
|
}
|
||||||
|
|
@ -133,7 +175,81 @@ public partial class CraftingViewModel : ObservableObject
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
|
|
||||||
foreach (var step in value.Model.Steps)
|
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]
|
[RelayCommand]
|
||||||
|
|
@ -163,7 +279,7 @@ public partial class CraftingViewModel : ObservableObject
|
||||||
if (SelectedCraft == null) return;
|
if (SelectedCraft == null) return;
|
||||||
var step = new CraftStep { Label = $"Step {SelectedCraft.Model.Steps.Count + 1}" };
|
var step = new CraftStep { Label = $"Step {SelectedCraft.Model.Steps.Count + 1}" };
|
||||||
SelectedCraft.Model.Steps.Add(step);
|
SelectedCraft.Model.Steps.Add(step);
|
||||||
Steps.Add(new CraftStepViewModel(step, SaveConfig));
|
Steps.Add(new CraftStepViewModel(step, CurrencyItems, SaveConfig));
|
||||||
SaveConfig();
|
SaveConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -235,11 +351,49 @@ public partial class CraftingViewModel : ObservableObject
|
||||||
CurrentAttempt = _executor.CurrentAttempt;
|
CurrentAttempt = _executor.CurrentAttempt;
|
||||||
TotalAttempts = _executor.TotalAttempts;
|
TotalAttempts = _executor.TotalAttempts;
|
||||||
LastItemText = _executor.LastItemText;
|
LastItemText = _executor.LastItemText;
|
||||||
|
UpdateParsedMods(_executor.LastItemText);
|
||||||
}
|
}
|
||||||
_bot.UpdateExecutorState();
|
_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()
|
private void SaveConfig()
|
||||||
{
|
{
|
||||||
_bot.Store.Save();
|
_bot.Store.Save();
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,7 @@ public partial class MainWindowViewModel : ObservableObject
|
||||||
public MappingViewModel? MappingVm { get; set; }
|
public MappingViewModel? MappingVm { get; set; }
|
||||||
public AtlasViewModel? AtlasVm { get; set; }
|
public AtlasViewModel? AtlasVm { get; set; }
|
||||||
public CraftingViewModel? CraftingVm { get; set; }
|
public CraftingViewModel? CraftingVm { get; set; }
|
||||||
|
public MemoryViewModel? MemoryVm { get; set; }
|
||||||
|
|
||||||
partial void OnBotModeChanged(BotMode value)
|
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"
|
FontSize="10" Foreground="#484f58"
|
||||||
VerticalAlignment="Center" Margin="8,0,0,0" />
|
VerticalAlignment="Center" Margin="8,0,0,0" />
|
||||||
</StackPanel>
|
</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>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Bottom: status + controls -->
|
<!-- Bottom: status + controls -->
|
||||||
|
|
@ -357,10 +372,32 @@
|
||||||
FontSize="13" Foreground="#484f58"
|
FontSize="13" Foreground="#484f58"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
</StackPanel>
|
</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}"
|
<TextBlock Text="{Binding LastItemText}"
|
||||||
FontSize="11" FontFamily="Consolas"
|
FontSize="11" FontFamily="Consolas"
|
||||||
Foreground="#8b949e" TextTrimming="CharacterEllipsis"
|
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">
|
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,4,0,0">
|
||||||
<Button Content="Start" Command="{Binding StartCraftCommand}"
|
<Button Content="Start" Command="{Binding StartCraftCommand}"
|
||||||
Padding="16,6" />
|
Padding="16,6" />
|
||||||
|
|
@ -369,8 +406,10 @@
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Steps + Mod Pool -->
|
||||||
|
<Grid RowDefinitions="*,Auto,*">
|
||||||
<!-- Steps list -->
|
<!-- Steps list -->
|
||||||
<DockPanel>
|
<DockPanel Grid.Row="0">
|
||||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal"
|
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal"
|
||||||
Spacing="6" Margin="0,0,0,6">
|
Spacing="6" Margin="0,0,0,6">
|
||||||
<TextBlock Text="STEPS" FontSize="11" FontWeight="SemiBold"
|
<TextBlock Text="STEPS" FontSize="11" FontWeight="SemiBold"
|
||||||
|
|
@ -408,9 +447,19 @@
|
||||||
Foreground="#8b949e"
|
Foreground="#8b949e"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Width="55" />
|
Width="55" />
|
||||||
<ComboBox ItemsSource="{ReflectionBinding #StepsControl.DataContext.CurrencyNames}"
|
<ComboBox ItemsSource="{ReflectionBinding #StepsControl.DataContext.CurrencyItems}"
|
||||||
SelectedItem="{Binding SelectedCurrency}"
|
SelectedItem="{Binding SelectedCurrency}"
|
||||||
MinWidth="180" />
|
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>
|
</StackPanel>
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<TextBlock Text="Until" FontSize="11"
|
<TextBlock Text="Until" FontSize="11"
|
||||||
|
|
@ -443,6 +492,50 @@
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</DockPanel>
|
</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" />
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
</DockPanel>
|
||||||
|
</Grid>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
@ -577,6 +670,118 @@
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</TabItem>
|
</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 ========== -->
|
<!-- ========== DEBUG TAB ========== -->
|
||||||
<TabItem Header="Debug">
|
<TabItem Header="Debug">
|
||||||
<ScrollViewer DataContext="{Binding DebugVm}" Margin="0,6,0,0">
|
<ScrollViewer DataContext="{Binding DebugVm}" Margin="0,6,0,0">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue