├── .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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------