├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── build-alternate-ui-frontend-html.yml │ ├── delete-old-artifacts.yml │ └── test-and-publish.yml ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── explore ├── 2019-12-28.explore-64-bit-client │ └── read-memory-64-bit │ │ ├── .gitignore │ │ ├── CommonConversion.cs │ │ ├── Program.cs │ │ ├── ZipArchive.cs │ │ └── read-memory-64-bit.csproj └── 2020-02-25-memory-reading │ └── 2020-02-25-eve-online-memory-reading-arithmetic-overflow.png ├── guide ├── how-to-collect-samples-for-64-bit-memory-reading-development.md └── image │ ├── 2015-01.eve-online-python-ui-tree-structure.png │ ├── 2015-12-13.uitree.extract.png │ ├── 2018-01-05.How-to-save-a-memory-measurement-to-a-file.gif │ ├── 2018-03-29.get-botengine-fee-credits.png │ ├── 2018-03-29.get-botengine-key.png │ ├── 2019-05-13.Sanderling-process-measurement-derivations.png │ ├── 2020-01-30.eve-online-overview-alternate-ui-and-game-client.png │ ├── 2020-03-11-eve-online-parsed-user-interface-inventory-inspect.png │ ├── 2020-07-12-visualize-ui-tree.png │ ├── 2022-10-25-eve-online-botlab-devtools-export-memory-reading-from-event-button.png │ └── Sanderling.UI.choose-target-process.png └── implement ├── alternate-ui ├── .gitignore ├── README.md ├── run-alternate-ui.ps1 └── source │ ├── elm-analyse.json │ ├── elm.json │ ├── src │ ├── Backend │ │ └── Main.elm │ ├── Common │ │ ├── App.elm │ │ └── EffectOnWindow.elm │ ├── CompilationInterface │ │ ├── ElmMake.elm │ │ ├── GenerateJsonConverters.elm │ │ └── SourceFiles.elm │ ├── EveOnline │ │ ├── MemoryReading.elm │ │ ├── ParseUserInterface.elm │ │ ├── VolatileProcess.csx │ │ └── VolatileProcessInterface.elm │ ├── Frontend │ │ ├── InspectParsedUserInterface.elm │ │ └── Main.elm │ ├── InterfaceToFrontendClient.elm │ ├── ListDict.elm │ └── Platform │ │ └── WebService.elm │ └── tests │ └── ParseMemoryReadingTest.elm └── read-memory-64-bit ├── .gitignore ├── EveOnline64.cs ├── JavaScript ├── Int64JsonConverter.cs └── UInt64JsonConverter.cs ├── MemoryReader.cs ├── ProcessSample.cs ├── Program.cs ├── Sanderling.slnx ├── WinApi.cs ├── ZipArchive.cs ├── build.bat ├── read-memory-64-bit.csproj └── readme.md /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | [*] 3 | end_of_line = lf 4 | 5 | [*.elm] 6 | indent_style = space 7 | charset = utf-8 8 | 9 | [*.cs] 10 | indent_style = space 11 | charset = utf-8 12 | 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * -text -------------------------------------------------------------------------------- /.github/workflows/build-alternate-ui-frontend-html.yml: -------------------------------------------------------------------------------- 1 | name: Build Alternate UI Frontend HTML 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | paths: 7 | - "implement/alternate-ui/**" 8 | - ".github/workflows/build-alternate-ui-frontend-html.yml" 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-24.04 14 | 15 | steps: 16 | - uses: actions/checkout@v5 17 | 18 | - name: download build tool 19 | run: | 20 | pwsh -nologo -noprofile -command "Invoke-WebRequest 'https://github.com/pine-vm/pine/releases/download/v0.4.21/pine-bin-v0.4.21-linux-x64.zip' -OutFile pine-bin-linux-x64.zip" 21 | pwsh -nologo -noprofile -command "& { Add-Type -A 'System.IO.Compression.FileSystem'; [IO.Compression.ZipFile]::ExtractToDirectory('pine-bin-linux-x64.zip','./pine');}" 22 | 23 | - name: install build tool 24 | run: | 25 | chmod +x ./pine/pine 26 | sudo ./pine/pine install 27 | 28 | - name: Build HTML 29 | working-directory: ./implement/alternate-ui/source 30 | run: | 31 | pine make src/Frontend/Main.elm --output=./eve-online-alternate-ui-${{github.sha}}.html 32 | 33 | - uses: actions/upload-artifact@v4 34 | with: 35 | name: eve-online-alternate-ui-${{github.sha}} 36 | path: ./implement/alternate-ui/source/eve-online-alternate-ui-${{github.sha}}.html 37 | -------------------------------------------------------------------------------- /.github/workflows/delete-old-artifacts.yml: -------------------------------------------------------------------------------- 1 | name: 'Delete old artifacts' 2 | on: 3 | schedule: 4 | - cron: '0 * * * *' # every hour 5 | 6 | jobs: 7 | delete-artifacts: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: kolpav/purge-artifacts-action@v1 11 | with: 12 | token: ${{ secrets.GITHUB_TOKEN }} 13 | expire-in: 14days # Setting this to 0 will delete all artifacts 14 | -------------------------------------------------------------------------------- /.github/workflows/test-and-publish.yml: -------------------------------------------------------------------------------- 1 | name: test-and-publish 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | environment: [windows-2022] 12 | include: 13 | - environment: windows-2022 14 | publish-runtime-id: win-x64 15 | 16 | runs-on: ${{ matrix.environment }} 17 | 18 | steps: 19 | - name: Avoid git mutating files on checkout 20 | run: | 21 | git config --global core.autocrlf false 22 | 23 | - uses: actions/checkout@v5 24 | 25 | - uses: actions/setup-dotnet@v5 26 | with: 27 | dotnet-version: '9.0.x' 28 | dotnet-quality: 'ga' 29 | 30 | - name: Check installed dotnet 31 | run: dotnet --info 32 | 33 | - name: Clean package cache as a temporary workaround for https://github.com/actions/setup-dotnet/issues/155 34 | run: dotnet clean ./implement/read-memory-64-bit/read-memory-64-bit.csproj && dotnet nuget locals all --clear 35 | 36 | - name: Run tests with dotnet test 37 | run: dotnet test ./implement/read-memory-64-bit/read-memory-64-bit.csproj --logger trx 38 | 39 | - name: dotnet publish - self contained single file executable 40 | env: 41 | PUBLISH_RUNTIME_ID: ${{ matrix.publish-runtime-id }} 42 | run: dotnet publish -c Release -r ${{ env.PUBLISH_RUNTIME_ID }} --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:IncludeAllContentForSelfExtract=true -p:PublishReadyToRun=true -p:PublishReadyToRunShowWarnings=true --output ./publish ./implement/read-memory-64-bit/read-memory-64-bit.csproj 43 | 44 | - name: Publish artifacts - self contained single file executable 45 | uses: actions/upload-artifact@v4 46 | with: 47 | name: read-memory-64-bit-self-contained-single-file-exe-${{github.sha}}-${{ matrix.publish-runtime-id }} 48 | path: ./publish 49 | 50 | - name: dotnet publish - separate assemblies 51 | run: dotnet publish -c Release ./implement/read-memory-64-bit/read-memory-64-bit.csproj --output ./publish-separate-assemblies 52 | 53 | - name: Publish artifacts - separate assemblies 54 | uses: actions/upload-artifact@v4 55 | with: 56 | name: read-memory-64-bit-separate-assemblies-${{github.sha}}-${{ matrix.publish-runtime-id }} 57 | path: ./publish-separate-assemblies 58 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "pine.pine" 4 | ] 5 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sanderling 2 | 3 | **Sanderling helps you read information from the [EVE Online](https://www.eveonline.com) game client using memory reading.** 4 | 5 | Sanderling is the eyes of bots and monitoring tools. It helps programs see the game client in a structured way, detecting objects and reading information about the game world. It also reads the locations of elements in the game clients' graphical user interface (e.g., in-game windows, overview entries, buttons, etc.). You can use this information to interact with the game client using mouse input. 6 | 7 |
8 | 9 | ![Visualization of data read from the EVE Online client memory.](guide/image/2020-07-12-visualize-ui-tree.png) 10 | 11 |
12 | 13 | ## Features 14 | 15 | + **safe**: does not inject into or write to the EVE Online client. That is why using it with EVE Online is not detectable. 16 | + **accurate & robust**: Sanderling uses memory reading to get information about the game state and user interface. In contrast to screen scraping, this approach won't be thrown off by a noisy background or non-default UI settings. 17 | + **comprehensive**: Sanderling memory reading is used in [mining](https://to.botlab.org/guide/app/eve-online-mining-bot), trading, mission running and [anomaly](https://to.botlab.org/guide/app/eve-online-combat-anomaly-bot) bots. 18 | 19 | ## Repository contents 20 | 21 | Below is an overview of the software components maintained in this repository. The tools and projects listed below come primarily in the form of source code, and you might have to build them from source if you want to use the latest version. For guidance on the build process(es), see the respective subdirectories containing the source codes. 22 | If you don't want to build the tools from source, you can find pre-built and ready-to-execute binaries in the [`releases` section on GitHub](https://github.com/Arcitectus/Sanderling/releases). 23 | 24 | ### Memory reading library 25 | 26 | This library implements the functionality to read from the memory of (64-bit) EVE Online client processes. It reads the UI tree from the game client user interface. It is written using the C# programming language, and the build output is the .NET Core assembly `read-memory-64-bit.dll`. 27 | 28 | Location in the repository: [/implement/read-memory-64-bit](/implement/read-memory-64-bit) 29 | 30 | ### Tool to get and save process samples and memory readings 31 | 32 | This software integrates multiple functions in a single executable file: 33 | 34 | + Saving process samples to files for later inspection, automated testing, and collaboration. This function is not specific to EVE Online but can be used with other game clients as well. 35 | + Reading the structures in the EVE Online clients' memory. You can let it read from a live process or a previously saved process sample. It also offers to save the memory reading results into a JSON file. 36 | 37 | The compiled binary is distributed in the file `read-memory-64-bit.exe`. 38 | 39 | Location in the repository: [/implement/read-memory-64-bit](/implement/read-memory-64-bit) 40 | 41 | The JSON file format here is the same one you get when exporting the memory reading using the development tools in the BotLab client. 42 | 43 | If you find the timing too tricky with the command-line interface or want to export from specific events of a larger session recording, exporting from the session timeline in BotLab is easier. After selecting an event in the bot session or play session, use the button labeled 'Export reading as JSON file' to get the memory reading in a separate file: 44 | 45 | ![Button to export EVE Online memory reading from event in a bot session](guide/image/2022-10-25-eve-online-botlab-devtools-export-memory-reading-from-event-button.png) 46 | 47 | ### Memory reading parsing library 48 | 49 | This library takes the result of an EVE Online memory reading and transforms it into a format that is easier to use for integrating applications like bots. 50 | 51 | When programming an app, we use functions to reach into the UI tree and extract the parts needed for our app. Sometimes the information we need is buried somewhere deep in the tree, contained in other nodes with redundant data. The structure we find in the UI tree is what CCP uses to build a visual representation of the game. It is not designed to be easily accessible to us, so it is not surprising to find many things there that we don't need for our applications and want to filter out. 52 | 53 | To find things faster and automatically detect program code errors, we also use types adapted to the user interface's shape. We use the type system of the Elm programming language to assign names to parts of the UI tree describe the values that we expect in certain parts of the UI. The types provide us with names more closely related to players' experience, such as the overview window or ship modules. 54 | 55 | The input for this library is the JSON string, as we get it from the memory reading. 56 | 57 | For an overview of the building blocks that you can find in the memory reading parsing library, see https://to.botlab.org/guide/parsed-user-interface-of-the-eve-online-game-client 58 | 59 | Location of the implementation in the repository: [/implement/alternate-ui/source/src/EveOnline/ParseUserInterface.elm](/implement/alternate-ui/source/src/EveOnline/ParseUserInterface.elm) 60 | 61 | ### Alternate UI for EVE Online 62 | 63 | The alternate UI is a web-based user interface for the EVE Online client. Because of the HTML based rendering, this user interface is better accessible with screen-readers. 64 | 65 | The alternate UI also lets you play the game from other devices that cannot run the EVE Online client but have a web browser. This way, you can play the game from your android smartphone or iPhone. This remote-play is possible because of the division into a frontend and backend, which communicate only via HTTP. The backend runs on the same machine as the EVE Online client and runs an HTTP server. The web-based frontend then connects to this HTTP server to read the game client's contents and send input commands. 66 | 67 | Location of the alternate UI in the repository: [/implement/alternate-ui/](/implement/alternate-ui/) 68 | 69 | ## Bots and Intel Tools 70 | 71 | The Sanderling repository contains no bots. However, developers often integrate the functionality from Sanderling in a bot to see and parse the game clients' user interface. 72 | 73 | For a guide on developing complete bots and intel apps for EVE Online (using also Sanderling + image processing), see 74 | 75 | Following is a selection of popular bots using Sanderling: 76 | 77 | + Warp-To-0 Autopilot: 78 | 79 | + Mining asteroids: 80 | 81 | + Combat anomalies: 82 | 83 | + List of EVE Online Bots for Beginners: 84 | 85 | + Up-to-date list of bots and intel-tools: 86 | 87 | ## Contributing 88 | 89 | ### Issues and other Feedback 90 | 91 | Spotted a bug or have a feature request? Post on the [BotLab forum](https://forum.botlab.org) or file an issue [on GitHub](https://github.com/Arcitectus/Sanderling/issues). 92 | -------------------------------------------------------------------------------- /explore/2019-12-28.explore-64-bit-client/read-memory-64-bit/.gitignore: -------------------------------------------------------------------------------- 1 | /obj/ 2 | /bin/ 3 | -------------------------------------------------------------------------------- /explore/2019-12-28.explore-64-bit-client/read-memory-64-bit/CommonConversion.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Security.Cryptography; 4 | 5 | namespace Kalmit 6 | { 7 | public class CommonConversion 8 | { 9 | static public byte[] ByteArrayFromStringBase16(string base16) => 10 | Enumerable.Range(0, base16.Length / 2) 11 | .Select(octetIndex => Convert.ToByte(base16.Substring(octetIndex * 2, 2), 16)) 12 | .ToArray(); 13 | 14 | static public string StringBase16FromByteArray(byte[] array) => 15 | BitConverter.ToString(array).Replace("-", "").ToUpperInvariant(); 16 | 17 | static public byte[] HashSHA256(byte[] input) 18 | { 19 | using (var hasher = new SHA256Managed()) 20 | return hasher.ComputeHash(input); 21 | } 22 | 23 | static public byte[] DecompressGzip(byte[] compressed) 24 | { 25 | using (var decompressStream = new System.IO.Compression.GZipStream( 26 | new System.IO.MemoryStream(compressed), System.IO.Compression.CompressionMode.Decompress)) 27 | { 28 | var decompressedStream = new System.IO.MemoryStream(); 29 | decompressStream.CopyTo(decompressedStream); 30 | return decompressedStream.ToArray(); 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /explore/2019-12-28.explore-64-bit-client/read-memory-64-bit/ZipArchive.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.IO; 5 | using System.Linq; 6 | 7 | namespace Kalmit 8 | { 9 | static public class ZipArchive 10 | { 11 | /// 12 | /// https://github.com/dotnet/corefx/blob/a10890f4ffe0fadf090c922578ba0e606ebdd16c/src/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs#L206-L234 13 | /// 14 | static public DateTimeOffset EntryLastWriteTimeDefault => new DateTimeOffset(1980, 1, 1, 0, 0, 0, TimeSpan.Zero); 15 | 16 | static public byte[] ZipArchiveFromEntries( 17 | IEnumerable<(string name, byte[] content)> entries, 18 | System.IO.Compression.CompressionLevel compressionLevel = System.IO.Compression.CompressionLevel.Optimal) => 19 | ZipArchiveFromEntries( 20 | entries.Select(entry => (entry.name, entry.content, EntryLastWriteTimeDefault)).ToImmutableList(), 21 | compressionLevel); 22 | 23 | static public byte[] ZipArchiveFromEntries( 24 | IReadOnlyDictionary, byte[]> entries, 25 | System.IO.Compression.CompressionLevel compressionLevel = System.IO.Compression.CompressionLevel.Optimal) => 26 | ZipArchiveFromEntries( 27 | entries.Select(entry => (name: String.Join("/", entry.Key), content: entry.Value)), 28 | compressionLevel); 29 | 30 | static public byte[] ZipArchiveFromEntries( 31 | IEnumerable<(string name, byte[] content, DateTimeOffset lastWriteTime)> entries, 32 | System.IO.Compression.CompressionLevel compressionLevel = System.IO.Compression.CompressionLevel.Optimal) 33 | { 34 | var stream = new MemoryStream(); 35 | 36 | using (var fclZipArchive = new System.IO.Compression.ZipArchive(stream, System.IO.Compression.ZipArchiveMode.Create, true)) 37 | { 38 | foreach (var (entryName, entryContent, lastWriteTime) in entries) 39 | { 40 | var entry = fclZipArchive.CreateEntry(entryName, compressionLevel); 41 | entry.LastWriteTime = lastWriteTime; 42 | using (var entryStream = entry.Open()) 43 | { 44 | entryStream.Write(entryContent, 0, entryContent.Length); 45 | } 46 | } 47 | } 48 | 49 | stream.Seek(0, SeekOrigin.Begin); 50 | 51 | var zipArchive = new byte[stream.Length]; 52 | stream.Read(zipArchive, 0, (int)stream.Length); 53 | stream.Dispose(); 54 | return zipArchive; 55 | } 56 | 57 | static public IEnumerable<(string name, byte[] content)> EntriesFromZipArchive(byte[] zipArchive) 58 | { 59 | using (var fclZipArchive = new System.IO.Compression.ZipArchive(new MemoryStream(zipArchive), System.IO.Compression.ZipArchiveMode.Read)) 60 | { 61 | foreach (var entry in fclZipArchive.Entries) 62 | { 63 | var entryContent = new byte[entry.Length]; 64 | 65 | using (var entryStream = entry.Open()) 66 | { 67 | if (entryStream.Read(entryContent, 0, entryContent.Length) != entryContent.Length) 68 | throw new NotImplementedException(); 69 | } 70 | 71 | yield return (entry.FullName, entryContent); 72 | } 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /explore/2019-12-28.explore-64-bit-client/read-memory-64-bit/read-memory-64-bit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | read_memory_64_bit 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /explore/2020-02-25-memory-reading/2020-02-25-eve-online-memory-reading-arithmetic-overflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcitectus/Sanderling/b9fbc74987b98dddc0570b155604a43db25450fc/explore/2020-02-25-memory-reading/2020-02-25-eve-online-memory-reading-arithmetic-overflow.png -------------------------------------------------------------------------------- /guide/how-to-collect-samples-for-64-bit-memory-reading-development.md: -------------------------------------------------------------------------------- 1 | # How to Collect Samples for 64-bit Memory Reading Development 2 | 3 | This guide explains how to save an example of a game client process to a file. Such examples are to support the development of memory reading frameworks or bots. 4 | 5 | The tool we use in this guide works only for 64-bit processes. 6 | 7 | The tool copies the memory contents of a chosen Windows process (such as a game client) and takes a screenshot from its main window and writes those to a file. This data is used in development to correlate screen contents with memory contents. 8 | 9 | Here you can see a typical scenario where we use this tool: https://forum.botlab.org/t/mining-bot-i-cannot-see-the-ore-hold-capacity-gauge/3101 10 | 11 | **Steps to collect a sample:** 12 | 13 | + Download and unpack the zip-archive from 14 | + Find the game client in the Windows Task Manager. 15 | + Make sure the name of the game client displayed in the Windows Task Manager does not contain `(32 bit)`. 16 | + Read the process ID of the game client process in the `PID` column in the Task Manager. 17 | + Ensure the game client window is visible and not minimized. 18 | + Use the Windows Command Prompt to run the tool, using the following command: 19 | 20 | ```cmd 21 | read-memory-64-bit.exe save-process-sample --pid=12345 22 | ``` 23 | 24 | + The tool then creates a process sample file in the directory currently selected in the Command Prompt. When successful, the program exits with a message like the following: 25 | 26 | ```cmd 27 | Saved sample F2CC4E4EC28482747A05172990F7B54CFABAA7F80C2DB83B81E86D3F41523551 to file 'process-sample-F2CC4E4EC2.zip'. 28 | ``` 29 | 30 | Since this tool does not interfere with the game client's operation, the game client can change data structures during the timespan needed to copy the memory contents. Because the memory contents are copied at different times, there is a risk of inconsistencies in the copy, which in turn can make it unusable for development. There are two ways to counter this risk: 31 | 32 | + Using more samples to increase the chance that one is good. 33 | + Reducing the time needed to copy the memory contents. You can achieve this by reducing the memory usage of the game client. Memory usage is often affected by settings, such as the resolution or quality of textures used for graphics rendering. Reducing memory use has the added benefit of smaller sample files. 34 | 35 | 36 | ### Troubleshooting 37 | 38 | #### `Parameter is not valid` in `Bitmap..ctor` 39 | 40 | ```txt 41 | Unhandled exception. System.ArgumentException: Parameter is not valid. 42 | at System.Drawing.SafeNativeMethods.Gdip.CheckStatus(Int32 status) 43 | at System.Drawing.Bitmap..ctor(Int32 width, Int32 height, PixelFormat format) 44 | at read_memory_64_bit.Program.GetScreenshotOfWindowClientAreaAsBitmap(IntPtr windowHandle) 45 | ``` 46 | 47 | This error happens when the main window of the chosen process is minimized. 48 | 49 | To avoid the error, ensure the game client window is visible and not minimized. 50 | -------------------------------------------------------------------------------- /guide/image/2015-01.eve-online-python-ui-tree-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcitectus/Sanderling/b9fbc74987b98dddc0570b155604a43db25450fc/guide/image/2015-01.eve-online-python-ui-tree-structure.png -------------------------------------------------------------------------------- /guide/image/2015-12-13.uitree.extract.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcitectus/Sanderling/b9fbc74987b98dddc0570b155604a43db25450fc/guide/image/2015-12-13.uitree.extract.png -------------------------------------------------------------------------------- /guide/image/2018-01-05.How-to-save-a-memory-measurement-to-a-file.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcitectus/Sanderling/b9fbc74987b98dddc0570b155604a43db25450fc/guide/image/2018-01-05.How-to-save-a-memory-measurement-to-a-file.gif -------------------------------------------------------------------------------- /guide/image/2018-03-29.get-botengine-fee-credits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcitectus/Sanderling/b9fbc74987b98dddc0570b155604a43db25450fc/guide/image/2018-03-29.get-botengine-fee-credits.png -------------------------------------------------------------------------------- /guide/image/2018-03-29.get-botengine-key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcitectus/Sanderling/b9fbc74987b98dddc0570b155604a43db25450fc/guide/image/2018-03-29.get-botengine-key.png -------------------------------------------------------------------------------- /guide/image/2019-05-13.Sanderling-process-measurement-derivations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcitectus/Sanderling/b9fbc74987b98dddc0570b155604a43db25450fc/guide/image/2019-05-13.Sanderling-process-measurement-derivations.png -------------------------------------------------------------------------------- /guide/image/2020-01-30.eve-online-overview-alternate-ui-and-game-client.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcitectus/Sanderling/b9fbc74987b98dddc0570b155604a43db25450fc/guide/image/2020-01-30.eve-online-overview-alternate-ui-and-game-client.png -------------------------------------------------------------------------------- /guide/image/2020-03-11-eve-online-parsed-user-interface-inventory-inspect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcitectus/Sanderling/b9fbc74987b98dddc0570b155604a43db25450fc/guide/image/2020-03-11-eve-online-parsed-user-interface-inventory-inspect.png -------------------------------------------------------------------------------- /guide/image/2020-07-12-visualize-ui-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcitectus/Sanderling/b9fbc74987b98dddc0570b155604a43db25450fc/guide/image/2020-07-12-visualize-ui-tree.png -------------------------------------------------------------------------------- /guide/image/2022-10-25-eve-online-botlab-devtools-export-memory-reading-from-event-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcitectus/Sanderling/b9fbc74987b98dddc0570b155604a43db25450fc/guide/image/2022-10-25-eve-online-botlab-devtools-export-memory-reading-from-event-button.png -------------------------------------------------------------------------------- /guide/image/Sanderling.UI.choose-target-process.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcitectus/Sanderling/b9fbc74987b98dddc0570b155604a43db25450fc/guide/image/Sanderling.UI.choose-target-process.png -------------------------------------------------------------------------------- /implement/alternate-ui/.gitignore: -------------------------------------------------------------------------------- 1 | **/elm-stuff/ 2 | /build-output/ 3 | /test-live-artifacts/ 4 | -------------------------------------------------------------------------------- /implement/alternate-ui/README.md: -------------------------------------------------------------------------------- 1 | # Alternate UI for EVE Online 2 | 3 | ![Alternate UI for EVE Online, this part shows the Overview.](./../../guide/image/2020-01-30.eve-online-overview-alternate-ui-and-game-client.png) 4 | 5 | The alternate UI is a web-based user interface for the EVE Online client. Because of the HTML based rendering, this user interface is better accessible with screen-readers. 6 | 7 | The alternate UI also lets you play the game from other devices that cannot run the EVE Online client but have a web browser. This way, you can play the game from your android smartphone or iPhone. This remote-play is possible because of the division into a frontend and backend, which communicate only via HTTP. The backend runs on the same machine as the EVE Online client and runs an HTTP server. The web-based frontend then connects to this HTTP server to read the game client's contents and send input commands. 8 | 9 | This tool also shows the UI tree from the game client and presents the properties of the UI nodes in text form. 10 | 11 | ![Alternate UI for EVE Online, Visualization of the UI tree](./../../guide/image/2020-07-12-visualize-ui-tree.png) 12 | 13 | There can be more than a thousand nodes in the UI tree, even in simple scenarios. And each of the nodes, in turn, can have many properties. So we have tens of thousands of properties in the UI tree when more objects are on the screen. 14 | 15 | This quantity might make for a confusing impression, so I introduced a way to better focus on what interests us in a given moment: In the alternate UI, we can expand and collapse individual nodes of the UI tree. For a collapsed node, it only shows a small summary, not all properties. When we get the first memory reading, all nodes are displayed collapsed, so only the summary of the root node is shown. We can expand that and then the children of the root node. This way, we can descend into the parts of the tree we want to see. 16 | 17 | ![Screenshot showing both the game client and the tree view in the alternate UI, illustrating the relations between the different representations of the same UI elements.](./../../guide/image/2020-03-11-eve-online-parsed-user-interface-inventory-inspect.png) 18 | 19 | There are two ways to get a memory reading into this interface: 20 | 21 | + Load from a live EVE Online client process. This user interface offers input elements to interact with input elements in the EVE Online client. Note: When we send an input command to the EVE Online client this way, the tool will switch the input focus to the EVE Online window and bring it to the foreground. If you run this user interface on the same desktop as the EVE Online client, place them side-by-side to avoid interference between the web browser window and the game client window so they don't overlap. 22 | 23 | + Load from a file: You can load memory readings in the JSON format you have saved earlier. Since this memory reading does not correspond to a live process, we only use this option to explore the general structure of information in the game client's memory. 24 | 25 | ## Guide on the Parsing Library and Examples 26 | 27 | Besides the program to read the UI tree from the game client, there is also a parsing library to help make sense of the raw UI tree. 28 | 29 | For a guide on the structures in the parsed memory reading, see 30 | 31 | Developers use the parsing library to make ratting, mining, and mission running bots and intel tools. Following are some links to bots and tools using the parsing library: 32 | 33 | + 34 | + 35 | 36 | ## Setup 37 | 38 | Using the PineVM runtime, we can run the alternative UI directly from the source code, loading directly from GitHub or a copy on the local file system. 39 | 40 | Download the zip archive from and extract it. 41 | 42 | The extracted files contain the `pine` tool for running Elm programs like the alternate UI. 43 | 44 | ## Usage 45 | 46 | To start the software: 47 | 48 | + Start PowerShell. 49 | + Run the `pine.exe` file from the zip archive you downloaded in the setup section. You can do this by navigating to the folder where you extracted the zip archive and then running the command `.\pine.exe`. 50 | 51 | You then might see an error message like this: 52 | 53 | ```txt 54 | You must install or update .NET to run this application. 55 | 56 | App: C:\Users\Shadow\Downloads\pine-separate-assemblies-888533a4202c45996ea9fc8563620eb628ca2768-win-x64\pine.exe 57 | Architecture: x64 58 | Framework: 'Microsoft.NETCore.App', version '9.0.0' (x64) 59 | .NET location: C:\Program Files\dotnet\ 60 | 61 | The following frameworks were found: 62 | 3.1.18 at [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] 63 | 6.0.3 at [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] 64 | 7.0.9 at [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] 65 | 66 | Learn about framework resolution: 67 | https://aka.ms/dotnet/app-launch-failed 68 | 69 | To install missing framework, download: 70 | https://aka.ms/dotnet-core-applaunch?framework=Microsoft.NETCore.App&framework_version=9.0.0&arch=x64&rid=win10-x64 71 | ``` 72 | 73 | To resolve this issue, install the .NET runtime version 9.X.X You can find the download link at 74 | 75 | + In the next command, we use the `pine.exe` file we got from the zip archive in the setup section. Below is an example of the complete command; you only need to replace the file path to the executable file: 76 | 77 | ```txt 78 | ."C:\replace-this-the-path-on-your-system\pine.exe" run-server --public-urls="http://*:80" --deploy=https://github.com/Arcitectus/Sanderling/tree/f417f147520a89b1c522fe8dcdd63c77a1d338a1/implement/alternate-ui/source 79 | ``` 80 | 81 | + The command starts a web server and the shell window will display an output like this: 82 | 83 | ```txt 84 | I got no path to a persistent store for the process. This process will not be persisted! 85 | Loading app config to deploy... 86 | This path looks like a URL into a remote git repository. Trying to load from there... 87 | This path points to commit f417f147520a89b1c522fe8dcdd63c77a1d338a1 88 | Loaded source composition ac0ea3b254b5575ec79d843886e42f5050b65a4efeaccaa3e94b5e1ded7bc083 from 'https://github.com/Arcitectus/Sanderling/tree/f417f147520a89b1c522fe8dcdd63c77a1d338a1/implement/alternate-ui/source'. 89 | Starting web server with admin interface... 90 | info: ElmTime.Platform.WebService.StartupAdminInterface[0] 91 | Begin to build the process live representation. 92 | info: ElmTime.Platform.WebService.StartupAdminInterface[0] 93 | Begin to restore the process state. 94 | info: ElmTime.Platform.WebService.StartupAdminInterface[0] 95 | Found 1 composition log records to use for restore. 96 | info: ElmTime.Platform.WebService.StartupAdminInterface[0] 97 | Restored the process state in 0 seconds. 98 | info: ElmTime.Platform.WebService.StartupAdminInterface[0] 99 | Completed building the process live representation. 100 | info: ElmTime.Platform.WebService.PublicAppState[0] 101 | I did not find 'letsEncryptOptions' in the configuration. I continue without Let's Encrypt. 102 | info: Microsoft.Hosting.Lifetime[14] 103 | Now listening on: http://[::]:80 104 | info: Microsoft.Hosting.Lifetime[0] 105 | Application started. Press Ctrl+C to shut down. 106 | info: Microsoft.Hosting.Lifetime[0] 107 | Hosting environment: Production 108 | info: ElmTime.Platform.WebService.StartupAdminInterface[0] 109 | Started the public app at 'http://[::]:80'. 110 | Completed starting the web server with the admin interface at 'http://*:4000'. 111 | ``` 112 | 113 | + As the program keeps running, it will eventually write more to the same shell window, so the last output there can become something else. 114 | + With the command above, the program will try to use network port 80 on your system. In case this network port is already in use by another process, the command fails. In this case you get an error message containing the following text: 115 | 116 | > System.IO.IOException: Failed to bind to address http://[::]:80: address already in use. 117 | 118 | After starting the web server, you don't need to look at the shell window anymore, but leave it in the background. Closing the shell window would also stop the web server process. 119 | 120 | Use a web browser (tested with Chrome and Firefox) to navigate to http://localhost:80/ 121 | There you find the Alternate EVE Online UI. 122 | 123 | At the top, you find a section titled 'Select a source for the memory reading'. Here are two radio buttons to choose between the two possible sources: 124 | 125 | + From file 126 | + From live game client process 127 | 128 | ### Reading from file 129 | 130 | Here you can load memory readings from JSON files. 131 | After loading a memory reading, you can inspect it: 132 | 133 | > Successfully read the reading from the file. Below is an interactive tree view to explore this reading. You can expand and collapse individual nodes. 134 | 135 | ### Reading from live process 136 | 137 | When reading from a live process, the system needs to perform a setup steps, including the search for the root of the UI tree in the EVE Online client process. During the setup stage you will see diverse messages informing about the current step. 138 | 139 | The memory reading setup should complete within 20 seconds. 140 | 141 | If no EVE Online client is started, it displays following message: 142 | 143 | > Looks like there is no EVE Online client process started. I continue looking in case one is started... 144 | 145 | As long as reading from live process is selected, the program tries once per seconds to get a new memory reading from the game client. 146 | 147 | When setup is complete you see following message: 148 | 149 | > Successfully read from the memory of the live process 150 | 151 | Below is a button labeled: 152 | 153 | > Click here to download this reading to a JSON file. 154 | 155 | The memory reading file you can download here is useful for collaboration: In the 'Reading from file' section, people can load this file into the UI to see the same memory reading that you had on your system. 156 | 157 | Under the save button, you get tools for closer examination of the memory reading: 158 | 159 | > Below is an interactive tree view to explore this reading. You can expand and collapse individual nodes. 160 | 161 | ### Enabling the Elm Inspection ('Debugger') Tool 162 | 163 | To use the Elm inspection tool in the frontend, open the page http://localhost:80/with-inspector instead of http://localhost:80 164 | -------------------------------------------------------------------------------- /implement/alternate-ui/run-alternate-ui.ps1: -------------------------------------------------------------------------------- 1 | 2 | pine run-server --public-urls="http://*:80" --deploy=./source/ 3 | -------------------------------------------------------------------------------- /implement/alternate-ui/source/elm-analyse.json: -------------------------------------------------------------------------------- 1 | { 2 | "checks": { 3 | "SingleFieldRecord": false, 4 | "MultiLineRecordFormatting": false, 5 | "UseConsOverConcat": false, 6 | "MapNothingToNothing": false, 7 | "ExposeAll": false 8 | } 9 | } -------------------------------------------------------------------------------- /implement/alternate-ui/source/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "danfishgold/base64-bytes": "1.1.0", 10 | "dillonkearns/elm-markdown": "7.0.1", 11 | "elm/browser": "1.0.2", 12 | "elm/bytes": "1.0.8", 13 | "elm/core": "1.0.5", 14 | "elm/file": "1.0.5", 15 | "elm/html": "1.0.0", 16 | "elm/http": "2.0.0", 17 | "elm/json": "1.1.3", 18 | "elm/regex": "1.0.0", 19 | "elm/svg": "1.0.1", 20 | "elm/time": "1.0.0", 21 | "elm/url": "1.0.0", 22 | "elm-community/list-extra": "8.7.0", 23 | "elm-community/maybe-extra": "5.3.0", 24 | "elm-community/result-extra": "2.4.0", 25 | "elm-community/string-extra": "4.0.1", 26 | "fapian/elm-html-aria": "1.4.0" 27 | }, 28 | "indirect": { 29 | "elm/parser": "1.1.0", 30 | "elm/virtual-dom": "1.0.3", 31 | "rtfeldman/elm-hex": "1.0.0" 32 | } 33 | }, 34 | "test-dependencies": { 35 | "direct": { 36 | "elm-explorations/test": "2.2.0" 37 | }, 38 | "indirect": { 39 | "elm/random": "1.0.0" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /implement/alternate-ui/source/src/Backend/Main.elm: -------------------------------------------------------------------------------- 1 | module Backend.Main exposing 2 | ( State 3 | , webServiceMain 4 | ) 5 | 6 | import Bytes 7 | import Bytes.Decode 8 | import Bytes.Encode 9 | import CompilationInterface.ElmMake 10 | import CompilationInterface.GenerateJsonConverters 11 | import CompilationInterface.SourceFiles 12 | import EveOnline.VolatileProcessInterface 13 | import InterfaceToFrontendClient 14 | import Json.Decode 15 | import Json.Encode 16 | import Platform.WebService 17 | import Url 18 | import Url.Parser 19 | 20 | 21 | type alias State = 22 | { posixTimeMilli : Int 23 | , setup : SetupState 24 | , lastTaskIndex : Int 25 | , httpRequestsTasks : List { httpRequestId : String } 26 | , log : List LogEntry 27 | } 28 | 29 | 30 | type alias SetupState = 31 | { createVolatileProcessResult : Maybe (Result String { processId : String }) 32 | , lastRunScriptResult : Maybe (Result String (Maybe String)) 33 | , eveOnlineProcessesIds : Maybe (List Int) 34 | } 35 | 36 | 37 | type alias LogEntry = 38 | { posixTimeMilli : Int 39 | , message : String 40 | } 41 | 42 | 43 | type Route 44 | = ApiRoute 45 | | FrontendWithInspectorRoute 46 | 47 | 48 | routeFromUrl : Url.Url -> Maybe Route 49 | routeFromUrl = 50 | Url.Parser.parse 51 | (Url.Parser.oneOf 52 | [ Url.Parser.map ApiRoute (Url.Parser.s "api") 53 | , Url.Parser.map FrontendWithInspectorRoute (Url.Parser.s "with-inspector") 54 | ] 55 | ) 56 | 57 | 58 | webServiceMain : Platform.WebService.WebServiceConfig State 59 | webServiceMain = 60 | { init = ( initState, [] ) 61 | , subscriptions = subscriptions 62 | } 63 | 64 | 65 | subscriptions : State -> Platform.WebService.Subscriptions State 66 | subscriptions _ = 67 | { httpRequest = updateForHttpRequestEvent 68 | , posixTimeIsPast = Nothing 69 | } 70 | 71 | 72 | initSetup : SetupState 73 | initSetup = 74 | { createVolatileProcessResult = Nothing 75 | , lastRunScriptResult = Nothing 76 | , eveOnlineProcessesIds = Nothing 77 | } 78 | 79 | 80 | maintainVolatileProcessTaskFromState : State -> Platform.WebService.Commands State 81 | maintainVolatileProcessTaskFromState state = 82 | if state.setup.createVolatileProcessResult /= Nothing then 83 | [] 84 | 85 | else 86 | [ Platform.WebService.CreateVolatileProcess 87 | { programCode = CompilationInterface.SourceFiles.file____src_EveOnline_VolatileProcess_csx.utf8 88 | , update = 89 | \createVolatileProcessResult stateBefore -> 90 | ( { stateBefore 91 | | setup = 92 | { initSetup 93 | | createVolatileProcessResult = 94 | Just (createVolatileProcessResult |> Result.mapError .exceptionToString) 95 | } 96 | } 97 | , [] 98 | ) 99 | } 100 | ] 101 | 102 | 103 | updateForHttpRequestEvent : 104 | Platform.WebService.HttpRequestEventStruct 105 | -> State 106 | -> ( State, Platform.WebService.Commands State ) 107 | updateForHttpRequestEvent httpRequestEvent stateBefore = 108 | let 109 | ( state, cmds ) = 110 | updateForHttpRequestEventWithoutVolatileProcessMaintenance httpRequestEvent stateBefore 111 | in 112 | ( state, cmds ++ maintainVolatileProcessTaskFromState state ) 113 | 114 | 115 | updateForHttpRequestEventWithoutVolatileProcessMaintenance : 116 | Platform.WebService.HttpRequestEventStruct 117 | -> State 118 | -> ( State, Platform.WebService.Commands State ) 119 | updateForHttpRequestEventWithoutVolatileProcessMaintenance httpRequestEvent stateBefore = 120 | let 121 | contentHttpHeaders { contentType, contentEncoding } = 122 | { cacheMaxAgeMinutes = Nothing 123 | , contentType = contentType 124 | , contentEncoding = contentEncoding 125 | } 126 | 127 | continueWithStaticHttpResponse httpResponse = 128 | ( stateBefore 129 | , [ Platform.WebService.RespondToHttpRequest 130 | { httpRequestId = httpRequestEvent.httpRequestId 131 | , response = httpResponse 132 | } 133 | ] 134 | ) 135 | 136 | httpResponseOkWithBodyString bodyString contentConfig = 137 | { statusCode = 200 138 | , body = 139 | bodyString 140 | |> Maybe.map encodeStringToBytes 141 | , headersToAdd = 142 | [ ( "Cache-Control" 143 | , contentConfig.cacheMaxAgeMinutes 144 | |> Maybe.map (\maxAgeMinutes -> "public, max-age=" ++ String.fromInt (maxAgeMinutes * 60)) 145 | ) 146 | , ( "Content-Type", Just contentConfig.contentType ) 147 | , ( "Content-Encoding", contentConfig.contentEncoding ) 148 | ] 149 | |> List.concatMap 150 | (\( name, maybeValue ) -> 151 | maybeValue 152 | |> Maybe.map (\value -> [ { name = name, values = [ value ] } ]) 153 | |> Maybe.withDefault [] 154 | ) 155 | } 156 | 157 | respondWithFrontendHtmlDocument { enableInspector } = 158 | httpResponseOkWithBodyString 159 | (Just 160 | (if enableInspector then 161 | CompilationInterface.ElmMake.elm_make____src_Frontend_Main_elm.debug.utf8 162 | 163 | else 164 | CompilationInterface.ElmMake.elm_make____src_Frontend_Main_elm.utf8 165 | ) 166 | ) 167 | (contentHttpHeaders { contentType = "text/html", contentEncoding = Nothing }) 168 | |> continueWithStaticHttpResponse 169 | in 170 | case httpRequestEvent.request.uri |> Url.fromString |> Maybe.andThen routeFromUrl of 171 | Nothing -> 172 | respondWithFrontendHtmlDocument { enableInspector = False } 173 | 174 | Just FrontendWithInspectorRoute -> 175 | respondWithFrontendHtmlDocument { enableInspector = True } 176 | 177 | Just ApiRoute -> 178 | -- TODO: Consolidate the different branches to reduce duplication. 179 | case 180 | httpRequestEvent.request.body 181 | |> Maybe.map (decodeBytesToString >> Maybe.withDefault "Failed to decode bytes to string") 182 | |> Maybe.withDefault "Missing HTTP body" 183 | |> Json.Decode.decodeString CompilationInterface.GenerateJsonConverters.jsonDecodeRequestFromFrontendClient 184 | of 185 | Err decodeError -> 186 | let 187 | httpResponse = 188 | { httpRequestId = httpRequestEvent.httpRequestId 189 | , response = 190 | { statusCode = 400 191 | , body = 192 | ("Failed to decode request: " ++ (decodeError |> Json.Decode.errorToString)) 193 | |> encodeStringToBytes 194 | |> Just 195 | , headersToAdd = [] 196 | } 197 | } 198 | in 199 | ( { stateBefore | posixTimeMilli = httpRequestEvent.posixTimeMilli } 200 | , [ Platform.WebService.RespondToHttpRequest httpResponse ] 201 | ) 202 | 203 | Ok requestFromClient -> 204 | case requestFromClient of 205 | InterfaceToFrontendClient.ReadLogRequest -> 206 | let 207 | httpResponse = 208 | { httpRequestId = httpRequestEvent.httpRequestId 209 | , response = 210 | { statusCode = 200 211 | , body = 212 | -- TODO: Also transmit time of log entry. 213 | (stateBefore.log |> List.map .message |> String.join "\n") 214 | |> encodeStringToBytes 215 | |> Just 216 | , headersToAdd = [] 217 | } 218 | } 219 | in 220 | ( { stateBefore | posixTimeMilli = httpRequestEvent.posixTimeMilli } 221 | , [ Platform.WebService.RespondToHttpRequest httpResponse ] 222 | ) 223 | 224 | InterfaceToFrontendClient.RunInVolatileProcessRequest runInVolatileProcessRequest -> 225 | case stateBefore.setup.createVolatileProcessResult of 226 | Just (Err createVolatileProcessErr) -> 227 | let 228 | httpResponse = 229 | { httpRequestId = httpRequestEvent.httpRequestId 230 | , response = 231 | { statusCode = 500 232 | , body = 233 | (("Failed to create volatile process: " ++ createVolatileProcessErr) 234 | |> InterfaceToFrontendClient.SetupNotCompleteResponse 235 | |> CompilationInterface.GenerateJsonConverters.jsonEncodeRunInVolatileProcessResponseStructure 236 | |> Json.Encode.encode 0 237 | ) 238 | |> encodeStringToBytes 239 | |> Just 240 | , headersToAdd = [] 241 | } 242 | } 243 | in 244 | ( { stateBefore | posixTimeMilli = httpRequestEvent.posixTimeMilli } 245 | , [ Platform.WebService.RespondToHttpRequest httpResponse ] 246 | ) 247 | 248 | Just (Ok createVolatileProcessOk) -> 249 | let 250 | httpRequestsTasks = 251 | { httpRequestId = httpRequestEvent.httpRequestId 252 | } 253 | :: stateBefore.httpRequestsTasks 254 | 255 | requestToVolatileProcessTask = 256 | Platform.WebService.RequestToVolatileProcess 257 | { processId = createVolatileProcessOk.processId 258 | , request = EveOnline.VolatileProcessInterface.buildRequestStringToGetResponseFromVolatileHost runInVolatileProcessRequest 259 | , update = 260 | \requestToVolatileProcessResult stateBeforeResult -> 261 | case requestToVolatileProcessResult of 262 | Err Platform.WebService.ProcessNotFound -> 263 | ( { stateBeforeResult 264 | | setup = initSetup 265 | } 266 | |> addLogEntry "ProcessNotFound" 267 | , [] 268 | ) 269 | 270 | Err (Platform.WebService.RequestToVolatileProcessOtherError err) -> 271 | ( { stateBeforeResult 272 | | setup = initSetup 273 | } 274 | |> addLogEntry ("RequestToVolatileProcessOtherError: " ++ err) 275 | , [] 276 | ) 277 | 278 | Ok requestToVolatileProcessOk -> 279 | processRequestToVolatileProcessComplete 280 | { httpRequestId = httpRequestEvent.httpRequestId } 281 | requestToVolatileProcessOk 282 | stateBeforeResult 283 | } 284 | in 285 | ( { stateBefore 286 | | posixTimeMilli = httpRequestEvent.posixTimeMilli 287 | , httpRequestsTasks = httpRequestsTasks 288 | , lastTaskIndex = stateBefore.lastTaskIndex + 1 289 | } 290 | , [ requestToVolatileProcessTask ] 291 | ) 292 | 293 | Nothing -> 294 | let 295 | httpResponse = 296 | { httpRequestId = httpRequestEvent.httpRequestId 297 | , response = 298 | { statusCode = 200 299 | , body = 300 | ("Volatile process not created yet." 301 | |> InterfaceToFrontendClient.SetupNotCompleteResponse 302 | |> CompilationInterface.GenerateJsonConverters.jsonEncodeRunInVolatileProcessResponseStructure 303 | |> Json.Encode.encode 0 304 | ) 305 | |> encodeStringToBytes 306 | |> Just 307 | , headersToAdd = [] 308 | } 309 | } 310 | in 311 | ( { stateBefore | posixTimeMilli = httpRequestEvent.posixTimeMilli } 312 | , [ Platform.WebService.RespondToHttpRequest httpResponse ] 313 | ) 314 | 315 | 316 | processRequestToVolatileProcessComplete : 317 | { httpRequestId : String } 318 | -> Platform.WebService.RequestToVolatileProcessComplete 319 | -> State 320 | -> ( State, Platform.WebService.Commands State ) 321 | processRequestToVolatileProcessComplete { httpRequestId } runInVolatileProcessComplete stateBefore = 322 | let 323 | httpRequestsTasks = 324 | stateBefore.httpRequestsTasks 325 | |> List.filter (.httpRequestId >> (/=) httpRequestId) 326 | 327 | httpResponseBody = 328 | runInVolatileProcessComplete 329 | |> InterfaceToFrontendClient.RunInVolatileProcessCompleteResponse 330 | |> CompilationInterface.GenerateJsonConverters.jsonEncodeRunInVolatileProcessResponseStructure 331 | >> Json.Encode.encode 0 332 | 333 | httpResponse = 334 | { httpRequestId = httpRequestId 335 | , response = 336 | { statusCode = 200 337 | , body = 338 | httpResponseBody 339 | |> encodeStringToBytes 340 | |> Just 341 | , headersToAdd = [] 342 | } 343 | } 344 | 345 | exceptionLogEntries = 346 | case runInVolatileProcessComplete.exceptionToString of 347 | Just exceptionToString -> 348 | [ "Run in volatile process failed with exception: " ++ exceptionToString ] 349 | 350 | Nothing -> 351 | [] 352 | in 353 | ( { stateBefore | httpRequestsTasks = httpRequestsTasks } 354 | |> addLogEntries exceptionLogEntries 355 | , [ Platform.WebService.RespondToHttpRequest httpResponse ] 356 | ) 357 | 358 | 359 | addLogEntry : String -> State -> State 360 | addLogEntry logMessage = 361 | addLogEntries [ logMessage ] 362 | 363 | 364 | addLogEntries : List String -> State -> State 365 | addLogEntries logMessages stateBefore = 366 | let 367 | log = 368 | (logMessages 369 | |> List.map 370 | (\logMessage -> { posixTimeMilli = stateBefore.posixTimeMilli, message = logMessage }) 371 | ) 372 | ++ stateBefore.log 373 | |> List.take 10 374 | in 375 | { stateBefore | log = log } 376 | 377 | 378 | decodeBytesToString : Bytes.Bytes -> Maybe String 379 | decodeBytesToString bytes = 380 | bytes |> Bytes.Decode.decode (Bytes.Decode.string (bytes |> Bytes.width)) 381 | 382 | 383 | encodeStringToBytes : String -> Bytes.Bytes 384 | encodeStringToBytes = 385 | Bytes.Encode.string >> Bytes.Encode.encode 386 | 387 | 388 | initState : State 389 | initState = 390 | { posixTimeMilli = 0 391 | , setup = initSetup 392 | , lastTaskIndex = 0 393 | , httpRequestsTasks = [] 394 | , log = [] 395 | } 396 | -------------------------------------------------------------------------------- /implement/alternate-ui/source/src/Common/App.elm: -------------------------------------------------------------------------------- 1 | module Common.App exposing (versionId) 2 | 3 | 4 | versionId : String 5 | versionId = 6 | "2025-10-24" 7 | -------------------------------------------------------------------------------- /implement/alternate-ui/source/src/Common/EffectOnWindow.elm: -------------------------------------------------------------------------------- 1 | module Common.EffectOnWindow exposing (..) 2 | 3 | {-| Names from 4 | -} 5 | 6 | 7 | type EffectOnWindowStructure 8 | = MouseMoveTo Location2d 9 | | KeyDown VirtualKeyCode 10 | | KeyUp VirtualKeyCode 11 | 12 | 13 | type alias MouseClickAtLocation = 14 | { location : Location2d 15 | , mouseButton : MouseButton 16 | } 17 | 18 | 19 | type alias Location2d = 20 | { x : Int, y : Int } 21 | 22 | 23 | type VirtualKeyCode 24 | = VirtualKeyCodeFromInt Int 25 | 26 | 27 | type MouseButton 28 | = MouseButtonLeft 29 | | MouseButtonRight 30 | 31 | 32 | effectsMouseClickAtLocation : MouseButton -> Location2d -> List EffectOnWindowStructure 33 | effectsMouseClickAtLocation mouseButton location = 34 | [ MouseMoveTo location 35 | , KeyDown (virtualKeyCodeFromMouseButton mouseButton) 36 | , KeyUp (virtualKeyCodeFromMouseButton mouseButton) 37 | ] 38 | 39 | 40 | effectsForDragAndDrop : { startLocation : Location2d, mouseButton : MouseButton, endLocation : Location2d } -> List EffectOnWindowStructure 41 | effectsForDragAndDrop { startLocation, mouseButton, endLocation } = 42 | [ MouseMoveTo startLocation 43 | , KeyDown (virtualKeyCodeFromMouseButton mouseButton) 44 | , MouseMoveTo endLocation 45 | , KeyUp (virtualKeyCodeFromMouseButton mouseButton) 46 | ] 47 | 48 | 49 | virtualKeyCodeFromMouseButton : MouseButton -> VirtualKeyCode 50 | virtualKeyCodeFromMouseButton mouseButton = 51 | case mouseButton of 52 | MouseButtonLeft -> 53 | vkey_LBUTTON 54 | 55 | MouseButtonRight -> 56 | vkey_RBUTTON 57 | 58 | 59 | virtualKeyCodeAsInteger : VirtualKeyCode -> Int 60 | virtualKeyCodeAsInteger keyCode = 61 | case keyCode of 62 | VirtualKeyCodeFromInt asInt -> 63 | asInt 64 | 65 | 66 | vkey_LBUTTON : VirtualKeyCode 67 | vkey_LBUTTON = 68 | VirtualKeyCodeFromInt 0x01 69 | 70 | 71 | vkey_RBUTTON : VirtualKeyCode 72 | vkey_RBUTTON = 73 | VirtualKeyCodeFromInt 0x02 74 | 75 | 76 | vkey_CANCEL : VirtualKeyCode 77 | vkey_CANCEL = 78 | VirtualKeyCodeFromInt 0x03 79 | 80 | 81 | vkey_MBUTTON : VirtualKeyCode 82 | vkey_MBUTTON = 83 | VirtualKeyCodeFromInt 0x04 84 | 85 | 86 | vkey_XBUTTON1 : VirtualKeyCode 87 | vkey_XBUTTON1 = 88 | VirtualKeyCodeFromInt 0x05 89 | 90 | 91 | vkey_XBUTTON2 : VirtualKeyCode 92 | vkey_XBUTTON2 = 93 | VirtualKeyCodeFromInt 0x06 94 | 95 | 96 | vkey_BACK : VirtualKeyCode 97 | vkey_BACK = 98 | VirtualKeyCodeFromInt 0x08 99 | 100 | 101 | vkey_TAB : VirtualKeyCode 102 | vkey_TAB = 103 | VirtualKeyCodeFromInt 0x09 104 | 105 | 106 | vkey_CLEAR : VirtualKeyCode 107 | vkey_CLEAR = 108 | VirtualKeyCodeFromInt 0x0C 109 | 110 | 111 | vkey_RETURN : VirtualKeyCode 112 | vkey_RETURN = 113 | VirtualKeyCodeFromInt 0x0D 114 | 115 | 116 | vkey_SHIFT : VirtualKeyCode 117 | vkey_SHIFT = 118 | VirtualKeyCodeFromInt 0x10 119 | 120 | 121 | vkey_CONTROL : VirtualKeyCode 122 | vkey_CONTROL = 123 | VirtualKeyCodeFromInt 0x11 124 | 125 | 126 | vkey_MENU : VirtualKeyCode 127 | vkey_MENU = 128 | VirtualKeyCodeFromInt 0x12 129 | 130 | 131 | vkey_PAUSE : VirtualKeyCode 132 | vkey_PAUSE = 133 | VirtualKeyCodeFromInt 0x13 134 | 135 | 136 | vkey_CAPITAL : VirtualKeyCode 137 | vkey_CAPITAL = 138 | VirtualKeyCodeFromInt 0x14 139 | 140 | 141 | vkey_KANA : VirtualKeyCode 142 | vkey_KANA = 143 | VirtualKeyCodeFromInt 0x15 144 | 145 | 146 | vkey_HANGUEL : VirtualKeyCode 147 | vkey_HANGUEL = 148 | VirtualKeyCodeFromInt 0x15 149 | 150 | 151 | vkey_IME_ON : VirtualKeyCode 152 | vkey_IME_ON = 153 | VirtualKeyCodeFromInt 0x16 154 | 155 | 156 | vkey_JUNJA : VirtualKeyCode 157 | vkey_JUNJA = 158 | VirtualKeyCodeFromInt 0x17 159 | 160 | 161 | vkey_FINAL : VirtualKeyCode 162 | vkey_FINAL = 163 | VirtualKeyCodeFromInt 0x18 164 | 165 | 166 | vkey_HANJA : VirtualKeyCode 167 | vkey_HANJA = 168 | VirtualKeyCodeFromInt 0x19 169 | 170 | 171 | vkey_KANJI : VirtualKeyCode 172 | vkey_KANJI = 173 | VirtualKeyCodeFromInt 0x19 174 | 175 | 176 | vkey_IME_OFF : VirtualKeyCode 177 | vkey_IME_OFF = 178 | VirtualKeyCodeFromInt 0x1A 179 | 180 | 181 | vkey_ESCAPE : VirtualKeyCode 182 | vkey_ESCAPE = 183 | VirtualKeyCodeFromInt 0x1B 184 | 185 | 186 | vkey_CONVERT : VirtualKeyCode 187 | vkey_CONVERT = 188 | VirtualKeyCodeFromInt 0x1C 189 | 190 | 191 | vkey_NONCONVERT : VirtualKeyCode 192 | vkey_NONCONVERT = 193 | VirtualKeyCodeFromInt 0x1D 194 | 195 | 196 | vkey_ACCEPT : VirtualKeyCode 197 | vkey_ACCEPT = 198 | VirtualKeyCodeFromInt 0x1E 199 | 200 | 201 | vkey_MODECHANGE : VirtualKeyCode 202 | vkey_MODECHANGE = 203 | VirtualKeyCodeFromInt 0x1F 204 | 205 | 206 | vkey_SPACE : VirtualKeyCode 207 | vkey_SPACE = 208 | VirtualKeyCodeFromInt 0x20 209 | 210 | 211 | vkey_PRIOR : VirtualKeyCode 212 | vkey_PRIOR = 213 | VirtualKeyCodeFromInt 0x21 214 | 215 | 216 | vkey_NEXT : VirtualKeyCode 217 | vkey_NEXT = 218 | VirtualKeyCodeFromInt 0x22 219 | 220 | 221 | vkey_END : VirtualKeyCode 222 | vkey_END = 223 | VirtualKeyCodeFromInt 0x23 224 | 225 | 226 | vkey_HOME : VirtualKeyCode 227 | vkey_HOME = 228 | VirtualKeyCodeFromInt 0x24 229 | 230 | 231 | vkey_LEFT : VirtualKeyCode 232 | vkey_LEFT = 233 | VirtualKeyCodeFromInt 0x25 234 | 235 | 236 | vkey_UP : VirtualKeyCode 237 | vkey_UP = 238 | VirtualKeyCodeFromInt 0x26 239 | 240 | 241 | vkey_RIGHT : VirtualKeyCode 242 | vkey_RIGHT = 243 | VirtualKeyCodeFromInt 0x27 244 | 245 | 246 | vkey_DOWN : VirtualKeyCode 247 | vkey_DOWN = 248 | VirtualKeyCodeFromInt 0x28 249 | 250 | 251 | vkey_SELECT : VirtualKeyCode 252 | vkey_SELECT = 253 | VirtualKeyCodeFromInt 0x29 254 | 255 | 256 | vkey_PRINT : VirtualKeyCode 257 | vkey_PRINT = 258 | VirtualKeyCodeFromInt 0x2A 259 | 260 | 261 | vkey_EXECUTE : VirtualKeyCode 262 | vkey_EXECUTE = 263 | VirtualKeyCodeFromInt 0x2B 264 | 265 | 266 | vkey_SNAPSHOT : VirtualKeyCode 267 | vkey_SNAPSHOT = 268 | VirtualKeyCodeFromInt 0x2C 269 | 270 | 271 | vkey_INSERT : VirtualKeyCode 272 | vkey_INSERT = 273 | VirtualKeyCodeFromInt 0x2D 274 | 275 | 276 | vkey_DELETE : VirtualKeyCode 277 | vkey_DELETE = 278 | VirtualKeyCodeFromInt 0x2E 279 | 280 | 281 | vkey_HELP : VirtualKeyCode 282 | vkey_HELP = 283 | VirtualKeyCodeFromInt 0x2F 284 | 285 | 286 | vkey_0 : VirtualKeyCode 287 | vkey_0 = 288 | VirtualKeyCodeFromInt 0x30 289 | 290 | 291 | vkey_1 : VirtualKeyCode 292 | vkey_1 = 293 | VirtualKeyCodeFromInt 0x31 294 | 295 | 296 | vkey_2 : VirtualKeyCode 297 | vkey_2 = 298 | VirtualKeyCodeFromInt 0x32 299 | 300 | 301 | vkey_3 : VirtualKeyCode 302 | vkey_3 = 303 | VirtualKeyCodeFromInt 0x33 304 | 305 | 306 | vkey_4 : VirtualKeyCode 307 | vkey_4 = 308 | VirtualKeyCodeFromInt 0x34 309 | 310 | 311 | vkey_5 : VirtualKeyCode 312 | vkey_5 = 313 | VirtualKeyCodeFromInt 0x35 314 | 315 | 316 | vkey_6 : VirtualKeyCode 317 | vkey_6 = 318 | VirtualKeyCodeFromInt 0x36 319 | 320 | 321 | vkey_7 : VirtualKeyCode 322 | vkey_7 = 323 | VirtualKeyCodeFromInt 0x37 324 | 325 | 326 | vkey_8 : VirtualKeyCode 327 | vkey_8 = 328 | VirtualKeyCodeFromInt 0x38 329 | 330 | 331 | vkey_9 : VirtualKeyCode 332 | vkey_9 = 333 | VirtualKeyCodeFromInt 0x39 334 | 335 | 336 | vkey_A : VirtualKeyCode 337 | vkey_A = 338 | VirtualKeyCodeFromInt 0x41 339 | 340 | 341 | vkey_B : VirtualKeyCode 342 | vkey_B = 343 | VirtualKeyCodeFromInt 0x42 344 | 345 | 346 | vkey_C : VirtualKeyCode 347 | vkey_C = 348 | VirtualKeyCodeFromInt 0x43 349 | 350 | 351 | vkey_D : VirtualKeyCode 352 | vkey_D = 353 | VirtualKeyCodeFromInt 0x44 354 | 355 | 356 | vkey_E : VirtualKeyCode 357 | vkey_E = 358 | VirtualKeyCodeFromInt 0x45 359 | 360 | 361 | vkey_F : VirtualKeyCode 362 | vkey_F = 363 | VirtualKeyCodeFromInt 0x46 364 | 365 | 366 | vkey_G : VirtualKeyCode 367 | vkey_G = 368 | VirtualKeyCodeFromInt 0x47 369 | 370 | 371 | vkey_H : VirtualKeyCode 372 | vkey_H = 373 | VirtualKeyCodeFromInt 0x48 374 | 375 | 376 | vkey_I : VirtualKeyCode 377 | vkey_I = 378 | VirtualKeyCodeFromInt 0x49 379 | 380 | 381 | vkey_J : VirtualKeyCode 382 | vkey_J = 383 | VirtualKeyCodeFromInt 0x4A 384 | 385 | 386 | vkey_K : VirtualKeyCode 387 | vkey_K = 388 | VirtualKeyCodeFromInt 0x4B 389 | 390 | 391 | vkey_L : VirtualKeyCode 392 | vkey_L = 393 | VirtualKeyCodeFromInt 0x4C 394 | 395 | 396 | vkey_M : VirtualKeyCode 397 | vkey_M = 398 | VirtualKeyCodeFromInt 0x4D 399 | 400 | 401 | vkey_N : VirtualKeyCode 402 | vkey_N = 403 | VirtualKeyCodeFromInt 0x4E 404 | 405 | 406 | vkey_O : VirtualKeyCode 407 | vkey_O = 408 | VirtualKeyCodeFromInt 0x4F 409 | 410 | 411 | vkey_P : VirtualKeyCode 412 | vkey_P = 413 | VirtualKeyCodeFromInt 0x50 414 | 415 | 416 | vkey_Q : VirtualKeyCode 417 | vkey_Q = 418 | VirtualKeyCodeFromInt 0x51 419 | 420 | 421 | vkey_R : VirtualKeyCode 422 | vkey_R = 423 | VirtualKeyCodeFromInt 0x52 424 | 425 | 426 | vkey_S : VirtualKeyCode 427 | vkey_S = 428 | VirtualKeyCodeFromInt 0x53 429 | 430 | 431 | vkey_T : VirtualKeyCode 432 | vkey_T = 433 | VirtualKeyCodeFromInt 0x54 434 | 435 | 436 | vkey_U : VirtualKeyCode 437 | vkey_U = 438 | VirtualKeyCodeFromInt 0x55 439 | 440 | 441 | vkey_V : VirtualKeyCode 442 | vkey_V = 443 | VirtualKeyCodeFromInt 0x56 444 | 445 | 446 | vkey_W : VirtualKeyCode 447 | vkey_W = 448 | VirtualKeyCodeFromInt 0x57 449 | 450 | 451 | vkey_X : VirtualKeyCode 452 | vkey_X = 453 | VirtualKeyCodeFromInt 0x58 454 | 455 | 456 | vkey_Y : VirtualKeyCode 457 | vkey_Y = 458 | VirtualKeyCodeFromInt 0x59 459 | 460 | 461 | vkey_Z : VirtualKeyCode 462 | vkey_Z = 463 | VirtualKeyCodeFromInt 0x5A 464 | 465 | 466 | vkey_LWIN : VirtualKeyCode 467 | vkey_LWIN = 468 | VirtualKeyCodeFromInt 0x5B 469 | 470 | 471 | vkey_RWIN : VirtualKeyCode 472 | vkey_RWIN = 473 | VirtualKeyCodeFromInt 0x5C 474 | 475 | 476 | vkey_APPS : VirtualKeyCode 477 | vkey_APPS = 478 | VirtualKeyCodeFromInt 0x5D 479 | 480 | 481 | vkey_SLEEP : VirtualKeyCode 482 | vkey_SLEEP = 483 | VirtualKeyCodeFromInt 0x5F 484 | 485 | 486 | vkey_NUMPAD0 : VirtualKeyCode 487 | vkey_NUMPAD0 = 488 | VirtualKeyCodeFromInt 0x60 489 | 490 | 491 | vkey_NUMPAD1 : VirtualKeyCode 492 | vkey_NUMPAD1 = 493 | VirtualKeyCodeFromInt 0x61 494 | 495 | 496 | vkey_NUMPAD2 : VirtualKeyCode 497 | vkey_NUMPAD2 = 498 | VirtualKeyCodeFromInt 0x62 499 | 500 | 501 | vkey_NUMPAD3 : VirtualKeyCode 502 | vkey_NUMPAD3 = 503 | VirtualKeyCodeFromInt 0x63 504 | 505 | 506 | vkey_NUMPAD4 : VirtualKeyCode 507 | vkey_NUMPAD4 = 508 | VirtualKeyCodeFromInt 0x64 509 | 510 | 511 | vkey_NUMPAD5 : VirtualKeyCode 512 | vkey_NUMPAD5 = 513 | VirtualKeyCodeFromInt 0x65 514 | 515 | 516 | vkey_NUMPAD6 : VirtualKeyCode 517 | vkey_NUMPAD6 = 518 | VirtualKeyCodeFromInt 0x66 519 | 520 | 521 | vkey_NUMPAD7 : VirtualKeyCode 522 | vkey_NUMPAD7 = 523 | VirtualKeyCodeFromInt 0x67 524 | 525 | 526 | vkey_NUMPAD8 : VirtualKeyCode 527 | vkey_NUMPAD8 = 528 | VirtualKeyCodeFromInt 0x68 529 | 530 | 531 | vkey_NUMPAD9 : VirtualKeyCode 532 | vkey_NUMPAD9 = 533 | VirtualKeyCodeFromInt 0x69 534 | 535 | 536 | vkey_MULTIPLY : VirtualKeyCode 537 | vkey_MULTIPLY = 538 | VirtualKeyCodeFromInt 0x6A 539 | 540 | 541 | vkey_ADD : VirtualKeyCode 542 | vkey_ADD = 543 | VirtualKeyCodeFromInt 0x6B 544 | 545 | 546 | vkey_SEPARATOR : VirtualKeyCode 547 | vkey_SEPARATOR = 548 | VirtualKeyCodeFromInt 0x6C 549 | 550 | 551 | vkey_SUBTRACT : VirtualKeyCode 552 | vkey_SUBTRACT = 553 | VirtualKeyCodeFromInt 0x6D 554 | 555 | 556 | vkey_DECIMAL : VirtualKeyCode 557 | vkey_DECIMAL = 558 | VirtualKeyCodeFromInt 0x6E 559 | 560 | 561 | vkey_DIVIDE : VirtualKeyCode 562 | vkey_DIVIDE = 563 | VirtualKeyCodeFromInt 0x6F 564 | 565 | 566 | vkey_F1 : VirtualKeyCode 567 | vkey_F1 = 568 | VirtualKeyCodeFromInt 0x70 569 | 570 | 571 | vkey_F2 : VirtualKeyCode 572 | vkey_F2 = 573 | VirtualKeyCodeFromInt 0x71 574 | 575 | 576 | vkey_F3 : VirtualKeyCode 577 | vkey_F3 = 578 | VirtualKeyCodeFromInt 0x72 579 | 580 | 581 | vkey_F4 : VirtualKeyCode 582 | vkey_F4 = 583 | VirtualKeyCodeFromInt 0x73 584 | 585 | 586 | vkey_F5 : VirtualKeyCode 587 | vkey_F5 = 588 | VirtualKeyCodeFromInt 0x74 589 | 590 | 591 | vkey_F6 : VirtualKeyCode 592 | vkey_F6 = 593 | VirtualKeyCodeFromInt 0x75 594 | 595 | 596 | vkey_F7 : VirtualKeyCode 597 | vkey_F7 = 598 | VirtualKeyCodeFromInt 0x76 599 | 600 | 601 | vkey_F8 : VirtualKeyCode 602 | vkey_F8 = 603 | VirtualKeyCodeFromInt 0x77 604 | 605 | 606 | vkey_F9 : VirtualKeyCode 607 | vkey_F9 = 608 | VirtualKeyCodeFromInt 0x78 609 | 610 | 611 | vkey_F10 : VirtualKeyCode 612 | vkey_F10 = 613 | VirtualKeyCodeFromInt 0x79 614 | 615 | 616 | vkey_F11 : VirtualKeyCode 617 | vkey_F11 = 618 | VirtualKeyCodeFromInt 0x7A 619 | 620 | 621 | vkey_F12 : VirtualKeyCode 622 | vkey_F12 = 623 | VirtualKeyCodeFromInt 0x7B 624 | 625 | 626 | vkey_F13 : VirtualKeyCode 627 | vkey_F13 = 628 | VirtualKeyCodeFromInt 0x7C 629 | 630 | 631 | vkey_F14 : VirtualKeyCode 632 | vkey_F14 = 633 | VirtualKeyCodeFromInt 0x7D 634 | 635 | 636 | vkey_F15 : VirtualKeyCode 637 | vkey_F15 = 638 | VirtualKeyCodeFromInt 0x7E 639 | 640 | 641 | vkey_F16 : VirtualKeyCode 642 | vkey_F16 = 643 | VirtualKeyCodeFromInt 0x7F 644 | 645 | 646 | vkey_F17 : VirtualKeyCode 647 | vkey_F17 = 648 | VirtualKeyCodeFromInt 0x80 649 | 650 | 651 | vkey_F18 : VirtualKeyCode 652 | vkey_F18 = 653 | VirtualKeyCodeFromInt 0x81 654 | 655 | 656 | vkey_F19 : VirtualKeyCode 657 | vkey_F19 = 658 | VirtualKeyCodeFromInt 0x82 659 | 660 | 661 | vkey_F20 : VirtualKeyCode 662 | vkey_F20 = 663 | VirtualKeyCodeFromInt 0x83 664 | 665 | 666 | vkey_F21 : VirtualKeyCode 667 | vkey_F21 = 668 | VirtualKeyCodeFromInt 0x84 669 | 670 | 671 | vkey_F22 : VirtualKeyCode 672 | vkey_F22 = 673 | VirtualKeyCodeFromInt 0x85 674 | 675 | 676 | vkey_F23 : VirtualKeyCode 677 | vkey_F23 = 678 | VirtualKeyCodeFromInt 0x86 679 | 680 | 681 | vkey_F24 : VirtualKeyCode 682 | vkey_F24 = 683 | VirtualKeyCodeFromInt 0x87 684 | 685 | 686 | vkey_NUMLOCK : VirtualKeyCode 687 | vkey_NUMLOCK = 688 | VirtualKeyCodeFromInt 0x90 689 | 690 | 691 | vkey_SCROLL : VirtualKeyCode 692 | vkey_SCROLL = 693 | VirtualKeyCodeFromInt 0x91 694 | 695 | 696 | vkey_LSHIFT : VirtualKeyCode 697 | vkey_LSHIFT = 698 | VirtualKeyCodeFromInt 0xA0 699 | 700 | 701 | vkey_RSHIFT : VirtualKeyCode 702 | vkey_RSHIFT = 703 | VirtualKeyCodeFromInt 0xA1 704 | 705 | 706 | vkey_LCONTROL : VirtualKeyCode 707 | vkey_LCONTROL = 708 | VirtualKeyCodeFromInt 0xA2 709 | 710 | 711 | vkey_RCONTROL : VirtualKeyCode 712 | vkey_RCONTROL = 713 | VirtualKeyCodeFromInt 0xA3 714 | 715 | 716 | vkey_LMENU : VirtualKeyCode 717 | vkey_LMENU = 718 | VirtualKeyCodeFromInt 0xA4 719 | 720 | 721 | vkey_RMENU : VirtualKeyCode 722 | vkey_RMENU = 723 | VirtualKeyCodeFromInt 0xA5 724 | 725 | 726 | vkey_BROWSER_BACK : VirtualKeyCode 727 | vkey_BROWSER_BACK = 728 | VirtualKeyCodeFromInt 0xA6 729 | 730 | 731 | vkey_BROWSER_FORWARD : VirtualKeyCode 732 | vkey_BROWSER_FORWARD = 733 | VirtualKeyCodeFromInt 0xA7 734 | 735 | 736 | vkey_BROWSER_REFRESH : VirtualKeyCode 737 | vkey_BROWSER_REFRESH = 738 | VirtualKeyCodeFromInt 0xA8 739 | 740 | 741 | vkey_BROWSER_STOP : VirtualKeyCode 742 | vkey_BROWSER_STOP = 743 | VirtualKeyCodeFromInt 0xA9 744 | 745 | 746 | vkey_BROWSER_SEARCH : VirtualKeyCode 747 | vkey_BROWSER_SEARCH = 748 | VirtualKeyCodeFromInt 0xAA 749 | 750 | 751 | vkey_BROWSER_FAVORITES : VirtualKeyCode 752 | vkey_BROWSER_FAVORITES = 753 | VirtualKeyCodeFromInt 0xAB 754 | 755 | 756 | vkey_BROWSER_HOME : VirtualKeyCode 757 | vkey_BROWSER_HOME = 758 | VirtualKeyCodeFromInt 0xAC 759 | 760 | 761 | vkey_VOLUME_MUTE : VirtualKeyCode 762 | vkey_VOLUME_MUTE = 763 | VirtualKeyCodeFromInt 0xAD 764 | 765 | 766 | vkey_VOLUME_DOWN : VirtualKeyCode 767 | vkey_VOLUME_DOWN = 768 | VirtualKeyCodeFromInt 0xAE 769 | 770 | 771 | vkey_VOLUME_UP : VirtualKeyCode 772 | vkey_VOLUME_UP = 773 | VirtualKeyCodeFromInt 0xAF 774 | -------------------------------------------------------------------------------- /implement/alternate-ui/source/src/CompilationInterface/ElmMake.elm: -------------------------------------------------------------------------------- 1 | module CompilationInterface.ElmMake exposing (..) 2 | 3 | {-| For documentation of the compilation interface, see 4 | -} 5 | 6 | import Basics 7 | 8 | 9 | elm_make____src_Frontend_Main_elm : { debug : { utf8 : String }, utf8 : String } 10 | elm_make____src_Frontend_Main_elm = 11 | { utf8 = "The compiler replaces this declaration." 12 | , debug = { utf8 = "The compiler replaces this declaration." } 13 | } 14 | -------------------------------------------------------------------------------- /implement/alternate-ui/source/src/CompilationInterface/GenerateJsonConverters.elm: -------------------------------------------------------------------------------- 1 | module CompilationInterface.GenerateJsonConverters exposing (..) 2 | 3 | import InterfaceToFrontendClient 4 | import Json.Decode 5 | import Json.Encode 6 | 7 | 8 | jsonEncodeRequestFromFrontendClient : InterfaceToFrontendClient.RequestFromClient -> Json.Encode.Value 9 | jsonEncodeRequestFromFrontendClient = 10 | always (Json.Encode.string "The compiler replaces this function.") 11 | 12 | 13 | jsonDecodeRequestFromFrontendClient : Json.Decode.Decoder InterfaceToFrontendClient.RequestFromClient 14 | jsonDecodeRequestFromFrontendClient = 15 | Json.Decode.fail "The compiler replaces this function." 16 | 17 | 18 | jsonEncodeRunInVolatileProcessResponseStructure : InterfaceToFrontendClient.RunInVolatileProcessResponseStructure -> Json.Encode.Value 19 | jsonEncodeRunInVolatileProcessResponseStructure = 20 | always (Json.Encode.string "The compiler replaces this function.") 21 | 22 | 23 | jsonDecodeRunInVolatileProcessResponseStructure : Json.Decode.Decoder InterfaceToFrontendClient.RunInVolatileProcessResponseStructure 24 | jsonDecodeRunInVolatileProcessResponseStructure = 25 | Json.Decode.fail "The compiler replaces this function." 26 | -------------------------------------------------------------------------------- /implement/alternate-ui/source/src/CompilationInterface/SourceFiles.elm: -------------------------------------------------------------------------------- 1 | module CompilationInterface.SourceFiles exposing (..) 2 | 3 | {-| For documentation of the compilation interface, see 4 | -} 5 | 6 | import Basics 7 | 8 | 9 | file____src_EveOnline_VolatileProcess_csx : { utf8 : String } 10 | file____src_EveOnline_VolatileProcess_csx = 11 | { utf8 = "The compiler replaces this declaration." } 12 | -------------------------------------------------------------------------------- /implement/alternate-ui/source/src/EveOnline/MemoryReading.elm: -------------------------------------------------------------------------------- 1 | module EveOnline.MemoryReading exposing (..) 2 | 3 | {-| This module contains: 4 | 5 | - Types to represent memory readings from the Sanderling project. 6 | - Decoders for the popular JSON representation of Sanderling memory readings. 7 | 8 | To learn more about Sanderling, see 9 | 10 | -} 11 | 12 | import Dict 13 | import Json.Decode 14 | import Json.Encode 15 | 16 | 17 | type alias UITreeNode = 18 | { originalJson : Json.Encode.Value 19 | , pythonObjectAddress : String 20 | , pythonObjectTypeName : String 21 | , dictEntriesOfInterest : Dict.Dict String Json.Encode.Value 22 | , children : Maybe (List UITreeNodeChild) 23 | } 24 | 25 | 26 | type UITreeNodeChild 27 | = UITreeNodeChild UITreeNode 28 | 29 | 30 | unwrapUITreeNodeChild : UITreeNodeChild -> UITreeNode 31 | unwrapUITreeNodeChild child = 32 | case child of 33 | UITreeNodeChild node -> 34 | node 35 | 36 | 37 | countDescendantsInUITreeNode : UITreeNode -> Int 38 | countDescendantsInUITreeNode parent = 39 | parent.children 40 | |> Maybe.withDefault [] 41 | |> List.map unwrapUITreeNodeChild 42 | |> List.map (countDescendantsInUITreeNode >> (+) 1) 43 | |> List.sum 44 | 45 | 46 | listDescendantsInUITreeNode : UITreeNode -> List UITreeNode 47 | listDescendantsInUITreeNode parent = 48 | parent.children 49 | |> Maybe.withDefault [] 50 | |> List.map unwrapUITreeNodeChild 51 | |> List.concatMap (\child -> child :: listDescendantsInUITreeNode child) 52 | 53 | 54 | decodeMemoryReadingFromString : String -> Result Json.Decode.Error UITreeNode 55 | decodeMemoryReadingFromString = 56 | Json.Decode.decodeString uiTreeNodeDecoder 57 | 58 | 59 | uiTreeNodeDecoder : Json.Decode.Decoder UITreeNode 60 | uiTreeNodeDecoder = 61 | Json.Decode.map5 62 | (\originalJson pythonObjectAddress pythonObjectTypeName dictEntriesOfInterest children -> 63 | { originalJson = originalJson 64 | , pythonObjectAddress = pythonObjectAddress 65 | , pythonObjectTypeName = pythonObjectTypeName 66 | , dictEntriesOfInterest = dictEntriesOfInterest |> Dict.fromList 67 | , children = children |> Maybe.map (List.map UITreeNodeChild) 68 | } 69 | ) 70 | Json.Decode.value 71 | (Json.Decode.field "pythonObjectAddress" Json.Decode.string) 72 | (decodeOptionalField "pythonObjectTypeName" Json.Decode.string |> Json.Decode.map (Maybe.withDefault "")) 73 | (Json.Decode.field "dictEntriesOfInterest" (Json.Decode.keyValuePairs Json.Decode.value)) 74 | (decodeOptionalOrNullField "children" (Json.Decode.list (Json.Decode.lazy (\_ -> uiTreeNodeDecoder)))) 75 | 76 | 77 | decodeOptionalOrNullField : String -> Json.Decode.Decoder a -> Json.Decode.Decoder (Maybe a) 78 | decodeOptionalOrNullField fieldName decoder = 79 | decodeOptionalField fieldName (Json.Decode.nullable decoder) 80 | |> Json.Decode.map (Maybe.andThen identity) 81 | 82 | 83 | decodeOptionalField : String -> Json.Decode.Decoder a -> Json.Decode.Decoder (Maybe a) 84 | decodeOptionalField fieldName decoder = 85 | let 86 | finishDecoding json = 87 | case Json.Decode.decodeValue (Json.Decode.field fieldName Json.Decode.value) json of 88 | Ok _ -> 89 | -- The field is present, so run the decoder on it. 90 | Json.Decode.map Just (Json.Decode.field fieldName decoder) 91 | 92 | Err _ -> 93 | -- The field was missing, which is fine! 94 | Json.Decode.succeed Nothing 95 | in 96 | Json.Decode.value 97 | |> Json.Decode.andThen finishDecoding 98 | -------------------------------------------------------------------------------- /implement/alternate-ui/source/src/EveOnline/VolatileProcess.csx: -------------------------------------------------------------------------------- 1 | #r "sha256:FE8A38EBCED27A112519023A7A1216C69FE0863BCA3EF766234E972E920096C1" 2 | #r "sha256:5229128932E6AAFB5433B7AA5E05E6AFA3C19A929897E49F83690AB8FE273162" 3 | #r "sha256:CADE001866564D185F14798ECFD077EDA6415E69D978748C19B98DDF0EE839BB" 4 | #r "sha256:831EF0489D9FA85C34C95F0670CC6393D1AD9548EE708E223C1AD87B51F7C7B3" 5 | #r "sha256:B9B4E633EA6C728BAD5F7CBBEF7F8B842F7E10181731DBE5EC3CD995A6F60287" 6 | #r "sha256:81110D44256397F0F3C572A20CA94BB4C669E5DE89F9348ABAD263FBD81C54B9" 7 | 8 | // https://github.com/Arcitectus/Sanderling/releases/download/v2025-10-24/read-memory-64-bit-separate-assemblies-594a2339a63d7e946872a77c0d5772acdf75bd98-win-x64.zip 9 | #r "sha256:b1cb3048db6b5be1016c3ef97f7054a99643a2e8376654b4964aada0669bc472" 10 | 11 | #r "mscorlib" 12 | #r "netstandard" 13 | #r "System" 14 | #r "System.Collections.Immutable" 15 | #r "System.ComponentModel.Primitives" 16 | #r "System.IO.Compression" 17 | #r "System.Net" 18 | #r "System.Net.WebClient" 19 | #r "System.Private.Uri" 20 | #r "System.Linq" 21 | #r "System.Security.Cryptography.Algorithms" 22 | #r "System.Security.Cryptography.Primitives" 23 | 24 | // "System.Drawing.Common" 25 | // https://www.nuget.org/api/v2/package/System.Drawing.Common/9.0.4 26 | #r "sha256:144bc126a785601c27754cde054c2423179ebca3f734dac2b0e98738f3b59bee" 27 | 28 | // "System.Drawing.Primitives" 29 | #r "sha256:CA24032E6D39C44A01D316498E18FE9A568D59C6009842029BC129AA6B989BCD" 30 | 31 | using System; 32 | using System.Collections.Generic; 33 | using System.Collections.Immutable; 34 | using System.Linq; 35 | using System.Security.Cryptography; 36 | using System.Runtime.InteropServices; 37 | 38 | 39 | int readingFromGameCount = 0; 40 | static var generalStopwatch = System.Diagnostics.Stopwatch.StartNew(); 41 | 42 | var readingFromGameHistory = new Queue(); 43 | 44 | 45 | string ToStringBase16(byte[] array) => BitConverter.ToString(array).Replace("-", ""); 46 | 47 | 48 | var searchUIRootAddressTasks = new Dictionary(); 49 | 50 | class SearchUIRootAddressTask 51 | { 52 | public Request.SearchUIRootAddressStructure request; 53 | 54 | public TimeSpan beginTime; 55 | 56 | public Response.SearchUIRootAddressCompletedStruct completed; 57 | 58 | public SearchUIRootAddressTask(Request.SearchUIRootAddressStructure request) 59 | { 60 | this.request = request; 61 | beginTime = generalStopwatch.Elapsed; 62 | 63 | System.Threading.Tasks.Task.Run(() => 64 | { 65 | var uiTreeRootAddress = FindUIRootAddressFromProcessId(request.processId); 66 | 67 | completed = new Response.SearchUIRootAddressCompletedStruct 68 | { 69 | uiRootAddress = uiTreeRootAddress?.ToString() 70 | }; 71 | }); 72 | } 73 | } 74 | 75 | struct ReadingFromGameClient 76 | { 77 | public IntPtr windowHandle; 78 | 79 | public string readingId; 80 | } 81 | 82 | class Request 83 | { 84 | public object ListGameClientProcessesRequest; 85 | 86 | public SearchUIRootAddressStructure SearchUIRootAddress; 87 | 88 | public ReadFromWindowStructure ReadFromWindow; 89 | 90 | public TaskOnWindow EffectSequenceOnWindow; 91 | 92 | public class SearchUIRootAddressStructure 93 | { 94 | public int processId; 95 | } 96 | 97 | public class ReadFromWindowStructure 98 | { 99 | public string windowId; 100 | 101 | public ulong uiRootAddress; 102 | } 103 | 104 | public class TaskOnWindow 105 | { 106 | public string windowId; 107 | 108 | public bool bringWindowToForeground; 109 | 110 | public Task task; 111 | } 112 | 113 | public class EffectSequenceElement 114 | { 115 | public EffectOnWindowStructure effect; 116 | 117 | public int? delayMilliseconds; 118 | } 119 | 120 | public class EffectOnWindowStructure 121 | { 122 | public MouseMoveToStructure MouseMoveTo; 123 | 124 | public KeyboardKey KeyDown; 125 | 126 | public KeyboardKey KeyUp; 127 | } 128 | 129 | public class KeyboardKey 130 | { 131 | public int virtualKeyCode; 132 | } 133 | 134 | public class MouseMoveToStructure 135 | { 136 | public Location2d location; 137 | } 138 | 139 | public enum MouseButton 140 | { 141 | left, right, 142 | } 143 | } 144 | 145 | class Response 146 | { 147 | public GameClientProcessSummaryStruct[] ListGameClientProcessesResponse; 148 | 149 | public SearchUIRootAddressResponseStruct SearchUIRootAddressResponse; 150 | 151 | public ReadFromWindowResultStructure ReadFromWindowResult; 152 | 153 | public string FailedToBringWindowToFront; 154 | 155 | public object CompletedEffectSequenceOnWindow; 156 | 157 | public object CompletedOtherEffect; 158 | 159 | public class GameClientProcessSummaryStruct 160 | { 161 | public int processId; 162 | 163 | public string mainWindowId; 164 | 165 | public string mainWindowTitle; 166 | 167 | public int mainWindowZIndex; 168 | } 169 | 170 | public class SearchUIRootAddressResponseStruct 171 | { 172 | public int processId; 173 | 174 | public SearchUIRootAddressStage stage; 175 | } 176 | 177 | 178 | public class SearchUIRootAddressStage 179 | { 180 | public SearchUIRootAddressInProgressStruct SearchUIRootAddressInProgress; 181 | 182 | public SearchUIRootAddressCompletedStruct SearchUIRootAddressCompleted; 183 | } 184 | 185 | public class SearchUIRootAddressInProgressStruct 186 | { 187 | public long searchBeginTimeMilliseconds; 188 | 189 | public long currentTimeMilliseconds; 190 | } 191 | 192 | 193 | public class SearchUIRootAddressCompletedStruct 194 | { 195 | public string uiRootAddress; 196 | } 197 | 198 | public class ReadFromWindowResultStructure 199 | { 200 | public object ProcessNotFound; 201 | 202 | public CompletedStructure Completed; 203 | 204 | public class CompletedStructure 205 | { 206 | public int processId; 207 | 208 | public Location2d windowClientRectOffset; 209 | 210 | public string readingId; 211 | 212 | public string memoryReadingSerialRepresentationJson; 213 | } 214 | } 215 | } 216 | 217 | public struct Location2d 218 | { 219 | public Int64 x, y; 220 | } 221 | 222 | string serialRequest(string serializedRequest) 223 | { 224 | var requestStructure = Newtonsoft.Json.JsonConvert.DeserializeObject(serializedRequest); 225 | 226 | var response = request(requestStructure); 227 | 228 | return SerializeToJsonForBot(response); 229 | } 230 | 231 | Response request(Request request) 232 | { 233 | SetProcessDPIAware(); 234 | 235 | if (request.ListGameClientProcessesRequest != null) 236 | { 237 | return new Response 238 | { 239 | ListGameClientProcessesResponse = 240 | ListGameClientProcesses().ToArray(), 241 | }; 242 | } 243 | 244 | if (request.SearchUIRootAddress != null) 245 | { 246 | searchUIRootAddressTasks.TryGetValue(request.SearchUIRootAddress.processId, out var searchTask); 247 | 248 | if (searchTask is null) 249 | { 250 | searchTask = new SearchUIRootAddressTask(request.SearchUIRootAddress); 251 | 252 | searchUIRootAddressTasks[request.SearchUIRootAddress.processId] = searchTask; 253 | } 254 | 255 | return new Response 256 | { 257 | SearchUIRootAddressResponse = new Response.SearchUIRootAddressResponseStruct 258 | { 259 | processId = request.SearchUIRootAddress.processId, 260 | stage = SearchUIRootAddressTaskAsResponseStage(searchTask) 261 | }, 262 | }; 263 | } 264 | 265 | if (request.ReadFromWindow is { } readFromWindow) 266 | { 267 | var readingFromGameIndex = System.Threading.Interlocked.Increment(ref readingFromGameCount); 268 | 269 | var readingId = readingFromGameIndex.ToString("D6") + "-" + generalStopwatch.ElapsedMilliseconds; 270 | 271 | var windowId = readFromWindow.windowId; 272 | var windowHandle = new IntPtr(long.Parse(windowId)); 273 | 274 | WinApi.GetWindowThreadProcessId(windowHandle, out var processIdUnsigned); 275 | 276 | if (processIdUnsigned is 0) 277 | { 278 | return new Response 279 | { 280 | ReadFromWindowResult = new Response.ReadFromWindowResultStructure 281 | { 282 | ProcessNotFound = new object(), 283 | } 284 | }; 285 | } 286 | 287 | var processId = (int)processIdUnsigned; 288 | 289 | var windowRect = new WinApi.Rect(); 290 | WinApi.GetWindowRect(windowHandle, ref windowRect); 291 | 292 | var clientRectOffsetFromScreen = new WinApi.Point(0, 0); 293 | WinApi.ClientToScreen(windowHandle, ref clientRectOffsetFromScreen); 294 | 295 | var windowClientRectOffset = 296 | new Location2d 297 | { x = clientRectOffsetFromScreen.x - windowRect.left, y = clientRectOffsetFromScreen.y - windowRect.top }; 298 | 299 | string memoryReadingSerialRepresentationJson = null; 300 | 301 | using (var memoryReader = new read_memory_64_bit.MemoryReaderFromLiveProcess(processId)) 302 | { 303 | var uiTree = read_memory_64_bit.EveOnline64.ReadUITreeFromAddress(readFromWindow.uiRootAddress, memoryReader, 99); 304 | 305 | if (uiTree != null) 306 | { 307 | memoryReadingSerialRepresentationJson = 308 | read_memory_64_bit.EveOnline64.SerializeMemoryReadingNodeToJson( 309 | uiTree.WithOtherDictEntriesRemoved()); 310 | } 311 | } 312 | 313 | if (false) // For alternative UI: Do not bring to foreground on cyclical read. 314 | { 315 | /* 316 | Maybe taking screenshots needs the window to be not occluded by other windows. 317 | We can review this later. 318 | */ 319 | var setForegroundWindowError = SetForegroundWindowInWindows.TrySetForegroundWindow(windowHandle); 320 | 321 | if (setForegroundWindowError is not null) 322 | { 323 | return new Response 324 | { 325 | FailedToBringWindowToFront = setForegroundWindowError, 326 | }; 327 | } 328 | } 329 | 330 | var historyEntry = new ReadingFromGameClient 331 | { 332 | windowHandle = windowHandle, 333 | readingId = readingId 334 | }; 335 | 336 | readingFromGameHistory.Enqueue(historyEntry); 337 | 338 | while (4 < readingFromGameHistory.Count) 339 | { 340 | readingFromGameHistory.Dequeue(); 341 | } 342 | 343 | return new Response 344 | { 345 | ReadFromWindowResult = new Response.ReadFromWindowResultStructure 346 | { 347 | Completed = new Response.ReadFromWindowResultStructure.CompletedStructure 348 | { 349 | processId = processId, 350 | windowClientRectOffset = windowClientRectOffset, 351 | memoryReadingSerialRepresentationJson = memoryReadingSerialRepresentationJson, 352 | readingId = readingId 353 | }, 354 | }, 355 | }; 356 | } 357 | 358 | if (request?.EffectSequenceOnWindow?.task != null) 359 | { 360 | var windowHandle = new IntPtr(long.Parse(request.EffectSequenceOnWindow.windowId)); 361 | 362 | if (request.EffectSequenceOnWindow.bringWindowToForeground) 363 | { 364 | var setForegroundWindowError = SetForegroundWindowInWindows.TrySetForegroundWindow(windowHandle); 365 | 366 | if (setForegroundWindowError != null) 367 | { 368 | return new Response 369 | { 370 | FailedToBringWindowToFront = setForegroundWindowError, 371 | }; 372 | } 373 | } 374 | 375 | foreach (var sequenceElement in request.EffectSequenceOnWindow.task) 376 | { 377 | if (sequenceElement?.effect != null) 378 | ExecuteEffectOnWindow(sequenceElement.effect, windowHandle, request.EffectSequenceOnWindow.bringWindowToForeground); 379 | 380 | if (sequenceElement?.delayMilliseconds != null) 381 | System.Threading.Thread.Sleep(sequenceElement.delayMilliseconds.Value); 382 | } 383 | 384 | return new Response 385 | { 386 | CompletedEffectSequenceOnWindow = new object(), 387 | }; 388 | } 389 | 390 | return null; 391 | } 392 | 393 | static Response.SearchUIRootAddressStage SearchUIRootAddressTaskAsResponseStage(SearchUIRootAddressTask task) 394 | { 395 | return task.completed switch 396 | { 397 | Response.SearchUIRootAddressCompletedStruct completed => 398 | new Response.SearchUIRootAddressStage { SearchUIRootAddressCompleted = completed }, 399 | 400 | _ => new Response.SearchUIRootAddressStage 401 | { 402 | SearchUIRootAddressInProgress = new Response.SearchUIRootAddressInProgressStruct 403 | { 404 | searchBeginTimeMilliseconds = (long)task.beginTime.TotalMilliseconds, 405 | currentTimeMilliseconds = generalStopwatch.ElapsedMilliseconds, 406 | } 407 | } 408 | }; 409 | } 410 | 411 | static ulong? FindUIRootAddressFromProcessId(int processId) 412 | { 413 | var candidatesAddresses = 414 | read_memory_64_bit.EveOnline64.EnumeratePossibleAddressesForUIRootObjectsFromProcessId(processId); 415 | 416 | using (var memoryReader = new read_memory_64_bit.MemoryReaderFromLiveProcess(processId)) 417 | { 418 | var uiTrees = 419 | candidatesAddresses 420 | .Select(candidateAddress => read_memory_64_bit.EveOnline64.ReadUITreeFromAddress(candidateAddress, memoryReader, 99)) 421 | .ToList(); 422 | 423 | return 424 | uiTrees 425 | .OrderByDescending(uiTree => uiTree?.EnumerateSelfAndDescendants().Count() ?? -1) 426 | .FirstOrDefault() 427 | ?.pythonObjectAddress; 428 | } 429 | } 430 | 431 | void ExecuteEffectOnWindow( 432 | Request.EffectOnWindowStructure effectOnWindow, 433 | IntPtr windowHandle, 434 | bool bringWindowToForeground) 435 | { 436 | if (bringWindowToForeground) 437 | BotEngine.WinApi.User32.SetForegroundWindow(windowHandle); 438 | 439 | if (effectOnWindow?.MouseMoveTo != null) 440 | { 441 | // Build motion description based on https://github.com/Arcitectus/Sanderling/blob/ada11c9f8df2367976a6bcc53efbe9917107bfa7/src/Sanderling/Sanderling/Motor/Extension.cs#L24-L131 442 | 443 | var mousePosition = new Bib3.Geometrik.Vektor2DInt( 444 | effectOnWindow.MouseMoveTo.location.x, 445 | effectOnWindow.MouseMoveTo.location.y); 446 | 447 | var mouseButtons = new BotEngine.Motor.MouseButtonIdEnum[] { }; 448 | 449 | var windowMotor = new Sanderling.Motor.WindowMotor(windowHandle); 450 | 451 | var motionSequence = new BotEngine.Motor.Motion[]{ 452 | new BotEngine.Motor.Motion( 453 | mousePosition: mousePosition, 454 | mouseButtonDown: mouseButtons, 455 | windowToForeground: bringWindowToForeground), 456 | new BotEngine.Motor.Motion( 457 | mousePosition: mousePosition, 458 | mouseButtonUp: mouseButtons, 459 | windowToForeground: bringWindowToForeground), 460 | }; 461 | 462 | windowMotor.ActSequenceMotion(motionSequence); 463 | } 464 | 465 | if (effectOnWindow?.KeyDown != null) 466 | { 467 | var virtualKeyCode = (WindowsInput.Native.VirtualKeyCode)effectOnWindow.KeyDown.virtualKeyCode; 468 | 469 | (MouseActionForKeyUpOrDown(keyCode: virtualKeyCode, buttonUp: false) 470 | ?? 471 | (() => new WindowsInput.InputSimulator().Keyboard.KeyDown(virtualKeyCode)))(); 472 | } 473 | 474 | if (effectOnWindow?.KeyUp != null) 475 | { 476 | var virtualKeyCode = (WindowsInput.Native.VirtualKeyCode)effectOnWindow.KeyUp.virtualKeyCode; 477 | 478 | (MouseActionForKeyUpOrDown(keyCode: virtualKeyCode, buttonUp: true) 479 | ?? 480 | (() => new WindowsInput.InputSimulator().Keyboard.KeyUp(virtualKeyCode)))(); 481 | } 482 | } 483 | 484 | static System.Action MouseActionForKeyUpOrDown(WindowsInput.Native.VirtualKeyCode keyCode, bool buttonUp) 485 | { 486 | WindowsInput.IMouseSimulator mouseSimulator() => new WindowsInput.InputSimulator().Mouse; 487 | 488 | var method = keyCode switch 489 | { 490 | WindowsInput.Native.VirtualKeyCode.LBUTTON => 491 | buttonUp ? 492 | (System.Func)mouseSimulator().LeftButtonUp 493 | : mouseSimulator().LeftButtonDown, 494 | WindowsInput.Native.VirtualKeyCode.RBUTTON => 495 | buttonUp ? 496 | (System.Func)mouseSimulator().RightButtonUp 497 | : mouseSimulator().RightButtonDown, 498 | _ => null 499 | }; 500 | 501 | if (method != null) 502 | return () => method(); 503 | 504 | return null; 505 | } 506 | 507 | string SerializeToJsonForBot(T value) => 508 | Newtonsoft.Json.JsonConvert.SerializeObject( 509 | value, 510 | // Use settings to get same derivation as at https://github.com/Arcitectus/Sanderling/blob/ada11c9f8df2367976a6bcc53efbe9917107bfa7/src/Sanderling/Sanderling.MemoryReading.Test/MemoryReadingDemo.cs#L91-L97 511 | new Newtonsoft.Json.JsonSerializerSettings 512 | { 513 | // Bot code does not expect properties with null values, see https://github.com/Viir/bots/blob/880d745b0aa8408a4417575d54ecf1f513e7aef4/explore/2019-05-14.eve-online-bot-framework/src/Sanderling_Interface_20190514.elm 514 | NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore, 515 | 516 | // https://stackoverflow.com/questions/7397207/json-net-error-self-referencing-loop-detected-for-type/18223985#18223985 517 | ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore, 518 | }); 519 | 520 | 521 | void SetProcessDPIAware() 522 | { 523 | // https://www.google.com/search?q=GetWindowRect+dpi 524 | // https://github.com/dotnet/wpf/issues/859 525 | // https://github.com/dotnet/winforms/issues/135 526 | WinApi.SetProcessDPIAware(); 527 | } 528 | 529 | static public class WinApi 530 | { 531 | [StructLayout(LayoutKind.Sequential)] 532 | public struct Rect 533 | { 534 | public int left; 535 | public int top; 536 | public int right; 537 | public int bottom; 538 | } 539 | 540 | [StructLayout(LayoutKind.Sequential)] 541 | public struct Point 542 | { 543 | public int x; 544 | public int y; 545 | 546 | public Point(int x, int y) 547 | { 548 | this.x = x; 549 | this.y = y; 550 | } 551 | } 552 | 553 | [DllImport("user32.dll", SetLastError = true)] 554 | static public extern bool SetProcessDPIAware(); 555 | 556 | [DllImport("user32.dll")] 557 | static public extern bool EnumWindows(EnumWindowsProc enumProc, IntPtr lParam); 558 | 559 | public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); 560 | 561 | /* 562 | https://stackoverflow.com/questions/19867402/how-can-i-use-enumwindows-to-find-windows-with-a-specific-caption-title/20276701#20276701 563 | https://stackoverflow.com/questions/295996/is-the-order-in-which-handles-are-returned-by-enumwindows-meaningful/296014#296014 564 | */ 565 | public static System.Collections.Generic.IReadOnlyList ListWindowHandlesInZOrder() 566 | { 567 | IntPtr found = IntPtr.Zero; 568 | System.Collections.Generic.List windowHandles = new System.Collections.Generic.List(); 569 | 570 | EnumWindows(delegate (IntPtr wnd, IntPtr param) 571 | { 572 | windowHandles.Add(wnd); 573 | 574 | // return true here so that we iterate all windows 575 | return true; 576 | }, IntPtr.Zero); 577 | 578 | return windowHandles; 579 | } 580 | 581 | [DllImport("user32.dll")] 582 | static public extern IntPtr ShowWindow(IntPtr hWnd, int nCmdShow); 583 | 584 | [DllImport("user32.dll")] 585 | static public extern IntPtr GetWindowRect(IntPtr hWnd, ref Rect rect); 586 | 587 | [DllImport("user32.dll", SetLastError = false)] 588 | static public extern IntPtr GetDesktopWindow(); 589 | 590 | [DllImport("user32.dll")] 591 | [return: MarshalAs(UnmanagedType.Bool)] 592 | static public extern bool SetCursorPos(int x, int y); 593 | 594 | [DllImport("user32.dll")] 595 | static public extern bool ClientToScreen(IntPtr hWnd, ref Point lpPoint); 596 | 597 | [DllImport("user32.dll", SetLastError = true)] 598 | static public extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId); 599 | } 600 | 601 | static public class SetForegroundWindowInWindows 602 | { 603 | static public int AltKeyPlusSetForegroundWindowWaitTimeMilliseconds = 60; 604 | 605 | /// 606 | /// 607 | /// 608 | /// null in case of success 609 | static public string TrySetForegroundWindow(IntPtr windowHandle) 610 | { 611 | try 612 | { 613 | /* 614 | * For the conditions for `SetForegroundWindow` to work, see https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setforegroundwindow 615 | * */ 616 | BotEngine.WinApi.User32.SetForegroundWindow(windowHandle); 617 | 618 | if (BotEngine.WinApi.User32.GetForegroundWindow() == windowHandle) 619 | return null; 620 | 621 | var windowsInZOrder = WinApi.ListWindowHandlesInZOrder(); 622 | 623 | var windowIndex = windowsInZOrder.ToList().IndexOf(windowHandle); 624 | 625 | if (windowIndex < 0) 626 | return "Did not find window for this handle"; 627 | 628 | { 629 | var simulator = new WindowsInput.InputSimulator(); 630 | 631 | simulator.Keyboard.KeyDown(WindowsInput.Native.VirtualKeyCode.MENU); 632 | BotEngine.WinApi.User32.SetForegroundWindow(windowHandle); 633 | simulator.Keyboard.KeyUp(WindowsInput.Native.VirtualKeyCode.MENU); 634 | 635 | System.Threading.Thread.Sleep(AltKeyPlusSetForegroundWindowWaitTimeMilliseconds); 636 | 637 | if (BotEngine.WinApi.User32.GetForegroundWindow() == windowHandle) 638 | return null; 639 | 640 | return "Alt key plus SetForegroundWindow approach was not successful."; 641 | } 642 | } 643 | catch (Exception e) 644 | { 645 | return "Exception: " + e.ToString(); 646 | } 647 | } 648 | } 649 | 650 | struct Rectangle 651 | { 652 | public Rectangle(Int64 left, Int64 top, Int64 right, Int64 bottom) 653 | { 654 | this.left = left; 655 | this.top = top; 656 | this.right = right; 657 | this.bottom = bottom; 658 | } 659 | 660 | readonly public Int64 top, left, bottom, right; 661 | 662 | override public string ToString() => 663 | Newtonsoft.Json.JsonConvert.SerializeObject(this); 664 | } 665 | 666 | 667 | System.Diagnostics.Process[] GetWindowsProcessesLookingLikeEVEOnlineClient() => 668 | System.Diagnostics.Process.GetProcessesByName("exefile"); 669 | 670 | 671 | System.Collections.Generic.IReadOnlyList ListGameClientProcesses() 672 | { 673 | var allWindowHandlesInZOrder = WinApi.ListWindowHandlesInZOrder(); 674 | 675 | int? zIndexFromWindowHandle(IntPtr windowHandleToSearch) => 676 | allWindowHandlesInZOrder 677 | .Select((windowHandle, index) => (windowHandle, index: (int?)index)) 678 | .FirstOrDefault(handleAndIndex => handleAndIndex.windowHandle == windowHandleToSearch) 679 | .index; 680 | 681 | var processes = 682 | GetWindowsProcessesLookingLikeEVEOnlineClient() 683 | .Select(process => 684 | { 685 | return new Response.GameClientProcessSummaryStruct 686 | { 687 | processId = process.Id, 688 | mainWindowId = process.MainWindowHandle.ToInt64().ToString(), 689 | mainWindowTitle = process.MainWindowTitle, 690 | mainWindowZIndex = zIndexFromWindowHandle(process.MainWindowHandle) ?? 9999, 691 | }; 692 | }) 693 | .ToList(); 694 | 695 | return processes; 696 | } 697 | 698 | string InterfaceToHost_Request(string request) 699 | { 700 | return serialRequest(request); 701 | } 702 | -------------------------------------------------------------------------------- /implement/alternate-ui/source/src/EveOnline/VolatileProcessInterface.elm: -------------------------------------------------------------------------------- 1 | module EveOnline.VolatileProcessInterface exposing (..) 2 | 3 | import Common.EffectOnWindow exposing (MouseButton(..), VirtualKeyCode(..), virtualKeyCodeAsInteger) 4 | import Json.Decode 5 | import Json.Encode 6 | import Maybe.Extra 7 | 8 | 9 | type RequestToVolatileHost 10 | = ListGameClientProcessesRequest 11 | | SearchUIRootAddress SearchUIRootAddressStructure 12 | | ReadFromWindow ReadFromWindowStructure 13 | | EffectSequenceOnWindow (TaskOnWindowStructure (List EffectSequenceElement)) 14 | 15 | 16 | type ResponseFromVolatileHost 17 | = ListGameClientProcessesResponse (List GameClientProcessSummaryStruct) 18 | | SearchUIRootAddressResponse SearchUIRootAddressResponseStruct 19 | | ReadFromWindowResult ReadFromWindowResultStructure 20 | | FailedToBringWindowToFront String 21 | | CompletedEffectSequenceOnWindow 22 | 23 | 24 | type alias GameClientProcessSummaryStruct = 25 | { processId : Int 26 | , mainWindowId : String 27 | , mainWindowTitle : String 28 | , mainWindowZIndex : Int 29 | } 30 | 31 | 32 | type alias ReadFromWindowStructure = 33 | { windowId : String 34 | , uiRootAddress : String 35 | } 36 | 37 | 38 | type alias SearchUIRootAddressStructure = 39 | { processId : Int 40 | } 41 | 42 | 43 | type alias SearchUIRootAddressResponseStruct = 44 | { processId : Int 45 | , stage : SearchUIRootAddressStage 46 | } 47 | 48 | 49 | type SearchUIRootAddressStage 50 | = SearchUIRootAddressInProgress SearchUIRootAddressInProgressStruct 51 | | SearchUIRootAddressCompleted SearchUIRootAddressCompletedStruct 52 | 53 | 54 | type alias SearchUIRootAddressInProgressStruct = 55 | { searchBeginTimeMilliseconds : Int 56 | , currentTimeMilliseconds : Int 57 | } 58 | 59 | 60 | type alias SearchUIRootAddressCompletedStruct = 61 | { uiRootAddress : Maybe String 62 | } 63 | 64 | 65 | type ReadFromWindowResultStructure 66 | = ProcessNotFound 67 | | Completed MemoryReadingCompletedStructure 68 | 69 | 70 | type alias MemoryReadingCompletedStructure = 71 | { processId : Int 72 | , readingId : String 73 | , memoryReadingSerialRepresentationJson : Maybe String 74 | } 75 | 76 | 77 | type alias TaskOnWindowStructure task = 78 | { windowId : WindowId 79 | , bringWindowToForeground : Bool 80 | , task : task 81 | } 82 | 83 | 84 | type EffectSequenceElement 85 | = Effect EffectOnWindowStructure 86 | | DelayMilliseconds Int 87 | 88 | 89 | {-| Using names from Windows API and 90 | -} 91 | type 92 | EffectOnWindowStructure 93 | {- 94 | = MouseMoveTo MouseMoveToStructure 95 | | MouseButtonDown MouseButtonChangeStructure 96 | | MouseButtonUp MouseButtonChangeStructure 97 | | MouseHorizontalScroll Int 98 | | MouseVerticalScroll Int 99 | | KeyboardKeyDown VirtualKeyCode 100 | | KeyboardKeyUp VirtualKeyCode 101 | | TextEntry String 102 | -} 103 | = MouseMoveTo MouseMoveToStructure 104 | | KeyDown VirtualKeyCode 105 | | KeyUp VirtualKeyCode 106 | 107 | 108 | type alias MouseMoveToStructure = 109 | { location : Location2d } 110 | 111 | 112 | type alias WindowId = 113 | String 114 | 115 | 116 | type alias Location2d = 117 | { x : Int, y : Int } 118 | 119 | 120 | deserializeResponseFromVolatileHost : String -> Result Json.Decode.Error ResponseFromVolatileHost 121 | deserializeResponseFromVolatileHost = 122 | Json.Decode.decodeString decodeResponseFromVolatileHost 123 | 124 | 125 | decodeResponseFromVolatileHost : Json.Decode.Decoder ResponseFromVolatileHost 126 | decodeResponseFromVolatileHost = 127 | Json.Decode.oneOf 128 | [ Json.Decode.field "ListGameClientProcessesResponse" (Json.Decode.list jsonDecodeGameClientProcessSummary) 129 | |> Json.Decode.map ListGameClientProcessesResponse 130 | , Json.Decode.field "SearchUIRootAddressResponse" decodeSearchUIRootAddressResponse 131 | |> Json.Decode.map SearchUIRootAddressResponse 132 | , Json.Decode.field "ReadFromWindowResult" decodeReadFromWindowResult 133 | |> Json.Decode.map ReadFromWindowResult 134 | , Json.Decode.field "FailedToBringWindowToFront" (Json.Decode.map FailedToBringWindowToFront Json.Decode.string) 135 | , Json.Decode.field "CompletedEffectSequenceOnWindow" (jsonDecodeSucceedWhenNotNull CompletedEffectSequenceOnWindow) 136 | ] 137 | 138 | 139 | encodeRequestToVolatileHost : RequestToVolatileHost -> Json.Encode.Value 140 | encodeRequestToVolatileHost request = 141 | case request of 142 | ListGameClientProcessesRequest -> 143 | Json.Encode.object [ ( "ListGameClientProcessesRequest", Json.Encode.object [] ) ] 144 | 145 | SearchUIRootAddress searchUIRootAddress -> 146 | Json.Encode.object [ ( "SearchUIRootAddress", searchUIRootAddress |> encodeSearchUIRootAddress ) ] 147 | 148 | ReadFromWindow readFromWindow -> 149 | Json.Encode.object [ ( "ReadFromWindow", readFromWindow |> encodeReadFromWindow ) ] 150 | 151 | EffectSequenceOnWindow taskOnWindow -> 152 | Json.Encode.object 153 | [ ( "EffectSequenceOnWindow" 154 | , taskOnWindow |> encodeTaskOnWindow (Json.Encode.list encodeEffectSequenceElement) 155 | ) 156 | ] 157 | 158 | 159 | decodeRequestToVolatileHost : Json.Decode.Decoder RequestToVolatileHost 160 | decodeRequestToVolatileHost = 161 | Json.Decode.oneOf 162 | [ Json.Decode.field "ListGameClientProcessesRequest" (jsonDecodeSucceedWhenNotNull ListGameClientProcessesRequest) 163 | , Json.Decode.field "SearchUIRootAddress" (decodeSearchUIRootAddress |> Json.Decode.map SearchUIRootAddress) 164 | , Json.Decode.field "ReadFromWindow" (decodeReadFromWindow |> Json.Decode.map ReadFromWindow) 165 | , Json.Decode.field "EffectSequenceOnWindow" (decodeTaskOnWindow (Json.Decode.list decodeEffectSequenceElement) |> Json.Decode.map EffectSequenceOnWindow) 166 | ] 167 | 168 | 169 | encodeEffectSequenceElement : EffectSequenceElement -> Json.Encode.Value 170 | encodeEffectSequenceElement sequenceElement = 171 | case sequenceElement of 172 | Effect effect -> 173 | Json.Encode.object [ ( "effect", encodeEffectOnWindowStructure effect ) ] 174 | 175 | DelayMilliseconds delayMilliseconds -> 176 | Json.Encode.object [ ( "delayMilliseconds", Json.Encode.int delayMilliseconds ) ] 177 | 178 | 179 | decodeEffectSequenceElement : Json.Decode.Decoder EffectSequenceElement 180 | decodeEffectSequenceElement = 181 | Json.Decode.oneOf 182 | [ Json.Decode.field "effect" (decodeEffectOnWindowStructure |> Json.Decode.map Effect) 183 | , Json.Decode.field "delayMilliseconds" (Json.Decode.int |> Json.Decode.map DelayMilliseconds) 184 | ] 185 | 186 | 187 | jsonDecodeGameClientProcessSummary : Json.Decode.Decoder GameClientProcessSummaryStruct 188 | jsonDecodeGameClientProcessSummary = 189 | Json.Decode.map4 GameClientProcessSummaryStruct 190 | (Json.Decode.field "processId" Json.Decode.int) 191 | (Json.Decode.field "mainWindowId" Json.Decode.string) 192 | (Json.Decode.field "mainWindowTitle" Json.Decode.string) 193 | (Json.Decode.field "mainWindowZIndex" Json.Decode.int) 194 | 195 | 196 | encodeTaskOnWindow : (task -> Json.Encode.Value) -> TaskOnWindowStructure task -> Json.Encode.Value 197 | encodeTaskOnWindow taskEncoder taskOnWindow = 198 | Json.Encode.object 199 | [ ( "windowId", taskOnWindow.windowId |> Json.Encode.string ) 200 | , ( "bringWindowToForeground", taskOnWindow.bringWindowToForeground |> Json.Encode.bool ) 201 | , ( "task", taskOnWindow.task |> taskEncoder ) 202 | ] 203 | 204 | 205 | decodeTaskOnWindow : Json.Decode.Decoder task -> Json.Decode.Decoder (TaskOnWindowStructure task) 206 | decodeTaskOnWindow taskDecoder = 207 | Json.Decode.map3 (\windowId bringWindowToForeground task -> { windowId = windowId, bringWindowToForeground = bringWindowToForeground, task = task }) 208 | (Json.Decode.field "windowId" Json.Decode.string) 209 | (Json.Decode.field "bringWindowToForeground" Json.Decode.bool) 210 | (Json.Decode.field "task" taskDecoder) 211 | 212 | 213 | encodeEffectOnWindowStructure : EffectOnWindowStructure -> Json.Encode.Value 214 | encodeEffectOnWindowStructure effectOnWindow = 215 | case effectOnWindow of 216 | MouseMoveTo mouseMoveTo -> 217 | Json.Encode.object 218 | [ ( "MouseMoveTo", mouseMoveTo |> encodeMouseMoveTo ) 219 | ] 220 | 221 | KeyDown virtualKeyCode -> 222 | Json.Encode.object 223 | [ ( "KeyDown", virtualKeyCode |> encodeKey ) 224 | ] 225 | 226 | KeyUp virtualKeyCode -> 227 | Json.Encode.object 228 | [ ( "KeyUp", virtualKeyCode |> encodeKey ) 229 | ] 230 | 231 | 232 | decodeEffectOnWindowStructure : Json.Decode.Decoder EffectOnWindowStructure 233 | decodeEffectOnWindowStructure = 234 | Json.Decode.oneOf 235 | [ Json.Decode.field "MouseMoveTo" (decodeMouseMoveTo |> Json.Decode.map MouseMoveTo) 236 | , Json.Decode.field "KeyDown" (decodeKey |> Json.Decode.map KeyDown) 237 | , Json.Decode.field "KeyUp" (decodeKey |> Json.Decode.map KeyUp) 238 | ] 239 | 240 | 241 | encodeKey : VirtualKeyCode -> Json.Encode.Value 242 | encodeKey virtualKeyCode = 243 | Json.Encode.object [ ( "virtualKeyCode", virtualKeyCode |> virtualKeyCodeAsInteger |> Json.Encode.int ) ] 244 | 245 | 246 | decodeKey : Json.Decode.Decoder VirtualKeyCode 247 | decodeKey = 248 | Json.Decode.field "virtualKeyCode" Json.Decode.int |> Json.Decode.map VirtualKeyCodeFromInt 249 | 250 | 251 | encodeMouseMoveTo : MouseMoveToStructure -> Json.Encode.Value 252 | encodeMouseMoveTo mouseMoveTo = 253 | Json.Encode.object 254 | [ ( "location", mouseMoveTo.location |> encodeLocation2d ) 255 | ] 256 | 257 | 258 | decodeMouseMoveTo : Json.Decode.Decoder MouseMoveToStructure 259 | decodeMouseMoveTo = 260 | Json.Decode.field "location" jsonDecodeLocation2d |> Json.Decode.map MouseMoveToStructure 261 | 262 | 263 | encodeLocation2d : Location2d -> Json.Encode.Value 264 | encodeLocation2d location = 265 | Json.Encode.object 266 | [ ( "x", location.x |> Json.Encode.int ) 267 | , ( "y", location.y |> Json.Encode.int ) 268 | ] 269 | 270 | 271 | jsonDecodeLocation2d : Json.Decode.Decoder Location2d 272 | jsonDecodeLocation2d = 273 | Json.Decode.map2 Location2d 274 | (Json.Decode.field "x" Json.Decode.int) 275 | (Json.Decode.field "y" Json.Decode.int) 276 | 277 | 278 | encodeSearchUIRootAddress : SearchUIRootAddressStructure -> Json.Encode.Value 279 | encodeSearchUIRootAddress searchUIRootAddress = 280 | Json.Encode.object 281 | [ ( "processId", searchUIRootAddress.processId |> Json.Encode.int ) 282 | ] 283 | 284 | 285 | decodeSearchUIRootAddress : Json.Decode.Decoder SearchUIRootAddressStructure 286 | decodeSearchUIRootAddress = 287 | Json.Decode.map SearchUIRootAddressStructure 288 | (Json.Decode.field "processId" Json.Decode.int) 289 | 290 | 291 | encodeReadFromWindow : ReadFromWindowStructure -> Json.Encode.Value 292 | encodeReadFromWindow readFromWindow = 293 | Json.Encode.object 294 | [ ( "windowId", readFromWindow.windowId |> Json.Encode.string ) 295 | , ( "uiRootAddress", readFromWindow.uiRootAddress |> Json.Encode.string ) 296 | ] 297 | 298 | 299 | decodeReadFromWindow : Json.Decode.Decoder ReadFromWindowStructure 300 | decodeReadFromWindow = 301 | Json.Decode.map2 ReadFromWindowStructure 302 | (Json.Decode.field "windowId" Json.Decode.string) 303 | (Json.Decode.field "uiRootAddress" Json.Decode.string) 304 | 305 | 306 | decodeSearchUIRootAddressResponse : Json.Decode.Decoder SearchUIRootAddressResponseStruct 307 | decodeSearchUIRootAddressResponse = 308 | Json.Decode.map2 SearchUIRootAddressResponseStruct 309 | (Json.Decode.field "processId" Json.Decode.int) 310 | (Json.Decode.field "stage" decodeSearchUIRootAddressStage) 311 | 312 | 313 | decodeSearchUIRootAddressStage : Json.Decode.Decoder SearchUIRootAddressStage 314 | decodeSearchUIRootAddressStage = 315 | Json.Decode.oneOf 316 | [ Json.Decode.field "SearchUIRootAddressInProgress" 317 | decodeSearchUIRootAddressInProgress 318 | |> Json.Decode.map SearchUIRootAddressInProgress 319 | , Json.Decode.field "SearchUIRootAddressCompleted" 320 | decodeSearchUIRootAddressComplete 321 | |> Json.Decode.map SearchUIRootAddressCompleted 322 | ] 323 | 324 | 325 | decodeSearchUIRootAddressInProgress : Json.Decode.Decoder SearchUIRootAddressInProgressStruct 326 | decodeSearchUIRootAddressInProgress = 327 | Json.Decode.map2 SearchUIRootAddressInProgressStruct 328 | (Json.Decode.field "searchBeginTimeMilliseconds" Json.Decode.int) 329 | (Json.Decode.field "currentTimeMilliseconds" Json.Decode.int) 330 | 331 | 332 | decodeSearchUIRootAddressComplete : Json.Decode.Decoder SearchUIRootAddressCompletedStruct 333 | decodeSearchUIRootAddressComplete = 334 | Json.Decode.map SearchUIRootAddressCompletedStruct 335 | (jsonDecode_optionalField "uiRootAddress" 336 | (Json.Decode.nullable Json.Decode.string) 337 | |> Json.Decode.map Maybe.Extra.join 338 | ) 339 | 340 | 341 | decodeReadFromWindowResult : Json.Decode.Decoder ReadFromWindowResultStructure 342 | decodeReadFromWindowResult = 343 | Json.Decode.oneOf 344 | [ Json.Decode.field "ProcessNotFound" (Json.Decode.succeed ProcessNotFound) 345 | , Json.Decode.field "Completed" decodeMemoryReadingCompleted |> Json.Decode.map Completed 346 | ] 347 | 348 | 349 | decodeMemoryReadingCompleted : Json.Decode.Decoder MemoryReadingCompletedStructure 350 | decodeMemoryReadingCompleted = 351 | Json.Decode.map3 MemoryReadingCompletedStructure 352 | (Json.Decode.field "processId" Json.Decode.int) 353 | (Json.Decode.field "readingId" Json.Decode.string) 354 | (jsonDecode_optionalField "memoryReadingSerialRepresentationJson" Json.Decode.string) 355 | 356 | 357 | buildRequestStringToGetResponseFromVolatileHost : RequestToVolatileHost -> String 358 | buildRequestStringToGetResponseFromVolatileHost = 359 | encodeRequestToVolatileHost 360 | >> Json.Encode.encode 0 361 | 362 | 363 | jsonDecodeSucceedWhenNotNull : a -> Json.Decode.Decoder a 364 | jsonDecodeSucceedWhenNotNull valueIfNotNull = 365 | Json.Decode.value 366 | |> Json.Decode.andThen 367 | (\asValue -> 368 | if asValue == Json.Encode.null then 369 | Json.Decode.fail "Is null." 370 | 371 | else 372 | Json.Decode.succeed valueIfNotNull 373 | ) 374 | 375 | 376 | jsonDecode_optionalField : String -> Json.Decode.Decoder a -> Json.Decode.Decoder (Maybe a) 377 | jsonDecode_optionalField fieldName decoder = 378 | let 379 | finishDecoding json = 380 | case Json.Decode.decodeValue (Json.Decode.field fieldName Json.Decode.value) json of 381 | Ok val -> 382 | -- The field is present, so run the decoder on it. 383 | Json.Decode.map Just (Json.Decode.field fieldName decoder) 384 | 385 | Err _ -> 386 | -- The field was missing, which is fine! 387 | Json.Decode.succeed Nothing 388 | in 389 | Json.Decode.value 390 | |> Json.Decode.andThen finishDecoding 391 | -------------------------------------------------------------------------------- /implement/alternate-ui/source/src/InterfaceToFrontendClient.elm: -------------------------------------------------------------------------------- 1 | module InterfaceToFrontendClient exposing (..) 2 | 3 | import EveOnline.VolatileProcessInterface 4 | 5 | 6 | type RequestFromClient 7 | = ReadLogRequest 8 | | RunInVolatileProcessRequest EveOnline.VolatileProcessInterface.RequestToVolatileHost 9 | 10 | 11 | type RunInVolatileProcessResponseStructure 12 | = SetupNotCompleteResponse String 13 | | RunInVolatileProcessCompleteResponse RunInVolatileProcessComplete 14 | 15 | 16 | type alias RunInVolatileProcessComplete = 17 | { exceptionToString : Maybe String 18 | , returnValueToString : Maybe String 19 | , durationInMilliseconds : Int 20 | } 21 | -------------------------------------------------------------------------------- /implement/alternate-ui/source/src/ListDict.elm: -------------------------------------------------------------------------------- 1 | module ListDict exposing 2 | ( Dict 3 | , empty, singleton, insert, update, remove 4 | , isEmpty, member, get, size 5 | , keys, values, fromList 6 | , map, filter 7 | , union 8 | , toListWithInsertionOrder 9 | ) 10 | 11 | {-| A dictionary mapping unique keys to values. 12 | 13 | 14 | # Dictionaries 15 | 16 | @docs Dict 17 | 18 | 19 | # Build 20 | 21 | @docs empty, singleton, insert, update, remove 22 | 23 | 24 | # Query 25 | 26 | @docs isEmpty, member, get, size 27 | 28 | 29 | # Lists 30 | 31 | @docs keys, values, toList, fromList 32 | 33 | 34 | # Transform 35 | 36 | @docs map, foldl, foldr, filter, partition 37 | 38 | 39 | # Combine 40 | 41 | @docs union, intersect, diff, merge 42 | 43 | -} 44 | 45 | 46 | type Dict key value 47 | = Dict (List ( key, value )) 48 | 49 | 50 | empty : Dict key value 51 | empty = 52 | Dict [] 53 | 54 | 55 | singleton : key -> value -> Dict key value 56 | singleton key value = 57 | empty |> insert key value 58 | 59 | 60 | insert : key -> value -> Dict key value -> Dict key value 61 | insert key value dictBefore = 62 | case dictBefore of 63 | Dict listBefore -> 64 | ((listBefore |> List.filter (Tuple.first >> (/=) key)) ++ [ ( key, value ) ]) |> Dict 65 | 66 | 67 | update : key -> (Maybe value -> Maybe value) -> Dict key value -> Dict key value 68 | update key valueMap originalDict = 69 | let 70 | insertion = 71 | case originalDict |> get key |> valueMap of 72 | Nothing -> 73 | identity 74 | 75 | Just newValue -> 76 | insert key newValue 77 | in 78 | originalDict 79 | |> remove key 80 | |> insertion 81 | 82 | 83 | remove : key -> Dict key value -> Dict key value 84 | remove key dictBefore = 85 | case dictBefore of 86 | Dict list -> 87 | list |> List.filter (Tuple.first >> (/=) key) |> Dict 88 | 89 | 90 | isEmpty : Dict key value -> Bool 91 | isEmpty = 92 | toListWithInsertionOrder >> List.isEmpty 93 | 94 | 95 | member : key -> Dict key value -> Bool 96 | member key dict = 97 | case dict of 98 | Dict list -> 99 | list |> List.any (\( k, _ ) -> k == key) 100 | 101 | 102 | get : key -> Dict key value -> Maybe value 103 | get key dict = 104 | case dict of 105 | Dict list -> 106 | list 107 | |> List.filter (Tuple.first >> (==) key) 108 | |> List.reverse 109 | |> List.head 110 | |> Maybe.map Tuple.second 111 | 112 | 113 | size : Dict key value -> Int 114 | size = 115 | toListWithInsertionOrder >> List.length 116 | 117 | 118 | keys : Dict key value -> List key 119 | keys = 120 | toListWithInsertionOrder >> List.map Tuple.first 121 | 122 | 123 | values : Dict key value -> List value 124 | values = 125 | toListWithInsertionOrder >> List.map Tuple.second 126 | 127 | 128 | fromList : List ( key, value ) -> Dict key value 129 | fromList = 130 | List.foldl (\( key, value ) dictBefore -> dictBefore |> insert key value) empty 131 | 132 | 133 | toListWithInsertionOrder : Dict key value -> List ( key, value ) 134 | toListWithInsertionOrder dict = 135 | case dict of 136 | Dict list -> 137 | list 138 | 139 | 140 | filter : (key -> value -> Bool) -> Dict key value -> Dict key value 141 | filter predicate = 142 | toListWithInsertionOrder >> List.filter (\( k, v ) -> predicate k v) >> fromList 143 | 144 | 145 | {-| Combine two dictionaries. For keys contained in both dictionaries, prefer the value from the second dictionary. 146 | -} 147 | union : Dict key value -> Dict key value -> Dict key value 148 | union firstDict secondDict = 149 | [ firstDict, secondDict ] |> List.map toListWithInsertionOrder |> List.concat |> fromList 150 | 151 | 152 | map : (key -> a -> b) -> Dict key a -> Dict key b 153 | map entryMap = 154 | toListWithInsertionOrder >> List.map (\( k, v ) -> ( k, entryMap k v )) >> fromList 155 | -------------------------------------------------------------------------------- /implement/alternate-ui/source/src/Platform/WebService.elm: -------------------------------------------------------------------------------- 1 | module Platform.WebService exposing (..) 2 | 3 | {-| This module contains the types describing the Pine / Elm web service platform. 4 | To build a web service app in Elm, copy this module file into your project and add a declaration with the name `webServiceMain` to an Elm module. 5 | 6 | For the latest version of the documentation, see 7 | 8 | -} 9 | 10 | import Bytes 11 | 12 | 13 | {-| Use the type `WebServiceConfig` on a declaration named `webServiceMain` to declare a web service program in an Elm module. 14 | A web service can subscribe to incoming HTTP requests and respond to them. It can also start and manage volatile processes to integrate other software. 15 | -} 16 | type alias WebServiceConfig state = 17 | { init : ( state, Commands state ) 18 | , subscriptions : state -> Subscriptions state 19 | } 20 | 21 | 22 | type alias Subscriptions state = 23 | { httpRequest : HttpRequestEventStruct -> state -> ( state, Commands state ) 24 | , posixTimeIsPast : 25 | Maybe 26 | { minimumPosixTimeMilli : Int 27 | , update : { currentPosixTimeMilli : Int } -> state -> ( state, Commands state ) 28 | } 29 | } 30 | 31 | 32 | type alias Commands state = 33 | List (Command state) 34 | 35 | 36 | type Command state 37 | = RespondToHttpRequest RespondToHttpRequestStruct 38 | | CreateVolatileProcess (CreateVolatileProcessStruct state) 39 | {- 40 | We use the `runtimeIdentifier` and `osPlatform` properties to select the right executable files when creating a (native) volatile process. 41 | The properties returned by this command comes from the `RuntimeInformation` documented at 42 | -} 43 | | ReadRuntimeInformationCommand (ReadRuntimeInformationCommandStruct state) 44 | | CreateVolatileProcessNativeCommand (CreateVolatileProcessNativeCommandStruct state) 45 | | RequestToVolatileProcess (RequestToVolatileProcessStruct state) 46 | | WriteToVolatileProcessNativeStdInCommand (WriteToVolatileProcessNativeStdInStruct state) 47 | | ReadAllFromVolatileProcessNativeCommand (ReadAllFromVolatileProcessNativeStruct state) 48 | | TerminateVolatileProcess TerminateVolatileProcessStruct 49 | 50 | 51 | type alias HttpRequestEventStruct = 52 | { httpRequestId : String 53 | , posixTimeMilli : Int 54 | , requestContext : HttpRequestContext 55 | , request : HttpRequestProperties 56 | } 57 | 58 | 59 | type alias HttpRequestContext = 60 | { clientAddress : Maybe String 61 | } 62 | 63 | 64 | type alias HttpRequestProperties = 65 | { method : String 66 | , uri : String 67 | , body : Maybe Bytes.Bytes 68 | , headers : List HttpHeader 69 | } 70 | 71 | 72 | type alias RespondToHttpRequestStruct = 73 | { httpRequestId : String 74 | , response : HttpResponse 75 | } 76 | 77 | 78 | type alias HttpResponse = 79 | { statusCode : Int 80 | , body : Maybe Bytes.Bytes 81 | , headersToAdd : List HttpHeader 82 | } 83 | 84 | 85 | type alias HttpHeader = 86 | { name : String 87 | , values : List String 88 | } 89 | 90 | 91 | type alias CreateVolatileProcessStruct state = 92 | { programCode : String 93 | , update : CreateVolatileProcessResult -> state -> ( state, Commands state ) 94 | } 95 | 96 | 97 | type alias ReadRuntimeInformationCommandStruct state = 98 | Result String RuntimeInformationRecord -> state -> ( state, Commands state ) 99 | 100 | 101 | type alias RuntimeInformationRecord = 102 | { runtimeIdentifier : String 103 | , osPlatform : Maybe String 104 | } 105 | 106 | 107 | type alias CreateVolatileProcessNativeCommandStruct state = 108 | { request : CreateVolatileProcessNativeRequestStruct 109 | , update : CreateVolatileProcessResult -> state -> ( state, Commands state ) 110 | } 111 | 112 | 113 | type alias CreateVolatileProcessNativeRequestStruct = 114 | { executableFile : LoadDependencyStruct 115 | , arguments : String 116 | , environmentVariables : List ProcessEnvironmentVariableStruct 117 | } 118 | 119 | 120 | type alias CreateVolatileProcessResult = 121 | Result CreateVolatileProcessErrorStruct CreateVolatileProcessComplete 122 | 123 | 124 | type alias CreateVolatileProcessErrorStruct = 125 | { exceptionToString : String 126 | } 127 | 128 | 129 | type alias CreateVolatileProcessComplete = 130 | { processId : String } 131 | 132 | 133 | type alias RequestToVolatileProcessStruct state = 134 | { processId : String 135 | , request : String 136 | , update : RequestToVolatileProcessResult -> state -> ( state, Commands state ) 137 | } 138 | 139 | 140 | type alias WriteToVolatileProcessNativeStdInStruct state = 141 | { processId : String 142 | , stdInBytes : Bytes.Bytes 143 | , update : 144 | Result RequestToVolatileProcessError () 145 | -> state 146 | -> ( state, Commands state ) 147 | } 148 | 149 | 150 | type alias ReadAllFromVolatileProcessNativeStruct state = 151 | { processId : String 152 | , update : 153 | Result RequestToVolatileProcessError ReadAllFromVolatileProcessNativeSuccessStruct 154 | -> state 155 | -> ( state, Commands state ) 156 | } 157 | 158 | 159 | type alias RequestToVolatileProcessResult = 160 | Result RequestToVolatileProcessError RequestToVolatileProcessComplete 161 | 162 | 163 | type alias ReadAllFromVolatileProcessNativeSuccessStruct = 164 | { stdOutBytes : Bytes.Bytes 165 | , stdErrBytes : Bytes.Bytes 166 | , exitCode : Maybe Int 167 | } 168 | 169 | 170 | type RequestToVolatileProcessError 171 | = ProcessNotFound 172 | | RequestToVolatileProcessOtherError String 173 | 174 | 175 | type alias RequestToVolatileProcessComplete = 176 | { exceptionToString : Maybe String 177 | , returnValueToString : Maybe String 178 | , durationInMilliseconds : Int 179 | } 180 | 181 | 182 | type alias TerminateVolatileProcessStruct = 183 | { processId : String } 184 | 185 | 186 | type alias ProcessEnvironmentVariableStruct = 187 | { key : String 188 | , value : String 189 | } 190 | 191 | 192 | type alias LoadDependencyStruct = 193 | { hashSha256Base16 : String 194 | , hintUrls : List String 195 | } 196 | -------------------------------------------------------------------------------- /implement/alternate-ui/source/tests/ParseMemoryReadingTest.elm: -------------------------------------------------------------------------------- 1 | module ParseMemoryReadingTest exposing (allTests) 2 | 3 | import Common.EffectOnWindow 4 | import EveOnline.ParseUserInterface 5 | import Expect 6 | import Test 7 | 8 | 9 | allTests : Test.Test 10 | allTests = 11 | Test.describe "Parse memory reading" 12 | [ overview_entry_distance_text_to_meter 13 | , inventory_capacity_gauge_text 14 | , parse_module_button_tooltip_shortcut 15 | , parse_neocom_clock_text 16 | , parse_security_status_percent_from_ui_node_text 17 | , parse_current_solar_system_from_ui_node_text 18 | ] 19 | 20 | 21 | overview_entry_distance_text_to_meter : Test.Test 22 | overview_entry_distance_text_to_meter = 23 | [ ( "2,856 m", Ok 2856 ) 24 | , ( "123 m", Ok 123 ) 25 | , ( "16 km", Ok 16000 ) 26 | , ( " 345 m ", Ok 345 ) 27 | 28 | -- 2020-03-12 from TheRealManiac (https://forum.botlab.org/t/last-version-of-mining-bot/3149) 29 | , ( "6.621 m ", Ok 6621 ) 30 | 31 | -- 2020-03-22 from istu233 at https://forum.botlab.org/t/mining-bot-problem/3169 32 | , ( "2 980 m", Ok 2980 ) 33 | 34 | -- Add case with more than two groups in number 35 | , ( " 3.444.555,6 m ", Ok 3444555 ) 36 | ] 37 | |> List.map 38 | (\( displayText, expectedResult ) -> 39 | Test.test displayText <| 40 | \_ -> 41 | displayText 42 | |> EveOnline.ParseUserInterface.parseOverviewEntryDistanceInMetersFromText 43 | |> Expect.equal expectedResult 44 | ) 45 | |> Test.describe "Overview entry distance text" 46 | 47 | 48 | inventory_capacity_gauge_text : Test.Test 49 | inventory_capacity_gauge_text = 50 | [ ( "1,211.9/5,000.0 m³", Ok { used = 1211, maximum = Just 5000, selected = Nothing } ) 51 | , ( " 123.4 / 5,000.0 m³ ", Ok { used = 123, maximum = Just 5000, selected = Nothing } ) 52 | 53 | -- Example from https://forum.botlab.org/t/standard-mining-bot-problems/2715/14 54 | , ( "4 999,8/5 000,0 m³", Ok { used = 4999, maximum = Just 5000, selected = Nothing } ) 55 | 56 | -- 2020-01-31 sample 'process-sample-2FA2DCF580-[In Space with selected Ore Hold].zip' from Leon Bechen. 57 | , ( "0/5.000,0 m³", Ok { used = 0, maximum = Just 5000, selected = Nothing } ) 58 | 59 | -- 2020-02-16-eve-online-sample 60 | , ( "(33.3) 53.6/450.0 m³", Ok { used = 53, maximum = Just 450, selected = Just 33 } ) 61 | 62 | -- 2020-02-23 process-sample-FFE3312944 contributed by ORly (https://forum.botlab.org/t/mining-bot-i-cannot-see-the-ore-hold-capacity-gauge/3101/5) 63 | , ( "0/5\u{00A0}000,0 m³", Ok { used = 0, maximum = Just 5000, selected = Nothing } ) 64 | 65 | -- 2020-07-26 scenario shared by neolexo at https://forum.botlab.org/t/issue-with-mining/3469/3 66 | , ( "0/5’000.0 m³", Ok { used = 0, maximum = Just 5000, selected = Nothing } ) 67 | 68 | -- Add case with more than two groups in number 69 | , ( " 3.444.555,0 / 12.333.444,6 m³", Ok { used = 3444555, maximum = Just 12333444, selected = Nothing } ) 70 | 71 | -- 2025-10-29 scenario shared by Tim Bbil at https://forum.botlab.org/t/eve-mining-bot-is-stuck-on-i-do-not-see-the-mining-hold-capacity-gauge/5272 72 | , ( "0/5'000.0 m³", Ok { used = 0, maximum = Just 5000, selected = Nothing } ) 73 | ] 74 | |> List.map 75 | (\( text, expectedResult ) -> 76 | Test.test text <| 77 | \_ -> 78 | text 79 | |> EveOnline.ParseUserInterface.parseInventoryCapacityGaugeText 80 | |> Expect.equal expectedResult 81 | ) 82 | |> Test.describe "Inventory capacity gauge text" 83 | 84 | 85 | parse_module_button_tooltip_shortcut : Test.Test 86 | parse_module_button_tooltip_shortcut = 87 | [ ( " F1 ", [ Common.EffectOnWindow.vkey_F1 ] ) 88 | , ( " CTRL-F3 ", [ Common.EffectOnWindow.vkey_LCONTROL, Common.EffectOnWindow.vkey_F3 ] ) 89 | , ( " STRG-F4 ", [ Common.EffectOnWindow.vkey_LCONTROL, Common.EffectOnWindow.vkey_F4 ] ) 90 | , ( " ALT+F4 ", [ Common.EffectOnWindow.vkey_LMENU, Common.EffectOnWindow.vkey_F4 ] ) 91 | , ( " SHIFT - F5 ", [ Common.EffectOnWindow.vkey_LSHIFT, Common.EffectOnWindow.vkey_F5 ] ) 92 | , ( " UMSCH-F6 ", [ Common.EffectOnWindow.vkey_LSHIFT, Common.EffectOnWindow.vkey_F6 ] ) 93 | ] 94 | |> List.map 95 | (\( text, expectedResult ) -> 96 | Test.test text <| 97 | \_ -> 98 | text 99 | |> EveOnline.ParseUserInterface.parseModuleButtonTooltipShortcut 100 | |> Expect.equal (Ok expectedResult) 101 | ) 102 | |> Test.describe "Parse module button tooltip shortcut" 103 | 104 | 105 | parse_neocom_clock_text : Test.Test 106 | parse_neocom_clock_text = 107 | [ ( " 0:00 ", { hour = 0, minute = 0 } ) 108 | , ( " 0:01 ", { hour = 0, minute = 1 } ) 109 | , ( " 3 : 17 ", { hour = 3, minute = 17 } ) 110 | , ( " 24 : 00 ", { hour = 24, minute = 0 } ) 111 | ] 112 | |> List.map 113 | (\( text, expectedResult ) -> 114 | Test.test text <| 115 | \_ -> 116 | text 117 | |> EveOnline.ParseUserInterface.parseNeocomClockText 118 | |> Expect.equal (Ok expectedResult) 119 | ) 120 | |> Test.describe "Parse neocom clock text" 121 | 122 | 123 | parse_security_status_percent_from_ui_node_text : Test.Test 124 | parse_security_status_percent_from_ui_node_text = 125 | [ ( """Jita 0.9 < Kimotoro < The Forge""", Just 90 ) 126 | 127 | -- Scenario by Samuel Pagé aka Mohano from https://forum.botlab.org/t/new-code-for-some-memory-elements-in-new-patch/3989 128 | , ( """0.5""", Just 50 ) 129 | ] 130 | |> List.map 131 | (\( text, expectedResult ) -> 132 | Test.test text <| 133 | \_ -> 134 | text 135 | |> EveOnline.ParseUserInterface.parseSecurityStatusPercentFromUINodeText 136 | |> Expect.equal expectedResult 137 | ) 138 | |> Test.describe "Parse security status from UI node text" 139 | 140 | 141 | parse_current_solar_system_from_ui_node_text : Test.Test 142 | parse_current_solar_system_from_ui_node_text = 143 | [ ( """Jita 0.9 < Kimotoro < The Forge""", Just "Jita" ) 144 | 145 | -- Scenario by Breazy shared with `session-2021-10-26T06-47-59-025605.zip` (https://forum.botlab.org/t/error-with-anom-bot/4195) 146 | , ( """1DQ1-A -0.4""", Just "1DQ1-A" ) 147 | ] 148 | |> List.map 149 | (\( text, expectedResult ) -> 150 | Test.test text <| 151 | \_ -> 152 | text 153 | |> EveOnline.ParseUserInterface.parseCurrentSolarSystemFromUINodeText 154 | |> Expect.equal expectedResult 155 | ) 156 | |> Test.describe "Parse current solar system from UI node text" 157 | -------------------------------------------------------------------------------- /implement/read-memory-64-bit/.gitignore: -------------------------------------------------------------------------------- 1 | /obj/ 2 | /bin/ 3 | 4 | .vs/ 5 | *.csproj.user 6 | -------------------------------------------------------------------------------- /implement/read-memory-64-bit/JavaScript/Int64JsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace read_memory_64_bit.JavaScript; 4 | 5 | 6 | public class Int64JsonConverter : System.Text.Json.Serialization.JsonConverter 7 | { 8 | public override long Read( 9 | ref System.Text.Json.Utf8JsonReader reader, 10 | Type typeToConvert, 11 | System.Text.Json.JsonSerializerOptions options) => 12 | long.Parse(reader.GetString()!); 13 | 14 | public override void Write( 15 | System.Text.Json.Utf8JsonWriter writer, 16 | long integer, 17 | System.Text.Json.JsonSerializerOptions options) => 18 | writer.WriteStringValue(integer.ToString()); 19 | } 20 | -------------------------------------------------------------------------------- /implement/read-memory-64-bit/JavaScript/UInt64JsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace read_memory_64_bit.JavaScript; 4 | 5 | public class UInt64JsonConverter : System.Text.Json.Serialization.JsonConverter 6 | { 7 | public override ulong Read( 8 | ref System.Text.Json.Utf8JsonReader reader, 9 | Type typeToConvert, 10 | System.Text.Json.JsonSerializerOptions options) => 11 | ulong.Parse(reader.GetString()!); 12 | 13 | public override void Write( 14 | System.Text.Json.Utf8JsonWriter writer, 15 | ulong integer, 16 | System.Text.Json.JsonSerializerOptions options) => 17 | writer.WriteStringValue(integer.ToString()); 18 | } 19 | -------------------------------------------------------------------------------- /implement/read-memory-64-bit/MemoryReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.Linq; 4 | 5 | namespace read_memory_64_bit; 6 | 7 | 8 | public interface IMemoryReader 9 | { 10 | ReadOnlyMemory? ReadBytes(ulong startAddress, int length); 11 | } 12 | 13 | public class MemoryReaderFromProcessSample( 14 | IImmutableList memoryRegions) 15 | : IMemoryReader 16 | { 17 | readonly IImmutableList memoryRegionsOrderedByAddress = 18 | memoryRegions 19 | .OrderBy(memoryRegion => memoryRegion.baseAddress) 20 | .ToImmutableList(); 21 | 22 | public ReadOnlyMemory? ReadBytes(ulong startAddress, int length) 23 | { 24 | var memoryRegion = 25 | memoryRegionsOrderedByAddress 26 | .Where(region => region.baseAddress <= startAddress) 27 | .LastOrDefault(); 28 | 29 | if (memoryRegion?.content is not { } memoryRegionContent) 30 | return null; 31 | 32 | var start = 33 | startAddress - memoryRegion.baseAddress; 34 | 35 | if ((int)start < 0) 36 | return null; 37 | 38 | if (memoryRegionContent.Length <= (int)start) 39 | return null; 40 | 41 | var sliceLengthBound = 42 | memoryRegionContent.Length - (int)start; 43 | 44 | var sliceLength = 45 | length < sliceLengthBound ? length : sliceLengthBound; 46 | 47 | return 48 | memoryRegionContent.Slice( 49 | start: (int)start, 50 | length: sliceLength); 51 | } 52 | } 53 | 54 | 55 | public class MemoryReaderFromLiveProcess : IMemoryReader, IDisposable 56 | { 57 | readonly IntPtr processHandle; 58 | 59 | public MemoryReaderFromLiveProcess(int processId) 60 | { 61 | processHandle = 62 | WinApi.OpenProcess( 63 | (int)(WinApi.ProcessAccessFlags.QueryInformation | WinApi.ProcessAccessFlags.VirtualMemoryRead), 64 | false, 65 | dwProcessId: processId); 66 | } 67 | 68 | public void Dispose() 69 | { 70 | if (processHandle != IntPtr.Zero) 71 | WinApi.CloseHandle(processHandle); 72 | } 73 | 74 | public ReadOnlyMemory? ReadBytes(ulong startAddress, int length) 75 | { 76 | if (length < 0) 77 | { 78 | /* 79 | * From https://www.ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf 80 | * III.4.20 newarr – create a zero-based, one-dimensional array 81 | * 82 | * [...] 83 | * System.OverflowException is thrown if numElems is < 0 84 | * */ 85 | return null; 86 | } 87 | 88 | var buffer = new byte[length]; 89 | 90 | UIntPtr numberOfBytesReadAsPtr = UIntPtr.Zero; 91 | 92 | if (!WinApi.ReadProcessMemory(processHandle, startAddress, buffer, (UIntPtr)buffer.LongLength, ref numberOfBytesReadAsPtr)) 93 | return null; 94 | 95 | var numberOfBytesRead = numberOfBytesReadAsPtr.ToUInt64(); 96 | 97 | if (numberOfBytesRead is 0) 98 | return null; 99 | 100 | if (int.MaxValue < numberOfBytesRead) 101 | return null; 102 | 103 | if (numberOfBytesRead == (ulong)buffer.LongLength) 104 | return buffer; 105 | 106 | return buffer; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /implement/read-memory-64-bit/ProcessSample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | 6 | namespace read_memory_64_bit; 7 | 8 | 9 | public class ProcessSample 10 | { 11 | static public byte[] ZipArchiveFromProcessSample( 12 | IImmutableList memoryRegions, 13 | IImmutableList logEntries, 14 | byte[] beginMainWindowClientAreaScreenshotBmp, 15 | byte[] endMainWindowClientAreaScreenshotBmp) 16 | { 17 | var screenshotEntriesCandidates = new[] 18 | { 19 | (filePath: ImmutableList.Create("begin-main-window-client-area.bmp"), content: beginMainWindowClientAreaScreenshotBmp), 20 | (filePath: ImmutableList.Create("end-main-window-client-area.bmp"), content: endMainWindowClientAreaScreenshotBmp), 21 | }; 22 | 23 | var screenshotEntries = 24 | screenshotEntriesCandidates 25 | .Where(filePathAndContent => filePathAndContent.content is not null) 26 | .Select(filePathAndContent => new KeyValuePair, byte[]>( 27 | filePathAndContent.filePath, filePathAndContent.content)) 28 | .ToArray(); 29 | 30 | var zipArchiveEntries = 31 | memoryRegions.ToImmutableDictionary( 32 | region => (IImmutableList)(["Process", "Memory", $"0x{region.baseAddress:X}"]), 33 | region => region.content.Value.ToArray()) 34 | .Add(new[] { "copy-memory-log" }.ToImmutableList(), System.Text.Encoding.UTF8.GetBytes(String.Join("\n", logEntries))) 35 | .AddRange(screenshotEntries); 36 | 37 | return Pine.ZipArchive.ZipArchiveFromEntries(zipArchiveEntries); 38 | } 39 | 40 | static public (IImmutableList memoryRegions, IImmutableList copyMemoryLog) ProcessSampleFromZipArchive(byte[] sampleFile) 41 | { 42 | var files = 43 | Pine.ZipArchive.EntriesFromZipArchive(sampleFile); 44 | 45 | IEnumerable<(IImmutableList filePath, byte[] fileContent)> GetFilesInDirectory(IImmutableList directory) 46 | { 47 | foreach (var fileFullPathAndContent in files) 48 | { 49 | var fullPath = fileFullPathAndContent.name.Split(['/', '\\']); 50 | 51 | if (!fullPath.Take(directory.Count).SequenceEqual(directory)) 52 | continue; 53 | 54 | yield return (fullPath.Skip(directory.Count).ToImmutableList(), fileFullPathAndContent.content); 55 | } 56 | } 57 | 58 | var memoryRegions = 59 | GetFilesInDirectory(ImmutableList.Create("Process", "Memory")) 60 | .Where(fileSubpathAndContent => fileSubpathAndContent.filePath.Count == 1) 61 | .Select(fileSubpathAndContent => 62 | { 63 | var baseAddressBase16 = System.Text.RegularExpressions.Regex.Match(fileSubpathAndContent.filePath.Single(), @"0x(.+)").Groups[1].Value; 64 | 65 | var baseAddress = ulong.Parse(baseAddressBase16, System.Globalization.NumberStyles.HexNumber); 66 | 67 | return new SampleMemoryRegion( 68 | baseAddress, 69 | length: (ulong)fileSubpathAndContent.fileContent.LongLength, 70 | content: fileSubpathAndContent.fileContent); 71 | }).ToImmutableList(); 72 | 73 | return (memoryRegions, null); 74 | } 75 | } 76 | 77 | public record SampleMemoryRegion( 78 | ulong baseAddress, 79 | ulong length, 80 | ReadOnlyMemory? content); 81 | 82 | -------------------------------------------------------------------------------- /implement/read-memory-64-bit/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using System.Runtime.InteropServices; 6 | using System.Threading.Tasks; 7 | using McMaster.Extensions.CommandLineUtils; 8 | 9 | namespace read_memory_64_bit; 10 | 11 | class Program 12 | { 13 | static string AppVersionId => "2025-10-24"; 14 | 15 | static int Main(string[] args) 16 | { 17 | var app = new CommandLineApplication 18 | { 19 | Name = "read-memory-64-bit", 20 | Description = "Welcome to the Sanderling memory reading command-line interface. This tool helps you read objects from the memory of a 64-bit EVE Online client process and save it to a file. In addition to that, you have the option to save the entire memory contents of a game client process to a file.\nTo get help or report an issue, see the project website at https://github.com/Arcitectus/Sanderling", 21 | }; 22 | 23 | app.HelpOption(inherited: true); 24 | 25 | app.VersionOption(template: "-v|--version", shortFormVersion: "version " + AppVersionId); 26 | 27 | app.Command("save-process-sample", saveProcessSampleCmd => 28 | { 29 | saveProcessSampleCmd.Description = "Save a sample from a live process to a file. Use the '--pid' parameter to specify the process id."; 30 | 31 | var processIdParam = 32 | saveProcessSampleCmd.Option("--pid", "[Required] Id of the Windows process to read from.", CommandOptionType.SingleValue).IsRequired(errorMessage: "From which process should I read?"); 33 | 34 | var delaySecondsParam = 35 | saveProcessSampleCmd.Option("--delay", "Timespan to wait before starting the collection of the sample, in seconds.", CommandOptionType.SingleValue); 36 | 37 | saveProcessSampleCmd.OnExecute(() => 38 | { 39 | var processIdArgument = processIdParam.Value(); 40 | 41 | var delayMilliSeconds = 42 | delaySecondsParam.HasValue() ? 43 | (int)(double.Parse(delaySecondsParam.Value()) * 1000) : 44 | 0; 45 | 46 | var processId = int.Parse(processIdArgument); 47 | 48 | if (0 < delayMilliSeconds) 49 | { 50 | Console.WriteLine("Delaying for " + delayMilliSeconds + " milliseconds."); 51 | Task.Delay(TimeSpan.FromMilliseconds(delayMilliSeconds)).Wait(); 52 | } 53 | 54 | Console.WriteLine("Starting to collect the sample..."); 55 | 56 | var processSampleFile = GetProcessSampleFileFromProcessId(processId); 57 | 58 | Console.WriteLine("Completed collecting the sample."); 59 | 60 | var processSampleId = 61 | Convert.ToHexStringLower( 62 | System.Security.Cryptography.SHA256.HashData(processSampleFile)); 63 | 64 | var fileName = "process-sample-" + processSampleId[..10] + ".zip"; 65 | 66 | System.IO.File.WriteAllBytes(fileName, processSampleFile); 67 | 68 | Console.WriteLine("Saved sample {0} to file '{1}'.", processSampleId, fileName); 69 | }); 70 | }); 71 | 72 | app.Command("read-memory-eve-online", readMemoryEveOnlineCmd => 73 | { 74 | readMemoryEveOnlineCmd.Description = "Read the memory of an 64 bit EVE Online client process. You can use a live process ('--pid') or a process sample file ('--source-file') as the source."; 75 | 76 | var processIdParam = readMemoryEveOnlineCmd.Option("--pid", "Id of the Windows process to read from.", CommandOptionType.SingleValue); 77 | var rootAddressParam = readMemoryEveOnlineCmd.Option("--root-address", "Address of the UI root. If the address is not specified, the program searches the whole process memory for UI roots.", CommandOptionType.SingleValue); 78 | var sourceFileParam = readMemoryEveOnlineCmd.Option("--source-file", "Process sample file to read from.", CommandOptionType.SingleValue); 79 | var outputFileParam = readMemoryEveOnlineCmd.Option("--output-file", "File to save the memory reading result to.", CommandOptionType.SingleValue); 80 | var removeOtherDictEntriesParam = readMemoryEveOnlineCmd.Option("--remove-other-dict-entries", "Use this to remove the other dict entries from the UI nodes in the resulting JSON representation.", CommandOptionType.NoValue); 81 | var warmupIterationsParam = readMemoryEveOnlineCmd.Option("--warmup-iterations", "Only to measure execution time: Use this to perform additional warmup runs before measuring execution time.", CommandOptionType.SingleValue); 82 | 83 | readMemoryEveOnlineCmd.OnExecute(() => 84 | { 85 | var processIdArgument = processIdParam.Value(); 86 | var rootAddressArgument = rootAddressParam.Value(); 87 | var sourceFileArgument = sourceFileParam.Value(); 88 | var outputFileArgument = outputFileParam.Value(); 89 | var removeOtherDictEntriesArgument = removeOtherDictEntriesParam.HasValue(); 90 | var warmupIterationsArgument = warmupIterationsParam.Value(); 91 | 92 | var processId = 93 | 0 < processIdArgument?.Length 94 | ? 95 | (int?)int.Parse(processIdArgument) 96 | : 97 | null; 98 | 99 | (IMemoryReader, IImmutableList) GetMemoryReaderAndRootAddressesFromProcessSampleFile(byte[] processSampleFile) 100 | { 101 | var processSampleId = 102 | Convert.ToHexStringLower( 103 | System.Security.Cryptography.SHA256.HashData(processSampleFile)); 104 | 105 | Console.WriteLine($"Reading from process sample {processSampleId}."); 106 | 107 | var processSampleUnpacked = ProcessSample.ProcessSampleFromZipArchive(processSampleFile); 108 | 109 | var memoryReader = new MemoryReaderFromProcessSample(processSampleUnpacked.memoryRegions); 110 | 111 | var searchUIRootsStopwatch = System.Diagnostics.Stopwatch.StartNew(); 112 | 113 | var memoryRegions = 114 | processSampleUnpacked.memoryRegions 115 | .Select(memoryRegion => (memoryRegion.baseAddress, length: (ulong)memoryRegion.content.Value.Length)) 116 | .ToImmutableList(); 117 | 118 | var uiRootCandidatesAddresses = 119 | EveOnline64.EnumeratePossibleAddressesForUIRootObjects(memoryRegions, memoryReader) 120 | .ToImmutableList(); 121 | 122 | searchUIRootsStopwatch.Stop(); 123 | 124 | Console.WriteLine($"Found {uiRootCandidatesAddresses.Count} candidates for UIRoot in {(int)searchUIRootsStopwatch.Elapsed.TotalSeconds} seconds: " + string.Join(",", uiRootCandidatesAddresses.Select(address => $"0x{address:X}"))); 125 | 126 | return (memoryReader, uiRootCandidatesAddresses); 127 | } 128 | 129 | (IMemoryReader, IImmutableList) GetMemoryReaderAndWithSpecifiedRootFromProcessSampleFile(byte[] processSampleFile, ulong rootAddress) 130 | { 131 | var processSampleId = 132 | Convert.ToHexStringLower( 133 | System.Security.Cryptography.SHA256.HashData(processSampleFile)); 134 | 135 | Console.WriteLine($"Reading from process sample {processSampleId}."); 136 | 137 | var processSampleUnpacked = ProcessSample.ProcessSampleFromZipArchive(processSampleFile); 138 | 139 | var memoryReader = new MemoryReaderFromProcessSample(processSampleUnpacked.memoryRegions); 140 | 141 | Console.WriteLine($"Reading UIRoot from specified address: {rootAddress}"); 142 | 143 | return (memoryReader, ImmutableList.Empty.Add(rootAddress)); 144 | } 145 | 146 | (IMemoryReader, IImmutableList) GetMemoryReaderAndRootAddresses() 147 | { 148 | if (processId.HasValue) 149 | { 150 | var possibleRootAddresses = 151 | 0 < rootAddressArgument?.Length 152 | ? 153 | ImmutableList.Create(ParseULong(rootAddressArgument)) 154 | : 155 | EveOnline64.EnumeratePossibleAddressesForUIRootObjectsFromProcessId(processId.Value); 156 | 157 | return (new MemoryReaderFromLiveProcess(processId.Value), possibleRootAddresses); 158 | } 159 | 160 | if (!(0 < sourceFileArgument?.Length)) 161 | { 162 | throw new Exception("Where should I read from?"); 163 | } 164 | 165 | if (0 < rootAddressArgument?.Length) 166 | { 167 | return GetMemoryReaderAndWithSpecifiedRootFromProcessSampleFile(System.IO.File.ReadAllBytes(sourceFileArgument), ParseULong(rootAddressArgument)); 168 | } 169 | 170 | return GetMemoryReaderAndRootAddressesFromProcessSampleFile(System.IO.File.ReadAllBytes(sourceFileArgument)); 171 | } 172 | 173 | var (memoryReader, uiRootCandidatesAddresses) = GetMemoryReaderAndRootAddresses(); 174 | 175 | IImmutableList ReadUITrees() => 176 | uiRootCandidatesAddresses 177 | .Select(uiTreeRoot => EveOnline64.ReadUITreeFromAddress(uiTreeRoot, memoryReader, 99)) 178 | .Where(uiTree => uiTree is not null) 179 | .ToImmutableList(); 180 | 181 | if (warmupIterationsArgument is not null) 182 | { 183 | var iterations = int.Parse(warmupIterationsArgument); 184 | 185 | Console.WriteLine("Performing " + iterations + " warmup iterations..."); 186 | 187 | for (var i = 0; i < iterations; i++) 188 | { 189 | ReadUITrees().ToList(); 190 | System.Threading.Thread.Sleep(1111); 191 | } 192 | } 193 | 194 | var readUiTreesStopwatch = System.Diagnostics.Stopwatch.StartNew(); 195 | 196 | var uiTrees = ReadUITrees(); 197 | 198 | readUiTreesStopwatch.Stop(); 199 | 200 | var uiTreesWithStats = 201 | uiTrees 202 | .Select(uiTree => 203 | new 204 | { 205 | uiTree = uiTree, 206 | nodeCount = uiTree.EnumerateSelfAndDescendants().Count() 207 | }) 208 | .OrderByDescending(uiTreeWithStats => uiTreeWithStats.nodeCount) 209 | .ToImmutableList(); 210 | 211 | var uiTreesReport = 212 | uiTreesWithStats 213 | .Select(uiTreeWithStats => $"\n0x{uiTreeWithStats.uiTree.pythonObjectAddress:X}: {uiTreeWithStats.nodeCount} nodes.") 214 | .ToImmutableList(); 215 | 216 | Console.WriteLine($"Read {uiTrees.Count} UI trees in {(int)readUiTreesStopwatch.Elapsed.TotalMilliseconds} milliseconds:" + string.Join("", uiTreesReport)); 217 | 218 | var largestUiTree = 219 | uiTreesWithStats 220 | .OrderByDescending(uiTreeWithStats => uiTreeWithStats.nodeCount) 221 | .FirstOrDefault().uiTree; 222 | 223 | if (largestUiTree is not null) 224 | { 225 | var uiTreePreparedForFile = largestUiTree; 226 | 227 | if (removeOtherDictEntriesArgument) 228 | { 229 | uiTreePreparedForFile = uiTreePreparedForFile.WithOtherDictEntriesRemoved(); 230 | } 231 | 232 | var serializeStopwatch = System.Diagnostics.Stopwatch.StartNew(); 233 | 234 | var uiTreeAsJson = EveOnline64.SerializeMemoryReadingNodeToJson(uiTreePreparedForFile); 235 | 236 | serializeStopwatch.Stop(); 237 | 238 | Console.WriteLine( 239 | "Serialized largest tree to " + uiTreeAsJson.Length + " characters of JSON in " + 240 | serializeStopwatch.ElapsedMilliseconds + " milliseconds."); 241 | 242 | var fileContent = System.Text.Encoding.UTF8.GetBytes(uiTreeAsJson); 243 | 244 | var sampleId = Convert.ToHexStringLower(System.Security.Cryptography.SHA256.HashData(fileContent)); 245 | 246 | var outputFilePath = outputFileArgument; 247 | 248 | if (!(0 < outputFileArgument?.Length)) 249 | { 250 | var outputFileName = "eve-online-memory-reading-" + sampleId[..10] + ".json"; 251 | 252 | outputFilePath = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), outputFileName); 253 | 254 | Console.WriteLine( 255 | "I found no configuration of an output file path, so I use '" + 256 | outputFilePath + "' as the default."); 257 | } 258 | 259 | System.IO.File.WriteAllBytes(outputFilePath, fileContent); 260 | 261 | Console.WriteLine($"I saved memory reading {sampleId} from address 0x{largestUiTree.pythonObjectAddress:X} to file '{outputFilePath}'."); 262 | } 263 | else 264 | { 265 | Console.WriteLine("No largest UI tree."); 266 | } 267 | }); 268 | }); 269 | 270 | app.OnExecute(() => 271 | { 272 | Console.WriteLine("Please specify a subcommand."); 273 | app.ShowHelp(); 274 | 275 | return 1; 276 | }); 277 | 278 | return app.Execute(args); 279 | } 280 | 281 | static byte[] GetProcessSampleFileFromProcessId(int processId) 282 | { 283 | var process = System.Diagnostics.Process.GetProcessById(processId); 284 | 285 | var beginMainWindowClientAreaScreenshotBmp = 286 | BMPFileFromBitmap(GetScreenshotOfWindowClientAreaAsBitmap(process.MainWindowHandle)); 287 | 288 | var (committedRegions, logEntries) = 289 | EveOnline64.ReadCommittedMemoryRegionsWithContentFromProcessId(processId); 290 | 291 | var endMainWindowClientAreaScreenshotBmp = 292 | BMPFileFromBitmap(GetScreenshotOfWindowClientAreaAsBitmap(process.MainWindowHandle)); 293 | 294 | return ProcessSample.ZipArchiveFromProcessSample( 295 | committedRegions, 296 | logEntries, 297 | beginMainWindowClientAreaScreenshotBmp: beginMainWindowClientAreaScreenshotBmp, 298 | endMainWindowClientAreaScreenshotBmp: endMainWindowClientAreaScreenshotBmp); 299 | } 300 | 301 | // Screenshot implementation found at https://github.com/Viir/bots/blob/225c680115328d9ba0223760cec85d56f2ea9a87/implement/templates/locate-object-in-window/src/BotEngine/VolatileHostWindowsApi.elm#L479-L557 302 | 303 | static public byte[] BMPFileFromBitmap(System.Drawing.Bitmap bitmap) 304 | { 305 | using var stream = new System.IO.MemoryStream(); 306 | 307 | bitmap.Save(stream, format: System.Drawing.Imaging.ImageFormat.Bmp); 308 | return stream.ToArray(); 309 | } 310 | 311 | static public int[][] GetScreenshotOfWindowAsPixelsValuesR8G8B8(IntPtr windowHandle) 312 | { 313 | var screenshotAsBitmap = 314 | GetScreenshotOfWindowAsBitmap(windowHandle); 315 | 316 | if (screenshotAsBitmap is null) 317 | return null; 318 | 319 | var bitmapData = 320 | screenshotAsBitmap.LockBits( 321 | new System.Drawing.Rectangle(0, 0, screenshotAsBitmap.Width, screenshotAsBitmap.Height), 322 | System.Drawing.Imaging.ImageLockMode.ReadOnly, 323 | System.Drawing.Imaging.PixelFormat.Format24bppRgb); 324 | 325 | int byteCount = bitmapData.Stride * screenshotAsBitmap.Height; 326 | 327 | byte[] pixelsArray = new byte[byteCount]; 328 | IntPtr ptrFirstPixel = bitmapData.Scan0; 329 | Marshal.Copy(ptrFirstPixel, pixelsArray, 0, pixelsArray.Length); 330 | 331 | screenshotAsBitmap.UnlockBits(bitmapData); 332 | var pixels = new int[screenshotAsBitmap.Height][]; 333 | 334 | for (var rowIndex = 0; rowIndex < screenshotAsBitmap.Height; ++rowIndex) 335 | { 336 | var rowPixelValues = new int[screenshotAsBitmap.Width]; 337 | for (var columnIndex = 0; columnIndex < screenshotAsBitmap.Width; ++columnIndex) 338 | { 339 | var pixelBeginInArray = bitmapData.Stride * rowIndex + columnIndex * 3; 340 | var red = pixelsArray[pixelBeginInArray + 2]; 341 | var green = pixelsArray[pixelBeginInArray + 1]; 342 | var blue = pixelsArray[pixelBeginInArray + 0]; 343 | rowPixelValues[columnIndex] = (red << 16) | (green << 8) | blue; 344 | } 345 | 346 | pixels[rowIndex] = rowPixelValues; 347 | } 348 | 349 | return pixels; 350 | } 351 | 352 | // https://github.com/Viir/bots/blob/225c680115328d9ba0223760cec85d56f2ea9a87/implement/templates/locate-object-in-window/src/BotEngine/VolatileHostWindowsApi.elm#L535-L557 353 | static public System.Drawing.Bitmap GetScreenshotOfWindowAsBitmap(IntPtr windowHandle) 354 | { 355 | SetProcessDPIAware(); 356 | 357 | var windowRect = new WinApi.Rect(); 358 | 359 | if (WinApi.GetWindowRect(windowHandle, ref windowRect) == IntPtr.Zero) 360 | return null; 361 | 362 | int width = windowRect.right - windowRect.left; 363 | int height = windowRect.bottom - windowRect.top; 364 | var asBitmap = new System.Drawing.Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format24bppRgb); 365 | 366 | System.Drawing.Graphics.FromImage(asBitmap).CopyFromScreen( 367 | windowRect.left, 368 | windowRect.top, 369 | 0, 370 | 0, 371 | new System.Drawing.Size(width, height), 372 | System.Drawing.CopyPixelOperation.SourceCopy); 373 | 374 | return asBitmap; 375 | } 376 | 377 | static public System.Drawing.Bitmap GetScreenshotOfWindowClientAreaAsBitmap(IntPtr windowHandle) 378 | { 379 | SetProcessDPIAware(); 380 | 381 | var clientRect = new WinApi.Rect(); 382 | 383 | if (WinApi.GetClientRect(windowHandle, ref clientRect) == IntPtr.Zero) 384 | return null; 385 | 386 | var clientRectLeftTop = new WinApi.Point { x = clientRect.left, y = clientRect.top }; 387 | var clientRectRightBottom = new WinApi.Point { x = clientRect.right, y = clientRect.bottom }; 388 | 389 | WinApi.ClientToScreen(windowHandle, ref clientRectLeftTop); 390 | WinApi.ClientToScreen(windowHandle, ref clientRectRightBottom); 391 | 392 | clientRect = new WinApi.Rect 393 | { 394 | left = clientRectLeftTop.x, 395 | top = clientRectLeftTop.y, 396 | right = clientRectRightBottom.x, 397 | bottom = clientRectRightBottom.y 398 | }; 399 | 400 | int width = clientRect.right - clientRect.left; 401 | int height = clientRect.bottom - clientRect.top; 402 | var asBitmap = new System.Drawing.Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format24bppRgb); 403 | 404 | System.Drawing.Graphics.FromImage(asBitmap).CopyFromScreen( 405 | clientRect.left, 406 | clientRect.top, 407 | 0, 408 | 0, 409 | new System.Drawing.Size(width, height), 410 | System.Drawing.CopyPixelOperation.SourceCopy); 411 | 412 | return asBitmap; 413 | } 414 | 415 | static void SetProcessDPIAware() 416 | { 417 | // https://www.google.com/search?q=GetWindowRect+dpi 418 | // https://github.com/dotnet/wpf/issues/859 419 | // https://github.com/dotnet/winforms/issues/135 420 | WinApi.SetProcessDPIAware(); 421 | } 422 | 423 | static ulong ParseULong(string asString) 424 | { 425 | if (asString.StartsWith("0x", StringComparison.InvariantCultureIgnoreCase)) 426 | return ulong.Parse(asString[2..], System.Globalization.NumberStyles.HexNumber); 427 | 428 | return ulong.Parse(asString); 429 | } 430 | } 431 | 432 | /// 433 | /// Offsets from https://docs.python.org/2/c-api/structures.html 434 | /// 435 | public class PyObject 436 | { 437 | public const int Offset_ob_refcnt = 0; 438 | public const int Offset_ob_type = 8; 439 | } 440 | 441 | public record UITreeNode( 442 | ulong pythonObjectAddress, 443 | string pythonObjectTypeName, 444 | IReadOnlyDictionary dictEntriesOfInterest, 445 | string[] otherDictEntriesKeys, 446 | IReadOnlyList children) 447 | { 448 | public record DictEntryValueGenericRepresentation( 449 | ulong address, 450 | string pythonObjectTypeName); 451 | 452 | public record DictEntry( 453 | string key, 454 | object value); 455 | 456 | public record Bunch( 457 | System.Text.Json.Nodes.JsonObject entriesOfInterest); 458 | 459 | public IEnumerable EnumerateSelfAndDescendants() => 460 | new[] { this } 461 | .Concat((children ?? []).SelectMany(child => child?.EnumerateSelfAndDescendants() ?? [])); 462 | 463 | public UITreeNode WithOtherDictEntriesRemoved() 464 | { 465 | return new UITreeNode 466 | ( 467 | pythonObjectAddress: pythonObjectAddress, 468 | pythonObjectTypeName: pythonObjectTypeName, 469 | dictEntriesOfInterest: dictEntriesOfInterest, 470 | otherDictEntriesKeys: null, 471 | children: children?.Select(child => child?.WithOtherDictEntriesRemoved()).ToArray() 472 | ); 473 | } 474 | } 475 | 476 | static class TransformMemoryContent 477 | { 478 | static public ReadOnlyMemory AsULongMemory(ReadOnlyMemory byteMemory) => 479 | MemoryMarshal.Cast(byteMemory.Span).ToArray(); 480 | } 481 | -------------------------------------------------------------------------------- /implement/read-memory-64-bit/Sanderling.slnx: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /implement/read-memory-64-bit/WinApi.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace read_memory_64_bit; 5 | 6 | 7 | static public class WinApi 8 | { 9 | [DllImport("kernel32.dll")] 10 | static public extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId); 11 | 12 | [DllImport("kernel32.dll")] 13 | static public extern int VirtualQueryEx(IntPtr hProcess, IntPtr lpAddress, out MEMORY_BASIC_INFORMATION64 lpBuffer, uint dwLength); 14 | 15 | [DllImport("kernel32.dll")] 16 | static public extern bool ReadProcessMemory(IntPtr hProcess, ulong lpBaseAddress, byte[] lpBuffer, UIntPtr nSize, ref UIntPtr lpNumberOfBytesRead); 17 | 18 | [DllImport("kernel32.dll", SetLastError = true)] 19 | static public extern bool CloseHandle(IntPtr hHandle); 20 | 21 | [DllImport("user32.dll", SetLastError = true)] 22 | static public extern bool SetProcessDPIAware(); 23 | 24 | [DllImport("user32.dll")] 25 | static public extern IntPtr GetWindowRect(IntPtr hWnd, ref Rect rect); 26 | 27 | [DllImport("user32.dll")] 28 | static public extern IntPtr GetClientRect(IntPtr hWnd, ref Rect rect); 29 | 30 | [DllImport("user32.dll")] 31 | [return: MarshalAs(UnmanagedType.Bool)] 32 | static public extern bool ClientToScreen(IntPtr hWnd, ref Point lpPoint); 33 | 34 | [StructLayout(LayoutKind.Sequential)] 35 | public struct Rect 36 | { 37 | public int left; 38 | public int top; 39 | public int right; 40 | public int bottom; 41 | } 42 | 43 | [StructLayout(LayoutKind.Sequential)] 44 | public struct Point 45 | { 46 | public int x; 47 | 48 | public int y; 49 | } 50 | 51 | // http://www.pinvoke.net/default.aspx/kernel32.virtualqueryex 52 | // https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-memory_basic_information 53 | [StructLayout(LayoutKind.Sequential)] 54 | public struct MEMORY_BASIC_INFORMATION64 55 | { 56 | public ulong BaseAddress; 57 | public ulong AllocationBase; 58 | public int AllocationProtect; 59 | public int __alignment1; 60 | public ulong RegionSize; 61 | public int State; 62 | public int Protect; 63 | public int Type; 64 | public int __alignment2; 65 | } 66 | 67 | public enum AllocationProtect : uint 68 | { 69 | PAGE_EXECUTE = 0x00000010, 70 | PAGE_EXECUTE_READ = 0x00000020, 71 | PAGE_EXECUTE_READWRITE = 0x00000040, 72 | PAGE_EXECUTE_WRITECOPY = 0x00000080, 73 | PAGE_NOACCESS = 0x00000001, 74 | PAGE_READONLY = 0x00000002, 75 | PAGE_READWRITE = 0x00000004, 76 | PAGE_WRITECOPY = 0x00000008, 77 | PAGE_GUARD = 0x00000100, 78 | PAGE_NOCACHE = 0x00000200, 79 | PAGE_WRITECOMBINE = 0x00000400 80 | } 81 | 82 | [Flags] 83 | public enum ProcessAccessFlags : uint 84 | { 85 | All = 0x001F0FFF, 86 | Terminate = 0x00000001, 87 | CreateThread = 0x00000002, 88 | VirtualMemoryOperation = 0x00000008, 89 | VirtualMemoryRead = 0x00000010, 90 | VirtualMemoryWrite = 0x00000020, 91 | DuplicateHandle = 0x00000040, 92 | CreateProcess = 0x000000080, 93 | SetQuota = 0x00000100, 94 | SetInformation = 0x00000200, 95 | QueryInformation = 0x00000400, 96 | QueryLimitedInformation = 0x00001000, 97 | Synchronize = 0x00100000 98 | } 99 | 100 | // https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-memory_basic_information 101 | public enum MemoryInformationState : int 102 | { 103 | MEM_COMMIT = 0x1000, 104 | MEM_FREE = 0x10000, 105 | MEM_RESERVE = 0x2000, 106 | } 107 | 108 | // https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-memory_basic_information 109 | public enum MemoryInformationType : int 110 | { 111 | MEM_IMAGE = 0x1000000, 112 | MEM_MAPPED = 0x40000, 113 | MEM_PRIVATE = 0x20000, 114 | } 115 | 116 | // https://docs.microsoft.com/en-au/windows/win32/memory/memory-protection-constants 117 | [Flags] 118 | public enum MemoryInformationProtection : int 119 | { 120 | PAGE_EXECUTE = 0x10, 121 | PAGE_EXECUTE_READ = 0x20, 122 | PAGE_EXECUTE_READWRITE = 0x40, 123 | PAGE_EXECUTE_WRITECOPY = 0x80, 124 | PAGE_NOACCESS = 0x01, 125 | PAGE_READONLY = 0x02, 126 | PAGE_READWRITE = 0x04, 127 | PAGE_WRITECOPY = 0x08, 128 | PAGE_TARGETS_INVALID = 0x40000000, 129 | PAGE_TARGETS_NO_UPDATE = 0x40000000, 130 | 131 | PAGE_GUARD = 0x100, 132 | PAGE_NOCACHE = 0x200, 133 | PAGE_WRITECOMBINE = 0x400, 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /implement/read-memory-64-bit/ZipArchive.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.IO; 5 | using System.Linq; 6 | 7 | namespace Pine; 8 | 9 | /* 10 | 2020-07-16 Discovered: The roundtrip over `ZipArchiveFromEntries` and `EntriesFromZipArchive` changed the order of entries! 11 | */ 12 | static public class ZipArchive 13 | { 14 | /// 15 | /// https://github.com/dotnet/corefx/blob/a10890f4ffe0fadf090c922578ba0e606ebdd16c/src/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs#L206-L234 16 | /// 17 | static public DateTimeOffset EntryLastWriteTimeDefault => new DateTimeOffset(1980, 1, 1, 0, 0, 0, TimeSpan.Zero); 18 | 19 | static public byte[] ZipArchiveFromEntries( 20 | IEnumerable<(string name, byte[] content)> entries, 21 | System.IO.Compression.CompressionLevel compressionLevel = System.IO.Compression.CompressionLevel.Optimal) => 22 | ZipArchiveFromEntries( 23 | entries.Select(entry => (entry.name, entry.content, EntryLastWriteTimeDefault)).ToImmutableList(), 24 | compressionLevel); 25 | 26 | static public byte[] ZipArchiveFromEntries( 27 | IReadOnlyDictionary, byte[]> entries, 28 | System.IO.Compression.CompressionLevel compressionLevel = System.IO.Compression.CompressionLevel.Optimal) => 29 | ZipArchiveFromEntries( 30 | entries.Select(entry => (name: String.Join("/", entry.Key), content: entry.Value)), 31 | compressionLevel); 32 | 33 | static public byte[] ZipArchiveFromEntries( 34 | IReadOnlyDictionary, IReadOnlyList> entries, 35 | System.IO.Compression.CompressionLevel compressionLevel = System.IO.Compression.CompressionLevel.Optimal) => 36 | ZipArchiveFromEntries( 37 | entries.ToImmutableDictionary(entry => entry.Key, entry => entry.Value.ToArray()), 38 | compressionLevel); 39 | 40 | static public byte[] ZipArchiveFromEntries( 41 | IEnumerable<(string name, byte[] content, DateTimeOffset lastWriteTime)> entries, 42 | System.IO.Compression.CompressionLevel compressionLevel = System.IO.Compression.CompressionLevel.Optimal) 43 | { 44 | var stream = new MemoryStream(); 45 | 46 | using (var fclZipArchive = new System.IO.Compression.ZipArchive(stream, System.IO.Compression.ZipArchiveMode.Create, true)) 47 | { 48 | foreach (var (entryName, entryContent, lastWriteTime) in entries) 49 | { 50 | var entry = fclZipArchive.CreateEntry(entryName, compressionLevel); 51 | entry.LastWriteTime = lastWriteTime; 52 | 53 | using var entryStream = entry.Open(); 54 | 55 | entryStream.Write(entryContent, 0, entryContent.Length); 56 | } 57 | } 58 | 59 | stream.Seek(0, SeekOrigin.Begin); 60 | 61 | var zipArchive = new byte[stream.Length]; 62 | stream.Read(zipArchive, 0, (int)stream.Length); 63 | stream.Dispose(); 64 | return zipArchive; 65 | } 66 | 67 | static public IEnumerable<(string name, byte[] content)> FileEntriesFromZipArchive(byte[] zipArchive) => 68 | EntriesFromZipArchive( 69 | zipArchive: zipArchive, 70 | includeEntry: entry => !entry.FullName.Replace('\\', '/').EndsWith('/')); 71 | 72 | static public IEnumerable<(string name, byte[] content)> EntriesFromZipArchive(byte[] zipArchive) => 73 | EntriesFromZipArchive(zipArchive: zipArchive, includeEntry: _ => true); 74 | 75 | static public IEnumerable<(string name, byte[] content)> EntriesFromZipArchive( 76 | byte[] zipArchive, 77 | Func includeEntry) 78 | { 79 | using var fclZipArchive = new System.IO.Compression.ZipArchive(new MemoryStream(zipArchive), System.IO.Compression.ZipArchiveMode.Read); 80 | 81 | foreach (var entry in fclZipArchive.Entries) 82 | { 83 | if (!includeEntry(entry)) 84 | continue; 85 | 86 | using var entryStream = entry.Open(); 87 | using var memoryStream = new MemoryStream(); 88 | 89 | entryStream.CopyTo(memoryStream); 90 | 91 | var entryContent = memoryStream.ToArray(); 92 | 93 | if (entryContent.Length != entry.Length) 94 | throw new Exception("Error trying to read entry '" + entry.FullName + "': got " + entryContent.Length + " bytes from entry instead of " + entry.Length); 95 | 96 | yield return (entry.FullName, entryContent); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /implement/read-memory-64-bit/build.bat: -------------------------------------------------------------------------------- 1 | dotnet publish -p:Platform=x64 2 | -------------------------------------------------------------------------------- /implement/read-memory-64-bit/read-memory-64-bit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0-windows 6 | read_memory_64_bit 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /implement/read-memory-64-bit/readme.md: -------------------------------------------------------------------------------- 1 | # Read Memory From the 64-Bit EVE Online Client 2 | 3 | For a guide on the functionality not specific to EVE Online, see 4 | 5 | ## read-memory-64-bit.exe 6 | 7 | This software helps you read from 64-bit game client processes. 8 | 9 | To download the latest version of `read-memory-64-bit.exe`, see the list of releases at 10 | 11 | ### Interactive Guide 12 | 13 | To see a guide on available (sub)commands, run the following command: 14 | 15 | ``` 16 | ./read-memory-64-bit.exe --help 17 | ``` 18 | 19 | ### `--pid` option 20 | 21 | The `--pid` option exists on several commands and lets us select a process to read from. 22 | It specifies the ID of the game client process assigned by the Windows operating system. You can find this ID in the Windows Task Manager in the `PID` column in the `Processes` tab. 23 | 24 | ### save-process-sample 25 | 26 | The `save-process-sample` command saves a sample of the process to a file. This sample includes all committed memory regions, so we can use it to support memory reading development. 27 | 28 | Below is an example of a full command to use the tool: 29 | ```cmd 30 | read-memory-64-bit.exe save-process-sample --pid=12345 31 | ``` 32 | 33 | ### read-memory-eve-online 34 | 35 | The `read-memory-eve-online` command produces an EVE Online memory reading of the given process. Supports saving to a JSON file using the `--output-file` option. 36 | 37 | The JSON file format here is the same one you get when exporting the memory reading using the development tools in the BotLab client. 38 | 39 | If you find the timing too tricky with the command-line interface or want to export from specific events of a larger session recording, exporting from the session timeline in BotLab is easier. After selecting an event in the bot session or play session, use the button labeled 'Export reading as JSON file' to get the memory reading in a separate file: 40 | 41 | ![Button to export EVE Online memory reading from event in a bot session](./../../guide/image/2022-10-25-eve-online-botlab-devtools-export-memory-reading-from-event-button.png) 42 | 43 | ## Sources 44 | 45 | + Discussion of the approach to read the UI Tree from the EVE Online client process. (Artifacts for 32-bit back then): 46 | https://forum.botlab.org/t/advanced-do-it-yourself-memory-reading-in-eve-online/68 47 | 48 | I found some articles that might help understand CPython in general: 49 | 50 | + Your Guide to the CPython Source Code: [https://realpython.com/cpython-source-code-guide/#memory-management-in-cpython](https://realpython.com/cpython-source-code-guide/#memory-management-in-cpython) 51 | + Python consumes a lot of memory or how to reduce the size of objects? [https://habr.com/en/post/458518/](https://habr.com/en/post/458518/) 52 | --------------------------------------------------------------------------------