Merge pull request #58 from rtw1x1/main

fix: BiDi and bad meshes crash
This commit is contained in:
rtw1x1
2025-12-31 09:07:44 +00:00
committed by GitHub
7 changed files with 590 additions and 153 deletions

351
extern/include/utf8.h vendored
View File

@@ -22,9 +22,11 @@ constexpr size_t ARABIC_SHAPING_EXPANSION_FACTOR_RETRY = 4;
constexpr size_t ARABIC_SHAPING_SAFETY_MARGIN_RETRY = 64; constexpr size_t ARABIC_SHAPING_SAFETY_MARGIN_RETRY = 64;
// ============================================================================ // ============================================================================
// DEBUG LOGGING (Uncomment to enable BiDi debugging) // DEBUG LOGGING (Only enabled in Debug builds)
// ============================================================================ // ============================================================================
// #define DEBUG_BIDI #ifdef _DEBUG
#define DEBUG_BIDI // Enabled in debug builds for diagnostics
#endif
#ifdef DEBUG_BIDI #ifdef DEBUG_BIDI
#include <cstdio> #include <cstdio>
@@ -242,8 +244,9 @@ static inline bool IsNameTokenPunct(wchar_t ch)
case L'\\': case L'\\':
case L'(': case L'(':
case L')': case L')':
case L'[': // Brackets are handled specially - see GetCharDirSmart
case L']': // case L'[':
// case L']':
case L'{': case L'{':
case L'}': case L'}':
case L'<': case L'<':
@@ -264,16 +267,44 @@ static inline bool IsStrongLTR(wchar_t ch)
static inline bool HasStrongLTRNeighbor(const wchar_t* s, int n, int i) static inline bool HasStrongLTRNeighbor(const wchar_t* s, int n, int i)
{ {
// Remove null/size check (caller guarantees validity) // Skip neutral characters (spaces, punctuation) to find nearest strong character
// Early exit after first strong neighbor found // This fixes mixed-direction text like "english + arabic"
// Check previous character // Search backwards for strong character (skip neutrals/whitespace)
if (i > 0 && IsStrongLTR(s[i - 1])) for (int j = i - 1; j >= 0; --j)
return true; {
wchar_t ch = s[j];
// Check next character // Skip spaces and common neutral punctuation
if (i + 1 < n && IsStrongLTR(s[i + 1])) if (ch == L' ' || ch == L'\t' || ch == L'\n')
return true; continue;
// Found strong LTR
if (IsStrongLTR(ch))
return true;
// Found strong RTL or other strong character
if (IsRTLCodepoint(ch) || IsStrongAlpha(ch))
break;
}
// Search forwards for strong character (skip neutrals/whitespace)
for (int j = i + 1; j < n; ++j)
{
wchar_t ch = s[j];
// Skip spaces and common neutral punctuation
if (ch == L' ' || ch == L'\t' || ch == L'\n')
continue;
// Found strong LTR
if (IsStrongLTR(ch))
return true;
// Found strong RTL or other strong character
if (IsRTLCodepoint(ch) || IsStrongAlpha(ch))
break;
}
return false; return false;
} }
@@ -302,6 +333,132 @@ static inline ECharDir GetCharDirSmart(const wchar_t* s, int n, int i)
if (IsStrongLTR(ch)) if (IsStrongLTR(ch))
return ECharDir::LTR; return ECharDir::LTR;
// Parentheses: always LTR to keep them with their content
if (ch == L'(' || ch == L')')
return ECharDir::LTR;
// Percentage sign: attach to numbers (scan nearby for digits/minus/plus)
// Handles: "%20", "20%", "-6%", "%d%%", etc.
if (ch == L'%')
{
// Look backward for digit, %, -, or +
for (int j = i - 1; j >= 0 && (i - j) < 5; --j)
{
wchar_t prev = s[j];
if (IsDigit(prev) || prev == L'%' || prev == L'-' || prev == L'+')
return ECharDir::LTR;
if (prev != L' ' && prev != L'\t')
break; // Stop if we hit non-numeric character
}
// Look forward for digit, %, -, or +
for (int j = i + 1; j < n && (j - i) < 5; ++j)
{
wchar_t next = s[j];
if (IsDigit(next) || next == L'%' || next == L'-' || next == L'+')
return ECharDir::LTR;
if (next != L' ' && next != L'\t')
break; // Stop if we hit non-numeric character
}
return ECharDir::Neutral;
}
// Minus/dash: attach to numbers (scan nearby for digits/%)
// Handles: "-6", "5-10", "-6%%", etc.
if (ch == L'-')
{
// Look backward for digit or %
for (int j = i - 1; j >= 0 && (i - j) < 3; --j)
{
wchar_t prev = s[j];
if (IsDigit(prev) || prev == L'%')
return ECharDir::LTR;
if (prev != L' ' && prev != L'\t')
break;
}
// Look forward for digit or %
for (int j = i + 1; j < n && (j - i) < 3; ++j)
{
wchar_t next = s[j];
if (IsDigit(next) || next == L'%')
return ECharDir::LTR;
if (next != L' ' && next != L'\t')
break;
}
return ECharDir::Neutral;
}
// Colon: attach to preceding text direction
// Look backward to find strong character
if (ch == L':')
{
for (int j = i - 1; j >= 0; --j)
{
if (s[j] == L' ' || s[j] == L'\t')
continue; // Skip spaces
if (IsRTLCodepoint(s[j]))
return ECharDir::RTL; // Attach to RTL text
if (IsStrongLTR(s[j]))
return ECharDir::LTR; // Attach to LTR text
}
return ECharDir::Neutral;
}
// Enhancement marker: '+' followed by digit(s) should attach to preceding text
// If preceded by RTL, treat as RTL to keep "+9" with the item name
// Otherwise treat as LTR
if (ch == L'+' && i + 1 < n && IsDigit(s[i + 1]))
{
// Look backward for the last strong character
for (int j = i - 1; j >= 0; --j)
{
if (IsRTLCodepoint(s[j]))
return ECharDir::RTL; // Attach to preceding RTL text
if (IsStrongLTR(s[j]))
return ECharDir::LTR; // Attach to preceding LTR text
// Skip neutral characters
}
return ECharDir::LTR; // Default to LTR if no strong character found
}
// Brackets: always attach to the content inside them
// This fixes hyperlinks like "[درع فولاذي أسود+9]"
if (ch == L'[' || ch == L']')
{
// Opening bracket '[': look forward for strong character
if (ch == L'[')
{
for (int j = i + 1; j < n; ++j)
{
wchar_t next = s[j];
if (next == L']') break; // End of bracket content
if (IsRTLCodepoint(next)) return ECharDir::RTL;
if (IsStrongLTR(next)) return ECharDir::LTR;
}
}
// Closing bracket ']': look backward for strong character
else if (ch == L']')
{
for (int j = i - 1; j >= 0; --j)
{
wchar_t prev = s[j];
if (prev == L'[') break; // Start of bracket content
if (IsRTLCodepoint(prev)) return ECharDir::RTL;
if (IsStrongLTR(prev)) return ECharDir::LTR;
}
}
// If we can't determine, treat as neutral
return ECharDir::Neutral;
}
// Spaces should attach to adjacent strong characters to avoid fragmentation
// This fixes "english + arabic" by keeping " + " with "english"
if (ch == L' ' || ch == L'\t')
{
if (HasStrongLTRNeighbor(s, n, i))
return ECharDir::LTR;
// Note: We don't check for RTL neighbor because ResolveNeutralDir handles that
}
// Name-token punctuation: if adjacent to LTR, treat as LTR to keep token intact // Name-token punctuation: if adjacent to LTR, treat as LTR to keep token intact
if (IsNameTokenPunct(ch) && HasStrongLTRNeighbor(s, n, i)) if (IsNameTokenPunct(ch) && HasStrongLTRNeighbor(s, n, i))
return ECharDir::LTR; return ECharDir::LTR;
@@ -318,10 +475,11 @@ struct TStrongDirCache
TStrongDirCache(const wchar_t* s, int n, EBidiDir base) : nextStrong(n), baseDir(base) TStrongDirCache(const wchar_t* s, int n, EBidiDir base) : nextStrong(n), baseDir(base)
{ {
// Build reverse lookup: scan from end to beginning // Build reverse lookup: scan from end to beginning
// Use GetCharDirSmart for context-aware character classification
EBidiDir lastSeen = baseDir; EBidiDir lastSeen = baseDir;
for (int i = n - 1; i >= 0; --i) for (int i = n - 1; i >= 0; --i)
{ {
ECharDir cd = GetCharDir(s[i]); ECharDir cd = GetCharDirSmart(s, n, i);
if (cd == ECharDir::LTR) if (cd == ECharDir::LTR)
lastSeen = EBidiDir::LTR; lastSeen = EBidiDir::LTR;
else if (cd == ECharDir::RTL) else if (cd == ECharDir::RTL)
@@ -397,66 +555,6 @@ static std::vector<wchar_t> BuildVisualBidiText_Tagless(const wchar_t* s, int n,
if (!s || n <= 0) if (!s || n <= 0)
return {}; return {};
// Detect chat format "name : msg" and extract components
int chatSepPos = -1;
for (int i = 0; i < n - 2; ++i)
{
if (s[i] == L' ' && s[i + 1] == L':' && s[i + 2] == L' ')
{
chatSepPos = i;
break;
}
}
// If chat format detected, process name and message separately
if (chatSepPos > 0 && forceRTL)
{
// Use pointers instead of copying (zero-copy optimization)
const wchar_t* name = s;
const int nameLen = chatSepPos;
const int msgStart = chatSepPos + 3;
const wchar_t* msg = s + msgStart;
const int msgLen = n - msgStart;
// Check if message contains RTL
bool msgHasRTL = false;
for (int i = 0; i < msgLen; ++i)
{
if (IsRTLCodepoint(msg[i]))
{
msgHasRTL = true;
break;
}
}
// Build result based on message direction (pre-reserve exact size)
std::vector<wchar_t> visual;
visual.reserve((size_t)n);
if (msgHasRTL)
{
// Arabic message: apply BiDi to message, then add " : name"
std::vector<wchar_t> msgVisual = BuildVisualBidiText_Tagless(msg, msgLen, false);
visual.insert(visual.end(), msgVisual.begin(), msgVisual.end());
visual.push_back(L' ');
visual.push_back(L':');
visual.push_back(L' ');
visual.insert(visual.end(), name, name + nameLen); // Direct pointer insert
}
else
{
// English message: "msg : name"
visual.insert(visual.end(), msg, msg + msgLen); // Direct pointer insert
visual.push_back(L' ');
visual.push_back(L':');
visual.push_back(L' ');
visual.insert(visual.end(), name, name + nameLen); // Direct pointer insert
}
return visual;
}
// 1) base direction // 1) base direction
EBidiDir base = forceRTL ? EBidiDir::RTL : DetectBaseDir_FirstStrong(s, n); EBidiDir base = forceRTL ? EBidiDir::RTL : DetectBaseDir_FirstStrong(s, n);
@@ -503,6 +601,16 @@ static std::vector<wchar_t> BuildVisualBidiText_Tagless(const wchar_t* s, int n,
d = ResolveNeutralDir(s, n, i, base, lastStrong, &strongCache); d = ResolveNeutralDir(s, n, i, base, lastStrong, &strongCache);
} }
#ifdef DEBUG_BIDI
if (i < 50) // Only log first 50 chars to avoid spam
{
BIDI_LOG("Char[%d] U+%04X '%lc' → CharDir=%s, RunDir=%s",
i, (unsigned int)ch, (ch >= 32 && ch < 127) ? ch : L'?',
cd == ECharDir::RTL ? "RTL" : (cd == ECharDir::LTR ? "LTR" : "Neutral"),
d == EBidiDir::RTL ? "RTL" : "LTR");
}
#endif
push_run(d); push_run(d);
runs.back().text.push_back(ch); runs.back().text.push_back(ch);
} }
@@ -528,7 +636,13 @@ static std::vector<wchar_t> BuildVisualBidiText_Tagless(const wchar_t* s, int n,
int outLen = Arabic_MakeShape(r.text.data(), (int)r.text.size(), shaped.data(), (int)shaped.size()); int outLen = Arabic_MakeShape(r.text.data(), (int)r.text.size(), shaped.data(), (int)shaped.size());
if (outLen <= 0) if (outLen <= 0)
{ {
BIDI_LOG("Arabic_MakeShape failed for run of %zu chars", r.text.size()); BIDI_LOG("Arabic_MakeShape FAILED for RTL run of %zu characters", r.text.size());
BIDI_LOG(" WARNING: This RTL text segment will NOT be displayed!");
BIDI_LOG(" First few characters: U+%04X U+%04X U+%04X U+%04X",
r.text.size() > 0 ? (unsigned int)r.text[0] : 0,
r.text.size() > 1 ? (unsigned int)r.text[1] : 0,
r.text.size() > 2 ? (unsigned int)r.text[2] : 0,
r.text.size() > 3 ? (unsigned int)r.text[3] : 0);
continue; continue;
} }
@@ -584,6 +698,95 @@ static std::vector<wchar_t> BuildVisualBidiText_Tagless(const wchar_t* s, int n,
return visual; return visual;
} }
// ============================================================================
// Chat Message BiDi Processing (Separate name/message handling)
// ============================================================================
// Build visual BiDi text for chat messages with separate name and message
// This avoids fragile " : " detection and handles cases where username contains " : "
//
// RECOMMENDED USAGE:
// Instead of: SetValue("PlayerName : Message")
// Use this function with separated components:
// - name: "PlayerName" (without " : ")
// - msg: "Message" (without " : ")
//
// INTEGRATION NOTES:
// To use this properly, you need to:
// 1. Modify the server/network code to send chat name and message separately
// 2. Or parse the chat string in PythonNetworkStreamPhaseGame.cpp BEFORE passing to GrpTextInstance
// 3. Then call this function instead of BuildVisualBidiText_Tagless
//
static inline std::vector<wchar_t> BuildVisualChatMessage(
const wchar_t* name, int nameLen,
const wchar_t* msg, int msgLen,
bool forceRTL)
{
if (!name || !msg || nameLen <= 0 || msgLen <= 0)
return {};
// Check if message contains RTL or hyperlink tags
bool msgHasRTL = false;
bool msgHasTags = false;
for (int i = 0; i < msgLen; ++i)
{
if (IsRTLCodepoint(msg[i]))
msgHasRTL = true;
if (msg[i] == L'|')
msgHasTags = true;
if (msgHasRTL && msgHasTags)
break;
}
// Build result based on UI direction (pre-reserve exact size)
std::vector<wchar_t> visual;
visual.reserve((size_t)(nameLen + msgLen + 3)); // +3 for " : "
// Decision: UI direction determines order (for visual consistency)
// RTL UI: "Message : Name" (message on right, consistent with RTL reading flow)
// LTR UI: "Name : Message" (name on left, consistent with LTR reading flow)
if (forceRTL)
{
// RTL UI: "Message : Name"
// Don't apply BiDi if message has tags (hyperlinks are pre-formatted)
if (msgHasTags)
{
visual.insert(visual.end(), msg, msg + msgLen);
}
else
{
// Apply BiDi to message based on its content
std::vector<wchar_t> msgVisual = BuildVisualBidiText_Tagless(msg, msgLen, msgHasRTL);
visual.insert(visual.end(), msgVisual.begin(), msgVisual.end());
}
visual.push_back(L' ');
visual.push_back(L':');
visual.push_back(L' ');
visual.insert(visual.end(), name, name + nameLen); // Name on left side
}
else
{
// LTR UI: "Name : Message"
visual.insert(visual.end(), name, name + nameLen); // Name on left side
visual.push_back(L' ');
visual.push_back(L':');
visual.push_back(L' ');
// Don't apply BiDi if message has tags (hyperlinks are pre-formatted)
if (msgHasTags)
{
visual.insert(visual.end(), msg, msg + msgLen);
}
else
{
// Apply BiDi to message based on its content
std::vector<wchar_t> msgVisual = BuildVisualBidiText_Tagless(msg, msgLen, msgHasRTL);
visual.insert(visual.end(), msgVisual.begin(), msgVisual.end());
}
}
return visual;
}
// ============================================================================ // ============================================================================
// TextTail formatting for RTL UI // TextTail formatting for RTL UI
// ============================================================================ // ============================================================================

View File

@@ -14,16 +14,33 @@ const CGrannyModel::TMeshNode* CGrannyModel::GetMeshNodeList(CGrannyMesh::EType
CGrannyMesh * CGrannyModel::GetMeshPointer(int iMesh) CGrannyMesh * CGrannyModel::GetMeshPointer(int iMesh)
{ {
assert(CheckMeshIndex(iMesh)); if (!CheckMeshIndex(iMesh))
assert(m_meshs != NULL); assert(m_meshs != NULL);
{
TraceError("CGrannyModel::GetMeshPointer - Invalid mesh index: %d (max: %d)", iMesh, m_meshNodeSize);
return nullptr;
}
if (m_meshs == NULL)
{
TraceError("CGrannyModel::GetMeshPointer - m_meshs is NULL");
return nullptr;
}
return m_meshs + iMesh; return m_meshs + iMesh;
} }
const CGrannyMesh* CGrannyModel::GetMeshPointer(int iMesh) const const CGrannyMesh* CGrannyModel::GetMeshPointer(int iMesh) const
{ {
assert(CheckMeshIndex(iMesh)); if (!CheckMeshIndex(iMesh))
assert(m_meshs != NULL); {
TraceError("CGrannyModel::GetMeshPointer(const) - Invalid mesh index: %d (max: %d)", iMesh, m_meshNodeSize);
return nullptr;
}
if (m_meshs == NULL)
{
TraceError("CGrannyModel::GetMeshPointer(const) - m_meshs is NULL");
return nullptr;
}
return m_meshs + iMesh; return m_meshs + iMesh;
} }

View File

@@ -101,6 +101,10 @@ bool CGrannyModelInstance::Intersect(const D3DXMATRIX * c_pMatrix,
granny_matrix_4x4* pgrnMatCompositeBuffer = GrannyGetWorldPoseComposite4x4Array(m_pgrnWorldPose); granny_matrix_4x4* pgrnMatCompositeBuffer = GrannyGetWorldPoseComposite4x4Array(m_pgrnWorldPose);
const CGrannyMesh* c_pMesh = m_pModel->GetMeshPointer(rcurBoundBox.meshIndex); const CGrannyMesh* c_pMesh = m_pModel->GetMeshPointer(rcurBoundBox.meshIndex);
if (!c_pMesh)
continue;
const granny_mesh* c_pgrnMesh = c_pMesh->GetGrannyMeshPointer(); const granny_mesh* c_pgrnMesh = c_pMesh->GetGrannyMeshPointer();
if (!GrannyMeshIsRigid(c_pgrnMesh)) if (!GrannyMeshIsRigid(c_pgrnMesh))

View File

@@ -177,6 +177,9 @@ void CGrannyModelInstance::UpdateWorldMatrices(const D3DXMATRIX* c_pWorldMatrix)
const CGrannyMesh * pMesh = m_pModel->GetMeshPointer(i); const CGrannyMesh * pMesh = m_pModel->GetMeshPointer(i);
if (!pMesh)
continue;
// WORK // WORK
int * boneIndices = __GetMeshBoneIndices(i); int * boneIndices = __GetMeshBoneIndices(i);
// END_OF_WORK // END_OF_WORK

View File

@@ -135,11 +135,37 @@ void CGraphicTextInstance::Update()
// UTF-8 -> UTF-16 conversion - reserve enough space to avoid reallocation // UTF-8 -> UTF-16 conversion - reserve enough space to avoid reallocation
std::vector<wchar_t> wTextBuf((size_t)utf8Len + 1u, 0); std::vector<wchar_t> wTextBuf((size_t)utf8Len + 1u, 0);
const int wTextLen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8, utf8Len, wTextBuf.data(), (int)wTextBuf.size()); int wTextLen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8, utf8Len, wTextBuf.data(), (int)wTextBuf.size());
// If strict UTF-8 conversion fails, try lenient mode (replaces invalid sequences)
if (wTextLen <= 0) if (wTextLen <= 0)
{ {
ResetState(); DWORD dwError = GetLastError();
return; BIDI_LOG("GrpTextInstance::Update() - STRICT UTF-8 conversion failed (error %u), trying LENIENT mode", dwError);
BIDI_LOG(" Text length: %d bytes", utf8Len);
BIDI_LOG(" First 32 bytes (hex): %02X %02X %02X %02X %02X %02X %02X %02X...",
(utf8Len > 0 ? (unsigned char)utf8[0] : 0),
(utf8Len > 1 ? (unsigned char)utf8[1] : 0),
(utf8Len > 2 ? (unsigned char)utf8[2] : 0),
(utf8Len > 3 ? (unsigned char)utf8[3] : 0),
(utf8Len > 4 ? (unsigned char)utf8[4] : 0),
(utf8Len > 5 ? (unsigned char)utf8[5] : 0),
(utf8Len > 6 ? (unsigned char)utf8[6] : 0),
(utf8Len > 7 ? (unsigned char)utf8[7] : 0));
BIDI_LOG(" Text preview: %.64s", utf8);
// Try lenient conversion (no MB_ERR_INVALID_CHARS flag)
// This will replace invalid UTF-8 sequences with default character
wTextLen = MultiByteToWideChar(CP_UTF8, 0, utf8, utf8Len, wTextBuf.data(), (int)wTextBuf.size());
if (wTextLen <= 0)
{
BIDI_LOG(" LENIENT conversion also failed! Text cannot be displayed.");
ResetState();
return;
}
BIDI_LOG(" LENIENT conversion succeeded - text will display with replacement characters");
} }
@@ -244,38 +270,57 @@ void CGraphicTextInstance::Update()
m_visualToLogicalPos[i] = i; m_visualToLogicalPos[i] = i;
} }
// Check for RTL characters and chat message format in single pass // Use optimized chat message processing if SetChatValue was used
bool hasRTL = false; if (m_isChatMessage && !m_chatName.empty() && !m_chatMessage.empty())
bool isChatMessage = false;
const wchar_t* wTextPtr = wTextBuf.data();
for (int i = 0; i < wTextLen; ++i)
{ {
if (!hasRTL && IsRTLCodepoint(wTextPtr[i])) // Convert chat name and message to wide chars
hasRTL = true; std::wstring wName = Utf8ToWide(m_chatName);
std::wstring wMsg = Utf8ToWide(m_chatMessage);
if (!isChatMessage && i < wTextLen - 2 && // Use BuildVisualChatMessage for proper BiDi handling
wTextPtr[i] == L' ' && wTextPtr[i + 1] == L':' && wTextPtr[i + 2] == L' ') std::vector<wchar_t> visual = BuildVisualChatMessage(
isChatMessage = true; wName.data(), (int)wName.size(),
wMsg.data(), (int)wMsg.size(),
baseRTL);
// Early exit if both found
if (hasRTL && isChatMessage)
break;
}
// Apply BiDi if text contains RTL OR is a chat message in RTL UI
// Skip BiDi for regular input text like :)) in RTL UI
if (hasRTL || (baseRTL && isChatMessage))
{
std::vector<wchar_t> visual = BuildVisualBidiText_Tagless(wTextBuf.data(), wTextLen, baseRTL);
for (size_t i = 0; i < visual.size(); ++i) for (size_t i = 0; i < visual.size(); ++i)
__DrawCharacter(pFontTexture, visual[i], defaultColor); __DrawCharacter(pFontTexture, visual[i], defaultColor);
} }
else else
{ {
// Pure LTR text or non-chat input - no BiDi processing // Legacy path: Check for RTL characters and chat message format in single pass
bool hasRTL = false;
bool isChatMessage = false;
const wchar_t* wTextPtr = wTextBuf.data();
for (int i = 0; i < wTextLen; ++i) for (int i = 0; i < wTextLen; ++i)
__DrawCharacter(pFontTexture, wTextBuf[i], defaultColor); {
if (!hasRTL && IsRTLCodepoint(wTextPtr[i]))
hasRTL = true;
if (!isChatMessage && i < wTextLen - 2 &&
wTextPtr[i] == L' ' && wTextPtr[i + 1] == L':' && wTextPtr[i + 2] == L' ')
isChatMessage = true;
// Early exit if both found
if (hasRTL && isChatMessage)
break;
}
// Apply BiDi if text contains RTL OR is a chat message in RTL UI
// Skip BiDi for regular input text like :)) in RTL UI
if (hasRTL || (baseRTL && isChatMessage))
{
std::vector<wchar_t> visual = BuildVisualBidiText_Tagless(wTextBuf.data(), wTextLen, baseRTL);
for (size_t i = 0; i < visual.size(); ++i)
__DrawCharacter(pFontTexture, visual[i], defaultColor);
}
else
{
// Pure LTR text or non-chat input - no BiDi processing
for (int i = 0; i < wTextLen; ++i)
__DrawCharacter(pFontTexture, wTextBuf[i], defaultColor);
}
} }
} }
// ======================================================================== // ========================================================================
@@ -283,6 +328,37 @@ void CGraphicTextInstance::Update()
// ======================================================================== // ========================================================================
else else
{ {
// Special handling for chat messages with tags (e.g., hyperlinks)
// We need to process the message separately and then combine with name
std::wstring chatNameWide;
bool isChatWithTags = false;
if (m_isChatMessage && !m_chatName.empty() && !m_chatMessage.empty())
{
isChatWithTags = true;
chatNameWide = Utf8ToWide(m_chatName);
// Process only the message part (not the full text)
const char* msgUtf8 = m_chatMessage.c_str();
const int msgUtf8Len = (int)m_chatMessage.size();
// Re-convert to wide chars for message only
wTextBuf.clear();
wTextBuf.resize((size_t)msgUtf8Len + 1u, 0);
wTextLen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, msgUtf8, msgUtf8Len, wTextBuf.data(), (int)wTextBuf.size());
if (wTextLen <= 0)
{
// Try lenient conversion
wTextLen = MultiByteToWideChar(CP_UTF8, 0, msgUtf8, msgUtf8Len, wTextBuf.data(), (int)wTextBuf.size());
if (wTextLen <= 0)
{
ResetState();
return;
}
}
}
// Check if text contains RTL characters (cache pointer for performance) // Check if text contains RTL characters (cache pointer for performance)
bool hasRTL = false; bool hasRTL = false;
const wchar_t* wTextPtr = wTextBuf.data(); const wchar_t* wTextPtr = wTextBuf.data();
@@ -302,7 +378,7 @@ void CGraphicTextInstance::Update()
int logicalPos; // logical index in original wTextBuf (includes tags) int logicalPos; // logical index in original wTextBuf (includes tags)
}; };
auto ReorderTaggedWithBidi = [&](std::vector<TVisChar>& vis, bool forceRTL) auto ReorderTaggedWithBidi = [&](std::vector<TVisChar>& vis, bool forceRTL, bool isHyperlink, bool shapeOnly = false)
{ {
if (vis.empty()) if (vis.empty())
return; return;
@@ -313,7 +389,71 @@ void CGraphicTextInstance::Update()
for (const auto& vc : vis) for (const auto& vc : vis)
buf.push_back(vc.ch); buf.push_back(vc.ch);
// Use the exact same BiDi engine as tagless text // Special handling for hyperlinks: extract content between [ and ], apply BiDi, then re-add brackets
if (isHyperlink && buf.size() > 2)
{
// Find opening and closing brackets
int openIdx = -1, closeIdx = -1;
for (int i = 0; i < (int)buf.size(); ++i)
{
if (buf[i] == L'[' && openIdx < 0) openIdx = i;
if (buf[i] == L']' && closeIdx < 0) closeIdx = i;
}
// If we have valid brackets, process content between them
if (openIdx >= 0 && closeIdx > openIdx)
{
// Extract content (without brackets)
std::vector<wchar_t> content(buf.begin() + openIdx + 1, buf.begin() + closeIdx);
// Apply BiDi to content with LTR base (keeps +9 at end)
std::vector<wchar_t> contentVisual = BuildVisualBidiText_Tagless(content.data(), (int)content.size(), false);
// Rebuild: everything before '[', then '[', then processed content, then ']', then everything after
std::vector<wchar_t> visual;
visual.reserve(buf.size() + contentVisual.size());
// Copy prefix (before '[')
visual.insert(visual.end(), buf.begin(), buf.begin() + openIdx);
// Add opening bracket
visual.push_back(L'[');
// Add processed content
visual.insert(visual.end(), contentVisual.begin(), contentVisual.end());
// Add closing bracket
visual.push_back(L']');
// Copy suffix (after ']')
visual.insert(visual.end(), buf.begin() + closeIdx + 1, buf.end());
// Handle size change due to Arabic shaping
if ((int)visual.size() != (int)vis.size())
{
std::vector<TVisChar> resized;
resized.reserve(visual.size());
for (size_t i = 0; i < visual.size(); ++i)
{
size_t src = (i < vis.size()) ? i : (vis.size() - 1);
TVisChar tmp = vis[src];
tmp.ch = visual[i];
resized.push_back(tmp);
}
vis.swap(resized);
}
else
{
// Same size: write back characters
for (size_t i = 0; i < vis.size(); ++i)
vis[i].ch = visual[i];
}
return;
}
}
// Non-hyperlink or no brackets found: use normal BiDi processing
std::vector<wchar_t> visual = BuildVisualBidiText_Tagless(buf.data(), (int)buf.size(), forceRTL); std::vector<wchar_t> visual = BuildVisualBidiText_Tagless(buf.data(), (int)buf.size(), forceRTL);
// If size differs (rare, but can happen with Arabic shaping expansion), // If size differs (rare, but can happen with Arabic shaping expansion),
@@ -434,7 +574,10 @@ void CGraphicTextInstance::Update()
// ==================================================================== // ====================================================================
// PHASE 2: Apply BiDi to hyperlinks (if RTL text or RTL UI) // PHASE 2: Apply BiDi to hyperlinks (if RTL text or RTL UI)
// ==================================================================== // ====================================================================
if (hasRTL || baseRTL) // DISABLED: Hyperlinks are pre-formatted and stored in visual order
// Applying BiDi to them reverses text that's already correct
// This section is kept for reference but should not execute
if (false)
{ {
// Collect all hyperlink ranges (reserve typical count) // Collect all hyperlink ranges (reserve typical count)
struct LinkRange { int start; int end; int linkIdx; }; struct LinkRange { int start; int end; int linkIdx; };
@@ -578,8 +721,13 @@ void CGraphicTextInstance::Update()
} }
} }
// Apply BiDi to non-hyperlink segments and reorder segments for RTL UI // Apply BiDi to segments - always process hyperlinks, optionally process other text
if (hasRTL || baseRTL) // For input fields: only process hyperlinks (preserve cursor for regular text)
// For display text: process everything and reorder segments
const bool hasHyperlinks = !linkTargets.empty();
const bool shouldProcessBidi = (hasRTL || baseRTL) && (!m_isCursor || hasHyperlinks);
if (shouldProcessBidi)
{ {
// Split text into hyperlink and non-hyperlink segments (reserve typical count) // Split text into hyperlink and non-hyperlink segments (reserve typical count)
const size_t estimatedSegments = linkTargets.size() * 2 + 1; const size_t estimatedSegments = linkTargets.size() * 2 + 1;
@@ -609,21 +757,32 @@ void CGraphicTextInstance::Update()
} }
} }
// Apply BiDi to non-hyperlink segments only (cache segment count) // Apply BiDi to segments
// For input fields: skip non-hyperlink segments to preserve cursor
// For display text: process all segments
const size_t numSegments = segments.size(); const size_t numSegments = segments.size();
for (size_t s = 0; s < numSegments; ++s) for (size_t s = 0; s < numSegments; ++s)
{ {
if (!isHyperlink[s]) // Skip non-hyperlink segments in input fields to preserve cursor
ReorderTaggedWithBidi(segments[s], baseRTL); if (m_isCursor && !isHyperlink[s])
continue;
ReorderTaggedWithBidi(segments[s], baseRTL, isHyperlink[s], false);
} }
// Rebuild text from segments (reverse order for RTL UI) // Rebuild text from segments
logicalVis.clear(); logicalVis.clear();
logicalVis.reserve(logicalVisSize2); // Reserve original size logicalVis.reserve(logicalVisSize2); // Reserve original size
if (baseRTL) // IMPORTANT: Only reverse segments for display text, NOT for input fields
// Input fields need to preserve logical order for proper cursor positioning
// Display text (received chat messages) can reverse for better RTL reading flow
const bool shouldReverseSegments = baseRTL && !m_isCursor;
if (shouldReverseSegments)
{ {
// RTL UI - reverse segments for right-to-left reading // RTL display text - reverse segments for right-to-left reading
// Example: "Hello [link]" becomes "[link] Hello" visually
for (int s = (int)numSegments - 1; s >= 0; --s) for (int s = (int)numSegments - 1; s >= 0; --s)
{ {
logicalVis.insert(logicalVis.end(), segments[s].begin(), segments[s].end()); logicalVis.insert(logicalVis.end(), segments[s].begin(), segments[s].end());
@@ -631,7 +790,8 @@ void CGraphicTextInstance::Update()
} }
else else
{ {
// LTR UI - keep original segment order // LTR UI or input field - keep original segment order
// Input fields must preserve logical order for cursor to work correctly
for (size_t s = 0; s < numSegments; ++s) for (size_t s = 0; s < numSegments; ++s)
{ {
logicalVis.insert(logicalVis.end(), segments[s].begin(), segments[s].end()); logicalVis.insert(logicalVis.end(), segments[s].begin(), segments[s].end());
@@ -690,6 +850,18 @@ void CGraphicTextInstance::Update()
curLinkRange.sx = 0; curLinkRange.sx = 0;
curLinkRange.ex = 0; curLinkRange.ex = 0;
// For LTR chat messages with tags, render name and separator first
if (isChatWithTags && !chatNameWide.empty() && !baseRTL)
{
// LTR UI: "Name : Message"
for (size_t i = 0; i < chatNameWide.size(); ++i)
x += __DrawCharacter(pFontTexture, chatNameWide[i], defaultColor);
x += __DrawCharacter(pFontTexture, L' ', defaultColor);
x += __DrawCharacter(pFontTexture, L':', defaultColor);
x += __DrawCharacter(pFontTexture, L' ', defaultColor);
}
// For RTL, we'll append name AFTER the message (see below)
// Cache size for loop (avoid repeated size() calls) // Cache size for loop (avoid repeated size() calls)
const size_t logicalVisRenderSize = logicalVis.size(); const size_t logicalVisRenderSize = logicalVis.size();
for (size_t idx = 0; idx < logicalVisRenderSize; ++idx) for (size_t idx = 0; idx < logicalVisRenderSize; ++idx)
@@ -733,6 +905,19 @@ void CGraphicTextInstance::Update()
curLinkRange.text = linkTargets[(size_t)currentLink]; curLinkRange.text = linkTargets[(size_t)currentLink];
m_hyperlinkVector.push_back(curLinkRange); m_hyperlinkVector.push_back(curLinkRange);
} }
// Add chat name and separator for RTL messages with tags
if (isChatWithTags && !chatNameWide.empty() && baseRTL)
{
// RTL UI: "Message : Name" (render order matches BuildVisualChatMessage)
// Message was already rendered above, now append separator and name
// When RTL-aligned, this produces visual: [item] on right, name on left
__DrawCharacter(pFontTexture, L' ', defaultColor);
__DrawCharacter(pFontTexture, L':', defaultColor);
__DrawCharacter(pFontTexture, L' ', defaultColor);
for (size_t i = 0; i < chatNameWide.size(); ++i)
__DrawCharacter(pFontTexture, chatNameWide[i], defaultColor);
}
} }
pFontTexture->UpdateTexture(); pFontTexture->UpdateTexture();
@@ -1090,38 +1275,7 @@ void CGraphicTextInstance::Render(RECT * pClipRect)
continue; continue;
STATEMANAGER.SetTexture(0, pTexture); STATEMANAGER.SetTexture(0, pTexture);
STATEMANAGER.DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, vtxBatch.size() - 2, vtxBatch.data(), sizeof(SVertex));
// Each character is 4 vertices forming a quad (0=TL, 1=BL, 2=TR, 3=BR)
// We need to convert quads to triangle list format
// Triangle list needs 6 vertices per quad: v0,v1,v2, v2,v1,v3
size_t numQuads = vtxBatch.size() / 4;
std::vector<SVertex> triangleVerts;
triangleVerts.reserve(numQuads * 6);
for (size_t i = 0; i < numQuads; ++i)
{
size_t baseIdx = i * 4;
const SVertex& v0 = vtxBatch[baseIdx + 0]; // TL
const SVertex& v1 = vtxBatch[baseIdx + 1]; // BL
const SVertex& v2 = vtxBatch[baseIdx + 2]; // TR
const SVertex& v3 = vtxBatch[baseIdx + 3]; // BR
// First triangle: TL, BL, TR
triangleVerts.push_back(v0);
triangleVerts.push_back(v1);
triangleVerts.push_back(v2);
// Second triangle: TR, BL, BR
triangleVerts.push_back(v2);
triangleVerts.push_back(v1);
triangleVerts.push_back(v3);
}
if (!triangleVerts.empty())
{
STATEMANAGER.DrawPrimitiveUP(D3DPT_TRIANGLELIST, triangleVerts.size() / 3, triangleVerts.data(), sizeof(SVertex));
}
} }
if (m_isCursor) if (m_isCursor)
@@ -1392,6 +1546,23 @@ void CGraphicTextInstance::SetValue(const char* c_szText, size_t len)
return; return;
m_stText = c_szText; m_stText = c_szText;
m_isChatMessage = false; // Reset chat mode
m_isUpdate = false;
}
void CGraphicTextInstance::SetChatValue(const char* c_szName, const char* c_szMessage)
{
if (!c_szName || !c_szMessage)
return;
// Store separated components
m_chatName = c_szName;
m_chatMessage = c_szMessage;
m_isChatMessage = true;
// Build combined text for rendering (will be processed by Update())
// Use BuildVisualChatMessage in Update() instead of BuildVisualBidiText_Tagless
m_stText = std::string(c_szName) + " : " + std::string(c_szMessage);
m_isUpdate = false; m_isUpdate = false;
} }
@@ -1525,6 +1696,9 @@ void CGraphicTextInstance::__Initialize()
// Only chat messages should be explicitly set to RTL // Only chat messages should be explicitly set to RTL
m_direction = ETextDirection::Auto; m_direction = ETextDirection::Auto;
m_computedRTL = false; m_computedRTL = false;
m_isChatMessage = false;
m_chatName = "";
m_chatMessage = "";
m_textWidth = 0; m_textWidth = 0;
m_textHeight = 0; m_textHeight = 0;

