Kenshi is a unique sandbox RPG known for its brutal difficulty and open-ended gameplay. As a single-player game, it naturally lacks multiplayer support. This project explores whether we can add cooperative multiplayer through external code injection—without touching the game's original binaries or MOD system.
The Challenge
Adding multiplayer to a game that was never designed for it presents several challenges:
- No network code exists — Everything must be built from scratch
- No source code — We work with compiled binaries only
- Anti-mod architecture — The game's MOD system doesn't support gameplay modification
- Complex game state — Hundreds of entities with positions, stats, inventory, etc.
Approach: External DLL Injection
Instead of modifying game files, we inject a DLL into the running process:
┌─────────────────────────────────────────────────────────┐
│ Kenshi.exe │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Game Memory Space │ │
│ │ ┌─────────────┐ ┌──────────────────────────┐ │ │
│ │ │ Game Code │ │ Injected KenshiMP.dll │ │ │
│ │ │ (Original) │ │ • Memory hooks │ │ │
│ │ │ │ │ • Network sync │ │ │
│ │ │ │ │ • State management │ │ │
│ │ └─────────────┘ └──────────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
The Injection Process
// Launcher: KenshiMultiplayer.exe
bool InjectDLL(HANDLE hProcess, const char* dllPath) {
// Allocate memory in target process for DLL path
LPVOID pRemotePath = VirtualAllocEx(
hProcess,
nullptr,
strlen(dllPath) + 1,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE
);
if (!pRemotePath) return false;
// Write DLL path to target process
WriteProcessMemory(hProcess, pRemotePath, dllPath, strlen(dllPath) + 1, nullptr);
// Get LoadLibraryA address
HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
FARPROC pLoadLibrary = GetProcAddress(hKernel32, "LoadLibraryA");
// Create remote thread to load our DLL
HANDLE hThread = CreateRemoteThread(
hProcess,
nullptr,
0,
(LPTHREAD_START_ROUTINE)pLoadLibrary,
pRemotePath,
0,
nullptr
);
WaitForSingleObject(hThread, INFINITE);
// Cleanup
VirtualFreeEx(hProcess, pRemotePath, 0, MEM_RELEASE);
CloseHandle(hThread);
return true;
}Memory Hooking: Finding the Right Addresses
The core of the multiplayer system is hooking game functions to intercept and synchronize state. We need to find memory addresses for:
1. Character Position
// Pattern scanning for character position structure
void* FindCharacterBase() {
// Known pattern from reverse engineering
// mov rax, [CharacterPool + offset]
// Pattern: 48 8B 05 ?? ?? ?? ?? 48 85 C0
const char pattern[] = "\x48\x8B\x05\x00\x00\x00\x00\x48\x85\xC0";
const char mask[] = "xxx???xxx";
return ScanPattern(GetModuleHandle(nullptr), pattern, mask, 10);
}2. Hooking Character Updates
// Trampoline hook for character position updates
void __fastcall HookedUpdatePosition(Character* character, float x, float y, float z) {
// Store original position
Vector3 originalPos = {character->x, character->y, character->z};
// Call original function
OriginalUpdatePosition(character, x, y, z);
// Broadcast to network if this is the selected character
if (character == GetSelectedCharacter()) {
NetworkManager::BroadcastPosition({
.id = character->id,
.x = character->x,
.y = character->y,
.z = character->z
});
}
}State Synchronization
Network Architecture
┌────────────┐ ┌────────────┐
│ Host │◄────── TCP ──────►│ Client │
│ │ │ │
│ • Authoritative │ • Receive state
│ • Broadcasts │ │ • Apply locally
│ • MOD verify│ │ • Predict
└────────────┘ └────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────┐
│ Synced State │
│ • Selected character position/HP │
│ • Gold amount │
│ • World time │
└─────────────────────────────────────────────┘
Position Sync Protocol
#pragma pack(push, 1)
struct PositionPacket {
uint8_t type; // PACKET_POSITION
uint32_t characterId;
float x, y, z;
uint32_t timestamp;
};
#pragma pack(pop)
void PositionSync::Update() {
// Host: broadcast selected character position
if (IsHost()) {
Character* selected = GetSelectedCharacter();
if (selected && selected->positionChanged) {
PositionPacket pkt = {
.type = PACKET_POSITION,
.characterId = selected->id,
.x = selected->x,
.y = selected->y,
.z = selected->z,
.timestamp = GetTickCount()
};
Broadcast(&pkt, sizeof(pkt));
selected->positionChanged = false;
}
}
// Client: apply received positions
else {
ApplyPendingPositions();
}
}MOD Verification
To ensure all players have the same game state, we verify MOD lists before connecting:
bool VerifyMODList(const std::vector<std::string>& clientMods) {
auto hostMods = GetInstalledMODs();
if (hostMods.size() != clientMods.size()) {
return false;
}
for (size_t i = 0; i < hostMods.size(); i++) {
if (hostMods[i].name != clientMods[i].name ||
hostMods[i].hash != clientMods[i].hash) {
return false;
}
}
return true;
}Crash Protection
Game crashes can happen unexpectedly. We implement SEH (Structured Exception Handling) to capture crash dumps:
__declspec(noinline) LONG WINAPI CrashHandler(EXCEPTION_POINTERS* pException) {
// Create minidump
HANDLE hFile = CreateFileA(
"kenshi_mp_crash.dmp",
GENERIC_WRITE,
0,
nullptr,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
nullptr
);
if (hFile != INVALID_HANDLE_VALUE) {
MINIDUMP_EXCEPTION_INFORMATION mei = {
.ThreadId = GetCurrentThreadId(),
.ExceptionPointers = pException,
.ClientPointers = FALSE
};
MiniDumpWriteDump(
GetCurrentProcess(),
GetCurrentProcessId(),
hFile,
MiniDumpWithDataSegs,
&mei,
nullptr,
nullptr
);
CloseHandle(hFile);
}
return EXCEPTION_CONTINUE_SEARCH;
}
// Install crash handler
SetUnhandledExceptionFilter(CrashHandler);Results and Limitations
What Works
- Character position synchronization
- Health point display
- Gold amount sync
- World time sync
- Reconnection within 90 seconds
Current Limitations
- Only selected character synced (not full world state)
- No NPC synchronization
- No combat state sync
- Offset-dependent (requires updates for game patches)
Ethical Considerations
This project is for educational and research purposes only. Key principles:
- No game modification — Original binaries remain untouched
- No cheat functionality — Multiplayer only, no gameplay advantages
- MOD compatibility — Works within existing MOD ecosystem
- Open source — Code is auditable under AGPL-3.0
References
- GitHub Repository(opens in a new tab)
- Offset Sources(opens in a new tab)
- RE_Kenshi project for reference implementation
This article covers the technical implementation. Game modification tools should be used responsibly and in accordance with the game's terms of service.