Compare commits
5 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f09ee5d106 | |||
| 8ca257bc79 | |||
| 703cfbfdee | |||
| 05bbcb244f | |||
| 0e7de0a5f3 |
310 changed files with 8882 additions and 1082 deletions
|
|
@ -5,29 +5,39 @@ VisualStudioVersion = 17.0.31903.59
|
|||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Core", "src\Automata.Core\Automata.Core.csproj", "{6432F6A5-11A0-4960-AFFC-E810D4325C35}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Core", "src\Nexus.Core\Nexus.Core.csproj", "{A31E6F94-A702-4B58-8317-83658E556B5C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Game", "src\Automata.Game\Automata.Game.csproj", "{97B8362D-777C-4ED1-B964-D6598B333E4C}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.GameOffsets", "src\Nexus.GameOffsets\Nexus.GameOffsets.csproj", "{C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Screen", "src\Automata.Screen\Automata.Screen.csproj", "{F92C5EA2-8999-41BC-9B28-D52AD5F3542C}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Memory", "src\Nexus.Memory\Nexus.Memory.csproj", "{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Items", "src\Automata.Items\Automata.Items.csproj", "{9CAB0D49-1E24-4F76-ABF8-9A5ED6819F00}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Input", "src\Nexus.Input\Nexus.Input.csproj", "{E61E96C5-3DE7-4B31-A7AD-CCA99BEF8E4A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Trade", "src\Automata.Trade\Automata.Trade.csproj", "{8F73A696-EB54-4C6F-9603-5A6BAC5D334A}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Systems", "src\Nexus.Systems\Nexus.Systems.csproj", "{95AC4C34-26A0-4D7F-A712-375EB28B54B8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Log", "src\Automata.Log\Automata.Log.csproj", "{B68D787D-7A83-4D8F-9F10-0B72C2E99B49}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Pathfinding", "src\Nexus.Pathfinding\Nexus.Pathfinding.csproj", "{F4B5C6D7-E8F9-0A1B-2C3D-4E5F6A7B8C9D}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Bot", "src\Automata.Bot\Automata.Bot.csproj", "{188C4F87-153F-4182-B816-9FB56F08CF3A}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Data", "src\Nexus.Data\Nexus.Data.csproj", "{1A2B3C4D-5E6F-7A8B-9C0D-E1F2A3B4C5D6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Inventory", "src\Automata.Inventory\Automata.Inventory.csproj", "{F186DDC8-6843-43E9-8BD3-9F914C5E784E}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Engine", "src\Nexus.Engine\Nexus.Engine.csproj", "{C2E97306-20E4-4A69-A7AB-541A72614C76}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Ui", "src\Automata.Ui\Automata.Ui.csproj", "{859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Game", "src\Nexus.Game\Nexus.Game.csproj", "{97B8362D-777C-4ED1-B964-D6598B333E4C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Navigation", "src\Automata.Navigation\Automata.Navigation.csproj", "{D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Screen", "src\Nexus.Screen\Nexus.Screen.csproj", "{F92C5EA2-8999-41BC-9B28-D52AD5F3542C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Roboto.Memory", "src\Roboto.Memory\Roboto.Memory.csproj", "{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Items", "src\Nexus.Items\Nexus.Items.csproj", "{9CAB0D49-1E24-4F76-ABF8-9A5ED6819F00}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Roboto.GameOffsets", "src\Roboto.GameOffsets\Roboto.GameOffsets.csproj", "{C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Trade", "src\Nexus.Trade\Nexus.Trade.csproj", "{8F73A696-EB54-4C6F-9603-5A6BAC5D334A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Log", "src\Nexus.Log\Nexus.Log.csproj", "{B68D787D-7A83-4D8F-9F10-0B72C2E99B49}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Bot", "src\Nexus.Bot\Nexus.Bot.csproj", "{188C4F87-153F-4182-B816-9FB56F08CF3A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Inventory", "src\Nexus.Inventory\Nexus.Inventory.csproj", "{F186DDC8-6843-43E9-8BD3-9F914C5E784E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Navigation", "src\Nexus.Navigation\Nexus.Navigation.csproj", "{D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Ui", "src\Nexus.Ui\Nexus.Ui.csproj", "{859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "lib", "lib", "{652F700E-4F84-4E66-BD62-717D3A8D6FBC}"
|
||||
EndProject
|
||||
|
|
@ -47,19 +57,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sidekick.Data", "lib\Sideki
|
|||
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
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "roboto", "roboto", "{D1A2B3C4-E5F6-7890-ABCD-EF1234567890}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Roboto.Core", "src\Roboto.Core\Roboto.Core.csproj", "{A31E6F94-A702-4B58-8317-83658E556B5C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Roboto.Input", "src\Roboto.Input\Roboto.Input.csproj", "{E61E96C5-3DE7-4B31-A7AD-CCA99BEF8E4A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Roboto.Systems", "src\Roboto.Systems\Roboto.Systems.csproj", "{95AC4C34-26A0-4D7F-A712-375EB28B54B8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Roboto.Engine", "src\Roboto.Engine\Roboto.Engine.csproj", "{C2E97306-20E4-4A69-A7AB-541A72614C76}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Roboto.Navigation", "src\Roboto.Navigation\Roboto.Navigation.csproj", "{F4B5C6D7-E8F9-0A1B-2C3D-4E5F6A7B8C9D}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Roboto.Data", "src\Roboto.Data\Roboto.Data.csproj", "{1A2B3C4D-5E6F-7A8B-9C0D-E1F2A3B4C5D6}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Simulator", "src\Nexus.Simulator\Nexus.Simulator.csproj", "{9198C826-9356-4763-87EF-BBC7166B745B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
|
@ -70,10 +68,38 @@ Global
|
|||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{6432F6A5-11A0-4960-AFFC-E810D4325C35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6432F6A5-11A0-4960-AFFC-E810D4325C35}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6432F6A5-11A0-4960-AFFC-E810D4325C35}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6432F6A5-11A0-4960-AFFC-E810D4325C35}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A31E6F94-A702-4B58-8317-83658E556B5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A31E6F94-A702-4B58-8317-83658E556B5C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A31E6F94-A702-4B58-8317-83658E556B5C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A31E6F94-A702-4B58-8317-83658E556B5C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}.Debug|Any CPU.ActiveCfg = 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.Build.0 = Release|Any CPU
|
||||
{E61E96C5-3DE7-4B31-A7AD-CCA99BEF8E4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E61E96C5-3DE7-4B31-A7AD-CCA99BEF8E4A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E61E96C5-3DE7-4B31-A7AD-CCA99BEF8E4A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E61E96C5-3DE7-4B31-A7AD-CCA99BEF8E4A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{95AC4C34-26A0-4D7F-A712-375EB28B54B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{95AC4C34-26A0-4D7F-A712-375EB28B54B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{95AC4C34-26A0-4D7F-A712-375EB28B54B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{95AC4C34-26A0-4D7F-A712-375EB28B54B8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F4B5C6D7-E8F9-0A1B-2C3D-4E5F6A7B8C9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F4B5C6D7-E8F9-0A1B-2C3D-4E5F6A7B8C9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F4B5C6D7-E8F9-0A1B-2C3D-4E5F6A7B8C9D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F4B5C6D7-E8F9-0A1B-2C3D-4E5F6A7B8C9D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1A2B3C4D-5E6F-7A8B-9C0D-E1F2A3B4C5D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1A2B3C4D-5E6F-7A8B-9C0D-E1F2A3B4C5D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1A2B3C4D-5E6F-7A8B-9C0D-E1F2A3B4C5D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1A2B3C4D-5E6F-7A8B-9C0D-E1F2A3B4C5D6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C2E97306-20E4-4A69-A7AB-541A72614C76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C2E97306-20E4-4A69-A7AB-541A72614C76}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C2E97306-20E4-4A69-A7AB-541A72614C76}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C2E97306-20E4-4A69-A7AB-541A72614C76}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{97B8362D-777C-4ED1-B964-D6598B333E4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{97B8362D-777C-4ED1-B964-D6598B333E4C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{97B8362D-777C-4ED1-B964-D6598B333E4C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
|
|
@ -102,22 +128,14 @@ Global
|
|||
{F186DDC8-6843-43E9-8BD3-9F914C5E784E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F186DDC8-6843-43E9-8BD3-9F914C5E784E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F186DDC8-6843-43E9-8BD3-9F914C5E784E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}.Debug|Any CPU.ActiveCfg = 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.Build.0 = Release|Any CPU
|
||||
{C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3}.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
|
||||
|
|
@ -150,33 +168,20 @@ Global
|
|||
{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
|
||||
{A31E6F94-A702-4B58-8317-83658E556B5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A31E6F94-A702-4B58-8317-83658E556B5C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A31E6F94-A702-4B58-8317-83658E556B5C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A31E6F94-A702-4B58-8317-83658E556B5C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E61E96C5-3DE7-4B31-A7AD-CCA99BEF8E4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E61E96C5-3DE7-4B31-A7AD-CCA99BEF8E4A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E61E96C5-3DE7-4B31-A7AD-CCA99BEF8E4A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E61E96C5-3DE7-4B31-A7AD-CCA99BEF8E4A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{95AC4C34-26A0-4D7F-A712-375EB28B54B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{95AC4C34-26A0-4D7F-A712-375EB28B54B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{95AC4C34-26A0-4D7F-A712-375EB28B54B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{95AC4C34-26A0-4D7F-A712-375EB28B54B8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C2E97306-20E4-4A69-A7AB-541A72614C76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C2E97306-20E4-4A69-A7AB-541A72614C76}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C2E97306-20E4-4A69-A7AB-541A72614C76}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C2E97306-20E4-4A69-A7AB-541A72614C76}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F4B5C6D7-E8F9-0A1B-2C3D-4E5F6A7B8C9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F4B5C6D7-E8F9-0A1B-2C3D-4E5F6A7B8C9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F4B5C6D7-E8F9-0A1B-2C3D-4E5F6A7B8C9D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F4B5C6D7-E8F9-0A1B-2C3D-4E5F6A7B8C9D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1A2B3C4D-5E6F-7A8B-9C0D-E1F2A3B4C5D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1A2B3C4D-5E6F-7A8B-9C0D-E1F2A3B4C5D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1A2B3C4D-5E6F-7A8B-9C0D-E1F2A3B4C5D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1A2B3C4D-5E6F-7A8B-9C0D-E1F2A3B4C5D6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9198C826-9356-4763-87EF-BBC7166B745B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9198C826-9356-4763-87EF-BBC7166B745B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9198C826-9356-4763-87EF-BBC7166B745B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9198C826-9356-4763-87EF-BBC7166B745B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{6432F6A5-11A0-4960-AFFC-E810D4325C35} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
|
||||
{A31E6F94-A702-4B58-8317-83658E556B5C} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
|
||||
{C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
|
||||
{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
|
||||
{E61E96C5-3DE7-4B31-A7AD-CCA99BEF8E4A} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
|
||||
{95AC4C34-26A0-4D7F-A712-375EB28B54B8} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
|
||||
{F4B5C6D7-E8F9-0A1B-2C3D-4E5F6A7B8C9D} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
|
||||
{1A2B3C4D-5E6F-7A8B-9C0D-E1F2A3B4C5D6} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
|
||||
{C2E97306-20E4-4A69-A7AB-541A72614C76} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
|
||||
{97B8362D-777C-4ED1-B964-D6598B333E4C} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
|
||||
{F92C5EA2-8999-41BC-9B28-D52AD5F3542C} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
|
||||
{9CAB0D49-1E24-4F76-ABF8-9A5ED6819F00} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
|
||||
|
|
@ -184,10 +189,8 @@ Global
|
|||
{B68D787D-7A83-4D8F-9F10-0B72C2E99B49} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
|
||||
{188C4F87-153F-4182-B816-9FB56F08CF3A} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
|
||||
{F186DDC8-6843-43E9-8BD3-9F914C5E784E} = {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}
|
||||
{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B} = {D1A2B3C4-E5F6-7890-ABCD-EF1234567890}
|
||||
{C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F} = {D1A2B3C4-E5F6-7890-ABCD-EF1234567890}
|
||||
{859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3} = {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}
|
||||
|
|
@ -196,12 +199,6 @@ Global
|
|||
{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}
|
||||
{D1A2B3C4-E5F6-7890-ABCD-EF1234567890} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
|
||||
{A31E6F94-A702-4B58-8317-83658E556B5C} = {D1A2B3C4-E5F6-7890-ABCD-EF1234567890}
|
||||
{E61E96C5-3DE7-4B31-A7AD-CCA99BEF8E4A} = {D1A2B3C4-E5F6-7890-ABCD-EF1234567890}
|
||||
{95AC4C34-26A0-4D7F-A712-375EB28B54B8} = {D1A2B3C4-E5F6-7890-ABCD-EF1234567890}
|
||||
{C2E97306-20E4-4A69-A7AB-541A72614C76} = {D1A2B3C4-E5F6-7890-ABCD-EF1234567890}
|
||||
{F4B5C6D7-E8F9-0A1B-2C3D-4E5F6A7B8C9D} = {D1A2B3C4-E5F6-7890-ABCD-EF1234567890}
|
||||
{1A2B3C4D-5E6F-7A8B-9C0D-E1F2A3B4C5D6} = {D1A2B3C4-E5F6-7890-ABCD-EF1234567890}
|
||||
{9198C826-9356-4763-87EF-BBC7166B745B} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
188
docs/architecture-overview.md
Normal file
188
docs/architecture-overview.md
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
# Nexus — Architecture Overview
|
||||
|
||||
## What Is This
|
||||
|
||||
A modular C# automation framework for POE2. The system reads game memory, builds a structured game state, runs AI systems (combat, navigation, threat assessment), and emits input commands. A separate trade pipeline monitors the trade site via a Node.js daemon and executes buy flows.
|
||||
|
||||
When the real game isn't available, a standalone **Simulator** replaces the memory layer with a procedural game world — same bot systems, same interfaces, zero game dependency.
|
||||
|
||||
## Solution Structure
|
||||
|
||||
```
|
||||
Nexus.sln (net8.0-windows10.0.19041.0)
|
||||
│
|
||||
├── Core Layer (shared types, no dependencies)
|
||||
│ └── Nexus.Core
|
||||
│
|
||||
├── Infrastructure Layer (reads from external sources)
|
||||
│ ├── Nexus.GameOffsets Pure offset structs for memory reading
|
||||
│ ├── Nexus.Memory Process memory reading (RPM, pattern scan)
|
||||
│ ├── Nexus.Screen Screen capture, OCR, grid detection
|
||||
│ ├── Nexus.Log Client.txt game log watcher
|
||||
│ └── Nexus.Input Win32 SendInput / Interception driver
|
||||
│
|
||||
├── Data Layer (transforms raw data into typed game state)
|
||||
│ └── Nexus.Data EntityMapper, EntityClassifier, GameDataCache, MemoryPoller, GameStateEnricher
|
||||
│
|
||||
├── Logic Layer (AI systems that decide what to do)
|
||||
│ ├── Nexus.Systems ThreatSystem, MovementSystem, CombatSystem, ResourceSystem, LootSystem
|
||||
│ ├── Nexus.Engine BotEngine (orchestrator), AreaProgressionSystem, MovementKeyTracker
|
||||
│ └── Nexus.Pathfinding NavigationController, A* PathFinder
|
||||
│
|
||||
├── Game Interaction Layer (acts on the game)
|
||||
│ ├── Nexus.Game Window focus, input sending, clipboard
|
||||
│ ├── Nexus.Items Item parsing via Sidekick
|
||||
│ ├── Nexus.Inventory Stash/inventory grid management
|
||||
│ ├── Nexus.Navigation Minimap-based real-time navigation
|
||||
│ └── Nexus.Trade Trade daemon IPC (Node.js Playwright)
|
||||
│
|
||||
├── Orchestration Layer (top-level coordination)
|
||||
│ ├── Nexus.Bot BotOrchestrator, Trade/Mapping/Crafting executors
|
||||
│ └── Nexus.Ui Avalonia 11.2 desktop GUI (entry point)
|
||||
│
|
||||
└── Testing Layer
|
||||
└── Nexus.Simulator Standalone game world for bot testing
|
||||
```
|
||||
|
||||
## Dependency Flow
|
||||
|
||||
```
|
||||
Nexus.Core
|
||||
│
|
||||
├── Nexus.GameOffsets ──→ Nexus.Memory ──→ Nexus.Data ──→ Nexus.Engine
|
||||
│ │ │
|
||||
├── Nexus.Input │ Nexus.Systems
|
||||
│ │ │
|
||||
├── Nexus.Screen ◄───────────────────────────────┘ │
|
||||
├── Nexus.Game │
|
||||
├── Nexus.Log │
|
||||
│ │
|
||||
├── Nexus.Pathfinding ◄─────────────────────────────────────────┘
|
||||
│
|
||||
├── Nexus.Items, Nexus.Inventory, Nexus.Navigation, Nexus.Trade
|
||||
│
|
||||
├── Nexus.Bot (consumes all above)
|
||||
│
|
||||
├── Nexus.Ui (DI hub, entry point, consumes all)
|
||||
│
|
||||
└── Nexus.Simulator (Core, Data, Systems, Pathfinding — NOT Memory/Input/Screen)
|
||||
```
|
||||
|
||||
## Data Flow — Per Tick
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ MemoryPoller Thread (60Hz hot / 10Hz cold) │
|
||||
│ │
|
||||
│ Hot tick (4 RPM calls): │
|
||||
│ Camera matrix, Player position, Player vitals, Loading state │
|
||||
│ │
|
||||
│ Cold tick (full hierarchical read): │
|
||||
│ Entity tree traversal → classification → EntitySnapshot[] │
|
||||
│ Terrain grid, Skills, Quests, UI elements │
|
||||
│ │
|
||||
│ Writes to GameDataCache (volatile references, lock-free) │
|
||||
└────────────────────────────┬────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ BotEngine Logic Thread (25Hz default) │
|
||||
│ │
|
||||
│ 1. Read latest GameState from cache │
|
||||
│ 2. GameStateEnricher → NearestEnemies, ThreatMap, DangerLevel │
|
||||
│ 3. Clear ActionQueue │
|
||||
│ 4. NavigationController.Update() → path, DesiredDirection │
|
||||
│ 5. Run systems in priority order: │
|
||||
│ ThreatSystem (50) → MovementSystem (100) → │
|
||||
│ AreaProgressionSystem (199) → NavigationSystem (200) → │
|
||||
│ CombatSystem (300) → ResourceSystem (400) → LootSystem (500) │
|
||||
│ 6. ActionQueue.Resolve() → conflict resolution │
|
||||
│ 7. ExecuteActions() → IInputController │
|
||||
└────────────────────────────┬────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ IInputController (Win32 SendInput or Interception driver) │
|
||||
│ │
|
||||
│ WASD keys (scan codes), mouse movement (Bézier curves), │
|
||||
│ skill casts, flask presses, clicks │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Action Resolution
|
||||
|
||||
Systems submit actions to a shared `ActionQueue`. The queue resolves conflicts:
|
||||
|
||||
| Action Type | Behavior |
|
||||
|-------------|----------|
|
||||
| FlaskAction | Always passes through |
|
||||
| MoveAction (priority ≤ 10) | Urgent flee — blocks CastAction |
|
||||
| MoveAction (priority > 10) | Normal move — allows CastAction alongside |
|
||||
| CastAction | Passes unless blocked by urgent flee |
|
||||
| Other (Key, Click, Chat) | Always passes through |
|
||||
|
||||
Priority values: lower number = higher priority. ThreatSystem uses priority 5 for emergency flee (blocks all casting).
|
||||
|
||||
## System Priority Table
|
||||
|
||||
| Priority | System | Purpose |
|
||||
|----------|--------|---------|
|
||||
| 50 | ThreatSystem | Emergency flee on High/Critical danger |
|
||||
| 100 | MovementSystem | Soft avoidance via inverse-square repulsion |
|
||||
| 199 | AreaProgressionSystem | Quest-driven area traversal and transitions |
|
||||
| 200 | NavigationSystem | Submits NavigationController's direction |
|
||||
| 300 | CombatSystem | Skill rotation, target selection, kiting |
|
||||
| 400 | ResourceSystem | Flask usage on life/mana thresholds |
|
||||
| 500 | LootSystem | Item pickup (stub) |
|
||||
|
||||
## Thread Model
|
||||
|
||||
| Thread | Rate | Responsibility |
|
||||
|--------|------|----------------|
|
||||
| MemoryPoller | 60Hz hot, 10Hz cold | Read game memory → GameDataCache |
|
||||
| BotEngine Logic | 25Hz | Run AI systems → emit actions |
|
||||
| Render (Simulator only) | vsync | ImGui + Veldrid drawing |
|
||||
| Trade Daemon | event-driven | Node.js Playwright → stdin/stdout JSON IPC |
|
||||
| Log Watcher | 200ms poll | Client.txt → area/whisper/trade events |
|
||||
|
||||
Cross-thread safety: GameDataCache uses `volatile` references. No locks — writer (MemoryPoller) atomically swaps reference types, readers (BotEngine) get consistent snapshots.
|
||||
|
||||
## Simulator Architecture
|
||||
|
||||
When the real game isn't available, `Nexus.Simulator` replaces the memory pipeline:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Nexus.Simulator.exe │
|
||||
│ │
|
||||
│ ┌──────────┐ GameState ┌──────────────────┐ │
|
||||
│ │ SimWorld │───────────────►│ GameDataCache │ │
|
||||
│ │ (terrain, │ SimPoller + │ (same as prod) │ │
|
||||
│ │ enemies, │ StateBuilder └────────┬─────────┘ │
|
||||
│ │ player) │ │ │
|
||||
│ └─────┬────┘ ┌────────▼─────────┐ │
|
||||
│ │ │ Bot Systems │ │
|
||||
│ │◄─────────────────────│ (unchanged) │ │
|
||||
│ │ SimInputController └──────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────▼───────────────────────────────────────┐ │
|
||||
│ │ ImGui + Veldrid Renderer (isometric 2D) │ │
|
||||
│ └─────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Bot systems don't know they're in a simulation — they see identical `GameState` objects and emit actions to `IInputController`.
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
| Decision | Rationale |
|
||||
|----------|-----------|
|
||||
| Immutable records (GameState, EntitySnapshot) | Thread-safe sharing without locks |
|
||||
| Priority-based ActionQueue | Natural conflict resolution without hardcoded if-else |
|
||||
| Two-tier memory polling (hot/cold) | Balance responsiveness (position at 60Hz) with CPU cost (entities at 10Hz) |
|
||||
| Scan codes over virtual keys | Games read hardware scan codes, not VK codes |
|
||||
| Separate Memory → Data layers | Memory reads raw bytes; Data interprets and classifies. Clean testability. |
|
||||
| IInputController interface | Swap real Win32 input for simulated input without changing bot logic |
|
||||
| BFS exploration + A* pathfinding | BFS finds what to explore; A* finds how to get there |
|
||||
| CharacterProfile auto-detection | Automatically applies combat/flask settings per character name |
|
||||
| External daemons (Trade, OCR) | Isolate browser/OCR concerns from main process |
|
||||
158
docs/core.md
Normal file
158
docs/core.md
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
# Nexus.Core — Shared Types & Abstractions
|
||||
|
||||
The foundation layer. Every other project depends on Core. Contains no logic — only types, interfaces, configuration, and utilities.
|
||||
|
||||
## GameState
|
||||
|
||||
Central immutable snapshot updated once per tick. All systems read from this; none mutate it.
|
||||
|
||||
```
|
||||
GameState
|
||||
├── Timing: TickNumber, DeltaTime, TimestampMs
|
||||
├── Player: PlayerState
|
||||
│ ├── CharacterName, Position, Z, HasPosition
|
||||
│ ├── Life/Mana/ES: Current, Total, Percent (derived)
|
||||
│ ├── ActionId (actor animation state)
|
||||
│ ├── Skills: SkillState[] (cooldowns, charges, cast state)
|
||||
│ ├── Flasks: FlaskState[] (charges, active, cooldown)
|
||||
│ └── Buffs: Buff[] (name, duration, charges, isDebuff)
|
||||
├── Entities: EntitySnapshot[] (all game entities)
|
||||
├── HostileMonsters: EntitySnapshot[] (alive monsters, filtered)
|
||||
├── NearbyLoot: EntitySnapshot[] (world items, filtered)
|
||||
├── NearestEnemies: EntitySnapshot[] (sorted by distance — enriched)
|
||||
├── Terrain: WalkabilitySnapshot (grid, offsets)
|
||||
├── Area: AreaHash, AreaLevel, CurrentAreaName
|
||||
├── UI: IsLoading, IsEscapeOpen, CameraMatrix
|
||||
├── Quests: ActiveQuests[], UiQuests[], Quests[] (enriched with paths)
|
||||
├── Threats: ThreatMap (zone buckets, centroid, rarity flags)
|
||||
├── Danger: DangerLevel (Safe/Low/Medium/High/Critical — enriched)
|
||||
└── GroundEffects: GroundEffect[] (hazard positions)
|
||||
```
|
||||
|
||||
## EntitySnapshot
|
||||
|
||||
Immutable record for any game entity:
|
||||
|
||||
- **Identity**: Id (uint), Path, Metadata, Category (EntityCategory enum)
|
||||
- **Spatial**: Position (Vector2), Z, DistanceToPlayer
|
||||
- **Combat**: IsAlive, LifeCurrent/Total, IsTargetable, ThreatLevel, Rarity, ModNames
|
||||
- **State**: ActionId, IsAttacking, IsMoving, Components (HashSet)
|
||||
- **Special**: TransitionName/State, ItemBaseName, IsQuestItem, LabelOffset
|
||||
|
||||
**EntityCategory** (17 types): Unknown, Player, Monster, Npc, WorldItem, Chest, Shrine, Portal, AreaTransition, Effect, Terrain, MiscObject, Waypoint, Door, Doodad, TownPortal, Critter
|
||||
|
||||
**MonsterRarity**: White, Magic, Rare, Unique
|
||||
|
||||
**MonsterThreatLevel**: None, Normal, Magic, Rare, Unique
|
||||
|
||||
## Actions
|
||||
|
||||
Polymorphic action system. All inherit from `BotAction` with a `Priority` field.
|
||||
|
||||
| Action | Fields | Purpose |
|
||||
|--------|--------|---------|
|
||||
| MoveAction | Direction (Vector2) | WASD movement |
|
||||
| CastAction | SkillScanCode, TargetScreenPos or TargetEntityId | Skill usage |
|
||||
| FlaskAction | FlaskScanCode | Flask consumption |
|
||||
| KeyAction | ScanCode, Type (Press/Down/Up) | Raw keyboard |
|
||||
| ClickAction | ScreenPosition, Type (Left/Right/Middle) | Mouse click |
|
||||
| ChatAction | Message | Send chat message |
|
||||
| WaitAction | DurationMs | Delay |
|
||||
|
||||
## ActionQueue
|
||||
|
||||
Manages conflict resolution between competing system outputs.
|
||||
|
||||
**Resolve() rules:**
|
||||
1. FlaskActions always pass through
|
||||
2. Get highest-priority MoveAction and CastAction
|
||||
3. If MoveAction priority ≤ 10 (urgent flee): include move, **block** cast
|
||||
4. Else: include both move and cast
|
||||
5. All other action types pass through
|
||||
|
||||
## ISystem Interface
|
||||
|
||||
```csharp
|
||||
public interface ISystem
|
||||
{
|
||||
int Priority { get; } // Execution order (lower = first)
|
||||
string Name { get; }
|
||||
bool IsEnabled { get; set; }
|
||||
void Update(GameState state, ActionQueue actions);
|
||||
}
|
||||
```
|
||||
|
||||
**SystemPriority constants**: Threat=50, Movement=100, Navigation=200, Combat=300, Resource=400, Loot=500
|
||||
|
||||
## IInputController Interface
|
||||
|
||||
Abstraction over Win32 input. Two implementations: SendInputController (vanilla), InterceptionInputController (driver).
|
||||
|
||||
```csharp
|
||||
public interface IInputController
|
||||
{
|
||||
bool IsInitialized { get; }
|
||||
void KeyDown(ushort scanCode);
|
||||
void KeyUp(ushort scanCode);
|
||||
void KeyPress(ushort scanCode, int holdMs = 50);
|
||||
void MouseMoveTo(int x, int y);
|
||||
void SmoothMoveTo(int x, int y); // Bézier curve interpolation
|
||||
void LeftClick(int x, int y);
|
||||
void RightClick(int x, int y);
|
||||
void MiddleClick(int x, int y);
|
||||
void LeftDown(); void LeftUp();
|
||||
void RightDown(); void RightUp();
|
||||
}
|
||||
```
|
||||
|
||||
## WalkabilitySnapshot
|
||||
|
||||
Grid-based terrain with offset support for infinite expansion:
|
||||
|
||||
```csharp
|
||||
public record WalkabilitySnapshot
|
||||
{
|
||||
public int Width, Height;
|
||||
public byte[] Data; // Row-major; 0=wall, nonzero=walkable
|
||||
public int OffsetX, OffsetY; // Absolute grid coords of top-left corner
|
||||
|
||||
public bool IsWalkable(int gx, int gy)
|
||||
{
|
||||
var lx = gx - OffsetX; // Absolute → local
|
||||
var ly = gy - OffsetY;
|
||||
if (out of bounds) return false;
|
||||
return Data[ly * Width + lx] != 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Coordinate conversion: `WorldToGrid = 23f / 250f ≈ 0.092`
|
||||
- World → Grid: `gx = (int)(worldX * WorldToGrid)`
|
||||
- Grid → World: `worldX = gx / WorldToGrid`
|
||||
|
||||
## Configuration
|
||||
|
||||
**BotConfig** — Static bot parameters:
|
||||
- Tick rates: LogicTickRateHz=60, MemoryPollRateHz=30
|
||||
- Movement: SafeDistance=400, RepulsionWeight=1.5, WaypointReachedDistance=80
|
||||
- Humanization: MinReactionDelayMs=50, MaxReactionDelayMs=150, ClickJitterRadius=3, MaxApm=250
|
||||
|
||||
**CharacterProfile** — Per-character settings:
|
||||
- Skills (8 slots: LMB, RMB, MMB, Q, E, R, T, F) with priority, cooldown, range, target selection
|
||||
- Flasks (thresholds, scan codes, cooldown)
|
||||
- Combat (global cooldown, attack/safe/kite ranges)
|
||||
|
||||
**SkillProfile** — Per-skill configuration:
|
||||
- InputType (KeyPress/LeftClick/RightClick/MiddleClick)
|
||||
- TargetSelection (Nearest/All/Rarest/MagicPlus/RarePlus/UniqueOnly)
|
||||
- RequiresTarget, IsAura, IsMovementSkill, MaintainPressed
|
||||
- MinMonstersInRange (AOE threshold)
|
||||
|
||||
## Utilities
|
||||
|
||||
- **WorldToScreen.Project()** — Matrix projection: world coords → screen coords via camera matrix
|
||||
- **TerrainQuery.HasLineOfSight()** — Bresenham line walk on walkability grid
|
||||
- **TerrainQuery.FindWalkableDirection()** — Rotates direction ±45°/90°/135°/180° to find clear path
|
||||
- **Helpers.Sleep()** — Task delay with ±10% variance
|
||||
- **DangerLevel**: Safe, Low, Medium, High, Critical
|
||||
- **ThreatMap**: TotalHostiles, CloseRange(<300), MidRange(300-600), FarRange(600-1200), ClosestDistance, ThreatCentroid, HasRareOrUnique
|
||||
229
docs/data-and-memory.md
Normal file
229
docs/data-and-memory.md
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
# Nexus.Data & Nexus.Memory — The Data Pipeline
|
||||
|
||||
## Overview
|
||||
|
||||
Two-layer architecture: **Memory** reads raw bytes from the game process; **Data** interprets, classifies, and caches them. This separation keeps memory reading logic free of business rules and makes the data layer testable independently.
|
||||
|
||||
```
|
||||
Game Process (RPM)
|
||||
│
|
||||
▼
|
||||
Nexus.Memory (raw reads, no business logic)
|
||||
│ GameMemoryReader → hierarchical state tree
|
||||
│ EntityList → red-black tree traversal
|
||||
│ ComponentReader → ECS component extraction
|
||||
│
|
||||
▼
|
||||
Nexus.Data (interpretation, classification, caching)
|
||||
│ MemoryPoller → two-tier event loop
|
||||
│ EntityMapper → Memory.Entity → Core.EntitySnapshot
|
||||
│ EntityClassifier → path + components → EntityCategory
|
||||
│ GameStateEnricher → derived threat/danger metrics
|
||||
│
|
||||
▼
|
||||
GameDataCache (volatile references, lock-free)
|
||||
│
|
||||
▼
|
||||
Bot Systems (read-only consumers)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GameDataCache — Single Source of Truth
|
||||
|
||||
Thread-safe, volatile reference holder. Writer: MemoryPoller thread. Readers: bot systems.
|
||||
|
||||
**Hot fields** (updated at 60Hz — 4 lightweight RPM calls):
|
||||
- CameraMatrix (64 bytes)
|
||||
- PlayerPosition (X, Y, Z)
|
||||
- PlayerVitals (HP, mana, ES current/max)
|
||||
- IsLoading, IsEscapeOpen
|
||||
|
||||
**Cold fields** (updated at 10Hz — full hierarchical read):
|
||||
- Entities, HostileMonsters, NearbyLoot
|
||||
- Terrain (WalkabilitySnapshot)
|
||||
- AreaHash, AreaLevel, CurrentAreaName
|
||||
- Quest data (linked lists, UI groups, state entries)
|
||||
- LatestState (complete GameState)
|
||||
|
||||
**Slow fields** (updated at 1Hz):
|
||||
- Character name
|
||||
- Quest linked lists, quest state entries
|
||||
|
||||
No locks — relies on volatile reference semantics for atomic swaps.
|
||||
|
||||
---
|
||||
|
||||
## MemoryPoller — Two-Tier Event Loop
|
||||
|
||||
Owns the memory-reading background thread.
|
||||
|
||||
### Hot Tick (60Hz)
|
||||
|
||||
4 pre-resolved RPM calls using cached addresses:
|
||||
1. Camera matrix (64 bytes from cached address)
|
||||
2. Player position (12 bytes: X, Y, Z)
|
||||
3. Player vitals (24 bytes: HP, mana, ES)
|
||||
4. Loading/escape state (pointer dereference + int)
|
||||
|
||||
No allocations, no GC. Targeting ~3-5ms per tick.
|
||||
|
||||
### Cold Tick (10Hz, every 6th hot tick)
|
||||
|
||||
Full hierarchical read:
|
||||
1. `GameMemoryReader.ReadSnapshot()` — cascades through state tree
|
||||
2. Re-resolve hot addresses via `ResolveHotAddresses()`
|
||||
3. `BuildGameState()` — map entities, filter lists, process quests
|
||||
4. `GameStateEnricher.Enrich()` — compute derived fields
|
||||
5. Update all cache fields
|
||||
|
||||
### BuildGameState() Flow
|
||||
|
||||
```
|
||||
ReadSnapshot() → GameStateSnapshot (raw)
|
||||
│
|
||||
├── Entity mapping:
|
||||
│ for each entity in snapshot:
|
||||
│ EntityMapper.MapEntity(entity, playerPos) → EntitySnapshot
|
||||
│
|
||||
├── Filter into:
|
||||
│ - HostileMonsters (Category==Monster && IsAlive)
|
||||
│ - NearbyLoot (Category==WorldItem)
|
||||
│ - All entities
|
||||
│
|
||||
├── Quest processing:
|
||||
│ - Filter active (StateId > 0)
|
||||
│ - Resolve state text via QuestStateLookup
|
||||
│ - Convert to QuestProgress, QuestInfo
|
||||
│
|
||||
└── Returns GameState
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GameMemoryReader — Hierarchical State Tree
|
||||
|
||||
Top-level orchestrator. Creates sub-readers on `Attach()`:
|
||||
|
||||
```
|
||||
GameStates (top)
|
||||
└── InGameState
|
||||
├── AreaInstance
|
||||
│ ├── EntityList (MSVC std::map red-black tree)
|
||||
│ ├── PlayerSkills (Actor component)
|
||||
│ ├── QuestStates (dat file entries)
|
||||
│ └── Terrain (walkability grid)
|
||||
├── UIElements (quest linked lists, UI tree)
|
||||
└── WorldData (camera matrix)
|
||||
```
|
||||
|
||||
Each `RemoteObject` caches its data and depends on parent for context. Single `Update()` call cascades through tree.
|
||||
|
||||
### Infrastructure
|
||||
|
||||
- **ProcessMemory**: P/Invoke wrapper for ReadProcessMemory. Tracks reads/sec and KB/sec.
|
||||
- **MemoryContext**: Shared state — process handle, offsets, module base, pattern scanner.
|
||||
- **ComponentReader**: Reads ECS components (Life, Render, Mods, etc.) from entities.
|
||||
- **MsvcStringReader**: Reads MSVC std::wstring (SSO-aware: inline if capacity ≤ 8, heap pointer otherwise).
|
||||
- **PatternScanner**: AOB scan for resolving base addresses.
|
||||
|
||||
---
|
||||
|
||||
## EntityList — Tree Traversal
|
||||
|
||||
Reads entities from AreaInstance's MSVC std::map (red-black tree, in-order traversal).
|
||||
|
||||
**Tree node layout:**
|
||||
```
|
||||
+0x00: left child ptr
|
||||
+0x08: parent ptr
|
||||
+0x10: right child ptr
|
||||
+0x28: entity pointer
|
||||
```
|
||||
|
||||
**Optimization**: Tree order is cached — re-walked only when entity count changes.
|
||||
|
||||
**Per-entity reads:**
|
||||
1. Path (EntityDetails → std::wstring)
|
||||
2. Skip low-priority types (effects, terrain, critters — no components read)
|
||||
3. Position (Render component: X, Y, Z)
|
||||
4. Component lookup (STL hash map: name → index)
|
||||
5. Component data:
|
||||
- Targetable (bool flag)
|
||||
- Mods/ObjectMagicProperties (rarity)
|
||||
- Life (HP, dynamic — re-read every frame for monsters)
|
||||
- Actor (action ID)
|
||||
- WorldItem (inner entity for ground loot)
|
||||
- AreaTransition (destination area)
|
||||
|
||||
**Caching strategy:**
|
||||
- Stable per entity: path, component list, targetable, rarity, transition name
|
||||
- Dynamic (re-read every frame): monster HP, action ID
|
||||
|
||||
---
|
||||
|
||||
## EntityClassifier — Path + Components → Category
|
||||
|
||||
Single source of truth for entity classification.
|
||||
|
||||
1. **Path-based** (primary): Parses `Metadata/[Category]/...` path segments
|
||||
2. **Component override**: Monster, Chest, Shrine, Waypoint, AreaTransition, Portal, TownPortal, NPC, Player
|
||||
|
||||
Output: `EntityCategory` (Core enum, 17 types)
|
||||
|
||||
---
|
||||
|
||||
## EntityMapper — Memory.Entity → Core.EntitySnapshot
|
||||
|
||||
Transforms raw memory data to enriched snapshots:
|
||||
|
||||
```
|
||||
Memory.Entity (raw)
|
||||
│
|
||||
├── Copy: ID, path, metadata, position, Z, vitals, components, mods
|
||||
├── Classify: EntityClassifier.Classify(path, components) → EntityCategory
|
||||
├── Threat level: Rarity → MonsterThreatLevel
|
||||
├── Area name: AreaNameLookup.Resolve() for transitions
|
||||
├── Distance: Vector2.Distance(position, playerPos)
|
||||
└── Alive state: HasVitals ? LifeCurrent > 0 : true
|
||||
│
|
||||
▼
|
||||
Core.EntitySnapshot (public, classified, enriched)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GameStateEnricher — Derived Metrics
|
||||
|
||||
Computed once per cold tick, before systems run.
|
||||
|
||||
**NearestEnemies**: HostileMonsters sorted by distance to player.
|
||||
|
||||
**ThreatMap**:
|
||||
- TotalHostiles, CloseRange (<300u), MidRange (300-600u), FarRange (600-1200u)
|
||||
- ClosestDistance, ThreatCentroid (position average), HasRareOrUnique
|
||||
|
||||
**DangerLevel** — Weighted threat score:
|
||||
```
|
||||
score = Σ (distance_weight × rarity_multiplier)
|
||||
|
||||
Distance weights: <200u = 3×, <400u = 2×, else = 1×
|
||||
Rarity multipliers: Unique=5×, Rare=3×, Magic=1.5×, White=1×
|
||||
|
||||
Life override: HP < 30% → Critical, HP < 50% → High
|
||||
Score thresholds: ≥15 = Critical, ≥8 = High, ≥4 = Medium, >0 = Low, 0 = Safe
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Architectural Patterns
|
||||
|
||||
| Pattern | Implementation |
|
||||
|---------|---------------|
|
||||
| Lock-free cross-thread | Volatile references in GameDataCache; no locks needed |
|
||||
| Two-tier polling | Hot (4 RPM, 60Hz) + Cold (full read, 10Hz) |
|
||||
| Hierarchical caching | Each RemoteObject caches data, re-reads only on change |
|
||||
| Entity caching | Stable data cached per entity/zone; dynamic data (HP) re-read per frame |
|
||||
| Separation of concerns | Memory: raw bytes. Data: interpretation + classification |
|
||||
| Area name resolution | AreaNameLookup loads areas.json, caches ID → display name |
|
||||
| Area graph | BFS pathfinding for quest progression ordering |
|
||||
197
docs/engine-and-systems.md
Normal file
197
docs/engine-and-systems.md
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
# Nexus.Engine & Nexus.Systems — Bot Brain
|
||||
|
||||
## BotEngine (Orchestrator)
|
||||
|
||||
The main loop. Owns systems, navigation, profiles, and action execution.
|
||||
|
||||
### Logic Loop (25Hz, background thread)
|
||||
|
||||
```
|
||||
1. Wait for MemoryPoller to provide latest GameState
|
||||
2. CheckCharacterProfile() → auto-load profile if character changed
|
||||
3. GameStateEnricher.Enrich() → compute NearestEnemies, ThreatMap, DangerLevel
|
||||
4. Clear ActionQueue
|
||||
5. NavigationController.Update(state) → compute path, set DesiredDirection
|
||||
6. Run all ISystem implementations in priority order
|
||||
7. NavigationSystem submits MoveAction if DesiredDirection is set
|
||||
8. ActionQueue.Resolve() → merge conflicts
|
||||
9. ExecuteActions() → emit key/mouse/click commands via IInputController
|
||||
```
|
||||
|
||||
### Action Execution
|
||||
|
||||
| Action | Execution |
|
||||
|--------|-----------|
|
||||
| MoveAction | Direction → MovementKeyTracker → WASD key state changes (delta-based) |
|
||||
| CastAction | SmoothMoveTo target + key press. Re-projects moving entities. Adds ±30-50px jitter. |
|
||||
| FlaskAction | Direct key press |
|
||||
| KeyAction | Press/Down/Up operations |
|
||||
| ClickAction | Left/Right/Middle click at screen position |
|
||||
|
||||
### MovementKeyTracker
|
||||
|
||||
Converts world-space direction vectors to WASD keys for isometric camera:
|
||||
|
||||
```
|
||||
1. Rotate direction 45° to align with isometric axes
|
||||
2. sx = dir.X * cos(45°) - dir.Y * sin(45°)
|
||||
3. sy = dir.X * sin(45°) + dir.Y * cos(45°)
|
||||
4. W if sy > 0.3, S if sy < -0.3, D if sx > 0.3, A if sx < -0.3
|
||||
5. Only emit key changes (delta-based — no redundant KeyDown/KeyUp)
|
||||
```
|
||||
|
||||
### Mouse Drift (Navigation)
|
||||
|
||||
During navigation, lazily repositions the mouse toward enemy clusters:
|
||||
- Projects enemy centroid ahead of player movement
|
||||
- Applies ±25° angular offset for organic appearance
|
||||
- Fires every 800-1500ms (randomized)
|
||||
|
||||
### Safety
|
||||
|
||||
- Releases all held keys when loading screen or escape menu detected
|
||||
- CombatSystem's `ReleaseAllHeld()` called on state transitions
|
||||
|
||||
---
|
||||
|
||||
## Systems
|
||||
|
||||
### ThreatSystem (Priority 50)
|
||||
|
||||
Emergency threat response. Runs first, only acts on elevated danger.
|
||||
|
||||
| Danger | Response |
|
||||
|--------|----------|
|
||||
| Safe / Low | No action |
|
||||
| Medium | No action (MovementSystem handles soft avoidance) |
|
||||
| High | Flee toward safety (priority 50, allows casting) |
|
||||
| Critical or point-blank (<150 units) | **Urgent flee (priority 5) — blocks all casting** |
|
||||
|
||||
**Flee direction**: `Player.Position - ThreatCentroid`, validated against terrain via `FindWalkableDirection()`.
|
||||
|
||||
### MovementSystem (Priority 100)
|
||||
|
||||
Continuous soft avoidance via **inverse-square repulsion field**.
|
||||
|
||||
For each hostile monster within SafeDistance (400 units):
|
||||
```
|
||||
force += (playerPos - enemyPos) / distanceSquared * RepulsionWeight
|
||||
```
|
||||
Normalizes sum, validates against terrain, submits as lower-priority MoveAction.
|
||||
|
||||
Effect: Player gently drifts away from enemies without hard fleeing.
|
||||
|
||||
### AreaProgressionSystem (Priority 199)
|
||||
|
||||
High-level area traversal. Runs before NavigationSystem to take precedence.
|
||||
|
||||
**State machine (7 phases):**
|
||||
```
|
||||
Exploring → Looting → NavigatingToChest → InteractingChest →
|
||||
NavigatingToTransition → Interacting → TalkingToNpc
|
||||
```
|
||||
|
||||
**Exploration strategy:**
|
||||
1. Check for elite enemies (Rare/Unique within 800u) → yield to combat
|
||||
2. Check for quest chests → navigate and interact
|
||||
3. Check for loot (if danger ≤ Low) → pick up within 600u
|
||||
4. Once fully explored → find area transition matching quest target
|
||||
5. In towns with active quest → talk to NPC
|
||||
|
||||
**Quest integration**: Queries active quests for target areas. Prioritizes tracked quests, then lowest act, then shortest path. Blacklists failed transitions after 5s timeout.
|
||||
|
||||
**Navigation delegation**: Uses `NavigationController.NavigateToEntity()` and `.Explore()`. Sets targets and yields until reached.
|
||||
|
||||
### NavigationSystem (Priority 200)
|
||||
|
||||
Ultra-thin passthrough. If `NavigationController.DesiredDirection` is set, submits a MoveAction. All actual pathfinding logic lives in NavigationController (see [pathfinding.md](pathfinding.md)).
|
||||
|
||||
### CombatSystem (Priority 300)
|
||||
|
||||
Skill rotation and target selection. Hot-swappable via CharacterProfile.
|
||||
|
||||
**Rotation loop:**
|
||||
```
|
||||
1. Check global cooldown (skip if recently cast)
|
||||
2. For each skill in priority order:
|
||||
a. Check per-skill cooldown
|
||||
b. Match skill to memory via slot index (fallback to name)
|
||||
c. If aura: cast once per zone
|
||||
d. If damage: find target → submit CastAction
|
||||
3. Release held keys for skills without valid targets
|
||||
```
|
||||
|
||||
**Target selection pipeline:**
|
||||
```
|
||||
1. Filter by TargetSelection (Nearest, Rarest, MagicPlus, RarePlus, UniqueOnly)
|
||||
2. Filter by range (SkillProfile.RangeMin/RangeMax)
|
||||
3. Filter by line-of-sight (terrain query)
|
||||
4. Check MinMonstersInRange (AOE threshold)
|
||||
5. Pick best: Rarest mode → prefer higher rarity then nearer; others → nearest
|
||||
6. Project to screen coordinates
|
||||
```
|
||||
|
||||
**Skill input types:**
|
||||
- LeftClick/RightClick/MiddleClick: Direct click at target position
|
||||
- KeyPress with MaintainPressed: Hold key continuously
|
||||
- KeyPress normal: Single tap
|
||||
|
||||
**Kiting/orbit (during global cooldown):**
|
||||
- Computes enemy centroid
|
||||
- Moves perpendicular to centroid (orbital movement)
|
||||
- Applies radial bias to maintain ideal distance
|
||||
- Flips orbit direction if terrain blocks path
|
||||
- Persists orbit sign across ticks for smooth motion
|
||||
|
||||
**Cooldown management:**
|
||||
- Per-skill: `max(skill.CooldownMs, globalCd + 50)` for rotation
|
||||
- MaintainPressed skills: use skill.CooldownMs directly
|
||||
- Area reset: clears aura tracking, resets orbit
|
||||
|
||||
### ResourceSystem (Priority 400)
|
||||
|
||||
Flask automation based on life/mana thresholds.
|
||||
|
||||
```
|
||||
if LifePercent < LifeFlaskThreshold (50%) && cooldown expired → FlaskAction
|
||||
if ManaPercent < ManaFlaskThreshold (50%) && cooldown expired → FlaskAction
|
||||
```
|
||||
|
||||
Flask cooldown: 4000ms default. Hot-swappable on character profile change.
|
||||
|
||||
### LootSystem (Priority 500)
|
||||
|
||||
Stub — disabled by default. Item pickup logic handled by AreaProgressionSystem's looting phase.
|
||||
|
||||
---
|
||||
|
||||
## System Interaction Diagram
|
||||
|
||||
```
|
||||
GameState (read-only, shared)
|
||||
│
|
||||
├─→ ThreatSystem ──→ MoveAction (priority 5 or 50)
|
||||
│ [blocks casting if priority ≤ 10]
|
||||
│
|
||||
├─→ MovementSystem ──→ MoveAction (priority 100)
|
||||
│ [soft repulsion, overridable]
|
||||
│
|
||||
├─→ AreaProgressionSystem ──→ NavigateTo/Explore commands
|
||||
│ [drives NavigationController]
|
||||
│
|
||||
├─→ NavigationSystem ──→ MoveAction (priority 200)
|
||||
│ [passthrough from NavigationController]
|
||||
│
|
||||
├─→ CombatSystem ──→ CastAction (priority 300)
|
||||
│ [skill rotation + target selection]
|
||||
│
|
||||
├─→ ResourceSystem ──→ FlaskAction (priority 400)
|
||||
│ [always passes through]
|
||||
│
|
||||
└─→ ActionQueue.Resolve()
|
||||
│
|
||||
├── Highest MoveAction wins
|
||||
├── CastAction passes unless blocked by urgent flee
|
||||
├── FlaskAction always passes
|
||||
└──→ ExecuteActions() → IInputController
|
||||
```
|
||||
207
docs/infrastructure.md
Normal file
207
docs/infrastructure.md
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
# Infrastructure & Game Interaction Projects
|
||||
|
||||
## Nexus.GameOffsets — Memory Layout Definitions
|
||||
|
||||
Pure offset structs for POE2 game memory. No logic, no reading — just struct layouts.
|
||||
|
||||
**Contents:**
|
||||
- **Entities/**: `EntityStruct`, `EntityDetails`, `ComponentLookup`, `ComponentNameAndIndex`, `ItemStruct`, `EntityTreeNode`
|
||||
- **Components/** (22 structs): Actor, Animated, Buffs, Chest, Life, Mods, Player, Positioned, Render, Stats, Targetable, Transitionable, WorldItem, etc.
|
||||
- **States/**: `InGameState`, `AreaInstance`, `AreaLoading`, `ServerData`, `WorldData`, `Inventory`, `ImportantUiElements`
|
||||
- **Natives/**: C++ STL memory layouts — `StdVector`, `StdMap`, `StdList`, `StdBucket`, `StdWString`, `StdTuple`
|
||||
|
||||
**Key offsets:**
|
||||
- Actor skills: 0xB00 (ActiveSkillsVector), 0xB18 (CooldownsVector)
|
||||
- UIElement: 0x10 (Children), 0x98 (StringId), 0x180 (Flags), 0x448 (Text)
|
||||
- Entity: 0x80 (ID), 0x84 (Flags), 0x08 (EntityDetails)
|
||||
|
||||
**Dependencies**: None. Used by Memory and Data layers.
|
||||
|
||||
---
|
||||
|
||||
## Nexus.Screen — Screen Capture, OCR & Detection
|
||||
|
||||
Screen capture, OCR, image processing, grid/item detection, loot label detection.
|
||||
|
||||
### Core Components
|
||||
|
||||
| Class | Purpose |
|
||||
|-------|---------|
|
||||
| ScreenReader | Main facade — OCR, template matching, screenshot, diff OCR |
|
||||
| IScreenCapture | Desktop duplication or GDI capture |
|
||||
| IOcrEngine | Interface for OCR backends (Win native, EasyOCR, OneOCR, WinOCR) |
|
||||
| PythonOcrBridge | Calls Python script via subprocess for EasyOCR/YOLO |
|
||||
|
||||
### Grid & Item Detection
|
||||
|
||||
| Class | Purpose |
|
||||
|-------|---------|
|
||||
| GridReader | Reads stash/inventory grids (12-col 70×70px or 24-col 35×35px) |
|
||||
| GridHandler | Template matching for cell occupancy, item size detection |
|
||||
| TemplateMatchHandler | NCC-based visual matching (find identical items in grid) |
|
||||
| DetectGridHandler | Edge detection to find grid boundaries |
|
||||
|
||||
### Detection Systems
|
||||
|
||||
| Class | Purpose |
|
||||
|-------|---------|
|
||||
| EnemyDetector | YOLO/ONNX object detection for enemy positions |
|
||||
| BossDetector | Boss-specific recognition |
|
||||
| HudReader | HUD element OCR (HP bar, mana, buffs) |
|
||||
| GameStateDetector | Main menu vs in-game state |
|
||||
| ScreenReader.DetectLootLabels() | Three-pass loot detection (polygon, contour, yellow text) |
|
||||
|
||||
### Frame Pipeline
|
||||
|
||||
Pub-sub for screen frames: `FramePipeline` distributes captured frames to multiple `IFrameConsumer` implementations (GameState, Enemy, Boss detectors, Minimap, Navigation).
|
||||
|
||||
**Used by**: Bot, Navigation, Inventory, Ui
|
||||
|
||||
---
|
||||
|
||||
## Nexus.Game — Game Interaction
|
||||
|
||||
Low-level game control — window focus, input sending, clipboard operations.
|
||||
|
||||
| Class | Purpose |
|
||||
|-------|---------|
|
||||
| GameController | Main facade — focus, chat, input, shortcuts |
|
||||
| InputSender | Win32 SendInput (scan codes), Bézier mouse movement, Ctrl+click |
|
||||
| WindowManager | SetForegroundWindow (with alt-key trick), GetWindowRect |
|
||||
| ClipboardHelper | System clipboard read/write |
|
||||
|
||||
**Key operations:**
|
||||
- `FocusWindow()` — SetForegroundWindow + alt-key trick (required for background processes)
|
||||
- `CtrlRightClick()` — buying from seller stash
|
||||
- `MoveMouse()` — Bézier curve smooth move
|
||||
- `MoveMouseInstant()` — direct teleport (no interpolation)
|
||||
- `TypeText()`, `SelectAll()`, `Paste()` — clipboard operations
|
||||
|
||||
**Used by**: Inventory, Trade, Items, Navigation, Bot
|
||||
|
||||
---
|
||||
|
||||
## Nexus.Log — Game Log Watcher
|
||||
|
||||
Parses Client.txt game log at 200ms poll intervals.
|
||||
|
||||
| Event | Pattern |
|
||||
|-------|---------|
|
||||
| AreaEntered | `[SCENE] Set Source [AreaName]` or `You have entered AreaName` |
|
||||
| WhisperReceived | Incoming whisper messages |
|
||||
| WhisperSent | Outgoing whisper messages |
|
||||
| TradeAccepted | Trade completion |
|
||||
| PartyJoined/Left | Party state changes |
|
||||
| LineReceived | Raw log lines |
|
||||
|
||||
`CurrentArea` detected from log tail on startup. Used by Bot (reset navigation on area change), Inventory (wait for area transitions), Navigation.
|
||||
|
||||
---
|
||||
|
||||
## Nexus.Trade — Trade Daemon IPC
|
||||
|
||||
Manages trade search monitoring via external Node.js Playwright daemon.
|
||||
|
||||
### TradeDaemonBridge
|
||||
|
||||
Spawns `node tools/trade-daemon/daemon.mjs`, communicates via stdin/stdout JSON.
|
||||
|
||||
**Commands (→ daemon):**
|
||||
- `start`, `addSearch`, `addDiamondSearch`
|
||||
- `pauseSearch`, `clickTravel`
|
||||
- `openScrapPage`, `reloadScrapPage`, `closeScrapPage`
|
||||
|
||||
**Events (← daemon):**
|
||||
- `newListings` → `NewListings(searchId, items[])`
|
||||
- `diamondListings` → `DiamondListings(searchId, pricedItems[])`
|
||||
- `wsClose` → websocket disconnection
|
||||
|
||||
**Trade flow**: Website "Travel to Hideout" button → stash opens → Ctrl+right-click to buy → `/hideout` to go home → store items
|
||||
|
||||
---
|
||||
|
||||
## Nexus.Items — Item Parsing
|
||||
|
||||
Parse item text from clipboard (Ctrl+C) using Sidekick item parser library.
|
||||
|
||||
| Class | Purpose |
|
||||
|-------|---------|
|
||||
| ItemReader | Move to item → Ctrl+C → read clipboard → parse |
|
||||
| SidekickBootstrapper | Initialize Sidekick parser on first use |
|
||||
|
||||
**Used by**: Bot (identify items during scraping)
|
||||
|
||||
---
|
||||
|
||||
## Nexus.Inventory — Stash & Grid Management
|
||||
|
||||
Scan player inventory, track item placement, deposit to stash.
|
||||
|
||||
| Class | Purpose |
|
||||
|-------|---------|
|
||||
| InventoryManager | Main interface — scan, deposit, clear |
|
||||
| InventoryTracker | Cell occupancy matrix + item metadata |
|
||||
| StashCalibrator | Grid boundary calibration via edge detection |
|
||||
|
||||
**Key operations:**
|
||||
- `ScanInventory()` → screenshot + grid scan → populate tracker
|
||||
- `DepositItemsToStash()` → find stash NPC → click items with Shift+Ctrl
|
||||
- `DepositAllToOpenStash()` → scan → click first occupied → repeat
|
||||
- `ClearToStash()` → scan → deposit all → return to hideout
|
||||
- `EnsureAtOwnHideout()` → `/hideout` command if needed
|
||||
|
||||
**Grid calibration (2560×1440):**
|
||||
- Cell sizes: 70×70px (12-col) or 35×35px (24-col), all 840px wide
|
||||
- Inventory (12×5): origin (1696, 788)
|
||||
- Stash 12×12: origin (23, 169) or (23, 216) in folder
|
||||
|
||||
---
|
||||
|
||||
## Nexus.Navigation — Minimap-Based Movement
|
||||
|
||||
Real-time navigation using minimap image matching + pathfinding. Separate from Nexus.Pathfinding (which is grid-based A*).
|
||||
|
||||
| Class | Purpose |
|
||||
|-------|---------|
|
||||
| NavigationExecutor | State machine: Capture → Process → Plan → Move → Stuck |
|
||||
| MinimapCapture | Frame pipeline consumer — wall color detection, checkpoint detection |
|
||||
| WorldMap | Position matching via cross-correlation, canvas stitching |
|
||||
| StuckDetector | No-progress detection |
|
||||
| WallColorTracker | Learns wall palette from initial spawn |
|
||||
|
||||
**Flow**: Capture minimap → detect position via wall color stitching → pathfind → send WASD keys
|
||||
|
||||
---
|
||||
|
||||
## Nexus.Bot — Top-Level Orchestration
|
||||
|
||||
Central coordinator that wires everything together.
|
||||
|
||||
| Class | Purpose |
|
||||
|-------|---------|
|
||||
| BotOrchestrator | DI container, state machine, frame pipeline management |
|
||||
| TradeExecutor | Single trade flow (navigate → buy → deposit) |
|
||||
| MappingExecutor | Map exploration (navigate + loot) |
|
||||
| KulemakExecutor | Boss fight with arena mechanics |
|
||||
| CraftingExecutor | Crafting bench operations |
|
||||
| DiamondExecutor | Diamond trade handling |
|
||||
| ScrapExecutor | Vendor scrapping |
|
||||
| TradeQueue | FIFO queue of trade tasks |
|
||||
| LinkManager | Trade search management |
|
||||
|
||||
**Bot modes**: Trading, Mapping, Crafting (via BotMode enum)
|
||||
|
||||
---
|
||||
|
||||
## Nexus.Ui — Avalonia Desktop Application
|
||||
|
||||
Entry point executable. Avalonia 11.2 + CommunityToolkit.MVVM + FluentTheme.
|
||||
|
||||
**App.xaml.cs** wires all DI:
|
||||
- Services: ConfigStore, GameController, ScreenReader, ClientLogWatcher, TradeMonitor, InventoryManager
|
||||
- Bot: FramePipelineService, LinkManager, TradeExecutor, TradeQueue, BotOrchestrator, ModPoolService
|
||||
- ViewModels: Main, Debug, Settings, Mapping, Atlas, Crafting, Memory, Nexus, ObjectBrowser
|
||||
|
||||
**Additional dependencies**: Vortice.Direct2D1 (overlay rendering), Microsoft.Extensions.DependencyInjection
|
||||
|
||||
**Views**: MainWindow, DebugWindow, SettingsWindow, MappingWindow, etc. with MVVM bindings.
|
||||
67
docs/input.md
Normal file
67
docs/input.md
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# Nexus.Input — Input Controllers & Humanization
|
||||
|
||||
## IInputController Implementations
|
||||
|
||||
### SendInputController (Default, No Driver)
|
||||
|
||||
Uses Win32 `SendInput` API with **KEYEVENTF_SCANCODE** flag. Games read hardware scan codes, not virtual key codes.
|
||||
|
||||
- **KeyDown/KeyUp**: Raw keyboard scan code via SendInput struct
|
||||
- **KeyPress**: Down → Sleep(holdMs) → Up with humanization
|
||||
- **SmoothMoveTo**: Cubic Bézier curve interpolation (10-40 steps) with random perpendicular spread
|
||||
- **MouseMoveTo**: Direct `SetCursorPos()` (instant teleport)
|
||||
- **Clicks**: Smooth move to target → humanized delay → click
|
||||
|
||||
### InterceptionInputController (Driver-Based)
|
||||
|
||||
Uses Interception keyboard/mouse driver for lower-level control:
|
||||
- Delegates to `KeyboardHook` and `MouseHook` via InputInterceptor COM library
|
||||
- Same smooth movement and humanization as SendInput
|
||||
- Returns false from `Initialize()` if driver not installed (graceful fallback)
|
||||
|
||||
### SimInputController (Simulator)
|
||||
|
||||
Implements `IInputController` but doesn't make Win32 calls. Instead:
|
||||
- **WASD** → Tracks held state, converts to direction vector with 45° isometric rotation
|
||||
- **Skills** → Queues skill casts to SimWorld via `QueueSkill()`
|
||||
- **Mouse** → Tracks screen position, converts to world coords via inverse camera matrix
|
||||
- **Visualization** → Maintains flash timers (0.15s) for InputOverlayRenderer
|
||||
|
||||
## Scan Codes
|
||||
|
||||
```
|
||||
Movement: W=0x11 A=0x1E S=0x1F D=0x20
|
||||
Skills: Q=0x10 E=0x12 R=0x13 T=0x14
|
||||
Numbers: 1=0x02 2=0x03 3=0x04 4=0x05 5=0x06
|
||||
Modifiers: LShift=0x2A LCtrl=0x1D LAlt=0x38
|
||||
Other: Space=0x39 Enter=0x1C Escape=0x01 Slash=0x35
|
||||
```
|
||||
|
||||
## Humanizer
|
||||
|
||||
Anti-detection layer applied to all input operations.
|
||||
|
||||
| Method | Purpose |
|
||||
|--------|---------|
|
||||
| GaussianDelay(baseMs) | Adds gaussian noise (Box-Muller transform), clamped to [50ms, 150ms] |
|
||||
| JitterPosition(x, y) | Random pixel offset within ClickJitterRadius (3px) |
|
||||
| ShouldThrottle() | Tracks actions in 60-second rolling window, blocks if APM > MaxApm (250) |
|
||||
| RecordAction() | Enqueues timestamp for APM tracking |
|
||||
| RandomizedInterval(baseMs) | Adds ±20% jitter to poll intervals |
|
||||
|
||||
## MovementKeyTracker
|
||||
|
||||
Converts normalized direction vectors to WASD key state for isometric camera:
|
||||
|
||||
```
|
||||
Rotate direction 45°:
|
||||
sx = dir.X * cos(45°) - dir.Y * sin(45°)
|
||||
sy = dir.X * sin(45°) + dir.Y * cos(45°)
|
||||
|
||||
Key mapping:
|
||||
W if sy > 0.3, S if sy < -0.3
|
||||
D if sx > 0.3, A if sx < -0.3
|
||||
|
||||
Delta-based: only sends KeyDown/KeyUp when state changes.
|
||||
Supports holding multiple keys (W+D for diagonal).
|
||||
```
|
||||
153
docs/pathfinding.md
Normal file
153
docs/pathfinding.md
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
# Nexus.Pathfinding — Navigation & Exploration
|
||||
|
||||
## Overview
|
||||
|
||||
Two classes: **NavigationController** (state machine — decides *where* to go) and **PathFinder** (A* algorithm — decides *how* to get there).
|
||||
|
||||
---
|
||||
|
||||
## NavigationController
|
||||
|
||||
### Modes
|
||||
|
||||
| Mode | Trigger | Behavior |
|
||||
|------|---------|----------|
|
||||
| Idle | `Stop()` | No movement |
|
||||
| NavigatingToPosition | `NavigateTo(pos)` | Path to fixed world coordinates |
|
||||
| NavigatingToEntity | `NavigateToEntity(id)` | Chase a moving entity (re-targets each tick) |
|
||||
| Exploring | `Explore()` | BFS frontier exploration of unmapped terrain |
|
||||
|
||||
### Update Loop (called every tick)
|
||||
|
||||
```
|
||||
1. Area change detection → clear path, explored grid, stuck history
|
||||
2. EnsureExploredGrid() → allocate/resize to match terrain (preserves old data on expansion)
|
||||
3. MarkExplored(playerPos) → mark cells near player as visited (radius 150 grid cells)
|
||||
4. ResolveGoal() → get target position based on mode
|
||||
5. If no goal and Exploring → PickExploreTarget() via BFS
|
||||
6. Reach detection → within WaypointReachedDistance (80u), clear goal or stop
|
||||
7. Stuck detection → if < 30u movement in 60 ticks (~1s), repath or pick new target
|
||||
8. Pathfinding → A* from player to goal (with explored grid bias in explore mode)
|
||||
9. Waypoint advancement → advance index as player reaches each waypoint
|
||||
10. Output → DesiredDirection (normalized vector to next waypoint)
|
||||
```
|
||||
|
||||
### Explored Grid
|
||||
|
||||
Parallel bool array matching terrain dimensions. Tracks which cells the player has visited.
|
||||
|
||||
- **Mark radius**: 150 grid cells (~1630 world units) — circular region around player
|
||||
- **Preservation**: On terrain expansion, overlapping explored data is copied to new grid
|
||||
- **Offset-aware**: Uses same OffsetX/OffsetY as terrain for absolute grid coordinates
|
||||
|
||||
### BFS Exploration (PickExploreTarget)
|
||||
|
||||
When Exploring mode needs a new goal:
|
||||
|
||||
1. **BFS frontier search** (up to 100,000 iterations)
|
||||
- 8-directional BFS outward from player
|
||||
- Finds nearest unexplored walkable cell
|
||||
- Returns that cell as world coordinates
|
||||
|
||||
2. **Random distant target** (if BFS finds nothing)
|
||||
- 20 attempts at random directions, 1500-3500 world units away
|
||||
- Pushes player toward terrain edges where expansion triggers
|
||||
|
||||
3. **Edge fallback** (if random fails)
|
||||
- Heads toward nearest terrain boundary (10 cells from edge)
|
||||
- Guarantees continued exploration with infinite terrain
|
||||
|
||||
4. **Exploration complete** (only if all fallbacks fail)
|
||||
- Sets `IsExplorationComplete = true`
|
||||
- Prevents expensive re-BFS every tick
|
||||
- Reset on area change
|
||||
|
||||
### Stuck Detection
|
||||
|
||||
- **Window**: Last 60 positions (~1 second at 60Hz)
|
||||
- **Threshold**: Must move at least 30 world units in window
|
||||
- **Grace period**: 120 ticks (2 seconds) after picking new explore target
|
||||
- **On stuck while exploring**: Mark failed goal as explored, pick new target, set grace period
|
||||
- **On stuck otherwise**: Repath
|
||||
|
||||
### Path Failure Handling
|
||||
|
||||
- **Explored bias fallback**: If A* with explored grid bias fails, retry without bias (bias can make distant targets unreachable)
|
||||
- **Cooldown**: 3 seconds before retrying after path failure (prevents CPU burn on impossible paths)
|
||||
|
||||
---
|
||||
|
||||
## PathFinder — A* Implementation
|
||||
|
||||
### Signature
|
||||
|
||||
```csharp
|
||||
public static List<Vector2>? FindPath(
|
||||
WalkabilitySnapshot terrain, Vector2 start, Vector2 goal, float worldToGrid,
|
||||
bool[]? exploredGrid, int exploredWidth, int exploredHeight,
|
||||
int exploredOffsetX, int exploredOffsetY)
|
||||
```
|
||||
|
||||
Returns world-coordinate waypoints or null if unreachable.
|
||||
|
||||
### Movement Model
|
||||
|
||||
- **8-directional grid**: Cardinal + diagonal
|
||||
- **Costs**: Cardinal = 1.0, Diagonal = √2 ≈ 1.414
|
||||
- **Explored penalty**: ×1.5 multiplier for explored cells (biases paths through unexplored territory)
|
||||
|
||||
### Heuristic
|
||||
|
||||
```
|
||||
h = max(dx, dy) + 0.414 * min(dx, dy)
|
||||
```
|
||||
Diagonal/Chebyshev-based. Admissible and consistent.
|
||||
|
||||
### Algorithm
|
||||
|
||||
1. **Snap to walkable**: If start/goal in wall, BFS search for nearest walkable cell (radius up to 20)
|
||||
2. **A* search** (budget: 200,000 iterations):
|
||||
- Priority queue ordered by f = g + h
|
||||
- 8 neighbors per expansion
|
||||
- **Corner-cut check**: Diagonals require at least one adjacent cardinal cell walkable
|
||||
- **Explored grid bias**: Multiply step cost by 1.5 for explored cells
|
||||
- Track `bestNode` (closest reachable) for fallback
|
||||
3. **Path reconstruction**: Backtrack via cameFrom map
|
||||
4. **Simplification**: Remove collinear waypoints, keep only turning points
|
||||
5. **Fallback**: If goal unreachable but bestNode is meaningfully closer (within 80% of starting heuristic), path to closest reachable cell
|
||||
|
||||
### Data Structures
|
||||
|
||||
| Structure | Type | Purpose |
|
||||
|-----------|------|---------|
|
||||
| Open set | PriorityQueue<(int,int), float> | Nodes to expand, ordered by f-score |
|
||||
| Closed set | HashSet<(int,int)> | Already expanded nodes |
|
||||
| gScore | Dictionary<(int,int), float> | Best known cost to each node |
|
||||
| cameFrom | Dictionary<(int,int), (int,int)> | Backtracking map |
|
||||
|
||||
---
|
||||
|
||||
## Integration
|
||||
|
||||
```
|
||||
AreaProgressionSystem
|
||||
│ .Explore() / .NavigateTo() / .NavigateToEntity()
|
||||
▼
|
||||
NavigationController
|
||||
│ .Update(GameState) → computes path, sets DesiredDirection
|
||||
│ calls PathFinder.FindPath() for A* routing
|
||||
▼
|
||||
NavigationSystem
|
||||
│ reads DesiredDirection → submits MoveAction
|
||||
▼
|
||||
ActionQueue → BotEngine → MovementKeyTracker → WASD keys
|
||||
```
|
||||
|
||||
### Coordinate Systems
|
||||
|
||||
| Space | Example | Conversion |
|
||||
|-------|---------|------------|
|
||||
| World | (1517, 4491) | Raw game units |
|
||||
| Grid | (139, 413) | world × WorldToGrid (23/250) |
|
||||
| Local grid | (139-ox, 413-oy) | grid - terrain offset |
|
||||
| Screen | project via CameraMatrix | WorldToScreen.Project() |
|
||||
180
docs/simulator.md
Normal file
180
docs/simulator.md
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
# Nexus.Simulator — Standalone Game World
|
||||
|
||||
## Purpose
|
||||
|
||||
Test bot systems (combat, navigation, threat assessment) without the real game. Replaces the memory-reading pipeline with a procedural game world. Bot systems run unmodified — they see identical `GameState` objects and emit actions to `IInputController`.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
SimWorld (game tick loop)
|
||||
│
|
||||
├── SimPoller (60Hz background thread)
|
||||
│ ├── FlushToWorld() → transfer input to SimWorld
|
||||
│ ├── Tick(dt) → advance simulation
|
||||
│ ├── SimStateBuilder.Build() → SimWorld → GameState
|
||||
│ └── Push to GameDataCache
|
||||
│
|
||||
├── SimInputController (captures bot actions)
|
||||
│ ├── WASD → MoveDirection vector (45° isometric conversion)
|
||||
│ ├── Skills → QueueSkill(scanCode, targetWorldPos)
|
||||
│ ├── Mouse → track position, screen↔world conversion
|
||||
│ └── Flash timers for input visualization
|
||||
│
|
||||
├── Bot Logic Thread (60Hz)
|
||||
│ ├── GameStateEnricher.Enrich(state)
|
||||
│ ├── All 6 systems: Threat, Movement, Navigation, Combat, Resource, Loot
|
||||
│ ├── NavigationController.Update()
|
||||
│ └── ExecuteActions() → SimInputController
|
||||
│
|
||||
└── Render Thread (ImGui + Veldrid)
|
||||
├── TerrainRenderer (diamond cells, isometric)
|
||||
├── EntityRenderer (player, enemies, health bars)
|
||||
├── EffectRenderer (melee cones, AOE circles, projectile lines)
|
||||
├── PathRenderer (A* waypoints)
|
||||
├── InputOverlayRenderer (keyboard + mouse state)
|
||||
└── DebugPanel (system toggles, stats, spawn controls)
|
||||
```
|
||||
|
||||
## SimWorld — Game Loop
|
||||
|
||||
### Tick (dt-based, 60Hz)
|
||||
|
||||
```
|
||||
1. CheckAndExpandTerrain() → expand when player within 50 cells of edge
|
||||
2. MovePlayer(dt) → WASD direction × speed × dt, collision with terrain
|
||||
3. ProcessSkills() → dequeue skill casts, dispatch by scan code
|
||||
4. UpdateProjectiles(dt) → move, check terrain/enemy collisions
|
||||
5. UpdateEffects(dt) → decay visual effects (0.3s duration)
|
||||
6. UpdateEnemies(dt) → AI state machine per enemy
|
||||
7. UpdateRespawns(dt) → cull far enemies, spawn new groups
|
||||
```
|
||||
|
||||
### Terrain
|
||||
|
||||
- Procedural: all walkable with scattered obstacles (rock clusters, wall segments, pillars)
|
||||
- 500×500 initial grid, `WorldToGrid = 23/250`
|
||||
- **Infinite expansion**: Expands 250 cells per side when player within 50 cells of edge
|
||||
- Preserves existing data via array copy with offset adjustment
|
||||
|
||||
### Player
|
||||
|
||||
- Position (Vector2), Health/Mana with regen (5 HP/s, 10 MP/s)
|
||||
- Move speed: 400 world units/s
|
||||
- Collision: slide-along-X / slide-along-Y fallback if direct move blocked
|
||||
|
||||
### Skills
|
||||
|
||||
| Scan Code | Type | Behavior |
|
||||
|-----------|------|----------|
|
||||
| Q (0x10), R (0x13) | AOE | Damage all enemies within 250u of target position |
|
||||
| E (0x12), T (0x14) | Projectile | Spawn projectile, 1200 speed, 800 range, 80u hit radius |
|
||||
| LMB, RMB | Melee | 150u cone, 120° angle from player toward target |
|
||||
|
||||
Base damage: 200 per hit. Configurable via SimConfig.
|
||||
|
||||
### Enemy AI
|
||||
|
||||
```
|
||||
State machine per SimEnemy:
|
||||
|
||||
Idle → wander randomly within 200u of spawn, new target every 2-5s
|
||||
│ player within 600u (aggro range)
|
||||
▼
|
||||
Chasing → move toward player at 75% player speed
|
||||
│ player within 100u (attack range)
|
||||
▼
|
||||
Attacking → stand still, deal 30 damage every 1.5s
|
||||
│ player escapes attack range
|
||||
▼ back to Chasing
|
||||
│ health ≤ 0
|
||||
▼
|
||||
Dead → visible for 2s → queue for respawn (5s delay)
|
||||
```
|
||||
|
||||
### Enemy Spawning
|
||||
|
||||
- **Groups**: 3-7 enemies per spawn, leader keeps rolled rarity, rest are Normal
|
||||
- **Rarity distribution**: 70% Normal, 20% Magic, 8% Rare, 2% Unique
|
||||
- **HP multipliers**: Magic=1.5×, Rare=3×, Unique=5× base (200)
|
||||
- **Spawn ring**: 800-2000 world units from player
|
||||
- **Direction bias**: ±90° cone ahead of player's movement direction
|
||||
- **Culling**: Remove enemies > 3000u from player
|
||||
- **Population**: Maintain 25 enemies, spawn new groups as needed
|
||||
|
||||
## Bridge Layer
|
||||
|
||||
### SimPoller
|
||||
|
||||
Replaces MemoryPoller. Background thread at 60Hz:
|
||||
1. `FlushToWorld()` — transfer accumulated input
|
||||
2. `world.Tick(dt)` — advance simulation (dt clamped to 0.1s max)
|
||||
3. `SimStateBuilder.Build()` — convert to GameState
|
||||
4. Push to GameDataCache (same fields as production)
|
||||
|
||||
### SimStateBuilder
|
||||
|
||||
Converts SimWorld state → GameState:
|
||||
- Each SimEnemy → EntitySnapshot (with rarity, threat level, AI state, HP)
|
||||
- SimPlayer → PlayerState (position, vitals, skills)
|
||||
- Camera matrix: orthographic projection (12800×7200 world units → 2560×1440 screen)
|
||||
|
||||
### SimInputController
|
||||
|
||||
Implements IInputController, captures actions instead of sending Win32 input:
|
||||
- WASD → direction vector (with 45° isometric inversion)
|
||||
- Skills → `SimWorld.QueueSkill(scanCode, worldPos)`
|
||||
- Mouse → screen position tracking, inverse camera transform for world coords
|
||||
- Input visualization: flash timers for keyboard/mouse overlay
|
||||
|
||||
## Rendering
|
||||
|
||||
### ViewTransform (Isometric Camera)
|
||||
|
||||
45° counter-clockwise rotation matching the game's camera:
|
||||
|
||||
```
|
||||
World → Grid: gx = worldX × WorldToGrid
|
||||
Grid → Screen: rx = (gx - gy) × cos(45°)
|
||||
ry = -(gx + gy) × cos(45°)
|
||||
Screen = canvasOrigin + viewOffset + (rx, ry) × zoom
|
||||
```
|
||||
|
||||
### Renderers
|
||||
|
||||
| Renderer | Draws |
|
||||
|----------|-------|
|
||||
| TerrainRenderer | Diamond cells (rotated grid), explored overlay, minimap |
|
||||
| EntityRenderer | Player (green circle), enemies (colored by rarity), health/mana bars |
|
||||
| EffectRenderer | Melee cones (red triangle fan), AOE circles (blue), projectile lines (cyan) |
|
||||
| PathRenderer | Cyan waypoint lines and dots from A* path |
|
||||
| InputOverlayRenderer | Keyboard (3 rows: 1-5, QWERT, ASDF) + mouse (L/R/M buttons) |
|
||||
| DebugPanel | Pause/speed, player stats, enemy counts, system toggles, threat info |
|
||||
|
||||
### VeldridImGuiRenderer
|
||||
|
||||
Custom ImGui backend for Veldrid 4.9.0 + D3D11:
|
||||
- HLSL shaders compiled at runtime via D3DCompiler P/Invoke
|
||||
- Dynamic vertex/index buffers, font texture from ImGui atlas
|
||||
- Alpha blending pipeline with scissor rect support
|
||||
|
||||
## SimConfig
|
||||
|
||||
```
|
||||
Terrain: 500×500, WorldToGrid=23/250, ExpandThreshold=50, ExpandAmount=250
|
||||
Player: Speed=400, HP=1000, MP=500, HPRegen=5/s, MPRegen=10/s
|
||||
Enemies: Count=25, Aggro=600u, Attack=100u, Speed=75%, HP=200, Damage=30
|
||||
Spawning: Ring=800-2000u, Groups=3-7, Cull=3000u
|
||||
Skills: Melee=150u/120°, AOE=250u, Projectile=1200speed/800range, Damage=200
|
||||
Rarity: Normal=70%, Magic=20%, Rare=8%, Unique=2%
|
||||
Simulation: SpeedMultiplier=1×, Pauseable
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
```
|
||||
dotnet run --project src/Nexus.Simulator
|
||||
```
|
||||
|
||||
Dependencies: Core, Data, Systems, Pathfinding, ImGui.NET, Veldrid, Veldrid.StartupUtilities
|
||||
Does NOT depend on: Memory, Input, Screen, Game, Bot, Ui, Trade
|
||||
95
docs/test.html
Normal file
95
docs/test.html
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Hold Timer</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #111;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: 24px;
|
||||
padding: 30px 60px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
#result {
|
||||
margin-top: 24px;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
#live {
|
||||
margin-top: 12px;
|
||||
font-size: 20px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<button id="btn">PRESS AND HOLD</button>
|
||||
<div id="result">Last hold: 0 ms</div>
|
||||
<div id="live">Current hold: 0 ms</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const btn = document.getElementById("btn");
|
||||
const result = document.getElementById("result");
|
||||
const live = document.getElementById("live");
|
||||
|
||||
let startTime = 0;
|
||||
let holding = false;
|
||||
let rafId = 0;
|
||||
|
||||
function updateLive() {
|
||||
if (!holding) return;
|
||||
const now = performance.now();
|
||||
live.textContent = "Current hold: " + (now - startTime).toFixed(2) + " ms";
|
||||
rafId = requestAnimationFrame(updateLive);
|
||||
}
|
||||
|
||||
function startHold() {
|
||||
if (holding) return;
|
||||
holding = true;
|
||||
startTime = performance.now();
|
||||
updateLive();
|
||||
}
|
||||
|
||||
function endHold() {
|
||||
if (!holding) return;
|
||||
holding = false;
|
||||
cancelAnimationFrame(rafId);
|
||||
const duration = performance.now() - startTime;
|
||||
result.textContent = "Last hold: " + duration.toFixed(2) + " ms";
|
||||
live.textContent = "Current hold: 0 ms";
|
||||
}
|
||||
|
||||
btn.addEventListener("mousedown", startHold);
|
||||
btn.addEventListener("mouseup", endHold);
|
||||
btn.addEventListener("mouseleave", endHold);
|
||||
|
||||
btn.addEventListener("touchstart", (e) => {
|
||||
e.preventDefault();
|
||||
startHold();
|
||||
}, { passive: false });
|
||||
|
||||
btn.addEventListener("touchend", endHold);
|
||||
btn.addEventListener("touchcancel", endHold);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
15
imgui.ini
Normal file
15
imgui.ini
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[Window][Debug##Default]
|
||||
Pos=60,60
|
||||
Size=400,400
|
||||
Collapsed=0
|
||||
|
||||
[Window][Simulator Controls]
|
||||
Pos=29,51
|
||||
Size=432,649
|
||||
Collapsed=0
|
||||
|
||||
[Window][Simulator]
|
||||
Pos=564,96
|
||||
Size=1023,810
|
||||
Collapsed=0
|
||||
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Automata.Core\Automata.Core.csproj" />
|
||||
<ProjectReference Include="..\Automata.Game\Automata.Game.csproj" />
|
||||
<ProjectReference Include="..\Automata.Screen\Automata.Screen.csproj" />
|
||||
<ProjectReference Include="..\Automata.Trade\Automata.Trade.csproj" />
|
||||
<ProjectReference Include="..\Automata.Log\Automata.Log.csproj" />
|
||||
<ProjectReference Include="..\Automata.Inventory\Automata.Inventory.csproj" />
|
||||
<ProjectReference Include="..\Automata.Navigation\Automata.Navigation.csproj" />
|
||||
<ProjectReference Include="..\Automata.Items\Automata.Items.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Automata.Core\Automata.Core.csproj" />
|
||||
<ProjectReference Include="..\Automata.Game\Automata.Game.csproj" />
|
||||
<ProjectReference Include="..\Automata.Screen\Automata.Screen.csproj" />
|
||||
<ProjectReference Include="..\Automata.Log\Automata.Log.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
using Automata.Core;
|
||||
using Automata.Game;
|
||||
using Automata.Inventory;
|
||||
using Automata.Navigation;
|
||||
using Automata.Screen;
|
||||
using Nexus.Core;
|
||||
using Nexus.Game;
|
||||
using Nexus.Inventory;
|
||||
using Nexus.Navigation;
|
||||
using Nexus.Screen;
|
||||
using Serilog;
|
||||
|
||||
namespace Automata.Bot;
|
||||
namespace Nexus.Bot;
|
||||
|
||||
/// <summary>
|
||||
/// Captures the full endgame atlas as a panorama image.
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
using Automata.Core;
|
||||
using Automata.Game;
|
||||
using Automata.Inventory;
|
||||
using Automata.GameLog;
|
||||
using Automata.Navigation;
|
||||
using Automata.Screen;
|
||||
using Automata.Trade;
|
||||
using Nexus.Core;
|
||||
using Nexus.Game;
|
||||
using Nexus.Inventory;
|
||||
using Nexus.GameLog;
|
||||
using Nexus.Navigation;
|
||||
using Nexus.Screen;
|
||||
using Nexus.Trade;
|
||||
using Serilog;
|
||||
|
||||
namespace Automata.Bot;
|
||||
namespace Nexus.Bot;
|
||||
|
||||
public class BotStatus
|
||||
{
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
using System.Diagnostics;
|
||||
using Automata.Core;
|
||||
using Automata.Game;
|
||||
using Automata.Screen;
|
||||
using Nexus.Core;
|
||||
using Nexus.Game;
|
||||
using Nexus.Screen;
|
||||
using Serilog;
|
||||
|
||||
namespace Automata.Bot;
|
||||
namespace Nexus.Bot;
|
||||
|
||||
/// <summary>
|
||||
/// Manages the attack state machine (click → hold) with mana monitoring and flask usage.
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
using Automata.Core;
|
||||
using Automata.Game;
|
||||
using Automata.Items;
|
||||
using Nexus.Core;
|
||||
using Nexus.Game;
|
||||
using Nexus.Items;
|
||||
using Serilog;
|
||||
|
||||
namespace Automata.Bot;
|
||||
namespace Nexus.Bot;
|
||||
|
||||
public class CraftingExecutor
|
||||
{
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
using System.Collections.Concurrent;
|
||||
using Automata.Core;
|
||||
using Automata.Game;
|
||||
using Automata.Inventory;
|
||||
using Automata.Screen;
|
||||
using Automata.Trade;
|
||||
using Nexus.Core;
|
||||
using Nexus.Game;
|
||||
using Nexus.Inventory;
|
||||
using Nexus.Screen;
|
||||
using Nexus.Trade;
|
||||
using Serilog;
|
||||
|
||||
namespace Automata.Bot;
|
||||
namespace Nexus.Bot;
|
||||
|
||||
public class DiamondExecutor
|
||||
{
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
using System.Diagnostics;
|
||||
using Automata.Game;
|
||||
using Automata.Screen;
|
||||
using Nexus.Game;
|
||||
using Nexus.Screen;
|
||||
using Serilog;
|
||||
|
||||
namespace Automata.Bot;
|
||||
namespace Nexus.Bot;
|
||||
|
||||
/// <summary>
|
||||
/// Monitors life/mana and presses flask keys when they drop below thresholds.
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
using System.Diagnostics;
|
||||
using Automata.Core;
|
||||
using Automata.Game;
|
||||
using Automata.Inventory;
|
||||
using Automata.Screen;
|
||||
using Nexus.Core;
|
||||
using Nexus.Game;
|
||||
using Nexus.Inventory;
|
||||
using Nexus.Screen;
|
||||
using Serilog;
|
||||
|
||||
namespace Automata.Bot;
|
||||
namespace Nexus.Bot;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for game executors that interact with the game world.
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
using Automata.Core;
|
||||
using Automata.Game;
|
||||
using Automata.GameLog;
|
||||
using Automata.Inventory;
|
||||
using Automata.Navigation;
|
||||
using Automata.Screen;
|
||||
using Nexus.Core;
|
||||
using Nexus.Game;
|
||||
using Nexus.GameLog;
|
||||
using Nexus.Inventory;
|
||||
using Nexus.Navigation;
|
||||
using Nexus.Screen;
|
||||
using Serilog;
|
||||
|
||||
namespace Automata.Bot;
|
||||
namespace Nexus.Bot;
|
||||
|
||||
/// <summary>
|
||||
/// Kulemak-specific boss run executor: scripted 4-phase + ring fight,
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
using System.Diagnostics;
|
||||
using Automata.Core;
|
||||
using Automata.Game;
|
||||
using Automata.GameLog;
|
||||
using Automata.Inventory;
|
||||
using Automata.Navigation;
|
||||
using Automata.Screen;
|
||||
using Nexus.Core;
|
||||
using Nexus.Game;
|
||||
using Nexus.GameLog;
|
||||
using Nexus.Inventory;
|
||||
using Nexus.Navigation;
|
||||
using Nexus.Screen;
|
||||
using Serilog;
|
||||
|
||||
namespace Automata.Bot;
|
||||
namespace Nexus.Bot;
|
||||
|
||||
/// <summary>
|
||||
/// Shared infrastructure for any map/boss activity: combat loop, WASD navigation,
|
||||
17
src/Nexus.Bot/Nexus.Bot.csproj
Normal file
17
src/Nexus.Bot/Nexus.Bot.csproj
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Nexus.Core\Nexus.Core.csproj" />
|
||||
<ProjectReference Include="..\Nexus.Game\Nexus.Game.csproj" />
|
||||
<ProjectReference Include="..\Nexus.Screen\Nexus.Screen.csproj" />
|
||||
<ProjectReference Include="..\Nexus.Trade\Nexus.Trade.csproj" />
|
||||
<ProjectReference Include="..\Nexus.Log\Nexus.Log.csproj" />
|
||||
<ProjectReference Include="..\Nexus.Inventory\Nexus.Inventory.csproj" />
|
||||
<ProjectReference Include="..\Nexus.Navigation\Nexus.Navigation.csproj" />
|
||||
<ProjectReference Include="..\Nexus.Items\Nexus.Items.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
using Automata.Core;
|
||||
using Automata.Game;
|
||||
using Automata.Inventory;
|
||||
using Automata.Screen;
|
||||
using Automata.Trade;
|
||||
using Nexus.Core;
|
||||
using Nexus.Game;
|
||||
using Nexus.Inventory;
|
||||
using Nexus.Screen;
|
||||
using Nexus.Trade;
|
||||
using Serilog;
|
||||
|
||||
namespace Automata.Bot;
|
||||
namespace Nexus.Bot;
|
||||
|
||||
public class ScrapExecutor
|
||||
{
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
using Automata.Core;
|
||||
using Automata.Game;
|
||||
using Automata.Inventory;
|
||||
using Automata.Screen;
|
||||
using Automata.Trade;
|
||||
using Nexus.Core;
|
||||
using Nexus.Game;
|
||||
using Nexus.Inventory;
|
||||
using Nexus.Screen;
|
||||
using Nexus.Trade;
|
||||
using Serilog;
|
||||
|
||||
namespace Automata.Bot;
|
||||
namespace Nexus.Bot;
|
||||
|
||||
public class TradeExecutor
|
||||
{
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
using Automata.Core;
|
||||
using Nexus.Core;
|
||||
using Serilog;
|
||||
|
||||
namespace Automata.Bot;
|
||||
namespace Nexus.Bot;
|
||||
|
||||
public class TradeQueue
|
||||
{
|
||||
89
src/Nexus.Core/ActionExecutor.cs
Normal file
89
src/Nexus.Core/ActionExecutor.cs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Nexus.Core;
|
||||
|
||||
public static class ActionExecutor
|
||||
{
|
||||
// Screen center (half of 2560x1440)
|
||||
private const float ScreenCenterX = 1280f;
|
||||
private const float ScreenCenterY = 720f;
|
||||
|
||||
// How far ahead of the player (in screen pixels) the idle cursor sits
|
||||
private const float IdleCursorDistance = 200f;
|
||||
|
||||
public static void Execute(List<BotAction> resolved, IInputController input,
|
||||
MovementKeyTracker moveTracker, MovementBlender blender, Vector2? playerPos = null,
|
||||
Matrix4x4? camera = null)
|
||||
{
|
||||
if (!input.IsInitialized) return;
|
||||
|
||||
var hasCast = false;
|
||||
|
||||
// Filter out physically impossible key combos (same finger)
|
||||
resolved = HandModel.Filter(resolved,
|
||||
moveTracker.IsWHeld, moveTracker.IsAHeld,
|
||||
moveTracker.IsSHeld, moveTracker.IsDHeld);
|
||||
|
||||
// Discrete actions
|
||||
foreach (var action in resolved)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case FlaskAction flask:
|
||||
input.KeyPress(flask.FlaskScanCode);
|
||||
break;
|
||||
|
||||
case CastAction cast:
|
||||
hasCast = true;
|
||||
if (cast.TargetScreenPos.HasValue)
|
||||
input.SmoothMoveTo((int)cast.TargetScreenPos.Value.X, (int)cast.TargetScreenPos.Value.Y);
|
||||
input.KeyPress(cast.SkillScanCode);
|
||||
break;
|
||||
|
||||
case ClickAction click:
|
||||
var cx = (int)click.ScreenPosition.X;
|
||||
var cy = (int)click.ScreenPosition.Y;
|
||||
switch (click.Type)
|
||||
{
|
||||
case ClickType.Left: input.LeftClick(cx, cy); break;
|
||||
case ClickType.Right: input.RightClick(cx, cy); break;
|
||||
case ClickType.Middle: input.MiddleClick(cx, cy); break;
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyAction key:
|
||||
switch (key.Type)
|
||||
{
|
||||
case KeyActionType.Press: input.KeyPress(key.ScanCode); break;
|
||||
case KeyActionType.Down: input.KeyDown(key.ScanCode); break;
|
||||
case KeyActionType.Up: input.KeyUp(key.ScanCode); break;
|
||||
}
|
||||
break;
|
||||
|
||||
case DodgeRollAction dodge:
|
||||
input.SetDodgeDirection(dodge.Direction);
|
||||
input.KeyPress(0x39); // Space bar
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Idle mouse tracking: when not casting, keep cursor ahead of player in movement direction.
|
||||
// This prevents jarring jumps from target to target and gives smooth cursor flow.
|
||||
if (!hasCast && blender.Direction is { } moveDir && camera.HasValue && playerPos.HasValue)
|
||||
{
|
||||
// Project a point slightly ahead of the player in the movement direction
|
||||
var aheadWorld = playerPos.Value + moveDir * 300f;
|
||||
var screenAhead = WorldToScreen.Project(aheadWorld, 0f, camera.Value);
|
||||
if (screenAhead.HasValue)
|
||||
{
|
||||
// Clamp to reasonable screen bounds
|
||||
var sx = Math.Clamp(screenAhead.Value.X, 100f, 2460f);
|
||||
var sy = Math.Clamp(screenAhead.Value.Y, 100f, 1340f);
|
||||
input.SmoothMoveTo((int)sx, (int)sy);
|
||||
}
|
||||
}
|
||||
|
||||
// WASD movement (delta-based held keys)
|
||||
moveTracker.Apply(input, blender.Direction, playerPos);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Roboto.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public class ActionQueue
|
||||
{
|
||||
|
|
@ -33,12 +33,11 @@ public class ActionQueue
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve conflicts and return the final action list:
|
||||
/// Resolve conflicts and return the final action list.
|
||||
/// Movement is handled by MovementBlender — only non-movement actions remain here.
|
||||
/// 1. FlaskActions always pass through
|
||||
/// 2. Get highest-priority MoveAction + CastAction
|
||||
/// 3. Urgent move (priority ≤ 10) → include move, BLOCK cast (flee)
|
||||
/// 4. Normal → include both cast + move
|
||||
/// 5. All other actions pass through
|
||||
/// 2. Get highest-priority CastAction
|
||||
/// 3. All other actions pass through
|
||||
/// </summary>
|
||||
public List<BotAction> Resolve()
|
||||
{
|
||||
|
|
@ -51,21 +50,9 @@ public class ActionQueue
|
|||
resolved.Add(action);
|
||||
}
|
||||
|
||||
var bestMove = GetHighestPriority<MoveAction>();
|
||||
var bestCast = GetHighestPriority<CastAction>();
|
||||
|
||||
if (bestMove is not null)
|
||||
{
|
||||
resolved.Add(bestMove);
|
||||
|
||||
// Urgent flee (priority ≤ 10) blocks casting
|
||||
if (bestMove.Priority > 10 && bestCast is not null)
|
||||
resolved.Add(bestCast);
|
||||
}
|
||||
else if (bestCast is not null)
|
||||
{
|
||||
if (bestCast is not null)
|
||||
resolved.Add(bestCast);
|
||||
}
|
||||
|
||||
// Pass through everything else (Key, Click, Chat, Wait) except types already handled
|
||||
foreach (var action in _actions)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Roboto.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public enum ClickType { Left, Right, Middle }
|
||||
public enum KeyActionType { Press, Down, Up }
|
||||
|
|
@ -25,3 +25,5 @@ public record FlaskAction(int Priority, ushort FlaskScanCode) : BotAction(Priori
|
|||
public record ChatAction(int Priority, string Message) : BotAction(Priority);
|
||||
|
||||
public record WaitAction(int Priority, int DurationMs) : BotAction(Priority);
|
||||
|
||||
public record DodgeRollAction(int Priority, Vector2 Direction) : BotAction(Priority);
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Roboto.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public class BotConfig
|
||||
{
|
||||
|
|
@ -7,13 +7,16 @@ public class BotConfig
|
|||
public int MemoryPollRateHz { get; set; } = 30;
|
||||
|
||||
// Movement
|
||||
public float SafeDistance { get; set; } = 400f;
|
||||
public float RepulsionWeight { get; set; } = 1.5f;
|
||||
public float SafeDistance { get; set; } = 500f;
|
||||
public float RepulsionWeight { get; set; } = 0.5f;
|
||||
public float WaypointReachedDistance { get; set; } = 80f;
|
||||
|
||||
// Navigation
|
||||
public float WorldToGrid { get; set; } = 23f / 250f;
|
||||
|
||||
// Combat engagement — suppress navigation when enemies are within this range
|
||||
public float CombatEngagementRange { get; set; } = 600f;
|
||||
|
||||
// Loot
|
||||
public float LootPickupRange { get; set; } = 600f;
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Roboto.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public record Buff
|
||||
{
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Roboto.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public class CharacterProfile
|
||||
{
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Roboto.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public class CombatSettings
|
||||
{
|
||||
|
|
@ -2,7 +2,7 @@ using System.Text.Json;
|
|||
using System.Text.Json.Serialization;
|
||||
using Serilog;
|
||||
|
||||
namespace Automata.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public class SavedLink
|
||||
{
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Automata.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public static class Delays
|
||||
{
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Roboto.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public enum EntityCategory
|
||||
{
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Roboto.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public enum DangerLevel
|
||||
{
|
||||
|
|
@ -12,6 +12,7 @@ public enum DangerLevel
|
|||
public static class SystemPriority
|
||||
{
|
||||
public const int Threat = 50;
|
||||
public const int Dodge = 75;
|
||||
public const int Movement = 100;
|
||||
public const int Navigation = 200;
|
||||
public const int Combat = 300;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Roboto.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public class FlaskSettings
|
||||
{
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Roboto.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public record FlaskState
|
||||
{
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Roboto.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public class GameState
|
||||
{
|
||||
|
|
@ -27,8 +27,11 @@ public class GameState
|
|||
/// <summary>In-progress quests from the quest linked list with target areas and paths.</summary>
|
||||
public IReadOnlyList<QuestInfo> Quests { get; set; } = [];
|
||||
|
||||
// Derived (computed once per tick by GameStateEnricher)
|
||||
public IReadOnlyList<ProjectileSnapshot> EnemyProjectiles { get; set; } = [];
|
||||
|
||||
// Derived (computed once per tick by GameStateEnricher / ThreatSystem)
|
||||
public ThreatMap Threats { get; set; } = new();
|
||||
public ThreatAssessment ThreatAssessment { get; set; } = new();
|
||||
public IReadOnlyList<EntitySnapshot> NearestEnemies { get; set; } = [];
|
||||
public IReadOnlyList<GroundEffect> GroundEffects { get; set; } = [];
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Roboto.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public record GroundEffect
|
||||
{
|
||||
110
src/Nexus.Core/HandModel.cs
Normal file
110
src/Nexus.Core/HandModel.cs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
namespace Nexus.Core;
|
||||
|
||||
public enum Finger { Pinky, Ring, Middle, Index, Thumb }
|
||||
|
||||
/// <summary>
|
||||
/// Maps left-hand keys to physical fingers and filters out actions that would
|
||||
/// require the same finger simultaneously. Dropped actions retry next tick (16ms).
|
||||
/// Right hand (mouse) is unconstrained.
|
||||
/// </summary>
|
||||
public static class HandModel
|
||||
{
|
||||
private static readonly Dictionary<ushort, Finger> FingerMap = new()
|
||||
{
|
||||
// Pinky: 1, Q, A
|
||||
[ScanCodes.Key1] = Finger.Pinky,
|
||||
[ScanCodes.Q] = Finger.Pinky,
|
||||
[ScanCodes.A] = Finger.Pinky,
|
||||
|
||||
// Ring: 2, W, S
|
||||
[ScanCodes.Key2] = Finger.Ring,
|
||||
[ScanCodes.W] = Finger.Ring,
|
||||
[ScanCodes.S] = Finger.Ring,
|
||||
|
||||
// Middle: 3, E, D
|
||||
[ScanCodes.Key3] = Finger.Middle,
|
||||
[ScanCodes.E] = Finger.Middle,
|
||||
[ScanCodes.D] = Finger.Middle,
|
||||
|
||||
// Index: 4, 5, R, T, F
|
||||
[ScanCodes.Key4] = Finger.Index,
|
||||
[ScanCodes.Key5] = Finger.Index,
|
||||
[ScanCodes.R] = Finger.Index,
|
||||
[ScanCodes.T] = Finger.Index,
|
||||
[ScanCodes.F] = Finger.Index,
|
||||
|
||||
// Thumb: Space, LAlt
|
||||
[ScanCodes.Space] = Finger.Thumb,
|
||||
[ScanCodes.LAlt] = Finger.Thumb,
|
||||
};
|
||||
|
||||
// Lower = higher priority when two actions compete for the same finger
|
||||
private static int ActionTypePriority(BotAction a) => a switch
|
||||
{
|
||||
DodgeRollAction => 0,
|
||||
FlaskAction => 1,
|
||||
CastAction => 2,
|
||||
KeyAction => 3,
|
||||
_ => 4,
|
||||
};
|
||||
|
||||
public static List<BotAction> Filter(List<BotAction> resolved,
|
||||
bool wHeld, bool aHeld, bool sHeld, bool dHeld)
|
||||
{
|
||||
// Build occupied set from currently held WASD keys
|
||||
var occupied = new HashSet<Finger>();
|
||||
if (wHeld) occupied.Add(Finger.Ring);
|
||||
if (aHeld) occupied.Add(Finger.Pinky);
|
||||
if (sHeld) occupied.Add(Finger.Ring);
|
||||
if (dHeld) occupied.Add(Finger.Middle);
|
||||
|
||||
// Sort by action type priority (dodge > flask > cast > key)
|
||||
resolved.Sort((a, b) => ActionTypePriority(a).CompareTo(ActionTypePriority(b)));
|
||||
|
||||
var result = new List<BotAction>(resolved.Count);
|
||||
|
||||
foreach (var action in resolved)
|
||||
{
|
||||
var scanCode = GetScanCode(action);
|
||||
|
||||
// No scan code (ClickAction, ChatAction, WaitAction, MoveAction) → always pass
|
||||
if (scanCode is null)
|
||||
{
|
||||
result.Add(action);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Key releases always pass — they free a finger
|
||||
if (action is KeyAction { Type: KeyActionType.Up })
|
||||
{
|
||||
result.Add(action);
|
||||
continue;
|
||||
}
|
||||
|
||||
// No finger mapping for this scan code → pass (right-hand or unmapped key)
|
||||
if (!FingerMap.TryGetValue(scanCode.Value, out var finger))
|
||||
{
|
||||
result.Add(action);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Finger free → accept and mark occupied
|
||||
if (occupied.Add(finger))
|
||||
{
|
||||
result.Add(action);
|
||||
}
|
||||
// else: finger already occupied → drop, will retry next tick
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ushort? GetScanCode(BotAction action) => action switch
|
||||
{
|
||||
DodgeRollAction => ScanCodes.Space,
|
||||
FlaskAction f => f.FlaskScanCode,
|
||||
CastAction c => c.SkillScanCode,
|
||||
KeyAction k => k.ScanCode,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Automata.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public static class Helpers
|
||||
{
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
namespace Roboto.Core;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Nexus.Core;
|
||||
|
||||
public interface IInputController
|
||||
{
|
||||
|
|
@ -16,4 +18,10 @@ public interface IInputController
|
|||
void LeftUp();
|
||||
void RightDown();
|
||||
void RightUp();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the direction for the next dodge roll. Called before KeyPress(0x21).
|
||||
/// Default no-op for real input controllers (direction comes from game state).
|
||||
/// </summary>
|
||||
void SetDodgeDirection(Vector2 direction) { }
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
namespace Roboto.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public interface ISystem
|
||||
{
|
||||
int Priority { get; }
|
||||
string Name { get; }
|
||||
bool IsEnabled { get; set; }
|
||||
void Update(GameState state, ActionQueue actions);
|
||||
void Update(GameState state, ActionQueue actions, MovementBlender movement);
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using Serilog;
|
||||
|
||||
namespace Automata.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public class TradeLink
|
||||
{
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Automata.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public static class Logging
|
||||
{
|
||||
public static void Setup()
|
||||
{
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Information()
|
||||
.MinimumLevel.Debug()
|
||||
.WriteTo.Console(
|
||||
outputTemplate: "[{Timestamp:HH:mm:ss.fff} {Level:u3}] {Message:lj}{NewLine}{Exception}")
|
||||
.WriteTo.File("logs/poe2trade-.log",
|
||||
|
|
@ -2,7 +2,7 @@ using System.Net.Http;
|
|||
using System.Text.Json;
|
||||
using Serilog;
|
||||
|
||||
namespace Automata.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public class ModPoolService
|
||||
{
|
||||
241
src/Nexus.Core/MovementBlender.cs
Normal file
241
src/Nexus.Core/MovementBlender.cs
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Nexus.Core;
|
||||
|
||||
public readonly record struct MovementIntent(
|
||||
int Layer,
|
||||
Vector2 Direction,
|
||||
float Override = 0f,
|
||||
string? Source = null
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Blends movement contributions from multiple systems using priority-layered attenuation.
|
||||
/// Higher-priority layers (lower number) attenuate lower-priority ones via their Override factor.
|
||||
/// Applies terrain validation once on the blended result. WASD hysteresis handles smoothing.
|
||||
/// </summary>
|
||||
public sealed class MovementBlender
|
||||
{
|
||||
private readonly List<MovementIntent> _intents = new();
|
||||
|
||||
// Stuck detection
|
||||
private Vector2 _lastResolvePos;
|
||||
private int _stuckFrames;
|
||||
private const int StuckFrameThreshold = 15; // ~250ms at 60Hz
|
||||
private const int StuckRecoveryThreshold = 45; // ~750ms — try random direction to break free
|
||||
private const float StuckMovePerFrame = 3f; // must move > 3 world units per frame to count as moving
|
||||
private static readonly Random StuckRng = new();
|
||||
|
||||
// EMA smoothing to dampen terrain validation jitter.
|
||||
// Snap decision based on INTENT change (pre-terrain), not terrain output — prevents
|
||||
// terrain probe noise from bypassing the EMA via the snap threshold.
|
||||
private Vector2? _smoothedDirection;
|
||||
private const float SmoothingAlpha = 0.20f; // 20% new, 80% previous
|
||||
|
||||
// Terrain validation cache — prevents re-probing within a small radius,
|
||||
// breaking the position↔direction feedback loop that causes zigzag oscillation
|
||||
private Vector2 _cachedTerrainInputDir;
|
||||
private Vector2 _cachedTerrainResult;
|
||||
private Vector2 _cachedTerrainPos;
|
||||
private const float TerrainCacheRadius = 20f; // don't re-probe within 20 world units
|
||||
|
||||
public Vector2? Direction { get; private set; }
|
||||
public Vector2? RawDirection { get; private set; }
|
||||
|
||||
/// <summary>True when layer 0 (critical flee) was submitted — blocks casting.</summary>
|
||||
public bool IsUrgentFlee { get; private set; }
|
||||
|
||||
/// <summary>True when the player hasn't moved for several frames — orbit/herd suppressed.</summary>
|
||||
public bool IsStuck { get; private set; }
|
||||
|
||||
/// <summary>Snapshot of intents from the last Resolve() call, for diagnostic logging.</summary>
|
||||
public IReadOnlyList<MovementIntent> LastIntents => _lastIntents;
|
||||
private List<MovementIntent> _lastIntents = new();
|
||||
|
||||
public void Submit(MovementIntent intent) => _intents.Add(intent);
|
||||
|
||||
/// <summary>
|
||||
/// Clears intents for a new frame. Called at the top of each logic tick.
|
||||
/// </summary>
|
||||
public void Clear() => _intents.Clear();
|
||||
|
||||
/// <summary>
|
||||
/// Updates stuck detection based on player movement. Call BEFORE systems run
|
||||
/// so that IsStuck is available for systems to check (e.g. MovementSystem suppresses orbit).
|
||||
/// </summary>
|
||||
public void UpdateStuckState(Vector2 playerPos)
|
||||
{
|
||||
var moved = Vector2.Distance(playerPos, _lastResolvePos);
|
||||
if (moved < StuckMovePerFrame)
|
||||
_stuckFrames++;
|
||||
else
|
||||
_stuckFrames = Math.Max(0, _stuckFrames - 3); // recover 3x faster than building up
|
||||
_lastResolvePos = playerPos;
|
||||
IsStuck = _stuckFrames > StuckFrameThreshold;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Blends all submitted intents and validates against terrain.
|
||||
/// Applies EMA smoothing after terrain validation to dampen probe jitter.
|
||||
/// </summary>
|
||||
public void Resolve(WalkabilitySnapshot? terrain, Vector2 playerPos, float worldToGrid)
|
||||
{
|
||||
IsUrgentFlee = false;
|
||||
|
||||
if (IsStuck)
|
||||
{
|
||||
// Drop orbit (L2) and herd (L4) — they don't help when stuck
|
||||
_intents.RemoveAll(i => i.Layer == 2 || i.Layer == 4);
|
||||
|
||||
// Drop flee (L0, L1) too — if we're stuck, flee is pointing into a wall.
|
||||
// Let wall push and navigation guide us out instead.
|
||||
_intents.RemoveAll(i => i.Layer <= 1);
|
||||
|
||||
// After 750ms stuck, inject a random nudge at high priority to break free
|
||||
if (_stuckFrames > StuckRecoveryThreshold)
|
||||
{
|
||||
var angle = StuckRng.NextDouble() * Math.PI * 2;
|
||||
var nudge = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle));
|
||||
_intents.Add(new MovementIntent(0, nudge, 0.6f, "StuckEscape"));
|
||||
// Reset counter so we try a new direction periodically
|
||||
if (_stuckFrames % 30 == 0)
|
||||
_stuckFrames = StuckRecoveryThreshold + 1;
|
||||
}
|
||||
}
|
||||
|
||||
_lastIntents = new List<MovementIntent>(_intents);
|
||||
|
||||
if (_intents.Count == 0)
|
||||
{
|
||||
RawDirection = null;
|
||||
Direction = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for urgent flee (layer 0)
|
||||
foreach (var intent in _intents)
|
||||
{
|
||||
if (intent.Layer == 0)
|
||||
{
|
||||
IsUrgentFlee = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Group by layer, sum within layer, track max override per layer
|
||||
var layers = new SortedDictionary<int, (Vector2 Sum, float MaxOverride)>();
|
||||
foreach (var intent in _intents)
|
||||
{
|
||||
if (layers.TryGetValue(intent.Layer, out var existing))
|
||||
layers[intent.Layer] = (existing.Sum + intent.Direction, Math.Max(existing.MaxOverride, intent.Override));
|
||||
else
|
||||
layers[intent.Layer] = (intent.Direction, intent.Override);
|
||||
}
|
||||
|
||||
// Blend across layers with priority-based attenuation
|
||||
var attenuation = 1f;
|
||||
var result = Vector2.Zero;
|
||||
|
||||
foreach (var (_, (sum, maxOverride)) in layers)
|
||||
{
|
||||
result += sum * attenuation;
|
||||
attenuation *= (1f - maxOverride);
|
||||
}
|
||||
|
||||
if (result.LengthSquared() < 0.0001f)
|
||||
{
|
||||
RawDirection = null;
|
||||
Direction = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Normalize the blended result
|
||||
var rawDir = Vector2.Normalize(result);
|
||||
|
||||
// Terrain validation with grid-cell caching.
|
||||
// Re-probe only when the raw direction changes (>~14°) or the player enters a new grid cell.
|
||||
// This prevents the feedback loop: direction jitter → zigzag movement → crosses cell boundary → more jitter.
|
||||
if (terrain is not null)
|
||||
{
|
||||
var dirSimilar = Vector2.Dot(rawDir, _cachedTerrainInputDir) > 0.97f;
|
||||
var nearbyPos = Vector2.DistanceSquared(playerPos, _cachedTerrainPos) < TerrainCacheRadius * TerrainCacheRadius;
|
||||
|
||||
if (dirSimilar && nearbyPos)
|
||||
{
|
||||
rawDir = _cachedTerrainResult;
|
||||
}
|
||||
else
|
||||
{
|
||||
var preTerrainDir = rawDir;
|
||||
rawDir = TerrainQuery.FindWalkableDirection(terrain, playerPos, rawDir, worldToGrid);
|
||||
_cachedTerrainInputDir = preTerrainDir;
|
||||
_cachedTerrainResult = rawDir;
|
||||
_cachedTerrainPos = playerPos;
|
||||
}
|
||||
}
|
||||
|
||||
RawDirection = rawDir;
|
||||
|
||||
// EMA smoothing. Only snap (bypass smoothing) on urgent flee (L0),
|
||||
// which needs instant response. All other direction changes (orbit flips,
|
||||
// terrain jitter, waypoint changes) get smoothed to prevent oscillation.
|
||||
if (_smoothedDirection.HasValue)
|
||||
{
|
||||
if (IsUrgentFlee)
|
||||
{
|
||||
// Emergency flee — snap immediately, no smoothing
|
||||
}
|
||||
else
|
||||
{
|
||||
var smoothed = Vector2.Lerp(_smoothedDirection.Value, rawDir, SmoothingAlpha);
|
||||
if (smoothed.LengthSquared() > 0.0001f)
|
||||
rawDir = Vector2.Normalize(smoothed);
|
||||
}
|
||||
}
|
||||
|
||||
_smoothedDirection = rawDir;
|
||||
Direction = rawDir;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Full reset — call on area change or loading screen.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
_intents.Clear();
|
||||
_lastIntents.Clear();
|
||||
Direction = null;
|
||||
RawDirection = null;
|
||||
IsUrgentFlee = false;
|
||||
IsStuck = false;
|
||||
_stuckFrames = 0;
|
||||
_lastResolvePos = Vector2.Zero;
|
||||
_smoothedDirection = null;
|
||||
_cachedTerrainPos = new Vector2(float.MinValue, float.MinValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compact diagnostic string: lists active intents and final direction.
|
||||
/// Example: "Orbit(L2,0.0) Navigation(L3,0.0) Herd(L4,0.2) → (0.71,-0.31)"
|
||||
/// </summary>
|
||||
public string DiagnosticSummary()
|
||||
{
|
||||
if (_lastIntents.Count == 0)
|
||||
return "none";
|
||||
|
||||
var parts = new List<string>();
|
||||
foreach (var intent in _lastIntents)
|
||||
{
|
||||
var dir = intent.Direction;
|
||||
var mag = dir.Length();
|
||||
parts.Add($"{intent.Source ?? "?"}(L{intent.Layer},ovr={intent.Override:F1},mag={mag:F2})");
|
||||
}
|
||||
|
||||
var dirStr = Direction.HasValue
|
||||
? $"({Direction.Value.X:F2},{Direction.Value.Y:F2})"
|
||||
: "null";
|
||||
|
||||
var stuckStr = IsStuck ? " [STUCK]" : "";
|
||||
return string.Join(" + ", parts) + " → " + dirStr + stuckStr;
|
||||
}
|
||||
}
|
||||
150
src/Nexus.Core/MovementKeyTracker.cs
Normal file
150
src/Nexus.Core/MovementKeyTracker.cs
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
using System.Numerics;
|
||||
using Serilog;
|
||||
|
||||
namespace Nexus.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Translates a movement direction vector into WASD key presses.
|
||||
/// Applies 45° rotation to account for isometric camera (W+A = one world axis).
|
||||
/// Tracks which keys are currently held and only sends changes (delta).
|
||||
/// Enforces a minimum hold duration (55±10ms gaussian) on every key press.
|
||||
/// </summary>
|
||||
public sealed class MovementKeyTracker
|
||||
{
|
||||
private bool _wHeld, _aHeld, _sHeld, _dHeld;
|
||||
|
||||
public bool IsWHeld => _wHeld;
|
||||
public bool IsAHeld => _aHeld;
|
||||
public bool IsSHeld => _sHeld;
|
||||
public bool IsDHeld => _dHeld;
|
||||
private long _wDownAt, _aDownAt, _sDownAt, _dDownAt;
|
||||
private int _wMinHold, _aMinHold, _sMinHold, _dMinHold;
|
||||
private long _wUpAt, _aUpAt, _sUpAt, _dUpAt;
|
||||
private int _wRepress, _aRepress, _sRepress, _dRepress;
|
||||
private Vector2? _lastPlayerPos;
|
||||
|
||||
private static readonly Random Rng = new();
|
||||
|
||||
// 45° rotation constants
|
||||
private const float Cos45 = 0.70710678f;
|
||||
private const float Sin45 = 0.70710678f;
|
||||
|
||||
// Hysteresis: higher threshold to press, lower to release — prevents oscillation
|
||||
private const float PressThreshold = 0.35f;
|
||||
private const float ReleaseThreshold = 0.15f;
|
||||
|
||||
/// <summary>
|
||||
/// Apply a movement direction. Null or zero direction releases all keys.
|
||||
/// Direction is in world space; we rotate 45° for the isometric camera before mapping to WASD.
|
||||
/// Uses hysteresis to prevent key oscillation.
|
||||
/// </summary>
|
||||
public void Apply(IInputController input, Vector2? direction, Vector2? playerPos = null)
|
||||
{
|
||||
_lastPlayerPos = playerPos;
|
||||
|
||||
bool wantW, wantA, wantS, wantD;
|
||||
|
||||
if (direction is { } dir && dir.LengthSquared() > 0.001f)
|
||||
{
|
||||
// Rotate 45° for isometric camera alignment
|
||||
var sx = dir.X * Cos45 - dir.Y * Sin45;
|
||||
var sy = dir.X * Sin45 + dir.Y * Cos45;
|
||||
|
||||
// Hysteresis: different thresholds for press vs release
|
||||
wantW = _wHeld ? sy > ReleaseThreshold : sy > PressThreshold;
|
||||
wantS = _sHeld ? sy < -ReleaseThreshold : sy < -PressThreshold;
|
||||
wantD = _dHeld ? sx > ReleaseThreshold : sx > PressThreshold;
|
||||
wantA = _aHeld ? sx < -ReleaseThreshold : sx < -PressThreshold;
|
||||
}
|
||||
else
|
||||
{
|
||||
wantW = wantA = wantS = wantD = false;
|
||||
}
|
||||
|
||||
var now = Environment.TickCount64;
|
||||
SetKey(input, ScanCodes.W, ref _wHeld, ref _wDownAt, ref _wMinHold, ref _wUpAt, ref _wRepress, wantW, now, _lastPlayerPos);
|
||||
SetKey(input, ScanCodes.A, ref _aHeld, ref _aDownAt, ref _aMinHold, ref _aUpAt, ref _aRepress, wantA, now, _lastPlayerPos);
|
||||
SetKey(input, ScanCodes.S, ref _sHeld, ref _sDownAt, ref _sMinHold, ref _sUpAt, ref _sRepress, wantS, now, _lastPlayerPos);
|
||||
SetKey(input, ScanCodes.D, ref _dHeld, ref _dDownAt, ref _dMinHold, ref _dUpAt, ref _dRepress, wantD, now, _lastPlayerPos);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Release all movement keys immediately (bypasses min hold — for shutdown/area change).
|
||||
/// </summary>
|
||||
public void ReleaseAll(IInputController input)
|
||||
{
|
||||
if (_wHeld) { input.KeyUp(ScanCodes.W); _wHeld = false; }
|
||||
if (_aHeld) { input.KeyUp(ScanCodes.A); _aHeld = false; }
|
||||
if (_sHeld) { input.KeyUp(ScanCodes.S); _sHeld = false; }
|
||||
if (_dHeld) { input.KeyUp(ScanCodes.D); _dHeld = false; }
|
||||
}
|
||||
|
||||
private static string KeyName(ushort scanCode) => scanCode switch
|
||||
{
|
||||
0x11 => "W", 0x1E => "A", 0x1F => "S", 0x20 => "D", _ => $"0x{scanCode:X2}"
|
||||
};
|
||||
|
||||
private static void SetKey(IInputController input, ushort scanCode,
|
||||
ref bool held, ref long downAt, ref int minHold,
|
||||
ref long upAt, ref int repressDelay, bool want, long now, Vector2? pos)
|
||||
{
|
||||
if (want && !held)
|
||||
{
|
||||
// Enforce re-press cooldown after release
|
||||
if (now - upAt < repressDelay) return;
|
||||
|
||||
input.KeyDown(scanCode);
|
||||
held = true;
|
||||
downAt = now;
|
||||
minHold = HoldMs();
|
||||
if (pos.HasValue)
|
||||
Log.Information("[WASD] {Key} DOWN (minHold={MinHold}ms) pos=({X:F0},{Y:F0})",
|
||||
KeyName(scanCode), minHold, pos.Value.X, pos.Value.Y);
|
||||
else
|
||||
Log.Information("[WASD] {Key} DOWN (minHold={MinHold}ms)", KeyName(scanCode), minHold);
|
||||
}
|
||||
else if (!want && held)
|
||||
{
|
||||
var elapsed = now - downAt;
|
||||
if (elapsed < minHold) return; // enforce minimum hold
|
||||
|
||||
input.KeyUp(scanCode);
|
||||
held = false;
|
||||
upAt = now;
|
||||
repressDelay = RepressMs();
|
||||
if (pos.HasValue)
|
||||
Log.Information("[WASD] {Key} UP (held={Elapsed}ms, min={MinHold}ms) pos=({X:F0},{Y:F0})",
|
||||
KeyName(scanCode), elapsed, minHold, pos.Value.X, pos.Value.Y);
|
||||
else
|
||||
Log.Information("[WASD] {Key} UP (held={Elapsed}ms, min={MinHold}ms)", KeyName(scanCode), elapsed, minHold);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gaussian hold duration peaked at 55ms, range [44, 76].</summary>
|
||||
private static int HoldMs()
|
||||
{
|
||||
double u, v, s;
|
||||
do
|
||||
{
|
||||
u = Rng.NextDouble() * 2.0 - 1.0;
|
||||
v = Rng.NextDouble() * 2.0 - 1.0;
|
||||
s = u * u + v * v;
|
||||
} while (s >= 1.0 || s == 0.0);
|
||||
var g = u * Math.Sqrt(-2.0 * Math.Log(s) / s);
|
||||
return Math.Clamp((int)Math.Round(55.0 + g * 6.0), 44, 76);
|
||||
}
|
||||
|
||||
/// <summary>Gaussian re-press cooldown peaked at 40ms, range [25, 65].</summary>
|
||||
private static int RepressMs()
|
||||
{
|
||||
double u, v, s;
|
||||
do
|
||||
{
|
||||
u = Rng.NextDouble() * 2.0 - 1.0;
|
||||
v = Rng.NextDouble() * 2.0 - 1.0;
|
||||
s = u * u + v * v;
|
||||
} while (s >= 1.0 || s == 0.0);
|
||||
var g = u * Math.Sqrt(-2.0 * Math.Log(s) / s);
|
||||
return Math.Clamp((int)Math.Round(40.0 + g * 8.0), 25, 65);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,6 @@
|
|||
<PackageReference Include="Serilog" Version="4.2.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Roboto.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public record PlayerState
|
||||
{
|
||||
|
|
@ -31,4 +31,8 @@ public record PlayerState
|
|||
|
||||
// Skill slots (populated by memory when available)
|
||||
public IReadOnlyList<SkillState> Skills { get; init; } = [];
|
||||
|
||||
// Dodge roll state
|
||||
public bool IsRolling { get; init; }
|
||||
public float RollCooldownRemaining { get; init; }
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
using System.Text.Json;
|
||||
using Serilog;
|
||||
|
||||
namespace Automata.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public static class Poe2ScoutClient
|
||||
{
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Roboto.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public class ProfileConfig
|
||||
{
|
||||
16
src/Nexus.Core/ProjectileSnapshot.cs
Normal file
16
src/Nexus.Core/ProjectileSnapshot.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Nexus.Core;
|
||||
|
||||
public record ProjectileSnapshot
|
||||
{
|
||||
public Vector2 Position { get; init; }
|
||||
public Vector2 Direction { get; init; }
|
||||
public float Speed { get; init; }
|
||||
public float HitRadius { get; init; }
|
||||
public float DistanceToPlayer { get; init; }
|
||||
/// <summary>Seconds until impact. Null if projectile will miss.</summary>
|
||||
public float? TimeToImpact { get; init; }
|
||||
/// <summary>Closest distance the projectile's trajectory passes to the player center.</summary>
|
||||
public float ClosestApproachDistance { get; init; }
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Roboto.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public class QuestInfo
|
||||
{
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Roboto.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public record QuestProgress
|
||||
{
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Automata.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public class RePoEMod
|
||||
{
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
namespace Roboto.Input;
|
||||
namespace Nexus.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Hardware scan codes for keyboard input via Interception driver.
|
||||
/// Hardware scan codes for keyboard input.
|
||||
/// </summary>
|
||||
public static class ScanCodes
|
||||
{
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Roboto.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public enum SkillInputType { KeyPress, LeftClick, RightClick, MiddleClick }
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Roboto.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public record SkillState
|
||||
{
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Automata.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public class StashTabInfo
|
||||
{
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Roboto.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public enum TargetSelection
|
||||
{
|
||||
188
src/Nexus.Core/TerrainQuery.cs
Normal file
188
src/Nexus.Core/TerrainQuery.cs
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Nexus.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Terrain line-of-sight and walkable direction queries on the walkability grid.
|
||||
/// </summary>
|
||||
public static class TerrainQuery
|
||||
{
|
||||
/// <summary>
|
||||
/// Bresenham line walk on the walkability grid. Returns false if any cell is unwalkable.
|
||||
/// </summary>
|
||||
public static bool HasLineOfSight(WalkabilitySnapshot terrain, Vector2 from, Vector2 to, float worldToGrid)
|
||||
{
|
||||
int x0 = (int)(from.X * worldToGrid);
|
||||
int y0 = (int)(from.Y * worldToGrid);
|
||||
int x1 = (int)(to.X * worldToGrid);
|
||||
int y1 = (int)(to.Y * worldToGrid);
|
||||
|
||||
int dx = Math.Abs(x1 - x0);
|
||||
int dy = Math.Abs(y1 - y0);
|
||||
int sx = x0 < x1 ? 1 : -1;
|
||||
int sy = y0 < y1 ? 1 : -1;
|
||||
int err = dx - dy;
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (!terrain.IsWalkable(x0, y0))
|
||||
return false;
|
||||
|
||||
if (x0 == x1 && y0 == y1)
|
||||
break;
|
||||
|
||||
int e2 = 2 * err;
|
||||
if (e2 > -dy) { err -= dy; x0 += sx; }
|
||||
if (e2 < dx) { err += dx; y0 += sy; }
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a desired movement direction against terrain. If blocked, tries rotations
|
||||
/// ±45°, ±90°, ±135°, 180° and returns the first clear direction.
|
||||
/// Returns original direction as fallback (game engine will wall-slide).
|
||||
/// </summary>
|
||||
public static Vector2 FindWalkableDirection(
|
||||
WalkabilitySnapshot terrain, Vector2 playerPos, Vector2 desiredDir, float worldToGrid,
|
||||
float probeDistance = 60f)
|
||||
{
|
||||
if (IsDirectionClear(terrain, playerPos, desiredDir, worldToGrid, probeDistance))
|
||||
return desiredDir;
|
||||
|
||||
// Try rotations: ±45°, ±90°, ±135°, 180°
|
||||
ReadOnlySpan<float> angles = [45f, -45f, 90f, -90f, 135f, -135f, 180f];
|
||||
|
||||
foreach (var angleDeg in angles)
|
||||
{
|
||||
var rotated = Rotate(desiredDir, angleDeg);
|
||||
if (IsDirectionClear(terrain, playerPos, rotated, worldToGrid, probeDistance))
|
||||
return rotated;
|
||||
}
|
||||
|
||||
return desiredDir;
|
||||
}
|
||||
|
||||
private static bool IsDirectionClear(
|
||||
WalkabilitySnapshot terrain, Vector2 origin, Vector2 dir, float worldToGrid, float distance)
|
||||
{
|
||||
// Check near (2-3 grid cells), mid, and far probes
|
||||
var nearpoint = origin + dir * 30f;
|
||||
var midpoint = origin + dir * (distance * 0.5f);
|
||||
var endpoint = origin + dir * distance;
|
||||
|
||||
int nx = (int)(nearpoint.X * worldToGrid);
|
||||
int ny = (int)(nearpoint.Y * worldToGrid);
|
||||
int mx = (int)(midpoint.X * worldToGrid);
|
||||
int my = (int)(midpoint.Y * worldToGrid);
|
||||
int ex = (int)(endpoint.X * worldToGrid);
|
||||
int ey = (int)(endpoint.Y * worldToGrid);
|
||||
|
||||
return terrain.IsWalkable(nx, ny) && terrain.IsWalkable(mx, my) && terrain.IsWalkable(ex, ey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Probes 8 directions around the player for nearby walls.
|
||||
/// Returns a normalized push-away vector, or Zero if no walls are close.
|
||||
/// </summary>
|
||||
public static Vector2 ComputeWallRepulsion(WalkabilitySnapshot terrain, Vector2 playerPos, float worldToGrid)
|
||||
{
|
||||
const float probeNear = 40f; // ~3-4 grid cells
|
||||
const float probeFar = 100f; // ~9-10 grid cells
|
||||
|
||||
var push = Vector2.Zero;
|
||||
|
||||
for (var i = 0; i < 8; i++)
|
||||
{
|
||||
var angle = i * MathF.PI / 4f;
|
||||
var dir = new Vector2(MathF.Cos(angle), MathF.Sin(angle));
|
||||
|
||||
// Near probe — strong push
|
||||
var near = playerPos + dir * probeNear;
|
||||
var nx = (int)(near.X * worldToGrid);
|
||||
var ny = (int)(near.Y * worldToGrid);
|
||||
if (!terrain.IsWalkable(nx, ny))
|
||||
{
|
||||
push -= dir * 1.0f;
|
||||
continue; // don't double-count
|
||||
}
|
||||
|
||||
// Far probe — gentle push
|
||||
var far = playerPos + dir * probeFar;
|
||||
var fx = (int)(far.X * worldToGrid);
|
||||
var fy = (int)(far.Y * worldToGrid);
|
||||
if (!terrain.IsWalkable(fx, fy))
|
||||
push -= dir * 0.4f;
|
||||
}
|
||||
|
||||
if (push.LengthSquared() < 0.0001f)
|
||||
return Vector2.Zero;
|
||||
|
||||
return Vector2.Normalize(push);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Predictive wall steering — casts rays ahead along the movement direction.
|
||||
/// If forward is blocked but a side is clear, returns a lateral steering vector.
|
||||
/// </summary>
|
||||
public static Vector2 ComputeWallSteering(
|
||||
WalkabilitySnapshot terrain, Vector2 playerPos, Vector2 moveDir, float worldToGrid)
|
||||
{
|
||||
if (moveDir.LengthSquared() < 0.0001f)
|
||||
return Vector2.Zero;
|
||||
|
||||
var dir = Vector2.Normalize(moveDir);
|
||||
var leftDir = Rotate(dir, 30f);
|
||||
var rightDir = Rotate(dir, -30f);
|
||||
|
||||
ReadOnlySpan<float> distances = [40f, 80f, 120f];
|
||||
|
||||
var forwardBlocked = false;
|
||||
var leftClear = true;
|
||||
var rightClear = true;
|
||||
|
||||
foreach (var dist in distances)
|
||||
{
|
||||
var fwd = playerPos + dir * dist;
|
||||
var fx = (int)(fwd.X * worldToGrid);
|
||||
var fy = (int)(fwd.Y * worldToGrid);
|
||||
if (!terrain.IsWalkable(fx, fy))
|
||||
forwardBlocked = true;
|
||||
|
||||
var left = playerPos + leftDir * dist;
|
||||
var lx = (int)(left.X * worldToGrid);
|
||||
var ly = (int)(left.Y * worldToGrid);
|
||||
if (!terrain.IsWalkable(lx, ly))
|
||||
leftClear = false;
|
||||
|
||||
var right = playerPos + rightDir * dist;
|
||||
var rx = (int)(right.X * worldToGrid);
|
||||
var ry = (int)(right.Y * worldToGrid);
|
||||
if (!terrain.IsWalkable(rx, ry))
|
||||
rightClear = false;
|
||||
}
|
||||
|
||||
if (!forwardBlocked)
|
||||
return Vector2.Zero;
|
||||
|
||||
// Steer toward the clear side
|
||||
var lateral = new Vector2(-dir.Y, dir.X); // perpendicular (left)
|
||||
if (leftClear && !rightClear)
|
||||
return lateral;
|
||||
if (rightClear && !leftClear)
|
||||
return -lateral;
|
||||
if (leftClear && rightClear)
|
||||
return lateral; // default left when both clear
|
||||
// Both blocked — push backward
|
||||
return -dir;
|
||||
}
|
||||
|
||||
private static Vector2 Rotate(Vector2 v, float degrees)
|
||||
{
|
||||
float rad = degrees * MathF.PI / 180f;
|
||||
float cos = MathF.Cos(rad);
|
||||
float sin = MathF.Sin(rad);
|
||||
return Vector2.Normalize(new Vector2(v.X * cos - v.Y * sin, v.X * sin + v.Y * cos));
|
||||
}
|
||||
}
|
||||
51
src/Nexus.Core/ThreatAssessment.cs
Normal file
51
src/Nexus.Core/ThreatAssessment.cs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Nexus.Core;
|
||||
|
||||
public enum ThreatCategory
|
||||
{
|
||||
Ignore, // score ≤ 2 — not worth reacting to
|
||||
Monitor, // score 2–6 — track but don't change behavior
|
||||
Engage, // score 6–15 — fight, stay mobile
|
||||
Flee, // score 15–25 — kite aggressively
|
||||
Emergency, // score > 25 OR player HP critical — run, pop flasks
|
||||
}
|
||||
|
||||
public class ThreatEntry
|
||||
{
|
||||
public uint EntityId { get; init; }
|
||||
public Vector2 Position { get; set; }
|
||||
public float DistanceToPlayer { get; set; }
|
||||
public float ThreatScore { get; set; }
|
||||
public float PerceivedDanger { get; set; } // normalized 0..1
|
||||
public ThreatCategory Category { get; set; }
|
||||
public bool HasLineOfSight { get; set; }
|
||||
public MonsterRarity Rarity { get; init; }
|
||||
public float HpPercent { get; set; }
|
||||
public bool IsAlive { get; set; }
|
||||
}
|
||||
|
||||
public class ThreatAssessment
|
||||
{
|
||||
public List<ThreatEntry> Entries { get; set; } = [];
|
||||
|
||||
// Precomputed aggregates — consumed by steering, combat, state machine
|
||||
public float ZoneThreatLevel { get; set; } // sum of all scores
|
||||
public ThreatEntry? PrimaryTarget { get; set; } // best kill target
|
||||
public ThreatEntry? MostDangerous { get; set; } // highest threat score
|
||||
public Vector2 ThreatCentroid { get; set; } // score-weighted center
|
||||
public Vector2 SafestDirection { get; set; } // away from centroid
|
||||
public bool AnyEmergency { get; set; }
|
||||
public bool ShouldFlee { get; set; } // zone threat > flee threshold
|
||||
public bool AreaClear { get; set; } // no Monitor+ threats remain
|
||||
public float ClosestDistance { get; set; }
|
||||
|
||||
/// <summary>Continuous 0..1 flee weight for steering blend.</summary>
|
||||
public float FleeWeight { get; set; }
|
||||
|
||||
// Convenience — backward compatibility
|
||||
public int CloseRange { get; set; } // < 300
|
||||
public int MidRange { get; set; } // 300–600
|
||||
public int FarRange { get; set; } // 600–1200
|
||||
public bool HasRareOrUnique { get; set; }
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Roboto.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public class ThreatMap
|
||||
{
|
||||
|
|
@ -8,7 +8,7 @@ public class ThreatMap
|
|||
public int CloseRange { get; init; } // < 300 units
|
||||
public int MidRange { get; init; } // 300–600
|
||||
public int FarRange { get; init; } // 600–1200
|
||||
public float ClosestDistance { get; init; } = float.MaxValue;
|
||||
public float ClosestDistance { get; init; }
|
||||
public Vector2 ThreatCentroid { get; init; }
|
||||
public bool HasRareOrUnique { get; init; }
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Automata.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public record Region(int X, int Y, int Width, int Height);
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Roboto.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Active quest info as displayed in the game UI.
|
||||
28
src/Nexus.Core/WalkabilitySnapshot.cs
Normal file
28
src/Nexus.Core/WalkabilitySnapshot.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
namespace Nexus.Core;
|
||||
|
||||
public record WalkabilitySnapshot
|
||||
{
|
||||
public int Width { get; init; }
|
||||
public int Height { get; init; }
|
||||
public byte[] Data { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Absolute grid X coordinate of the top-left corner of Data.
|
||||
/// Grid coord gx maps to local index gx - OffsetX.
|
||||
/// </summary>
|
||||
public int OffsetX { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Absolute grid Y coordinate of the top-left corner of Data.
|
||||
/// </summary>
|
||||
public int OffsetY { get; init; }
|
||||
|
||||
public bool IsWalkable(int gx, int gy)
|
||||
{
|
||||
var lx = gx - OffsetX;
|
||||
var ly = gy - OffsetY;
|
||||
if (lx < 0 || lx >= Width || ly < 0 || ly >= Height)
|
||||
return false;
|
||||
return Data[ly * Width + lx] != 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Roboto.Core;
|
||||
namespace Nexus.Core;
|
||||
|
||||
public static class WorldToScreen
|
||||
{
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Text.Json;
|
||||
|
||||
namespace Roboto.Data;
|
||||
namespace Nexus.Data;
|
||||
|
||||
public record AreaNode(
|
||||
string Id,
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Text.Json;
|
||||
|
||||
namespace Roboto.Data;
|
||||
namespace Nexus.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves area IDs (e.g. "G1_4") to display names (e.g. "The Grelwood")
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using Roboto.Core;
|
||||
using Nexus.Core;
|
||||
|
||||
namespace Roboto.Data;
|
||||
namespace Nexus.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Classifies entities from path + component data into EntityCategory.
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
using System.Numerics;
|
||||
using Roboto.Core;
|
||||
using MemEntity = Roboto.Memory.Entity;
|
||||
using Nexus.Core;
|
||||
using MemEntity = Nexus.Memory.Entity;
|
||||
|
||||
namespace Roboto.Data;
|
||||
namespace Nexus.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Maps raw Memory.Entity → Core.EntitySnapshot. Single source of truth for entity mapping.
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
using System.Numerics;
|
||||
using Roboto.Core;
|
||||
using Roboto.Memory;
|
||||
using Nexus.Core;
|
||||
using Nexus.Memory;
|
||||
|
||||
namespace Roboto.Data;
|
||||
namespace Nexus.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Immutable snapshot of player position for lock-free cross-thread reads.
|
||||
26
src/Nexus.Data/GameStateEnricher.cs
Normal file
26
src/Nexus.Data/GameStateEnricher.cs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
using System.Numerics;
|
||||
using Nexus.Core;
|
||||
|
||||
namespace Nexus.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Computes derived fields on GameState once per tick.
|
||||
/// Threat scoring is now handled by ThreatSystem (runs as ISystem).
|
||||
/// </summary>
|
||||
public static class GameStateEnricher
|
||||
{
|
||||
public static void Enrich(GameState state)
|
||||
{
|
||||
state.NearestEnemies = ComputeNearestEnemies(state.HostileMonsters);
|
||||
state.GroundEffects = []; // stub until memory reads ground effects
|
||||
}
|
||||
|
||||
private static IReadOnlyList<EntitySnapshot> ComputeNearestEnemies(IReadOnlyList<EntitySnapshot> hostiles)
|
||||
{
|
||||
if (hostiles.Count == 0) return [];
|
||||
|
||||
var sorted = new List<EntitySnapshot>(hostiles);
|
||||
sorted.Sort((a, b) => a.DistanceToPlayer.CompareTo(b.DistanceToPlayer));
|
||||
return sorted;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using Roboto.Memory;
|
||||
using Roboto.Core;
|
||||
using Nexus.Memory;
|
||||
using Nexus.Core;
|
||||
using Serilog;
|
||||
|
||||
namespace Roboto.Data;
|
||||
namespace Nexus.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Owns the memory read thread. Runs a two-tier loop:
|
||||
|
|
@ -28,7 +28,7 @@ public sealed class MemoryPoller : IDisposable
|
|||
private nint _playerLifeAddr;
|
||||
private nint _inGameStateAddr;
|
||||
private nint _controllerAddr;
|
||||
private Roboto.Memory.GameOffsets? _offsets;
|
||||
private Nexus.Memory.GameOffsets? _offsets;
|
||||
private ProcessMemory? _mem;
|
||||
|
||||
private int _hotHz;
|
||||
|
|
@ -64,7 +64,7 @@ public sealed class MemoryPoller : IDisposable
|
|||
|
||||
_thread = new Thread(PollLoop)
|
||||
{
|
||||
Name = "Roboto.MemoryPoller",
|
||||
Name = "Nexus.MemoryPoller",
|
||||
IsBackground = true,
|
||||
};
|
||||
_thread.Start();
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
<PackageReference Include="Serilog" Version="4.2.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Roboto.Core\Roboto.Core.csproj" />
|
||||
<ProjectReference Include="..\Nexus.Core\Nexus.Core.csproj" />
|
||||
<ProjectReference Include="..\Nexus.Memory\Nexus.Memory.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Automata.Game;
|
||||
namespace Nexus.Game;
|
||||
|
||||
/// <summary>
|
||||
/// Win32 clipboard access without WinForms dependency.
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
using Automata.Core;
|
||||
using Nexus.Core;
|
||||
using Serilog;
|
||||
|
||||
namespace Automata.Game;
|
||||
namespace Nexus.Game;
|
||||
|
||||
public class GameController : IGameController
|
||||
{
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Automata.Game;
|
||||
namespace Nexus.Game;
|
||||
|
||||
public interface IGameController
|
||||
{
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using Automata.Core;
|
||||
using Nexus.Core;
|
||||
|
||||
namespace Automata.Game;
|
||||
namespace Nexus.Game;
|
||||
|
||||
public class InputSender
|
||||
{
|
||||
|
|
@ -6,6 +6,6 @@
|
|||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Automata.Core\Automata.Core.csproj" />
|
||||
<ProjectReference Include="..\Nexus.Core\Nexus.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using Serilog;
|
||||
|
||||
namespace Automata.Game;
|
||||
namespace Nexus.Game;
|
||||
|
||||
public class WindowManager
|
||||
{
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Roboto.GameOffsets.Components;
|
||||
namespace Nexus.GameOffsets.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Actor component offsets — confirmed from ExileCore2.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Roboto.GameOffsets.Components;
|
||||
namespace Nexus.GameOffsets.Components;
|
||||
|
||||
/// <summary>A deployed entity (totem, mine, etc.).</summary>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Roboto.GameOffsets.Components;
|
||||
namespace Nexus.GameOffsets.Components;
|
||||
|
||||
/// <summary>
|
||||
/// An entry in the ActiveSkills vector: shared_ptr pair (0x10 bytes).
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using Roboto.GameOffsets.Natives;
|
||||
using Nexus.GameOffsets.Natives;
|
||||
|
||||
namespace Roboto.GameOffsets.Components;
|
||||
namespace Nexus.GameOffsets.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Cooldown state for a skill. Entries in Actor+0xB18 vector.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Roboto.GameOffsets.Components;
|
||||
namespace Nexus.GameOffsets.Components;
|
||||
|
||||
/// <summary>Vaal soul tracking.</summary>
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Roboto.GameOffsets.Components;
|
||||
namespace Nexus.GameOffsets.Components;
|
||||
|
||||
/// <summary>Animated component — reference to the animated entity.</summary>
|
||||
[StructLayout(LayoutKind.Explicit, Size = 0x300)]
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using Roboto.GameOffsets.Natives;
|
||||
using Nexus.GameOffsets.Natives;
|
||||
|
||||
namespace Roboto.GameOffsets.Components;
|
||||
namespace Nexus.GameOffsets.Components;
|
||||
|
||||
/// <summary>Buffs component — active status effects.</summary>
|
||||
[StructLayout(LayoutKind.Explicit, Size = 0x178)]
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Roboto.GameOffsets.Components;
|
||||
namespace Nexus.GameOffsets.Components;
|
||||
|
||||
/// <summary>Charges component — flask/skill charges.</summary>
|
||||
[StructLayout(LayoutKind.Explicit, Size = 0x20)]
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Roboto.GameOffsets.Components;
|
||||
namespace Nexus.GameOffsets.Components;
|
||||
|
||||
/// <summary>Chest component.</summary>
|
||||
[StructLayout(LayoutKind.Explicit, Size = 0x170)]
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Roboto.GameOffsets.Components;
|
||||
namespace Nexus.GameOffsets.Components;
|
||||
|
||||
/// <summary>Common header at the start of every component.</summary>
|
||||
[StructLayout(LayoutKind.Explicit, Size = 0x10)]
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Roboto.GameOffsets.Components;
|
||||
namespace Nexus.GameOffsets.Components;
|
||||
|
||||
/// <summary>Life component — contains Health, Mana, and ES vitals.</summary>
|
||||
[StructLayout(LayoutKind.Explicit, Size = 0x268)]
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using Roboto.GameOffsets.Natives;
|
||||
using Nexus.GameOffsets.Natives;
|
||||
|
||||
namespace Roboto.GameOffsets.Components;
|
||||
namespace Nexus.GameOffsets.Components;
|
||||
|
||||
/// <summary>Mods component — ModsAndObjectMagicProperties inline at +0x00. Rarity at +0x94.</summary>
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x1A0)]
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using Roboto.GameOffsets.Natives;
|
||||
using Nexus.GameOffsets.Natives;
|
||||
|
||||
namespace Roboto.GameOffsets.Components;
|
||||
namespace Nexus.GameOffsets.Components;
|
||||
|
||||
/// <summary>Player component — name, XP, level.</summary>
|
||||
[StructLayout(LayoutKind.Explicit, Size = 0x208)]
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Roboto.GameOffsets.Components;
|
||||
namespace Nexus.GameOffsets.Components;
|
||||
|
||||
/// <summary>Positioned component — reaction (friendly/hostile/neutral).</summary>
|
||||
[StructLayout(LayoutKind.Explicit, Size = 0x1E8)]
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using Roboto.GameOffsets.Natives;
|
||||
using Nexus.GameOffsets.Natives;
|
||||
|
||||
namespace Roboto.GameOffsets.Components;
|
||||
namespace Nexus.GameOffsets.Components;
|
||||
|
||||
/// <summary>Render component — world position, bounds, terrain height.</summary>
|
||||
[StructLayout(LayoutKind.Explicit, Size = 0x1B0)]
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Roboto.GameOffsets.Components;
|
||||
namespace Nexus.GameOffsets.Components;
|
||||
|
||||
/// <summary>Shrine component.</summary>
|
||||
[StructLayout(LayoutKind.Explicit, Size = 0x28)]
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue