Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@
<PackageVersion Include="NosCore.Packets" Version="20.0.3" />
<PackageVersion Include="NosCore.ParserInputGenerator" Version="4.1.0" />
<PackageVersion Include="NosCore.Shared" Version="6.0.0" />
<PackageVersion Include="ModelContextProtocol" Version="2.2.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.12" />
</ItemGroup>
</Project>
17 changes: 16 additions & 1 deletion NosCore.DeveloperTools.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.5.11709.299 stable
VisualStudioVersion = 18.5.11709.299
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NosCore.DeveloperTools", "src\NosCore.DeveloperTools\NosCore.DeveloperTools.csproj", "{C6B8F7A1-0001-0001-0001-000000000001}"
EndProject
Expand All @@ -11,6 +11,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosCore.DeveloperTools.GfStub", "src\NosCore.DeveloperTools.GfStub\NosCore.DeveloperTools.GfStub.csproj", "{D3A3DE1B-5292-4AAB-A335-31D2F4885A3C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosCore.DeveloperTools.Cli", "src\NosCore.DeveloperTools.Cli\NosCore.DeveloperTools.Cli.csproj", "{73E0BCBB-53D4-4F2D-B794-9F37C7BEF963}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -57,6 +59,18 @@ Global
{D3A3DE1B-5292-4AAB-A335-31D2F4885A3C}.Release|x64.Build.0 = Release|Any CPU
{D3A3DE1B-5292-4AAB-A335-31D2F4885A3C}.Release|x86.ActiveCfg = Release|Any CPU
{D3A3DE1B-5292-4AAB-A335-31D2F4885A3C}.Release|x86.Build.0 = Release|Any CPU
{73E0BCBB-53D4-4F2D-B794-9F37C7BEF963}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{73E0BCBB-53D4-4F2D-B794-9F37C7BEF963}.Debug|Any CPU.Build.0 = Debug|Any CPU
{73E0BCBB-53D4-4F2D-B794-9F37C7BEF963}.Debug|x64.ActiveCfg = Debug|Any CPU
{73E0BCBB-53D4-4F2D-B794-9F37C7BEF963}.Debug|x64.Build.0 = Debug|Any CPU
{73E0BCBB-53D4-4F2D-B794-9F37C7BEF963}.Debug|x86.ActiveCfg = Debug|Any CPU
{73E0BCBB-53D4-4F2D-B794-9F37C7BEF963}.Debug|x86.Build.0 = Debug|Any CPU
{73E0BCBB-53D4-4F2D-B794-9F37C7BEF963}.Release|Any CPU.ActiveCfg = Release|Any CPU
{73E0BCBB-53D4-4F2D-B794-9F37C7BEF963}.Release|Any CPU.Build.0 = Release|Any CPU
{73E0BCBB-53D4-4F2D-B794-9F37C7BEF963}.Release|x64.ActiveCfg = Release|Any CPU
{73E0BCBB-53D4-4F2D-B794-9F37C7BEF963}.Release|x64.Build.0 = Release|Any CPU
{73E0BCBB-53D4-4F2D-B794-9F37C7BEF963}.Release|x86.ActiveCfg = Release|Any CPU
{73E0BCBB-53D4-4F2D-B794-9F37C7BEF963}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -65,5 +79,6 @@ Global
{C6B8F7A1-0001-0001-0001-000000000001} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{C6B8F7A1-0002-0001-0001-000000000001} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{D3A3DE1B-5292-4AAB-A335-31D2F4885A3C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{73E0BCBB-53D4-4F2D-B794-9F37C7BEF963} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
EndGlobal
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,45 @@ This is an independent and unofficial tool for educational use ONLY. Using the P
- Per-direction capture toggles, blacklist/whitelist filters (filtered packets are dropped at intake), Ctrl+A / Ctrl+C / right-click copy with and without tags, Clear button.
- Custom packet inject — send / receive synthetic packets through the client's own send/recv functions using a Delphi register-convention invoker thunk + a hand-rolled Delphi AnsiString.

### Client Control

Drives the character through the client's own routines rather than through synthetic packets, so the client's local state stays consistent with the server's.

- **Walk to x/y** calls the client's movement routine. The client updates its own position, animates, and builds the outgoing `walk` packet itself — including the checksum byte, which we therefore never have to reproduce. Injecting a `walk` packet instead would move the character server-side only and desync every packet the client originates afterwards.
- **Where am I?** reads the live character id and coordinates out of the client's player manager, which is the ground truth to assert against when comparing with what the server thinks.
- **Hook diagnostics** reports which signatures resolved, whether the client thread is ticking, and whether the character is in-world — the first thing to check when a client patch drifts a signature.

All client calls are marshalled onto the client's own thread via a per-frame periodic detour. The client keeps its game state under no synchronisation, so calling a routine straight from the pipe thread races the frame loop. Packet injection now takes the same path, falling back to a direct call only if the periodic signature fails to resolve.

Both movement and state reads need the character to be in-world; the player-manager slot is null until then, and the reply says so rather than guessing.

### Headless driver

`NosCore.DeveloperTools.Cli` runs the same attach and control flow without the GUI, holding the pipe session open and exposing it over `http://127.0.0.1:8787`. It exists so a change can be tested against a real client without anyone clicking through login: launch, attach, drive, assert.

| Endpoint | Purpose |
| --- | --- |
| `POST /launch` `{password, hooks?}` | Authenticate against NosCore and start the patched client |
| `POST /attach` `{pid?}` | Inject the hook and open the pipe |
| `GET /diag` | Resolved signatures, tick count, in-world and character state |
| `GET /pos` | Live character id and coordinates |
| `POST /walk` `{x, y, un0, un1}` | Move through the client's own routine |
| `POST /click` `{x, y}` | Real mouse input at a point in client coordinates |
| `GET /screenshot?path=&mode=` | Capture the client window alone |
| `POST /inject` `{payload, direction}` | Raw packet injection |
| `GET /packets?since=N` | Captured traffic, cursor-paged |
| `GET /log?since=N` | Hook status lines |

It must run elevated, because the client inherits that elevation and UIPI discards window calls and injected input arriving from lower integrity.

Two arguments to `/walk` are unnamed in every reference we have. `un0: 0, un1: 1` is what works; the invoker restores `ESP` from `EBP`, so a wrong guess is a no-op rather than a crash.

`hooks` on `/launch` selects which detours to install — `send`, `recv`, `login-recv`, `periodic`, plus `no-bootstrap` to skip the thread-attach stub. Only useful for bisecting a client that misbehaves; leave it unset otherwise.

**Screenshots.** `PrintWindow` first, so occlusion and off-screen area don't matter. Accelerated surfaces sometimes refuse to render into the device context and come back flat; that is detected (ignoring the title bar, which always paints) and retried as a screen grab with the window pulled to the origin.

**Clicking.** The client reads the mouse below the window-message layer, so a posted `WM_LBUTTONDOWN` is never seen — `/click` drives the real cursor. Points are in client coordinates, which is what a screenshot gives you once you subtract the window chrome.

### Client Creator
Point it at a copy of `NostaleClientX.exe`, pick a new server address and output filename, hit **Patch**. The output binary gets three edits:

Expand Down
Loading
Loading