Skip to content

Commit 04fd929

Browse files
committed
2 parents a4d39db + 29e66fd commit 04fd929

8 files changed

Lines changed: 372 additions & 15 deletions

File tree

.external/interposer

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Subproject commit cee070268da7e7757f6411c53f03f0564aa1ff69
1+
Subproject commit f7a3bf644a6068e9c31609d473d708deb1c5181e

Interposer/FileRedirection.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
sidebar_label: Overview
2+
sidebar_label: File Redirection
33
sidebar_position: 4
44
---
55

@@ -11,7 +11,7 @@ File redirection intercepts calls to `CreateFileW/A`, `GetFileAttributesW/A`, `F
1111

1212
Common use cases:
1313

14-
- **Portable save files** — redirect a hard-coded save path (e.g. `C:\Program Files\MyGame\saves\`) to a per-user location (`%APPDATA%\MyGame\saves\`)
14+
- **Portable save files** — redirect a hard-coded save path (e.g. `C:\Program Files\MyGame\saves\`) to a per-user location (`%USERPROFILE%\Saved Games\My Game\`)
1515
- **Config file portability** — redirect absolute paths baked into old games to locations inside the game directory
1616
- **Multi-user coexistence** — different users can be redirected to different profile directories without modifying the game
1717
- **Registry-adjacent paths** — some games write config to hard-coded paths under `C:\Windows` or `C:\Program Files`; redirect these to writable locations
@@ -23,7 +23,7 @@ Redirects are defined as a list in `.interposer/Config.yml` under the `Redirects
2323
```yaml
2424
Redirects:
2525
- Pattern: 'C:\\Games\\MyGame\\Saves\\(.+)'
26-
Replacement: '%APPDATA%\MyGame\Saves\$1'
26+
Replacement: '%USERPROFILE%\Saved Games\My Game\$1'
2727
```
2828
2929
:::tip Use single-quoted strings for patterns
@@ -51,12 +51,12 @@ Use parentheses to capture parts of the matched path for use in the replacement:
5151

5252
```yaml
5353
- Pattern: 'C:\\Games\\MyGame\\Saves\\(.+)'
54-
Replacement: '%APPDATA%\MyGame\Saves\$1'
54+
Replacement: '%USERPROFILE%\Saved Games\My Game\$1'
5555
```
5656

5757
For the path `C:\Games\MyGame\Saves\profile.dat`:
5858
- `$1` captures `profile.dat`
59-
- The replacement expands to `%APPDATA%\MyGame\Saves\profile.dat` (with `%APPDATA%` further expanded)
59+
- The replacement expands to `%USERPROFILE%\Saved Games\My Game\profile.dat` (with `%APPDATA%` further expanded)
6060

6161
Up to nine capture groups (`$1` through `$9`) are supported.
6262

@@ -102,9 +102,9 @@ FileRedirects:
102102
```yaml
103103
Redirects:
104104
- Pattern: 'C:\\Games\\MyGame\\Saves\\current\\(.+)'
105-
Replacement: '%APPDATA%\MyGame\Saves\slot1\$1'
105+
Replacement: '%USERPROFILE%\Saved Games\My Game\slot1\$1'
106106
- Pattern: 'C:\\Games\\MyGame\\Saves\\(.+)'
107-
Replacement: '%APPDATA%\MyGame\Saves\$1'
107+
Replacement: '%USERPROFILE%\Saved Games\My Game\$1'
108108
```
109109

