351
extern/include/utf8.h
vendored
351
extern/include/utf8.h
vendored
@@ -22,9 +22,11 @@ constexpr size_t ARABIC_SHAPING_EXPANSION_FACTOR_RETRY = 4;
|
||||
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
|
||||
#include <cstdio>
|
||||
@@ -242,8 +244,9 @@ static inline bool IsNameTokenPunct(wchar_t ch)
|
||||
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'<':
|
||||
@@ -264,16 +267,44 @@ static inline bool IsStrongLTR(wchar_t ch)
|
||||
|
||||
static inline bool HasStrongLTRNeighbor(const wchar_t* s, int n, int i)
|
||||
{
|
||||
// Remove null/size check (caller guarantees validity)
|
||||
// Early exit after first strong neighbor found
|
||||
// Skip neutral characters (spaces, punctuation) to find nearest strong character
|
||||
// This fixes mixed-direction text like "english + arabic"
|
||||
|
||||
// Check previous character
|
||||
if (i > 0 && IsStrongLTR(s[i - 1]))
|
||||
return true;
|
||||
// Search backwards for strong character (skip neutrals/whitespace)
|
||||
for (int j = i - 1; j >= 0; --j)
|
||||
{
|
||||
wchar_t ch = s[j];
|
||||
|
||||
// Check next character
|
||||
if (i + 1 < n && IsStrongLTR(s[i + 1]))
|
||||
return true;
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
@@ -302,6 +333,132 @@ static inline ECharDir GetCharDirSmart(const wchar_t* s, int n, int i)
|
||||
if (IsStrongLTR(ch))
|
||||
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
|
||||
if (IsNameTokenPunct(ch) && HasStrongLTRNeighbor(s, n, i))
|
||||
return ECharDir::LTR;
|
||||
@@ -318,10 +475,11 @@ struct TStrongDirCache
|
||||
TStrongDirCache(const wchar_t* s, int n, EBidiDir base) : nextStrong(n), baseDir(base)
|
||||
{
|
||||
// Build reverse lookup: scan from end to beginning
|
||||
// Use GetCharDirSmart for context-aware character classification
|
||||
EBidiDir lastSeen = baseDir;
|
||||
for (int i = n - 1; i >= 0; --i)
|
||||
{
|
||||
ECharDir cd = GetCharDir(s[i]);
|
||||
ECharDir cd = GetCharDirSmart(s, n, i);
|
||||
if (cd == ECharDir::LTR)
|
||||
lastSeen = EBidiDir::LTR;
|
||||
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)
|
||||
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
|
||||
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);
|
||||
}
|
||||
|
||||
#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);
|
||||
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());
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -584,6 +698,95 @@ static std::vector<wchar_t> BuildVisualBidiText_Tagless(const wchar_t* s, int n,
|
||||
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
|
||||
// ============================================================================
|
||||
|
||||
@@ -14,16 +14,33 @@ const CGrannyModel::TMeshNode* CGrannyModel::GetMeshNodeList(CGrannyMesh::EType
|
||||
|
||||
CGrannyMesh * CGrannyModel::GetMeshPointer(int iMesh)
|
||||
{
|
||||
assert(CheckMeshIndex(iMesh));
|
||||
if (!CheckMeshIndex(iMesh))
|
||||
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;
|
||||
}
|
||||
|
||||
const CGrannyMesh* CGrannyModel::GetMeshPointer(int iMesh) const
|
||||
{
|
||||
assert(CheckMeshIndex(iMesh));
|
||||
assert(m_meshs != NULL);
|
||||
if (!CheckMeshIndex(iMesh))
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -101,6 +101,10 @@ bool CGrannyModelInstance::Intersect(const D3DXMATRIX * c_pMatrix,
|
||||
|
||||
granny_matrix_4x4* pgrnMatCompositeBuffer = GrannyGetWorldPoseComposite4x4Array(m_pgrnWorldPose);
|
||||
const CGrannyMesh* c_pMesh = m_pModel->GetMeshPointer(rcurBoundBox.meshIndex);
|
||||
|
||||
if (!c_pMesh)
|
||||
continue;
|
||||
|
||||
const granny_mesh* c_pgrnMesh = c_pMesh->GetGrannyMeshPointer();
|
||||
|
||||
if (!GrannyMeshIsRigid(c_pgrnMesh))
|
||||
|
||||
@@ -177,6 +177,9 @@ void CGrannyModelInstance::UpdateWorldMatrices(const D3DXMATRIX* c_pWorldMatrix)
|
||||
|
||||
const CGrannyMesh * pMesh = m_pModel->GetMeshPointer(i);
|
||||
|
||||
if (!pMesh)
|
||||
continue;
|
||||
|
||||
// WORK
|
||||
int * boneIndices = __GetMeshBoneIndices(i);
|
||||
// END_OF_WORK
|
||||
|
||||
@@ -135,11 +135,37 @@ void CGraphicTextInstance::Update()
|
||||
|
||||
// UTF-8 -> UTF-16 conversion - reserve enough space to avoid reallocation
|
||||
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)
|
||||
{
|
||||
ResetState();
|
||||
return;
|
||||
DWORD dwError = GetLastError();
|
||||
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;
|
||||
}
|
||||
|
||||
// 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)
|
||||
// Use optimized chat message processing if SetChatValue was used
|
||||
if (m_isChatMessage && !m_chatName.empty() && !m_chatMessage.empty())
|
||||
{
|
||||
if (!hasRTL && IsRTLCodepoint(wTextPtr[i]))
|
||||
hasRTL = true;
|
||||
// Convert chat name and message to wide chars
|
||||
std::wstring wName = Utf8ToWide(m_chatName);
|
||||
std::wstring wMsg = Utf8ToWide(m_chatMessage);
|
||||
|
||||
if (!isChatMessage && i < wTextLen - 2 &&
|
||||
wTextPtr[i] == L' ' && wTextPtr[i + 1] == L':' && wTextPtr[i + 2] == L' ')
|
||||
isChatMessage = true;
|
||||
// Use BuildVisualChatMessage for proper BiDi handling
|
||||
std::vector<wchar_t> visual = BuildVisualChatMessage(
|
||||
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)
|
||||
__DrawCharacter(pFontTexture, visual[i], defaultColor);
|
||||
}
|
||||
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)
|
||||
__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
|
||||
{
|
||||
// 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)
|
||||
bool hasRTL = false;
|
||||
const wchar_t* wTextPtr = wTextBuf.data();
|
||||
@@ -302,7 +378,7 @@ void CGraphicTextInstance::Update()
|
||||
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())
|
||||
return;
|
||||
@@ -313,7 +389,71 @@ void CGraphicTextInstance::Update()
|
||||
for (const auto& vc : vis)
|
||||
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);
|
||||
|
||||
// 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)
|
||||
// ====================================================================
|
||||
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)
|
||||
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
|
||||
if (hasRTL || baseRTL)
|
||||
// Apply BiDi to segments - always process hyperlinks, optionally process other text
|
||||
// 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)
|
||||
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();
|
||||
for (size_t s = 0; s < numSegments; ++s)
|
||||
{
|
||||
if (!isHyperlink[s])
|
||||
ReorderTaggedWithBidi(segments[s], baseRTL);
|
||||
// Skip non-hyperlink segments in input fields to preserve cursor
|
||||
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.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)
|
||||
{
|
||||
logicalVis.insert(logicalVis.end(), segments[s].begin(), segments[s].end());
|
||||
@@ -631,7 +790,8 @@ void CGraphicTextInstance::Update()
|
||||
}
|
||||
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)
|
||||
{
|
||||
logicalVis.insert(logicalVis.end(), segments[s].begin(), segments[s].end());
|
||||
@@ -690,6 +850,18 @@ void CGraphicTextInstance::Update()
|
||||
curLinkRange.sx = 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)
|
||||
const size_t logicalVisRenderSize = logicalVis.size();
|
||||
for (size_t idx = 0; idx < logicalVisRenderSize; ++idx)
|
||||
@@ -733,6 +905,19 @@ void CGraphicTextInstance::Update()
|
||||
curLinkRange.text = linkTargets[(size_t)currentLink];
|
||||
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();
|
||||
@@ -1090,38 +1275,7 @@ void CGraphicTextInstance::Render(RECT * pClipRect)
|
||||
continue;
|
||||
|
||||
STATEMANAGER.SetTexture(0, pTexture);
|
||||
|
||||
// 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));
|
||||
}
|
||||
STATEMANAGER.DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, vtxBatch.size() - 2, vtxBatch.data(), sizeof(SVertex));
|
||||
}
|
||||
|
||||
if (m_isCursor)
|
||||
@@ -1392,6 +1546,23 @@ void CGraphicTextInstance::SetValue(const char* c_szText, size_t len)
|
||||
return;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1525,6 +1696,9 @@ void CGraphicTextInstance::__Initialize()
|
||||
// Only chat messages should be explicitly set to RTL
|
||||
m_direction = ETextDirection::Auto;
|
||||
m_computedRTL = false;
|
||||
m_isChatMessage = false;
|
||||
m_chatName = "";
|
||||
m_chatMessage = "";
|
||||
|
||||
m_textWidth = 0;
|
||||
m_textHeight = 0;
|
||||
|
||||
@@ -62,6 +62,7 @@ class CGraphicTextInstance
|
||||
void SetTextPointer(CGraphicText* pText);
|
||||
void SetValueString(const std::string& c_stValue);
|
||||
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 SetSecret(bool Value);
|
||||
void SetOutline(bool Value);
|
||||
@@ -131,6 +132,9 @@ class CGraphicTextInstance
|
||||
bool m_isUpdate;
|
||||
bool m_isUpdateFontTexture;
|
||||
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;
|
||||
CGraphicFontTexture::TPCharacterInfomationVector m_pCharInfoVector;
|
||||
|
||||
@@ -499,9 +499,27 @@ void CPythonChat::AppendChat(int iType, const char * c_szChat)
|
||||
SChatLine * pChatLine = SChatLine::New();
|
||||
pChatLine->iType = iType;
|
||||
|
||||
// Pass chat text as-is to BiDi algorithm
|
||||
// BuildVisualBidiText_Tagless will detect chat format and handle reordering
|
||||
pChatLine->Instance.SetValue(c_szChat);
|
||||
// Parse chat format "name : message" for proper BiDi handling
|
||||
// This avoids issues with usernames containing " : "
|
||||
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())
|
||||
{
|
||||
@@ -768,9 +786,23 @@ void CWhisper::AppendChat(int iType, const char * c_szChat)
|
||||
|
||||
SChatLine * pChatLine = SChatLine::New();
|
||||
|
||||
// Pass chat text as-is to BiDi algorithm
|
||||
// BuildVisualBidiText_Tagless will detect chat format and handle reordering
|
||||
pChatLine->Instance.SetValue(c_szChat);
|
||||
// Parse chat format "name : message" for proper BiDi handling
|
||||
const char* colonPos = strstr(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())
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user