* fix(installer): TSF DLL 改装到系统目录,CS2 等 Trusted Mode 游戏里可以切出输入法 CS2 的 Trusted Mode 只放行位于系统目录且有代码签名的第三方 in-proc DLL, 装在 Program Files 下的 DLL 会被拒绝加载,游戏里切不出输入法。 x64 DLL 改装到 System32\IME\MetasequoiaIME\msime_v*,x86 装到 SysWOW64 下同名目录, 语言配置图标随之放到两处根目录;升级时清掉旧版留在 Program Files 的版本目录。 * fix(windows): CS2、Dota 2 不理会 UI-less 声明,照常显示候选窗 起源 2 引擎的 Valve 游戏以 UI-less 激活 TSF,但只替白名单里的输入法绘制候选。 我们据声明收起了自己的候选窗,游戏又不画,结果只剩左上角的系统默认 IME 小框,只能盲打。 对 cs2.exe、dota2.exe 忽略 UI-less,照常由 Server 绘制候选窗。 * fix(server): 前台为 D3D 独占全屏时不弹候选窗与光标状态提示 独占全屏下别的进程的窗口画不上去,弹出去反而可能把游戏踢出独占模式(闪黑、切分辨率、卡死)。 在 UI 线程真正显示前用 SHQueryUserNotificationState 判一次(300ms 缓存),独占全屏就收起候选窗, 并清掉已渲染可见标记,数字键与空格选词不再等待不会到来的绘制。 * fix(engine,server,windows): 游戏自绘候选时后台改动的候选列表实时同步给宿主 英雄联盟等 UI-less 宿主自己画候选。云候选、AI 候选、模型重排、英文混排在按键回复之后才改 候选列表,却从不通知 DLL,游戏还画着上一键读到的旧列表,数字键和空格却按新列表上屏, 于是「选的和上屏的不一样」。 - 契约新增 Worker opcode 35 UiLessCandidatesChanged,载荷与 UiLessComposition 同形, 由 uiless_page.h 统一格式化与解析,并附候选页代号;旧 DLL 读选中位时在第三个制表符处停下,兼容。 - Server 在上述后台改动之后把新页推给候选归属的客户端。 - DLL 收到后回到 TSF 线程核对焦点与组合纪元,更新候选 UI 元素并通知宿主重读。推送与按键回复 走不同管道,按代号只接受不旧于当前的页,避免旧页盖掉新页。 - 从共享内存重读到不同的页时高亮回到第一项,与 Server 每次重建都选中首项一致, 不再保留旧高亮造成「高亮这个、空格上屏那个」。 * feat(windows,server,ui-html,installer): 游戏兼容开关,CS2 / Dota 2 独占全屏打字不再黑屏 CS2、Dota 2 只替内置白名单(按输入法登记名称全等比对)里的输入法处理输入法消息。不在名单里时 组合落到 Windows 默认组合窗 CiceroUIWndFrame,左上角弹出编码小框;独占全屏下这个窗口一出现 游戏就被踢出全屏,表现为一打字就黑屏。清风输入法未开兼容时同样如此。 - 设置页帮助页新增「游戏兼容(CS2 / Dota 2)」:开启后把水杉在系统里登记的名称换成白名单里的 名字(默认「拼音输入法」),游戏会自己画候选。需要管理员权限:设置进程以 --apply-game-compat-name 提权重启自身,写 HKLM 记录值并改语言配置的 Description。 - TSF DLL 注册时以记录值为准,安装/升级重新注册不再把用户选的名称冲掉;卸载时删除记录值。 - 开启时不再对 cs2.exe / dota2.exe 忽略「宿主自己画候选」的声明,避免两个候选框。 - Server 的独占全屏判断覆盖进出独占模式时的 QUNS_BUSY 间隙,不在切换途中弹候选窗。 - TSF 诊断日志在组合开始/结束与 BeginUIElement 时记录宿主窗口状态(只有窗口与标志,没有文本)。 * fix(windows): 后台刷新回调持有加载器引用,宿主卸载 TSF DLL 后不再崩溃 当前输入法是水杉时,CS2 启动途中会加载再卸载 TSF DLL。诊断日志的线程池回调 FlushTsfDiagnosticLogs 正在 Sleep(250),只靠 DllAddRef 撑着:那只回答 DllCanUnloadNow, 挡不住宿主直接卸载。DLL 卸载后 SDL3 映射进同一段地址,回调醒来跑进 SDL3 的代码, 游戏崩溃(0xc0000005,崩溃转储里卸载模块的地址范围与崩溃地址吻合)。 - 诊断日志与统计上报的线程池回调改为用 GetModuleHandleEx 取加载器引用, 回调里 FreeLibraryWhenCallbackReturns,函数完全返回后才放手。 - 析构函数把 DllRelease 挪到最后,不在等待 IPC 线程退出之前就让 DLL 可卸载。 * fix(windows): 游戏自绘候选时不再把 Server 的 HTML 候选页当成候选交给游戏 CS2 开了游戏兼容后打一个码,游戏里的候选只有一项且是乱码。宿主要等 BeginUIElement 才说 自己画候选,而那时组合首键已经不带 UI-less 标记发给了 Server;Server 往共享内存写的是给 自己候选窗用的 HTML 页(辅助码、译文 span),DLL 的候选元素从共享内存回读后整串交给了游戏。 - 开了游戏兼容名称的 cs2.exe / dota2.exe 在 ActivateEx 就认定由游戏画候选,每个按键都带标记。 - 候选会话里收到过 Server 的 UI-less 页之后才从共享内存回读,之前一律不读。 * fix(windows): 游戏自绘候选时翻页能刷新,部分上屏后不再出现乱码候选 三角洲行动(虚幻引擎,以 TF_TMF_UIELEMENTENABLEDONLY 激活)里按 = / - / PageUp / PageDown 翻页,Server 已翻到第 N 页,游戏却还画着第一页;接着按空格上屏的是第 N 页的首项(多为单字), 剩下的拼音继续组合时候选框里出现乱码。 - 翻页、移高亮等键走异步的 WM_AsyncServerCandidateKey,以前把回复当作「只是确认」丢掉; UI-less 下这份回复就是新的候选页,现在应用并通知宿主重读。 - UI-less 的 _SetText 拿不到 Server 的候选页时不再退回本地引擎的占位列表——那不是候选, 宿主会画成一条乱码。部分上屏后从共享内存回读 Server 写好的新页;_ClearList 同时清掉上次 的页记录,页文本相同也会重新载入。
Product contracts
These headers are independently consumable C++17 contracts. Including them does not link the input engine, start threads, access dictionaries, or require any third-party code. The TSF text service consumes only these headers; the server consumes the same source by linking the engine. Both live in this repository — windows/ and server/ — and so does the engine, so one commit covers all three and there is no pin to keep in step.
windows_ipc.h owns the wire layouts, pipe names and opcodes. voice_composition_pipe.h owns voice framing. uiless_page.h owns the candidate-page payload for UI-less (host-drawn) clients. Consumers may wrap these headers but must not duplicate their definitions.
punctuation/policy.json is the source of truth for the ASCII punctuation keys and their Chinese
translations. Its generated policy.h is consumed by the engine and included directly by the hosts
that route punctuation keys. Stateful quote alternation and nested book-title marks remain
part of the same contract, rather than being re-created in each host.
product_lock.py contains the shared product-lock primitives used by the three platform locks.
See product_lock.md for the vendoring and byte-equality rule.
Main handshake
Reverse endpoints retain their 16-byte hello and PipeReady acknowledgement. Once both endpoints are registered, the new client sends the existing 304-byte ClientHello with:
| Field | Meaning |
|---|---|
| keycode | MSIP magic |
| wch | Major wire protocol version |
| point[0] | Minor additive version |
| modifiers_down | Supported capability bits |
| point[1] | Required capability bits |
| request_id | Nonzero registration correlation ID |
The Server sends ProtocolReady or ProtocolMismatch on the registered reply endpoint before activating the main route. The reply echoes the correlation ID and contains major/minor, negotiated capabilities and magic in the first six UTF-16 units. These packets are consumed only during registration and are never text commits.
Existing DLLs with unversioned hello remain accepted by the new Server using the established v1 semantics; they receive no new opcode. New DLLs require an acknowledgement from the new Server within the existing bounded handshake budget. Against an old or incompatible Server they remain disconnected and use the existing raw-input fallback, rather than interpreting unnegotiated frames. A late acknowledgement from an old registration cannot authorize another one.
Append opcodes; never change released values or reuse them. An incompatible layout needs a new major protocol and an explicit migration, not another copy of a header. Minor additions must be optional capabilities.
CharacterSetShortcut is optional and is not included in the default capability set.
Windows implementations that support it pass Capabilities | CharacterSetShortcut
to Hello / Negotiate. Only after negotiating this bit may a Chinese-mode client
consume Ctrl+Shift+F (no Alt or Windows modifier) and send the existing KeyEvent
with keycode F and Shift/Control modifier bits. The server toggles the persisted
character set without clearing composition, and refreshes the current candidate
page and toolbar. It sends no key reply. Clients suppress auto-repeat and must not
replay an ambiguously delivered toggle; older peers simply omit the optional bit.
tests/windows_ipc_contract.cpp executes wire-layout/upgrade/framing cases on all Engine CI platforms and both Windows TSF architectures.
TrilingualCycle is optional. A supporting client advertises its capability and consumes
the existing configured language hotkeys only while the Server's TrilingualCycleChanged
setting ([keybindings] trilingual_cycle) is enabled. After matching a hotkey through the
existing shortcut handler, the client sends a normalized VK_SHIFT whose modifiers_down
carries FanyImeTrilingualInput::EncodeCycleRequest(ime_open, japanese): an explicit request
bit plus the client's own OPENCLOSE and native-language state. A plain VK_SHIFT is still the
binary toggle and is never treated as a cycle, and raw Shift key-down events never request one.
The Server computes the destination from the request's state with
FanyImeTrilingualInput::Destination; if its setting was turned off while the request was in
flight, it still answers, with the binary-toggle destination.
The Server retains any already selected word prefix and commits the remaining raw spelling,
matching the existing language toggle. It replies with TrilingualCycle: destination
(0 Chinese, 1 Japanese, 2 English),
a tab separator, and the exact text to commit. The client commits and ends composition before
applying the destination compartment in the same edit session. The Server must not also send
a worker candidate commit or compartment switch for that key, and leaves the requester out of
the InputModeChanged broadcast: the reply is that client's language update.
Clients that did not negotiate the capability retain their existing language toggle.
backend/protocol.json 定义 MSIME-Backend 的可选共通 HTTP 服务 API,详见后端协议与兼容性。