View File

@@ -62,6 +62,7 @@ class CGraphicTextInstance
void SetTextPointer(CGraphicText* pText); void SetTextPointer(CGraphicText* pText);
void SetValueString(const std::string& c_stValue); void SetValueString(const std::string& c_stValue);
void SetValue(const char* c_szValue, size_t len = -1); void SetValue(const char* c_szValue, size_t len = -1);
void SetChatValue(const char* c_szName, const char* c_szMessage); // Chat-specific setter with name/message separation
void SetPosition(float fx, float fy, float fz = 0.0f); void SetPosition(float fx, float fy, float fz = 0.0f);
void SetSecret(bool Value); void SetSecret(bool Value);
void SetOutline(bool Value); void SetOutline(bool Value);
@@ -131,6 +132,9 @@ class CGraphicTextInstance
bool m_isUpdate; bool m_isUpdate;
bool m_isUpdateFontTexture; bool m_isUpdateFontTexture;
bool m_computedRTL; // Result of BiDi analysis (used when m_direction == Auto) bool m_computedRTL; // Result of BiDi analysis (used when m_direction == Auto)
bool m_isChatMessage; // True if this text was set via SetChatValue (has separated name/message)
std::string m_chatName; // Chat sender name (only used when m_isChatMessage is true)
std::string m_chatMessage; // Chat message text (only used when m_isChatMessage is true)
CGraphicText::TRef m_roText; CGraphicText::TRef m_roText;
CGraphicFontTexture::TPCharacterInfomationVector m_pCharInfoVector; CGraphicFontTexture::TPCharacterInfomationVector m_pCharInfoVector;