110110
The first rule redirects any path under `current\` specifically; the second catches everything else under `Saves\`.

Interposer/Overview.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@ This compatibility shim is _not_ meant to tackle things like graphics APIs or Li
2929
## Installation and Use
3030
Configuration and use of the Interposer is broken down on this site under the following resources:
3131

32-
- [Releases](Releases)
33-
- [Getting Started](GettingStarted)
34-
- [Logging](Logging)
35-
- [File Redirection](FileRedirection)
36-
- [Registry Emulation](RegistryEmulation)
37-
- [FastDL](FastDL)
38-
- [Borderless Fullscreen](BorderlessFullscreen)
32+
- [Releases](/Interposer/Category/Releases)
33+
- [Getting Started](/Interposer/GettingStarted)
34+
- [Logging](/Interposer/Logging)
35+
- [File Redirection](/Interposer/FileRedirection)
36+
- [Registry Emulation](/Interposer/RegistryEmulation)
37+
- [FastDL](/Interposer/FastDL)
38+
- [Borderless Fullscreen](/Interposer/BorderlessFullscreen)
39+
- [Player Identity](/Interposer/PlayerIdentity)
40+
- [Plugins](/Interposer/Plugins/Overview)

Interposer/Plugins/CDKey.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
---
2+
sidebar_label: CD Key
3+
sidebar_position: 3
4+
---
5+
6+
# CD Key Plugin
7+
8+
The CD Key plugin generates a deterministic CD key from the player's username and injects it into a registry value so the game reads it as if the game were installed with that key. The same username always produces the same key for a given mask, so every player on a LAN has a unique key that is consistent across sessions without any manual entry.
9+
10+
## How It Works
11+
12+
On load the plugin:
13+
14+
1. Reads the key mask, registry key path suffix, and value name from `Config.yml`.
15+
2. Resolves the player username via the Interposer identity system (the `Player.Username` config value or `--username` injector flag, falling back to the real Windows account name).
16+
3. Generates a key by hashing the username with FNV-1a and stepping an LCG to fill each `*` position in the mask with a character from `[A-Z0-9]`.
17+
4. Injects the generated key into the virtual registry store using suffix matching, so any registered key whose path ends with the configured `KeyPath` receives the value.
18+
19+
The injection is transient — it is not written back to `.interposer\Registry.reg`.
20+
21+
## Setup
22+
23+
### 1. Add the key to Registry.reg
24+
25+
The target registry key must appear in `.interposer\Registry.reg` for registry emulation to intercept reads. If the game stores the CD key as the default (unnamed) value, an empty key header is sufficient:
26+
27+
```
28+
Windows Registry Editor Version 5.00
29+
30+
[HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Electronic Arts\EA Games\Battlefield 1942\ergc]
31+
```
32+
33+
If the key already has other values you want to preserve, export it from `regedit.exe` and include the full entry.
34+
35+
### 2. Configure the plugin
36+
37+
Add a `Plugins.CDKey` section to `.interposer\Config.yml`:
38+
39+
```yaml
40+
Plugins:
41+
CDKey:
42+
Mask: "****-**********-*******-****"
43+
KeyPath: "Battlefield 1942\\ergc"
44+
ValueName: "@"
45+
```
46+
47+
| Option | Required | Description |
48+
|---|---|---|
49+
| `Mask` | Yes | Key template — `*` is replaced with a generated character, all other characters are copied verbatim |
50+
| `KeyPath` | Yes | Suffix of the registry key path to target. Matched case-insensitively on a backslash boundary against all keys in the virtual store |
51+
| `ValueName` | No | Name of the registry value to write. Use `@` or omit entirely to target the default (unnamed) value. Defaults to `@` |
52+
53+
### 3. Place the plugin
54+
55+
Copy `LANCommander.Interposer.Plugin.CDKey.dll` into `.interposer\Plugins\` next to the main DLL:
56+
57+
```
58+
C:\Games\Battlefield 1942\
59+
BF1942.exe
60+
LANCommander.Interposer.dll
61+
.interposer\
62+
Config.yml
63+
Registry.reg
64+
Plugins\
65+
LANCommander.Interposer.Plugin.CDKey.dll
66+
```
67+
68+
## Mask Syntax
69+
70+
The mask defines the shape of the generated key. Any `*` character is replaced with a letter or digit (`[A-Z0-9]`). All other characters — including hyphens, spaces, and brackets — are preserved exactly as written.
71+
72+
| Mask | Example output |
73+
|---|---|
74+
| `****-****-****-****` | `K7MN-2BPQ-X4RT-9LWA` |
75+
| `****-**********-*******-****` | `K7MN-2BPQ3X4RT9L-WA5FM2Z-8QBR` |
76+
| `{****-****}` | `{K7MN-2BPQ}` |
77+
78+
## Log Output
79+
80+
With the plugin loaded, the session log shows the injected value:
81+
82+
```
83+
2025-03-14 12:00:01 [PLUGIN LOAD] ...\.interposer\Plugins\LANCommander.Interposer.Plugin.CDKey.dll
84+
2025-03-14 12:00:01 [CDKEY] Battlefield 1942\ergc\@ -> K7MN-2BPQ3X4RT9L-WA5FM2Z-8QBR
85+
```
86+
87+
If the configured `KeyPath` suffix matches no keys in the virtual store, a warning is logged instead:
88+
89+
```
90+
2025-03-14 12:00:01 [CDKEY] No virtual store keys matched suffix "Battlefield 1942\ergc" — add the key to Registry.reg
91+
```
92+
93+
## Notes
94+
95+
- Key generation is deterministic: the same username and mask always produce the same key.
96+
- The generated key is uppercase alphanumeric only. If a game requires a specific character set or checksum validation, a custom plugin with a tailored generation algorithm will be needed instead.
97+
- If `KeyPath` matches more than one key in the virtual store (e.g. two subkeys both ending with the same suffix), the value is injected into all of them and the log line notes how many keys were updated.
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
---
2+
sidebar_label: Creating a Plugin
3+
sidebar_position: 2
4+
---
5+
6+
# Creating a Plugin
7+
8+
A plugin is a standard Windows DLL (`.dll`) or ASI file (`.asi`) placed in `.interposer\Plugins\`. It has no link-time dependency on the Interposer — all API functions are resolved at runtime via `GetProcAddress`.
9+
10+
## Project Setup
11+
12+
Create a new DLL project targeting the same architecture as the game (x86 for 32-bit games, x64 for 64-bit games). No additional libraries or headers are required beyond the Windows SDK.
13+
14+
The only entry point needed is `DllMain`:
15+
16+
```cpp
17+
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD fdwReason, LPVOID /*lpReserved*/)
18+
{
19+
if (fdwReason == DLL_PROCESS_ATTACH)
20+
Initialize();
21+
return TRUE;
22+
}
23+
```
24+
25+
## Resolving the API
26+
27+
Declare function pointer types for the Interposer exports you need and resolve them with `GetProcAddress`. The Interposer may be loaded under different filenames depending on the deployment variant, so check the known names in order:
28+
29+
```cpp
30+
using FnInterposerLog = void (WINAPI*)(const wchar_t* verb, const wchar_t* message);
31+
using FnInterposerGetConfigString = BOOL (WINAPI*)(const wchar_t* dotPath, wchar_t* buf, DWORD bufSize);
32+
33+
static FnInterposerLog pfnLog = nullptr;
34+
static FnInterposerGetConfigString pfnGetConfig = nullptr;
35+
36+
static bool ResolveAPI()
37+
{
38+
static const wchar_t* kCandidates[] = {
39+
L"LANCommander.Interposer.dll",
40+
L"version.dll", // proxy variant
41+
};
42+
43+
HMODULE hInterposer = nullptr;
44+
for (const wchar_t* name : kCandidates)
45+
{
46+
hInterposer = GetModuleHandleW(name);
47+
if (hInterposer) break;
48+
}
49+
50+
if (!hInterposer) return false;
51+
52+
pfnLog = (FnInterposerLog) GetProcAddress(hInterposer, "InterposerLog");
53+
pfnGetConfig = (FnInterposerGetConfigString)GetProcAddress(hInterposer, "InterposerGetConfigString");
54+
55+
return pfnLog && pfnGetConfig;
56+
}
57+
```
58+
59+
## API Reference
60+
61+
All exported functions use the `WINAPI` (`__stdcall`) calling convention and undecorated `extern "C"` names.
62+
63+
### `InterposerLog`
64+
65+
```cpp
66+
void InterposerLog(const wchar_t* verb, const wchar_t* message);
67+
```
68+
69+
Writes a line to the session log regardless of the `Logging` flags in `Config.yml`. The log line format matches the rest of the session log:
70+
71+
```
72+
YYYY-MM-DD HH:MM:SS [VERB] <message>
73+
```
74+
75+
`verb` is normalised automatically: any existing `[`/`]` brackets and surrounding whitespace are stripped, the content is truncated to 16 characters, and it is re-wrapped as `[verb]` right-padded to 18 characters. Pass a plain string such as `L"MYPLUGIN"` — no manual padding required.
76+
77+
---
78+
79+
### `InterposerGetConfigString`
80+
81+
```cpp
82+
BOOL InterposerGetConfigString(const wchar_t* dotPath, wchar_t* buffer, DWORD bufferSize);
83+
```
84+
85+
Reads a scalar value from `Config.yml` by dot-separated YAML path. Returns `TRUE` on success, `FALSE` if the key does not exist, is not a scalar, or the buffer is too small.
86+
87+
`bufferSize` is in `wchar_t` units and must include room for the null terminator.
88+
89+
```cpp
90+
wchar_t setting[256];
91+
if (pfnGetConfig(L"Plugins.MyPlugin.Setting", setting, ARRAYSIZE(setting)))
92+
{
93+
// use setting
94+
}
95+
```
96+
97+
Plugin configuration should live under a `Plugins.<PluginName>` namespace in `Config.yml` to avoid collisions:
98+
99+
```yaml
100+
Plugins:
101+
MyPlugin:
102+
Setting: hello
103+
Count: 42
104+
```
105+
106+
---
107+
108+
### `InterposerGetUsername`
109+
110+
```cpp
111+
BOOL InterposerGetUsername(wchar_t* buffer, DWORD bufferSize);
112+
```
113+
114+
Returns the effective player username: the value configured in `Config.yml` under `Player.Username` or passed via the `--username` injector flag. Falls back to the real Windows account name (`GetUserNameW`) if no override is configured.
115+
116+
`bufferSize` is in `wchar_t` units including the null terminator. Returns `TRUE` on success.
117+
118+
---
119+
120+
### `InterposerSetRegistryValue`
121+
122+
```cpp
123+
void InterposerSetRegistryValue(const wchar_t* keyPath, const wchar_t* valueName, const wchar_t* value);
124+
```
125+
126+
Injects a `REG_SZ` string value into the in-memory virtual registry store. Subsequent `RegQueryValueEx` calls for `keyPath\valueName` return `value` without touching the real registry. The injection is transient — it is not persisted to `.interposer\Registry.reg`.
127+
128+
`keyPath` must be a full path beginning with a hive name:
129+
130+
```
131+
HKEY_LOCAL_MACHINE\SOFTWARE\MyGame\1.0
132+
```
133+
134+
Set `valueName` to `L"@"`, `L""`, or `nullptr` to target the default (unnamed) registry value — the entry shown as `(Default)` in Registry Editor.
135+
136+
:::note
137+
The target key must already exist in `.interposer\Registry.reg` for reads to be intercepted. Add an empty key header if no values need to be pre-populated:
138+
139+
```
140+
[HKEY_LOCAL_MACHINE\SOFTWARE\MyGame\1.0]
141+
```
142+
:::
143+
144+
---
145+
146+
### `InterposerSetRegistryValueBySuffix`
147+
148+
```cpp
149+
DWORD InterposerSetRegistryValueBySuffix(const wchar_t* keySuffix, const wchar_t* valueName, const wchar_t* value);
150+
```
151+
152+
Like `InterposerSetRegistryValue`, but matches by suffix rather than exact path. Any key in the virtual store whose path ends with `\keySuffix` (matched case-insensitively on a backslash component boundary) receives the injected value.
153+
154+
Returns the number of keys updated. A return value of `0` means the suffix matched nothing in the virtual store — check that the target key is present in `.interposer\Registry.reg`.
155+
156+
This is useful when the full registry path varies between game versions or installations:
157+
158+
```cpp
159+
// Matches HKEY_LOCAL_MACHINE\...\Electronic Arts\EA Games\Battlefield 1942\ergc
160+
// regardless of any intermediate path components.
161+
pfnSetBySuffix(L"Battlefield 1942\\ergc", L"@", generatedKey);
162+
```
163+
164+
## Minimal Example
165+
166+
```cpp
167+
#define WIN32_LEAN_AND_MEAN
168+
#include <windows.h>
169+
#include <string>
170+
171+
using FnInterposerLog = void (WINAPI*)(const wchar_t*, const wchar_t*);
172+
using FnInterposerGetConfigString = BOOL (WINAPI*)(const wchar_t*, wchar_t*, DWORD);
173+
174+
static FnInterposerLog pfnLog = nullptr;
175+
static FnInterposerGetConfigString pfnGetConfig = nullptr;
176+
177+
static void Initialize()
178+
{
179+
HMODULE h = GetModuleHandleW(L"LANCommander.Interposer.dll");
180+
if (!h) h = GetModuleHandleW(L"version.dll");
181+
if (!h) return;
182+
183+
pfnLog = (FnInterposerLog) GetProcAddress(h, "InterposerLog");
184+
pfnGetConfig = (FnInterposerGetConfigString)GetProcAddress(h, "InterposerGetConfigString");
185+
if (!pfnLog || !pfnGetConfig) return;
186+
187+
wchar_t greeting[256] = L"hello";
188+
pfnGetConfig(L"Plugins.MyPlugin.Greeting", greeting, ARRAYSIZE(greeting));
189+
190+
pfnLog(L"MYPLUGIN", greeting);
191+
}
192+
193+
BOOL APIENTRY DllMain(HMODULE, DWORD reason, LPVOID)
194+
{
195+
if (reason == DLL_PROCESS_ATTACH)
196+
Initialize();
197+
return TRUE;
198+
}
199+
```
200+
201+
```yaml
202+
# .interposer\Config.yml
203+
Plugins:
204+
MyPlugin:
205+
Greeting: "Plugin loaded successfully"
206+
```

0 commit comments

Comments
 (0)