Files
mtgodot-poc/extension/src/platform/EterLib/IME.cpp
T
shenleiandClaude Opus 5.5 6ea0fa9889 port 4-a: text input through CPythonIME (login typed, in-game chat)
- UserInterface/PythonIME(+Module) ported verbatim; ime module stubs removed, initime() after initdbg()
- EterLib/IME.cpp in platform/: 40250 buffer/WMChar bodies, IMM/TSF processors no-op; committed
  multi-byte text via PythonBoot::UIChar emulating GCS_RESULTSTR
- GameApplicationAdapter RunIME* -> CWindowManager; Godot keys -> ui_ime_key/ui_char
- port.login_flow/login_live type the account through the IME; chat CG_CHAT round-trip on the fake server
- python_game_render_test logs in with Godot key events

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 08:35:53 +09:00

753 lines
17 KiB
C++

// Platform implementation of EterLib/IME.h (40250 EterLib/IME.cpp).
// The edit buffer (m_wText, cursor, SetText/GetText, OnChar/InsertString, IsMax, the colour-tag aware cursor
// moves) and WMChar are the 40250 bodies verbatim. What 40250 gets from Win32 IMM/TSF — composition strings,
// the candidate list, the reading window, keyboard-layout code pages — has no counterpart under Godot: the
// host delivers committed characters only (Metin2PythonHost.ui_char -> WMChar, or InsertString for text the
// input code page cannot hold, which is what 40250's ResultProcess does with a GCS_RESULTSTR). Those entry
// points keep 40250's "no IME present" behaviour: no composition, empty candidate/reading lists.
#include "EterLib/StdAfx.h"
#include "EterLib/IME.h"
#include "EterLib/TextTag.h"
#include "EterLib/GrpTextInstance.h"
#include "EterLib/Util.h"
#include <algorithm>
#include <cwctype>
namespace
{
// 40250 s_aszIndicator[INDICATOR_NON_IME] (IME.cpp:67); CheckInputLocale overwrites it with the locale's
// two-letter abbreviation, which here is the locale code page's language.
wchar_t s_aszNonImeIndicator[3] = L"En";
}
/*---------------------------------------------------------------------------*/ /* Public */
CIME::CIME()
{
ms_hWnd = NULL;
ms_bCandidateList = false;
ms_bReadingInformation = false;
Clear();
m_max = 0;
m_userMax = 0;
m_bOnlyNumberMode = FALSE;
m_hOrgIMC = NULL;
m_bEnablePaste = false;
m_bUseDefaultIME = false;
}
CIME::~CIME()
{
}
// PORT: 40250 loads imm32.dll, disables Cicero, reads the keyboard layout and sets up the TSF sinks. The
// Godot host has no IMM context; only the part the edit buffer depends on — the input/output code page from
// CheckInputLocale and SetSupportLevel(2) — is kept.
bool CIME::Initialize(HWND hWnd)
{
if(ms_bInitialized)
return true;
ms_hWnd = hWnd;
ms_bDisableIMECompletely = true;
ms_bInitialized = true;
CheckInputLocale();
ChangeInputLanguageWorker();
SetSupportLevel(2);
ms_bUILessMode = false;
CheckToggleState();
return true;
}
void CIME::Uninitialize()
{
if ( !ms_bInitialized )
return;
ms_hWnd = NULL;
m_hOrgIMC = NULL;
ms_bInitialized = false;
}
void CIME::UseDefaultIME()
{
m_bUseDefaultIME = true;
}
bool CIME::IsIMEEnabled()
{
return ms_bImeEnabled;
}
// PORT: 40250 ImmAssociateContext()s the window; ms_bDisableIMECompletely (no imm32) forces bEnable=false,
// which is always the case here.
void CIME::EnableIME(bool bEnable)
{
if (!ms_bInitialized || !ms_hWnd)
return;
if (ms_bDisableIMECompletely)
bEnable = false;
ms_bImeEnabled = bEnable;
if (bEnable)
CheckToggleState();
}
void CIME::DisableIME()
{
EnableIME(false);
}
void CIME::EnableCaptureInput()
{
ms_bCaptureInput = true;
}
void CIME::DisableCaptureInput()
{
ms_bCaptureInput = false;
}
bool CIME::IsCaptureEnabled()
{
return ms_bCaptureInput;
}
void CIME::Clear()
{
ms_lastpos = 0;
ms_curpos = 0;
ms_compLen = 0;
ms_ulbegin = 0;
ms_ulend = 0;
}
int CIME::GetReading(std::string & rstrText)
{
char reading[IMEREADING_MAXLEN];
if(ms_wstrReading.size() == 0)
return 0;
int readingLen = WideCharToMultiByte(ms_uOutputCodePage, 0, &ms_wstrReading[0], ms_wstrReading.size(), reading, sizeof(reading), NULL, NULL);
rstrText.append(GetCodePageText());
rstrText.append(reading, reading + readingLen);
return rstrText.size();
}
int CIME::GetReadingError()
{
return ms_iReadingError;
}
void CIME::SetMax(int iMax)
{
m_max = iMax;
}
void CIME::SetUserMax(int iMax)
{
m_userMax = iMax;
}
void CIME::SetText(const char* szText, int len)
{
ms_compLen = 0;
ms_ulbegin = 0;
ms_ulend = 0;
const char* begin = szText;
const char* end = begin + len;
const char* iter = FindToken(begin, end);
int m_wTextLen = sizeof(m_wText)/sizeof(wchar_t);
ms_lastpos = MultiByteToWideChar(ms_uInputCodePage, 0, begin, iter-begin, m_wText, m_wTextLen);
if (iter < end)
ms_lastpos += MultiByteToWideChar(ReadToken(iter), 0, (iter+5), end-(iter+5), m_wText+ms_lastpos, m_wTextLen-ms_lastpos);
ms_curpos = std::min(ms_curpos, ms_lastpos);
}
int CIME::GetText(std::string & rstrText, bool addCodePage)
{
int outCodePage = ms_uOutputCodePage;
int dataCodePage;
switch (outCodePage)
{
//case 1256: // ARABIC
case 1268: // VIETNAM
dataCodePage = CP_UTF8;
break;
default:
dataCodePage = outCodePage;
}
int len = 0;
char text[IMESTR_MAXLEN];
len += WideCharToMultiByte(dataCodePage, 0, m_wText, ms_curpos, text, sizeof(text)-len, NULL, NULL);
len += WideCharToMultiByte(dataCodePage, 0, m_wszComposition, ms_compLen, text+len, sizeof(text)-len, NULL, NULL);
len += WideCharToMultiByte(dataCodePage, 0, m_wText+ms_curpos, ms_lastpos-ms_curpos, text+len, sizeof(text)-len, NULL, NULL);
int i;
for(i=0; i<len; ++i)
if((BYTE)text[i] > 0x7F) break;
if(i == len)
{
rstrText.append(text, text+len);
}
else
{
rstrText.append(text, text+i);
//if (addCodePage)
// rstrText.append(GetCodePageText());
rstrText.append(text+i, text+len);
}
return rstrText.size();
}
const char* CIME::GetCodePageText()
{
static char szCodePage[16];
const int defCodePage = GetDefaultCodePage();
const int outCodePage = ms_uOutputCodePage;
if (outCodePage != defCodePage)
{
sprintf(szCodePage, "@%04d", outCodePage);
}
else
{
szCodePage[0] = 0;
}
return szCodePage;
}
int CIME::GetCodePage()
{
return ms_uOutputCodePage;
}
int CIME::GetCandidatePageCount()
{
return ms_dwCandidatePageSize;
}
int CIME::GetCandidateCount()
{
return ms_dwCandidateCount;
}
int CIME::GetCandidate(DWORD index, std::string & rstrText)
{
if(index >= MAX_CANDLIST)
return 0;
LPCWSTR wszText = ms_wszCandidate[index];
if(wszText == NULL)
return 0;
int wTextLen = wcslen(wszText);
if(wTextLen == 0)
return 0;
char text[IMESTR_MAXLEN];
int len = ::WideCharToMultiByte(CP_UTF8, 0, wszText, wTextLen, text, sizeof(text), 0, 0);
rstrText.append("@9999");
rstrText.append(text, text+len);
return wTextLen;
}
int CIME::GetCandidateSelection()
{
return ms_dwCandidateSelection;
}
// PORT: 40250 ImmSetConversionStatus / ImmGetConversionStatus. Without an IMM context the conversion mode is
// the alphanumeric one (IME_CMODE_ALPHANUMERIC == 0) and cannot be changed.
void CIME::SetInputMode(DWORD)
{
}
DWORD CIME::GetInputMode()
{
return 0;
}
void CIME::SetNumberMode()
{
m_bOnlyNumberMode = TRUE;
}
void CIME::SetStringMode()
{
m_bOnlyNumberMode = FALSE;
}
void CIME::AddExceptKey(wchar_t key)
{
m_exceptKey.push_back(key);
}
void CIME::ClearExceptKey()
{
m_exceptKey.clear();
}
bool CIME::__IsWritable(wchar_t key)
{
if ( m_exceptKey.end() == std::find(m_exceptKey.begin(),m_exceptKey.end(),key) )
return true;
else
return false;
}
void CIME::EnablePaste(bool bFlag)
{
m_bEnablePaste = bFlag;
}
// PORT: 40250 reads CF_TEXT from the Win32 clipboard. The host clipboard is not wired to the extension yet,
// so this pastes nothing (the same as an empty clipboard).
void CIME::PasteTextFromClipBoard()
{
if (!m_bEnablePaste)
return;
}
// PORT: 40250 cancels the IMM composition string and closes the candidate list. There is no composition
// here; the candidate-list close (and its event) is kept.
void CIME::FinalizeString(bool)
{
static bool s_bProcessing = false; // to avoid infinite recursion
if ( !ms_bInitialized || s_bProcessing )
return;
s_bProcessing = true;
CloseCandidateList();
s_bProcessing = false;
}
int CIME::GetCompLen()
{
return ms_compLen;
}
int CIME::GetULBegin()
{
return ms_ulbegin;
}
int CIME::GetULEnd()
{
return ms_ulend;
}
void CIME::CloseCandidateList()
{
ms_bCandidateList = false;
ms_dwCandidateCount = 0;
memset(&ms_wszCandidate, 0, sizeof(ms_wszCandidate));
if(ms_pEvent)
ms_pEvent->OnCloseCandidateList();
}
void CIME::CloseReadingInformation()
{
CIME::ms_bReadingInformation = false;
if(CIME::ms_pEvent)
CIME::ms_pEvent->OnCloseReadingWnd();
}
// PORT: 40250 re-reads the keyboard layout (GETLANG) and re-applies the IME support level when the language
// changed. The host has one fixed input locale, so only the toggle check and the code-page event remain.
void CIME::ChangeInputLanguage()
{
CheckToggleState();
ChangeInputLanguageWorker();
if(ms_pEvent)
ms_pEvent->OnChangeCodePage();
}
void CIME::ChangeInputLanguageWorker()
{
if ( !ms_bUILessMode )
ms_iCandListIndexBase = 1;
SetupImeApi();
}
// PORT: 40250 forces level 3 for the Korean keyboard layout; there is no keyboard layout here.
void CIME::SetSupportLevel( DWORD dwImeLevel )
{
if ( dwImeLevel < 2 || 3 < dwImeLevel )
return;
ms_dwIMELevel = dwImeLevel;
// cancel current composition string.
FinalizeString();
}
LRESULT CIME::WMInputLanguage(HWND, UINT, WPARAM, LPARAM)
{
ChangeInputLanguage();
return 0;
}
LRESULT CIME::WMStartComposition(HWND, UINT, WPARAM, LPARAM)
{
return 1L;
}
// PORT: 40250 pulls GCS_RESULTSTR/GCS_COMPSTR out of the IMM context. The host never sends compositions.
LRESULT CIME::WMComposition(HWND, UINT, WPARAM, LPARAM)
{
return 0;
}
LRESULT CIME::WMEndComposition(HWND, UINT, WPARAM, LPARAM)
{
ms_compLen = 0;
ms_ulbegin = 0;
ms_ulend = 0;
if(ms_pEvent)
ms_pEvent->OnUpdate();
return 0L;
}
// PORT: 40250 handles IMN_* candidate/reading/toggle notifications from IMM; none are sent here.
LRESULT CIME::WMNotify(HWND, UINT, WPARAM, LPARAM)
{
return 0;
}
LRESULT CIME::WMChar(HWND /*hWnd*/, UINT /*uiMsg*/, WPARAM wParam, LPARAM lParam)
{
unsigned char c = (unsigned char)(wParam & 0xff);
switch (c)
{
case 8:
if(ms_bCaptureInput == false)
return 0;
if (ms_curpos > 0)
{
DecCurPos();
DelCurPos();
}
if(ms_pEvent)
ms_pEvent->OnUpdate();
return 0;
break;
default:
if(ms_pEvent) {
if (ms_pEvent->OnWM_CHAR(wParam, lParam))
break;
}
if(ms_bCaptureInput == false)
return 0;
wchar_t w[10];
MultiByteToWideChar(ms_uInputCodePage, 0, (char*)&c, 1, w, 1);
OnChar(w[0]);
if (w[0] == L'|')
OnChar(w[0]);
if(ms_pEvent)
ms_pEvent->OnUpdate();
break;
}
return 0;
}
/*---------------------------------------------------------------------------*/ /* Protected */
void CIME::IncCurPos()
{
if (ms_curpos < ms_lastpos)
{
int pos = FindColorTagEndPosition(m_wText + ms_curpos, ms_lastpos - ms_curpos);
if (pos > 0)
ms_curpos = std::min(ms_lastpos, std::max(0, ms_curpos + (pos + 1)));
else
++ms_curpos;
//++ms_curpos;
}
}
void CIME::DecCurPos()
{
if (ms_curpos > 0)
{
int pos = FindColorTagStartPosition(m_wText + ms_curpos - 1, ms_curpos);
if (pos > 0)
ms_curpos = std::min(ms_lastpos, std::max(0, ms_curpos - (pos + 1)));
else
--ms_curpos;
//--ms_curpos;
}
}
int CIME::GetCurPos()
{
int pos = GetTextTagOutputLen(m_wText, ms_curpos);
return pos;
//return ms_curpos;
}
void CIME::SetCurPos(int offset)
{
if (offset < 0 || offset > ms_lastpos)
{
ms_curpos = ms_lastpos;
return;
}
else
{
// offset is a position in the rendered text, so it has to be mapped back to the buffer.
//ms_curpos = min(ms_lastpos, offset);
ms_curpos = std::min(ms_lastpos, GetTextTagInternalPosFromRenderPos(m_wText, ms_lastpos, offset));
}
}
void CIME::DelCurPos()
{
if (ms_curpos < ms_lastpos)
{
int eraseCount = FindColorTagEndPosition(m_wText + ms_curpos, ms_lastpos - ms_curpos) + 1;
// PORT: 40250 wcscpy()s the overlapping tail down; that is undefined for overlapping buffers, and the
// buffer is not guaranteed NUL-terminated at ms_lastpos, so move exactly the live tail.
memmove(m_wText + ms_curpos, m_wText + ms_curpos + eraseCount, sizeof(wchar_t) * (ms_lastpos - ms_curpos - eraseCount));
ms_lastpos -= eraseCount;
ms_curpos = std::min(ms_lastpos, ms_curpos);
}
}
void CIME::PasteString(const char * str)
{
const char * begin = str;
const char * end = str + strlen(str);
wchar_t m_wText[IMESTR_MAXLEN];
int wstrLen = MultiByteToWideChar(ms_uInputCodePage, 0, begin, end - begin, m_wText, IMESTR_MAXLEN);
InsertString(m_wText, wstrLen);
if(ms_pEvent)
ms_pEvent->OnUpdate();
}
// PORT: 40250 reads the keyboard layout's default ANSI code page (GetLocaleInfoA LOCALE_IDEFAULTANSICODEPAGE).
// The host's input locale is the client locale, so its code page (SetDefaultCodePage from locale.cfg) is used.
void CIME::CheckInputLocale()
{
static UINT s_uPrevCodePage = 0xFFFF;
ms_uInputCodePage = GetDefaultCodePage();
if ( s_uPrevCodePage == ms_uInputCodePage )
return;
s_uPrevCodePage = ms_uInputCodePage;
ms_bVerticalCandidate = false;
ms_wszCurrentIndicator = s_aszNonImeIndicator;
if(ms_uOutputCodePage != 1256) {
ms_uOutputCodePage = ms_uInputCodePage;
Clear();
}
}
// PORT: 40250 asks IMM whether an IME is open; there is none, so the state is IMEUI_STATE_OFF (0).
void CIME::CheckToggleState()
{
CheckInputLocale();
ms_bChineseIME = false;
ms_dwImeState = 0;
}
/*---------------------------------------------------------------------------*/ /* Private */
void CIME::InsertString(wchar_t* wString, int iSize)
{
if (IsMax(wString, iSize))
return;
if (ms_curpos < ms_lastpos)
memmove(m_wText+ms_curpos+iSize, m_wText+ms_curpos, sizeof(wchar_t)*(ms_lastpos-ms_curpos));
memcpy(m_wText+ms_curpos, wString, sizeof(wchar_t)*iSize);
ms_curpos += iSize;
ms_lastpos += iSize;
}
void CIME::OnChar(wchar_t c)
{
if (m_bOnlyNumberMode)
if (!iswdigit(c))
return;
if (!__IsWritable(c))
return;
InsertString(&c, 1);
}
// PORT: 40250 maps a keyboard-layout LANGID to its ANSI code page. Only CheckInputLocale's code page is used
// here (see there), so this answers with it.
UINT CIME::GetCodePageFromLang(LANGID)
{
return GetDefaultCodePage();
}
// PORT: the IMM composition/candidate/reading processors (40250 IME.cpp:951-1227). No IMM context exists.
void CIME::ResultProcess(HIMC) {}
void CIME::CompositionProcessBuilding(HIMC) {}
void CIME::CompositionProcess(HIMC) {}
void CIME::AttributeProcess(HIMC) {}
void CIME::CandidateProcess(HIMC) {}
void CIME::ReadingProcess(HIMC) {}
bool CIME::IsMax(const wchar_t* wInput, int len)
{
if (ms_lastpos + len > IMESTR_MAXLEN)
return true;
int textLen = WideCharToMultiByte(ms_uOutputCodePage, 0, m_wText, ms_lastpos, 0, 0, NULL, NULL);
int inputLen = WideCharToMultiByte(ms_uOutputCodePage, 0, wInput, len, 0, 0, NULL, NULL);
//return textLen + inputLen > m_max;
if (textLen + inputLen > m_max)
return true;
else if (m_userMax != 0 && m_max != m_userMax)
{
std::wstring str = GetTextTagOutputString(m_wText, ms_lastpos);
std::wstring input = GetTextTagOutputString(wInput, len);
int textLen = WideCharToMultiByte(ms_uOutputCodePage, 0, str.c_str(), str.length(), 0, 0, NULL, NULL);
int inputLen = WideCharToMultiByte(ms_uOutputCodePage, 0, input.c_str(), input.length(), 0, 0, NULL, NULL);
return textLen + inputLen > m_userMax;
}
return false;
}
// PORT: 40250 identifies the CHT/CHS IME DLL version and the reading-window orientation. No IME DLL here.
DWORD CIME::GetImeId(UINT)
{
return 0;
}
bool CIME::GetReadingWindowOrientation()
{
return true;
}
void CIME::SetupImeApi()
{
_GetReadingString = NULL;
_ShowReadingWindow = NULL;
}
decltype(CIME::_ImmLockIMC) CIME::_ImmLockIMC{};
decltype(CIME::_ImmUnlockIMC) CIME::_ImmUnlockIMC{};
decltype(CIME::_ImmLockIMCC) CIME::_ImmLockIMCC{};
decltype(CIME::_ImmUnlockIMCC) CIME::_ImmUnlockIMCC{};
decltype(CIME::_GetReadingString) CIME::_GetReadingString{};
decltype(CIME::_ShowReadingWindow) CIME::_ShowReadingWindow{};
decltype(CIME::ms_bInitialized) CIME::ms_bInitialized{};
decltype(CIME::ms_bDisableIMECompletely) CIME::ms_bDisableIMECompletely{};
decltype(CIME::ms_bUILessMode) CIME::ms_bUILessMode{};
decltype(CIME::ms_bImeEnabled) CIME::ms_bImeEnabled{};
decltype(CIME::ms_bCaptureInput) CIME::ms_bCaptureInput{};
decltype(CIME::ms_bChineseIME) CIME::ms_bChineseIME{};
decltype(CIME::ms_bUseIMMCandidate) CIME::ms_bUseIMMCandidate{};
decltype(CIME::ms_hWnd) CIME::ms_hWnd{};
decltype(CIME::ms_hklCurrent) CIME::ms_hklCurrent{};
decltype(CIME::ms_szKeyboardLayout) CIME::ms_szKeyboardLayout{};
decltype(CIME::ms_stOSVI) CIME::ms_stOSVI{};
decltype(CIME::ms_hImm32Dll) CIME::ms_hImm32Dll{};
decltype(CIME::ms_hCurrentImeDll) CIME::ms_hCurrentImeDll{};
decltype(CIME::ms_dwImeState) CIME::ms_dwImeState{};
decltype(CIME::ms_adwId) CIME::ms_adwId{};
decltype(CIME::ms_dwIMELevel) CIME::ms_dwIMELevel{};
decltype(CIME::ms_dwIMELevelSaved) CIME::ms_dwIMELevelSaved{};
decltype(CIME::ms_bCandidateList) CIME::ms_bCandidateList{};
decltype(CIME::ms_dwCandidateCount) CIME::ms_dwCandidateCount{};
decltype(CIME::ms_bVerticalCandidate) CIME::ms_bVerticalCandidate{};
decltype(CIME::ms_iCandListIndexBase) CIME::ms_iCandListIndexBase{};
decltype(CIME::ms_wszCandidate) CIME::ms_wszCandidate{};
decltype(CIME::ms_dwCandidateSelection) CIME::ms_dwCandidateSelection{};
decltype(CIME::ms_dwCandidatePageSize) CIME::ms_dwCandidatePageSize{};
decltype(CIME::ms_bReadingInformation) CIME::ms_bReadingInformation{};
decltype(CIME::ms_iReadingError) CIME::ms_iReadingError{};
decltype(CIME::ms_bHorizontalReading) CIME::ms_bHorizontalReading{};
decltype(CIME::ms_wstrReading) CIME::ms_wstrReading{};
decltype(CIME::ms_wszCurrentIndicator) CIME::ms_wszCurrentIndicator{};
decltype(CIME::ms_pEvent) CIME::ms_pEvent{};
decltype(CIME::m_wText) CIME::m_wText{};
decltype(CIME::ms_compLen) CIME::ms_compLen{};
decltype(CIME::ms_curpos) CIME::ms_curpos{};
decltype(CIME::ms_lastpos) CIME::ms_lastpos{};
decltype(CIME::ms_ulbegin) CIME::ms_ulbegin{};
decltype(CIME::ms_ulend) CIME::ms_ulend{};
decltype(CIME::ms_uOutputCodePage) CIME::ms_uOutputCodePage{};
decltype(CIME::ms_uInputCodePage) CIME::ms_uInputCodePage{};