├── .editorconfig ├── .github └── workflows │ └── dotnetcore.yml ├── .gitignore ├── D.ConsoleApp ├── D.ConsoleApp.csproj ├── GameStateBenchmark.cs └── Program.cs ├── D.Core ├── BotConfiguration.cs ├── D.Core.csproj ├── GameState.cs ├── GetOutBot.cs ├── GetOutBotAnalytic.cs ├── IStateUiWriter.cs ├── InteropTcpHelper.cs ├── MemoryReader.cs └── WindowsHelper.cs ├── D.Models ├── D.Models.csproj ├── StatEnum.cs └── Structs.cs ├── D.WpfApp ├── App.xaml ├── App.xaml.cs ├── ApplicationHelper.cs ├── D.WpfApp.csproj ├── GitVersion.yml ├── MachineStamp.cs ├── MainWindow.xaml ├── MainWindow.xaml.cs ├── StateUiWriter.cs ├── TextPath.cs ├── app.manifest ├── config.yml └── run.cmd ├── GitVersion.yml ├── README.md ├── d2rStructs.reclass ├── dependencies └── Analytics.Lib.dll └── diablobot.sln /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # See https://github.com/dotnet/format/blob/master/docs/Supported-.editorconfig-options.md for reference 4 | [*.cs] 5 | csharp_new_line_before_open_brace = none 6 | csharp_new_line_before_catch = false 7 | csharp_new_line_before_else = false 8 | csharp_new_line_before_finally = false 9 | csharp_new_line_before_members_in_anonymous_types = false 10 | csharp_new_line_before_members_in_object_initializers = false 11 | csharp_new_line_between_query_expression_clauses = true -------------------------------------------------------------------------------- /.github/workflows/dotnetcore.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core 2 | 3 | on: 4 | push: 5 | # Sequence of patterns matched against refs/heads 6 | branches: 7 | - main 8 | 9 | jobs: 10 | build: 11 | runs-on: windows-latest 12 | steps: 13 | - name: Install GitVersion 14 | uses: gittools/actions/gitversion/setup@v0.9.7 15 | with: 16 | versionSpec: '5.5.0' 17 | - name: Show variable 18 | run: | 19 | echo "token: ${{ secrets.GITHUB_TOKEN }}" 20 | - uses: actions/checkout@v1 21 | name: "Checkout" 22 | - name: Determine Version 23 | id: gitversion # step id used as reference for output values 24 | uses: gittools/actions/gitversion/execute@v0.9.7 25 | with: 26 | useConfigFile: true 27 | configFilePath: D.WpfApp/GitVersion.yml 28 | - name: Display GitVersion outputs 29 | run: | 30 | echo "Major: ${{ steps.gitversion.outputs.major }}" 31 | echo "Minor: ${{ steps.gitversion.outputs.minor }}" 32 | echo "Patch: ${{ steps.gitversion.outputs.patch }}" 33 | echo "PreReleaseTag: ${{ steps.gitversion.outputs.preReleaseTag }}" 34 | echo "PreReleaseTagWithDash: ${{ steps.gitversion.outputs.preReleaseTagWithDash }}" 35 | echo "PreReleaseLabel: ${{ steps.gitversion.outputs.preReleaseLabel }}" 36 | echo "PreReleaseNumber: ${{ steps.gitversion.outputs.preReleaseNumber }}" 37 | echo "WeightedPreReleaseNumber: ${{ steps.gitversion.outputs.weightedPreReleaseNumber }}" 38 | echo "BuildMetaData: ${{ steps.gitversion.outputs.buildMetaData }}" 39 | echo "BuildMetaDataPadded: ${{ steps.gitversion.outputs.buildMetaDataPadded }}" 40 | echo "FullBuildMetaData: ${{ steps.gitversion.outputs.fullBuildMetaData }}" 41 | echo "MajorMinorPatch: ${{ steps.gitversion.outputs.majorMinorPatch }}" 42 | echo "SemVer: ${{ steps.gitversion.outputs.semVer }}" 43 | echo "LegacySemVer: ${{ steps.gitversion.outputs.legacySemVer }}" 44 | echo "LegacySemVerPadded: ${{ steps.gitversion.outputs.legacySemVerPadded }}" 45 | echo "AssemblySemVer: ${{ steps.gitversion.outputs.assemblySemVer }}" 46 | echo "AssemblySemFileVer: ${{ steps.gitversion.outputs.assemblySemFileVer }}" 47 | echo "FullSemVer: ${{ steps.gitversion.outputs.fullSemVer }}" 48 | echo "InformationalVersion: ${{ steps.gitversion.outputs.informationalVersion }}" 49 | echo "BranchName: ${{ steps.gitversion.outputs.branchName }}" 50 | echo "EscapedBranchName: ${{ steps.gitversion.outputs.escapedBranchName }}" 51 | echo "Sha: ${{ steps.gitversion.outputs.sha }}" 52 | echo "ShortSha: ${{ steps.gitversion.outputs.shortSha }}" 53 | echo "NuGetVersionV2: ${{ steps.gitversion.outputs.nuGetVersionV2 }}" 54 | echo "NuGetVersion: ${{ steps.gitversion.outputs.nuGetVersion }}" 55 | echo "NuGetPreReleaseTagV2: ${{ steps.gitversion.outputs.nuGetPreReleaseTagV2 }}" 56 | echo "NuGetPreReleaseTag: ${{ steps.gitversion.outputs.nuGetPreReleaseTag }}" 57 | echo "VersionSourceSha: ${{ steps.gitversion.outputs.versionSourceSha }}" 58 | echo "CommitsSinceVersionSource: ${{ steps.gitversion.outputs.commitsSinceVersionSource }}" 59 | echo "CommitsSinceVersionSourcePadded: ${{ steps.gitversion.outputs.commitsSinceVersionSourcePadded }}" 60 | echo "UncommittedChanges: ${{ steps.gitversion.outputs.uncommittedChanges }}" 61 | echo "CommitDate: ${{ steps.gitversion.outputs.commitDate }}" 62 | - uses: actions/setup-dotnet@v1 63 | with: 64 | dotnet-version: 6.0.100 65 | - name: Build with dotnet 66 | run: dotnet publish ./D.WpfApp/D.WpfApp.csproj -r win10-x64 -c release --self-contained /p:PublishSingleFile=true /p:DebugType=None /p:DebugSymbols=false 67 | - name: Rename file 68 | run: powershell Rename-Item D.WpfApp/bin/release/net6.0-windows/win10-x64/publish/D.WpfApp.exe -NewName "GetOutBot.exe" 69 | - name: Build project # This would actually build your project, using zip for an example artifact 70 | run: | 71 | Compress-Archive -Path D.WpfApp/bin/release/net6.0-windows/win10-x64/publish/ -DestinationPath package.zip 72 | - name: Create Release 73 | id: create_release 74 | uses: actions/create-release@v1 75 | env: 76 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 77 | with: 78 | tag_name: ${{ steps.gitversion.outputs.fullSemVer }} 79 | release_name: Release ${{ steps.gitversion.outputs.fullSemVer }} 80 | draft: false 81 | prerelease: false 82 | - name: Upload Release Asset 83 | id: upload-release-asset 84 | uses: actions/upload-release-asset@v1 85 | env: 86 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 87 | with: 88 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 89 | asset_path: ./package.zip 90 | asset_name: package.zip 91 | asset_content_type: application/zip 92 | # - name: 'Upload package' 93 | # uses: actions/upload-artifact@v1 94 | # with: 95 | # name: windows-operationroom 96 | # path: ./bin/release/netcoreapp3.1/win10-x64/publish/operation-rooms.exe 97 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | .vs/ 9 | .gitattributes 10 | 11 | # Build results 12 | [Dd]ebug/ 13 | [Dd]ebugPublic/ 14 | [Rr]elease/ 15 | x64/ 16 | build/ 17 | bld/ 18 | [Bb]in/ 19 | [Oo]bj/ 20 | App_Data/ 21 | 22 | # Git 23 | *.orig 24 | 25 | .idea/** 26 | 27 | **D.ConsoleApp\BenchmarkDotnet** -------------------------------------------------------------------------------- /D.ConsoleApp/D.ConsoleApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /D.ConsoleApp/GameStateBenchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using BenchmarkDotNet.Order; 3 | using D.Core; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace D.ConsoleApp { 7 | [Orderer(SummaryOrderPolicy.FastestToSlowest)] 8 | public class GameStateBenchmark { 9 | public MemoryReader memoryReader; 10 | 11 | [GlobalSetup] 12 | public void Setup() { 13 | memoryReader = new MemoryReader(new Logger(new LoggerFactory())); 14 | } 15 | 16 | [Benchmark] 17 | public void GetState() { 18 | var state = memoryReader.GetState(); 19 | } 20 | 21 | [Benchmark] 22 | public void GetTcpTable() { 23 | var state = InteropTcpHelper.GetAllTcpConnections(); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /D.ConsoleApp/Program.cs: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/new-console-template for more information 2 | 3 | using BenchmarkDotNet.Running; 4 | using D.ConsoleApp; 5 | using D.Core; 6 | using D.Models; 7 | using Microsoft.Extensions.Logging; 8 | using System.Text; 9 | 10 | public class Program { 11 | 12 | static string mainPlayer = ""; 13 | static void Main(string[] args) { 14 | // BenchmarkRunner.Run(); 15 | 16 | var memoryReader = new MemoryReader(new Logger(new LoggerFactory())); 17 | var p = memoryReader.GetProcessInfo(); 18 | var hashTable = memoryReader.GetUnitHashtableAddress(); 19 | 20 | var unitHashTable = WindowsHelper.Read(p.Handle, hashTable); 21 | 22 | var dic = new Dictionary>(); 23 | foreach (var unitPtr in unitHashTable.UnitTable) { 24 | var unitAny = WindowsHelper.Read(p.Handle, unitPtr); 25 | ReadAllUnit(unitAny, unitPtr, dic, p.Handle); 26 | //do { 27 | // if (unitAny.UnityType != 0) 28 | // continue; 29 | 30 | // var playerName = Encoding.ASCII.GetString(WindowsHelper.Read(p.Handle, unitAny.UnitData, 16)).TrimEnd((char)0); 31 | // if (!string.IsNullOrEmpty(playerName)) { 32 | // if (dic.ContainsKey(playerName)) { 33 | // dic[playerName].Add(unitPtr); 34 | // mainPlayer = playerName; 35 | // } else { 36 | // dic.Add(playerName, new List { unitPtr }); 37 | // } 38 | // } 39 | 40 | // unitAny = WindowsHelper.Read(p.Handle, unitAny.pListNext); 41 | //}while() 42 | 43 | } 44 | 45 | foreach (var entry in dic) { 46 | Console.WriteLine($"{entry.Key} count: {entry.Value.Count}"); 47 | } 48 | 49 | Console.WriteLine("finish"); 50 | } 51 | 52 | public static void ReadAllUnit(UnitAny unitAny, IntPtr curUnitPtr, Dictionary> dic, IntPtr hdl) { 53 | if (curUnitPtr == IntPtr.Zero || unitAny.UnityType != 0) 54 | return; 55 | 56 | var playerName = Encoding.ASCII.GetString(WindowsHelper.Read(hdl, unitAny.UnitData, 16)).TrimEnd((char)0); 57 | if (!string.IsNullOrEmpty(playerName)) { 58 | if (dic.ContainsKey(playerName)) { 59 | dic[playerName].Add(curUnitPtr); 60 | mainPlayer = playerName; 61 | } else { 62 | dic.Add(playerName, new List { curUnitPtr }); 63 | } 64 | } 65 | ReadAllUnit(WindowsHelper.Read(hdl, unitAny.pListNext), unitAny.pListNext, dic, hdl); 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /D.Core/BotConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | 3 | namespace D.Core { 4 | public enum QuitMethod { 5 | None, 6 | Both, 7 | Socket, 8 | Menu 9 | } 10 | 11 | public class BotConfiguration { 12 | public BotConfiguration(IConfiguration config) { 13 | QuitOnHealthPercentage = (double)config.GetValue("QuitOnHealthPercentage", 35) / 100; 14 | QuitMethod = config.GetValue("QuitMethod", QuitMethod.Both); 15 | QuitOnMajorHit = config.GetValue("QuitOnMajorHit", true); 16 | } 17 | 18 | public double QuitOnHealthPercentage { get; set; } 19 | public QuitMethod QuitMethod { get; set; } 20 | public bool QuitOnMajorHit { get; set; } 21 | 22 | public bool QuitWithSocket => QuitMethod is QuitMethod.Both or QuitMethod.Socket; 23 | 24 | public bool QuitWithMenu => QuitMethod is QuitMethod.Both or QuitMethod.Menu; 25 | } 26 | } -------------------------------------------------------------------------------- /D.Core/D.Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ..\dependencies\Analytics.Lib.dll 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /D.Core/GameState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | using D.Models; 4 | 5 | namespace D.Core { 6 | public record GameState { 7 | public string PlayerName { get; set; } 8 | public Area CurrentArea { get; set; } 9 | public bool IsInTown { get; set; } 10 | 11 | public bool IsGameMenuOpen { get; set; } 12 | 13 | public int? PreviousHealth { get; set; } 14 | 15 | public int CurrentHealth { get; set; } 16 | 17 | public int MaxHealth { get; set; } 18 | 19 | [JsonIgnore] 20 | public IntPtr WindowHandle { get; set; } 21 | 22 | 23 | [JsonIgnore] 24 | public InteropTcpHelper.MIB_TCPROW_OWNER_PID? GameSocket { get; set; } 25 | 26 | public bool IsInGame() => CurrentArea != Area.None; 27 | 28 | public bool IsDead() => IsInGame() && CurrentHealth <= 0; 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /D.Core/GetOutBot.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using D.Models; 5 | using Microsoft.Extensions.Logging; 6 | using Stateless; 7 | 8 | namespace D.Core { 9 | public class GetOutBot { 10 | public enum State { 11 | LookingForPlayer, 12 | WatchingYou, 13 | InTown, 14 | GettingOut, 15 | IsDead 16 | } 17 | 18 | public enum Trigger { 19 | Watch, 20 | Quited, 21 | SearchPlayer, 22 | GetOut, 23 | IsSafeInTown, 24 | Death 25 | } 26 | 27 | private readonly CancellationTokenSource m_BotTaskSrc = new(); 28 | 29 | private readonly BotConfiguration m_Config; 30 | 31 | private readonly ILogger m_Logger; 32 | 33 | private readonly MemoryReader m_MemoryReader; 34 | 35 | public readonly StateMachine m_stateMachine; 36 | private readonly IStateUiWriter m_StateUiWriter; 37 | 38 | private readonly CancellationToken m_token; 39 | private StateMachine.TriggerWithParameters _setDeathTrigger; 40 | 41 | private StateMachine.TriggerWithParameters _setGetOutTrigger; 42 | 43 | public GetOutBot(IStateUiWriter stateUiWriter, BotConfiguration config, MemoryReader memoryReader, 44 | ILogger mLogger, CancellationToken mToken = default) { 45 | m_Config = config; 46 | m_Logger = mLogger; 47 | m_MemoryReader = memoryReader; 48 | m_StateUiWriter = stateUiWriter; 49 | m_stateMachine = new StateMachine(State.LookingForPlayer); 50 | m_token = mToken; 51 | m_token = m_BotTaskSrc.Token; 52 | 53 | ConfigureStateMachine(); 54 | } 55 | 56 | public void ConfigureStateMachine() { 57 | m_StateUiWriter.WriteState($"{State.LookingForPlayer}"); 58 | _setGetOutTrigger = m_stateMachine.SetTriggerParameters(Trigger.GetOut); 59 | _setDeathTrigger = m_stateMachine.SetTriggerParameters(Trigger.Death); 60 | m_stateMachine.OnTransitioned(transition => { 61 | // if change in state 62 | if (transition.Source != transition.Destination) { 63 | m_StateUiWriter.WriteState($"{transition.Destination}"); 64 | m_Logger.LogInformation($"State transition: {transition.Destination}"); 65 | if (transition.Source == State.LookingForPlayer) 66 | m_StateUiWriter.WriteAdditionalData(""); 67 | else if (transition.Destination == State.LookingForPlayer && transition.Source != State.GettingOut) 68 | m_StateUiWriter.WriteAdditionalData(""); 69 | } 70 | }); 71 | 72 | m_stateMachine.Configure(State.LookingForPlayer) 73 | .PermitReentry(Trigger.SearchPlayer) 74 | .OnEntryFromAsync(Trigger.SearchPlayer, async () => await Task.Delay(1000)) 75 | .Permit(Trigger.Death, State.IsDead) 76 | .Permit(Trigger.IsSafeInTown, State.InTown) 77 | .Permit(Trigger.GetOut, State.GettingOut) 78 | .Permit(Trigger.Watch, State.WatchingYou) 79 | .OnEntryAsync(async () => await OnWatching()); 80 | 81 | m_stateMachine.Configure(State.WatchingYou) 82 | .PermitReentry(Trigger.Watch) 83 | .Permit(Trigger.Death, State.IsDead) 84 | .Permit(Trigger.IsSafeInTown, State.InTown) 85 | .OnEntryFromAsync(Trigger.IsSafeInTown, async () => await Task.Delay(500)) 86 | .Permit(Trigger.GetOut, State.GettingOut) 87 | .Permit(Trigger.SearchPlayer, State.LookingForPlayer) 88 | .OnEntryAsync(async () => await OnWatching()); 89 | 90 | m_stateMachine.Configure(State.InTown) 91 | .SubstateOf(State.WatchingYou) 92 | .PermitReentry(Trigger.IsSafeInTown) 93 | .OnEntryFromAsync(Trigger.IsSafeInTown, async () => await Task.Delay(500)) 94 | .Permit(Trigger.Watch, State.WatchingYou); 95 | 96 | m_stateMachine.Configure(State.GettingOut) 97 | .Permit(Trigger.Death, State.IsDead) 98 | .Permit(Trigger.Watch, State.WatchingYou) 99 | .Permit(Trigger.IsSafeInTown, State.InTown) 100 | .OnEntryFromAsync(_setGetOutTrigger, async state => await OnGetOut(state)) 101 | .Permit(Trigger.Quited, State.LookingForPlayer); 102 | 103 | m_stateMachine.Configure(State.IsDead) 104 | .OnEntryFromAsync(_setDeathTrigger, async state => await OnDeath(state)) 105 | .Permit(Trigger.Watch, State.WatchingYou); 106 | 107 | // just ignore trigger in bad state 108 | m_stateMachine.OnUnhandledTrigger((s, t) => { m_Logger.LogWarning($"Invalid transition: {s} {t}"); }); 109 | } 110 | 111 | public async Task Run() { 112 | m_Logger.LogInformation("run bot"); 113 | await Task.Run(async () => { 114 | do { 115 | await OnWatching(); 116 | await Task.Delay(10); 117 | } while (true); 118 | }); 119 | } 120 | 121 | public async Task OnWatching() { 122 | try { 123 | if (m_token.IsCancellationRequested) 124 | return; 125 | 126 | var state = GetState(); 127 | 128 | if (state == null) { 129 | await m_stateMachine.FireAsync(Trigger.SearchPlayer); 130 | return; 131 | } 132 | 133 | m_StateUiWriter.WriteAdditionalData($"guarding: {state.PlayerName}"); 134 | if (state.IsInTown && state.CurrentArea != Area.None) { 135 | await m_stateMachine.FireAsync(Trigger.IsSafeInTown); 136 | return; 137 | } 138 | 139 | if (state.IsDead()) { 140 | await m_stateMachine.FireAsync(_setDeathTrigger, state); 141 | return; 142 | } 143 | 144 | if (HasToGetOut(state)) { 145 | await m_stateMachine.FireAsync(_setGetOutTrigger, state); 146 | return; 147 | } 148 | 149 | await Task.Delay(1); 150 | await m_stateMachine.FireAsync(Trigger.Watch); 151 | } catch (Exception e) { 152 | GetOutBotAnalytic.SendError(e.Message); 153 | m_Logger.LogError("Exception in OnWatching {@e}", e); 154 | throw; 155 | } 156 | } 157 | 158 | private async Task OnGetOut(GameState receivedState) { 159 | try { 160 | m_StateUiWriter.WriteAdditionalData("Try to get out!"); 161 | var currentHealth = receivedState.CurrentHealth; 162 | 163 | var successfulOut = await TryToGetOut(receivedState); 164 | var state = GetState(); 165 | if (!successfulOut && state != null && state.IsDead()) { 166 | await m_stateMachine.FireAsync(_setDeathTrigger, state); 167 | } else if (!successfulOut) { 168 | m_Logger.LogCritical("Couldn't get out!"); 169 | m_Logger.LogWarning("Couldn't get out :( {@state}", state); 170 | m_StateUiWriter.WriteAdditionalData("Couldn't get out :("); 171 | GetOutBotAnalytic.SendInfo("couldn't get out", receivedState); 172 | await Task.Delay(2000); 173 | await m_stateMachine.FireAsync(Trigger.Watch); 174 | } else // Success! 175 | { 176 | m_Logger.LogInformation("Got out! With {currentHealth} hp", currentHealth); 177 | m_StateUiWriter.WriteAdditionalData($"Got out! With {currentHealth} hp"); 178 | GetOutBotAnalytic.SendGotOutEvent(receivedState); 179 | await Task.Delay(6000); 180 | await m_stateMachine.FireAsync(Trigger.Quited); 181 | } 182 | } catch (Exception e) { 183 | GetOutBotAnalytic.SendError(e.Message); 184 | m_Logger.LogError("Exception in OnGetOut {@e}", e); 185 | throw; 186 | } 187 | } 188 | 189 | private async Task OnDeath(GameState state) { 190 | try { 191 | m_Logger.LogWarning("Death {@state}", state); 192 | m_StateUiWriter.WriteAdditionalData("woups.."); 193 | GetOutBotAnalytic.SendDeathEvent(state); 194 | // stay in that state until not death (so in menu or other char) 195 | SpinWait.SpinUntil(() => { 196 | Thread.Sleep(500); 197 | return !GetState()?.IsDead() ?? true; 198 | }, -1); 199 | m_StateUiWriter.Clear(); 200 | await m_stateMachine.FireAsync(Trigger.Watch); 201 | } catch (Exception e) { 202 | GetOutBotAnalytic.SendError(e.Message); 203 | m_Logger.LogError("exception in OnDeath {@e}", e); 204 | throw; 205 | } 206 | } 207 | 208 | private bool HasToGetOut(GameState? state) { 209 | if (state == null || state.IsDead() || state.IsInTown) 210 | return false; 211 | 212 | if (m_Config.QuitOnMajorHit && state.PreviousHealth.HasValue 213 | && state.PreviousHealth.Value <= state.MaxHealth) { // to prevent quitting when buff end 214 | var previousHealth = state.PreviousHealth.Value; 215 | var previousHit = previousHealth - state.CurrentHealth; 216 | var wasMajorHit = previousHit >= state.CurrentHealth; 217 | if (wasMajorHit) { 218 | m_Logger.LogInformation( 219 | "previous hit was {previousHit} and life remaining {currentHealth} so get out", previousHit, 220 | state.CurrentHealth); 221 | return true; 222 | } 223 | } 224 | 225 | return (double)state.CurrentHealth / state.MaxHealth <= m_Config.QuitOnHealthPercentage; 226 | } 227 | 228 | private GameState? GetState() { 229 | var state = m_MemoryReader.GetState(); 230 | return state; 231 | } 232 | 233 | private async Task TryToGetOut(GameState? state) { 234 | var tokenSrc = new CancellationTokenSource(); 235 | var token = tokenSrc.Token; 236 | m_Logger.LogInformation("Quit method: {QuitMethod}", m_Config.QuitMethod); 237 | 238 | var outFromSocketTsk = m_Config.QuitWithSocket 239 | ? Task.Run(() => TryToGetOutSocket(state)) 240 | : Task.FromResult(false); 241 | var outFromMenuTsk = m_Config.QuitWithMenu 242 | ? Task.Run(() => TryToGetOutWithGameMenu(state, token)) 243 | : Task.FromResult(false); 244 | 245 | Task.WaitAny(outFromSocketTsk, outFromMenuTsk); 246 | // cancel only if successfully killed socket 247 | if (outFromSocketTsk.IsCompleted && outFromSocketTsk.Result) 248 | tokenSrc.Cancel(); 249 | 250 | if (outFromMenuTsk.Result) 251 | m_Logger.LogInformation("out from menu"); 252 | if (outFromSocketTsk.Result) 253 | m_Logger.LogInformation("out from socket"); 254 | 255 | return outFromSocketTsk.Result || outFromMenuTsk.Result; 256 | } 257 | 258 | private async Task TryToGetOutWithGameMenu(GameState receivedState, CancellationToken token) { 259 | var state = receivedState; 260 | m_Logger.LogInformation("start get out from menu task"); 261 | try { 262 | // try some time 263 | var delayInMilliseconds = 10; 264 | var tryCountMax = 2500 / delayInMilliseconds; 265 | var tryCount = 0; 266 | var hasToGetOut = true; 267 | do { 268 | // we at least try 4 time before cancelling. since maybe the socket kill wasn't the right one lol 269 | if (token.IsCancellationRequested && tryCount > 4) { 270 | m_Logger.LogInformation("get out from menu was cancelled"); 271 | return false; 272 | } 273 | 274 | WindowsHelper.SetForegroundWindow(state.WindowHandle); 275 | if (!state.IsGameMenuOpen) WindowsHelper.SendEscapeKey(state.WindowHandle); 276 | 277 | var windowRect = WindowsHelper.GetWindowRect(state.WindowHandle); 278 | 279 | var width = windowRect.Right - windowRect.Left; 280 | var height = windowRect.Bottom - windowRect.Top; 281 | WindowsHelper.LeftMouseClick(windowRect.Left + width / 2, windowRect.Top + height / 20 * 9); 282 | m_Logger.LogInformation($"Tried to get out from menu. tryCount: {tryCount}"); 283 | await Task.Delay(10); 284 | tryCount++; 285 | state = GetState(); 286 | hasToGetOut = HasToGetOut(state); 287 | } while (hasToGetOut && tryCount <= tryCountMax); 288 | 289 | return !hasToGetOut; 290 | } catch (TaskCanceledException) { 291 | m_Logger.LogInformation("get out from menu was cancelled"); 292 | return false; 293 | } 294 | } 295 | 296 | private async Task TryToGetOutSocket(GameState? state) { 297 | m_Logger.LogInformation("start get out from socket task"); 298 | if (state?.GameSocket == null) 299 | return false; 300 | 301 | var result = InteropTcpHelper.DeleteSocket(state.GameSocket.Value); 302 | if (result != 0) { 303 | GetOutBotAnalytic.SendError($"couldn't kill socket {result}"); 304 | m_Logger.LogError("couldn't kill socket {@result}", result); 305 | return false; 306 | } 307 | 308 | m_Logger.LogInformation("successful kill socket"); 309 | return true; 310 | } 311 | } 312 | } -------------------------------------------------------------------------------- /D.Core/GetOutBotAnalytic.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using analytics_lib; 4 | using System.Text.Json; 5 | 6 | namespace D.Core { 7 | public static class EventTypes { 8 | public static string Session = "session"; 9 | 10 | public static string Error = "error"; 11 | 12 | public static string Info = "info"; 13 | 14 | public static string Death = "death"; 15 | 16 | public static string GotOut = "gotOut"; 17 | 18 | public static string Start = "start"; 19 | } 20 | 21 | public static class GetOutBotAnalytic { 22 | 23 | public static IDisposable StartSession() => new GetOutAnalytic(); 24 | 25 | public static void SendError(string message) => new BotErrorEvent(message).Send(); 26 | 27 | public static void SendInfo(string message, object data = null) => new InfoEvent(message, data).Send(); 28 | public static void SendDeathEvent(object data) => new DeathEvent(data).Send(); 29 | 30 | public static void SendStart() => new StartEvent().Send(); 31 | 32 | public static void SendGotOutEvent(GameState data) => new GotOutEvent(data).Send(); 33 | } 34 | 35 | public class BotSpanBase : AnalyticSpanBase { 36 | static readonly JsonSerializerOptions k_SerializeOptions = new() { 37 | Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, 38 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 39 | DictionaryKeyPolicy = JsonNamingPolicy.CamelCase 40 | }; 41 | 42 | public BotSpanBase(string name) : base(name) { 43 | AddData("ts", DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ssK")); 44 | AddData("eventType", name); 45 | } 46 | 47 | public void SetMessage(string message) => AddData("message", message); 48 | 49 | public void SetAdditionalData(IDictionary data) => AddData("data", JsonSerializer.Serialize(data, k_SerializeOptions)); 50 | } 51 | 52 | public class BotEventBase : AnalyticEventBase { 53 | static readonly JsonSerializerOptions k_SerializeOptions = new() { 54 | Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, 55 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 56 | DictionaryKeyPolicy = JsonNamingPolicy.CamelCase 57 | }; 58 | 59 | public BotEventBase(string name) : base(name) { 60 | AddData("ts", DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ssK")); 61 | AddData("eventType", name); 62 | } 63 | 64 | public void SetMessage(string message) => AddData("message", message); 65 | 66 | public void SetAdditionalData(object data) => AddData("data", JsonSerializer.Serialize(data, k_SerializeOptions)); 67 | } 68 | 69 | public class StartEvent : BotEventBase { 70 | public StartEvent() : base(EventTypes.Start) { 71 | } 72 | } 73 | 74 | public class InfoEvent : BotEventBase { 75 | public InfoEvent(string message, object data) : base(EventTypes.Info) { 76 | SetMessage(message); 77 | if (data != null) 78 | SetAdditionalData(data); 79 | } 80 | } 81 | 82 | 83 | public class GetOutAnalytic : BotSpanBase { 84 | public GetOutAnalytic() : base(EventTypes.Session) { 85 | } 86 | } 87 | 88 | public class BotErrorEvent : BotEventBase { 89 | public BotErrorEvent(string message) : base(EventTypes.Error) { 90 | SetMessage(message); 91 | } 92 | } 93 | 94 | public class DeathEvent : BotEventBase { 95 | public DeathEvent(object data) : base(EventTypes.Death) { 96 | SetAdditionalData(data); 97 | } 98 | } 99 | 100 | public class GotOutEvent : BotEventBase { 101 | public GotOutEvent(GameState state) : base(EventTypes.GotOut) { 102 | SetAdditionalData(new Dictionary { 103 | {"state", state }, 104 | {"percentage", ((double)state.CurrentHealth/state.MaxHealth).ToString("0.##") } 105 | }); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /D.Core/IStateUiWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace D.Core { 8 | public interface IStateUiWriter { 9 | void Clear(); 10 | void WriteState(string state); 11 | void WriteAdditionalData(string data); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /D.Core/InteropTcpHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace D.Core { 5 | public static class InteropTcpHelper { 6 | public static int DeleteSocket(MIB_TCPROW_OWNER_PID socket) { 7 | socket.state = (int)InteropTcpHelper.State.Delete_TCB; 8 | var ptr = GetPtrToNewObject(socket); 9 | // https://github.com/magisterquis/EDRSniper#error-317 10 | // 317 The function is unable to set the TCP entry since the application is running non-elevated. 11 | return InteropTcpHelper.SetTcpEntry(ptr); 12 | } 13 | 14 | private static IntPtr GetPtrToNewObject(object? obj) { 15 | if (obj == null) return IntPtr.Zero; 16 | IntPtr ptr = Marshal.AllocCoTaskMem(Marshal.SizeOf(obj)); 17 | Marshal.StructureToPtr(obj, ptr, false); 18 | return ptr; 19 | } 20 | 21 | [DllImport("iphlpapi.dll")] 22 | //private static extern int SetTcpEntry(MIB_TCPROW tcprow); 23 | public static extern int SetTcpEntry(IntPtr pTcprow); 24 | 25 | public enum TCP_TABLE_CLASS { 26 | TCP_TABLE_BASIC_LISTENER, 27 | TCP_TABLE_BASIC_CONNECTIONS, 28 | TCP_TABLE_BASIC_ALL, 29 | TCP_TABLE_OWNER_PID_LISTENER, 30 | TCP_TABLE_OWNER_PID_CONNECTIONS, 31 | TCP_TABLE_OWNER_PID_ALL, 32 | TCP_TABLE_OWNER_MODULE_LISTENER, 33 | TCP_TABLE_OWNER_MODULE_CONNECTIONS, 34 | TCP_TABLE_OWNER_MODULE_ALL 35 | } 36 | 37 | [DllImport("iphlpapi.dll", SetLastError = true)] 38 | private static extern uint GetExtendedTcpTable(IntPtr pTcpTable, 39 | ref int dwOutBufLen, 40 | bool sort, 41 | int ipVersion, 42 | TCP_TABLE_CLASS tblClass, 43 | int reserved); 44 | 45 | public static MIB_TCPROW_OWNER_PID[] GetAllTcpConnections() { 46 | var AF_INET = 2; // IP_v4 47 | var buffSize = 0; 48 | 49 | // how much memory do we need? 50 | var ret = GetExtendedTcpTable(IntPtr.Zero, ref buffSize, true, AF_INET, 51 | TCP_TABLE_CLASS.TCP_TABLE_OWNER_PID_ALL, 0); 52 | if (ret != 0 && ret != 122) // 122 insufficient buffer size 53 | throw new Exception("bad ret on check " + ret); 54 | var buffTable = Marshal.AllocHGlobal(buffSize); 55 | 56 | try { 57 | ret = GetExtendedTcpTable(buffTable, 58 | ref buffSize, 59 | true, 60 | AF_INET, 61 | TCP_TABLE_CLASS.TCP_TABLE_OWNER_PID_ALL, 62 | 0); 63 | if (ret != 0) 64 | throw new Exception("bad ret " + ret); 65 | 66 | // get the number of entries in the table 67 | var tab = (MIB_TCPTABLE_OWNER_PID)Marshal.PtrToStructure(buffTable, typeof(MIB_TCPTABLE_OWNER_PID)); 68 | var rowPtr = (IntPtr)((long)buffTable + Marshal.SizeOf(tab.dwNumEntries)); 69 | MIB_TCPROW_OWNER_PID[] tTable = new MIB_TCPROW_OWNER_PID[tab.dwNumEntries]; 70 | 71 | for (var i = 0; i < tab.dwNumEntries; i++) { 72 | var tcpRow = (MIB_TCPROW_OWNER_PID)Marshal 73 | .PtrToStructure(rowPtr, typeof(MIB_TCPROW_OWNER_PID)); 74 | tTable[i] = tcpRow; 75 | // next entry 76 | rowPtr = (IntPtr)((long)rowPtr + Marshal.SizeOf(tcpRow)); 77 | } 78 | 79 | return tTable; 80 | } finally { 81 | // Free the Memory 82 | Marshal.FreeHGlobal(buffTable); 83 | } 84 | } 85 | 86 | [StructLayout(LayoutKind.Sequential)] 87 | public struct MIB_TCPROW_OWNER_PID { 88 | public uint state; 89 | public uint localAddr; 90 | public byte localPort1; 91 | public byte localPort2; 92 | public byte localPort3; 93 | public byte localPort4; 94 | public uint remoteAddr; 95 | public byte remotePort1; 96 | public byte remotePort2; 97 | public byte remotePort3; 98 | public byte remotePort4; 99 | public int owningPid; 100 | 101 | public ushort LocalPort { 102 | get { 103 | return BitConverter.ToUInt16( 104 | new byte[2] { localPort2, localPort1 }, 0); 105 | } 106 | } 107 | 108 | public ushort RemotePort { 109 | get { 110 | return BitConverter.ToUInt16( 111 | new byte[2] { remotePort2, remotePort1 }, 0); 112 | } 113 | } 114 | } 115 | 116 | public enum State { 117 | /// All 118 | All = 0, 119 | 120 | /// Closed 121 | Closed = 1, 122 | 123 | /// Listen 124 | Listen = 2, 125 | 126 | /// Syn_Sent 127 | Syn_Sent = 3, 128 | 129 | /// Syn_Rcvd 130 | Syn_Rcvd = 4, 131 | 132 | /// Established 133 | Established = 5, 134 | 135 | /// Fin_Wait1 136 | Fin_Wait1 = 6, 137 | 138 | /// Fin_Wait2 139 | Fin_Wait2 = 7, 140 | 141 | /// Close_Wait 142 | Close_Wait = 8, 143 | 144 | /// Closing 145 | Closing = 9, 146 | 147 | /// Last_Ack 148 | Last_Ack = 10, 149 | 150 | /// Time_Wait 151 | Time_Wait = 11, 152 | 153 | /// Delete_TCB 154 | Delete_TCB = 12 155 | } 156 | 157 | [StructLayout(LayoutKind.Sequential)] 158 | public struct MIB_TCPTABLE_OWNER_PID { 159 | public uint dwNumEntries; 160 | private readonly MIB_TCPROW_OWNER_PID table; 161 | } 162 | } 163 | } -------------------------------------------------------------------------------- /D.Core/MemoryReader.cs: -------------------------------------------------------------------------------- 1 | using D.Models; 2 | using Reloaded.Memory.Sigscan; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.Linq; 7 | using System.Runtime.InteropServices; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using Microsoft.Extensions.Logging; 11 | 12 | namespace D.Core { 13 | public partial class MemoryReader { 14 | private static readonly string ProcessName = Encoding.UTF8.GetString(new byte[] { 68, 50, 82 }); 15 | public static int UnitHashTable = 0x20AF660; 16 | public static int UiSettings = 0x20BF310; 17 | public static int ExpansionCheck = 0x20BF335; 18 | 19 | private ProcessInfo m_ProcessInfo; 20 | private MemoryState _mMemoryState; 21 | 22 | private Models.Path mPath = default; 23 | private Room mRoom = default; 24 | private RoomEx mRoomEx = default; 25 | private Level mLevel = default; 26 | private StatList mStatList = default; 27 | private UiGameMenus mGameMenus = default; 28 | private static ILogger Logger; 29 | private GameState? previousState = null; 30 | 31 | public MemoryReader(ILogger logger) { 32 | Logger = logger; 33 | _mMemoryState = new MemoryState(); 34 | m_ProcessInfo = GetProcessInfo(); 35 | } 36 | 37 | class MemoryState { 38 | public MemoryState() { 39 | GameSocket = null; 40 | } 41 | 42 | public IntPtr UiSettingPtr { get; set; } 43 | public IntPtr PlayerUnitPtr { get; set; } 44 | public IntPtr UnitHashtableAddressPtr { get; set; } 45 | public IntPtr UiSettingAddressPtr { get; set; } 46 | public InteropTcpHelper.MIB_TCPROW_OWNER_PID? GameSocket { get; set; } = null; 47 | 48 | public UnitAny PlayerUnit = default; 49 | } 50 | 51 | void Reset() { 52 | previousState = null; 53 | _mMemoryState = new MemoryState(); 54 | } 55 | // search process 56 | // search player 57 | // check if we still have the right player 58 | 59 | public class ProcessInfo { 60 | public Process? Process; 61 | public IntPtr Handle; 62 | public IntPtr BaseAddress; 63 | public IntPtr ProcessWindowHandle; 64 | public Scanner Scanner; 65 | 66 | public static ProcessInfo Create() { 67 | try { 68 | var process = Process.GetProcessesByName(ProcessName).FirstOrDefault(); 69 | if (process == null) 70 | return null; 71 | 72 | 73 | return new ProcessInfo() { 74 | Process = process, 75 | BaseAddress = process.MainModule.BaseAddress, 76 | Handle = WindowsHelper.OpenProcess((uint)WindowsHelper.ProcessAccessFlags.VirtualMemoryRead, false, process.Id), 77 | ProcessWindowHandle = process.MainWindowHandle, 78 | Scanner = new Scanner(process, process.MainModule) 79 | }; 80 | } catch (Exception e) { 81 | Logger.LogError("Get process info {@e}", e); 82 | throw; 83 | } 84 | } 85 | } 86 | 87 | public ProcessInfo GetProcessInfo() { 88 | if (m_ProcessInfo == null || m_ProcessInfo!.Process!.Id != Process.GetProcessesByName(ProcessName).FirstOrDefault()?.Id 89 | || m_ProcessInfo.ProcessWindowHandle == IntPtr.Zero) { 90 | Reset(); 91 | return ProcessInfo.Create(); 92 | } 93 | 94 | return m_ProcessInfo; 95 | } 96 | 97 | public IntPtr GetUnitHashtableAddress() { 98 | if (_mMemoryState.UnitHashtableAddressPtr != IntPtr.Zero) 99 | return _mMemoryState.UnitHashtableAddressPtr; 100 | 101 | // shameless copy from https://github.com/OneXDeveloper/MapAssist/ Helpers/Pattern.cs 102 | var offsetResult = m_ProcessInfo.Scanner.CompiledFindPattern("48 03 C7 49 8B 8C C6"); 103 | var resultRelativeAddress = IntPtr.Add(IntPtr.Add(m_ProcessInfo.BaseAddress, offsetResult.Offset), 7); 104 | var offSetAddress = WindowsHelper.Read(m_ProcessInfo.Handle, resultRelativeAddress); 105 | 106 | return IntPtr.Add(m_ProcessInfo.BaseAddress, offSetAddress); 107 | } 108 | 109 | //https://github.com/OneXDeveloper/MapAssist/blob/main/Helpers/ProcessContext.cs 110 | public IntPtr GetUiSettingAddress() { 111 | if (_mMemoryState.UiSettingAddressPtr != IntPtr.Zero) 112 | return _mMemoryState.UiSettingAddressPtr; 113 | 114 | var offsetResult = m_ProcessInfo.Scanner.CompiledFindPattern("40 84 ed 0f 94 05"); 115 | var resultRelativeAddress = IntPtr.Add(IntPtr.Add(m_ProcessInfo.BaseAddress, offsetResult.Offset), 6); 116 | var offSetAddress = WindowsHelper.Read(m_ProcessInfo.Handle, resultRelativeAddress); 117 | 118 | return IntPtr.Add(m_ProcessInfo.BaseAddress, offsetResult.Offset + 9 + offSetAddress); 119 | } 120 | 121 | public bool CurrentPlayerValid() { 122 | var unitAny = WindowsHelper.Read(m_ProcessInfo.Handle, _mMemoryState.PlayerUnitPtr); 123 | return unitAny.Inventory != IntPtr.Zero; 124 | } 125 | 126 | // Logic took from https://github.com/RushTheOne/MapAssist/blob/main/Helpers/GameMemory.cs 127 | public unsafe GameState? GetState() { 128 | try { 129 | m_ProcessInfo = GetProcessInfo(); 130 | if (m_ProcessInfo == null) { 131 | Reset(); 132 | return null; 133 | } 134 | 135 | if (_mMemoryState.PlayerUnitPtr == IntPtr.Zero || !CurrentPlayerValid()) { 136 | var unitHashTablePtr = GetUnitHashtableAddress(); 137 | var uiSettingPtr = GetUiSettingAddress(); 138 | 139 | var result = FindPlayer(unitHashTablePtr, m_ProcessInfo); 140 | if (!result.HasValue) 141 | return null; 142 | 143 | _mMemoryState = new MemoryState() { 144 | PlayerUnit = result.Value.data, 145 | PlayerUnitPtr = result.Value.ptr, 146 | UnitHashtableAddressPtr = unitHashTablePtr, 147 | UiSettingAddressPtr = uiSettingPtr, 148 | GameSocket = GetGameSocket() 149 | }; 150 | } 151 | 152 | mGameMenus = WindowsHelper.Read(m_ProcessInfo.Handle, _mMemoryState.UiSettingAddressPtr); 153 | mPath = ReadAndValidate(m_ProcessInfo, _mMemoryState.PlayerUnit.pPath); 154 | mRoom = ReadAndValidate(m_ProcessInfo, mPath.pRoom); 155 | mRoomEx = ReadAndValidate(m_ProcessInfo, mRoom.pRoomEx); 156 | mLevel = ReadAndValidate(m_ProcessInfo, mRoomEx.pLevel); 157 | mStatList = ReadAndValidate(m_ProcessInfo, _mMemoryState.PlayerUnit.StatsList); 158 | 159 | var playerName = Encoding.ASCII.GetString(WindowsHelper.Read(m_ProcessInfo.Handle, _mMemoryState.PlayerUnit.UnitData, 16)).TrimEnd((char)0); 160 | var stats = WindowsHelper.Read(m_ProcessInfo.Handle, mStatList.Stats.Array, (int)mStatList.Stats.Size); 161 | var levelId = mLevel.LevelId; 162 | if (levelId == Area.None) 163 | throw new Exception("Invalid area"); 164 | 165 | var currentState = new GameState { 166 | PlayerName = playerName, 167 | CurrentArea = mLevel.LevelId, 168 | PreviousHealth = previousState?.CurrentHealth, 169 | CurrentHealth = GetCurrentHealth(stats), 170 | MaxHealth = GetMaxHp(stats), 171 | IsInTown = 172 | levelId is Area.RogueEncampment or Area.LutGholein or Area.KurastDocks or Area 173 | .ThePandemoniumFortress or Area.Harrogath, 174 | IsGameMenuOpen = mGameMenus.IsGameMenuOpen == 1, 175 | WindowHandle = m_ProcessInfo.ProcessWindowHandle, 176 | GameSocket = _mMemoryState.GameSocket 177 | }; 178 | previousState = currentState; 179 | return currentState; 180 | } catch (Exception) { 181 | Reset(); 182 | return null; 183 | } 184 | } 185 | 186 | static (IntPtr ptr, UnitAny data)? FindPlayer(IntPtr unitHashTablePtr, ProcessInfo pInfo) { 187 | var dic = new Dictionary>(); 188 | var mainPlayerName = string.Empty; 189 | var mainPlayerPtr = IntPtr.Zero; 190 | var unitHashTable = WindowsHelper.Read(pInfo.Handle, unitHashTablePtr); 191 | 192 | foreach (var unitPtr in unitHashTable.UnitTable) { 193 | var unitAny = WindowsHelper.Read(pInfo.Handle, unitPtr); 194 | ReadAllUnits(unitAny, unitPtr, dic, pInfo.Handle, ref mainPlayerName); 195 | } 196 | foreach (var ptrAndData in dic[mainPlayerName]) { 197 | var path = WindowsHelper.Read(pInfo.Handle, ptrAndData.data.pPath); 198 | if (path.DynamicY != 0 && path.DynamicY != 0) { 199 | return ptrAndData; 200 | } 201 | } 202 | 203 | return null; 204 | 205 | } 206 | 207 | public static void ReadAllUnits(UnitAny unitAny, IntPtr curUnitPtr, Dictionary> dic, IntPtr hdl, ref string mainPlayer) { 208 | if (curUnitPtr == IntPtr.Zero) 209 | return; 210 | 211 | if(unitAny.UnityType == 0) { 212 | var playerName = Encoding.ASCII.GetString(WindowsHelper.Read(hdl, unitAny.UnitData, 16)).TrimEnd((char)0); 213 | if (!string.IsNullOrEmpty(playerName)) { 214 | if (dic.ContainsKey(playerName)) { 215 | dic[playerName].Add((curUnitPtr, unitAny)); 216 | mainPlayer = playerName; 217 | } else { 218 | dic.Add(playerName, new List<(IntPtr, UnitAny)> { (curUnitPtr, unitAny) }); 219 | } 220 | } 221 | } 222 | ReadAllUnits(WindowsHelper.Read(hdl, unitAny.pListNext), unitAny.pListNext, dic, hdl, ref mainPlayer); 223 | } 224 | 225 | static int GetCurrentHealth(StatValue[] stats) => stats.FirstOrDefault(x => x.Stat == Stat.STAT_HP).Value >> 8; 226 | static int GetMaxHp(StatValue[] stats) => stats.FirstOrDefault(x => x.Stat == Stat.STAT_MAXHP).Value >> 8; 227 | 228 | private T ReadAndValidate(ProcessInfo pInfo, IntPtr ptr) where T : struct { 229 | if (ptr == IntPtr.Zero) 230 | throw new Exception($"Pointer to {typeof(T).Name} is null"); 231 | return WindowsHelper.Read(pInfo.Handle, ptr); 232 | } 233 | 234 | /* 235 | This is best effort to find the game socket. We take the most recent one. 236 | */ 237 | public InteropTcpHelper.MIB_TCPROW_OWNER_PID GetGameSocket() { 238 | if (this._mMemoryState.GameSocket.HasValue) 239 | return this._mMemoryState.GameSocket.Value; 240 | var d2rSockets = InteropTcpHelper.GetAllTcpConnections().Where(x => 241 | x.state == (uint)InteropTcpHelper.State.Established && 242 | x.owningPid == m_ProcessInfo.Process.Id 243 | ).ToList(); 244 | 245 | var seen = new HashSet(); 246 | var duplicate = new HashSet(); 247 | foreach (var s in d2rSockets) { 248 | if (seen.Contains(s.remoteAddr)) 249 | duplicate.Add(s.remoteAddr); 250 | seen.Add(s.remoteAddr); 251 | } 252 | d2rSockets = d2rSockets.Where(x => !duplicate.Contains(x.remoteAddr)).ToList(); 253 | var socketForThatIp = d2rSockets.Last(); 254 | this._mMemoryState.GameSocket = socketForThatIp; 255 | Logger.LogInformation("found ip: {gameIp}", IPIntToString((int)socketForThatIp.remoteAddr)); 256 | 257 | return socketForThatIp; 258 | } 259 | 260 | public static string IPIntToString(int IP) { 261 | byte[] addr = System.BitConverter.GetBytes(IP); 262 | return addr[0] + "." + addr[1] + "." + addr[2] + "." + addr[3]; 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /D.Core/WindowsHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace D.Core { 5 | public static class WindowsHelper { 6 | [System.Runtime.InteropServices.DllImport("user32.dll")] 7 | public static extern int SetForegroundWindow(IntPtr hWnd); 8 | 9 | //This is a replacement for Cursor.Position in WinForms 10 | [System.Runtime.InteropServices.DllImport("user32.dll")] 11 | static extern bool SetCursorPos(int x, int y); 12 | 13 | [System.Runtime.InteropServices.DllImport("user32.dll")] 14 | public static extern void mouse_event(int dwFlags, int dx, int dy, int cButtons, int dwExtraInfo); 15 | 16 | public const int MOUSEEVENTF_LEFTDOWN = 0x02; 17 | public const int MOUSEEVENTF_LEFTUP = 0x04; 18 | 19 | //This simulates a left mouse click 20 | public static void LeftMouseClick(int xpos, int ypos) { 21 | SetCursorPos(xpos, ypos); 22 | mouse_event(MOUSEEVENTF_LEFTDOWN, xpos, ypos, 0, 0); 23 | mouse_event(MOUSEEVENTF_LEFTUP, xpos, ypos, 0, 0); 24 | } 25 | 26 | [System.Runtime.InteropServices.DllImport("user32.dll")] 27 | public static extern bool GetWindowRect(IntPtr hwnd, ref Rect rectangle); 28 | 29 | const UInt32 WM_KEYDOWN = 0x0100; 30 | const int VK_ESCAPE = 0x1B; 31 | 32 | [System.Runtime.InteropServices.DllImport("user32.dll")] 33 | public static extern bool PostMessage(IntPtr hWnd, UInt32 Msg, int wParam, int lParam); 34 | 35 | public static void SendEscapeKey(IntPtr hWnd) => PostMessage(hWnd, WM_KEYDOWN, VK_ESCAPE, 0); 36 | 37 | public static Rect GetWindowRect(IntPtr hWnd) { 38 | var windowRec = new Rect(); 39 | GetWindowRect(hWnd, ref windowRec); 40 | return windowRec; 41 | } 42 | 43 | public enum AllocationProtect : uint { 44 | PAGE_EXECUTE = 0x00000010, 45 | PAGE_EXECUTE_READ = 0x00000020, 46 | PAGE_EXECUTE_READWRITE = 0x00000040, 47 | PAGE_EXECUTE_WRITECOPY = 0x00000080, 48 | PAGE_NOACCESS = 0x00000001, 49 | PAGE_READONLY = 0x00000002, 50 | PAGE_READWRITE = 0x00000004, 51 | PAGE_WRITECOPY = 0x00000008, 52 | PAGE_GUARD = 0x00000100, 53 | PAGE_NOCACHE = 0x00000200, 54 | PAGE_WRITECOMBINE = 0x00000400 55 | } 56 | 57 | // https://docs.microsoft.com/en-au/windows/win32/memory/memory-protection-constants 58 | [Flags] 59 | public enum MemoryInformationProtection { 60 | PAGE_EXECUTE = 0x10, 61 | PAGE_EXECUTE_READ = 0x20, 62 | PAGE_EXECUTE_READWRITE = 0x40, 63 | PAGE_EXECUTE_WRITECOPY = 0x80, 64 | PAGE_NOACCESS = 0x01, 65 | PAGE_READONLY = 0x02, 66 | PAGE_READWRITE = 0x04, 67 | PAGE_WRITECOPY = 0x08, 68 | PAGE_TARGETS_INVALID = 0x40000000, 69 | PAGE_TARGETS_NO_UPDATE = 0x40000000, 70 | 71 | PAGE_GUARD = 0x100, 72 | PAGE_NOCACHE = 0x200, 73 | PAGE_WRITECOMBINE = 0x400 74 | } 75 | 76 | // https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-memory_basic_information 77 | public enum MemoryInformationState { 78 | MEM_COMMIT = 0x1000, 79 | MEM_FREE = 0x10000, 80 | MEM_RESERVE = 0x2000 81 | } 82 | 83 | // https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-memory_basic_information 84 | public enum MemoryInformationType { 85 | MEM_IMAGE = 0x1000000, 86 | MEM_MAPPED = 0x40000, 87 | MEM_PRIVATE = 0x20000 88 | } 89 | 90 | [Flags] 91 | public enum ProcessAccessFlags : uint { 92 | All = 0x001F0FFF, 93 | Terminate = 0x00000001, 94 | CreateThread = 0x00000002, 95 | VirtualMemoryOperation = 0x00000008, 96 | VirtualMemoryRead = 0x00000010, 97 | VirtualMemoryWrite = 0x00000020, 98 | DuplicateHandle = 0x00000040, 99 | CreateProcess = 0x000000080, 100 | SetQuota = 0x00000100, 101 | SetInformation = 0x00000200, 102 | QueryInformation = 0x00000400, 103 | QueryLimitedInformation = 0x00001000, 104 | Synchronize = 0x00100000 105 | } 106 | 107 | public static T Read(IntPtr processHandle, IntPtr address) where T : struct { 108 | return Read(processHandle, address, 1)[0]; 109 | } 110 | 111 | // copied from https://github.com/misterokaygo/MapAssist/ 112 | public static T[] Read(IntPtr processHandle, IntPtr address, int count) where T : struct { 113 | var size = Marshal.SizeOf(); 114 | var buffer = new byte[size * count]; 115 | 116 | ReadProcessMemory(processHandle, address, buffer, buffer.Length, out var bytesRead); 117 | 118 | var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); 119 | try { 120 | var result = new T[count]; 121 | for (var i = 0; i < count; i++) 122 | result[i] = (T)Marshal.PtrToStructure(handle.AddrOfPinnedObject() + i * size, typeof(T)); 123 | 124 | return result; 125 | } finally { 126 | handle.Free(); 127 | } 128 | } 129 | 130 | [DllImport("kernel32.dll", SetLastError = true)] 131 | public static extern IntPtr OpenProcess( 132 | uint processAccess, 133 | bool bInheritHandle, 134 | int processId 135 | ); 136 | 137 | [DllImport("kernel32.dll", SetLastError = true)] 138 | public static extern bool ReadProcessMemory( 139 | IntPtr hProcess, 140 | IntPtr lpBaseAddress, 141 | [Out] byte[] lpBuffer, 142 | int dwSize, 143 | out IntPtr lpNumberOfBytesRead); 144 | 145 | // This helper static method is required because the 32-bit version of user32.dll does not contain this API 146 | // (on any versions of Windows), so linking the method will fail at run-time. The bridge dispatches the request 147 | // to the correct function (GetWindowLong in 32-bit mode and GetWindowLongPtr in 64-bit mode) 148 | 149 | // If that doesn't work, the following signature can be used alternatively. 150 | [DllImport("user32.dll")] 151 | public static extern int SetWindowLong(IntPtr hWnd, int nIndex, uint dwNewLong); 152 | 153 | [DllImport("user32.dll", EntryPoint = "GetWindowLong")] 154 | private static extern IntPtr GetWindowLongPtr32(IntPtr hWnd, int nIndex); 155 | 156 | [DllImport("user32.dll", EntryPoint = "GetWindowLongPtr")] 157 | private static extern IntPtr GetWindowLongPtr64(IntPtr hWnd, int nIndex); 158 | 159 | // This static method is required because Win32 does not support 160 | // GetWindowLongPtr directly 161 | public static IntPtr GetWindowLongPtr(IntPtr hWnd, int nIndex) { 162 | if (IntPtr.Size == 8) 163 | return GetWindowLongPtr64(hWnd, nIndex); 164 | return GetWindowLongPtr32(hWnd, nIndex); 165 | } 166 | 167 | [DllImport("user32.dll")] 168 | [return: MarshalAs(UnmanagedType.Bool)] 169 | public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, 170 | uint uFlags); 171 | 172 | [DllImport("user32.dll")] 173 | public static extern IntPtr GetForegroundWindow(); 174 | 175 | [DllImport("kernel32.dll")] 176 | public static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId); 177 | 178 | [DllImport("kernel32.dll")] 179 | public static extern int VirtualQueryEx(IntPtr hProcess, IntPtr lpAddress, 180 | out MEMORY_BASIC_INFORMATION64 lpBuffer, uint dwLength); 181 | 182 | [DllImport("kernel32.dll")] 183 | public static extern bool ReadProcessMemory2(IntPtr hProcess, ulong lpBaseAddress, byte[] lpBuffer, 184 | UIntPtr nSize, ref UIntPtr lpNumberOfBytesRead); 185 | 186 | [DllImport("kernel32.dll", SetLastError = true)] 187 | public static extern bool CloseHandle(IntPtr hHandle); 188 | 189 | [DllImport("user32.dll", SetLastError = true)] 190 | public static extern bool SetProcessDPIAware(); 191 | 192 | [DllImport("user32.dll")] 193 | public static extern IntPtr GetClientRect(IntPtr hWnd, ref Rect rect); 194 | 195 | [DllImport("user32.dll")] 196 | [return: MarshalAs(UnmanagedType.Bool)] 197 | public static extern bool ClientToScreen(IntPtr hWnd, ref Point lpPoint); 198 | 199 | [StructLayout(LayoutKind.Sequential)] 200 | public struct Rect { 201 | public int Left { get; set; } 202 | public int Top { get; set; } 203 | public int Right { get; set; } 204 | public int Bottom { get; set; } 205 | } 206 | 207 | [StructLayout(LayoutKind.Sequential)] 208 | public struct Point { 209 | public int x; 210 | 211 | public int y; 212 | } 213 | 214 | // http://www.pinvoke.net/default.aspx/kernel32.virtualqueryex 215 | // https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-memory_basic_information 216 | [StructLayout(LayoutKind.Sequential)] 217 | public struct MEMORY_BASIC_INFORMATION64 { 218 | public ulong BaseAddress; 219 | public ulong AllocationBase; 220 | public int AllocationProtect; 221 | public int __alignment1; 222 | public ulong RegionSize; 223 | public int State; 224 | public int Protect; 225 | public int Type; 226 | public int __alignment2; 227 | } 228 | } 229 | } -------------------------------------------------------------------------------- /D.Models/D.Models.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | true 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /D.Models/StatEnum.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | // https://github.com/noah-/d2bs/blob/master/Constants.h 4 | public enum Stat : short { 5 | STAT_STRENGTH = 0, // str 6 | STAT_ENERGY = 1, // energy 7 | STAT_DEXTERITY = 2, // dexterity 8 | STAT_VITALITY = 3, // vitality 9 | STAT_STATPOINTSLEFT = 4, 10 | STAT_SKILLPOINTSLEFT = 5, 11 | STAT_HP = 6, // life 12 | STAT_MAXHP = 7, // max life 13 | STAT_MANA = 8, // mana 14 | STAT_MAXMANA = 9, // max mana 15 | STAT_STAMINA = 10, // stamina 16 | STAT_MAXSTAMINA = 11, // max stamina 17 | STAT_LEVEL = 12, // level 18 | STAT_EXP = 13, // experience 19 | STAT_GOLD = 14, // gold 20 | STAT_GOLDBANK = 15, // stash gold 21 | STAT_ENHANCEDDEFENSE = 16, 22 | STAT_ENHANCEDDAMAGEMAX = 17, // njipStats["itemmaxdamagepercent"]=[17,0]; 23 | STAT_ENHANCEDDAMAGEMIN = 18, // njipStats["itemmindamagepercent"]=[18,0]; njipStats["enhanceddamage"]=[18,0]; 24 | STAT_TOBLOCK = 20, // to block 25 | STAT_LASTEXP = 29, 26 | STAT_NEXTEXP = 30, 27 | STAT_DAMAGEREDUCTION = 36, // damage reduction 28 | STAT_MAGICDAMAGEREDUCTION = 35, // magic damage reduction 29 | STAT_MAGICRESIST = 37, // magic resist 30 | STAT_MAXMAGICRESIST = 38, // max magic resist 31 | STAT_FIRERESIST = 39, // fire resist 32 | STAT_MAXFIRERESIST = 40, // max fire resist 33 | STAT_LIGHTNINGRESIST = 41, // lightning resist 34 | STAT_MAXLIGHTNINGRESIST = 42, // max lightning resist 35 | STAT_COLDRESIST = 43, // cold resist 36 | STAT_MAXCOLDRESIST = 44, // max cold resist 37 | STAT_POISONRESIST = 45, // poison resist 38 | STAT_MAXPOISONRESIST = 46, // max poison resist 39 | STAT_LIFELEECH = 60, // Life Leech 40 | STAT_MANALEECH = 62, // Mana Leech 41 | STAT_VELOCITYPERCENT = 67, // effective run/walk 42 | STAT_AMMOQUANTITY = 70, // ammo quantity(arrow/bolt/throwing) 43 | STAT_DURABILITY = 72, // item durability 44 | STAT_MAXDURABILITY = 73, // max item durability 45 | STAT_GOLDFIND = 79, // Gold find (GF) 46 | STAT_MAGICFIND = 80, // magic find (MF) 47 | STAT_ITEMLEVELREQ = 92, 48 | STAT_IAS = 93, // IAS 49 | STAT_FASTERRUNWALK = 96, // faster run/walk 50 | STAT_FASTERHITRECOVERY = 99, // faster hit recovery 51 | STAT_FASTERBLOCK = 102, // faster block rate 52 | STAT_FASTERCAST = 105, // faster cast rate 53 | STAT_POISONLENGTHREDUCTION = 110, // Poison length reduction 54 | STAT_OPENWOUNDS = 135, // Open Wounds 55 | STAT_CRUSHINGBLOW = 136, // crushing blow 56 | STAT_DEADLYSTRIKE = 141, // deadly strike 57 | STAT_FIREABSORBPERCENT = 142, // fire absorb % 58 | STAT_FIREABSORB = 143, // fire absorb 59 | STAT_LIGHTNINGABSORBPERCENT = 144, // lightning absorb % 60 | STAT_LIGHTNINGABSORB = 145, // lightning absorb 61 | STAT_COLDABSORBPERCENT = 148, // cold absorb % 62 | STAT_COLDABSORB = 149, // cold absorb 63 | STAT_SLOW = 150, // slow % 64 | } -------------------------------------------------------------------------------- /D.Models/Structs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace D.Models { 5 | 6 | [StructLayout(LayoutKind.Explicit)] 7 | unsafe public struct UiGameMenus { 8 | [FieldOffset(0x00)] public byte IsGameMenuOpen; 9 | [FieldOffset(0x08)] public byte IsInGame; 10 | [FieldOffset(0x09)] public byte IsTrading; // not sure 11 | [FieldOffset(0x0A)] public byte StatsWindowOpen; 12 | [FieldOffset(0x0B)] public byte MouseAttackMenuOpen; 13 | [FieldOffset(0x0C)] public byte SkillTreeMenuOpen; 14 | [FieldOffset(0x10)] public byte IsTalkingToNpc; 15 | [FieldOffset(0x13)] public byte IsTrading2; // not sure 16 | [FieldOffset(0x1B)] public byte WaypointMenuOpen; 17 | [FieldOffset(0x20)] public byte ChestOpen; 18 | } 19 | 20 | [StructLayout(LayoutKind.Explicit)] 21 | unsafe public struct StatList { 22 | [FieldOffset(0x30)] public readonly StatArray Stats; 23 | [FieldOffset(0x80)] public readonly StatArray Stats2; 24 | } 25 | 26 | [StructLayout(LayoutKind.Explicit)] 27 | unsafe public struct StatArray { 28 | [FieldOffset(0x0)] public readonly IntPtr Array; 29 | [FieldOffset(0x8)] public readonly ulong Size; 30 | } 31 | 32 | [StructLayout(LayoutKind.Explicit)] 33 | public readonly struct StatValue { 34 | [FieldOffset(0x2)] public readonly Stat Stat; 35 | [FieldOffset(0x4)] public readonly int Value; 36 | } 37 | 38 | [StructLayout(LayoutKind.Explicit)] 39 | unsafe public struct UnitHashTable { 40 | [FieldOffset(0x00)] 41 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 128)] 42 | public IntPtr[] UnitTable; 43 | } 44 | 45 | [StructLayout(LayoutKind.Explicit)] 46 | unsafe public struct UnitAny { 47 | //https://github.com/noah-/d2bs#public-int-type-2 48 | [FieldOffset(0x00)] public uint UnityType; 49 | [FieldOffset(0x04)] public uint TxtFileNo; 50 | [FieldOffset(0x08)] public uint UnitId; 51 | [FieldOffset(0x10)] public IntPtr UnitData; 52 | [FieldOffset(0x20)] public IntPtr pAct; 53 | [FieldOffset(0x38)] public IntPtr pPath; 54 | [FieldOffset(0x88)] public IntPtr StatsList; 55 | [FieldOffset(0x90)] public IntPtr Inventory; 56 | [FieldOffset(0xB8)] public uint OwnerType; // ? 57 | [FieldOffset(0xC4)] public ushort X; 58 | [FieldOffset(0xC6)] public ushort Y; 59 | [FieldOffset(0x150)] public IntPtr pListNext; 60 | [FieldOffset(0x158)] public IntPtr pRoomNext; 61 | } 62 | 63 | [StructLayout(LayoutKind.Explicit)] 64 | unsafe public struct Path { 65 | [FieldOffset(0x02)] public ushort DynamicX; 66 | [FieldOffset(0x06)] public ushort DynamicY; 67 | [FieldOffset(0x10)] public ushort StaticX; 68 | [FieldOffset(0x14)] public ushort StaticY; 69 | [FieldOffset(0x20)] public IntPtr pRoom; 70 | } 71 | 72 | [StructLayout(LayoutKind.Explicit)] 73 | unsafe public struct Room { 74 | [FieldOffset(0x00)] public IntPtr pRoomsNear; 75 | [FieldOffset(0x18)] public IntPtr pRoomEx; 76 | [FieldOffset(0x40)] public uint numRoomsNear; 77 | [FieldOffset(0x48)] public IntPtr pAct; 78 | [FieldOffset(0xA8)] public IntPtr pUnitFirst; 79 | [FieldOffset(0xB0)] public IntPtr pRoomNext; 80 | } 81 | 82 | [StructLayout(LayoutKind.Explicit)] 83 | unsafe public struct RoomEx { 84 | [FieldOffset(0x90)] public IntPtr pLevel; 85 | } 86 | 87 | [StructLayout(LayoutKind.Explicit)] 88 | unsafe public struct Act { 89 | [FieldOffset(0x14)] public uint MapSeed; 90 | [FieldOffset(0x20)] public uint ActId; 91 | [FieldOffset(0x70)] public IntPtr ActMisc; 92 | } 93 | 94 | [StructLayout(LayoutKind.Explicit)] 95 | unsafe public struct ActMisc { 96 | [FieldOffset(0x830)] public Difficulty GameDifficulty; 97 | [FieldOffset(0x858)] public IntPtr pAct; 98 | [FieldOffset(0x868)] public IntPtr pLevelFirst; 99 | } 100 | 101 | public enum Difficulty : ushort { 102 | Normal = 0, 103 | Nightmare = 1, 104 | Hell = 2 105 | } 106 | 107 | [StructLayout(LayoutKind.Explicit)] 108 | unsafe public struct Level { 109 | [FieldOffset(0x1F8)] public Area LevelId; 110 | } 111 | 112 | public enum Area : uint { 113 | Abaddon = 125, 114 | AncientTunnels = 65, 115 | ArcaneSanctuary = 74, 116 | ArreatPlateau = 112, 117 | ArreatSummit = 120, 118 | Barracks = 28, 119 | BlackMarsh = 6, 120 | BloodMoor = 2, 121 | BloodyFoothills = 110, 122 | BurialGrounds = 17, 123 | CanyonOfTheMagi = 46, 124 | CatacombsLevel1 = 34, 125 | CatacombsLevel2 = 35, 126 | CatacombsLevel3 = 36, 127 | CatacombsLevel4 = 37, 128 | Cathedral = 33, 129 | CaveLevel1 = 9, 130 | CaveLevel2 = 13, 131 | ChaosSanctuary = 108, 132 | CityOfTheDamned = 106, 133 | ClawViperTempleLevel1 = 58, 134 | ClawViperTempleLevel2 = 61, 135 | ColdPlains = 3, 136 | Crypt = 18, 137 | CrystallinePassage = 113, 138 | DarkWood = 5, 139 | DenOfEvil = 8, 140 | DisusedFane = 95, 141 | DisusedReliquary = 99, 142 | DrifterCavern = 116, 143 | DryHills = 42, 144 | DuranceOfHateLevel1 = 100, 145 | DuranceOfHateLevel2 = 101, 146 | DuranceOfHateLevel3 = 102, 147 | DurielsLair = 73, 148 | FarOasis = 43, 149 | FlayerDungeonLevel1 = 88, 150 | FlayerDungeonLevel2 = 89, 151 | FlayerDungeonLevel3 = 91, 152 | FlayerJungle = 78, 153 | ForgottenReliquary = 96, 154 | ForgottenSands = 134, 155 | ForgottenTemple = 97, 156 | ForgottenTower = 20, 157 | FrigidHighlands = 111, 158 | FrozenRiver = 114, 159 | FrozenTundra = 117, 160 | FurnaceOfPain = 135, 161 | GlacialTrail = 115, 162 | GreatMarsh = 77, 163 | HallsOfAnguish = 122, 164 | HallsOfPain = 123, 165 | HallsOfTheDeadLevel1 = 56, 166 | HallsOfTheDeadLevel2 = 57, 167 | HallsOfTheDeadLevel3 = 60, 168 | HallsOfVaught = 124, 169 | HaremLevel1 = 50, 170 | HaremLevel2 = 51, 171 | Harrogath = 109, 172 | HoleLevel1 = 11, 173 | HoleLevel2 = 15, 174 | IcyCellar = 119, 175 | InfernalPit = 127, 176 | InnerCloister = 32, 177 | JailLevel1 = 29, 178 | JailLevel2 = 30, 179 | JailLevel3 = 31, 180 | KurastBazaar = 80, 181 | KurastCauseway = 82, 182 | KurastDocks = 75, 183 | LostCity = 44, 184 | LowerKurast = 79, 185 | LutGholein = 40, 186 | MaggotLairLevel1 = 62, 187 | MaggotLairLevel2 = 63, 188 | MaggotLairLevel3 = 64, 189 | MatronsDen = 133, 190 | Mausoleum = 19, 191 | MonasteryGate = 26, 192 | MooMooFarm = 39, 193 | NihlathaksTemple = 121, 194 | None = 0, 195 | OuterCloister = 27, 196 | OuterSteppes = 104, 197 | PalaceCellarLevel1 = 52, 198 | PalaceCellarLevel2 = 53, 199 | PalaceCellarLevel3 = 54, 200 | PitLevel1 = 12, 201 | PitLevel2 = 16, 202 | PitOfAcheron = 126, 203 | PlainsOfDespair = 105, 204 | RiverOfFlame = 107, 205 | RockyWaste = 41, 206 | RogueEncampment = 1, 207 | RuinedFane = 98, 208 | RuinedTemple = 94, 209 | SewersLevel1Act2 = 47, 210 | SewersLevel1Act3 = 92, 211 | SewersLevel2Act2 = 48, 212 | SewersLevel2Act3 = 93, 213 | SewersLevel3Act2 = 49, 214 | SpiderCave = 84, 215 | SpiderCavern = 85, 216 | SpiderForest = 76, 217 | StonyField = 4, 218 | StonyTombLevel1 = 55, 219 | StonyTombLevel2 = 59, 220 | SwampyPitLevel1 = 86, 221 | SwampyPitLevel2 = 87, 222 | SwampyPitLevel3 = 90, 223 | TalRashasTomb1 = 66, 224 | TalRashasTomb2 = 67, 225 | TalRashasTomb3 = 68, 226 | TalRashasTomb4 = 69, 227 | TalRashasTomb5 = 70, 228 | TalRashasTomb6 = 71, 229 | TalRashasTomb7 = 72, 230 | TamoeHighland = 7, 231 | TheAncientsWay = 118, 232 | ThePandemoniumFortress = 103, 233 | TheWorldstoneChamber = 132, 234 | TheWorldStoneKeepLevel1 = 128, 235 | TheWorldStoneKeepLevel2 = 129, 236 | TheWorldStoneKeepLevel3 = 130, 237 | ThroneOfDestruction = 131, 238 | TowerCellarLevel1 = 21, 239 | TowerCellarLevel2 = 22, 240 | TowerCellarLevel3 = 23, 241 | TowerCellarLevel4 = 24, 242 | TowerCellarLevel5 = 25, 243 | Travincal = 83, 244 | Tristram = 38, 245 | UberTristram = 136, 246 | UndergroundPassageLevel1 = 10, 247 | UndergroundPassageLevel2 = 14, 248 | UpperKurast = 81, 249 | ValleyOfSnakes = 45, 250 | MapsAncientTemple = 137, 251 | MapsDesecratedTemple = 138, 252 | MapsFrigidPlateau = 139, 253 | MapsInfernalTrial = 140, 254 | MapsRuinedCitadel = 141, 255 | } 256 | } 257 | 258 | -------------------------------------------------------------------------------- /D.WpfApp/App.xaml: -------------------------------------------------------------------------------- 1 |  6 | 7 | Status: 8 | 9 | 10 | -------------------------------------------------------------------------------- /D.WpfApp/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Configuration; 4 | using System.Data; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using System.Windows; 8 | 9 | namespace D.WpfApp { 10 | /// 11 | /// Interaction logic for App.xaml 12 | /// 13 | public partial class App : Application { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /D.WpfApp/ApplicationHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | 4 | namespace D.WpfApp { 5 | public static class ApplicationHelper { 6 | private static readonly string MyApplicationKey = "a7a0d62e-187f-46a0-8773-b32e6052cd8d"; 7 | 8 | private static Mutex _instanceMutex; 9 | 10 | // https://xcalibursystems.com/restricting-wpf-applications-run-mutex/ 11 | public static bool ReserveMutex() { 12 | // Set mutex 13 | _instanceMutex = new Mutex(true, MyApplicationKey); 14 | 15 | // Check if already running 16 | var isAlreadyInUse = false; 17 | try { 18 | isAlreadyInUse = !_instanceMutex.WaitOne(TimeSpan.Zero, true); 19 | } catch (AbandonedMutexException) { 20 | FreeMutex(); 21 | isAlreadyInUse = false; 22 | } catch (Exception) { 23 | _instanceMutex.Close(); 24 | isAlreadyInUse = false; 25 | } 26 | 27 | return isAlreadyInUse; 28 | } 29 | 30 | /// 31 | /// Kills the instance. 32 | /// 33 | /// The code. 34 | public static void FreeMutex(int code = 0) { 35 | if (_instanceMutex == null) return; 36 | 37 | try { 38 | _instanceMutex.ReleaseMutex(); 39 | } catch (Exception) { 40 | } 41 | 42 | _instanceMutex.Close(); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /D.WpfApp/D.WpfApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WinExe 5 | net6.0-windows 6 | enable 7 | true 8 | win10-x64 9 | False 10 | False 11 | app.manifest 12 | true 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ..\dependencies\Analytics.Lib.dll 23 | 24 | 25 | 26 | 27 | Always 28 | Always 29 | 30 | 31 | Always 32 | Always 33 | 34 | 35 | runtime; build; native; contentfiles; analyzers; buildtransitive 36 | all 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /D.WpfApp/GitVersion.yml: -------------------------------------------------------------------------------- 1 | mode: Mainline 2 | assembly-versioning-format: '{Major}.{Minor}.{Patch}.{env:BUILDCOUNT ?? 0}' 3 | assembly-informational-format: '{NuGetVersionV2}+Build.{env:BUILDCOUNT ?? 0}.Date.{CommitDate}.Branch.{env:SAFEBRANCHNAME ?? unknown}.Sha.{Sha}' 4 | 5 | tag-prefix: '[vV]' 6 | continuous-delivery-fallback-tag: '' # ci 7 | major-version-bump-message: '\+semver:\s?(breaking|major)' 8 | minor-version-bump-message: '\+semver:\s?(feature|minor)' 9 | patch-version-bump-message: '\+semver:\s?(fix|patch)' 10 | no-bump-message: '\+semver:\s?(none|skip)' 11 | commit-message-incrementing: Enabled 12 | branches: 13 | main: 14 | regex: ^master$|^main$ 15 | tag: '' 16 | increment: Patch 17 | prevent-increment-of-merged-branch-version: true 18 | source-branches: 19 | - develop 20 | - release 21 | tracks-release-branches: false 22 | is-release-branch: false 23 | is-mainline: true 24 | develop: 25 | regex: ^dev(elop)?(ment)?$ 26 | tag: alpha 27 | increment: Patch 28 | prevent-increment-of-merged-branch-version: false 29 | tracks-release-branches: true 30 | is-release-branch: false 31 | is-mainline: true 32 | release: 33 | regex: ^release?[/-] 34 | tag: beta 35 | increment: None 36 | prevent-increment-of-merged-branch-version: true 37 | source-branches: 38 | - develop 39 | - main 40 | - release 41 | is-release-branch: true 42 | feature: 43 | regex: ^features?[/-] 44 | tag: useBranchName 45 | increment: Inherit 46 | prevent-increment-of-merged-branch-version: false 47 | source-branches: 48 | - develop 49 | - main 50 | - release 51 | - feature 52 | - hotfix 53 | pull-request: 54 | tag: '' # PullRequest 55 | increment: Inherit 56 | prevent-increment-of-merged-branch-version: false 57 | tag-number-pattern: '[/-](?\d+)' 58 | regex: ^(pull|pull\-requests|pr)[/-] 59 | source-branches: 60 | - develop 61 | - main 62 | - release 63 | - feature 64 | - hotfix 65 | tracks-release-branches: false 66 | is-release-branch: false 67 | is-mainline: false 68 | hotfix: 69 | regex: ^hotfix(es)?[/-] 70 | tag: beta 71 | increment: Patch 72 | prevent-increment-of-merged-branch-version: false 73 | source-branches: 74 | - develop 75 | - main 76 | tracks-release-branches: false 77 | is-release-branch: false 78 | is-mainline: false 79 | ignore: 80 | sha: [] 81 | commit-date-format: yyyy-MM-dd-HHmmss 82 | merge-message-formats: {} 83 | update-build-number: true 84 | -------------------------------------------------------------------------------- /D.WpfApp/MachineStamp.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Management; 3 | using System.Reflection; 4 | using System.Security.Cryptography; 5 | using System.Text; 6 | 7 | namespace D.WpfApp { 8 | public static class MachineStamp { 9 | public static string Version { 10 | get { 11 | var assembly = Assembly.GetAssembly(typeof(MainWindow)); 12 | 13 | var gitVersionInformationType = assembly.GetType("GitVersionInformation"); 14 | // GitVersionInformation 15 | //"MajorMinorPatch"; 16 | var versionField = gitVersionInformationType.GetField("MajorMinorPatch"); 17 | if (versionField != null) { 18 | return versionField.GetValue(null).ToString(); 19 | } 20 | 21 | var versionProperty = gitVersionInformationType.GetProperty("MajorMinorPatch"); 22 | if (versionProperty != null) return versionProperty.GetGetMethod(true).Invoke(null, null).ToString(); 23 | 24 | return ""; 25 | } 26 | } 27 | 28 | public static string CreateMachineId() { 29 | var sha1 = SHA256.Create(); 30 | var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes($"{GetProcessorId()}{FecthMACAddressInternal()}")); 31 | var base64Hash = Convert.ToBase64String(hash); 32 | return base64Hash[^9..]; 33 | } 34 | 35 | public static string GetProcessorId() { 36 | var cpu = new ManagementObjectSearcher("SELECT * FROM Win32_Processor"); 37 | foreach (var cpuObj in cpu.Get()) 38 | return cpuObj["ProcessorId"]?.ToString() ?? ""; 39 | return string.Empty; 40 | } 41 | 42 | public static string FecthMACAddressInternal() { 43 | try { 44 | var macAddress = string.Empty; 45 | var networkAdapterObjs = new ManagementClass("Win32_NetworkAdapterConfiguration").GetInstances(); 46 | foreach (var networkAdapterObj in networkAdapterObjs) 47 | if (networkAdapterObj["MacAddress"] != null && 48 | bool.TryParse(networkAdapterObj["IPEnabled"]?.ToString(), out var isIpEnabled) && isIpEnabled) 49 | return networkAdapterObj["MacAddress"]?.ToString() ?? string.Empty; 50 | return ""; 51 | } catch (Exception) { 52 | return ""; 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /D.WpfApp/MainWindow.xaml: -------------------------------------------------------------------------------- 1 |  19 | 20 | 21 | 22 | 27 | 28 | 45 | 46 | 47 | 51 | 52 | 55 | 56 | 57 | 73 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 101 | 114 | 127 | 128 | 141 | 142 | 147 | 148 | 149 | 150 | 151 | 152 | 160 | 161 | 165 | 166 | 170 | 171 | -------------------------------------------------------------------------------- /D.WpfApp/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using analytics_lib; 8 | using D.Core; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.Configuration.Yaml; 11 | using Microsoft.Extensions.DependencyInjection; 12 | using Microsoft.Extensions.Logging; 13 | using Serilog; 14 | using Serilog.Events; 15 | using ILogger = Microsoft.Extensions.Logging.ILogger; 16 | 17 | namespace D.WpfApp { 18 | /// 19 | /// Interaction logic for MainWindow.xaml 20 | /// 21 | public partial class MainWindow { 22 | private static readonly string machineId = MachineStamp.CreateMachineId(); 23 | 24 | private readonly TaskScheduler _uiScheduler; 25 | private readonly AnalyticService m_AnalyticService; 26 | 27 | private readonly IServiceProvider m_ServiceProvider; 28 | private Thread botThread; 29 | private CancellationTokenSource cancellationTokenSource; 30 | private readonly ILogger logger; 31 | private IDisposable sessionSpan; 32 | 33 | public MainWindow() { 34 | _uiScheduler = TaskScheduler.FromCurrentSynchronizationContext(); 35 | InitializeComponent(); 36 | var v = MachineStamp.Version; 37 | 38 | version.Text = $"V{v}"; 39 | 40 | var builder = new ConfigurationBuilder() 41 | #if !DEBUG 42 | .SetBasePath(System.IO.Path.GetDirectoryName(Environment.ProcessPath)) 43 | #endif 44 | .AddYamlFile("config.yml", false, false); 45 | var conf = builder.Build(); 46 | 47 | var services = new ServiceCollection() 48 | .AddSingleton(conf) 49 | .AddSingleton() 50 | .AddAnalytics( 51 | (x, p) => 52 | x.WithSourceName("diablo get out bot") 53 | .WithCommonHeader(new Dictionary 54 | { 55 | {"version", v}, 56 | {"machineId", machineId} 57 | })) 58 | .AddLogging(x => { 59 | x.ClearProviders(); 60 | x.AddSerilog(new LoggerConfiguration() 61 | .MinimumLevel.Information() 62 | .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) 63 | .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) 64 | .MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Warning) 65 | .Enrich.FromLogContext() 66 | // "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"; 67 | .WriteTo.File("bot.log", rollOnFileSizeLimit: true, 68 | outputTemplate: 69 | "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}][{SourceContext}] {Message}{NewLine}{Exception}") 70 | .CreateLogger()); 71 | x.AddFilter("Microsoft", LogLevel.Warning); 72 | }); 73 | 74 | m_ServiceProvider = services.BuildServiceProvider(); 75 | logger = m_ServiceProvider.GetService().CreateLogger(); 76 | m_AnalyticService = m_ServiceProvider.InitAnalytics(); 77 | 78 | logger.LogInformation("Initialize, machineId: {@machineId}", machineId); 79 | 80 | if (!ApplicationHelper.ReserveMutex()) return; 81 | logger.LogWarning("App already launched. Exiting!"); 82 | Close(); 83 | } 84 | 85 | private void OnWindowLoaded(object sender, RoutedEventArgs e) { 86 | sessionSpan = GetOutBotAnalytic.StartSession(); 87 | GetOutBotAnalytic.SendStart(); 88 | StartBot(); 89 | } 90 | 91 | private void WindowClosing(object sender, CancelEventArgs e) { 92 | e.Cancel = true; 93 | sessionSpan?.Dispose(); 94 | m_AnalyticService.ForceFlush().GetAwaiter().GetResult(); 95 | ApplicationHelper.FreeMutex(); 96 | e.Cancel = false; 97 | } 98 | 99 | public void StartBot() { 100 | cancellationTokenSource = new CancellationTokenSource(); 101 | 102 | botThread = new Thread(() => Run(cancellationTokenSource.Token)); 103 | botThread.Start(); 104 | } 105 | 106 | public async Task Run(CancellationToken token = default) { 107 | await EnsureWindowVisible(); 108 | var uiWriter = new StateUiWriter(statusBox, additionalData, _uiScheduler); 109 | try { 110 | var bot = new GetOutBot(uiWriter, m_ServiceProvider.GetService(), 111 | new MemoryReader(m_ServiceProvider.GetService().CreateLogger()), 112 | m_ServiceProvider.GetService().CreateLogger(), 113 | cancellationTokenSource.Token); 114 | await bot.Run(); 115 | } catch (ThreadInterruptedException) { 116 | } catch (OperationCanceledException) { 117 | Console.WriteLine("Bot stopped"); 118 | } catch (Exception exception) { 119 | Console.WriteLine(exception); 120 | logger.LogError("Error {@exception}", exception); 121 | uiWriter.WriteState("Error:"); 122 | uiWriter.WriteAdditionalData(exception.Message); 123 | GetOutBotAnalytic.SendError(exception.Message); 124 | throw; 125 | } 126 | } 127 | 128 | private async Task EnsureWindowVisible() { 129 | await Task.Factory.StartNew(async () => { 130 | await Task.Delay(8000); 131 | WindowState = WindowState.Normal; 132 | Activate(); 133 | }, CancellationToken.None, TaskCreationOptions.None, _uiScheduler); 134 | } 135 | } 136 | } -------------------------------------------------------------------------------- /D.WpfApp/StateUiWriter.cs: -------------------------------------------------------------------------------- 1 | using D.Core; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using System.Threading.Tasks.Dataflow; 9 | using System.Windows.Controls; 10 | 11 | namespace D.WpfApp { 12 | internal class StateUiWriter : IStateUiWriter { 13 | 14 | private readonly TaskScheduler _uiScheduler; 15 | public readonly TextPath m_Status; 16 | public readonly TextPath m_AdditionalData; 17 | 18 | 19 | public StateUiWriter(TextPath statusBox, TextPath additionalData, TaskScheduler uiScheduler) { 20 | _uiScheduler = uiScheduler; 21 | m_Status = statusBox; 22 | m_AdditionalData = additionalData; 23 | } 24 | 25 | public void WriteState(string state) { 26 | Task.Factory.StartNew( 27 | () => { 28 | m_Status.Text = state; 29 | }, 30 | CancellationToken.None, 31 | TaskCreationOptions.None, 32 | this._uiScheduler); 33 | } 34 | 35 | public void WriteAdditionalData(string data) { 36 | Task.Factory.StartNew( 37 | () => { 38 | m_AdditionalData.Text = data; 39 | }, 40 | CancellationToken.None, 41 | TaskCreationOptions.None, 42 | this._uiScheduler); 43 | } 44 | 45 | public void Clear() { 46 | WriteState(""); 47 | WriteAdditionalData(""); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /D.WpfApp/TextPath.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Threading; 4 | using System.Windows; 5 | using System.Windows.Documents; 6 | using System.Windows.Media; 7 | using System.Windows.Shapes; 8 | 9 | namespace D.WpfApp { 10 | // https://www.codeproject.com/Articles/1106695/WPF-Text-Outline-Font 11 | public class TextPath : Shape { 12 | private Geometry _textGeometry; 13 | 14 | #region Dependency Properties 15 | public static readonly DependencyProperty FontFamilyProperty = TextElement.FontFamilyProperty.AddOwner(typeof(TextPath), 16 | new FrameworkPropertyMetadata(SystemFonts.MessageFontFamily, 17 | FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.Inherits, 18 | OnPropertyChanged)); 19 | [Bindable(true), Category("Appearance")] 20 | [Localizability(LocalizationCategory.Font)] 21 | [TypeConverter(typeof(FontFamilyConverter))] 22 | public FontFamily FontFamily { get { return (FontFamily)GetValue(FontFamilyProperty); } set { SetValue(FontFamilyProperty, value); } } 23 | 24 | public static readonly DependencyProperty FontSizeProperty = TextElement.FontSizeProperty.AddOwner(typeof(TextPath), 25 | new FrameworkPropertyMetadata(SystemFonts.MessageFontSize, 26 | FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure, 27 | OnPropertyChanged)); 28 | [Bindable(true), Category("Appearance")] 29 | [TypeConverter(typeof(FontSizeConverter))] 30 | [Localizability(LocalizationCategory.None)] 31 | public double FontSize { get { return (double)GetValue(FontSizeProperty); } set { SetValue(FontSizeProperty, value); } } 32 | 33 | public static readonly DependencyProperty FontStretchProperty = TextElement.FontStretchProperty.AddOwner(typeof(TextPath), 34 | new FrameworkPropertyMetadata(TextElement.FontStretchProperty.DefaultMetadata.DefaultValue, 35 | FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.Inherits, 36 | OnPropertyChanged)); 37 | [Bindable(true), Category("Appearance")] 38 | [TypeConverter(typeof(FontStretchConverter))] 39 | public FontStretch FontStretch { get { return (FontStretch)GetValue(FontStretchProperty); } set { SetValue(FontStretchProperty, value); } } 40 | 41 | public static readonly DependencyProperty FontStyleProperty = TextElement.FontStyleProperty.AddOwner(typeof(TextPath), 42 | new FrameworkPropertyMetadata(SystemFonts.MessageFontStyle, 43 | FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.Inherits, 44 | OnPropertyChanged)); 45 | [Bindable(true), Category("Appearance")] 46 | [TypeConverter(typeof(FontStyleConverter))] 47 | public FontStyle FontStyle { get { return (FontStyle)GetValue(FontStyleProperty); } set { SetValue(FontStyleProperty, value); } } 48 | 49 | public static readonly DependencyProperty FontWeightProperty = TextElement.FontWeightProperty.AddOwner(typeof(TextPath), 50 | new FrameworkPropertyMetadata(SystemFonts.MessageFontWeight, 51 | FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.Inherits, 52 | OnPropertyChanged)); 53 | [Bindable(true), Category("Appearance")] 54 | [TypeConverter(typeof(FontWeightConverter))] 55 | public FontWeight FontWeight { get { return (FontWeight)GetValue(FontWeightProperty); } set { SetValue(FontWeightProperty, value); } } 56 | 57 | public static readonly DependencyProperty OriginPointProperty = 58 | DependencyProperty.Register("Origin", typeof(Point), typeof(TextPath), 59 | new FrameworkPropertyMetadata(new Point(0, 0), 60 | FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure, 61 | OnPropertyChanged)); 62 | [Bindable(true), Category("Appearance")] 63 | [TypeConverter(typeof(PointConverter))] 64 | public Point Origin { get { return (Point)GetValue(OriginPointProperty); } set { SetValue(OriginPointProperty, value); } } 65 | 66 | public static readonly DependencyProperty TextProperty = 67 | DependencyProperty.Register("Text", typeof(string), typeof(TextPath), 68 | new FrameworkPropertyMetadata(string.Empty, 69 | FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure, 70 | OnPropertyChanged)); 71 | [Bindable(true), Category("Appearance")] 72 | public string Text { get { return (string)GetValue(TextProperty); } set { SetValue(TextProperty, value); } } 73 | #endregion 74 | 75 | protected override Geometry DefiningGeometry => _textGeometry ?? Geometry.Empty; 76 | 77 | private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => ((TextPath)d).CreateTextGeometry(); 78 | 79 | private void CreateTextGeometry() { 80 | var formattedText = new FormattedText(Text, Thread.CurrentThread.CurrentUICulture, FlowDirection.LeftToRight, 81 | new Typeface(FontFamily, FontStyle, FontWeight, FontStretch), FontSize, Brushes.Black); 82 | _textGeometry = formattedText.BuildGeometry(Origin); 83 | } 84 | 85 | protected override Size MeasureOverride(Size availableSize) { 86 | if (_textGeometry == null) CreateTextGeometry(); 87 | if (_textGeometry.Bounds == Rect.Empty) 88 | return new Size(0, 0); 89 | // return the desired size 90 | return new Size(Math.Min(availableSize.Width, _textGeometry.Bounds.Width), Math.Min(availableSize.Height, _textGeometry.Bounds.Height)); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /D.WpfApp/app.manifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 54 | 62 | 63 | 64 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /D.WpfApp/config.yml: -------------------------------------------------------------------------------- 1 | QuitOnHealthPercentage: 35 2 | QuitMethod: Both # Other values are: Socket, Menu 3 | # If you receive a hit that put you with less life than the hit you took then it will quit 4 | # this can increase survivability when QuitOnHealthPercentage is too low 5 | QuitOnMajorHit: true 6 | 7 | # To help diagnostic problems, i send error data so i can see if there is a problem. you can turn it off 8 | AnalyticLib: 9 | IsDisabled: false 10 | -------------------------------------------------------------------------------- /D.WpfApp/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | Rem Make powershell read this file, skip a number of lines, and execute it. 3 | Rem This works around .ps1 bad file association as non executables. 4 | PowerShell -Command "Get-Content '%~dpnx0' | Select-Object -Skip 5 | Out-String | Invoke-Expression" 5 | goto :eof 6 | 7 | # preferable to open battle.net since it will log you in 8 | $battleNetLauncherNotOpen = $null -eq (Get-Process "Battle.net" -ErrorAction SilentlyContinue) 9 | $merged = & { 10 | Get-ChildItem HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall 11 | Get-ChildItem HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall 12 | } 13 | $names = $merged |foreach-object {Get-ItemProperty $_.PsPath} 14 | 15 | Write-Host $battleNetLauncherNotOpen 16 | if($battleNetLauncherNotOpen){ 17 | foreach ($name in $names) { 18 | IF (-Not [string]::IsNullOrEmpty($name.DisplayName) -AND $name.DisplayName -eq "Battle.net") { 19 | $battleNetLauncherPath = Join-Path $name.InstallLocation "Battle.net Launcher.exe" 20 | $getOutBotPath = Resolve-Path -Relative ".\GetOutBot.exe" 21 | Start-Process -FilePath $battleNetLauncherPath 22 | Start-Process -FilePath $getOutBotPath 23 | return; 24 | } 25 | } 26 | } else { 27 | foreach ($name in $names) { 28 | IF (-Not [string]::IsNullOrEmpty($name.DisplayName) -AND $name.DisplayName -eq "Diablo II Resurrected") { 29 | $d2rPath = Join-Path $name.InstallLocation "D2R.exe" 30 | $getOutBotPath = Resolve-Path -Relative ".\GetOutBot.exe" 31 | Write-Host $d2rPath 32 | Write-Host $getOutBotPath 33 | Start-Process -FilePath $d2rPath 34 | Start-Process -FilePath $getOutBotPath 35 | return; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /GitVersion.yml: -------------------------------------------------------------------------------- 1 | mode: ContinuousDelivery 2 | branches: {} 3 | ignore: 4 | sha: [] 5 | merge-message-formats: {} 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # How it work 2 | This is a simple script that keep you alive by quiting the game before you die. 3 | It look at your life and check if it passed a threshold (default is 35% life). When it does it save&quit and kill tcp socket. 4 | 5 | This is not intented for online usage! 6 | Also i do advise testing it on a softcore character before using it on a hardcore 😁 7 | 8 | **[Download](https://github.com/GitMyCode/d2r-chicken-bot/releases)** 9 | 10 | ### Usage 11 | Click on the `run.cmd` to automaticly launch diablo 2 and the bot together. This is more convenient than having to think about launch the bot each time to launch diablo 2. You can make a shortcut from it directly to your desktop 12 | 13 | # Demo 14 | https://user-images.githubusercontent.com/3288428/144772204-4a7184a5-6f7b-45d9-b4af-5b9d27c89f51.mp4 15 | 16 | # Configuration 17 | in the folder there is `config.yml` file that should look like this: 18 | ``` 19 | QuitOnHealthPercentage: 35 20 | # If you are offline then put the value: Menu 21 | QuitMethod: Both # Other values are: Socket, Menu 22 | # If you receive a hit that put you with less life than the hit you took then it will quit 23 | # this can increase survivability when QuitOnHealthPercentage is too low 24 | QuitOnMajorHit: true 25 | ``` 26 | the number is the percentage of life at which the script will quit. 27 | 28 | # States 29 | 30 | *Looking for player* 31 | image 32 | This mean that the bot is searching for your player. If you are not in a game this is normal. If you are this mean there is a problem. 33 | 34 | *In town* 35 | image 36 | When you are safe in town you don't need to quit if your life is under the threshold. So it won't do anything in that state. 37 | 38 | *Watching you* 39 | image 40 | If you are out then it will be in that state and quit if your life goes under the threshold. 41 | 42 | 43 | # Credits 44 | credits to the nice MapAssists project where i took many piece for the offsets and learned a lot 45 | https://github.com/OneXDeveloper/MapAssist 46 | -------------------------------------------------------------------------------- /dependencies/Analytics.Lib.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitMyCode/d2r-chicken-bot/4e2ec516f82eb4f6301817feeb7be231d0a7391e/dependencies/Analytics.Lib.dll -------------------------------------------------------------------------------- /diablobot.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "D.Models", "D.Models\D.Models.csproj", "{AC848FD2-ED36-4178-9F96-36DCA3187037}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "D.WpfApp", "D.WpfApp\D.WpfApp.csproj", "{E80F4BAB-2B2C-4260-8BF6-94887A18CAA7}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "D.Core", "D.Core\D.Core.csproj", "{94116328-4A42-4437-A8AF-B382454878F4}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "D.ConsoleApp", "D.ConsoleApp\D.ConsoleApp.csproj", "{B3E31C2B-3CB1-4E64-AD3B-78EFD3E39CD6}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {AC848FD2-ED36-4178-9F96-36DCA3187037}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {AC848FD2-ED36-4178-9F96-36DCA3187037}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {AC848FD2-ED36-4178-9F96-36DCA3187037}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {AC848FD2-ED36-4178-9F96-36DCA3187037}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {E80F4BAB-2B2C-4260-8BF6-94887A18CAA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {E80F4BAB-2B2C-4260-8BF6-94887A18CAA7}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {E80F4BAB-2B2C-4260-8BF6-94887A18CAA7}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {E80F4BAB-2B2C-4260-8BF6-94887A18CAA7}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {94116328-4A42-4437-A8AF-B382454878F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {94116328-4A42-4437-A8AF-B382454878F4}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {94116328-4A42-4437-A8AF-B382454878F4}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {94116328-4A42-4437-A8AF-B382454878F4}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {B3E31C2B-3CB1-4E64-AD3B-78EFD3E39CD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {B3E31C2B-3CB1-4E64-AD3B-78EFD3E39CD6}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {B3E31C2B-3CB1-4E64-AD3B-78EFD3E39CD6}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {B3E31C2B-3CB1-4E64-AD3B-78EFD3E39CD6}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {0134F2DC-18A9-4E43-A59C-E19E1E584BC9} 42 | EndGlobalSection 43 | EndGlobal 44 | --------------------------------------------------------------------------------