Kenshi 是一款独特的沙盒 RPG,以其残酷的难度和开放的游戏性著称。作为一款单机游戏,它天生不支持多人联机。本项目探索是否可以通过外部代码注入添加协作多人模式——而不触碰游戏的原始二进制文件或 MOD 系统。
技术挑战
为一款从未设计过多人模式的游戏添加联机功能面临诸多挑战:
- 没有网络代码 — 一切必须从零构建
- 没有源代码 — 我们只能操作编译后的二进制文件
- 反 MOD 架构 — 游戏的 MOD 系统不支持玩法修改
- 复杂的游戏状态 — 数百个实体,每个都有位置、属性、物品等
方案:外部 DLL 注入
我们不修改游戏文件,而是向运行中的进程注入一个 DLL:
┌─────────────────────────────────────────────────────────┐
│ Kenshi.exe │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 游戏内存空间 │ │
│ │ ┌─────────────┐ ┌──────────────────────────┐ │ │
│ │ │ 游戏代码 │ │ 注入的 KenshiMP.dll │ │ │
│ │ │ (原始) │ │ • 内存 hook │ │ │
│ │ │ │ │ • 网络同步 │ │ │
│ │ │ │ │ • 状态管理 │ │ │
│ │ └─────────────┘ └──────────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
注入过程
// 启动器: KenshiMultiplayer.exe
bool InjectDLL(HANDLE hProcess, const char* dllPath) {
// 在目标进程中为 DLL 路径分配内存
LPVOID pRemotePath = VirtualAllocEx(
hProcess,
nullptr,
strlen(dllPath) + 1,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE
);
if (!pRemotePath) return false;
// 将 DLL 路径写入目标进程
WriteProcessMemory(hProcess, pRemotePath, dllPath, strlen(dllPath) + 1, nullptr);
// 获取 LoadLibraryA 地址
HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
FARPROC pLoadLibrary = GetProcAddress(hKernel32, "LoadLibraryA");
// 创建远程线程来加载我们的 DLL
HANDLE hThread = CreateRemoteThread(
hProcess,
nullptr,
0,
(LPTHREAD_START_ROUTINE)pLoadLibrary,
pRemotePath,
0,
nullptr
);
WaitForSingleObject(hThread, INFINITE);
// 清理
VirtualFreeEx(hProcess, pRemotePath, 0, MEM_RELEASE);
CloseHandle(hThread);
return true;
}内存 Hook:找到正确的地址
多人系统的核心是 Hook 游戏函数来拦截和同步状态。我们需要找到以下内容的内存地址:
1. 角色位置
// 通过模式扫描找角色结构基址
void* FindCharacterBase() {
// 通过逆向工程发现的已知模式
// 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. Hook 角色更新
// 角色位置更新的 Trampoline hook
void __fastcall HookedUpdatePosition(Character* character, float x, float y, float z) {
// 保存原始位置
Vector3 originalPos = {character->x, character->y, character->z};
// 调用原始函数
OriginalUpdatePosition(character, x, y, z);
// 如果是选中的角色,广播到网络
if (character == GetSelectedCharacter()) {
NetworkManager::BroadcastPosition({
.id = character->id,
.x = character->x,
.y = character->y,
.z = character->z
});
}
}状态同步
网络架构
┌────────────┐ ┌────────────┐
│ 主机 │◄────── TCP ──────►│ 客户端 │
│ │ │ │
│ • 权威状态 │ │ • 接收状态 │
│ • 广播变更 │ │ • 本地应用 │
│ • MOD 验证 │ │ • 预测补偿 │
└────────────┘ └────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────┐
│ 同步状态 │
│ • 选中角色的位置/HP │
│ • 金币数量 │
│ • 世界时间 │
└─────────────────────────────────────────────┘
位置同步协议
#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() {
// 主机: 广播选中角色位置
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;
}
}
// 客户端: 应用接收到的位置
else {
ApplyPendingPositions();
}
}MOD 验证
为确保所有玩家拥有相同的游戏状态,连接前需验证 MOD 列表:
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;
}崩溃保护
游戏崩溃可能随时发生。我们实现 SEH(结构化异常处理)来捕获崩溃转储:
__declspec(noinline) LONG WINAPI CrashHandler(EXCEPTION_POINTERS* pException) {
// 创建 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;
}
// 安装崩溃处理器
SetUnhandledExceptionFilter(CrashHandler);结果与限制
已实现功能
- 角色位置同步
- 生命值显示
- 金币数量同步
- 世界时间同步
- 90 秒内断线重连
当前限制
- 仅同步选中角色(非完整世界状态)
- 无 NPC 同步
- 无战斗状态同步
- 依赖固定偏移(游戏更新后需调整)
伦理考量
本项目仅用于教育和研究目的。核心原则:
- 不修改游戏 — 原始二进制文件保持不变
- 不作弊 — 仅多人联机,无游戏优势
- MOD 兼容 — 在现有 MOD 生态内工作
- 开源 — 代码可审计,AGPL-3.0 许可
参考
- GitHub 仓库(opens in a new tab)
- 偏移来源(opens in a new tab)
- RE_Kenshi 项目参考实现
本文涵盖技术实现。游戏修改工具应负责任地使用,并遵守游戏服务条款。