* 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 同时清掉上次 的页记录,页文本相同也会重新载入。
69 lines
2.4 KiB
C++
69 lines
2.4 KiB
C++
#pragma once
|
|
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cwchar>
|
|
#include <cstdlib>
|
|
#include <string>
|
|
|
|
// Candidate page handed to a UI-less (host-drawn) TSF client, carried by FanyImeReplyType::UiLessComposition and
|
|
// FanyImeWorkerReplyType::UiLessCandidatesChanged:
|
|
//
|
|
// <preedit> \t <page> \t <selection> \t <generation>
|
|
//
|
|
// page is the current page's words joined by ','; selection is page-relative. generation is the Server's candidate
|
|
// page generation: the worker push and the key reply travel on different pipes, so the client applies a page only
|
|
// when it is not older than the one it already shows. Generation 0 means "unknown" (a Server predating the field)
|
|
// and is always applied. DLLs that predate the field read selection with _wtoi, which stops at the third tab.
|
|
namespace FanyImeUiLessPage
|
|
{
|
|
struct Payload
|
|
{
|
|
std::wstring preedit;
|
|
std::wstring page;
|
|
int selection = 0;
|
|
std::uint64_t generation = 0;
|
|
// The payload carried a page and selection, i.e. it was built for a UI-less client.
|
|
bool hasPage = false;
|
|
};
|
|
|
|
inline std::wstring Format(const std::wstring &preedit, const std::wstring &page, int selection,
|
|
std::uint64_t generation)
|
|
{
|
|
return preedit + L'\t' + page + L'\t' + std::to_wstring(selection) + L'\t' + std::to_wstring(generation);
|
|
}
|
|
|
|
inline Payload Parse(const std::wstring &payload)
|
|
{
|
|
Payload result;
|
|
const std::size_t firstTab = payload.find(L'\t');
|
|
if (firstTab == std::wstring::npos)
|
|
{
|
|
result.preedit = payload;
|
|
return result;
|
|
}
|
|
result.preedit = payload.substr(0, firstTab);
|
|
const std::size_t secondTab = payload.find(L'\t', firstTab + 1);
|
|
if (secondTab == std::wstring::npos)
|
|
{
|
|
result.page = payload.substr(firstTab + 1);
|
|
return result;
|
|
}
|
|
result.page = payload.substr(firstTab + 1, secondTab - firstTab - 1);
|
|
result.hasPage = true;
|
|
result.selection = static_cast<int>(std::wcstol(payload.c_str() + secondTab + 1, nullptr, 10));
|
|
const std::size_t thirdTab = payload.find(L'\t', secondTab + 1);
|
|
if (thirdTab != std::wstring::npos)
|
|
{
|
|
result.generation = std::wcstoull(payload.c_str() + thirdTab + 1, nullptr, 10);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// True when a page tagged `incoming` may replace the one tagged `shown`.
|
|
inline bool IsCurrent(std::uint64_t incoming, std::uint64_t shown)
|
|
{
|
|
return incoming == 0 || incoming >= shown;
|
|
}
|
|
} // namespace FanyImeUiLessPage
|