View File

@@ -499,9 +499,27 @@ void CPythonChat::AppendChat(int iType, const char * c_szChat)
SChatLine * pChatLine = SChatLine::New(); SChatLine * pChatLine = SChatLine::New();
pChatLine->iType = iType; pChatLine->iType = iType;
// Pass chat text as-is to BiDi algorithm // Parse chat format "name : message" for proper BiDi handling
// BuildVisualBidiText_Tagless will detect chat format and handle reordering // This avoids issues with usernames containing " : "
pChatLine->Instance.SetValue(c_szChat); const char* colonPos = strstr(c_szChat, " : ");
if (colonPos && colonPos != c_szChat) // Make sure " : " is not at the start
{
// Extract name and message
size_t nameLen = colonPos - c_szChat;
const char* msgStart = colonPos + 3; // Skip " : "
// Create temporary buffers
std::string name(c_szChat, nameLen);
std::string message(msgStart);
// Use new SetChatValue API for proper BiDi handling
pChatLine->Instance.SetChatValue(name.c_str(), message.c_str());
}
else
{
// Fallback: Not in chat format (INFO, NOTICE, etc.)
pChatLine->Instance.SetValue(c_szChat);
}
if (IsRTL()) if (IsRTL())
{ {
@@ -768,9 +786,23 @@ void CWhisper::AppendChat(int iType, const char * c_szChat)
SChatLine * pChatLine = SChatLine::New(); SChatLine * pChatLine = SChatLine::New();
// Pass chat text as-is to BiDi algorithm // Parse chat format "name : message" for proper BiDi handling
// BuildVisualBidiText_Tagless will detect chat format and handle reordering const char* colonPos = strstr(c_szChat, " : ");
pChatLine->Instance.SetValue(c_szChat); if (colonPos && colonPos != c_szChat)
{
// Extract name and message
size_t nameLen = colonPos - c_szChat;
const char* msgStart = colonPos + 3;
std::string name(c_szChat, nameLen);
std::string message(msgStart);
pChatLine->Instance.SetChatValue(name.c_str(), message.c_str());
}
else
{
pChatLine->Instance.SetValue(c_szChat);
}
if (IsRTL()) if (IsRTL())
{ {